Skip to content

Migrate pipeline from CircleCI to GitHub Actions#168

Merged
phelma merged 2 commits into
mainfrom
gha-migration
Jul 22, 2026
Merged

Migrate pipeline from CircleCI to GitHub Actions#168
phelma merged 2 commits into
mainfrom
gha-migration

Conversation

@phelma

@phelma phelma commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Cutover to GitHub Actions per the Variant A family plan (gem pilot).
Includes decommission — merging this PR completes the repo's migration.

  • main + pr workflows: check/test, prerelease, release environment gate
  • PR CI publishes a namespaced pre-release to RubyGems (see below)
  • git-crypt unlock on the runner; encrypted CI GPG key moved to .github/
  • Slack notifications via rake_slack; dependabot auto-merge job
  • Rakefile provisioning swapped to rake_github secrets/environments; rake_circle_ci dropped
  • CircleCI pipeline removed: .circleci/, scripts/ci/, the CI SSH deploy
    key pair and its keys:deploy/deploy_keys provisioning, and the stored
    CircleCI/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 release publishes to RubyGems before the version-bump commit is
    pushed — pre-existing ordering inside the untouched release logic.
  • Prerelease publishes on every push to main with no approval gate; only
    full releases are gated (environment: release).
  • Dependabot auto-merge accepts any update type that passes checks, and the
    merge does not trigger a release build — on CircleCI the merge commit
    carried [skip ci], so this matches. Updates ship with the next
    human-triggered release.
  • The release job pulls main at approval time, so a delayed approval
    publishes main as it stands then, not the SHA this run tested — parity with
    the old release.sh (which also pulled; prerelease.sh did not, so the
    prerelease job has no pull).
  • asdf_install@v1 is our own action (infrablocks/github-actions); we are
    happy tracking its major version tag.
  • Job scaffolding is repeated flat per job by design: the logic lives in the
    build system (./go/rake) and CI stays lean — it just triggers tasks and
    supplies secrets/context.
  • Gemfile.lock carries transitive major bumps — the unavoidable resolution
    of the targeted bundle lock --update, not scope creep.
  • Small library hunks may appear where the refreshed toolchain's rubocop
    autocorrects existing code (e.g. Style/ArgumentsForwarding) — required
    by the library:check verification gate, not drive-by refactoring.
  • Provisioning (pipeline:prepare) authenticates with the operator's ambient
    gh login (GITHUB_TOKEN fallback) instead of a stored PAT — a deliberate
    parity deviation; the stored token in config/secrets/github/config.yaml
    is deleted with the rest of the CircleCI-era credentials.

PR-CI prerelease publish (deliberate, permanent)

pr.yaml has a prerelease job that publishes a namespaced pre-release of
this 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 new prerelease:publish
Rakefile task), so it can never collide with main's version: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 release is 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-request does not
depend 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 CircleCI deploy key are
deferred 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.

Pipeline Task pack Family Run
migrate migrate-gem gem 2026-07-22T17-48-59-192Z

atomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T17-48-59-192Z

phelma added 2 commits July 22, 2026 22:30
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.
@phelma

phelma commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

𔵠Plan-conformance (§4.5) — stale operator-local platforms left in Gemfile.lock

The PLATFORMS section still lists Darwin platforms:

PLATFORMS
  arm64-darwin-21
  arm64-darwin-22
  ruby
  x86_64-darwin-19
  x86_64-darwin-20
  x86_64-darwin-21
  x86_64-linux

Plan §4.5 states: ā€œensure the PLATFORMS section retains the generic ruby and x86_64-linux platforms and contains no operator-local platforms (e.g. arm64-darwin-*) … Run bundle lock --remove-platform <darwin platform> as needed.ā€

Impact: low — ruby and x86_64-linux are both present, so the ubuntu-latest CI runners resolve the graph correctly; the Darwin entries are inert on Linux. These pre-existed in the lockfile and were not introduced by this PR (the PLATFORMS block is unchanged in the diff), but the §4.5 lockfile step that should have trimmed them was not applied.

Suggestion: run bundle lock --remove-platform arm64-darwin-21 arm64-darwin-22 x86_64-darwin-19 x86_64-darwin-20 x86_64-darwin-21 so the lockfile carries only ruby + x86_64-linux, per §4.5. Cosmetic/hygiene only — not blocking.

@phelma phelma left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: max concurrency key (correctness, safety) — both lenses flag it as outside the native Actions concurrency schema. Plan concern, not a diff defect: §4.2 prescribes queue: max and 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: read with 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 spoofable github.actor; forks/Dependabot excluded.
  • ✅ Dependabot auto-merge uses --match-head-commit "$HEAD_SHA" and [skip ci].
  • prerelease:publish restores version.rb and removes the gem in an ensure block; provisioning refuses to upload git-crypt ciphertext.
  • ✅ Decommission complete: .circleci/ gone, scripts/ci/ trimmed to the two portable scripts, credentials removed, git grep -i circleci clean.

General Findings

In-scope defects (minor/cosmetic):

  • 🔵 Lockfile (§4.5): Gemfile.lock PLATFORMS still lists operator-local arm64-darwin-*/x86_64-darwin-*. §4.5 asked for these to be trimmed. Low impact — ruby + x86_64-linux present so CI resolves. (Posted as a PR comment.)
  • 🔵 Standards: # rubocop:disable Metrics/BlockLength on the RakeGithub block 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 prerelease unlocks 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 RakeGithub config block — exact code prescribed by §4.4.3.
  • 🔵 Safety (§1 hazard): gem release publishes before git push (transient push failure wedges pipeline) — documented pre-existing (D5).
  • 🔵 Safety (§1 hazard): prerelease publishes to RubyGems on every push to main with no approval gate — documented parity.
  • 🔵 Correctness: prerelease job emits both on_hold and success Slack 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).

Comment thread .github/workflows/pr.yaml
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]"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@phelma
phelma marked this pull request as ready for review July 22, 2026 21:56
@phelma
phelma merged commit b107863 into main Jul 22, 2026
5 of 6 checks passed
@phelma
phelma deleted the gha-migration branch July 22, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant