Migrate pipeline from CircleCI to GitHub Actions#117
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes the repo’s CI migration from CircleCI to GitHub Actions, moving pipeline execution, secret handling (git-crypt/GPG), releases/prereleases, Slack notifications, and Dependabot auto-merge into GitHub-native workflows while removing CircleCI-era scripts/config.
Changes:
- Added GitHub Actions workflows for PR validation and main-branch prerelease/release automation (including a release environment gate).
- Updated build/provisioning Rake tasks to use
rake_githubsecrets/environments andrake_slack; removed CircleCI/SSH deploy-key provisioning. - Removed CircleCI configuration and CI shell scripts; updated docs and Ruby dependencies accordingly.
Reviewed changes
Copilot reviewed 19 out of 25 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/pr.yaml |
Adds PR checks and Dependabot auto-merge job. |
.github/workflows/main.yaml |
Adds main-branch pipeline for check/test + prerelease and gated release. |
Rakefile |
Replaces CircleCI provisioning with GitHub secrets/environments and Slack notification tasks; adds build task. |
Gemfile |
Swaps CircleCI/SSH rake helpers for Slack helper gem. |
Gemfile.lock |
Updates transitive dependencies per the new bundle resolution and gem changes. |
README.md |
Updates CI key management docs and moves encrypted key path to .github/. |
.github/gpg.private.enc |
Adds encrypted CI GPG key under .github/. |
.circleci/config.yml |
Removes CircleCI pipeline configuration. |
scripts/ci/** |
Removes CircleCI CI helper scripts/steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
| - name: Prerelease | ||
| run: ./go "version:bump[pre]" && ./go release | ||
| - name: Push release commit | ||
| run: git push && git push --tags |
| - name: Pull latest main | ||
| # Approval can land long after the run starts; release publishes main | ||
| # as of approval time, not the tested SHA — parity with the old | ||
| # release.sh, which also pulled. prerelease.sh never pulled, so the | ||
| # prerelease job deliberately has no pull. | ||
| run: git pull |
| - name: Release | ||
| run: ./go "version:bump[minor]" && ./go release | ||
| - name: Push release commit | ||
| run: git push && git push --tags |
| - name: Merge pull request | ||
| run: gh pr merge --merge "$PR_URL" | ||
| env: | ||
| PR_URL: ${{ github.event.pull_request.html_url }} | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| # Operator's ambient auth — the stored PAT is gone (see the cutover PR's | ||
| # deliberate-decisions list). | ||
| t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip } |
phelma
left a comment
There was a problem hiding this comment.
Code Review: #117 - Migrate pipeline from CircleCI to GitHub Actions
Verdict: APPROVE (posted as COMMENT — GitHub does not allow approving one's own PR)
A disciplined, plan-conformant cutover. All five lenses (correctness, security, safety, standards, code quality) found the diff faithful to the Variant A family plan: workflows match the plan's §4.1/§4.2 shapes with correct per-repo parameterisation (version:bump[minor] mirroring the deleted release.sh, no documentation:update), the Rakefile/Gemfile/README changes match §4.4–§4.6, and decommission (§4.7) is complete. Every substantive finding either challenges a decision the plan or PR description documents as deliberate (recorded as plan concerns for post-migration follow-up) or was disproven on verification. Zero in-scope defects.
Verified False Positives
- "
queue: maxis not valid concurrency syntax" (flagged by correctness 🔴, safety 🟡, code-quality 🔵) — disproven. GitHub shipped larger concurrency queues on 2026-05-07;queue: maxwithcancel-in-progress: falseis the documented deployment pattern (keeps up to 100 pending runs, FIFO), exactly as the workflow comment claims. Sources: GitHub changelog, concurrency docs.
Cross-Cutting Themes (plan concerns — post-migration follow-up, not blockers)
- Inherited publish-pipeline hazards (security, safety, correctness) — ungated prerelease publish on every main push, publish-before-push ordering inside
./go release, and release-at-approval-time pulling untested SHAs. All documented in the plan §1 parity list / PR description as deliberate. Post-migration candidates: RubyGems Trusted Publishing (OIDC), push-before-publish, release the tested SHA. - Dependabot runs and secret stores (correctness, security) — dependabot-triggered runs read the Dependabot secret store, so
SLACK_BOT_TOKENresolves empty and thebuilds-dependabotrouting is silently dead until issue 19 provisions the org-wide Dependabot store. Explicitly accepted in the plan.
Strengths
- ✅ Workflow YAML matches the plan's authoritative shapes, including top-level least-privilege
permissions:with per-job elevation, andcontinue-on-errornotify steps so Slack outages can never fail a build. - ✅ Head commit message passed via env var in the skip-ci check — avoids the classic GHA command-injection vector.
- ✅ pr.yaml never unlocks git-crypt; fork/dependabot runs cannot reach the passphrase or the secrets it protects.
- ✅ Stored PAT, CircleCI API token, and CI SSH deploy key pair removed in favour of ambient operator auth and the ephemeral
GITHUB_TOKEN. - ✅ Full releases gated behind the reviewed
releaseenvironment; release step correctly parameterised to mirror the old release.sh rather than blindly copying the plan template'spatch. - ✅ Decommission thorough: no CircleCI references remain in scope; the encrypted GPG key move is a pure rename; commit message matches org convention.
General Findings (all plan concerns)
- 🟡 Security: ungated
prereleasejob holds full publish capability — any push to main yields runtime access to the RubyGems key via the singleENCRYPTION_PASSPHRASE. Documented parity; consider OIDC Trusted Publishing post-migration. - 🔵 Security: CircleCI remains a live credential holder (passphrase env var, deploy key) until the deferred sweep — rotate
ENCRYPTION_PASSPHRASEonce CircleCI is disabled. - 🔵 Security: committed encrypted GPG key uses
openssl -md sha1single-iteration KDF (inherited) — re-encrypt with-pbkdf2 -iter 600000at next rotation. - 🔵 Safety:
./go releasepublishes before pushing the bump commit — a failed push leaves registry/repo inconsistent (documented D5 parity; add a recovery runbook note). - 🔵 Safety: release job pulls main at approval time, so delayed approval can publish untested commits (documented parity).
- 🔵 Safety: rollback to CircleCI now requires recovering git-crypted credentials from history (recoverable at ee37453) — note the path in the migration issue until the first successful gated release.
- 🔵 Correctness:
skip-ci-checkduplicates native[skip ci]handling andGITHUB_TOKEN-push semantics — plan keeps it as belt-and-braces (D4). - 🔵 Code quality: RubyGems credential-copy shell duplicated across jobs and the fleet — a
repository:configure_rubygems_credentialsrake task would restore the "logic lives in the build system" convention. - 🔵 Code quality: Slack routing rules (hard-coded channel IDs) will be copy-pasted into ~103 Rakefiles — consider a shared default routing table in
rake_slacklater. - 🔵 Standards: plan §4.5 assumes gemspec-managed dev deps; this repo declares them in the Gemfile and the PR correctly applied the equivalent edit — worth adding to the plan's non-disqualifying deviations list.
Full analysis persisted to meta/reviews/prs/117-review-1.md.
Review generated by /accelerator:review-pr
| merge-pull-request: | ||
| needs: [check, test, build] | ||
| runs-on: ubuntu-latest | ||
| if: github.actor == 'dependabot[bot]' |
There was a problem hiding this comment.
🟡 Security — plan concern (not a blocker; plan §4.1 mandates this exact gate)
github.actor reflects who triggered the event, not the PR author, so a write-access account pushing to a dependabot branch could get code auto-merged to main without review — feeding the auto-publishing prerelease pipeline. For post-migration fleet-wide hardening: gate on github.event.pull_request.user.login == 'dependabot[bot]' (optionally alongside the actor check), and consider dependabot/fetch-metadata to verify the PR is a genuine dependabot update.
| continue-on-error: true | ||
| run: ./go "slack:notify[${{ job.status }}]" | ||
| env: | ||
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} |
There was a problem hiding this comment.
🔵 Correctness — plan concern (explicitly accepted; Dependabot secret store deferred to issue 19)
Dependabot-triggered pull_request runs read the Dependabot secrets store, not the Actions store, so secrets.SLACK_BOT_TOKEN resolves empty for exactly the runs the builds-dependabot routing targets — and continue-on-error: true hides the failure. Noting so the gap stays tracked: the routing is dead code until issue 19 provisions the org-wide Dependabot store.
| } | ||
| # Operator's ambient auth — the stored PAT is gone (see the cutover PR's | ||
| # deliberate-decisions list). | ||
| t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip } |
There was a problem hiding this comment.
🔵 Code quality — plan concern (snippet is plan §4.4 verbatim)
ENV.fetch('GITHUB_TOKEN') { %x(gh auth token).strip } degrades silently: if gh is missing or logged out it yields an empty string, and the failure surfaces later as an opaque GitHub API 401 from rake_github. A plan-level improvement: validate the resolved token and raise a clear "set GITHUB_TOKEN or run gh auth login" error when empty.
| if: needs.skip-ci-check.outputs.should_skip_ci != 'true' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔵 Security — plan concern (D6 documents tracking asdf_install@v1; actions/checkout@v4 is plan YAML verbatim)
These jobs handle the git-crypt passphrase and RubyGems credentials while referencing actions by mutable tags. SHA-pinning third-party actions (with a version comment) is cheap insurance against the tag-repointing attack pattern (cf. tj-actions/changed-files). Fleet-wide post-migration hardening candidate.
| # deliberate-decisions list). | ||
| t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip } | ||
| # Actions store only: nothing in pr.yaml unlocks git-crypt, so dependabot | ||
| # runs never need the passphrase. |
There was a problem hiding this comment.
🔵 Correctness — plan concern (rules are the plan's exact Ruby)
The bare { actor: 'dependabot[bot]' } failure rule also matches successful dependabot runs; correct routing relies on rake_slack's first-match semantics and the ordering of this list. Making the failure rule explicit (outcome: 'failure' alongside the actor match) would remove the ordering assumption — a plan-level tweak.
Cutover to GitHub Actions per the Variant A family plan (gem pilot).
Includes decommission — merging this PR completes the repo's migration.
releaseenvironment gate.github/rake_slack; dependabot auto-merge jobrake_githubsecrets/environments;rake_circle_cidropped.circleci/,scripts/ci/, the CI SSH deploykey pair and its
keys:deploy/deploy_keysprovisioning, and the storedCircleCI/GitHub API credentials (
config/secrets/{circle_ci,github}/)Deliberate decisions (not defects)
This cutover reproduces the CircleCI pipeline's behaviour, warts included;
fixing inherited hazards is post-migration work. In particular:
./go releasepublishes to RubyGems before the version-bump commit ispushed — pre-existing ordering inside the untouched release logic.
mainwith no approval gate; onlyfull releases are gated (
environment: release).merge does not trigger a release build — on CircleCI the merge commit
carried
[skip ci], so this matches. Updates ship with the nexthuman-triggered release.
releasejob pullsmainat approval time, so a delayed approvalpublishes main as it stands then, not the SHA this run tested — parity with
the old
release.sh(which also pulled;prerelease.shdid not, so theprerelease job has no pull).
asdf_install@v1is our own action (infrablocks/github-actions); we arehappy tracking its major version tag.
build system (
./go/rake) and CI stays lean — it just triggers tasks andsupplies secrets/context.
Gemfile.lockcarries transitive major bumps — the unavoidable resolutionof the targeted
bundle lock --update, not scope creep.autocorrects existing code (e.g.
Style/ArgumentsForwarding) — requiredby the
library:checkverification gate, not drive-by refactoring.pipeline:prepare) authenticates with the operator's ambientghlogin (GITHUB_TOKENfallback) instead of a stored PAT — a deliberateparity deviation; the stored token in
config/secrets/github/config.yamlis deleted with the rest of the CircleCI-era credentials.
Do not merge manually — the pipeline merges once checks are green.
Disabling the CircleCI project and deleting the
CircleCIdeploy key aredeferred to the end-of-migration sweep.