From a090dd709c82621b350729a678b283b6850c53fd Mon Sep 17 00:00:00 2001 From: idevlab Date: Thu, 24 Sep 2026 15:58:16 +0800 Subject: [PATCH 1/2] Fix release workflow caller permissions --- .github/workflows/nightly-release.yml | 2 + .github/workflows/release.yml | 2 + .../intent.md | 57 +++++++++++++++++++ .../plan.md | 30 ++++++++++ .../spec.md | 53 +++++++++++++++++ .../verification.md | 37 ++++++++++++ scripts/tests/test_nightly_release_plan.sh | 8 +++ 7 files changed, 189 insertions(+) create mode 100644 docs/sdlc/changes/2026-09-24-release-workflow-permissions/intent.md create mode 100644 docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md create mode 100644 docs/sdlc/changes/2026-09-24-release-workflow-permissions/spec.md create mode 100644 docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index f5098eeb..f091bf13 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -108,6 +108,8 @@ jobs: name: Sign, Verify & Publish needs: [plan, validate] if: ${{ needs.plan.outputs.changed == 'true' }} + permissions: + contents: write uses: ./.github/workflows/release-artifact.yml with: version: ${{ needs.plan.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4fe8a60..00c52c71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,6 +43,8 @@ jobs: release: name: Sign, Verify & Publish needs: [validate] + permissions: + contents: write uses: ./.github/workflows/release-artifact.yml with: # `v0.0.46` -> `0.0.46`; the same script the reusable workflow enforces. diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/intent.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/intent.md new file mode 100644 index 00000000..3fbbd565 --- /dev/null +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/intent.md @@ -0,0 +1,57 @@ +# Intent: Restore nightly and tag-triggered release startup + +**Status:** approved +**Approved-by:** User (explicit approval of all stages in the task) +**Approved-date:** 2026-09-24 +**Upstream:** [Nightly release pipeline](../2026-09-18-nightly-release-pipeline/intent.md) and [Nightly run #35888630166](https://github.com/IchenDEV/utter/actions/runs/35888630166) + +## Problem + +Six scheduled Nightly Release runs since 2026-09-18 ended with +`startup_failure` before any job ran. GitHub's annotation on run #35888630166 +identifies the exact conflict: the reusable `release-artifact.yml` job requests +`contents: write`, while its `nightly-release.yml` caller allows only +`contents: read`. The tag-triggered `release.yml` has the same caller permission +and therefore shares the defect, although its most recent successful run +predates the reusable workflow change. + +## Outcome + +Both release entry points pass GitHub's reusable-workflow permission check. +Planning and validation remain read-only. The reusable release job can create +an immutable tag and publish the verified artifact only through the existing +`production` environment gate. + +## Scope + +- Change the `release` caller job in `nightly-release.yml` and `release.yml` to + grant the `contents: write` scope already required by their shared release + workflow. +- Extend the existing workflow wiring test to reject this specific permission + mismatch before another scheduled run. +- Preserve the schedule, version planner, signing requirements, artifact + verification, production protection, and shared release implementation. + +## Constraints + +- High-risk release/permission change: require reviewed design and plan, + independent verification, PR approval, and protected production approval. +- Do not grant write access to planning or validation jobs. +- Do not create a tag or publish a release during local or PR verification. +- Keep the fix on an isolated branch, separate from the voice-input PR. + +## Acceptance criteria + +- A deterministic local check fails against the current caller permission + mismatch and passes after both callers grant `contents: write` on their + reusable-workflow call jobs. +- `bash scripts/sdlc-checks.sh`, `bash scripts/ci-basic-checks.sh`, and + `swift test` pass on the fix branch. +- GitHub accepts a run of the updated Nightly workflow and starts its `plan` + job; the release job remains subject to the existing `production` gate. +- The tag-triggered caller is covered by the same static regression check. + +## Open questions + +None. GitHub's startup annotation identifies the mismatch and the intended +write scope is already present in the reusable release job. diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md new file mode 100644 index 00000000..f2f23729 --- /dev/null +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md @@ -0,0 +1,30 @@ +# Plan: Restore nightly and tag-triggered release startup + +**Status:** approved +**Approved-by:** User (explicit approval of all stages in the task) +**Approved-date:** 2026-09-24 +**Upstream:** [spec.md](spec.md) + +## Work items + +- [x] Add a regression assertion to the existing workflow wiring test and + observe it fail on the current callers. +- [x] Grant `contents: write` on the `release` calling job in both entry-point + workflows, leaving top-level permissions read-only. +- [ ] Run local checks, review the exact diff, and create a conflict-free PR. + +## Verification plan + +- [x] `bash scripts/tests/test_nightly_release_plan.sh` red before the fix and + green after it. +- [x] `bash scripts/sdlc-checks.sh` +- [x] `bash scripts/ci-basic-checks.sh` +- [x] `swift test --scratch-path /tmp/utter-silent-insertion-build` +- [ ] GitHub PR checks and mergeability; actual Nightly startup after merge is + a separate protected release observation. + +## Human gates + +Independent high-risk review, PR approval, and protected production approval +remain required. The user approved the intent, design, and implementation +stages in this task; verification evidence will be recorded after the checks. diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/spec.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/spec.md new file mode 100644 index 00000000..d082032d --- /dev/null +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/spec.md @@ -0,0 +1,53 @@ +# Spec: Restore nightly and tag-triggered release startup + +**Status:** approved +**Approved-by:** User (explicit approval of all stages in the task) +**Approved-date:** 2026-09-24 +**Upstream:** [intent.md](intent.md) + +## Context + +GitHub rejects Nightly run #35888630166 at workflow composition, before any +job is created. Its annotation names `nightly-release.yml` line 107 and the +nested `release-artifact.yml` job: the caller allows `contents: read` while the +called job requests `contents: write`. `release.yml` calls the same reusable +workflow with the same read-only ceiling. The reusable job needs write access +to push an immutable tag for Nightly and publish release assets for either +entry point. + +## Design + +Add `permissions: { contents: write }` to the `release` calling job in each +entry-point workflow. Keep their top-level `contents: read` permissions so +`plan` and `validate` remain read-only. Keep the reusable workflow's job-level +write requirement as the one source of the publishing job's permission need. + +Add one shared shell regression assertion to the existing release workflow +wiring test. It checks that both calling jobs grant the write scope required by +the reusable job and that each entry point still defaults to read-only. The +test has no GitHub credentials and creates no release. + +## Safety and failure modes + +- Write scope applies only to release calls, which already depend on successful + validation and use the `production` environment in the reusable job. +- The fix does not change tag immutability, `main` tip checks, signing, + notarization, artifact verification, or release asset replacement guards. +- GitHub repository or environment policy can still block a protected release; + that is a separate runtime condition to report from a real run. +- Roll back by reverting the two caller permission additions and the test, + which restores the prior startup failure without moving any tag or asset. + +## Test strategy + +Run the existing workflow wiring test before and after the YAML fix, then the +SDLC/basic checks and Swift tests. Inspect the PR's GitHub checks and +mergeability. After protected merge, a Nightly run must reach `plan`; no local +or PR check should create a tag or publish an artifact. + +## Rollout and rollback + +Submit as an isolated PR against `main`. An independent reviewer checks the +permission boundary and production protection before merge. Observe the first +scheduled run after merge. If it fails beyond startup, inspect its actual job +logs before changing signing or publication behavior. diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md new file mode 100644 index 00000000..f3d908c2 --- /dev/null +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md @@ -0,0 +1,37 @@ +# Verification: Restore nightly and tag-triggered release startup + +**Status:** pending approval +**Approved-by:** — +**Approved-date:** — +**Upstream:** [plan.md](plan.md) + +## Evidence + +| Check | Result | Evidence | +|---|---|---| +| Workflow permission regression | Pass locally | `bash scripts/tests/test_nightly_release_plan.sh` failed on the old `release.yml` permission ceiling, then passed after both callers granted job-level write access. | +| `bash scripts/sdlc-checks.sh` | Pass locally | Stage order and approval fields passed. | +| `bash scripts/ci-basic-checks.sh` | Pass locally | Basic CI checks passed, including the workflow wiring test. | +| `swift test --scratch-path /tmp/utter-silent-insertion-build` | Pass locally | 793 XCTest cases, 18 skipped, zero failures; one Swift Testing case passed. | +| GitHub PR checks and mergeability | Pending | — | +| Real Nightly startup | Pending | Requires merge to `main` and an authorized run. | + +## Acceptance criteria + +- Both release entry points grant the required scope to the shared workflow — pass in source and local regression check. +- Planning and validation remain read-only — pass in source; workflow-level defaults remain `contents: read`. +- A regression check rejects the prior mismatch — pass; observed red before the fix and green afterward. +- A real Nightly run reaches `plan` — pending post-merge observation. + +## Residual risk + +GitHub validates reusable workflow composition only on a real run of the +updated workflow. Local checks cannot establish that the next scheduled run +will reach `plan`. Protected release signing and publication are also outside +local and PR validation. + +## Decision + +The local fix is ready for PR checks and independent review. The first real +Nightly startup after merge remains the production acceptance check; protected +release approval is separate. diff --git a/scripts/tests/test_nightly_release_plan.sh b/scripts/tests/test_nightly_release_plan.sh index ec576a5a..6d9dc24f 100755 --- a/scripts/tests/test_nightly_release_plan.sh +++ b/scripts/tests/test_nightly_release_plan.sh @@ -109,6 +109,14 @@ grep -Fq './scripts/nightly-release-plan.sh . origin/main' "$WORKFLOW" \ for workflow in "$RELEASE_WORKFLOW" "$WORKFLOW"; do grep -Fq 'uses: ./.github/workflows/release-artifact.yml' "$workflow" \ || fail "$workflow must call the reusable artifact workflow" + awk ' + /^permissions:$/ { getline; if ($0 == " contents: read") read_default = 1 } + /^ release:$/ { in_release = 1; next } + in_release && /^ [a-z_-]+:$/ { exit } + in_release && /^ permissions:$/ { in_permissions = 1; next } + in_permissions && /^ contents: write$/ { release_write = 1 } + END { exit !(read_default && release_write) } + ' "$workflow" || fail "$workflow must keep read-only defaults and grant contents: write to its release call" done # Guardrails copied from the tag workflow must survive in the shared pipeline. From 3f66dda680dd29f50ce4ba2be6f5ae1149ebd6d5 Mon Sep 17 00:00:00 2001 From: idevlab Date: Thu, 24 Sep 2026 15:59:32 +0800 Subject: [PATCH 2/2] Record release workflow PR review state --- .../changes/2026-09-24-release-workflow-permissions/plan.md | 2 +- .../2026-09-24-release-workflow-permissions/verification.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md index f2f23729..cdf4c17c 100644 --- a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/plan.md @@ -11,7 +11,7 @@ observe it fail on the current callers. - [x] Grant `contents: write` on the `release` calling job in both entry-point workflows, leaving top-level permissions read-only. -- [ ] Run local checks, review the exact diff, and create a conflict-free PR. +- [x] Run local checks, review the exact diff, and create a conflict-free PR. ## Verification plan diff --git a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md index f3d908c2..0764fd08 100644 --- a/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md +++ b/docs/sdlc/changes/2026-09-24-release-workflow-permissions/verification.md @@ -13,7 +13,7 @@ | `bash scripts/sdlc-checks.sh` | Pass locally | Stage order and approval fields passed. | | `bash scripts/ci-basic-checks.sh` | Pass locally | Basic CI checks passed, including the workflow wiring test. | | `swift test --scratch-path /tmp/utter-silent-insertion-build` | Pass locally | 793 XCTest cases, 18 skipped, zero failures; one Swift Testing case passed. | -| GitHub PR checks and mergeability | Pending | — | +| GitHub PR checks and mergeability | In progress | [Draft PR #114](https://github.com/IchenDEV/utter/pull/114) was reported `MERGEABLE` at `a090dd7`; remote checks were still running. | | Real Nightly startup | Pending | Requires merge to `main` and an authorized run. | ## Acceptance criteria