diff --git a/.github/workflows/pr-review-panel.lock.yml b/.github/workflows/pr-review-panel.lock.yml index 179f774fe..fdebc61e5 100644 --- a/.github/workflows/pr-review-panel.lock.yml +++ b/.github/workflows/pr-review-panel.lock.yml @@ -405,6 +405,11 @@ jobs: merge-multiple: false path: /tmp/gh-aw/apm-bundles pattern: ${{ needs.activation.outputs.artifact_prefix }}apm-* + - env: + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} + EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} + name: Normalise bundle layout (single-artifact flatten workaround) + run: "set -euo pipefail\n# actions/download-artifact (>=v5) flattens contents directly into `path/`\n# whenever exactly one artifact matches the pattern, ignoring\n# `merge-multiple: false`. Re-shape into the per-group subdir layout so\n# downstream validation sees a stable structure regardless of matrix size.\n# Upstream reference:\n# https://github.com/actions/download-artifact/blob/v8.0.1/src/download-artifact.ts\n# (see the `isSingleArtifactDownload || mergeMultiple || artifacts.length === 1`\n# branch). Remove this step once download-artifact stops flattening or\n# exposes an opt-out.\nexpected_count=$(echo \"$EXPECTED_MATRIX\" | jq '.group // [] | length')\nif [ \"$expected_count\" -eq 1 ]; then\n group_id=$(echo \"$EXPECTED_MATRIX\" | jq -r '.group[0].id')\n # Defence-in-depth: group_id is interpolated into a shell path. apm-prep\n # produces a sanitised id today, but enforce a strict allowlist here so\n # any future schema drift cannot smuggle traversal sequences.\n if ! printf '%s' \"$group_id\" | grep -Eq '^[A-Za-z0-9_-]+$'; then\n echo \"::error::unsafe group_id '$group_id' (must match ^[A-Za-z0-9_-]+$)\"\n exit 1\n fi\n group_dir=\"/tmp/gh-aw/apm-bundles/${ARTIFACT_PREFIX}apm-${group_id}\"\n if [ ! -d \"$group_dir\" ]; then\n mkdir -p \"$group_dir\"\n find /tmp/gh-aw/apm-bundles -mindepth 1 -maxdepth 1 ! -path \"$group_dir\" -exec mv {} \"$group_dir/\" \\;\n fi\nfi\n" - env: ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} diff --git a/.github/workflows/shared/apm.md b/.github/workflows/shared/apm.md index 2e91fa8e1..e1cc90644 100644 --- a/.github/workflows/shared/apm.md +++ b/.github/workflows/shared/apm.md @@ -279,6 +279,37 @@ steps: pattern: ${{ needs.activation.outputs.artifact_prefix }}apm-* path: /tmp/gh-aw/apm-bundles merge-multiple: false + - name: Normalise bundle layout (single-artifact flatten workaround) + env: + EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} + run: | + set -euo pipefail + # actions/download-artifact (>=v5) flattens contents directly into `path/` + # whenever exactly one artifact matches the pattern, ignoring + # `merge-multiple: false`. Re-shape into the per-group subdir layout so + # downstream validation sees a stable structure regardless of matrix size. + # Upstream reference: + # https://github.com/actions/download-artifact/blob/v8.0.1/src/download-artifact.ts + # (see the `isSingleArtifactDownload || mergeMultiple || artifacts.length === 1` + # branch). Remove this step once download-artifact stops flattening or + # exposes an opt-out. + expected_count=$(echo "$EXPECTED_MATRIX" | jq '.group // [] | length') + if [ "$expected_count" -eq 1 ]; then + group_id=$(echo "$EXPECTED_MATRIX" | jq -r '.group[0].id') + # Defence-in-depth: group_id is interpolated into a shell path. apm-prep + # produces a sanitised id today, but enforce a strict allowlist here so + # any future schema drift cannot smuggle traversal sequences. + if ! printf '%s' "$group_id" | grep -Eq '^[A-Za-z0-9_-]+$'; then + echo "::error::unsafe group_id '$group_id' (must match ^[A-Za-z0-9_-]+$)" + exit 1 + fi + group_dir="/tmp/gh-aw/apm-bundles/${ARTIFACT_PREFIX}apm-${group_id}" + if [ ! -d "$group_dir" ]; then + mkdir -p "$group_dir" + find /tmp/gh-aw/apm-bundles -mindepth 1 -maxdepth 1 ! -path "$group_dir" -exec mv {} "$group_dir/" \; + fi + fi - name: Validate downloaded bundles match matrix manifest env: EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} diff --git a/.github/workflows/triage-panel.lock.yml b/.github/workflows/triage-panel.lock.yml index ced41708f..4945684b7 100644 --- a/.github/workflows/triage-panel.lock.yml +++ b/.github/workflows/triage-panel.lock.yml @@ -417,6 +417,11 @@ jobs: merge-multiple: false path: /tmp/gh-aw/apm-bundles pattern: ${{ needs.activation.outputs.artifact_prefix }}apm-* + - env: + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} + EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} + name: Normalise bundle layout (single-artifact flatten workaround) + run: "set -euo pipefail\n# actions/download-artifact (>=v5) flattens contents directly into `path/`\n# whenever exactly one artifact matches the pattern, ignoring\n# `merge-multiple: false`. Re-shape into the per-group subdir layout so\n# downstream validation sees a stable structure regardless of matrix size.\n# Upstream reference:\n# https://github.com/actions/download-artifact/blob/v8.0.1/src/download-artifact.ts\n# (see the `isSingleArtifactDownload || mergeMultiple || artifacts.length === 1`\n# branch). Remove this step once download-artifact stops flattening or\n# exposes an opt-out.\nexpected_count=$(echo \"$EXPECTED_MATRIX\" | jq '.group // [] | length')\nif [ \"$expected_count\" -eq 1 ]; then\n group_id=$(echo \"$EXPECTED_MATRIX\" | jq -r '.group[0].id')\n # Defence-in-depth: group_id is interpolated into a shell path. apm-prep\n # produces a sanitised id today, but enforce a strict allowlist here so\n # any future schema drift cannot smuggle traversal sequences.\n if ! printf '%s' \"$group_id\" | grep -Eq '^[A-Za-z0-9_-]+$'; then\n echo \"::error::unsafe group_id '$group_id' (must match ^[A-Za-z0-9_-]+$)\"\n exit 1\n fi\n group_dir=\"/tmp/gh-aw/apm-bundles/${ARTIFACT_PREFIX}apm-${group_id}\"\n if [ ! -d \"$group_dir\" ]; then\n mkdir -p \"$group_dir\"\n find /tmp/gh-aw/apm-bundles -mindepth 1 -maxdepth 1 ! -path \"$group_dir\" -exec mv {} \"$group_dir/\" \\;\n fi\nfi\n" - env: ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b11d843..0c7dc47d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `shared/apm.md` gh-aw workflow no longer fails the "Validate downloaded bundles match matrix manifest" step with a spurious `missing APM bundles (group did not pack successfully): apm-default` error when the matrix has exactly one credential group. `actions/download-artifact@v5+` flattens contents directly into the destination path whenever a single artifact matches the pattern (overriding `merge-multiple: false`), which collapsed the per-group subdir layout the validator expects. A new "Normalise bundle layout" step re-creates the expected `apm-/` directory in the single-group case before validation runs. (#1051) - `apm install` and `apm compile` no longer exit 0 with success messages when `target:` in `apm.yml` is a CSV string -- the value now parses identically to the same input on `--target`, and zero-target resolution surfaces a warning instead of a silent no-op. (#820) - Remove redundant `seen` set from `_scan_patterns()` discovery walk (#918) - `apm pack` (marketplace producer) now respects `GITHUB_HOST` for GitHub Enterprise repos -- ref resolution, token lookup, and metadata fetch all use the configured host instead of hardcoded `github.com`. `git ls-remote` is authenticated so private repos work without separate credential setup. (#1008)