Skip to content

Migrate pipeline from CircleCI to GitHub Actions#117

Closed
phelma wants to merge 1 commit into
mainfrom
gha-migration
Closed

Migrate pipeline from CircleCI to GitHub Actions#117
phelma wants to merge 1 commit into
mainfrom
gha-migration

Conversation

@phelma

@phelma phelma commented Jul 13, 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: loop-guard, check/test, prerelease, release environment gate
  • 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.

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.

@phelma phelma marked this pull request as ready for review July 13, 2026 17:08
Copilot AI review requested due to automatic review settings July 13, 2026 17:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR completes the repo’s CI migration from CircleCI to GitHub Actions, moving pipeline execution, secret handling (git-crypt/GPG), releases/prereleases, Slack notifications, and Dependabot auto-merge into GitHub-native workflows while removing CircleCI-era scripts/config.

Changes:

  • Added GitHub Actions workflows for PR validation and main-branch prerelease/release automation (including a release environment gate).
  • Updated build/provisioning Rake tasks to use rake_github secrets/environments and rake_slack; removed CircleCI/SSH deploy-key provisioning.
  • Removed CircleCI configuration and CI shell scripts; updated docs and Ruby dependencies accordingly.

Reviewed changes

Copilot reviewed 19 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
.github/workflows/pr.yaml Adds PR checks and Dependabot auto-merge job.
.github/workflows/main.yaml Adds main-branch pipeline for check/test + prerelease and gated release.
Rakefile Replaces CircleCI provisioning with GitHub secrets/environments and Slack notification tasks; adds build task.
Gemfile Swaps CircleCI/SSH rake helpers for Slack helper gem.
Gemfile.lock Updates transitive dependencies per the new bundle resolution and gem changes.
README.md Updates CI key management docs and moves encrypted key path to .github/.
.github/gpg.private.enc Adds encrypted CI GPG key under .github/.
.circleci/config.yml Removes CircleCI pipeline configuration.
scripts/ci/** Removes CircleCI CI helper scripts/steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +17
concurrency:
group: main
cancel-in-progress: false
queue: max
Comment on lines +93 to +96
- name: Prerelease
run: ./go "version:bump[pre]" && ./go release
- name: Push release commit
run: git push && git push --tags
Comment on lines +119 to +124
- name: Pull latest main
# Approval can land long after the run starts; release publishes main
# as of approval time, not the tested SHA — parity with the old
# release.sh, which also pulled. prerelease.sh never pulled, so the
# prerelease job deliberately has no pull.
run: git pull
Comment on lines +140 to +143
- name: Release
run: ./go "version:bump[minor]" && ./go release
- name: Push release commit
run: git push && git push --tags
Comment thread .github/workflows/pr.yaml
Comment on lines +62 to +66
- name: Merge pull request
run: gh pr merge --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Comment thread Rakefile
Comment on lines +124 to +126
# Operator's ambient auth — the stored PAT is gone (see the cutover PR's
# deliberate-decisions list).
t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip }

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

Verdict: APPROVE (posted as COMMENT — GitHub does not allow approving one's own PR)

A disciplined, plan-conformant cutover. All five lenses (correctness, security, safety, standards, code quality) found the diff faithful to the Variant A family plan: workflows match the plan's §4.1/§4.2 shapes with correct per-repo parameterisation (version:bump[minor] mirroring the deleted release.sh, no documentation:update), the Rakefile/Gemfile/README changes match §4.4–§4.6, and decommission (§4.7) is complete. Every substantive finding either challenges a decision the plan or PR description documents as deliberate (recorded as plan concerns for post-migration follow-up) or was disproven on verification. Zero in-scope defects.

Verified False Positives

  • "queue: max is not valid concurrency syntax" (flagged by correctness 🔴, safety 🟡, code-quality 🔵) — disproven. GitHub shipped larger concurrency queues on 2026-05-07; queue: max with cancel-in-progress: false is the documented deployment pattern (keeps up to 100 pending runs, FIFO), exactly as the workflow comment claims. Sources: GitHub changelog, concurrency docs.

Cross-Cutting Themes (plan concerns — post-migration follow-up, not blockers)

  • Inherited publish-pipeline hazards (security, safety, correctness) — ungated prerelease publish on every main push, publish-before-push ordering inside ./go release, and release-at-approval-time pulling untested SHAs. All documented in the plan §1 parity list / PR description as deliberate. Post-migration candidates: RubyGems Trusted Publishing (OIDC), push-before-publish, release the tested SHA.
  • Dependabot runs and secret stores (correctness, security) — dependabot-triggered runs read the Dependabot secret store, so SLACK_BOT_TOKEN resolves empty and the builds-dependabot routing is silently dead until issue 19 provisions the org-wide Dependabot store. Explicitly accepted in the plan.

Strengths

  • ✅ Workflow YAML matches the plan's authoritative shapes, including top-level least-privilege permissions: with per-job elevation, and continue-on-error notify steps so Slack outages can never fail a build.
  • ✅ Head commit message passed via env var in the skip-ci check — avoids the classic GHA command-injection vector.
  • ✅ pr.yaml never unlocks git-crypt; fork/dependabot runs cannot reach the passphrase or the secrets it protects.
  • ✅ Stored PAT, CircleCI API token, and CI SSH deploy key pair removed in favour of ambient operator auth and the ephemeral GITHUB_TOKEN.
  • ✅ Full releases gated behind the reviewed release environment; release step correctly parameterised to mirror the old release.sh rather than blindly copying the plan template's patch.
  • ✅ Decommission thorough: no CircleCI references remain in scope; the encrypted GPG key move is a pure rename; commit message matches org convention.

General Findings (all plan concerns)

  • 🟡 Security: ungated prerelease job holds full publish capability — any push to main yields runtime access to the RubyGems key via the single ENCRYPTION_PASSPHRASE. Documented parity; consider OIDC Trusted Publishing post-migration.
  • 🔵 Security: CircleCI remains a live credential holder (passphrase env var, deploy key) until the deferred sweep — rotate ENCRYPTION_PASSPHRASE once CircleCI is disabled.
  • 🔵 Security: committed encrypted GPG key uses openssl -md sha1 single-iteration KDF (inherited) — re-encrypt with -pbkdf2 -iter 600000 at next rotation.
  • 🔵 Safety: ./go release publishes before pushing the bump commit — a failed push leaves registry/repo inconsistent (documented D5 parity; add a recovery runbook note).
  • 🔵 Safety: release job pulls main at approval time, so delayed approval can publish untested commits (documented parity).
  • 🔵 Safety: rollback to CircleCI now requires recovering git-crypted credentials from history (recoverable at ee37453) — note the path in the migration issue until the first successful gated release.
  • 🔵 Correctness: skip-ci-check duplicates native [skip ci] handling and GITHUB_TOKEN-push semantics — plan keeps it as belt-and-braces (D4).
  • 🔵 Code quality: RubyGems credential-copy shell duplicated across jobs and the fleet — a repository:configure_rubygems_credentials rake task would restore the "logic lives in the build system" convention.
  • 🔵 Code quality: Slack routing rules (hard-coded channel IDs) will be copy-pasted into ~103 Rakefiles — consider a shared default routing table in rake_slack later.
  • 🔵 Standards: plan §4.5 assumes gemspec-managed dev deps; this repo declares them in the Gemfile and the PR correctly applied the equivalent edit — worth adding to the plan's non-disqualifying deviations list.

Full analysis persisted to meta/reviews/prs/117-review-1.md.


Review generated by /accelerator:review-pr

Comment thread .github/workflows/pr.yaml
merge-pull-request:
needs: [check, test, build]
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'

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 (not a blocker; plan §4.1 mandates this exact gate)

github.actor reflects who triggered the event, not the PR author, so a write-access account pushing to a dependabot branch could get code auto-merged to main without review — feeding the auto-publishing prerelease pipeline. For post-migration fleet-wide hardening: gate on github.event.pull_request.user.login == 'dependabot[bot]' (optionally alongside the actor check), and consider dependabot/fetch-metadata to verify the PR is a genuine dependabot update.

Comment thread .github/workflows/pr.yaml
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

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.

🔵 Correctness — plan concern (explicitly accepted; Dependabot secret store deferred to issue 19)

Dependabot-triggered pull_request runs read the Dependabot secrets store, not the Actions store, so secrets.SLACK_BOT_TOKEN resolves empty for exactly the runs the builds-dependabot routing targets — and continue-on-error: true hides the failure. Noting so the gap stays tracked: the routing is dead code until issue 19 provisions the org-wide Dependabot store.

Comment thread Rakefile
}
# Operator's ambient auth — the stored PAT is gone (see the cutover PR's
# deliberate-decisions list).
t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip }

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 (snippet is plan §4.4 verbatim)

ENV.fetch('GITHUB_TOKEN') { %x(gh auth token).strip } degrades silently: if gh is missing or logged out it yields an empty string, and the failure surfaces later as an opaque GitHub API 401 from rake_github. A plan-level improvement: validate the resolved token and raise a clear "set GITHUB_TOKEN or run gh auth login" error when empty.

if: needs.skip-ci-check.outputs.should_skip_ci != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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 (D6 documents tracking asdf_install@v1; actions/checkout@v4 is plan YAML verbatim)

These jobs handle the git-crypt passphrase and RubyGems credentials while referencing actions by mutable tags. SHA-pinning third-party actions (with a version comment) is cheap insurance against the tag-repointing attack pattern (cf. tj-actions/changed-files). Fleet-wide post-migration hardening candidate.

Comment thread Rakefile
# deliberate-decisions list).
t.access_token = ENV.fetch('GITHUB_TOKEN') { `gh auth token`.strip }
# Actions store only: nothing in pr.yaml unlocks git-crypt, so dependabot
# runs never need the passphrase.

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.

🔵 Correctness — plan concern (rules are the plan's exact Ruby)

The bare { actor: 'dependabot[bot]' } failure rule also matches successful dependabot runs; correct routing relies on rake_slack's first-match semantics and the ordering of this list. Making the failure rule explicit (outcome: 'failure' alongside the actor match) would remove the ordering assumption — a plan-level tweak.

@phelma phelma closed this Jul 14, 2026
@phelma phelma deleted the gha-migration branch July 14, 2026 13:43
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.

2 participants