GoogleCloudPlatform / GoogleCloudPlatform/appengine-plugins
`extraFilesDirectories` breaks re-staging: running `deploy` + `deployCron` in one build fails with `FileAlreadyExistsException`
- Dominant language
- Java
- Stars
- 47
- Forks
- 27
- PR merge metrics
- No merged PRs in 30d
Description
## Environment
- `appengine-maven-plugin` 2.8.7 (bundles `appengine-plugins-core`)
- app.yaml-based project, App Engine standard, Java 17
- One Maven invocation that both deploys the app and the cron config
## What happens
When `extraFilesDirectories` is configured and a single Maven invocation runs two goals that both stage the app — e.g. `appengine:deploy` followed by `appengine:deployCron` — the **second** staging fails:
```
Failed to execute goal com.google.cloud.tools:appengine-maven-plugin:2.8.7:deployCron ... :
com.google.cloud.tools.appengine.AppEngineException:
java.nio.file.FileAlreadyExistsException: .../target/appengine-staging/
```
Both goals stage into the same directory (`target/appengine-staging` by default). The first staging populates it; the second one aborts because the extra file already exists.
## Minimal reproduction
`pom.xml`:
```xml
com.google.cloud.tools
appengine-maven-plugin
2.8.7
my-project
1
${project.basedir}/src/main/appengine-extra
```
with any file in `src/main/appengine-extra/` (e.g. `foo.properties`), then:
```shell
mvn -B package appengine:deploy appengine:deployCron
```
`deploy` succeeds, `deployCron` fails with `FileAlreadyExistsException` on the extra file.
## Root cause
In `AppYamlProjectStaging`, staging steps are inconsistent about overwriting:
- `copyArtifact` and `copyAppEngineContext` (app.yaml) use `copyFileAndReplace` → `Files.copy(src, dest, REPLACE_EXISTING)`, so they are idempotent across re-stages.
- `copyExtraFiles` uses `copyService.copyDirectory(...)` → `FileUtil.copyDirectory(...)`, which copies **without** `REPLACE_EXISTING` and therefore throws `FileAlreadyExistsException` when a destination file already exists.
So the artifact and app.yaml survive a second staging into the same directory, but extra files do not. This still holds on `main`.
## Expected behavior
Staging should be idempotent for extra files just like it already is for the artifact and `app.yaml`. Re-staging into a populated staging directory should overwrite, not fail — otherwise `extraFilesDirectories` is unusable whenever two staging goals run in one build (a very common `deploy` + `deployCron` / `deployQueue` etc. pattern).
## Suggested fix
Make `copyExtraFiles` (i.e. `FileUtil.copyDirectory`, or the extra-files copy path specifically) overwrite existing files, consistent with `copyFileAndReplace`.
## Workaround
Give each staging goal its own staging directory so their extra-file copies never collide, e.g. a dedicated execution for `deployCron`:
```xml
cron
none
deployCron
${project.build.directory}/appengine-staging-cron
```
and invoke it as `appengine:deployCron@cron`.
Contributor guide
Research direction
Start in AppYamlProjectStaging at copyExtraFiles and compare its copyService.copyDirectory path with copyArtifact and copyAppEngineContext, which use copyFileAndReplace. Trace FileUtil.copyDirectory and the existing staging-related tests, then verify that running both staging goals no longer fails when the destination already contains an extra file.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100