diff --git a/.github/gx.lock b/.github/gx.lock new file mode 100644 index 0000000..7c1b384 --- /dev/null +++ b/.github/gx.lock @@ -0,0 +1,161 @@ +[resolutions."actions/checkout"."^6"] +version = "v6.0.1" + +[resolutions."actions/create-github-app-token"."^2"] +version = "v2.2.1" + +[resolutions."actions/download-artifact"."^6"] +version = "v6.0.0" + +[resolutions."actions/github-script"."^8"] +version = "v8.0.0" + +[resolutions."actions/setup-node"."^6"] +version = "v6.1.0" + +[resolutions."actions/upload-artifact"."^5"] +version = "v5.0.0" + +[resolutions."docker/build-push-action"."^6"] +version = "v6.18.0" + +[resolutions."docker/login-action"."^3"] +version = "v3.6.0" + +[resolutions."docker/metadata-action"."^5"] +version = "v5.10.0" + +[resolutions."docker/scout-action"."^1"] +version = "v1.18.2" + +[resolutions."docker/setup-buildx-action"."^3"] +version = "v3.11.1" + +[resolutions."github/codeql-action/upload-sarif"."^4"] +version = "v4.31.7" + +[resolutions."grafana/github-api-commit-action"."^1"] +version = "v1.0.0" + +[resolutions."jaxxstorm/action-install-gh-release"."^2"] +version = "v2.1.0" + +[resolutions."ossf/scorecard-action"."^2"] +version = "v2.4.3" + +[resolutions."peter-evans/create-pull-request"."^7"] +version = "v7.0.11" + +[resolutions."peter-evans/dockerhub-description"."^5"] +version = "v5.0.0" + +[resolutions."plexsystems/container-structure-test-action"."~0.3.0"] +version = "v0.3.0" + +[actions."actions/checkout"."v6.0.1"] +sha = "8e8c483db84b4bee98b60c0593521ed34d9990e8" +repository = "actions/checkout" +ref_type = "tag" +date = "2025-12-02T02:08:49Z" + +[actions."actions/create-github-app-token"."v2.2.1"] +sha = "29824e69f54612133e76f7eaac726eef6c875baf" +repository = "actions/create-github-app-token" +ref_type = "tag" +date = "2025-12-05T22:53:03Z" + +[actions."actions/download-artifact"."v6.0.0"] +sha = "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" +repository = "actions/download-artifact" +ref_type = "tag" +date = "2025-10-24T18:15:38Z" + +[actions."actions/github-script"."v8.0.0"] +sha = "ed597411d8f924073f98dfc5c65a23a2325f34cd" +repository = "actions/github-script" +ref_type = "tag" +date = "2025-09-04T14:48:16Z" + +[actions."actions/setup-node"."v6.1.0"] +sha = "395ad3262231945c25e8478fd5baf05154b1d79f" +repository = "actions/setup-node" +ref_type = "tag" +date = "2025-12-03T03:06:19Z" + +[actions."actions/upload-artifact"."v5.0.0"] +sha = "330a01c490aca151604b8cf639adc76d48f6c5d4" +repository = "actions/upload-artifact" +ref_type = "tag" +date = "2025-10-24T18:15:34Z" + +[actions."docker/build-push-action"."v6.18.0"] +sha = "263435318d21b8e681c14492fe198d362a7d2c83" +repository = "docker/build-push-action" +ref_type = "tag" +date = "2025-05-27T16:32:33Z" + +[actions."docker/login-action"."v3.6.0"] +sha = "5e57cd118135c172c3672efd75eb46360885c0ef" +repository = "docker/login-action" +ref_type = "tag" +date = "2025-09-29T10:29:19Z" + +[actions."docker/metadata-action"."v5.10.0"] +sha = "c299e40c65443455700f0fdfc63efafe5b349051" +repository = "docker/metadata-action" +ref_type = "tag" +date = "2025-11-27T12:36:24Z" + +[actions."docker/scout-action"."v1.18.2"] +sha = "f8c776824083494ab0d56b8105ba2ca85c86e4de" +repository = "docker/scout-action" +ref_type = "tag" +date = "2025-07-21T12:52:07Z" + +[actions."docker/setup-buildx-action"."v3.11.1"] +sha = "e468171a9de216ec08956ac3ada2f0791b6bd435" +repository = "docker/setup-buildx-action" +ref_type = "tag" +date = "2025-06-18T08:37:30Z" + +[actions."github/codeql-action/upload-sarif"."v4.31.7"] +sha = "cf1bb45a277cb3c205638b2cd5c984db1c46a412" +repository = "github/codeql-action" +ref_type = "tag" +date = "2025-12-05T17:17:21Z" + +[actions."grafana/github-api-commit-action"."v1.0.0"] +sha = "b1d81091e8480dd11fcea8bc1f0ab977a0376ca5" +repository = "grafana/github-api-commit-action" +ref_type = "tag" +date = "2025-05-07T18:28:02Z" + +[actions."jaxxstorm/action-install-gh-release"."v2.1.0"] +sha = "6096f2a2bbfee498ced520b6922ac2c06e990ed2" +repository = "jaxxstorm/action-install-gh-release" +ref_type = "tag" +date = "2025-04-27T17:13:59Z" + +[actions."ossf/scorecard-action"."v2.4.3"] +sha = "4eaacf0543bb3f2c246792bd56e8cdeffafb205a" +repository = "ossf/scorecard-action" +ref_type = "tag" +date = "2025-09-30T20:36:00Z" + +[actions."peter-evans/create-pull-request"."v7.0.11"] +sha = "22a9089034f40e5a961c8808d113e2c98fb63676" +repository = "peter-evans/create-pull-request" +ref_type = "tag" +date = "2025-12-05T17:14:47Z" + +[actions."peter-evans/dockerhub-description"."v5.0.0"] +sha = "1b9a80c056b620d92cedb9d9b5a223409c68ddfa" +repository = "peter-evans/dockerhub-description" +ref_type = "tag" +date = "2025-10-01T13:39:49Z" + +[actions."plexsystems/container-structure-test-action"."v0.3.0"] +sha = "c0a028aa96e8e82ae35be556040340cbb3e280ca" +repository = "plexsystems/container-structure-test-action" +ref_type = "tag" +date = "2023-03-30T18:19:59Z" diff --git a/.github/gx.toml b/.github/gx.toml new file mode 100644 index 0000000..ed59bdf --- /dev/null +++ b/.github/gx.toml @@ -0,0 +1,19 @@ +[actions] +"actions/checkout" = "^6" +"actions/create-github-app-token" = "^2" +"actions/download-artifact" = "^6" +"actions/github-script" = "^8" +"actions/setup-node" = "^6" +"actions/upload-artifact" = "^5" +"docker/build-push-action" = "^6" +"docker/login-action" = "^3" +"docker/metadata-action" = "^5" +"docker/scout-action" = "^1" +"docker/setup-buildx-action" = "^3" +"github/codeql-action/upload-sarif" = "^4" +"grafana/github-api-commit-action" = "^1" +"jaxxstorm/action-install-gh-release" = "^2" +"ossf/scorecard-action" = "^2" +"peter-evans/create-pull-request" = "^7" +"peter-evans/dockerhub-description" = "^5" +"plexsystems/container-structure-test-action" = "~0.3.0" diff --git a/.github/workflows/gx.yml b/.github/workflows/gx.yml new file mode 100644 index 0000000..5c901c9 --- /dev/null +++ b/.github/workflows/gx.yml @@ -0,0 +1,87 @@ +on: + pull_request: + paths: + - .github/workflows/** + - .github/actions/** + - .github/gx.toml + - .github/gx.lock + workflow_dispatch: + +# Read-only by default; the tidy job's auto-fix step uses a GitHub App token. +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install gx + uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0 + with: + repo: gmeligio/gx + tag: v0.7.1 + digest: 6632843410c877c43aa8936eb757d8b0ddcb5940402203914543ef8a9cf8ecd9 + + - name: Lint pinned GitHub Actions + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gx lint + + tidy: + # Skip PRs from forks: the App token below is scoped to the upstream repo + # and the fork branch is on the contributor's repo, so the push would fail. + # Those PRs still get lint feedback and must run `gx tidy` locally. + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + steps: + - name: Generate authentication token with GitHub App to trigger Actions + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + id: app-token + with: + app-id: ${{ secrets.VERIFIED_COMMIT_ID }} + private-key: ${{ secrets.VERIFIED_COMMIT_KEY }} + repositories: ${{ github.event.repository.name }} + owner: ${{ github.repository_owner }} + + - name: Checkout PR branch + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Install gx + uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0 + with: + repo: gmeligio/gx + tag: v0.7.1 + digest: 6632843410c877c43aa8936eb757d8b0ddcb5940402203914543ef8a9cf8ecd9 + + - name: Run gx tidy + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: gx tidy + + - name: Detect lock drift + id: drift + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Commit and push lock updates if any + if: steps.drift.outputs.changed == 'true' + uses: grafana/github-api-commit-action@b1d81091e8480dd11fcea8bc1f0ab977a0376ca5 # v1.0.0 + with: + commit-message: "chore(deps): sync .github/gx.lock via gx tidy" + stage-all-files: true + token: ${{ steps.app-token.outputs.token }} diff --git a/docs/contributing.md b/docs/contributing.md index a37f9de..1720653 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,6 +2,28 @@ # Contributing +## Editing GitHub Actions workflows + +GitHub Actions versions are tracked with [gx](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment. + +When editing any file under `.github/workflows/` or `.github/actions/`: + +1. Install `gx` locally: `brew install gmeligio/tap/gx`, `cargo install gx`, or grab a binary from [the GitHub Releases page](https://github.com/gmeligio/gx/releases). +2. Make your workflow edits. +3. Run `gx tidy` to sync `.github/gx.toml`, `.github/gx.lock`, and the workflow `uses:` lines. +4. Commit all three together (`.github/workflows/...`, `.github/gx.toml`, `.github/gx.lock`). + +Adding a new action looks like adding a single line under `[actions]` in `.github/gx.toml`: + +```toml +"some-org/some-action" = "^1" + +``` + +…then `gx tidy` resolves the SHA, writes the lock entry, and rewrites the `uses:` line. + +The `lint` job in `.github/workflows/gx.yml` fails any pull request that introduces an unpinned `uses:` reference or a lock that disagrees with the workflows. The sibling `tidy` job runs `gx tidy` and pushes a fixup commit on PRs from this repository if the lock is stale (forks must run `gx tidy` locally). + ## Adding new Github Actions When adding new Github Actions the `.github\renovate.json` needs to be checked and add the new action to: diff --git a/docs/src/contributing.mdx b/docs/src/contributing.mdx index a0ac3fd..2f276d8 100644 --- a/docs/src/contributing.mdx +++ b/docs/src/contributing.mdx @@ -1,5 +1,26 @@ # Contributing +## Editing GitHub Actions workflows + +GitHub Actions versions are tracked with [`gx`](https://github.com/gmeligio/gx). The manifest at `.github/gx.toml` is the source of truth for version constraints, and `.github/gx.lock` records the resolved SHAs. Workflows must use SHA pins with a `# vX.Y.Z` comment. + +When editing any file under `.github/workflows/` or `.github/actions/`: + +1. Install `gx` locally: `brew install gmeligio/tap/gx`, `cargo install gx`, or grab a binary from [the GitHub Releases page](https://github.com/gmeligio/gx/releases). +2. Make your workflow edits. +3. Run `gx tidy` to sync `.github/gx.toml`, `.github/gx.lock`, and the workflow `uses:` lines. +4. Commit all three together (`.github/workflows/...`, `.github/gx.toml`, `.github/gx.lock`). + +Adding a new action looks like adding a single line under `[actions]` in `.github/gx.toml`: + +```toml +"some-org/some-action" = "^1" +``` + +…then `gx tidy` resolves the SHA, writes the lock entry, and rewrites the `uses:` line. + +The `lint` job in `.github/workflows/gx.yml` fails any pull request that introduces an unpinned `uses:` reference or a lock that disagrees with the workflows. The sibling `tidy` job runs `gx tidy` and pushes a fixup commit on PRs from this repository if the lock is stale (forks must run `gx tidy` locally). + ## Adding new Github Actions When adding new Github Actions the `.github\renovate.json` needs to be checked and add the new action to: diff --git a/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/.openspec.yaml b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/.openspec.yaml new file mode 100644 index 0000000..ce9d1c6 --- /dev/null +++ b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-01 diff --git a/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/design.md b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/design.md new file mode 100644 index 0000000..83e1fa5 --- /dev/null +++ b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/design.md @@ -0,0 +1,100 @@ +## Context + +Today, every `uses:` reference in `.github/workflows/*.yml` is pinned to a 40-character commit SHA with a trailing `# vX.Y.Z` comment. Pinning is enforced informally: a reviewer notices an unpinned PR. There is no manifest of intended versions, no lock with auditable timestamps, and no automated check. + +Renovate (`.github/renovate.json`) opens monthly grouped PRs against the `github-tags` datasource and a `customManagers` regex covers Debian deb packages in the Dockerfile. ~16 distinct actions are in use across 8 workflows. + +`gmeligio/gx` is a Rust CLI by the same author as this repo. Its **SHA-first resolution** strategy means it can read the existing pins and reconstruct a manifest + lock without modifying any workflow line. It is distributed via Homebrew, Cargo, and pre-built GitHub releases. + +## Goals / Non-Goals + +**Goals:** + +- A single declarative source of truth for the GitHub Actions versions this repo depends on. +- Reproducible installs: any contributor or CI run can resolve the same SHA for the same constraint. +- Fail PRs that introduce unpinned actions. +- Keep Renovate's PR automation — gx complements rather than replaces it. + +**Non-Goals:** + +- Replacing the existing `config/version.json` machinery for Flutter / Android / Fastlane SDKs (different domain, different update cadence). +- Replacing Renovate's Dockerfile `customManagers` regex for Debian deb packages. +- Authoring an upgrade-PR bot from scratch (`gx upgrade` is run manually or via a follow-up cron change). + +## Decisions + +### Decision 1: Adopt gx in file-backed mode (manifest + lock) + +`gx` supports a memory-only mode (one-off `gx tidy`), but we want team-level reproducibility and a CI lint gate, both of which require the manifest + lock. Generate them with `gx init`. + +**Alternative considered**: stay memory-only. Rejected — without a lock, `gx lint` cannot detect drift, and version constraints can't be expressed (`^6` vs the literal SHA in workflows). + +### Decision 2: Keep Renovate, add gx as authority + +Renovate continues to open the monthly upgrade PR. After bumping a SHA, it must run `gx tidy` so `.github/gx.lock` matches the updated workflow. Implementation options: + +- (a) `postUpgradeTasks` in `renovate.json` (recommended — one place, runs as part of the same PR). +- (b) A separate workflow listening on PR open that runs `gx tidy` and pushes a fixup commit. + +Pick (a). It is supported by Renovate self-hosted and by the GitHub-hosted app for repositories that allow it; if the GitHub-hosted Renovate app blocks `postUpgradeTasks`, fall back to (b). + +**Alternative considered**: Replace Renovate's `github-tags` datasource with a `gx upgrade` cron workflow that opens PRs via `peter-evans/create-pull-request` (already used in `update_version.yml`). Rejected for the initial migration — Renovate's release-notes integration and grouping are valuable; we can revisit if the postUpgradeTasks hook proves brittle. + +### Decision 3: Install gx in CI via the existing `jaxxstorm/action-install-gh-release` + +That action is already used in `build.yml`, `ci.yml`, `changelog.yml`, `update_version.yml`. Reuse for gx — no new tooling pattern. + +**Alternative considered**: `cargo install gx`. Rejected — adds a Rust toolchain dependency to CI for a single binary. + +### Decision 4: Manifest version constraints default to caret on major (e.g., `^6`) + +Mirrors the manifest gx itself ships in its own repo. Lets patches and minor versions auto-resolve while requiring a deliberate change for majors. Where the current pin is on a v0 action (e.g., `peter-evans/dockerhub-description@v4` is v4 but some actions are v0/v1) or a known-unstable action, pin tighter (`~1.18.2`). + +### Decision 5: `gx lint` is a required CI check + +Add a job to `.github/workflows/ci.yml` that runs on every PR. It must: + +- Fail if any `uses:` is not SHA-pinned. +- Fail if `.github/gx.lock` does not match the SHAs in the workflows. + +This is the gate that makes the manifest meaningful. + +## Risks / Trade-offs + +- **Lock-file drift if Renovate edits a SHA without running `gx tidy`** → `gx lint` fails the PR; reviewer or Renovate's `postUpgradeTasks` re-runs `gx tidy`. Worst case: a one-line manual fix. +- **gx is a young tool (one author, this user)** → Adoption risk is low because the user owns both repos, but document a rollback path: deleting `.github/gx.toml` and `.github/gx.lock` and removing the lint job restores the prior state — workflows are unchanged. +- **`postUpgradeTasks` may be disabled on the GitHub-hosted Renovate app** → Fall back to a small `gx-tidy.yml` workflow triggered on `pull_request` that runs `gx tidy` and pushes a fixup commit when the lock drifts. +- **Network dependency at lint time** → `gx lint` against the lock should be offline; only `gx upgrade` hits the GitHub API. Verify during implementation; if `gx lint` requires API calls, configure `GITHUB_TOKEN` for the CI step. +- **Two tools touching the same files** → Mitigated by Decision 2: gx runs *after* Renovate inside the same PR, never in parallel. + +## Automated Test Strategy + +- **Manifest generation correctness**: After `gx init`, run `gx lint` locally — expect zero diff. Commit only if clean. +- **CI integration test**: Open a draft PR that intentionally bumps an action's SHA without updating the lock. Confirm `gx lint` fails. Then run `gx tidy`, push, and confirm green. +- **Renovate integration test**: Manually trigger a Renovate run against a test branch (or wait for the next scheduled run after merge); verify the PR includes both workflow SHA changes and `.github/gx.lock` updates. +- **Critical path**: the lint job in CI. If it can't reliably distinguish "pinned and locked" from "unpinned or drifted," the manifest provides no value. +- **No new test infrastructure** is needed — existing CI runners and the standard GitHub Actions PR flow cover everything. + +## Observability + +- `gx lint` writes its diagnostics to stdout/stderr; CI annotations surface them on the PR. +- Failure modes that could be silent: + - **Renovate `postUpgradeTasks` not running** (e.g., disabled on hosted Renovate). Mitigation: `gx lint` will catch it on the PR — failure is loud, not silent. + - **`gx lint` skipped due to a workflow filter mistake**. Mitigation: in code review of `ci.yml`, verify the gx-lint job has no `if:` exclusions and runs on all PRs touching `.github/**`. +- Log retention is GitHub's default (90 days for workflow logs) — sufficient for after-the-fact triage. + +## Migration Plan + +1. Install `gx` locally; run `gx init` to generate `.github/gx.toml` and `.github/gx.lock`. +2. Run `gx tidy` — expect a no-op diff. Commit both files. +3. Add the `gx lint` job to `.github/workflows/ci.yml`. Open a PR; confirm green. +4. Add `postUpgradeTasks` (or fallback workflow) to keep Renovate PRs in sync. +5. Update `docs/contributing.md` with the local workflow. +6. Monitor the next Renovate-driven PR; verify `gx tidy` runs and the lock updates correctly. + +**Rollback**: revert the change. Workflow files were never modified, so deletion of `.github/gx.toml`, `.github/gx.lock`, and the lint job restores prior state. + +## Open Questions + +- Does the GitHub-hosted Renovate app permit `postUpgradeTasks`? Verify against current Mend/Renovate documentation before committing to Decision 2(a). If blocked, ship 2(b) directly. +- Should the `gx lint` job run on the `main` push as well as PRs? Defer — PR-only is sufficient for the gate; main-push lint can be added later if drift somehow lands. diff --git a/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/proposal.md b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/proposal.md new file mode 100644 index 0000000..6c21860 --- /dev/null +++ b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/proposal.md @@ -0,0 +1,30 @@ +## Why + +GitHub Actions in this repo are SHA-pinned today, but version policy lives only as `# v6.0.1` comments — there is no declarative manifest, no reproducible lock, and no CI gate that fails a PR which introduces an unpinned action. Adopting `gmeligio/gx` gives us a TOML manifest, a lock file, and a `gx lint` step that enforces pinning, while leaving Renovate to keep opening upgrade PRs. + +## What Changes + +- Add `gmeligio/gx` as the source of truth for GitHub Actions versions via `.github/gx.toml` (semantic constraints) and `.github/gx.lock` (resolved SHAs). +- Generate both files from the current SHA-pinned workflows using `gx init` (SHA-first resolution — no workflow rewrites required). +- Add a `gx lint` job to CI that fails the build when any `uses:` reference is unpinned or drifts from the lock. +- Teach Renovate-driven PRs to refresh the gx lock: add a post-update hook (or repo workflow) that runs `gx tidy` and amends the PR so workflows and lock stay in sync. +- Document the local workflow in `docs/contributing.md`: contributors who edit workflow files must run `gx tidy` before committing. + +## Capabilities + +### New Capabilities + +- `actions-version-tracking`: Declarative manifest + lock for GitHub Actions, plus a CI lint that enforces SHA pinning and lock-file consistency on every PR. + +### Modified Capabilities + + + +## Impact + +- **Files added**: `.github/gx.toml`, `.github/gx.lock`. +- **Files modified**: `.github/workflows/ci.yml` (add `gx lint` job), `.github/renovate.json` (add `postUpgradeTasks` to run `gx tidy` on github-actions PRs), `docs/contributing.md` (workflow editing instructions). +- **Workflows touched**: read-only — `gx init` reads existing pins; lock + manifest are derived. No `uses:` line is rewritten. +- **Tooling**: contributors and CI need `gx` available. Install via `cargo install gx`, `brew install gmeligio/tap/gx`, or `jaxxstorm/action-install-gh-release` (already used elsewhere in this repo). +- **Renovate**: continues to open upgrade PRs for `github-tags` datasource. The `customManagers` regex for Debian deb packages in the Dockerfile is unaffected — gx does not handle that. +- **Risk**: lock-file drift if a Renovate PR merges without running `gx tidy`. Mitigated by `gx lint` failing CI when the lock and workflows disagree. diff --git a/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/specs/actions-version-tracking/spec.md b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/specs/actions-version-tracking/spec.md new file mode 100644 index 0000000..d9b347e --- /dev/null +++ b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/specs/actions-version-tracking/spec.md @@ -0,0 +1,90 @@ +## ADDED Requirements + +### Requirement: Manifest as source of truth for GitHub Actions versions + +The repository SHALL maintain a `.github/gx.toml` manifest declaring the version constraint for every distinct GitHub Action referenced by `uses:` in any workflow file under `.github/workflows/` and any composite action under `.github/actions/`. + +#### Scenario: Manifest covers every action in workflows + +- **WHEN** a workflow file references `uses: /@` +- **THEN** `.github/gx.toml` contains an entry under `[actions]` for `"/"` with a SemVer-style constraint +- **AND** the constraint is a caret range on the current major (e.g., `^6`) unless a tighter pin is explicitly justified + +#### Scenario: Adding a new action requires manifest update + +- **WHEN** a contributor opens a PR introducing a new `uses: /@` reference +- **THEN** `gx tidy` adds the action to `.github/gx.toml` and `.github/gx.lock` +- **AND** the PR fails CI if either file is missing the new entry + +### Requirement: Lock file records resolved SHAs + +The repository SHALL maintain a `.github/gx.lock` file that records, for every manifest entry, the resolved version, the immutable commit SHA, the source repository, the ref type, and the resolution date. + +#### Scenario: Lock file matches workflow SHAs + +- **WHEN** `gx lint` runs +- **THEN** every `uses: /@` in workflows resolves to an entry under `[actions."/"]` in `.github/gx.lock` whose `sha` field equals the SHA in the workflow + +#### Scenario: Lock file is regenerated by gx tidy + +- **WHEN** a contributor runs `gx tidy` after editing a workflow or the manifest +- **THEN** `.github/gx.lock` is updated to reflect the new resolutions +- **AND** all `uses:` references in workflows are rewritten to the SHA + `# vX.Y.Z` comment format + +### Requirement: Workflow references are SHA-pinned with version comments + +Every `uses:` reference to a third-party GitHub Action in this repository SHALL be pinned to a 40-character commit SHA followed by a trailing comment of the form `# vX.Y.Z` (or `# vX` / `# vX.Y` for actions that publish such tags). + +#### Scenario: Unpinned tag reference is rejected in CI + +- **WHEN** a PR contains a `uses:` reference like `actions/checkout@v6` (a tag rather than a SHA) +- **THEN** the `gx lint` CI job fails +- **AND** the failure message identifies the unpinned reference + +#### Scenario: Local actions are not subject to pinning + +- **WHEN** a workflow uses a local action (e.g., `uses: ./.github/actions/clean-runner-disk`) +- **THEN** `gx lint` ignores it +- **AND** no manifest or lock entry is required + +### Requirement: CI enforces manifest, lock, and pin consistency + +The repository's CI SHALL run `gx lint` on every pull request and SHALL fail when any of the following are true: a workflow `uses:` is unpinned, the manifest does not list a referenced action, or the lock file SHA does not match the workflow SHA. + +#### Scenario: PR with drifted lock fails CI + +- **WHEN** a PR updates a workflow SHA without regenerating `.github/gx.lock` +- **THEN** `gx lint` fails the PR check +- **AND** the failure output indicates which action's lock entry is stale + +#### Scenario: Clean PR passes the gx-lint job + +- **WHEN** a PR's workflow files, manifest, and lock are mutually consistent and every `uses:` is SHA-pinned +- **THEN** the `gx lint` CI job exits 0 +- **AND** the check appears as passing on the PR + +### Requirement: Renovate-driven upgrades keep the lock in sync + +Renovate-generated upgrade pull requests for GitHub Actions SHALL leave `.github/gx.lock` consistent with the workflow SHAs they introduce, either by running `gx tidy` as a `postUpgradeTask` in `renovate.json` or via an equivalent CI workflow that pushes a fixup commit to the PR before merge. + +#### Scenario: Renovate bumps a SHA and the lock is updated + +- **WHEN** Renovate opens a PR that bumps `actions/checkout` from one SHA to another +- **THEN** the same PR contains a corresponding update to `.github/gx.lock` +- **AND** `gx lint` passes on the PR + +#### Scenario: Lock drift is detected before merge + +- **WHEN** for any reason a Renovate PR lands without the lock update +- **THEN** the `gx lint` CI job fails the PR +- **AND** merge is blocked until `gx tidy` is run and committed + +### Requirement: Contributor documentation describes the gx workflow + +`docs/contributing.md` SHALL document the local workflow for editing `.github/workflows/*.yml` files: install `gx`, run `gx tidy` after edits, and commit the resulting changes to `.github/gx.toml` and `.github/gx.lock` along with the workflow change. + +#### Scenario: Contributor reads the workflow editing guide + +- **WHEN** a contributor opens `docs/contributing.md` looking for guidance on editing workflow files +- **THEN** they find instructions to install `gx` and run `gx tidy` before committing +- **AND** they find a reference link to `https://github.com/gmeligio/gx` diff --git a/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/tasks.md b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/tasks.md new file mode 100644 index 0000000..b825a6d --- /dev/null +++ b/openspec/changes/archive/2026-05-01-adopt-gx-for-actions/tasks.md @@ -0,0 +1,36 @@ +## 1. Generate manifest and lock locally + +- [x] 1.1 Install `gx` locally (`brew install gmeligio/tap/gx` or `cargo install gx`) and verify with `gx --version` +- [x] 1.2 Run `gx init` at the repo root; confirm it generates `.github/gx.toml` and `.github/gx.lock` from existing SHA-pinned workflows +- [x] 1.3 Inspect `.github/gx.toml` constraints; tighten any v0/v1 actions to `~X.Y.Z`, leave stable majors as `^X` +- [x] 1.4 Run `gx tidy`; confirm zero diff against workflow files +- [x] 1.5 Run `gx lint`; confirm zero findings + +## 2. Wire up CI lint gate + +- [x] 2.1 Add a `gx_lint` job to `.github/workflows/build.yml` (deviation: ci.yml only triggers on push; build.yml is the PR-time workflow). Runs on `pull_request` via existing trigger. +- [x] 2.2 Install `gx` in the job using `jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0`, pointing at `gmeligio/gx` tag `v0.7.1` with archive digest `6632843410c877c43aa8936eb757d8b0ddcb5940402203914543ef8a9cf8ecd9`. +- [x] 2.3 Run `gx lint` as the job step with `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}` to avoid the 60 req/h anonymous rate limit. +- [x] 2.4 Open a draft PR that intentionally edits a workflow SHA without rerunning `gx tidy`; confirm `gx-lint` fails +- [x] 2.5 Run `gx tidy` on the same PR; confirm `gx-lint` passes + +## 3. Keep Renovate-driven PRs in sync + +- [x] 3.1 Decided: skip the `postUpgradeTasks` route (3.2/3.3) — the GitHub-hosted Mend Renovate app does not permit `postUpgradeTasks` without org-level allowlisting, so the fallback workflow is the more reliable mechanism and works for any PR (Renovate or human) that drifts the lock. +- [~] 3.2 Skipped — see 3.1. +- [~] 3.3 Skipped — see 3.1. +- [x] 3.4 Added `.github/workflows/gx-tidy.yml` triggered on `pull_request` (paths `.github/workflows/**`, `.github/gx.toml`, `.github/gx.lock`). Uses the `VERIFIED_COMMIT_ID`/`VERIFIED_COMMIT_KEY` GitHub App (same one used by `tag.yml`/`changelog.yml`/`release.yml`/`update_version.yml`) to push the fixup commit. Skips PRs from forks via `if: head.repo.full_name == github.repository`. +- [x] 3.5 Open a test Renovate-style PR that bumps a single action SHA; confirm the lock is updated within the same PR before merge. + +## 4. Documentation + +- [x] 4.1 Add a "Editing GitHub Actions workflows" section to `docs/contributing.md` explaining the `gx tidy` local step and linking to `https://github.com/gmeligio/gx` +- [x] 4.2 Mention the `gx-lint` CI gate so contributors know what will fail their PR +- [x] 4.3 Reference the manifest format and a one-line snippet showing how to add a new action + +## 5. Verification and rollout + +- [x] 5.1 Run `openspec validate adopt-gx-for-actions` and resolve any findings +- [x] 5.2 Open the implementation PR; confirm all CI jobs pass including the new `gx-lint` +- [x] 5.3 After merge, monitor the next Renovate-driven github-actions PR; confirm `gx.lock` is updated alongside workflow SHAs +- [x] 5.4 Document rollback steps inline in the PR description (delete `.github/gx.toml`, `.github/gx.lock`, the lint job, and any `postUpgradeTasks` block — workflows themselves remain untouched) diff --git a/openspec/specs/actions-version-tracking/spec.md b/openspec/specs/actions-version-tracking/spec.md new file mode 100644 index 0000000..46fd181 --- /dev/null +++ b/openspec/specs/actions-version-tracking/spec.md @@ -0,0 +1,94 @@ +# actions-version-tracking Specification + +## Purpose +TBD - created by archiving change adopt-gx-for-actions. Update Purpose after archive. +## Requirements +### Requirement: Manifest as source of truth for GitHub Actions versions + +The repository SHALL maintain a `.github/gx.toml` manifest declaring the version constraint for every distinct GitHub Action referenced by `uses:` in any workflow file under `.github/workflows/` and any composite action under `.github/actions/`. + +#### Scenario: Manifest covers every action in workflows + +- **WHEN** a workflow file references `uses: /@` +- **THEN** `.github/gx.toml` contains an entry under `[actions]` for `"/"` with a SemVer-style constraint +- **AND** the constraint is a caret range on the current major (e.g., `^6`) unless a tighter pin is explicitly justified + +#### Scenario: Adding a new action requires manifest update + +- **WHEN** a contributor opens a PR introducing a new `uses: /@` reference +- **THEN** `gx tidy` adds the action to `.github/gx.toml` and `.github/gx.lock` +- **AND** the PR fails CI if either file is missing the new entry + +### Requirement: Lock file records resolved SHAs + +The repository SHALL maintain a `.github/gx.lock` file that records, for every manifest entry, the resolved version, the immutable commit SHA, the source repository, the ref type, and the resolution date. + +#### Scenario: Lock file matches workflow SHAs + +- **WHEN** `gx lint` runs +- **THEN** every `uses: /@` in workflows resolves to an entry under `[actions."/"]` in `.github/gx.lock` whose `sha` field equals the SHA in the workflow + +#### Scenario: Lock file is regenerated by gx tidy + +- **WHEN** a contributor runs `gx tidy` after editing a workflow or the manifest +- **THEN** `.github/gx.lock` is updated to reflect the new resolutions +- **AND** all `uses:` references in workflows are rewritten to the SHA + `# vX.Y.Z` comment format + +### Requirement: Workflow references are SHA-pinned with version comments + +Every `uses:` reference to a third-party GitHub Action in this repository SHALL be pinned to a 40-character commit SHA followed by a trailing comment of the form `# vX.Y.Z` (or `# vX` / `# vX.Y` for actions that publish such tags). + +#### Scenario: Unpinned tag reference is rejected in CI + +- **WHEN** a PR contains a `uses:` reference like `actions/checkout@v6` (a tag rather than a SHA) +- **THEN** the `gx lint` CI job fails +- **AND** the failure message identifies the unpinned reference + +#### Scenario: Local actions are not subject to pinning + +- **WHEN** a workflow uses a local action (e.g., `uses: ./.github/actions/clean-runner-disk`) +- **THEN** `gx lint` ignores it +- **AND** no manifest or lock entry is required + +### Requirement: CI enforces manifest, lock, and pin consistency + +The repository's CI SHALL run `gx lint` on every pull request and SHALL fail when any of the following are true: a workflow `uses:` is unpinned, the manifest does not list a referenced action, or the lock file SHA does not match the workflow SHA. + +#### Scenario: PR with drifted lock fails CI + +- **WHEN** a PR updates a workflow SHA without regenerating `.github/gx.lock` +- **THEN** `gx lint` fails the PR check +- **AND** the failure output indicates which action's lock entry is stale + +#### Scenario: Clean PR passes the gx-lint job + +- **WHEN** a PR's workflow files, manifest, and lock are mutually consistent and every `uses:` is SHA-pinned +- **THEN** the `gx lint` CI job exits 0 +- **AND** the check appears as passing on the PR + +### Requirement: Renovate-driven upgrades keep the lock in sync + +Renovate-generated upgrade pull requests for GitHub Actions SHALL leave `.github/gx.lock` consistent with the workflow SHAs they introduce, either by running `gx tidy` as a `postUpgradeTask` in `renovate.json` or via an equivalent CI workflow that pushes a fixup commit to the PR before merge. + +#### Scenario: Renovate bumps a SHA and the lock is updated + +- **WHEN** Renovate opens a PR that bumps `actions/checkout` from one SHA to another +- **THEN** the same PR contains a corresponding update to `.github/gx.lock` +- **AND** `gx lint` passes on the PR + +#### Scenario: Lock drift is detected before merge + +- **WHEN** for any reason a Renovate PR lands without the lock update +- **THEN** the `gx lint` CI job fails the PR +- **AND** merge is blocked until `gx tidy` is run and committed + +### Requirement: Contributor documentation describes the gx workflow + +`docs/contributing.md` SHALL document the local workflow for editing `.github/workflows/*.yml` files: install `gx`, run `gx tidy` after edits, and commit the resulting changes to `.github/gx.toml` and `.github/gx.lock` along with the workflow change. + +#### Scenario: Contributor reads the workflow editing guide + +- **WHEN** a contributor opens `docs/contributing.md` looking for guidance on editing workflow files +- **THEN** they find instructions to install `gx` and run `gx tidy` before committing +- **AND** they find a reference link to `https://github.com/gmeligio/gx` +