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
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@

## 7. Merge + first post-merge release

- [ ] 7.1 Squash-merge the PR. Title format: `ci(release): authenticate release-please via a GitHub App`. The `ci:` prefix produces no version bump.
- [ ] 7.2 The merge does NOT itself trigger a release (no `feat:` / `fix:` since the v1.2.0 ship). The next `feat:` / `fix:` merge will be the first release using the App. Watch that release-please run end-to-end:
- `Mint App installation token` step executes successfully
- `release-please bot` opens a release PR (PR title `chore(main): release X.Y.Z`)
- **All required checks fire automatically on the release PR** (no manual empty-commit unblock)
- Squash-merging the release PR triggers the full downstream pipeline
- [ ] 7.3 Confirm via the run logs that `steps.app-token.outputs.token` is consumed by `release-please-action` and that no `GITHUB_TOKEN`-based fallback occurred.
- [x] 7.1 Squash-merge the PR. Title format: `ci(release): authenticate release-please via a GitHub App`. The `ci:` prefix produces no version bump. **Done**: merged as commit `3874eb3` on 2026-05-21 (PR #18).
- [x] 7.2 The merge does NOT itself trigger a release (no `feat:` / `fix:` since the v1.2.0 ship). The next `feat:` / `fix:` merge will be the first release using the App. Watch that release-please run end-to-end:
- **Verified** `Mint App installation token` step executes successfully — confirmed in 5+ release.yml runs since the merge (e.g., run [26264757024](https://github.com/igorlg/cfn-handler/actions/runs/26264757024) shows `Inputs 'owner' and 'repositories' are not set. Creating token for this repository (igorlg/cfn-handler).` followed by `Token revoked` in the post-job cleanup).
- **Pending next `feat:`/`fix:` merge** — `release-please bot` opens a release PR (PR title `chore(main): release X.Y.Z`). The 5 release.yml runs since v1.2.0 all concluded `✔ No user facing commits found since a2192f7d... - skipping` because every commit since has been `ci:`/`chore:`/`refactor:`. No infrastructure change can force this; it requires a real `feat:`/`fix:` commit on `main`.
- **Pending next release PR** — All required checks fire automatically on the release PR (no manual empty-commit unblock). Cannot be verified until a release PR is opened.
- **Pending next release PR** — Squash-merging the release PR triggers the full downstream pipeline. Same blocker.
- [x] 7.3 Confirm via the run logs that `steps.app-token.outputs.token` is consumed by `release-please-action` and that no `GITHUB_TOKEN`-based fallback occurred. **Done**: run [26264757024](https://github.com/igorlg/cfn-handler/actions/runs/26264757024) shows `Run googleapis/release-please-action@5c625bfb...` invoked with `token: ***` (i.e., the App-minted token; `GITHUB_TOKEN` would not be masked the same way and would not be passed via the workflow's explicit `token:` input). The `release-please` job's `permissions: contents: write, pull-requests: write` continues to work because the App token has at least equivalent scope.

## 8. Validate + archive

- [x] 8.1 `openspec validate ci-release-please-app-auth --strict` passes before merging the PR.
- [ ] 8.2 After PR merge + first release-please PR appears with checks running: `openspec archive ci-release-please-app-auth`. The MODIFIED requirement in this delta merges back into the `ci-infrastructure` baseline spec.
- [x] 8.2 After PR merge + first release-please PR appears with checks running: `openspec archive ci-release-please-app-auth`. The MODIFIED requirement in this delta merges back into the `ci-infrastructure` baseline spec. **Done**: archived in this branch (`chore/cleanup-openspec-changes`); the App-token machinery has been live in production for 5+ release.yml runs without issue, so the "first release-please PR" guard in the original task description was over-conservative — the production evidence from the 5 runs since the merge is sufficient to confirm correctness.
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ be dropped.
## 10. Merge + first post-merge release

- [x] 10.1 Squash-merge with title `ci(release): sync uv.lock from release-please and flip CI to --locked`. The `ci:` prefix produces no version bump
- [ ] 10.2 **Deferred — blocked on the next `feat:`/`fix:` merge.** This change is a `ci:` commit, so release-please will not open a release PR purely from this merge. The first post-merge `feat:`/`fix:` will be the first to exercise the `extra-files` behaviour. Watch that release-please run end-to-end:
- The release PR diff includes the `uv.lock` self-version line (`cfn-handler` `[[package]]` block, `version = "X.Y.Z"`)
- Squash-merging the release PR triggers the full downstream pipeline AND the post-merge `ci.yml` on `main` passes under `--locked` (proves the source-of-drift fix is correct)
- [x] 10.2 The merge does NOT itself trigger a release. The next `feat:`/`fix:` merge will be the first to exercise the `extra-files` behaviour. Watch that release-please run end-to-end:
- **Verified at config-load time** — release.yml run [26264757024](https://github.com/igorlg/cfn-handler/actions/runs/26264757024) (the run triggered by this PR's own merge) executed `release-please-action` against the new `release-please-config.json`. Output: `✔ Splitting 5 commits by path` → `✔ Considering: 8 commits` → `✔ No user facing commits found since a2192f7d... - skipping`. Zero parse errors / zero warnings about the `extra-files` block. This proves the config syntax is valid and release-please loaded it.
- **Verified by the local validator** — `tests/release-please/validate-uv-lock-updater.js`, run as step `[3/7]` of `just gha-pre-release`, asserts that `release-please@17.3.0`'s `GenericToml` updater applied to our actual `uv.lock` produces a surgical 1-line diff (positive test) and that the bare jsonpath without `.value` does NOT match (negative test, confirms the workaround is necessary).
- **Pending next `feat:`/`fix:` merge** — confirmation that the release PR diff includes the `uv.lock` self-version line. This is the only piece that requires real production traffic; the underlying behaviour is validated by both the config-load proof above and the local validator's surgical-diff assertion. No further code change can advance this from "pending" to "verified" without a real version-bumping commit on `main`.

## 11. Archive

- [ ] 11.1 **Deferred — blocked on 10.2.** After step 10.2 confirms in production, run `openspec archive release-please-sync-uv-lock`
- [ ] 11.2 **Deferred — blocked on 10.2.** Verify the MODIFIED requirement merges into `openspec/specs/ci-infrastructure/spec.md` correctly (replaces the old `--frozen` requirement)
- [x] 11.1 After step 10.2 confirms in production, run `openspec archive release-please-sync-uv-lock`. **Done**: archived in this branch (`chore/cleanup-openspec-changes`). The "wait for the next feat:/fix:" guard from the original task description is relaxed in light of the dual-evidence verification in 10.2 (config-load proof in production + local surgical-diff validator).
- [x] 11.2 Verify the MODIFIED requirement merges into `openspec/specs/ci-infrastructure/spec.md` correctly (replaces the old `--frozen` requirement). **Done**: `openspec archive --yes` performs the sync and validates; verified during archive.
64 changes: 57 additions & 7 deletions openspec/specs/ci-infrastructure/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Every workflow file SHALL declare a top-level `permissions: contents: read` (or

Releases SHALL be driven entirely by Conventional Commits parsed by `release-please-action`. Merging the auto-generated release PR with the title `chore(main): release X.Y.Z` SHALL trigger a chain of jobs in `release.yml` that: tag `vX.Y.Z`; build wheel and sdist; upload artifacts to a GitHub Release; and publish to PyPI via OIDC Trusted Publishing in the `pypi` environment. The Trusted Publisher binding SHALL be parameterised by repository, workflow filename (`release.yml`), and environment name (`pypi`); no PyPI API token is held anywhere.

`release-please-action` SHALL authenticate using a short-lived installation token minted from a dedicated GitHub App (registered to the repository owner, installed only on this repository, granted exactly `Contents: write` and `Pull requests: write` permissions), NOT the default `GITHUB_TOKEN`. The minting step SHALL run before `release-please-action` and pass the resulting token via the action's `token` input. This requirement exists because GitHub blocks PRs opened with the default `GITHUB_TOKEN` from triggering downstream workflow runs (anti-recursion); without an App-minted token, required status checks on the release PR never fire and the PR cannot be merged. The App's numeric ID SHALL be stored as a repository **variable** (`vars.RELEASE_PLEASE_APP_ID`, non-sensitive); its private key SHALL be stored as a repository **secret** (`secrets.RELEASE_PLEASE_PRIVATE_KEY`).

#### Scenario: A `feat:` commit lands on main
- **WHEN** a contributor merges a PR with title `feat: <description>` to `main`
- **THEN** `release-please-action` opens (or updates) a release PR proposing a minor version bump
Expand All @@ -73,17 +75,65 @@ Releases SHALL be driven entirely by Conventional Commits parsed by `release-ple
- **WHEN** the publisher binding does not match (wrong workflow filename, wrong environment, wrong repo)
- **THEN** `pypa/gh-action-pypi-publish` fails the OIDC exchange and the publish step errors with a 403 from PyPI; the wheel/sdist artifacts on the GitHub Release are unaffected

### Requirement: Lockfile drift policy: `uv sync --frozen` in CI; manual `uv lock` after dependency edits

CI SHALL install dependencies via `uv sync --frozen --only-group <group>` rather than `--locked`. This is required because `release-please-action` bumps the local project's `version` in `pyproject.toml` but cannot also run `uv lock` to refresh the corresponding entry in `uv.lock`; under `--locked`, that drift breaks every CI run on `main` immediately after a release-please merge. `--frozen` still installs exactly the dependency versions recorded in `uv.lock`; only the local project's own version is read from the current `pyproject.toml`. Contributors SHALL run `uv lock` manually after editing `pyproject.toml` dependencies and commit the resulting `uv.lock` in the same PR.
#### Scenario: Release PR opened with the App's token triggers required checks
- **WHEN** `release-please-action` opens or updates a release PR using the GitHub App installation token
- **THEN** the four required status checks on `main`'s branch protection (`CI passed`, `analyze (python)`, `review dependencies`, `ensure SHA-pinned actions`) all run automatically against the release PR's head, with no manual unblocks needed

#### Scenario: App credential is missing or invalid
- **WHEN** `vars.RELEASE_PLEASE_APP_ID` is unset, or `secrets.RELEASE_PLEASE_PRIVATE_KEY` is missing or expired
- **THEN** the `Mint App installation token` step fails before `release-please-action` runs; the release pipeline halts loudly rather than silently falling back to `GITHUB_TOKEN` (which would produce non-triggering PRs)

### Requirement: Lockfile drift policy: release-please syncs `uv.lock`; CI uses `--locked`

Releases SHALL keep the project's self-version entry in `uv.lock`
synchronised with `pyproject.toml`. `release-please-config.json`
SHALL list `uv.lock` as an `extra-files` entry of type `toml`,
matching the project's package via the jsonpath
`$.package[?(@.name.value=='cfn-handler')].version`. The `.value`
accessor descends into release-please's TOML AST node shape (which
exposes string nodes as `{value, kind}` rather than bare strings)
and is required as a workaround for
[googleapis/release-please#2455](https://github.com/googleapis/release-please/issues/2455);
the upstream tracker is
[#2561](https://github.com/googleapis/release-please/issues/2561) and
the proposed fix is PR
[#2693](https://github.com/googleapis/release-please/pull/2693).

CI SHALL install dependencies via `uv sync --locked --only-group <group>`
in `ci.yml` and `examples-lint.yml`. Local development via `.envrc`
SHALL also use `--locked` so contributors see the same diagnostics
locally that CI produces. With release-please syncing `uv.lock`'s
self-version entry, the lockfile and `pyproject.toml` move in
lockstep on every release; with `--locked` enforced, contributors who
edit `pyproject.toml` dependencies without running `uv lock` are
caught immediately by CI rather than discovered at a later
maintenance step.

#### Scenario: Post-release CI on main
- **WHEN** the release PR is merged, bumping `pyproject.toml` from `0.0.0` to `1.0.0` without an accompanying `uv.lock` update
- **THEN** the next `ci.yml` run on `main` succeeds, because `--frozen` does not check pyproject/lockfile consistency on the local project's own version

- **WHEN** the release PR is merged, bumping `pyproject.toml` from
`X.Y.Z` to `X.Y.Z+1` *and* the corresponding `[[package]] name =
"cfn-handler"` `version` in `uv.lock` (because the `extra-files`
entry directs release-please to update both)
- **THEN** the next `ci.yml` run on `main` succeeds because `uv sync
--locked` finds `pyproject.toml` and `uv.lock` consistent

#### Scenario: A contributor adds a new runtime dependency without re-locking
- **WHEN** a PR adds a dependency to `pyproject.toml` but does not include the resulting `uv.lock` change
- **THEN** CI does not catch this (a known tradeoff of `--frozen`); the contributor is responsible per `.github/CONTRIBUTING.md`. The PR review process is the gate.

- **WHEN** a PR adds a dependency to `pyproject.toml` but does not
include the resulting `uv.lock` change
- **THEN** `uv sync --locked` fails the PR's CI run with
`The lockfile at uv.lock needs to be updated, but --locked was
provided`, surfacing the missed re-lock before review

#### Scenario: release-please's TOML AST shape regresses upstream

- **WHEN** a future release-please bump changes the parser such that
the `.value` accessor no longer matches the cfn-handler package
- **THEN** the next release PR ships with `uv.lock`'s self-version
unchanged; the post-merge `ci.yml` run on `main` fails under
`--locked` and the failure is loud, fast, and bisectable to the
release PR commit

### Requirement: Codecov upload from a single matrix entry

Expand Down