ci(workflows): add CI, security, release, and supply-chain pipeline#10
Merged
ci(workflows): add CI, security, release, and supply-chain pipeline#10
Conversation
Implements phase 2 of the v0.1 bootstrap (#1). Adds eight GitHub Actions workflows, the GoReleaser configuration, the Codecov configuration, and an operator's runbook. Workflows - ci.yml build matrix, lint, test (race+shuffle+coverage), Codecov upload from ubuntu-latest. Test on ubuntu-latest + macos-latest. Build matrix: darwin/{amd64,arm64}, linux/{amd64,arm64}, windows/amd64. ci-pass aggregator job for branch protection. - security.yml gosec (SARIF), govulncheck, Semgrep (container pinned by manifest digest with OWASP/golang/ security-audit/secrets rulesets), Trivy fs scan. - codeql.yml Go, security-extended + security-and-quality. - scorecard.yml weekly, on-push, and branch_protection_rule. Opportunistically uses SCORECARD_READ_TOKEN. - dependency-review.yml fail-on-severity: high; license allowlist mirrors spec §14.7. - pr-title.yml Conventional Commits PR title check via amannn/action-semantic-pull-request. - fuzz.yml nightly cron + manual dispatch. Dynamically discovers Fuzz* targets and fans them out across a matrix. - release.yml tag-driven. GoReleaser + Cosign keyless signing + Syft SBOMs + actions/attest-build-provenance for SLSA v1 in-toto provenance. Optional Homebrew tap publish via HOMEBREW_TAP_GITHUB_TOKEN. Configurations - .goreleaser.yaml homebrew_casks (post-brews-deprecation), 5-arch cross-compile, cosign blob signing of checksums, CycloneDX + SPDX SBOMs per archive, Conventional Commits changelog, skip_upload: auto on the tap. - .github/codecov.yml core/ flag at 80% target with 1pp threshold; cmd/, examples/, docs/, tests, and mocks ignored. Documentation - docs/ci.md operator runbook: workflow inventory, secret matrix, action-pinning policy, release flow with verification commands, branch-protection setup, troubleshooting. Hard requirements - Every third-party action is SHA-pinned with the human-readable version as a trailing comment, per spec §14.10. SHAs fetched live via the GitHub API before writing. - Empty source-tree tolerant: a detect job decides whether lint/ test/build/CodeQL/gosec/semgrep steps run. go mod verify, go mod tidy -diff, and golangci-lint config verify always run. When phase 3 (#4) lands the first Go source file, the rest activates automatically. - Out of scope, manual: branch protection on main (docs/ci.md lists the required status-check names), and any first-time secret creation (SCORECARD_READ_TOKEN, HOMEBREW_TAP_GITHUB_TOKEN). Verified locally - actionlint -color exit 0 - goreleaser check 1 config validated, no deprecations - go tool golangci-lint config verify exit 0 - python3 -c yaml.safe_load all 10 YAML files parse Closes #3.
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
Fixes the blocker (fuzz matrix builder) and seven should-fix / nit
items surfaced by the review.
Blocker
- fuzz.yml: the previous awk pipeline never associated FuzzXxx names
with their packages because `go test -list` prints the names
*before* the trailing "ok\t<pkg>\t<time>" line. Verified against a
real Go module: the prior implementation produced
["::FuzzAlpha","::FuzzBravo"] (empty pkg). Rewrote using
`go list ./...` + per-package `go test -list 'Fuzz.*' "$pkg"` so
each fuzz name is paired with its origin package. Confirmed
output now: ["example.com/fuzztest::FuzzAlpha", "...::FuzzBravo"].
Should-fix
- ci.yml: `go mod tidy -diff` ran twice (once silently, once
captured). Now a single invocation captured into a variable;
the diff is printed when drift is detected.
- release.yml: dropped `packages: write` permission. Container
image publishing to ghcr is deferred per spec §14.8 and the
scope was unused. Comment left for when that work lands.
- release.yml: added `env.GO_VERSION` so the Go version matches
ci.yml in one place rather than being duplicated as a literal.
- ci.yml / codeql.yml / security.yml: dropped the redundant
`<workflow-name>-` prefix from `concurrency.group`.
`${{ github.workflow }}` already disambiguates per workflow.
- ci.yml / codeql.yml / security.yml: `cancel-in-progress` is now
conditional on `github.event_name == 'pull_request'`. Pushes to
`main` (and tagged release builds) run to completion rather than
being cancelled by a rapid follow-up push.
- ci.yml / codeql.yml / security.yml: detect step's `find` now
excludes `./vendor/*` in addition to dotfiles and testdata. We
do not vendor (spec §14.9), but this guards against accidental
`go mod vendor`.
Nits
- scorecard.yml: workflow-level `permissions: read-all` was dead
(single job overrides). Now `contents: read` at workflow scope.
- pr-title.yml: added `style` to the Conventional Commits allowed
types list.
Not applied (intentional)
- `actions/attest-build-provenance` vs `slsa-framework/slsa-github-
generator`: kept the former. Modern Sigstore path, verifiable via
`gh attestation verify`.
- cosign signing the checksums file rather than each artifact:
kept. GoReleaser-recommended pattern; functionally equivalent to
per-artifact signing via the checksum chain.
- codecov `if_ci_failed: error`: kept. CI failure means coverage is
unreliable; erroring the check is the desired interlock.
Verified locally
- actionlint -color exit 0
- goreleaser check 1 config validated, no deprecations
- go tool golangci-lint config verify exit 0
- replacement fuzz.sh against real go test -list matrix correct
govulncheck exits 1 with "no packages matched the provided patterns"
when no Go source exists, breaking the security workflow on every
empty-tree PR. Same failure mode as `go test ./...`. Adds the same
`if: needs.detect.outputs.has-source == 'true'` gate already used by
gosec, semgrep, and the codeql/test/build jobs.
Updates docs/ci.md:
- Lists govulncheck as a source-dependent (gated) check.
- Adds a branch-protection note: do not require source-dependent
status checks until after phase 3 lands the first .go file —
skipped jobs do not satisfy a required status check.
Verified: actionlint -color exit 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3. Part of #1 (project bootstrap, v0.1).
This is phase 2 of 7 in the v0.1 bootstrap. It lands the entire CI / security / release pipeline before any Go source exists, so subsequent phases (#4 through #8) merge with all gates already enforced. The workflows are designed to tolerate the empty source tree and to activate automatically once phase 3 (#4) introduces the first
.gofile.Summary
main, weekly cron, tag push, branch-protection rule, manual dispatch).core/>80% gate,cmd//examples//docs//tests excluded.docs/ci.mdcovering workflow inventory, secret matrix, action-pinning policy, release process with verification commands, branch-protection setup, and troubleshooting.Phase mapping
feat/bootstrap-skeletonfeat/bootstrap-cicore/event+core/providerfeat/bootstrap-event-providercore/mcp+core/sessionfeat/bootstrap-mcp-sessioncore/loop+core/router+core/approvalfeat/bootstrap-loop-router-approvalcmd/askCLIfeat/bootstrap-askexamples/acme-revenue+ ADR + architecture docfeat/bootstrap-acme-exampleWorkflow inventory
ci.ymlPipeline:
detect→ (lint‖test‖build) →ci-pass.detectrunsfind . -name '*.go' -not -path './.*' -not -path '*/testdata/*' -print -quitand exposes ahas-sourceoutput. Downstream jobs useif: needs.detect.outputs.has-source == 'true'to gate themselves until phase 3 lands.lintrunsgo mod verify,go mod tidy -diff(fails on drift),gofmt -l,go vet,golangci-lint config verify,golangci-lint run. The first three run unconditionally so config / module-graph regressions are caught immediately even with no source.testruns onubuntu-latestandmacos-latestwithgo test -race -shuffle=on -count=1 -covermode=atomic -coverprofile=coverage.out ./.... Coverage uploaded to Codecov fromubuntu-latestonly with thecoreflag.buildcross-compiles onubuntu-latestacrossdarwin/{amd64,arm64},linux/{amd64,arm64},windows/amd64withCGO_ENABLED=0and-trimpath.ci-passis an aggregator that surfaces a single status check for branch protection. It toleratesskippedfromtest/build(empty-tree case) but not fromlint.security.ymlFour parallel scanners. All findings flow through SARIF into the GitHub security tab.
gosecwith-fmt sarif -no-fail, uploaded viagithub/codeql-action/upload-sarif.govulncheckinvoked throughgo tool govulncheck ./...; runs even on the empty tree to validate the module graph.semgrepruns in the official Semgrep container pinned to a multi-arch manifest digest, withp/security-audit,p/secrets,p/golang, andp/owasp-top-tenrulesets. SARIF upload via the CodeQL action.trivyfilesystem scan;exit-code: 0so the SARIF gate (visible in the security tab) is the source of truth, not a forced PR failure.codeql.ymlGo analysis with the
security-extendedandsecurity-and-qualityquery packs. Init/autobuild/analyze gated behind the samedetectstep so the workflow is a no-op until source exists.scorecard.ymlWeekly + push-to-main +
branch_protection_ruletriggers. Uses${{ secrets.SCORECARD_READ_TOKEN || secrets.GITHUB_TOKEN }}so the workflow runs without the optional fine-grained PAT (Branch-Protection check returns?until the PAT is added). Results published and uploaded as SARIF.dependency-review.ymlPR-only.
fail-on-severity: high,comment-summary-in-pr: on-failure, license allowlist mirroring spec §14.7 (Apache-2.0, MIT, BSD-2/3-Clause, ISC, MPL-2.0, Unlicense, CC0-1.0, Zlib, BSD-2-Clause-Patent).pr-title.ymlConventional Commits enforcement via
amannn/action-semantic-pull-request. Allowed types:feat,fix,docs,chore,refactor,test,perf,ci,build,revert. Subject must start with an alphanumeric character.fuzz.ymlNightly cron at 04:00 UTC plus
workflow_dispatchwith a configurabledurationinput (default5m). Thedetectjob dynamically discoversFuzz*targets viago test -list 'Fuzz.*'and emits a JSON matrix; thefuzzjob fans out one runner per(package, FuzzXxx)pair. Failures uploadtestdata/fuzz/as a workflow artifact for triage. No-op when no targets exist.release.ymlTag-driven (
v*.*.*) with manualworkflow_dispatchfallback. Singlereleasejob with the full provenance permission set (contents: write,packages: write,id-token: write,attestations: write).Steps: harden-runner → checkout (
fetch-depth: 0) → setup-go → install Cosign → install Syft → GoReleaser (~> v2,release --clean) →actions/attest-build-provenanceoverdist/*.{tar.gz,zip,sbom.*}→ SBOM upload as workflow artifact.The provenance step produces SLSA v1 in-toto attestations signed via Sigstore (keyless OIDC), verifiable with
gh attestation verify..goreleaser.yamlhomebrew_casks(post-brews-deprecation) withbinaries: [ask](post-binary-deprecation), pushing toplexara/homebrew-tapunderCasks/.skip_upload: autoso an absentHOMEBREW_TAP_GITHUB_TOKENdoesn't fail the release.darwin/{amd64,arm64},linux/{amd64,arm64},windows/amd64) withCGO_ENABLED=0,-trimpath,-s -w, and-X main.{version,commit,date,builtBy}ldflags.tar.gzarchives (zipfor Windows) carryingLICENSE,README.md,SECURITY.md.cosign sign-blobover the SHA-256 checksums file with--yes(CI-safe; OIDC issuer istoken.actions.githubusercontent.com).chore(deps):,chore(ci):,test:,ci:, merge commits) filtered out..github/codecov.ymlcoverage.range: 70...95,precision: 2,round: down.coreflag scoped tocore/paths only, gated at 80% target with 1pp threshold on bothprojectandpatchchecks.if_ci_failed: errorso a CI failure also fails the coverage check.cmd/,examples/,docs/,**/testdata/**,**/*_test.go, and**/mocks/**are ignored.reach,diff,flags,files,footer.docs/ci.mdSingle source of truth for operating the pipeline. Sections:
gh attestation verify).ci pass,lint,analyze go,govulncheck,gosec,semgrep,trivy fs,review,conventional commit).Hard requirements verified
actionlint -colorover all 8 workflows → exit 0goreleaser check→ 1 configuration file(s) validated, no deprecationsgo tool golangci-lint config verify→ exit 0python3 -c yaml.safe_load# vX.Y.Ztrailing commentsEmpty source-tree handling
Until phase 3 (#4) lands the first Go source file:
ci-passaggregator returns success.go mod verify,go mod tidy -diff,golangci-lint config verify, andgovulncheckrun unconditionally and stay green.detectjob'shas-sourceoutput.The first time
core/event/event.go(or any other.gofile) lands, thedetectjob flips and the rest of the pipeline activates without any workflow change.Out of scope (deferred or manual)
main. Cannot be configured in workflow YAML; requires manual setup under repo Settings.docs/ci.mdlists the exact required-status-check names and ancillary toggles.ghcr.io. Spec §14.8 says "added when the project starts publishing images" — not v0.1.Test plan
ci.yml.detectjob setshas-source=false.lintruns (go mod verify,go mod tidy -diff,golangci-lint config verify) and passes.testandbuildjobs skip with the empty-tree message.ci-passaggregator is green.security.ymlruns.govulncheckpasses.gosec,semgrep, and Trivy gate themselves out / produce empty SARIF.trivy fsruns on the source tree and reports any container/IaC findings (none expected).codeql.ymlruns.analyze goskips with empty-tree message; check stays green.dependency-review.ymlruns on this PR. Confirms allowlisted licenses, no high-severity vulns introduced.pr-title.ymlvalidates this PR's title against the Conventional Commits spec (ci(workflows): ...).scorecard.ymlruns on push to main after merge; confirm it shows up in the security tab.HOMEBREW_TAP_GITHUB_TOKEN(optional) andSCORECARD_READ_TOKEN(optional). Configure branch protection perdocs/ci.md..gofile; CI activates the gated steps automatically.