Skip to content

commit-generated-on-merge writes azuredeploy.json to repo root, not the sample folder (path-stripping in upload-artifact@v4) #14754

@alex-frankel

Description

@alex-frankel

Summary

The commit-generated-on-merge job in ValidateSampleDeployments.yml will commit the generated azuredeploy.json to the repo root instead of the sample's folder. This happens because actions/upload-artifact@v4 strips the common path prefix when only one of the listed paths actually exists, producing a flat-rooted zip.

Found by inspecting the artifact uploaded by PR #14752 (the canary for the new pipeline). The canary has not been merged yet because this bug would corrupt master.

Repro

PR #14752, /validate run 25772629137 succeeded with all checks green. The artifact generated-azuredeploy-14752-9ac735dee3bdfc1e7b4f75741b7aba94e7b94aa8 was uploaded successfully. Its zip contents:

$ unzip -l artifact.zip
  Length    Name
   3989    azuredeploy.json

The file is at the zip root, not under quickstarts/microsoft.keyvault/key-vault-create/.

Why it happens

upload-artifact@v4 strips the common path prefix:

- name: Upload generated azuredeploy.json artifact
  uses: actions/upload-artifact@v4
  with:
    name: generated-azuredeploy-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.head_sha }}
    path: |
      ${{ steps.find-metadata.outputs.sample_path }}/azuredeploy.json
      ${{ steps.find-metadata.outputs.sample_path }}/prereqs/azuredeploy.json
      ${{ steps.find-metadata.outputs.sample_path }}/prereqs/prereq.azuredeploy.json

When the sample has no prereqs/ (as with quickstarts/microsoft.keyvault/key-vault-create), only the first path matches → upload-artifact treats quickstarts/microsoft.keyvault/key-vault-create/ as the common ancestor → strips that prefix → zip contains just azuredeploy.json.

The commit-generated-on-merge job then:

- name: Download generated azuredeploy.json
  uses: actions/download-artifact@v4
  with:
    name: ${{ steps.locate-artifact.outputs.name }}
    path: .
    ...

- name: Commit and push generated azuredeploy.json
  run: |
    git add -A -- '**/azuredeploy.json' '**/prereq.azuredeploy.json' || true
    git commit -m "Auto-add generated azuredeploy.json from PR #${PR_NUM}"
    git push origin "HEAD:${DEFAULT_BRANCH}"

Extracts to ../azuredeploy.json at repo root. git add -A -- '**/azuredeploy.json' happily stages it from the root. Pushed to master.

Net effect: master would get a phantom /azuredeploy.json at the repo root, not the file in the correct sample folder. The sample stays broken.

Suggested fixes

  1. Pass include-hidden-files: false won't help — that's about dotfiles, not path-stripping.
  2. Use include-hidden-files is the wrong knob. The actual upload-artifact@v4 control for path preservation is path shape. Listing the sample's root folder and filtering excludes the prefix-stripping behavior.

The simplest fix is to upload with the full repo-relative path always preserved. One reliable pattern:

- name: Upload generated azuredeploy.json artifact
  uses: actions/upload-artifact@v4
  with:
    name: generated-azuredeploy-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.head_sha }}
    path: |
      ${{ steps.find-metadata.outputs.sample_path }}/azuredeploy.json
      ${{ steps.find-metadata.outputs.sample_path }}/prereqs/azuredeploy.json
      ${{ steps.find-metadata.outputs.sample_path }}/prereqs/prereq.azuredeploy.json
    if-no-files-found: ignore
    include-hidden-files: false
    # Force preservation of full path structure:
    overwrite: true

Actually, upload-artifact@v4 does not provide a "preserve full path" flag. The fix that works: write the file to a known-safe relative location (e.g., create a generated/ dir, copy with full path preserved, upload generated/**), and adjust the download path accordingly. Pseudocode:

- name: Stage artifact tree
  run: |
    mkdir -p "$RUNNER_TEMP/artifact-root"
    cp --parents "${{ steps.find-metadata.outputs.sample_path }}/azuredeploy.json" "$RUNNER_TEMP/artifact-root/"
    # ... same for prereqs files if they exist

- uses: actions/upload-artifact@v4
  with:
    name: generated-azuredeploy-${{ steps.pr.outputs.number }}-${{ steps.pr.outputs.head_sha }}
    path: ${{ runner.temp }}/artifact-root/

Then on download, extract to . and the full quickstarts/.../azuredeploy.json structure is preserved.

Alternatively, encode the sample path in the artifact name and reconstruct it in the download job (less elegant).

Severity

This is a silent corruption bug: every PR that successfully passes /validate and gets merged today commits azuredeploy.json to the wrong location in master. The sample stays broken (no JSON in the sample folder), and master accumulates phantom root-level azuredeploy.json files.

I don't think any PR has actually hit this in the wild yet because the only PR I'm aware of that exercised the new flow end-to-end is #14752 (this canary), and I noticed the issue before merging. But anyone who runs /validate on a bicep-only PR going forward will trigger this.

Recommendation

Hold all bicep-only PRs that would rely on commit-generated-on-merge until this is fixed. Contributors should explicitly commit azuredeploy.json in their PR for now (per the older 1-CONTRIBUTION-GUIDE/README.md guidance, which is also stale per [a separate doc-drift issue I'm filing].

cc @ouldsid

Related: #14753 (CI bicep version mismatch causing templateHash failures)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
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