Repository navigation
Migrate pipeline from CircleCI to GitHub Actions - #212
Conversation
phelma
left a comment
There was a problem hiding this comment.
Code Review: #212 - Migrate pipeline from CircleCI to GitHub Actions
Verdict: COMMENT
This CircleCI → GitHub Actions cutover is a high-quality, faithful implementation of the Variant A family plan. Across correctness, security, safety, standards and code-quality lenses no critical or major in-scope defects were found; the diff tracks the plan's authoritative templates closely and its notable safety/security properties (env-passed facts, guarded prerelease job, --match-head-commit, namespaced prerelease versions, git-crypt ciphertext guard) are correct. The single genuine in-scope finding is a cosmetic divergence from the prescribed Rakefile shape; the remaining observations all challenge documented-deliberate plan decisions and are recorded as plan concerns for a human to revisit.
Strengths
- ✅ All user-controllable values (PR title/URL/SHA, PR/run facts) are passed via env vars, never interpolated into
run:— closing the${{ }}injection vector. - ✅ Least-privilege permissions:
contents: readdefault with per-job escalation. - ✅ The secret-bearing
prereleasejob is guarded to same-repo, non-Dependabot PRs. - ✅ Dependabot auto-merge keys off the immutable
user.loginand uses--match-head-commit. - ✅
prerelease:publishrestoresversion.rband removes the built gem in anensureblock on every path. - ✅ Provisioning fails fast on a missing/locked passphrase, detecting the
\x00GITCRYPTmagic rather than uploading ciphertext as a live secret. - ✅ Alphabetical require/dependency ordering preserved; comments self-contained.
General Findings
- 🔵 Standards:
resolve_github_tokenwas extracted to a top-level method rather than the plan's prescribed inline block (§4.4 step 3 says "do not restructure anything else"). Functionally equivalent; the guiding comment now sits above a method that no longer contains the raise it references.
Plan Concerns (documented-deliberate — not blocking)
These challenge decisions the family plan / PR description document as deliberate. Recorded so a human can revisit the plan, but they do not block a plan-conformant diff.
- 🔵 Security: The
prereleasePR-CI job exposes RubyGems push credentials and the git-crypt passphrase to same-repo PR-authored code — a wider pre-merge secret surface than CircleCI. Documented D8 trade-off (prove the publish path pre-merge). - 🔵 Security:
SLACK_BOT_TOKENreaches PR-authoredslack:notifycode on same-repo PRs — the standardon: pull_requestsame-repo trust boundary; the notify steps are plan-prescribed. - 🔵 Code Quality:
rescue Errno::ENOENTaround`gh auth token`is unlikely to fire (backticks run via/bin/sh, so a missingghyields exit 127 + empty stdout, notErrno::ENOENT). Handled incidentally by the empty-token fallthrough. This is the plan-prescribed code (§4.4 step 3). - 🔵 Code Quality: The
%i[pr_number run_number run_attempt]list is duplicated between the task signature and the validation loop. Verbatim from the plan template (§4.4 step 8). - 🔵 Correctness: The prerelease version compounds an already-
.prebase (2.16.0.pre.2.pr<PR>.<run>.<attempt>). ValidGem::Version, uniquely namespaced, cannot collide — consistent with the documented D8 format.
Review generated by /accelerator:review-pr
| # otherwise surface later as an opaque Octokit 401. An empty or | ||
| # whitespace-only GITHUB_TOKEN is treated as absent so an authenticated | ||
| # operator falls through to `gh auth token` rather than hitting the raise. | ||
| def resolve_github_token |
There was a problem hiding this comment.
🔵 Standards (severity: minor, confidence: medium)
The GitHub-token resolution is implemented as a top-level def resolve_github_token method, whereas the family plan (§4.4 step 3) prescribes the logic inline within the RakeGithub.define_repository_tasks block and explicitly instructs "Do not restructure anything else in the Rakefile." The extraction also leaves the guiding comment (which mentions "rather than hitting the raise") above a method that no longer contains that raise. Functionally equivalent — inline for cross-repo fleet consistency, or confirm this as an accepted variation.
| token = ENV['GITHUB_TOKEN'].to_s.strip | ||
| token = `gh auth token`.strip if token.empty? | ||
| token | ||
| rescue Errno::ENOENT |
There was a problem hiding this comment.
🔵 Code Quality (severity: minor, confidence: medium)
Ruby backticks execute via /bin/sh -c, so a missing gh binary produces exit status 127 and empty stdout rather than raising Errno::ENOENT. This rescue reads as protection that never triggers; the "absent gh" case is actually handled by the empty-stdout fallthrough to the token-empty raise. Note this matches the plan-prescribed code, so treat as a plan concern. Consider checking $?.success? if explicit handling of the not-installed / unauthenticated cases is wanted.
| 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 (severity: suggestion, confidence: high)
%i[pr_number run_number run_attempt] is repeated verbatim as the task signature (line 214) and in this validation loop, and must be kept in sync by hand. Bind it to a local (e.g. arg_names = %i[...]) and reference it in both places. This is verbatim from the plan template — a plan concern.
| run: ./scripts/ci/common/configure-rubygems.sh | ||
| - name: Publish prerelease | ||
| # Facts via env, never interpolated | ||
| run: ./go "prerelease:publish[$PR_NUMBER,$RUN_NUMBER,$RUN_ATTEMPT]" |
There was a problem hiding this comment.
🔵 Security (severity: minor, confidence: medium)
The prerelease job checks out the PR branch, unlocks git-crypt with ENCRYPTION_PASSPHRASE, configures RubyGems push credentials, then runs PR-authored ./go prerelease:publish. A collaborator with push access could modify the task to exfiltrate the RubyGems API key and git-crypt passphrase pre-merge — a wider secret surface than CircleCI, which published nothing pre-merge. Documented as a deliberate, accepted D8 trade-off; no change required for this migration. If the collaborator trust assumption weakens, consider a scoped/short-lived RubyGems token.
Part of PP-709.
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