Skip to content

Migrate pipeline from CircleCI to GitHub Actions#174

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

Migrate pipeline from CircleCI to GitHub Actions#174
phelma merged 1 commit into
mainfrom
gha-migration

Conversation

@phelma

@phelma phelma commented Jul 24, 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-24T08-28-29-246Z

atomic-foundry-pr · foundry-pipeline: migrate · foundry-task: migrate-gem · foundry-run: 2026-07-24T08-28-29-246Z

@phelma

phelma commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Status from the automation that opened this pull request — updated as it progresses.

Assessment

I checked out a gha-migration branch and inspected rake_github to decide whether it fits our standard Ruby-gem CircleCI-to-GitHub-Actions migration. It does: it's a gem published to RubyGems with the usual ./go/Rakefile build system, git-crypt secrets, and a CircleCI config in our familiar build/test/prerelease/release/merge shape. A few repo-specific details need carrying through, none of them blocking. The test step script calls ./go spec, but that task doesn't actually exist in this repo (the real unit-test task is test:unit), so the new workflow should run test:unit. The release step bumps the minor version rather than the patch version and doesn't regenerate documentation, so the release job should mirror that. The repo needs libsodium installed for one of its gems, so we keep that small install script and call it from the workflow. Finally, the Ruby version is actually 3.3.11 (our offline survey had recorded 3.1.1 by mistake), and there's no CI status badge in the README to swap. Everything else lines up with the standard plan, so this is good to proceed.

Changes

I migrated this gem's CI from CircleCI to GitHub Actions. I added two workflows — one for pull requests (lint, test, build, a pre-release publish to RubyGems, and Dependabot auto-merge) and one for the main branch (lint, test, then a gated release) — and removed the old CircleCI config and its helper scripts. The Rakefile now provisions the pipeline through GitHub instead of CircleCI, notifies Slack, and gained small build/release helper tasks; the encrypted CI key moved from the CircleCI folder to the GitHub one, and the gem's dependencies and README were updated to match. The gem needs the libsodium system library, which I kept and wired into the test job (the only job that actually loads it). All checks pass locally — linting, the full unit suite, and a clean-room dependency install — and I provisioned the repository's Actions secret and protected release environment. Four small, harmless deviations from the standard recipe are documented for the reviewer, none of which affect behaviour.

Verification

I independently verified that the CI provisioning for infrablocks/rake_github took effect, using read-only GitHub API calls. The Actions secret ENCRYPTION_PASSPHRASE is present, so CI will have the credential it needs to run. The gated 'release' environment exists and is protected by a required-reviewers rule (the infrablocks Maintainers team must approve releases). Everything the CI pipeline depends on is in place, so this PR is ready for review.

Automated checks

Automated checks passed first time.

Smoke test

I confirmed that PR #174's CI published a working prerelease of the rake_github gem. Querying RubyGems, I found the namespaced prerelease 0.18.0.pre.2.pr174.1.1, published at 2026-07-24T08:47:28Z (after this run's cutoff). I installed exactly that version into a throwaway directory outside the repo using Ruby 3.3.11 (the gem requires Ruby >= 3.3), then loaded rake_github/version and confirmed the RakeGithub::VERSION constant equalled 0.18.0.pre.2.pr174.1.1. The published artifact installs and loads correctly from the outside, so the PR-CI publish path works.

Automated review

I reviewed this CircleCI-to-GitHub-Actions migration for the rake_github gem across correctness, security, safety, standards and code-quality. The change is clean and matches its migration specification in both directions, with no defects that hold up merging. The protective machinery is sound: secrets are guarded against fork and bot pull requests, git-crypt content is checked before anything is uploaded, version-publishing jobs are serialised so releases cannot race, and the dependabot auto-merge only lands if no new commit arrived after the checks passed. One point is worth calling out: two automated reviewers flagged a queue: max setting as an invalid workflow key and marked it critical, but I confirmed against GitHub's own May 2026 changelog that it is a genuine, correctly-used feature — a false alarm caused by the reviewers' knowledge predating it. The remaining observations are all about decisions the plan deliberately made (for example publishing a pre-release from pull-request CI, and relying on GitHub's native skip-ci handling); I've recorded them for a human to revisit at the fleet level, but none block this PR. It looks safe to merge.

Ready to merge

The automated review and checks have passed. This is waiting for a maintainer to merge it.

atomic-foundry-status · foundry-run: 2026-07-24T08-28-29-246Z

@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: #174 - Migrate pipeline from CircleCI to GitHub Actions

Verdict: COMMENT

This is a clean, highly plan-conformant CircleCI → GitHub Actions cutover for
the rake_github gem. Across five lenses (correctness, security, safety,
standards, code quality) no in-scope defects survived verification: the diff
matches the family plan §4 in both directions, protective mechanisms
(git-crypt ciphertext guard, concurrency serialisation, --match-head-commit,
ensure-based version.rb restore) are sound, and the fork/Dependabot secret
guard on the PR prerelease job is correct. It looks safe to merge.

Verified false positive (not counted)

  • queue: max is NOT invalid. Three lens findings (2 flagged critical)
    claimed queue: max under concurrency: is not a valid GitHub Actions key.
    I verified against the GitHub changelog dated 2026-05-07
    (github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues):
    queue: max is a real, GA feature allowing up to 100 queued runs per group,
    and only works when cancel-in-progress is false — exactly how it is used
    here. The agents' (and my own) training predates the feature. No change
    required.

Verified correct (not a finding)

  • libsodium scoping. libsodium is installed only in the test job. The
    correctness lens verified rbnacl is lazily required inside
    Provision#encrypt (lib/rake_github/tasks/secrets/provision.rb:72), so
    check/build/prerelease/release never load it at library-require time
    and do not need it. Scoping is correct.

Plan concerns (documented-deliberate; for a human to revisit the plan, not blocking)

These challenge decisions the family plan or PR description documents as
deliberate. They are captured for a plan owner to revisit fleet-wide; none
block this plan-conformant diff.

  • 🔵 Security — secrets exposed to same-repo collaborator PR code
    (pr.yaml prerelease job). The prerelease job runs PR-branch code with
    ENCRYPTION_PASSPHRASE and RubyGems credentials in scope before any review
    gate. The fork/Dependabot guard is correct, but any collaborator with push
    access could exfiltrate secrets via a PR-branch task change. This is
    inherent to the deliberate D8 "prove the publish path pre-merge" design.
    Worth revisiting fleet-wide (e.g. an environment: approval on the
    prerelease publish).
  • 🔵 Safety — publish-before-push window (main.yaml prerelease). ./go release publishes to RubyGems before the bump commit is pushed; a failed
    push leaves a published version whose commit never reached main. This is
    the inherited hazard explicitly listed as out of scope in plan §1.
  • 🔵 Safety — approval-hold queue accumulation (main.yaml release).
    A run awaiting release approval holds the main concurrency slot, so pushes
    during the window queue behind it. Intentional and correct for version-race
    safety per plan §4.2; operationally worth monitoring queue depth.
  • 🔵 Standards — job named merge-pull-request vs the reference's
    dependabot-auto-merge. Explicitly a Variant A decision per plan §4.1
    (plain --merge, not --auto).
  • 🔵 Standards — no skip-ci-check job in main.yaml unlike the
    reference. Deliberate per D4: GITHUB_TOKEN push semantics + native
    [skip ci] handling make a loop-guard job unnecessary.
  • 🔵 Standards — Slack notify idiom uses if: ${{ !cancelled() }} +
    continue-on-error: true rather than the reference's if: always(). Plan
    §3 marks the §4 YAML as authoritative where it differs from the reference.
  • 🔵 Code quality — define_repository_tasks block mixes token resolution
    and the git-crypt guard
    (~30 lines, hard to unit-test). This is the plan's
    verbatim §4.4 prescribed code; extracting helpers would deviate from
    "do not restructure anything else in the Rakefile".
  • 🔵 Code quality — version regex /(VERSION\s*=\s*')([^']+)(')/ only
    matches single-quoted literals. Verbatim plan §4.4 code; safe in practice
    because rubocop enforces single quotes in this repo.
  • 🔵 Code quality — gem name / gem build duplication across
    library:build and prerelease:publish. Both are plan-prescribed verbatim.

Observation (non-blocking, not counted)

  • .rubocop.yml adds define_repository_tasks to the Metrics/BlockLength
    exclusions. Not in the §4 change list, but a necessary consequence of the
    plan-mandated (now longer) RakeGithub block so that the §5 library:check
    gate passes. Reasonable and in-spirit.

Strengths

  • ✅ Attacker-controllable facts (PR title, number, run metadata, HEAD SHA)
    are passed via env: and referenced as shell $VARS, never ${{ }}-
    interpolated into run: — no workflow shell injection.
  • GITHUB_TOKEN least privilege: top-level contents: read, with
    contents: write / pull-requests: write elevated only where needed.
  • ✅ Git-crypt ciphertext guard (\x00GITCRYPT magic via File.binread)
    fails closed before uploading garbage as ENCRYPTION_PASSPHRASE.
  • prerelease:publish restores version.rb and rm_fs the gem in an
    ensure block; namespaced version embeds run/attempt so re-runs never
    collide.
  • ✅ Dependabot merge guarded by --match-head-commit and [skip ci] subject.
  • ✅ Requires and dev-dependencies correctly alphabetised; comments are
    self-contained and avoid migration/process references.
  • ✅ Decommission is complete and removes stored credentials without
    committing any decrypted secret material.

Review generated by /accelerator:review-pr

Per-Lens Results

Correctness

Summary: Migration largely correct; git-crypt guard, version regex, and
prerelease:publish logic sound; libsodium correctly scoped to test (rbnacl
lazily required). Flagged queue: max as invalid (2× critical) — verified
false positive
, it is a real GA feature used correctly.

Security

Summary: Injection and secret-exposure hazards handled correctly; facts
via env, least-privilege tokens, correct fork/Dependabot guard, ciphertext
guard. One residual trust observation: prerelease runs collaborator PR code
with secrets (deliberate D8 design) → plan concern.

Safety

Summary: Unusually careful; ensure-based restore, concurrency
serialisation, --match-head-commit, --ff-only, full timeout coverage. Two
minor observations (publish-before-push window; approval-hold queue) both
documented-deliberate → plan concerns.

Standards

Summary: Tracks the rake_slack reference closely and improves on it
(alphabetised, least-privilege). Deviations (job name, no skip-ci-check,
notify idiom, queue) are all plan-mandated or false positives → plan
concerns.

Code Quality

Summary: Clean, well-commented, fail-fast error messages. Observations
(config block responsibilities, version regex, gem-name/build duplication) all
target plan-prescribed verbatim code → plan concerns.

@phelma
phelma marked this pull request as ready for review July 24, 2026 08:56
@phelma

phelma commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Ready to merge. The automated review and checks have passed; a maintainer can merge this when ready.

atomic-foundry-event · foundry-run: 2026-07-24T08-28-29-246Z

@phelma
phelma merged commit 570e593 into main Jul 24, 2026
5 of 6 checks passed
@phelma
phelma deleted the gha-migration branch July 24, 2026 09:54
@phelma

phelma commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Post-merge prerelease validation

Validated the 0.18.0.pre.3 prerelease published by the Main workflow after this PR merged, with particular focus on the libsodium handling (this repo's one deviation from the gem-family norm — the retained scripts/ci/common/install-libsodium.sh step).

CI side

  • check, test, and prerelease jobs all green on the post-merge run — the test job passing confirms the sudo-trimmed install-libsodium.sh works on GHA runners, since the rbnacl-dependent specs can't run without it.
  • 0.18.0.pre.3 published to RubyGems at 09:59Z (the earlier PR-CI prerelease 0.18.0.pre.2.pr174.1.1 had already validated the PR publish path).

Consumer side (clean ruby:3.3.11 Linux container with libsodium-dev)

  • gem install rake_github -v 0.18.0.pre.3 resolves and installs cleanly, including rbnacl 7.1.2.
  • Round-tripped the actual libsodium code path: RakeGithub::Tasks::Secrets::Provision#encrypt sealed a value via RbNaCl::Boxes::Sealed against a generated public key, and decrypting with the matching private key returned the original — the native binding genuinely works through the published gem, not just loads.

The release job on the Main run is holding at its approval gate; once approved, the same smoke test can be re-run against the final release version.

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