diff --git a/openspec/changes/ci-release-please-app-auth/proposal.md b/openspec/changes/archive/2026-05-22-ci-release-please-app-auth/proposal.md similarity index 100% rename from openspec/changes/ci-release-please-app-auth/proposal.md rename to openspec/changes/archive/2026-05-22-ci-release-please-app-auth/proposal.md diff --git a/openspec/changes/ci-release-please-app-auth/specs/ci-infrastructure/spec.md b/openspec/changes/archive/2026-05-22-ci-release-please-app-auth/specs/ci-infrastructure/spec.md similarity index 100% rename from openspec/changes/ci-release-please-app-auth/specs/ci-infrastructure/spec.md rename to openspec/changes/archive/2026-05-22-ci-release-please-app-auth/specs/ci-infrastructure/spec.md diff --git a/openspec/changes/ci-release-please-app-auth/tasks.md b/openspec/changes/archive/2026-05-22-ci-release-please-app-auth/tasks.md similarity index 68% rename from openspec/changes/ci-release-please-app-auth/tasks.md rename to openspec/changes/archive/2026-05-22-ci-release-please-app-auth/tasks.md index 93960e4..a533dca 100644 --- a/openspec/changes/ci-release-please-app-auth/tasks.md +++ b/openspec/changes/archive/2026-05-22-ci-release-please-app-auth/tasks.md @@ -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. diff --git a/openspec/changes/release-please-sync-uv-lock/.openspec.yaml b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/.openspec.yaml similarity index 100% rename from openspec/changes/release-please-sync-uv-lock/.openspec.yaml rename to openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/.openspec.yaml diff --git a/openspec/changes/release-please-sync-uv-lock/design.md b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/design.md similarity index 100% rename from openspec/changes/release-please-sync-uv-lock/design.md rename to openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/design.md diff --git a/openspec/changes/release-please-sync-uv-lock/proposal.md b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/proposal.md similarity index 100% rename from openspec/changes/release-please-sync-uv-lock/proposal.md rename to openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/proposal.md diff --git a/openspec/changes/release-please-sync-uv-lock/specs/ci-infrastructure/spec.md b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/specs/ci-infrastructure/spec.md similarity index 100% rename from openspec/changes/release-please-sync-uv-lock/specs/ci-infrastructure/spec.md rename to openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/specs/ci-infrastructure/spec.md diff --git a/openspec/changes/release-please-sync-uv-lock/tasks.md b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/tasks.md similarity index 73% rename from openspec/changes/release-please-sync-uv-lock/tasks.md rename to openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/tasks.md index ca00b98..b5fea3c 100644 --- a/openspec/changes/release-please-sync-uv-lock/tasks.md +++ b/openspec/changes/archive/2026-05-22-release-please-sync-uv-lock/tasks.md @@ -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. diff --git a/openspec/specs/ci-infrastructure/spec.md b/openspec/specs/ci-infrastructure/spec.md index e3adf84..6965011 100644 --- a/openspec/specs/ci-infrastructure/spec.md +++ b/openspec/specs/ci-infrastructure/spec.md @@ -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: ` to `main` - **THEN** `release-please-action` opens (or updates) a release PR proposing a minor version bump @@ -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 ` 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 ` +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