Skip to content
Merged
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
50 changes: 7 additions & 43 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,50 +1,14 @@
name: CI

permissions:
contents: read

on:
push:
branches: [ 'master' ]
branches: [master]
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-24.04
env:
GOTOOLCHAIN: local
steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.26"
cache: true

- name: Format check
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "gofmt needed for:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.2

- name: Run tests
run: go test -race -v ./...

- name: Module tidy check
run: go mod tidy -diff
permissions:
contents: read

- name: Module verify
run: go mod verify
jobs:
verify:
uses: ./.github/workflows/verify.yml
78 changes: 41 additions & 37 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
version:
description: "Release version, for example v0.12.0"
required: true
default: "v0.12.0"

permissions:
contents: read
Expand All @@ -20,6 +19,9 @@ env:
RELEASE_VERSION_INPUT: ${{ inputs.version }}

jobs:
verify:
uses: ./.github/workflows/verify.yml

validate:
name: Validate release
runs-on: ubuntu-24.04
Expand All @@ -30,11 +32,6 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: actions/setup-go@v7
with:
go-version: "1.26"
cache: true

- name: Resolve release version
id: version
shell: bash
Expand Down Expand Up @@ -73,26 +70,20 @@ jobs:

- name: Validate release scripts
run: |
bash -n scripts/*.sh
for script in scripts/*.sh; do bash -n "$script"; done
bash scripts/prepare-release-test.sh

- name: Check changelog readiness
env:
CHANGELOG_VERSION: ${{ steps.version.outputs.changelog_version }}
run: bash scripts/prepare-release.sh "$CHANGELOG_VERSION" --check-only

- name: Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.2

- name: Full release validation
run: bash scripts/test-all.sh

publish:
name: Publish GitHub Release
needs: validate
needs: [validate, verify]
runs-on: ubuntu-24.04
outputs:
release_sha: ${{ steps.release_sha.outputs.sha }}
permissions:
contents: write
steps:
Expand All @@ -117,14 +108,12 @@ jobs:
exit 1
fi

git checkout master
git pull --ff-only origin master
git checkout --detach "$DISPATCH_SHA"
bash scripts/prepare-release.sh "$CHANGELOG_VERSION"

if ! git diff --quiet -- CHANGELOG.md; then
git add CHANGELOG.md
git commit -m "chore(release): prepare CHANGELOG for ${RELEASE_VERSION}"
git push origin master
fi

- name: Resolve release commit SHA
Expand All @@ -133,20 +122,8 @@ jobs:
env:
DISPATCH_SHA: ${{ needs.validate.outputs.dispatch_sha }}
run: |
bash scripts/verify-release-candidate.sh "$DISPATCH_SHA"
release_sha=$(git rev-parse HEAD)
if [[ "$release_sha" == "$DISPATCH_SHA" ]]; then
echo "changelog was already finalized on the validated commit"
elif [[ $(git rev-list --count "${DISPATCH_SHA}..HEAD") -eq 1 ]]; then
parent_sha=$(git rev-parse HEAD^)
changed_files=$(git diff-tree --no-commit-id --name-only -r HEAD)
if [[ "$parent_sha" != "$DISPATCH_SHA" || "$changed_files" != "CHANGELOG.md" ]]; then
echo "release commit must be a single changelog-only child of $DISPATCH_SHA" >&2
exit 1
fi
else
echo "unexpected commits between validated and release state" >&2
exit 1
fi
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"

- name: Verify finalized changelog
Expand Down Expand Up @@ -181,11 +158,17 @@ jobs:
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md
if [[ ! -s release-notes.md ]]; then
echo "api-testing ${{ needs.validate.outputs.version }}" > release-notes.md
echo >> release-notes.md
echo "See CHANGELOG.md for details." >> release-notes.md
fi
test -s release-notes.md

- name: Push verified changelog commit
shell: bash
env:
DISPATCH_SHA: ${{ needs.validate.outputs.dispatch_sha }}
run: |
git fetch origin master
test "$(git rev-parse origin/master)" = "$DISPATCH_SHA"
bash scripts/verify-release-candidate.sh "$DISPATCH_SHA"
git push origin HEAD:refs/heads/master

- name: Publish source-only GitHub Release
uses: softprops/action-gh-release@v3
Expand All @@ -195,3 +178,24 @@ jobs:
name: api-testing ${{ needs.validate.outputs.version }}
body_path: release-notes.md
prerelease: ${{ contains(needs.validate.outputs.version, '-') }}

consumer:
name: Verify published module
needs: [validate, publish]
runs-on: ubuntu-24.04
permissions:
contents: read
env:
GOTOOLCHAIN: local
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.publish.outputs.release_sha }}
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Resolve and compile exact published version
env:
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: bash scripts/verify-published-module.sh "$RELEASE_VERSION"
100 changes: 100 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Shared verification

on:
workflow_call:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-24.04
env:
GOTOOLCHAIN: local
steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.26.6"
cache: true

- name: Format check
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "gofmt needed for:"
echo "$unformatted"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.2

- name: Run tests
run: go test -race -v ./...

- name: Module tidy check
run: go mod tidy -diff

- name: Module verify
run: go mod verify

- name: Release script contracts
run: |
for script in scripts/*.sh; do bash -n "$script"; done
bash scripts/prepare-release-test.sh
python3 scripts/release-candidate-test.py
python3 scripts/security-report-test.py

minimum:
name: Minimum supported Go
runs-on: ubuntu-24.04
env:
GOTOOLCHAIN: local
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Verify public module on supported minimum
run: |
go version
go build ./...
go test ./...
go test -race ./...

security:
name: Security
runs-on: ubuntu-24.04
env:
GOTOOLCHAIN: local
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.26.6'
- name: Install pinned scanner
run: GOBIN="$RUNNER_TEMP/vuln-bin" go install golang.org/x/vuln/cmd/govulncheck@v1.8.0
- name: Scan
id: scan
run: bash scripts/security-report.sh "$RUNNER_TEMP/vuln-bin/govulncheck" "$RUNNER_TEMP/security-report"
- name: Report incomplete scan
if: always() && steps.scan.outcome == 'skipped'
run: |
mkdir -p "$RUNNER_TEMP/security-report"
echo '## Security scan incomplete: checkout, setup or scanner installation failed. See job logs; no clean result is available.' | tee "$RUNNER_TEMP/security-report/summary.md" >> "$GITHUB_STEP_SUMMARY"
- name: Upload security report
if: always()
uses: actions/upload-artifact@v4
with:
name: security
path: ${{ runner.temp }}/security-report
if-no-files-found: error
67 changes: 66 additions & 1 deletion docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ workflow-verified release commit.
## Preflight

- [ ] Work is merged to `master` and the branch is not moving during publication.
- [ ] Normal CI is green.
- [ ] Required quality, minimum-Go and Security checks are green.
- [ ] `CHANGELOG.md` has either a non-empty exact planned version section or non-empty
`[Unreleased]` section.
- [ ] README is current.
Expand Down Expand Up @@ -78,3 +78,68 @@ a second local tag or move the published tag.
- If a release or tag was published incorrectly, follow repository governance,
document a retraction, and publish a corrective version. Do not rewrite public
history or silently move a consumed tag.

## Shared CI contract

Normal CI and Release call `.github/workflows/verify.yml` at the caller's
revision. Required checks are the existing quality gate, `Minimum supported Go`
(build/tests on the `go.mod` minimum), and `Security` (govulncheck v1.8.0, analysis
Go 1.26.6). Security fails on both reachable findings and scanner/setup errors.

The security check retains a GitHub Summary and an artifact with scanner output
and the effective toolchain. It distinguishes findings from scanner/setup errors;
an incomplete scan is never treated as clean. CI and releases do not scan old Go
versions. A minimum-toolchain security audit can be performed manually when needed.

The minimum Go directive promises build/API compatibility, not a vulnerability-free
old standard library. Consumers should use a maintained, patched Go toolchain.
A clean modern scan does not establish safety on the minimum version. Findings
still need review; raising the support floor is a separate compatibility decision.
A two-component Go directive selects that family's available patch; a
three-component directive pins the specified minimum patch.

Project-specific checks remain required, including Squirrel's database, nested
modules, differential/API and dependency gates where present. Modern tooling and
`go mod tidy -diff` do not run in the old minimum-Go compatibility lane.

The publish job prepares from the immutable validated SHA and checks ancestry,
changed files, nonempty finalized notes and tag identity **before** pushing anything.
It rechecks the branch and uses a normal non-force push of that exact commit.
Branch movement rejects publication; no pull/rebase incorporates unverified work.
A subsequent unrelated branch commit does not change the already selected release
SHA. Repository release concurrency does not lock human pushes.

After publication, the `consumer` job resolves the exact version in a temporary
external module, with workspace use disabled and an isolated module cache, and
compiles the public imports listed in `scripts/release-consumer-imports.txt`.
The cache is outside the consumer source directory and writable for cleanup. Download retries are bounded to accommodate proxy propagation;
a failed smoke check does not delete or move the published tag. Because bot pushes
need not trigger push workflows, this check is an explicit dependent job.

## Recovery and rollout

If the changelog commit was pushed but publication failed, inspect the tag/release
first and start a **fresh dispatch** on the current release branch with the same
version. Re-running the old event still has its old SHA and can fail the freshness
check. Existing same-commit tags may be reconciled; conflicting tags stop the flow.
The version input has no prefilled default to avoid reusing a previous release.

Before merging this CI change, review branch protection required-check names:
reusable workflow jobs have new check names. Configure rules to require the new
quality, minimum and Security lanes. Remove the obsolete vulnerability matrix
contexts only when their replacements are selected. Do not disable existing
protections without their replacements. Confirm the release bot can push its changelog commit.
No local validation can establish those hosted repository settings.

Local release regression checks:

```bash
bash scripts/prepare-release-test.sh
python3 scripts/release-candidate-test.py
python3 scripts/security-report-test.py
```

The tests reject heading-only/planned/finalized empty notes, duplicate versions,
non-changelog or multiple child commits, merges and dirty tracked files; validation
modes do not rewrite the changelog. Hosted publication itself still needs a real
maintainer-authorized run after the workflow is merged.
Loading
Loading