Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 0 additions & 176 deletions .circleci/config.yml

This file was deleted.

File renamed without changes.
178 changes: 178 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: Main
on:
push:
branches:
- main

permissions:
contents: read

# Concurrency is JOB-level, on prerelease + release only (shared group), NOT
# workflow-level: a run waiting on the release approval counts as the group's
# in-progress occupant, so a workflow-level group would freeze ALL main CI
# (check/test/prerelease of later pushes) for up to the 30-day approval
# window. The approval itself lives on the slot-free release-gate job, so a
# pending approval holds no slot. queue: max keeps every queued publish (the
# default keeps only the newest pending) so close-together merges serialise
# FIFO on tag operations instead of cancelling each other.
# 'queue: max' — GA 2026-05-07: https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/
# Gate/slot split pattern: https://github.com/orgs/community/discussions/17401

jobs:
skip-ci-check:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
should_skip_ci: ${{ steps.skip_ci_check.outputs.should_skip_ci }}
steps:
- id: skip_ci_check
env:
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
if [[ "$HEAD_COMMIT_MESSAGE" == *"[no ci]"* ]] \
|| [[ "$HEAD_COMMIT_MESSAGE" == *"[skip ci]"* ]] \
|| [[ "$HEAD_COMMIT_MESSAGE" == *"[ci skip]"* ]]; then
echo "should_skip_ci=true" >> "$GITHUB_OUTPUT"
else
echo "should_skip_ci=false" >> "$GITHUB_OUTPUT"
fi

check:
needs: [skip-ci-check]
if: needs.skip-ci-check.outputs.should_skip_ci != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Check
run: ./go test:code:check
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

test:
needs: [skip-ci-check]
if: needs.skip-ci-check.outputs.should_skip_ci != 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Ensure docker-compose is available
# Pinned + checksum-verified: this runs as root on runners that later
# hold secrets; the checksum is from the release's own .sha256 asset.
run: |
if ! command -v docker-compose >/dev/null 2>&1; then
sudo curl -fsSL \
"https://github.com/docker/compose/releases/download/v5.3.1/docker-compose-linux-x86_64" \
-o /usr/local/bin/docker-compose
echo "f9ebc6ebdb19d769b793c245a736caaeb198c62587f13b25c660c13b4987f959 /usr/local/bin/docker-compose" \
| sha256sum -c -
sudo chmod +x /usr/local/bin/docker-compose
fi
- name: Test
run: ./go test:integration
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

prerelease:
needs: [check, test]
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🔵 Safetyplan concern, not a defect of this diff

The main-tags group serialises the publish jobs, and the header comment describes this as making close-together merges "serialise FIFO on tag operations". Worth noting the serialisation is narrower than that reads.

version:bump ultimately does repo.push('origin', 'main', tags: true) — i.e. git push origin main --tags, which pushes the main branch as well as the tag. actions/checkout on a push event creates local main at the tested SHA (git checkout --force -B main <github.sha>), and unlike release, prerelease deliberately does not pull. So when two merges land close together, the older run's prerelease pushes a stale main and git rejects it as non-fast-forward. Because git push updates refspecs independently (no --atomic), the tag lands anyway while the command exits non-zero — so && ./go image:publish never runs.

Impact: an rc tag on origin with no image behind it, plus a red prerelease. Same orphan-tag hazard the PR already documents, but reached via a benign cause. Serialising the jobs cannot prevent it, because the staleness comes from the checkout SHA rather than from job overlap.

Nothing to change — the push logic is untouched per D5. Two suggestions for the plan: narrow the concurrency comment to say it serialises tag creation, and log the branch-push-from-a-stale-SHA behaviour as a fleet-wide item (the fix is for version:bump to push the tag refspec alone, not main).

group: main-tags
cancel-in-progress: false
queue: max
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Install secrets tools
run: sudo apt-get update && sudo apt-get install -y git-crypt gnupg
- name: Unlock git-crypt
run: ./go git_crypt:unlock_with_encrypted_gpg_key
env:
ENCRYPTION_PASSPHRASE: ${{ secrets.ENCRYPTION_PASSPHRASE }}
- name: Set CI git author
run: ./go repository:set_ci_author
- name: Prerelease
run: ./go "version:bump[rc]" && ./go image:publish
- name: Notify Slack of release hold
continue-on-error: true
run: ./go "slack:notify[success,on_hold]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

release-gate:
# Approval-only job: carries the environment but NO concurrency group, so
# a pending approval never occupies the main-tags slot. Multiple pushes
# can await approval independently.
needs: [prerelease]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
steps:
- name: Release approved
run: "true"

release:
needs: [release-gate]
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: main-tags
cancel-in-progress: false
queue: max
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Pull latest main

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🔵 Correctness + Safetyplan concern, not a defect of this diff

Two independent lenses landed on this step, so recording it together.

It is redundant. The step's comment says its purpose is that "approval can land long after the run starts; release publishes main as of approval time" — but the preceding actions/checkout@v4 with ref: main already achieves exactly that. release does not start until release-gate completes (i.e. post-approval), so its checkout resolves main at approval time regardless.

It is ordered before the git author is set. Set CI git author runs three steps later. CircleCI had this inverted — configure-git.sh ran during configure_tools, before release.sh reached its git pull. GitHub runners carry no global git identity, so a pull needing a merge commit aborts with unable to auto-detect email address. Low risk in practice (checkout at ref: main means the pull can only fast-forward, and fast-forwards need no identity) — but the safety of this step rests on an invariant established four steps away.

Impact: a redundant step whose stated rationale doesn't hold, contributing latent failure risk to the one job that pushes tags and publishes images. If an identity ever did exist, a non-fast-forward pull would silently create a merge commit that version:release's repo.push('origin', 'main', tags: true) would then push to main unreviewed.

This is the plan's exact YAML (§4.2) and the PR description documents the pull as parity with the old release.sh — so nothing to change here. Worth raising against the plan: git pull --ff-only with Set CI git author hoisted above it would make the intended invariant enforced rather than assumed, at zero parity cost.

# 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
- name: Install tools
uses: infrablocks/github-actions/asdf_install@v1
- name: Install secrets tools
run: sudo apt-get update && sudo apt-get install -y git-crypt gnupg
- name: Unlock git-crypt
run: ./go git_crypt:unlock_with_encrypted_gpg_key
env:
ENCRYPTION_PASSPHRASE: ${{ secrets.ENCRYPTION_PASSPHRASE }}
- name: Set CI git author
run: ./go repository:set_ci_author
- name: Release
run: ./go version:release && ./go image:publish
- name: Notify Slack
if: ${{ !cancelled() }}
continue-on-error: true
run: ./go "slack:notify[${{ job.status }}]"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Loading
Loading