From 8ef4e267bf4813626c4142230095e916213ea887 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Wed, 29 Apr 2026 20:17:28 +0200 Subject: [PATCH 1/2] fix(shared/apm): re-shape single-artifact download to expected layout actions/download-artifact (>=v5) silently overrides 'merge-multiple: false' and flattens contents directly into 'path/' whenever exactly one artifact matches the pattern. Our 'Validate downloaded bundles match matrix manifest' step assumed the per-artifact subdir always exists, so any single-group consumer (the common case: one credential group, one packed bundle) hits 'missing APM bundles (group did not pack successfully): apm-' even though the bundle downloaded and verified successfully. This was latent until #1033 unblocked apm-prep -- previously apm-prep itself was failing on the malformed jq input, so the agent job never got far enough to run validation. With prep fixed, every single-group PR run of pr-review-panel and triage-panel now fails at the validation step. Fix: add a normalisation step that, when matrix has exactly one group, re-creates the expected '${prefix}apm-/' subdir and moves the flattened bundle file(s) into it. No-op for multi-group matrices where download-artifact already produces per-artifact subdirs. Validation script is unchanged. Refs: actions/download-artifact src/download-artifact.ts -- the 'isSingleArtifactDownload || mergeMultiple || artifacts.length === 1' check that triggers the flatten. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/aw/actions-lock.json | 5 ----- .github/workflows/pr-review-panel.lock.yml | 5 +++++ .github/workflows/shared/apm.md | 19 +++++++++++++++++++ .github/workflows/triage-panel.lock.yml | 5 +++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index af13fe770..6834af10b 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -25,11 +25,6 @@ "version": "v7", "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup-cli@v0.71.2": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.71.2", - "sha": "ab8940d1df237fc4fae4ab8091e7ba66ada55a55" - }, "github/gh-aw-actions/setup@v0.71.2": { "repo": "github/gh-aw-actions/setup", "version": "v0.71.2", diff --git a/.github/workflows/pr-review-panel.lock.yml b/.github/workflows/pr-review-panel.lock.yml index 179f774fe..131441b70 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.\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 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..3c72ffffd 100644 --- a/.github/workflows/shared/apm.md +++ b/.github/workflows/shared/apm.md @@ -279,6 +279,25 @@ 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. + expected_count=$(echo "$EXPECTED_MATRIX" | jq '.group | length') + if [ "$expected_count" -eq 1 ]; then + group_id=$(echo "$EXPECTED_MATRIX" | jq -r '.group[0].id') + 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..0478f7ef6 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.\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 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 }} From f231989c589b35f1bd1f3ed3d42ced6eb2a2758e Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Wed, 29 Apr 2026 21:23:16 +0200 Subject: [PATCH 2/2] address panel review: restore setup-cli lock entry, harden group_id, changelog - Restore github/gh-aw-actions/setup-cli@v0.71.2 entry in actions-lock.json (still referenced by .github/workflows/agentics-maintenance.yml; the prior recompile dropped it because pr-review-panel and triage-panel don't use it). - Sanitise group_id with strict allowlist (^[A-Za-z0-9_-]+$) before interpolating into the shell path, as defence-in-depth even though apm-prep produces a sanitised id today. - Guard jq '.group | length' against null with '.group // []'. - Link upstream actions/download-artifact source in the comment so future maintainers can verify the workaround is still needed. - CHANGELOG entry under [Unreleased] -> Fixed referencing #1051. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/aw/actions-lock.json | 5 +++++ .github/workflows/pr-review-panel.lock.yml | 2 +- .github/workflows/shared/apm.md | 14 +++++++++++++- .github/workflows/triage-panel.lock.yml | 2 +- CHANGELOG.md | 1 + 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 6834af10b..af13fe770 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -25,6 +25,11 @@ "version": "v7", "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, + "github/gh-aw-actions/setup-cli@v0.71.2": { + "repo": "github/gh-aw-actions/setup-cli", + "version": "v0.71.2", + "sha": "ab8940d1df237fc4fae4ab8091e7ba66ada55a55" + }, "github/gh-aw-actions/setup@v0.71.2": { "repo": "github/gh-aw-actions/setup", "version": "v0.71.2", diff --git a/.github/workflows/pr-review-panel.lock.yml b/.github/workflows/pr-review-panel.lock.yml index 131441b70..fdebc61e5 100644 --- a/.github/workflows/pr-review-panel.lock.yml +++ b/.github/workflows/pr-review-panel.lock.yml @@ -409,7 +409,7 @@ jobs: 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.\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 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" + 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 3c72ffffd..e1cc90644 100644 --- a/.github/workflows/shared/apm.md +++ b/.github/workflows/shared/apm.md @@ -289,9 +289,21 @@ steps: # 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. - expected_count=$(echo "$EXPECTED_MATRIX" | jq '.group | length') + # 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" diff --git a/.github/workflows/triage-panel.lock.yml b/.github/workflows/triage-panel.lock.yml index 0478f7ef6..4945684b7 100644 --- a/.github/workflows/triage-panel.lock.yml +++ b/.github/workflows/triage-panel.lock.yml @@ -421,7 +421,7 @@ jobs: 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.\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 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" + 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)