diff --git a/.changelog/unreleased/106-anomaly-triage-write-permissions.md b/.changelog/unreleased/106-anomaly-triage-write-permissions.md new file mode 100644 index 0000000..50a5f42 --- /dev/null +++ b/.changelog/unreleased/106-anomaly-triage-write-permissions.md @@ -0,0 +1,3 @@ +### Fixed + +- Declare the anomaly-triage caller's least-privilege write permissions so reusable jobs start and route entries in repositories whose default `GITHUB_TOKEN` is read-only. diff --git a/docs/repo-update-log.md b/docs/repo-update-log.md index fc31059..8befa84 100644 --- a/docs/repo-update-log.md +++ b/docs/repo-update-log.md @@ -15,6 +15,15 @@ This log records agent-visible repository changes that should be easy to audit l - **Propagation:** none | pending | completed ``` +## 2026-07-09 - Anomaly-triage caller write permissions + +- **Issue/PR:** #106 / #107 +- **Branch:** agent/codex/106-anomaly-triage-write-permissions +- **Changed paths:** examples/anomaly-triage.yml, scripts/workflow-structure.test.mjs, .changelog/unreleased/106-anomaly-triage-write-permissions.md, docs/repo-update-log.md +- **What changed:** The managed anomaly-triage caller now grants `contents: read`, `pull-requests: write`, and `issues: write`, which are the exact scopes its reusable workflow needs. Added a structural regression test because read-default consumers previously failed during reusable-workflow expansion before GitHub created any jobs. +- **Verification:** TDD RED: `npm test -- scripts/workflow-structure.test.mjs` → 1 failed / 21 passed on the missing caller permission block. GREEN: the same command → 22 passed / 0 failed after adding the three scopes. `actionlint examples/anomaly-triage.yml` exited 0. Full `npm test` → 9 files / 180 tests / 180 passed. `git diff --check` exited 0 with working-tree LF-to-CRLF warnings only. Live consumer proof remains part of propagation. +- **Propagation:** pending — refresh the `archon-setup` snapshot from the delivered provider source, update every opted-in ArchonVII consumer through normal updater lanes, and prove routing/idempotency in a read-default consumer PR. + ## 2026-07-04 - Docs-gate lane (docs-system input) + fragment example cleanup - **Issue/PR:** #104 / (pending) diff --git a/examples/anomaly-triage.yml b/examples/anomaly-triage.yml index 30d97cc..52024ca 100644 --- a/examples/anomaly-triage.yml +++ b/examples/anomaly-triage.yml @@ -27,6 +27,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + pull-requests: write + issues: write + jobs: triage: uses: ArchonVII/github-workflows/.github/workflows/anomaly-triage.yml@v1 diff --git a/scripts/workflow-structure.test.mjs b/scripts/workflow-structure.test.mjs index 62b0fe6..2b1a8e3 100644 --- a/scripts/workflow-structure.test.mjs +++ b/scripts/workflow-structure.test.mjs @@ -229,6 +229,22 @@ describe('repo-required-gate caller example', () => { }); }); +describe('anomaly-triage caller permission contract', () => { + it('grants the reusable workflow the least write permissions it requires', () => { + const body = readExample('anomaly-triage'); + + // Reusable workflows cannot elevate the caller token. Without this block, + // read-default consumers fail at workflow startup before jobs exist (#106). + const permissionsStart = body.indexOf('permissions:'); + const jobsStart = body.indexOf('jobs:'); + expect(permissionsStart).toBeGreaterThan(-1); + expect(permissionsStart).toBeLessThan(jobsStart); + expect(body.slice(permissionsStart, jobsStart).replaceAll('\r\n', '\n').trim()).toBe( + ['permissions:', ' contents: read', ' pull-requests: write', ' issues: write'].join('\n'), + ); + }); +}); + describe('pr-policy workflow contract source', () => { it('uses the shared PR contract validator instead of inline body regexes', () => { const body = readWorkflow('pr-policy');