Skip to content

Migrate pipeline from CircleCI to GitHub Actions#143

Merged
phelma merged 1 commit into
mainfrom
gha-migration
Jul 22, 2026
Merged

Migrate pipeline from CircleCI to GitHub Actions#143
phelma merged 1 commit 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-22T15-58-21-187Z

atomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-22T15-58-21-187Z

@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: #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 specifies queue: max and 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 .pre version encoding: verified correct — a valid Gem::Version that 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 old prerelease.sh).
  • 🔵 Abandoned release approval freezes all main CI — inherent to the deliberate §4.2 main concurrency 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).
  • 🔵 RakeGithub block 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 spoofable github.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:publish restores version.rb + removes the gem in an ensure block; 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

Comment thread .github/workflows/pr.yaml
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

prerelease:

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.

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

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

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.

🔵 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

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.

🔵 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:

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.

🔵 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

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.

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

Comment thread Rakefile
end

RakeGithub.define_repository_tasks(
RakeGithub.define_repository_tasks( # rubocop:disable Metrics/BlockLength

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

Comment thread Rakefile
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|

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

Comment thread Rakefile
{ when: { actor: 'dependabot[bot]' },
channel: 'C03N711HVDG', format: :failure }, # builds-dependabot
{ when: { outcome: 'success' },
channel: 'C023XUE76GH', format: :success }, # builds

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

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.

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.

@phelma
phelma marked this pull request as ready for review July 22, 2026 17:01
@phelma
phelma merged commit f590eeb into main Jul 22, 2026
5 of 6 checks passed
@phelma
phelma deleted the gha-migration branch July 22, 2026 17:02
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