Migrate pipeline from CircleCI to GitHub Actions#226
Conversation
phelma
left a comment
There was a problem hiding this comment.
Code Review: #226 - Migrate pipeline from CircleCI to GitHub Actions
Verdict: COMMENT
This is a faithful, tightly plan-conformant Variant A cutover: the workflow YAML, Rakefile edits, gemspec/lockfile swap, GPG-key move, README retitling and CircleCI decommission all match the family plan, and the repo-specific parameterisations (version:bump[minor] with no documentation:update, matching the old release.sh; test:unit as the unit task) are correct. Security posture is strong — least-privilege permissions, untrusted commit message routed through an env var, injection-safe gh pr merge. The findings below are dominated by plan concerns (issues inherited verbatim from the plan's authoritative YAML that should be revisited fleet-wide, not defects of this diff) plus two trivial in-scope suggestions.
Cross-Cutting Themes
queue: maxis not a valid GitHub Actionsconcurrencykey (flagged by: correctness, code-quality) — GHAconcurrencysupports onlygroupandcancel-in-progress;queueis silently ignored (or may fail schema validation), so the documented "keep every queued run" serialisation does not hold. This is copied verbatim from the family plan §4.2, so it is a plan concern affecting every migrated repo — the plan's YAML should be corrected — not a defect introduced by this PR.
Strengths
- ✅ skip-ci-check reads
github.event.head_commit.messageinto an env var and matches with bash[[ ]]— the correct defence against Actions script injection. - ✅ Least-privilege permissions: top-level
contents: read, write elevated only on prerelease/release and the dependabot merge job. - ✅ Dependabot merge passes the PR URL via env, gates on the non-spoofable
github.actorplusneeds: [check, test, build]. - ✅ RubyGems credential copied only after git-crypt unlock and locked to
chmod 0600;pull_request(notpull_request_target) keeps secrets away from fork PRs. - ✅ Require and dev-dependency lists kept strictly alphabetical; dead CircleCI machinery removed wholesale.
General Findings
- 🔵 Safety: Neither
prereleasenorreleasesetstimeout-minutes, so a hung step holds themainconcurrency slot for up to GHA's 6-hour default and queues every later run behind it. The plan's YAML omits timeouts entirely — a plan concern worth raising fleet-wide. - 🔵 Security (suggestion, no action): prerelease/release unlock git-crypt and copy the RubyGems credential in the same job that runs
./go release. Acceptable under the currentpush: [main]trigger model; would become an exfiltration path only if that trigger were ever widened to untrusted refs.
Review generated by /accelerator:review-pr
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
There was a problem hiding this comment.
🔴 Correctness / Code Quality — PLAN CONCERN, not a defect of this diff
The top-level concurrency block sets queue: max, but native GitHub Actions concurrency only supports group and cancel-in-progress. queue is not a recognised key, and the accompanying comment's claim that it "keeps every queued run" does not match GHA semantics: with cancel-in-progress: false, GHA allows exactly one pending run per group and cancels any older pending run when a newer one queues — there is no built-in full-queue retention.
Impact: At best the key is silently ignored and the intended anti-race serialisation does not fully hold — close-together merges can drop a queued prerelease. At worst a stricter workflow parser rejects the unknown key and the entire main pipeline fails to run.
This is reproduced verbatim from the Variant A family plan §4.2, so it applies to every migrated repo. It should be fixed in the plan rather than as a one-off deviation here; it does not block this plan-conformant diff. If genuine full-queue retention is required, it must be implemented outside the concurrency block.
| # 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 |
There was a problem hiding this comment.
🔵 Safety — PLAN CONCERN
The release job runs git pull before Set CI git author, with no pull strategy (--ff-only/--rebase). If main has diverged at approval time, git pull may attempt a merge commit with no git identity set, or fail on divergent branches. Serialisation via the concurrency group makes divergence unlikely, and the job fails safe (before publishing) if it happens.
This ordering and the bare git pull come straight from the plan's §4.2 YAML, so it is a plan concern (consider git pull --ff-only, and/or moving author config earlier, fleet-wide), not a defect of this diff.
| # Failures go to builds, not team-dev (org default), to keep noise | ||
| # out of a popular channel while this pipeline beds in. | ||
| { when: {}, | ||
| channel: 'C023XUE76GH', format: :failure } # builds |
There was a problem hiding this comment.
🔵 Standards
The catch-all routing rule's trailing comment uses a single space before # (format: :failure } # builds), whereas the four preceding rules use two spaces, and the plan's authoritative YAML aligns this comment with two spaces. A genuine (trivial) deviation from the plan. Suggest: format: :failure } # builds.
There was a problem hiding this comment.
Pull request overview
Migrates the repository’s CI/CD pipeline from CircleCI to GitHub Actions, replacing CircleCI-specific scripts/config with GitHub Actions workflows and updating Rake provisioning/tasks to manage GitHub secrets/environments and Slack notifications.
Changes:
- Added GitHub Actions workflows for PR and main branch runs (checks/tests/builds, prerelease/release flow, dependabot auto-merge).
- Removed CircleCI configuration and related CI shell scripts/tooling.
- Updated Rake tasks and gem dependencies to drop CircleCI/SSH tooling and add Slack/GitHub Actions-oriented provisioning.
Reviewed changes
Copilot reviewed 19 out of 25 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/ci/steps/test.sh | Removed CircleCI test step wrapper. |
| scripts/ci/steps/release.sh | Removed CircleCI release step wrapper. |
| scripts/ci/steps/prerelease.sh | Removed CircleCI prerelease step wrapper. |
| scripts/ci/steps/merge-pull-request.sh | Removed CircleCI dependabot merge step wrapper. |
| scripts/ci/steps/build.sh | Removed CircleCI build/check step wrapper. |
| scripts/ci/common/install-slack-deps.sh | Removed CircleCI runner package install helper. |
| scripts/ci/common/install-gpg-key.sh | Removed CircleCI GPG decrypt/import helper. |
| scripts/ci/common/install-git-crypt.sh | Removed CircleCI git-crypt install helper. |
| scripts/ci/common/install-asdf.sh | Removed CircleCI asdf install helper. |
| scripts/ci/common/install-asdf-dependencies.sh | Removed CircleCI asdf dependency install helper. |
| scripts/ci/common/configure-rubygems.sh | Removed CircleCI RubyGems credential setup helper. |
| scripts/ci/common/configure-git.sh | Removed CircleCI git identity configuration helper. |
| scripts/ci/common/configure-asdf.sh | Removed CircleCI asdf plugin configuration helper. |
| README.md | Updated CI key management docs to reference .github/gpg.private.enc. |
| Rakefile | Switched provisioning from CircleCI to GitHub secrets/environments; added Slack tasks and a build task. |
| rake_dependencies.gemspec | Replaced CircleCI/SSH dev dependencies with rake_slack. |
| Gemfile.lock | Updated dependency graph consistent with CI migration/dependency changes. |
| .github/workflows/pr.yaml | New PR workflow running check/test/build and dependabot auto-merge. |
| .github/workflows/main.yaml | New main workflow running check/test and prerelease + gated release. |
| .github/gpg.private.enc | Added encrypted GPG key blob under .github/ for CI unlock flow. |
| .circleci/config.yml | Removed CircleCI pipeline configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
| # 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 } |
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.