feat(ci): adopt gx for GitHub Actions version tracking (#439)

Adopts [`gmeligio/gx`](https://github.com/gmeligio/gx) (0.7.1) as the
source of truth for GitHub Actions versions in this repo:

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eligio Mariño
2026-05-01 17:31:26 +02:00
committed by GitHub
parent 7759bf1b37
commit 846ffd66cb
11 changed files with 662 additions and 0 deletions
+161
View File
@@ -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"
+19
View File
@@ -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"
+87
View File
@@ -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 }}
+22
View File
@@ -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:
+21
View File
@@ -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:
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-01
@@ -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.
@@ -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
<!-- None — no prior specs exist in openspec/specs/. -->
## 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.
@@ -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: <owner>/<repo>@<ref>`
- **THEN** `.github/gx.toml` contains an entry under `[actions]` for `"<owner>/<repo>"` 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: <owner>/<repo>@<ref>` 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: <owner>/<repo>@<sha>` in workflows resolves to an entry under `[actions."<owner>/<repo>"]` 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`
@@ -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)
@@ -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: <owner>/<repo>@<ref>`
- **THEN** `.github/gx.toml` contains an entry under `[actions]` for `"<owner>/<repo>"` 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: <owner>/<repo>@<ref>` 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: <owner>/<repo>@<sha>` in workflows resolves to an entry under `[actions."<owner>/<repo>"]` 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`