Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/nightly-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
- [x] 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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 | 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

- 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.
8 changes: 8 additions & 0 deletions scripts/tests/test_nightly_release_plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading