diff --git a/.changeset/patch-align-shared-apm.md b/.changeset/patch-align-shared-apm.md new file mode 100644 index 00000000000..98ecd40e08e --- /dev/null +++ b/.changeset/patch-align-shared-apm.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Updated the shared APM workflow to use `microsoft/apm-action` v1.5.0 with multi-bundle restore and optional GitHub App credential groups. diff --git a/.github/workflows/shared/apm.md b/.github/workflows/shared/apm.md index cd875eb3360..7e1f2dcf40b 100644 --- a/.github/workflows/shared/apm.md +++ b/.github/workflows/shared/apm.md @@ -2,162 +2,342 @@ # APM (Agent Package Manager) - Shared Workflow # Install Microsoft APM packages in your agentic workflow. # -# This shared workflow creates a dedicated "apm" job (depending on activation) that -# packs packages using microsoft/apm-action, caches the bundle for cross-run reuse, -# and uploads it as an artifact for reliable same-run access. -# The agent job restores from cache (preferred) or downloads the artifact as a fallback. +# This shared workflow normalises packages, single-app inputs, and apps[] (multi-org +# GitHub App credential groups) into one canonical list of credential groups in an +# "apm-prep" job, then fans the "apm" job out one matrix replica per group. Each +# replica mints its own installation token (when an app-id is set), packs only its +# declared packages with microsoft/apm-action, and uploads a uniquely-named artifact. +# Pre-agent-steps then download all bundles and restore them in one apm-action call. # -# Documentation: https://github.com/microsoft/APM +# Source of truth: https://github.com/microsoft/apm/blob/main/.github/workflows/shared/apm.md +# apm-action pin: microsoft/apm-action@v1.5.0 +# To check whether a vendored copy is current, compare these two lines. # -# Usage: -# imports: -# - uses: shared/apm.md -# with: -# packages: -# - microsoft/apm-sample-package -# - github/awesome-copilot/skills/review-and-refactor +# Documentation: https://microsoft.github.io/apm/integrations/gh-aw/ +# +# Three user-facing forms (all valid, additive): +# +# 1. Public + default-token packages (no App credentials): +# +# imports: +# - uses: shared/apm.md +# with: +# packages: +# - microsoft/apm-sample-package +# - github/awesome-copilot/skills/review-and-refactor +# +# 2. Single GitHub App (one org) -- canonical shorthand: +# +# imports: +# - uses: shared/apm.md +# with: +# app-id: ${{ vars.APP_ID }} +# private-key: ${{ secrets.APP_PRIVATE_KEY }} +# owner: my-org +# packages: +# - my-org/my-private-skills +# +# 3. Multiple GitHub Apps (cross-org): +# +# imports: +# - uses: shared/apm.md +# with: +# packages: +# - microsoft/apm-sample-package +# apps: +# - id: acme +# app-id: ${{ vars.ACME_APP_ID }} +# private-key: ${{ secrets.ACME_KEY }} +# owner: acme-org +# packages: +# - acme-org/acme-skills/skills/code-review +# - app-id: ${{ vars.BETA_APP_ID }} +# private-key: ${{ secrets.BETA_KEY }} +# owner: beta-org +# packages: +# - beta-org/beta-pkg import-schema: packages: type: array items: type: string - required: true + required: false description: > - List of APM package references to install. + Public APM packages or packages reachable via the default token cascade + (GH_AW_PLUGINS_TOKEN, GH_AW_GITHUB_TOKEN, GITHUB_TOKEN). Optional. At + least one of `packages`, the single-app inputs, or `apps` must be provided. Format: owner/repo or owner/repo/path/to/skill. - Examples: microsoft/apm-sample-package, github/awesome-copilot/skills/review-and-refactor + + # Single-app convenience form (canonical shorthand for one-org users) + app-id: + type: string + required: false + description: > + GitHub App ID. With `private-key`, mints an installation token for the + packages listed in `packages:`. For multiple orgs, use `apps:` instead. + private-key: + type: string + required: false + description: > + PEM private key matching `app-id`. Required when `app-id` is set. Pass via + a repository or organization secret. + owner: + type: string + required: false + description: > + App installation owner. Defaults to the current repository owner when + omitted. Only used when `app-id` is set. + repositories: + type: string + required: false + description: > + Repositories the minted token is scoped to. Comma- or newline-separated. + Empty defaults to the calling repo or the App installation default scope. + Note: literal "*" is NOT a wildcard for actions/create-github-app-token; + leave empty for org-wide access via App installation config. + + # Multi-app form (cross-org) + apps: + type: array + required: false + description: > + List of GitHub App credential groups. Each entry mints its own + installation token and packs its own packages. Use when packages span + multiple orgs requiring different App installations. + items: + type: object + properties: + id: + type: string + required: false + description: > + Stable identifier used for matrix-row and artifact naming. + Auto-derived from `owner` (slugified) when omitted. Required when + two entries share the same owner. + app-id: + type: string + required: true + private-key: + type: string + required: true + owner: + type: string + required: false + repositories: + type: string + required: false + packages: + type: array + items: + type: string + required: true jobs: - apm: + apm-prep: runs-on: ubuntu-slim needs: [activation] - permissions: - contents: read + permissions: {} + outputs: + matrix: ${{ steps.compute.outputs.matrix }} steps: - - name: Checkout workflow lock files - uses: actions/checkout@v6.0.2 - with: - sparse-checkout: | - .github/workflows - sparse-checkout-cone-mode: false - persist-credentials: false - - name: Restore APM bundle from cache - id: apm_cache - uses: actions/cache/restore@v5.0.5 - with: - path: /tmp/gh-aw/apm-workspace - key: apm-${{ needs.activation.outputs.engine_id }}-${{ hashFiles('.github/workflows/*.lock.yml') }} - - name: Prepare APM package list - id: apm_prep - if: steps.apm_cache.outputs.cache-hit != 'true' + # SECURITY (S3): never echo $groups, $matrix, or any matrix.group.* value + # in any apm-prep step. private-key is a real secret string here. + - name: Compute APM credential-group matrix + id: compute env: AW_APM_PACKAGES: '${{ github.aw.import-inputs.packages }}' + AW_APM_APPS: '${{ github.aw.import-inputs.apps }}' + AW_APM_LEGACY_APP_ID: ${{ github.aw.import-inputs.app-id }} + AW_APM_LEGACY_PRIVATE_KEY: ${{ github.aw.import-inputs.private-key }} + AW_APM_LEGACY_OWNER: ${{ github.aw.import-inputs.owner }} + AW_APM_LEGACY_REPOS: ${{ github.aw.import-inputs.repositories }} run: | - DEPS=$(echo "$AW_APM_PACKAGES" | jq -r '.[] | "- " + .') + set -euo pipefail + packages_json=${AW_APM_PACKAGES:-null} + apps_json=${AW_APM_APPS:-null} + legacy_id=${AW_APM_LEGACY_APP_ID:-} + + groups=$(jq -nc \ + --argjson packages "$packages_json" \ + --argjson apps "$apps_json" \ + --arg legacy_id "$legacy_id" \ + --arg legacy_pk "${AW_APM_LEGACY_PRIVATE_KEY:-}" \ + --arg legacy_owner "${AW_APM_LEGACY_OWNER:-}" \ + --arg legacy_repos "${AW_APM_LEGACY_REPOS:-}" \ + 'def slug(s): s | gsub("[^a-zA-Z0-9-]"; "-") | ascii_downcase | .[0:32]; + def with_id(g): + g + (if (g.id // "") == "" then {id: ("auto-" + slug(g.owner // "default"))} else {} end); + [ + (if (($packages // []) | length) > 0 and $legacy_id == "" + then [{id:"default",("app-id"):"",("private-key"):"",owner:"",repositories:"",packages:$packages}] + else [] end), + (if $legacy_id != "" + then [with_id({id:"legacy",("app-id"):$legacy_id,("private-key"):$legacy_pk,owner:$legacy_owner,repositories:$legacy_repos,packages:($packages // [])})] + else [] end), + (($apps // []) | map(with_id(.))) + ] | add // []') + + count=$(echo "$groups" | jq 'length') + if [ "$count" = "0" ]; then + echo "::error::shared/apm.md import provided no packages. Add packages: , single-app inputs (app-id + private-key), or apps: in the with: block." + exit 1 + fi + + dups=$(echo "$groups" | jq -r '[.[].id] | group_by(.) | map(select(length > 1) | first) | join(", ")') + if [ -n "$dups" ]; then + echo "::error::duplicate apm group ids after auto-derivation: $dups. Set apps[].id explicitly when two entries share the same owner." + exit 1 + fi + + while IFS= read -r id; do + if ! echo "$id" | grep -Eq '^[a-z0-9-]{1,32}$'; then + echo "::error::invalid apm group id: '$id' (lowercase alphanumeric and dashes, 1-32 chars). Set apps[].id explicitly." + exit 1 + fi + done < <(echo "$groups" | jq -r '.[].id') + + # SAFE: emit only id + package-count to logs. Never $groups in full. + { + echo "matrix={\"group\":$groups}" + } >> "$GITHUB_OUTPUT" + printf "::notice::APM matrix: %d credential group(s)\n" "$count" + echo "$groups" | jq -r '.[] | " - " + .id + " (" + (.packages | length | tostring) + " package(s))"' + + apm: + runs-on: ubuntu-slim + needs: [activation, apm-prep] + permissions: {} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.apm-prep.outputs.matrix) }} + steps: + - name: Mint installation token + id: token + if: ${{ matrix.group.app-id != '' }} + uses: actions/create-github-app-token@v3.1.1 + with: + app-id: ${{ matrix.group.app-id }} + private-key: ${{ matrix.group.private-key }} + owner: ${{ matrix.group.owner != '' && matrix.group.owner || github.repository_owner }} + repositories: ${{ matrix.group.repositories }} + - name: Render package list + id: list + env: + AW_PKG: ${{ toJSON(matrix.group.packages) }} + run: | + DEPS=$(echo "$AW_PKG" | jq -r '.[] | "- " + .') { echo "deps<> "$GITHUB_OUTPUT" - name: Pack APM packages - id: apm_pack - if: steps.apm_cache.outputs.cache-hit != 'true' - uses: microsoft/apm-action@v1.4.2 + id: pack + uses: microsoft/apm-action@v1.5.0 env: - GITHUB_TOKEN: ${{ secrets.GH_AW_PLUGINS_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.token.outputs.token || secrets.GH_AW_PLUGINS_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: - dependencies: ${{ steps.apm_prep.outputs.deps }} + dependencies: ${{ steps.list.outputs.deps }} isolated: 'true' pack: 'true' archive: 'true' target: all working-directory: /tmp/gh-aw/apm-workspace - - name: Save APM bundle to cache - if: steps.apm_cache.outputs.cache-hit != 'true' && success() - uses: actions/cache/save@v5.0.5 - with: - path: /tmp/gh-aw/apm-workspace - key: ${{ steps.apm_cache.outputs.cache-primary-key }} - - name: Find APM bundle path - id: apm_bundle_path + - name: Rename bundle to group-scoped filename + id: rename-bundle + env: + BUNDLE_SRC: ${{ steps.pack.outputs.bundle-path }} + GROUP_ID: ${{ matrix.group.id }} + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} run: | - bundle=$(find /tmp/gh-aw/apm-workspace -name '*.tar.gz' | head -1) - if [ -z "$bundle" ]; then - echo "::error::APM bundle not found in /tmp/gh-aw/apm-workspace" - exit 1 - fi - echo "path=$bundle" >> "$GITHUB_OUTPUT" + dst="$(dirname "$BUNDLE_SRC")/${ARTIFACT_PREFIX}apm-${GROUP_ID}.tar.gz" + mv "$BUNDLE_SRC" "$dst" + echo "bundle-path=$dst" >> "$GITHUB_OUTPUT" - name: Upload APM bundle artifact if: success() uses: actions/upload-artifact@v7.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}apm - path: ${{ steps.apm_bundle_path.outputs.path }} + name: ${{ needs.activation.outputs.artifact_prefix }}apm-${{ matrix.group.id }} + path: ${{ steps.rename-bundle.outputs.bundle-path }} retention-days: '1' pre-agent-steps: - - name: Restore APM bundle from cache - id: apm_cache_restore - uses: actions/cache/restore@v5.0.5 - with: - path: /tmp/gh-aw/apm-workspace - key: apm-${{ needs.activation.outputs.engine_id }}-${{ hashFiles('.github/workflows/*.lock.yml') }} - - name: Download APM bundle artifact - if: steps.apm_cache_restore.outputs.cache-hit != 'true' + - name: Download APM bundle artifacts (all groups) uses: actions/download-artifact@v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}apm - path: /tmp/gh-aw/apm-bundle - - name: Find APM bundle path - id: apm_bundle + pattern: ${{ needs.activation.outputs.artifact_prefix }}apm-* + path: /tmp/gh-aw/apm-bundles + merge-multiple: false + - name: Validate downloaded bundles match matrix manifest + env: + EXPECTED_MATRIX: ${{ needs.apm-prep.outputs.matrix }} + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} run: | - bundle=$(find /tmp/gh-aw/apm-workspace /tmp/gh-aw/apm-bundle -name '*.tar.gz' 2>/dev/null | head -1) - if [ -z "$bundle" ]; then - echo "::error::APM bundle not found in /tmp/gh-aw/apm-workspace or /tmp/gh-aw/apm-bundle" + set -euo pipefail + mapfile -t expected_names < <(echo "$EXPECTED_MATRIX" | jq -r --arg prefix "$ARTIFACT_PREFIX" '.group | map($prefix + "apm-" + .id) | sort | .[]') + missing=() + for name in "${expected_names[@]}"; do + if ! find /tmp/gh-aw/apm-bundles -maxdepth 2 -name "${name}.tar.gz" | grep -q .; then + missing+=("$name") + fi + done + if [ ${#missing[@]} -gt 0 ]; then + echo "::error::missing APM bundles (group did not pack successfully): ${missing[*]}" exit 1 fi - echo "path=$bundle" >> "$GITHUB_OUTPUT" - - name: Restore APM packages - uses: microsoft/apm-action@v1.4.2 + actual_count=$(find /tmp/gh-aw/apm-bundles -maxdepth 2 -name "${ARTIFACT_PREFIX}apm-*.tar.gz" | wc -l) + expected_count=${#expected_names[@]} + if [ "$actual_count" -gt "$expected_count" ]; then + echo "::error::unexpected APM bundles found: $actual_count file(s) but only $expected_count expected (possible collision)" + exit 1 + fi + - name: Build bundle list + id: bundles + run: | + set -euo pipefail + mapfile -t list < <(find /tmp/gh-aw/apm-bundles -name '*.tar.gz' | sort) + [ ${#list[@]} -gt 0 ] || { echo '::error::no apm bundles found'; exit 1; } + printf '%s\n' "${list[@]}" > /tmp/gh-aw/apm-bundle-list.txt + - name: Restore APM packages (all bundles) + uses: microsoft/apm-action@v1.5.0 with: - bundle: ${{ steps.apm_bundle.outputs.path }} + bundles-file: /tmp/gh-aw/apm-bundle-list.txt --- diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index 8a1c30b55f1..efa2027496b 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"79e2a7bb8b55dd046421af2bd3d2d7b69d7f91c9537311f80f635f68a9f8f5d2","agent_id":"claude"} -# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN","TAVILY_API_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4"},{"repo":"github/codeql-action/upload-sarif","sha":"7fc6561ed893d15cec696e062df840b21db27eb0","version":"v4.35.2"},{"repo":"microsoft/apm-action","sha":"677ddbfb986cdf36d99d26e167aff29b3e80486d","version":"v1.4.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29","digest":"sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29","digest":"sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29@sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29","digest":"sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29@sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.1","digest":"sha256:287fad0236959f3b3d9936ea1ef8d5b4f135ef2a5f5789713495cbbef191e60c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.1@sha256:287fad0236959f3b3d9936ea1ef8d5b4f135ef2a5f5789713495cbbef191e60c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"mcr.microsoft.com/playwright/mcp","digest":"sha256:7b82f29c6ef83480a97f612d53ac3fd5f30a32df3fea1e06923d4204d3532bb2","pinned_image":"mcr.microsoft.com/playwright/mcp@sha256:7b82f29c6ef83480a97f612d53ac3fd5f30a32df3fea1e06923d4204d3532bb2"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_PLUGINS_TOKEN","GITHUB_TOKEN","TAVILY_API_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/create-github-app-token","sha":"1b10c78c7865c340bc4f6099eb2f838309f1e8c3","version":"v3.1.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4"},{"repo":"github/codeql-action/upload-sarif","sha":"7fc6561ed893d15cec696e062df840b21db27eb0","version":"v4.35.2"},{"repo":"microsoft/apm-action","sha":"454b8a1d279376a47df8bb8d525ec076ca0fcef7","version":"v1.5.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29","digest":"sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29","digest":"sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29@sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29","digest":"sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29@sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.1","digest":"sha256:287fad0236959f3b3d9936ea1ef8d5b4f135ef2a5f5789713495cbbef191e60c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.1@sha256:287fad0236959f3b3d9936ea1ef8d5b4f135ef2a5f5789713495cbbef191e60c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"mcr.microsoft.com/playwright/mcp","digest":"sha256:7b82f29c6ef83480a97f612d53ac3fd5f30a32df3fea1e06923d4204d3532bb2","pinned_image":"mcr.microsoft.com/playwright/mcp@sha256:7b82f29c6ef83480a97f612d53ac3fd5f30a32df3fea1e06923d4204d3532bb2"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -53,6 +53,7 @@ # - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 # - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 # - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 @@ -61,7 +62,7 @@ # - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 # - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 # - github/codeql-action/upload-sarif@7fc6561ed893d15cec696e062df840b21db27eb0 # v4.35.2 -# - microsoft/apm-action@677ddbfb986cdf36d99d26e167aff29b3e80486d # v1.4.2 +# - microsoft/apm-action@454b8a1d279376a47df8bb8d525ec076ca0fcef7 # v1.5.0 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4 @@ -786,6 +787,7 @@ jobs: needs: - activation - apm + - apm-prep runs-on: ubuntu-latest permissions: actions: read @@ -957,31 +959,24 @@ jobs: GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode" GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md opencode.jsonc" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - - id: apm_cache_restore - name: Restore APM bundle from cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - key: apm-${{ needs.activation.outputs.engine_id }}-${{ hashFiles('.github/workflows/*.lock.yml') }} - path: /tmp/gh-aw/apm-workspace - - if: steps.apm_cache_restore.outputs.cache-hit != 'true' - name: Download APM bundle artifact + - name: Download APM bundle artifacts (all groups) uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}apm - path: /tmp/gh-aw/apm-bundle - - id: apm_bundle - name: Find APM bundle path - run: | - bundle=$(find /tmp/gh-aw/apm-workspace /tmp/gh-aw/apm-bundle -name '*.tar.gz' 2>/dev/null | head -1) - if [ -z "$bundle" ]; then - echo "::error::APM bundle not found in /tmp/gh-aw/apm-workspace or /tmp/gh-aw/apm-bundle" - exit 1 - fi - echo "path=$bundle" >> "$GITHUB_OUTPUT" - - name: Restore APM packages - uses: microsoft/apm-action@677ddbfb986cdf36d99d26e167aff29b3e80486d # v1.4.2 - with: - bundle: ${{ steps.apm_bundle.outputs.path }} + 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: Validate downloaded bundles match matrix manifest + run: "set -euo pipefail\nmapfile -t expected_names < <(echo \"$EXPECTED_MATRIX\" | jq -r --arg prefix \"$ARTIFACT_PREFIX\" '.group | map($prefix + \"apm-\" + .id) | sort | .[]')\nmissing=()\nfor name in \"${expected_names[@]}\"; do\n if ! find /tmp/gh-aw/apm-bundles -maxdepth 2 -name \"${name}.tar.gz\" | grep -q .; then\n missing+=(\"$name\")\n fi\ndone\nif [ ${#missing[@]} -gt 0 ]; then\n echo \"::error::missing APM bundles (group did not pack successfully): ${missing[*]}\"\n exit 1\nfi\nactual_count=$(find /tmp/gh-aw/apm-bundles -maxdepth 2 -name \"${ARTIFACT_PREFIX}apm-*.tar.gz\" | wc -l)\nexpected_count=${#expected_names[@]}\nif [ \"$actual_count\" -gt \"$expected_count\" ]; then\n echo \"::error::unexpected APM bundles found: $actual_count file(s) but only $expected_count expected (possible collision)\"\n exit 1\nfi\n" + - id: bundles + name: Build bundle list + run: "set -euo pipefail\nmapfile -t list < <(find /tmp/gh-aw/apm-bundles -name '*.tar.gz' | sort)\n[ ${#list[@]} -gt 0 ] || { echo '::error::no apm bundles found'; exit 1; }\nprintf '%s\\n' \"${list[@]}\" > /tmp/gh-aw/apm-bundle-list.txt\n" + - name: Restore APM packages (all bundles) + uses: microsoft/apm-action@454b8a1d279376a47df8bb8d525ec076ca0fcef7 # v1.5.0 + with: + bundles-file: /tmp/gh-aw/apm-bundle-list.txt - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.29@sha256:e68f37e36962dcb3f3d1de680a49bc2302cefd001b941a7dc377155ec7ce42f4 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29@sha256:d1219e4110684402aabbeb5a43858f26790c9d0be210581cf3f7a521bd2c87b6 ghcr.io/github/gh-aw-firewall/squid:0.25.29@sha256:8a71ad9e40454051672312917e51567abfb8251d7c294d086c48f63d84e4cb53 ghcr.io/github/gh-aw-mcpg:v0.3.1@sha256:287fad0236959f3b3d9936ea1ef8d5b4f135ef2a5f5789713495cbbef191e60c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5 mcr.microsoft.com/playwright/mcp@sha256:7b82f29c6ef83480a97f612d53ac3fd5f30a32df3fea1e06923d4204d3532bb2 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f @@ -2511,10 +2506,15 @@ jobs: if-no-files-found: ignore apm: - needs: activation + needs: + - activation + - apm-prep runs-on: ubuntu-slim + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.apm-prep.outputs.matrix) }} permissions: - contents: read + {} steps: - name: Configure GH_HOST for enterprise compatibility @@ -2526,72 +2526,141 @@ jobs: GH_HOST="${GITHUB_SERVER_URL#https://}" GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Checkout workflow lock files - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - sparse-checkout: | - .github/workflows - sparse-checkout-cone-mode: false - - name: Restore APM bundle from cache - id: apm_cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - key: apm-${{ needs.activation.outputs.engine_id }}-${{ hashFiles('.github/workflows/*.lock.yml') }} - path: /tmp/gh-aw/apm-workspace - - name: Prepare APM package list - id: apm_prep - if: steps.apm_cache.outputs.cache-hit != 'true' + - name: Mint installation token + id: token + if: ${{ matrix.group.app-id != '' }} + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + app-id: ${{ matrix.group.app-id }} + owner: ${{ matrix.group.owner != '' && matrix.group.owner || github.repository_owner }} + private-key: ${{ matrix.group.private-key }} + repositories: ${{ matrix.group.repositories }} + - name: Render package list + id: list run: | - DEPS=$(echo "$AW_APM_PACKAGES" | jq -r '.[] | "- " + .') + DEPS=$(echo "$AW_PKG" | jq -r '.[] | "- " + .') { echo "deps<> "$GITHUB_OUTPUT" env: - AW_APM_PACKAGES: "[\"microsoft/apm-sample-package\"]" + AW_PKG: ${{ toJSON(matrix.group.packages) }} - name: Pack APM packages - id: apm_pack - if: steps.apm_cache.outputs.cache-hit != 'true' - uses: microsoft/apm-action@677ddbfb986cdf36d99d26e167aff29b3e80486d # v1.4.2 + id: pack + uses: microsoft/apm-action@454b8a1d279376a47df8bb8d525ec076ca0fcef7 # v1.5.0 env: - GITHUB_TOKEN: ${{ secrets.GH_AW_PLUGINS_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.token.outputs.token || secrets.GH_AW_PLUGINS_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: archive: "true" - dependencies: ${{ steps.apm_prep.outputs.deps }} + dependencies: ${{ steps.list.outputs.deps }} isolated: "true" pack: "true" target: all working-directory: /tmp/gh-aw/apm-workspace - - name: Save APM bundle to cache - if: steps.apm_cache.outputs.cache-hit != 'true' && success() - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - key: ${{ steps.apm_cache.outputs.cache-primary-key }} - path: /tmp/gh-aw/apm-workspace - - name: Find APM bundle path - id: apm_bundle_path + - name: Rename bundle to group-scoped filename + id: rename-bundle run: | - bundle=$(find /tmp/gh-aw/apm-workspace -name '*.tar.gz' | head -1) - if [ -z "$bundle" ]; then - echo "::error::APM bundle not found in /tmp/gh-aw/apm-workspace" - exit 1 - fi - echo "path=$bundle" >> "$GITHUB_OUTPUT" + dst="$(dirname "$BUNDLE_SRC")/${ARTIFACT_PREFIX}apm-${GROUP_ID}.tar.gz" + mv "$BUNDLE_SRC" "$dst" + echo "bundle-path=$dst" >> "$GITHUB_OUTPUT" + env: + ARTIFACT_PREFIX: ${{ needs.activation.outputs.artifact_prefix }} + BUNDLE_SRC: ${{ steps.pack.outputs.bundle-path }} + GROUP_ID: ${{ matrix.group.id }} - name: Upload APM bundle artifact if: success() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}apm - path: ${{ steps.apm_bundle_path.outputs.path }} + name: ${{ needs.activation.outputs.artifact_prefix }}apm-${{ matrix.group.id }} + path: ${{ steps.rename-bundle.outputs.bundle-path }} retention-days: "1" + apm-prep: + needs: activation + runs-on: ubuntu-slim + permissions: + {} + + outputs: + matrix: ${{ steps.compute.outputs.matrix }} + steps: + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Compute APM credential-group matrix + id: compute + run: | + set -euo pipefail + packages_json=${AW_APM_PACKAGES:-null} + apps_json=${AW_APM_APPS:-null} + legacy_id=${AW_APM_LEGACY_APP_ID:-} + + groups=$(jq -nc \ + --argjson packages "$packages_json" \ + --argjson apps "$apps_json" \ + --arg legacy_id "$legacy_id" \ + --arg legacy_pk "${AW_APM_LEGACY_PRIVATE_KEY:-}" \ + --arg legacy_owner "${AW_APM_LEGACY_OWNER:-}" \ + --arg legacy_repos "${AW_APM_LEGACY_REPOS:-}" \ + 'def slug(s): s | gsub("[^a-zA-Z0-9-]"; "-") | ascii_downcase | .[0:32]; + def with_id(g): + g + (if (g.id // "") == "" then {id: ("auto-" + slug(g.owner // "default"))} else {} end); + [ + (if (($packages // []) | length) > 0 and $legacy_id == "" + then [{id:"default",("app-id"):"",("private-key"):"",owner:"",repositories:"",packages:$packages}] + else [] end), + (if $legacy_id != "" + then [with_id({id:"legacy",("app-id"):$legacy_id,("private-key"):$legacy_pk,owner:$legacy_owner,repositories:$legacy_repos,packages:($packages // [])})] + else [] end), + (($apps // []) | map(with_id(.))) + ] | add // []') + + count=$(echo "$groups" | jq 'length') + if [ "$count" = "0" ]; then + echo "::error::shared/apm.md import provided no packages. Add packages: , single-app inputs (app-id + private-key), or apps: in the with: block." + exit 1 + fi + + dups=$(echo "$groups" | jq -r '[.[].id] | group_by(.) | map(select(length > 1) | first) | join(", ")') + if [ -n "$dups" ]; then + echo "::error::duplicate apm group ids after auto-derivation: $dups. Set apps[].id explicitly when two entries share the same owner." + exit 1 + fi + + while IFS= read -r id; do + if ! echo "$id" | grep -Eq '^[a-z0-9-]{1,32}$'; then + echo "::error::invalid apm group id: '$id' (lowercase alphanumeric and dashes, 1-32 chars). Set apps[].id explicitly." + exit 1 + fi + done < <(echo "$groups" | jq -r '.[].id') + + # SAFE: emit only id + package-count to logs. Never $groups in full. + { + echo "matrix={\"group\":$groups}" + } >> "$GITHUB_OUTPUT" + printf "::notice::APM matrix: %d credential group(s)\n" "$count" + echo "$groups" | jq -r '.[] | " - " + .id + " (" + (.packages | length | tostring) + " package(s))"' + env: + AW_APM_APPS: ${{ github.aw.import-inputs.apps }} + AW_APM_LEGACY_APP_ID: ${{ github.aw.import-inputs.app-id }} + AW_APM_LEGACY_OWNER: ${{ github.aw.import-inputs.owner }} + AW_APM_LEGACY_PRIVATE_KEY: ${{ github.aw.import-inputs.private-key }} + AW_APM_LEGACY_REPOS: ${{ github.aw.import-inputs.repositories }} + AW_APM_PACKAGES: "[\"microsoft/apm-sample-package\"]" + conclusion: needs: - activation - agent - apm + - apm-prep - detection - safe_outputs - update_cache_memory diff --git a/docs/src/content/docs/reference/dependencies.md b/docs/src/content/docs/reference/dependencies.md index 35a83ebca0b..5efaa882b10 100644 --- a/docs/src/content/docs/reference/dependencies.md +++ b/docs/src/content/docs/reference/dependencies.md @@ -11,6 +11,22 @@ APM is configured by importing the `shared/apm.md` workflow, which creates a ded > [!NOTE] > The `dependencies:` frontmatter field is deprecated and no longer supported. Migrate to the import-based approach shown below. +> +> The `dependencies:` input on the underlying `microsoft/apm-action` (used inside `shared/apm.md`) is also deprecated in favour of the `packages:` and `apps:` inputs — do not reach for `dependencies:` when hand-editing a vendored copy of the file. + +## Where `shared/apm.md` comes from + +`shared/apm.md` is a **local workflow file** that gh-aw resolves at `.github/workflows/shared/apm.md` in your repository — it is not a remote import (the `uses:` syntax inside `imports:` is gh-aw's local-import shape, not GitHub Actions' `uses: owner/repo@ref`). + +You must vendor the file into your own repository. The canonical, current source is maintained in [microsoft/apm](https://github.com/microsoft/apm/blob/main/.github/workflows/shared/apm.md): + +```bash +mkdir -p .github/workflows/shared +curl -sSL https://raw.githubusercontent.com/microsoft/apm/main/.github/workflows/shared/apm.md \ + > .github/workflows/shared/apm.md +``` + +The canonical version pins `microsoft/apm-action@v1.5.0` and supports multi-org GitHub App authentication (`apps:[]`) and multi-bundle restore. To check whether your vendored copy is current, compare the `Source of truth:` and `apm-action pin:` lines near the top of the file with the canonical copy linked above. ## Usage @@ -77,3 +93,4 @@ To reproduce or debug the pack/unpack flow locally, run `apm pack` and `apm unpa | gh-aw integration (APM docs) | https://microsoft.github.io/apm/integrations/gh-aw/ | | apm-action (GitHub) | https://github.com/microsoft/apm-action | | microsoft/apm (GitHub) | https://github.com/microsoft/apm | +| shared/apm.md (canonical) | https://github.com/microsoft/apm/blob/main/.github/workflows/shared/apm.md |