|
| 1 | +# Spec 0002: Update GitHub Actions Pipeline |
| 2 | + |
| 3 | +## Status: Complete |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## 1. Background & Motivation |
| 8 | + |
| 9 | +The current `run-full-ci.yml` workflow has become outdated in several ways: |
| 10 | + |
| 11 | +- Action versions are significantly behind latest releases |
| 12 | +- Python versions are wrong (test job uses 3.11, lint job uses 3.12; project requires ≥3.14) |
| 13 | +- uv is installed via `pip install uv` rather than the official `astral-sh/setup-uv` action |
| 14 | +- No uv package cache — each run reinstalls all packages from scratch |
| 15 | +- Node.js version 20 is behind the current LTS (Node 22) |
| 16 | +- No top-level `permissions` restriction (security best practice) |
| 17 | +- Inconsistent virtual environment strategy between `lint` and `test` jobs |
| 18 | +- No `concurrency` group to cancel redundant runs on the same branch |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 2. Research Findings |
| 23 | + |
| 24 | +### 2.1 Action Version Inventory |
| 25 | + |
| 26 | +| Action | Current | Latest | Notes | |
| 27 | +| ---------------------- | ------- | ---------- | ----------------------------------------- | |
| 28 | +| `actions/checkout` | v4 | **v6.0.2** | v5 and v6 both released since v4 | |
| 29 | +| `actions/setup-python` | v5 | **v6.2.0** | v6 is a breaking change (Node 24 runtime) | |
| 30 | +| `actions/setup-node` | v4 | **v6.4.0** | v5 and v6 both released since v4 | |
| 31 | +| `pre-commit/action` | v3.0.1 | **v3.0.1** | Already at latest | |
| 32 | + |
| 33 | +### 2.2 Python Version Status (as of May 2026) |
| 34 | + |
| 35 | +| Version | Status | Notes | |
| 36 | +| -------- | ------------------- | ----------------------------------------------------------------------------- | |
| 37 | +| 3.15 | Pre-release | Planned release October 2026 | |
| 38 | +| **3.14** | **Bugfix (active)** | Released October 7, 2025. Latest: 3.14.4 (April 7, 2026). **Project target.** | |
| 39 | +| 3.13 | Bugfix | Released October 7, 2024 | |
| 40 | +| 3.12 | Security-only | Released October 2, 2023 — used in current lint job | |
| 41 | +| 3.11 | Security-only | Released October 24, 2022 — used in current test job | |
| 42 | + |
| 43 | +The project's `pyproject.toml` declares `requires-python = ">=3.14"`, and `ty.toml` sets `python-version = "3.14"`. Both the `lint` and `test` jobs must use **Python 3.14**. |
| 44 | + |
| 45 | +### 2.3 Node.js LTS Status |
| 46 | + |
| 47 | +Node.js 20 (used in the lint job) is in "maintenance LTS" as of May 2026. Node.js **22** is the current active LTS. The workflow should target Node 22. |
| 48 | + |
| 49 | +### 2.4 uv GitHub Actions Best Practices |
| 50 | + |
| 51 | +The official Astral documentation recommends using the `astral-sh/setup-uv` action instead of manually running `pip install uv`. Benefits: |
| 52 | + |
| 53 | +- Automatically adds uv to PATH |
| 54 | +- Built-in uv package **cache** support (`enable-cache: true`) — avoids reinstalling packages on every run |
| 55 | +- Cross-platform support |
| 56 | +- Version pinning support |
| 57 | +- Eliminates the need for `pip install uv` + manual venv creation steps |
| 58 | +- Supports setting `UV_SYSTEM_PYTHON: 1` env var to install into the system Python (avoids needing to activate a venv in CI) |
| 59 | + |
| 60 | +Example pattern (from uv docs): |
| 61 | + |
| 62 | +```yaml |
| 63 | +- name: Install uv |
| 64 | + uses: astral-sh/setup-uv@v8 |
| 65 | + with: |
| 66 | + enable-cache: true |
| 67 | +``` |
| 68 | +
|
| 69 | +The latest version of `astral-sh/setup-uv` is **v8** (as of the uv 0.11.8 documentation). |
| 70 | + |
| 71 | +### 2.5 GitHub Actions Security Best Practices |
| 72 | + |
| 73 | +**Minimal permissions principle**: GitHub recommends setting a top-level `permissions: read-all` (or `permissions: {}`) to restrict the default `GITHUB_TOKEN` to read-only, then explicitly grant write permissions only to the specific jobs that need them. Currently only the `release` job has an explicit `permissions` block; the `lint` and `test` jobs implicitly have broad write permissions. |
| 74 | + |
| 75 | +**Action pinning**: GitHub's security guidance recommends pinning actions to full commit SHAs (most secure) or at minimum to a specific major version tag. Using floating tags like `@v4` risks supply chain attacks if a tag is moved. Version tags from trusted/verified publishers (e.g., GitHub's own `actions/` org) are low-risk, but SHA pinning is the gold standard for high-security environments. |
| 76 | + |
| 77 | +**Dependabot for actions**: GitHub recommends enabling Dependabot version updates for GitHub Actions to automatically keep action versions current via PRs. |
| 78 | + |
| 79 | +**Concurrency groups**: Best practice is to add a `concurrency` block to cancel in-progress runs when a new push arrives on the same branch/ref, reducing wasted CI minutes. |
| 80 | + |
| 81 | +### 2.6 Current Workflow Issues Summary |
| 82 | + |
| 83 | +1. **Wrong Python versions**: lint uses 3.12, test uses 3.11; project requires ≥3.14 |
| 84 | +2. **Outdated action versions**: checkout@v4, setup-python@v5, setup-node@v4 |
| 85 | +3. **Manual uv install**: `pip install uv` is the old way; no cache means slow CI |
| 86 | +4. **Inconsistent venv strategy**: lint job creates and activates a venv; test job uses `--system`. Both should use the same approach. |
| 87 | +5. **Old Node.js version**: Node 20 is maintenance LTS; Node 22 is current LTS |
| 88 | +6. **No workflow-level `permissions`**: All jobs have implicit broad permissions |
| 89 | +7. **No concurrency group**: Redundant pushes waste CI minutes |
| 90 | +8. **No uv cache**: Package installation time is not amortized across runs |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## 3. Update Plan |
| 95 | + |
| 96 | +### Goal |
| 97 | + |
| 98 | +Bring the CI pipeline up to date with current best practices: correct Python/Node versions, latest action versions, proper uv integration with caching, and improved security posture via permission restrictions and a concurrency group. |
| 99 | + |
| 100 | +### Jobs to Update |
| 101 | + |
| 102 | +#### 3.1 `lint` Job |
| 103 | + |
| 104 | +- Update `actions/checkout` from `v4` → `v6` |
| 105 | +- Update `actions/setup-python` from `v5` → `v6`, change `python-version` from `"3.12"` → `"3.14"` |
| 106 | +- Update `actions/setup-node` from `v4` → `v6`, change `node-version` from `20` → `22` |
| 107 | +- Replace the manual `pip install uv` + `uv venv venv` + `source venv/bin/activate` + `uv pip install` pattern with `astral-sh/setup-uv@v8` with `enable-cache: true` |
| 108 | +- Use `uv pip install -r requirements-dev.txt --system` (consistent with test job, no venv activation needed) |
| 109 | +- `pre-commit/action@v3.0.1` stays as-is (already latest) |
| 110 | + |
| 111 | +#### 3.2 `test` Job |
| 112 | + |
| 113 | +- Update `actions/checkout` (implicit from `pre-commit/action`) — the test job itself doesn't use checkout directly, but add it for clarity and consistency |
| 114 | +- Actually: the test job _does_ use `actions/checkout@v4` (via the `uses: actions/checkout@v4` step) — update to `v6` |
| 115 | +- Update `actions/setup-python` from `v5` → `v6`, change `python-version` from `"3.11"` → `"3.14"` |
| 116 | +- Fix the job name label comment (currently says "Set up Python 3.11" — update to "3.14") |
| 117 | +- Replace `pip install uv` + `uv pip install --system` with `astral-sh/setup-uv@v8` + `uv pip install --system` (with cache enabled) |
| 118 | + |
| 119 | +#### 3.3 `release` Job |
| 120 | + |
| 121 | +- Update `actions/checkout` from `v4` → `v6` |
| 122 | +- No other changes needed (no Python/Node needed here) |
| 123 | + |
| 124 | +### 3.4 Workflow-Level Changes |
| 125 | + |
| 126 | +- Add `permissions: read-all` at the top of the workflow (before `jobs:`) to restrict the default `GITHUB_TOKEN` to read-only across all jobs. The `release` job already has its own `permissions: contents: write` block which continues to override this. |
| 127 | + |
| 128 | + ```yaml |
| 129 | + permissions: read-all |
| 130 | + ``` |
| 131 | + |
| 132 | +- Add a `concurrency` block to cancel in-progress runs when a new push arrives on the same ref, reducing wasted CI minutes: |
| 133 | + |
| 134 | + ```yaml |
| 135 | + concurrency: |
| 136 | + group: ${{ github.workflow }}-${{ github.ref }} |
| 137 | + cancel-in-progress: true |
| 138 | + ``` |
| 139 | + |
| 140 | +### 3.5 Action SHA Pinning |
| 141 | + |
| 142 | +GitHub's security guidance recommends pinning third-party actions to their full commit SHA rather than a mutable version tag. This prevents supply chain attacks where a tag is silently moved to malicious code. |
| 143 | + |
| 144 | +For each action used, look up the commit SHA corresponding to the target version tag and pin to it, keeping the version tag as an inline comment for readability: |
| 145 | + |
| 146 | +```yaml |
| 147 | +# Example pattern |
| 148 | +uses: actions/checkout@<full-commit-sha> # v6.0.2 |
| 149 | +``` |
| 150 | + |
| 151 | +All five actions used in this workflow require SHA pinning: |
| 152 | + |
| 153 | +| Action | Target Version | Commit SHA | |
| 154 | +| ---------------------- | -------------- | ------------------------------------------ | |
| 155 | +| `actions/checkout` | v6.0.2 | `de0fac2e4500dabe0009e67214ff5f5447ce83dd` | |
| 156 | +| `actions/setup-python` | v6.2.0 | `a309ff8b426b58ec0e2a45f0f869d46889d02405` | |
| 157 | +| `actions/setup-node` | v6.4.0 | `48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e` | |
| 158 | +| `pre-commit/action` | v3.0.1 | `2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd` | |
| 159 | +| `astral-sh/setup-uv` | v8.1.0 | `08807647e7069bb48b6ef5acd8ec9567f424441b` | |
| 160 | + |
| 161 | +SHAs were verified via the GitHub API (`/repos/{owner}/{repo}/git/ref/tags/{tag}`). |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## 4. Reference: Action Latest Versions |
| 166 | + |
| 167 | +| Action | Latest Version | URL | |
| 168 | +| ---------------------- | -------------- | -------------------------------------------------- | |
| 169 | +| `actions/checkout` | v6.0.2 | <https://github.com/actions/checkout/releases> | |
| 170 | +| `actions/setup-python` | v6.2.0 | <https://github.com/actions/setup-python/releases> | |
| 171 | +| `actions/setup-node` | v6.4.0 | <https://github.com/actions/setup-node/releases> | |
| 172 | +| `pre-commit/action` | v3.0.1 | <https://github.com/pre-commit/action/releases> | |
| 173 | +| `astral-sh/setup-uv` | v8 (v8.1.0) | <https://github.com/astral-sh/setup-uv/releases> | |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## 5. Risk Assessment |
| 178 | + |
| 179 | +| Change | Risk Level | Notes | |
| 180 | +| ------------------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------- | |
| 181 | +| Python 3.11 → 3.14 | Medium | Project already targets 3.14, but CI may expose previously hidden 3.14-specific issues. Tests pass locally. | |
| 182 | +| `actions/checkout` v4 → v6 | Low | Breaking change in v6 is credentials storage location; doesn't affect this workflow's use case. | |
| 183 | +| `actions/setup-python` v5 → v6 | Low | v6 breaking change is Node 24 runtime upgrade — transparent to Python consumers. | |
| 184 | +| `actions/setup-node` v4 → v6 | Low | v6 breaking change limits automatic caching to npm only — our workflow doesn't rely on automatic caching. | |
| 185 | +| `astral-sh/setup-uv` adoption | Low | Well-maintained official action from Astral. Simplifies setup; `--system` flag usage unchanged. | |
| 186 | +| `permissions: read-all` | Very Low | Only affects `GITHUB_TOKEN` scope; this workflow doesn't use it in lint/test. Release job keeps its explicit override. | |
| 187 | +| Concurrency group | Very Low | Only cancels in-progress runs when a new push arrives. Does not affect final push results. | |
| 188 | +| Node 20 → 22 | Low | Used only for Tailwind CSS / npm tooling in pre-commit. No breaking changes expected. | |
| 189 | +| SHA pinning all actions | Very Low | Increases security; no functional change. SHAs must be verified against the correct release tags. | |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## 6. Detailed Implementation Checklist |
| 194 | + |
| 195 | +### Pre-work |
| 196 | + |
| 197 | +- [x] Review the current workflow file end-to-end to catch any additional issues not covered in this spec |
| 198 | +- [x] Verify that Python 3.14 is available on `ubuntu-latest` GitHub-hosted runners (it is — GitHub Actions runners include Python 3.14 via `actions/setup-python@v6`) |
| 199 | +- [x] Verify that `astral-sh/setup-uv@v8` is the correct latest major version (confirmed from uv docs showing `v8.1.0` as latest) |
| 200 | +- [x] Confirm Node 22 is the current LTS (verified — Node 22 became LTS in October 2024) |
| 201 | + |
| 202 | +### Workflow-Level Changes |
| 203 | + |
| 204 | +- [x] Add top-level `permissions: read-all` block (before `jobs:`) to restrict default `GITHUB_TOKEN` scope |
| 205 | +- [x] Add `concurrency` block to cancel in-progress runs for the same `github.ref` |
| 206 | + |
| 207 | +### `lint` Job |
| 208 | + |
| 209 | +- [x] Update `actions/checkout` from `@v4` → `@v6` |
| 210 | +- [x] Update `actions/setup-python` from `@v5` → `@v6` |
| 211 | +- [x] Change `python-version` in setup-python from `"3.12"` → `"3.14"` |
| 212 | +- [x] Update `actions/setup-node` from `@v4` → `@v6` |
| 213 | +- [x] Change `node-version` in setup-node from `20` → `22` |
| 214 | +- [x] Replace the "Install python dependencies" step with `astral-sh/setup-uv@v8` with `enable-cache: true` |
| 215 | +- [x] Remove manual `pip install uv`, `uv venv venv`, and `source venv/bin/activate` commands |
| 216 | +- [x] Install python dependencies using `uv pip install -r requirements-dev.txt --system` (no venv needed) |
| 217 | +- [x] Verify `pre-commit/action@v3.0.1` remains unchanged (already latest) |
| 218 | + |
| 219 | +### `test` Job |
| 220 | + |
| 221 | +- [x] Update `actions/checkout` from `@v4` → `@v6` |
| 222 | +- [x] Update `actions/setup-python` step name label from `"Set up Python 3.11"` → `"Set up Python 3.14"` (cosmetic, but accurate) |
| 223 | +- [x] Update `actions/setup-python` from `@v5` → `@v6` |
| 224 | +- [x] Change `python-version` in setup-python from `"3.11"` → `"3.14"` |
| 225 | +- [x] Replace the "Install dependencies" step: swap `pip install uv` for `astral-sh/setup-uv@v8` with `enable-cache: true` |
| 226 | +- [x] Keep `uv pip install -r requirements-dev.txt --system` (the `--system` flag remains correct here) |
| 227 | + |
| 228 | +### `release` Job |
| 229 | + |
| 230 | +- [x] Update `actions/checkout` from `@v4` → `@v6` |
| 231 | +- [x] Verify `permissions: contents: write` is still present (it is — this overrides the new top-level `read-all`) |
| 232 | +- [x] No other changes needed |
| 233 | + |
| 234 | +### SHA Pinning |
| 235 | + |
| 236 | +- [x] Look up the commit SHA for `actions/checkout@v6.0.2` on GitHub and pin |
| 237 | +- [x] Look up the commit SHA for `actions/setup-python@v6.2.0` on GitHub and pin |
| 238 | +- [x] Look up the commit SHA for `actions/setup-node@v6.4.0` on GitHub and pin |
| 239 | +- [x] Look up the commit SHA for `pre-commit/action@v3.0.1` on GitHub and pin |
| 240 | +- [x] Confirm the SHA for `astral-sh/setup-uv@v8.1.0` (documented in uv docs as `08807647e7069bb48b6ef5acd8ec9567f424441b`) and pin |
| 241 | +- [x] Add the version tag as an inline comment on each pinned line, e.g. `@<sha> # v6.0.2` |
| 242 | + |
| 243 | +### Post-work Validation |
| 244 | + |
| 245 | +> **Note**: All git and remote-push validation steps are to be performed manually by the developer after implementation. No mutating git actions are performed by the agent. |
| 246 | + |
| 247 | +- [ ] Run `pre-commit run --all-files` locally to confirm nothing broke |
| 248 | +- [ ] Commit and push to a non-main branch to trigger the CI pipeline |
| 249 | +- [ ] Verify all three jobs (`lint`, `test`, `release`) pass successfully |
| 250 | +- [ ] Confirm the correct Python version (3.14) is reported in CI logs |
| 251 | +- [ ] Confirm uv cache is populated and subsequent runs show cache hits |
| 252 | +- [ ] Confirm the `concurrency` group works by pushing two commits in quick succession |
0 commit comments