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/<extra-file>
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:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>appengine-maven-plugin</artifactId>
<version>2.8.7</version>
<configuration>
<projectId>my-project</projectId>
<version>1</version>
<extraFilesDirectories>${project.basedir}/src/main/appengine-extra</extraFilesDirectories>
</configuration>
</plugin>
with any file in src/main/appengine-extra/ (e.g. foo.properties), then:
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:
<executions>
<execution>
<id>cron</id>
<phase>none</phase>
<goals><goal>deployCron</goal></goals>
<configuration>
<stagingDirectory>${project.build.directory}/appengine-staging-cron</stagingDirectory>
</configuration>
</execution>
</executions>
and invoke it as appengine:deployCron@cron.
Environment
appengine-maven-plugin2.8.7 (bundlesappengine-plugins-core)What happens
When
extraFilesDirectoriesis configured and a single Maven invocation runs two goals that both stage the app — e.g.appengine:deployfollowed byappengine:deployCron— the second staging fails:Both goals stage into the same directory (
target/appengine-stagingby default). The first staging populates it; the second one aborts because the extra file already exists.Minimal reproduction
pom.xml:with any file in
src/main/appengine-extra/(e.g.foo.properties), then:deploysucceeds,deployCronfails withFileAlreadyExistsExceptionon the extra file.Root cause
In
AppYamlProjectStaging, staging steps are inconsistent about overwriting:copyArtifactandcopyAppEngineContext(app.yaml) usecopyFileAndReplace→Files.copy(src, dest, REPLACE_EXISTING), so they are idempotent across re-stages.copyExtraFilesusescopyService.copyDirectory(...)→FileUtil.copyDirectory(...), which copies withoutREPLACE_EXISTINGand therefore throwsFileAlreadyExistsExceptionwhen 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 — otherwiseextraFilesDirectoriesis unusable whenever two staging goals run in one build (a very commondeploy+deployCron/deployQueueetc. pattern).Suggested fix
Make
copyExtraFiles(i.e.FileUtil.copyDirectory, or the extra-files copy path specifically) overwrite existing files, consistent withcopyFileAndReplace.Workaround
Give each staging goal its own staging directory so their extra-file copies never collide, e.g. a dedicated execution for
deployCron:and invoke it as
appengine:deployCron@cron.