Migrate pipeline from CircleCI to GitHub Actions#143
Conversation
phelma
left a comment
There was a problem hiding this comment.
Code Review: #143 — Migrate pipeline from CircleCI to GitHub Actions
Verdict: COMMENT
A faithful, plan-conformant CircleCI → GitHub Actions cutover. Reviewed through five lenses (correctness, security, safety, standards, code quality). The diff matches the Variant A family plan's authoritative YAML and Rakefile prescriptions in both directions — every required change is present and nothing beyond the plan was altered. It is notably security-conscious. No in-scope defects were found. Every substantive lens finding either challenges a decision the plan documents as deliberate (surfaced below as plan concerns — for a future revisit of the plan, not blocking) or is a false positive against the reviewing models' knowledge cutoff.
False positives (disproven — not counted)
queue: max"invalid concurrency key" (correctness + standards, main.yaml:54 & :99): incorrect. The plan §4.2 specifiesqueue: maxand cites its GitHub Actions GA on 2026-05-07; the feature post-dates the models' Jan-2026 cutoff. The diff reproduces the plan verbatim; the key is valid. Not a defect.- Prerelease double
.preversion encoding: verified correct — a validGem::Versionthat sorts below any full release.
Plan concerns (documented-deliberate — not blocking)
- 🟡 PR prerelease job runs unreviewed PR-branch code with release-grade secrets — the deliberate D8 PR-CI prerelease publish; same-repo-collaborator trust boundary accepted by the plan (forks/Dependabot guarded out).
- 🔵 Dependabot auto-merge accepts any update type — documented deliberate under D3.
- 🔵 Prerelease publishes before a push that can fail non-fast-forward — §1 inherited-hazard parity (no
git pull, mirrors oldprerelease.sh). - 🔵 Abandoned release approval freezes all main CI — inherent to the deliberate §4.2
mainconcurrency group. - 🔵 Split
git push/git push --tags— follows the plan's one-command-per-step principle. - 🔵 Decommission incomplete until host sweep — CircleCI project disable / deploy-key deletion are out-of-scope per §1.
- 🔵 Notify guard / merge job name / no skip-ci-check differ from sibling
rake_slack— all documented Variant A decisions (§3, §4.1, D4). - 🔵
RakeGithubblock three responsibilities, duplicated arg list, magic channel IDs — all target the plan's verbatim §4.4 code (§4.4: do not restructure the Rakefile).
Strengths
- ✅ User data (PR title/number/status) passed via env, not
${{ }}-interpolated — closes script injection. - ✅ Dependabot gating on immutable
user.login, not spoofablegithub.actor; prerelease requires same-repo head. - ✅
gh pr merge --match-head-commit "$HEAD_SHA"closes the post-checks TOCTOU. - ✅ Least-privilege permissions; explicit
timeout-minutes. - ✅ Provisioning refuses to upload git-crypt ciphertext as a secret and fails fast on a missing token.
- ✅
prerelease:publishrestoresversion.rb+ removes the gem in anensureblock; namespaced version can't collide with main.
Independent §4 plan-conformance check
Verified both directions — job graphs, byte-identical gpg key move, all Rakefile changes, gemspec dep swap, lockfile (rake_git 0.2.0 / rake_git_crypt 0.4.0 / rake_github 0.17.0, no prerelease gems), README, and all §4.7 deletions/trims present and correct. version:bump[minor] and the omission of documentation:update correctly mirror the repo's old release.sh. No violations; nothing beyond the plan changed.
Review generated by /accelerator:review-pr
| env: | ||
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} | ||
|
|
||
| prerelease: |
There was a problem hiding this comment.
🟡 Security — plan concern (D8, not blocking)
The prerelease job checks out the PR head and runs ./go tasks defined by the same PR's Rakefile/gemspec after exposing ENCRYPTION_PASSPHRASE and the git-crypt-decrypted RubyGems credential. Forks/Dependabot are guarded out, but any same-repo collaborator could alter prerelease:publish to exfiltrate secrets. This is the deliberate D8 PR-CI prerelease feature; the collaborator trust boundary is accepted by the plan. Worth revisiting fleet-wide post-migration (approval environment or OIDC trusted publishing).
| # --match-head-commit fails the merge if a commit landed after checks passed | ||
| # [skip ci] stops the merge commit triggering a release build | ||
| # PR title via env, never interpolated | ||
| run: gh pr merge --merge --match-head-commit "$HEAD_SHA" "$PR_URL" --subject "$PR_TITLE [skip ci]" |
There was a problem hiding this comment.
🔵 Security — plan concern (D3, not blocking)
No patch/minor/major filter on the auto-merge. Documented deliberate under D3 as parity; the [skip ci] subject stops the merge triggering a release, so updates ship with the next human release. A dependabot/fetch-metadata update-type gate is reasonable post-migration hardening.
| - name: Release | ||
| run: ./go release | ||
| - name: Push commits | ||
| run: git push |
There was a problem hiding this comment.
🔵 Safety — plan concern (not blocking)
./go release publishes to RubyGems before this git push, and the prerelease job (unlike release) does no git pull first — mirroring the old prerelease.sh. A concurrent [skip ci] merge could make the push fail non-fast-forward after the gem is already published. This is the §1 publish-before-push inherited hazard. A follow-up could git pull --ff-only before publishing.
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| environment: release | ||
| concurrency: |
There was a problem hiding this comment.
🔵 Safety — plan concern (not blocking)
The release job holds the shared main concurrency group while awaiting approval; with cancel-in-progress: false + queue: max, subsequent pushes queue behind it indefinitely if the approval is forgotten. Inherent to the deliberate §4.2 concurrency design. Consider a runbook note and/or a "release awaiting approval" Slack alert so a frozen queue is surfaced.
| - name: Push commits | ||
| run: git push | ||
| - name: Push tags | ||
| run: git push --tags |
There was a problem hiding this comment.
🔵 Safety — plan concern (not blocking)
git push and git push --tags are separate steps after the publish; if one succeeds and the other fails the release record is partially consistent. Follows the plan's one-command-per-step principle. A single git push --follow-tags would make them atomic — a post-migration consideration.
| end | ||
|
|
||
| RakeGithub.define_repository_tasks( | ||
| RakeGithub.define_repository_tasks( # rubocop:disable Metrics/BlockLength |
There was a problem hiding this comment.
🔵 Code quality — plan concern (not blocking)
The block inlines token resolution, passphrase validation, and secrets/environments config, hence the rubocop:disable Metrics/BlockLength. Extracting resolve_github_token / read_unlocked_passphrase helpers would improve testability — but this is the plan's verbatim §4.4 code and §4.4 says not to restructure the Rakefile. Revisit in the plan, not this diff.
| desc 'Build and push a namespaced pre-release to RubyGems ' \ | ||
| '(PR CI only; no bump, no tag, no commit, no push)' | ||
| task :publish, %i[pr_number run_number run_attempt] do |_, args| | ||
| %i[pr_number run_number run_attempt].each do |name| |
There was a problem hiding this comment.
🔵 Code quality — plan concern (not blocking)
%i[pr_number run_number run_attempt] is written twice (task signature and validation loop). Binding it to a local would keep them in sync. This is the plan's verbatim §4.4 step 8 code.
| { when: { actor: 'dependabot[bot]' }, | ||
| channel: 'C03N711HVDG', format: :failure }, # builds-dependabot | ||
| { when: { outcome: 'success' }, | ||
| channel: 'C023XUE76GH', format: :success }, # builds |
There was a problem hiding this comment.
🔵 Code quality — plan concern (not blocking)
Raw channel IDs (C023XUE76GH appears three times) annotated by trailing comments that can drift. Named locals would self-document. This is the plan's verbatim §4.4 step 4 code; IDs are org constants routed by ID per §3.
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
There was a problem hiding this comment.
⚪ FALSE POSITIVE (disproven) — no change required
Three lenses flagged queue: max as an invalid concurrency key. It is valid: a real GitHub Actions concurrency option (GA 2026-05-07), specified verbatim by the family plan §4.2. The flag post-dates the reviewing models' knowledge cutoff. Recorded so the concern is visible but explicitly disproven.
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.
PR-CI prerelease publish (deliberate, permanent)
pr.yamlhas aprereleasejob that publishes a namespaced pre-release ofthis gem to RubyGems from the PR branch — a permanent CI feature, not
migration-only. This is a deliberate deviation from CircleCI (which published
nothing pre-merge): it proves the publish path before merge instead of
discovering it broken on
main. The version is<committed-version>.pr<PR>.<run>.<attempt>(via the newprerelease:publishRakefile task), so it can never collide with
main'sversion:bump[pre]sequence; the task builds the gem and pushes it straight to RubyGems, then
restores
version.rb, so nothing is committed, tagged, or pushed(
gem releaseis not used — it aborts on the uncommitted version rewrite).The job is skipped for fork
and Dependabot PRs (they hold no secrets), and
merge-pull-requestdoes notdepend on it. PR pre-release versions accumulate permanently on RubyGems —
accepted.
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.
🏭 This PR was opened by Foundry, Atomic's AI software development
factory. Implementation, review, and fixes are performed by AI agents;
merges happen automatically once the review and checks gates pass.
This task migrates a Ruby gem's CI from CircleCI to GitHub Actions.
migratemigrate-gem2026-07-22T15-58-21-187Zatomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T15-58-21-187Z