diff --git a/.github/workflows/apm-materialize.yml b/.github/workflows/apm-materialize.yml index 4cc8d74f..714baff6 100644 --- a/.github/workflows/apm-materialize.yml +++ b/.github/workflows/apm-materialize.yml @@ -3,19 +3,24 @@ name: APM Materialize # Renovate maintains the `owner/repo#vX.Y.Z` tag pins under dependencies.apm in # apm.yml (see .renovate/customManagers.json5), but it only edits the manifest -# text — apm.lock.yaml and the deployed primitives under .github/ stay stale. -# This workflow runs on any PR that touches apm.yml, executes `apm install` to -# materialize the pinned version (re-resolve the lockfile + redeploy the harness -# files), and commits the result back onto the PR branch so the bump is complete. +# text — apm.lock.yaml and the deployed primitives under .github/ (and other +# harness dirs) stay stale. This workflow materializes them. # -# Why a workflow and not a Renovate postUpgradeTask: this org uses Mend-hosted -# Renovate, which does not run arbitrary postUpgrade commands. +# IMPORTANT — runs POST-MERGE on main, never on the PR: +# An earlier version triggered on `pull_request` and pushed the materialized +# files back onto the PR branch. For Renovate's own bump PRs that corrupted +# Renovate's branch ownership — it stopped rebasing/automerging and refused to +# autoclose superseded PRs ("branch already modified"). So instead this runs +# AFTER apm.yml lands on main and opens a SEPARATE `chore: materialize` PR via +# the open-pr composite. Renovate's bump PR stays pristine and merges/automerges +# normally; this follow-up PR carries the lockfile + deployed files. # -# The commit is pushed with a GitHub App token so required CI re-runs on the -# updated PR (a push with the default GITHUB_TOKEN would not retrigger it). +# The PR is opened with a GitHub App token so required CI fires on it. on: - pull_request: + push: + branches: + - main paths: - apm.yml workflow_dispatch: @@ -24,19 +29,16 @@ permissions: contents: read concurrency: - group: apm-materialize-${{ github.event.pull_request.number || github.ref }} + group: apm-materialize cancel-in-progress: false jobs: materialize: name: Materialize APM primitives runs-on: ubuntu-24.04 - # Forks can't be pushed to; only run for same-repo PR branches (or manual). - if: >- - github.event_name == 'workflow_dispatch' || - github.event.pull_request.head.repo.full_name == github.repository permissions: contents: write + pull-requests: write steps: - name: Harden runner # renovate: datasource=github-tags depName=DevSecNinja/.github @@ -50,16 +52,16 @@ jobs: app-id: ${{ vars.RELEASE_PLEASE_APP_ID }} private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }} permission-contents: write + permission-pull-requests: write - - name: Checkout PR branch + - name: Checkout main # zizmor: ignore[artipacked] - # Persisted credentials are required: the final step pushes the - # materialized files back to the PR branch. This job runs no untrusted - # code — it installs the pinned apm CLI and runs `apm install` over the - # repo's own apm.yml. + # Persisted credentials are required: the open-pr step below pushes the + # materialize branch using the token checkout writes into the remote URL. + # This job runs no untrusted code — it installs the pinned apm CLI and + # runs `apm install` over the repo's own committed apm.yml. uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: - ref: ${{ github.event.pull_request.head.ref || github.ref }} token: ${{ steps.app-token.outputs.token }} - name: Check for apm.yml @@ -90,18 +92,24 @@ jobs: apm --version apm install - - name: Commit materialized files back to the PR branch + - name: Open materialize PR if: steps.precheck.outputs.skip != 'true' - env: - BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} - run: |- - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - echo "Already materialized — nothing to commit." - exit 0 - fi - git commit -m "chore: materialize APM primitives for apm.yml change" - git push origin "HEAD:${BRANCH}" + # renovate: datasource=github-tags depName=DevSecNinja/.github + uses: DevSecNinja/.github/actions/open-pr@7f2abdd838a855dba31a2b2de6358529fdcef09d # v1.8.0 + with: + branch: chore/apm-materialize + base: main + title: "chore: materialize APM primitives" + commit-message: "chore: materialize APM primitives from apm.yml" + labels: apm-sync + github-token: ${{ steps.app-token.outputs.token }} + # Stage the lockfile and deployed harness files. apm.yml itself is + # unchanged (already on main); apm_modules/ is gitignored. + if-no-changes: skip + body: |- + Automated PR — materializes the APM primitives for the current + `apm.yml`. Runs `apm install` to refresh `apm.lock.yaml` and redeploy + the primitives into the harness directories after an `apm.yml` change + landed on `main` (e.g. a Renovate tag bump). + + Triggered by `${{ github.workflow }}` at commit `${{ github.sha }}`. diff --git a/docs/apm-sync-onboarding.md b/docs/apm-sync-onboarding.md index 52e79012..c42a85a7 100644 --- a/docs/apm-sync-onboarding.md +++ b/docs/apm-sync-onboarding.md @@ -4,7 +4,7 @@ How to keep a repo's installed [APM (Agent Package Manager)](https://microsoft.g primitives — prompts, instructions, agents, skills — up to date with their upstream packages (e.g. `DevSecNinja/ai-toolkit`). -**The model: Renovate bumps the pin, a workflow materializes it.** +**The model: Renovate bumps the pin, a post-merge workflow materializes it.** APM dependencies are pinned to an exact tag in `apm.yml` (`DevSecNinja/ai-toolkit#v0.1.1`). That gives reproducible installs, but it means @@ -13,16 +13,21 @@ use the same tool that versions everything else in the org: 1. **Renovate** maintains the `#vX.Y.Z` tag pin in `apm.yml` (via a custom manager in [`.renovate/customManagers.json5`](../.renovate/customManagers.json5)). - It opens a normal `v0.1.1 → v0.2.0` PR — version-aware, with the usual soak, - grouping, and changelog links. -2. The [`apm-materialize.yml`](../.github/workflows/apm-materialize.yml) workflow - runs on that PR, executes `apm install` to re-resolve `apm.lock.yaml` and - redeploy the primitives into the harness directories, and commits the result - back onto the PR branch. You review the complete diff and merge. - -> Why a workflow and not a Renovate `postUpgradeTask`: this org uses **Mend-hosted -> Renovate**, which doesn't run arbitrary postUpgrade commands. The workflow does -> the materialization instead. + It opens a normal `v0.1.1 → v0.2.0` PR that changes **only the manifest text**, + so it rebases, automerges, and autocloses like any other Renovate PR. +2. Once that bump lands on `main`, the + [`apm-materialize.yml`](../.github/workflows/apm-materialize.yml) workflow runs + `apm install` to re-resolve `apm.lock.yaml` and redeploy the primitives, and + opens a **separate** `chore: materialize APM primitives` PR with the result. + +> **Why post-merge, not on the Renovate PR:** pushing the materialized files onto +> Renovate's own branch corrupts Renovate's branch ownership — it stops rebasing +> and automerging the PR and refuses to autoclose it when superseded +> ("branch already modified"). So the materialize step never touches the bump PR; +> it runs after merge and opens its own PR. +> +> (A Renovate `postUpgradeTask` would fold both into one commit, but this org uses +> **Mend-hosted Renovate**, which doesn't run arbitrary postUpgrade commands.) This is the agentic-primitive counterpart to [config-sync](../.github/workflows/config-sync.yml): config-sync distributes @@ -37,7 +42,7 @@ primitives. commit the result: ```sh - apm install DevSecNinja/ai-toolkit#v0.2.0 --target copilot + apm install DevSecNinja/ai-toolkit#v0.3.0 --target copilot git add apm.yml apm.lock.yaml .github/ # plus any other harness dirs APM wrote git commit -m "feat: install ai-toolkit APM primitives" ``` @@ -46,8 +51,8 @@ primitives. > package cache — APM adds it to `.gitignore` on first install; don't commit it. - The `RELEASE_PLEASE_APP_ID` variable + `RELEASE_PLEASE_APP_PRIVATE_KEY` secret - (the same App used for release-please). The materialize workflow commits with - the App token so required CI re-runs on the updated PR. + (the same App used for release-please). The materialize workflow opens its PR + with the App token so required CI runs on it. - The repo extends the org Renovate presets (so it inherits the apm.yml custom manager): @@ -64,57 +69,41 @@ primitives. ## Per-repo adoption -Add the materialize workflow. Pin it to a release tag of `DevSecNinja/.github`. - -### `.github/workflows/apm-materialize.yml` +Copy the materialize workflow into the consuming repo and pin the action SHAs. +The reference implementation lives in `DevSecNinja/.github` at +[`.github/workflows/apm-materialize.yml`](../.github/workflows/apm-materialize.yml). -```yaml ---- -name: APM Materialize -on: - pull_request: - paths: - - apm.yml - workflow_dispatch: -permissions: - contents: read -jobs: - materialize: - # renovate: datasource=github-tags depName=DevSecNinja/.github - uses: DevSecNinja/.github/.github/workflows/apm-materialize.yml@ # vX.Y.Z - permissions: - contents: write - secrets: inherit -``` - -> The reference implementation currently lives as a repo-local workflow in -> `DevSecNinja/.github` itself (it pushes back to the PR branch, so it isn't a -> `workflow_call` reusable yet). Copy -> [`.github/workflows/apm-materialize.yml`](../.github/workflows/apm-materialize.yml) -> into the consuming repo and pin the action SHAs. +It triggers on `push` to `main` (paths: `apm.yml`) plus `workflow_dispatch`, runs +`apm install`, and opens a separate PR via the `open-pr` composite. It is not a +`workflow_call` reusable yet (it opens a PR rather than returning outputs); if a +second consumer adopts it, extract a reusable + config-sync template. --- ## What a run does -1. Triggers on any PR that changes `apm.yml` (Renovate's bump PR, or a manual edit). +1. Triggers **after** an `apm.yml` change lands on `main` (a merged Renovate bump, + or a manual edit) — or via `workflow_dispatch`. 2. Installs the pinned `apm` CLI (`pip install apm-cli`). 3. Runs `apm install` — re-resolves `apm.lock.yaml` to the new pin and redeploys the primitives into the harness directories (`.github/`, `.claude/`, …). -4. Commits the materialized files back onto the PR branch (via the App token, so - CI re-runs). When nothing changed, it's a no-op. +4. Opens a separate `chore: materialize APM primitives` PR (App token → CI runs). + When nothing changed, it's a no-op (no PR). -You review the combined diff (pin bump + lockfile + deployed files) and merge. +Net flow per upstream release: **Renovate bump PR** (manifest only, automerges) → +**materialize PR** (lockfile + deployed files, you review and merge). `main` is +briefly ahead on `apm.yml` until the materialize PR merges — harmless for +agent-context files. --- ## Troubleshooting -### The materialize commit lands but no CI runs on it +### The materialize PR opens but no CI runs on it -The push used `GITHUB_TOKEN` instead of the App. Confirm `RELEASE_PLEASE_APP_ID` -(variable) + `RELEASE_PLEASE_APP_PRIVATE_KEY` (secret) exist and the workflow's -App-token step is wired. Quick unblock: a human closes and reopens the PR. +The PR was opened with `GITHUB_TOKEN` instead of the App. Confirm +`RELEASE_PLEASE_APP_ID` (variable) + `RELEASE_PLEASE_APP_PRIVATE_KEY` (secret) +exist and the workflow's App-token step is wired. ### Renovate isn't opening apm.yml bump PRs