Skip to content

extraFilesDirectories breaks re-staging: running deploy + deployCron in one build fails with FileAlreadyExistsException #1076

Description

@martiner

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 copyFileAndReplaceFiles.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions