Migrate pipeline from CircleCI to GitHub Actions#168
Conversation
ffi 1.17.1 was yanked from RubyGems, breaking bundle install on fresh CI runners. Targeted `bundle lock --update=ffi` bumps it to 1.17.4; no other gems drift.
|
𔵠Plan-conformance (§4.5) — stale operator-local platforms left in The Plan §4.5 states: ā€œensure the PLATFORMS section retains the generic Impact: low — Suggestion: run |
phelma
left a comment
There was a problem hiding this comment.
Code Review: #168 - Migrate pipeline from CircleCI to GitHub Actions
Verdict: COMMENT
This is a faithful, plan-conformant Variant A cutover. The new main.yaml/pr.yaml workflows match the family shape and sibling repos, the Rakefile provisioning swap (ambient gh token, git-crypt-ciphertext guard, rake_slack routing, prerelease:publish) is defensive and well-commented, and the CircleCI decommission is complete and clean. No in-scope critical or major defects were found. Most lens concerns, checked against the family plan and PR description, are documented deliberate decisions (D8 PR-CI prerelease publish, the queue: max key cited in §4.2, flat per-job scaffolding, publish-before-push ordering, verbatim provisioning/routing code) rather than defects — kept below so a human can revisit the plan, but they do not block.
Cross-Cutting Themes
queue: maxconcurrency key (correctness, safety) — both lenses flag it as outside the native Actionsconcurrencyschema. Plan concern, not a diff defect: §4.2 prescribesqueue: maxand cites a GA changelog dated 2026-05-07; the diff reproduces the plan faithfully. A human should confirm the feature shipped as claimed — if not, the Main workflow could fail validation (blocking all releases) or silently drop the intended version-race protection. Highest-value follow-up.
Strengths
- ✅ Least-privilege
permissions: contents: readwith per-job elevation. - ✅ Untrusted inputs passed via
env:, referenced as shell vars — injection-safe. - ✅ Secret-bearing jobs gated on immutable
pull_request.user.login, not spoofablegithub.actor; forks/Dependabot excluded. - ✅ Dependabot auto-merge uses
--match-head-commit "$HEAD_SHA"and[skip ci]. - ✅
prerelease:publishrestoresversion.rband removes the gem in anensureblock; provisioning refuses to upload git-crypt ciphertext. - ✅ Decommission complete:
.circleci/gone,scripts/ci/trimmed to the two portable scripts, credentials removed,git grep -i circleciclean.
General Findings
In-scope defects (minor/cosmetic):
- 🔵 Lockfile (§4.5):
Gemfile.lockPLATFORMSstill lists operator-localarm64-darwin-*/x86_64-darwin-*. §4.5 asked for these to be trimmed. Low impact —ruby+x86_64-linuxpresent so CI resolves. (Posted as a PR comment.) - 🔵 Standards:
# rubocop:disable Metrics/BlockLengthon theRakeGithubblock diverges from sibling Rakefiles; a reasonable, correctly-scoped resolution of the plan-prescribed inline guard logic (could be avoided by extracting helper methods).
Plan concerns (documented deliberate — do not block):
- 🟡 Security (D8): PR-CI
prereleaseunlocks git-crypt + RubyGems creds while running PR-branch code, widening secret blast radius vs CircleCI — documented, guarded to same-repo non-Dependabot PRs. Worth confirming the RubyGems key is push-only/gem-scoped. - 🟡 Code-quality: provisioning control-flow embedded in the
RakeGithubconfig block — exact code prescribed by §4.4.3. - 🔵 Safety (§1 hazard):
gem releasepublishes beforegit push(transient push failure wedges pipeline) — documented pre-existing (D5). - 🔵 Safety (§1 hazard): prerelease publishes to RubyGems on every push to
mainwith no approval gate — documented parity. - 🔵 Correctness: prerelease job emits both
on_holdandsuccessSlack messages on success — consequence of the exact §4.2 YAML. - 🔵 Security: verify the retired CircleCI deploy key / stored API tokens are revoked — host-side sweep is out of scope (§1).
- 🔵 Code-quality: opaque Slack channel IDs, duplicated arg list, per-job scaffolding — all verbatim from §4.4 / documented flat scaffolding.
False positive (verified, counted nowhere): the correctness lens suspected slack:notify[success,on_hold] mis-maps its 2nd arg. Verified against rake_slack's default_argument_names %i[outcome type]: success→outcome, on_hold→type correctly matches when: { type: 'on_hold' } → release channel. Code is correct.
Review generated by /accelerator:review-pr
| concurrency: | ||
| group: main | ||
| cancel-in-progress: false | ||
| queue: max |
There was a problem hiding this comment.
🟡 queue: max concurrency key — verify the feature exists (plan concern, not a diff defect)
The concurrency block uses queue: max, which is not part of the historically-native GitHub Actions concurrency schema (group + cancel-in-progress only). This is prescribed by family plan §4.2, which cites a GA changelog dated 2026-05-07, so the diff is plan-conformant.
Action for a human reviewer: confirm the queue: key actually shipped as the plan claims. If it did not, the Main workflow could fail schema validation (blocking all releases), or silently ignore the key — in which case cancel-in-progress: false keeps only the newest pending run and the version-race serialisation the comment relies on would not hold. Same key repeats on the release job (line 99).
| 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.
🟡 PR-CI prerelease publishes from PR-branch code (D8 — documented deliberate)
This job unlocks git-crypt (ENCRYPTION_PASSPHRASE, which decrypts all repo secrets) and configures RubyGems publish credentials, then runs ./go/Rakefile/gemspec code checked out from the PR branch. A collaborator able to open a same-repo PR could modify the build to exfiltrate the passphrase or the RubyGems key.
This is the documented, deliberate D8 feature (prove the publish path pre-merge), guarded to same-repo non-Dependabot PRs — not a diff defect. Recommend confirming the trust model is acceptable and that the RubyGems API key is scoped push-only to this gem, so a compromised run cannot reach other gems.
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-22T17-48-59-192Zatomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T17-48-59-192Z