Skip to content

ci(workflows): add CI, security, release, and supply-chain pipeline#10

Merged
cjimti merged 3 commits intomainfrom
feat/bootstrap-ci
May 6, 2026
Merged

ci(workflows): add CI, security, release, and supply-chain pipeline#10
cjimti merged 3 commits intomainfrom
feat/bootstrap-ci

Conversation

@cjimti
Copy link
Copy Markdown
Contributor

@cjimti cjimti commented May 6, 2026

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 .go file.

Summary

  • 8 GitHub Actions workflows wired to the events called for in spec §14.2 (PR, push to main, weekly cron, tag push, branch-protection rule, manual dispatch).
  • Every third-party action SHA-pinned with the human-readable version as a trailing comment, per spec §14.10. SHAs were fetched live from the GitHub API immediately before writing this PR.
  • GoReleaser configuration with five-arch cross-compile, Cosign keyless signing, dual-format SBOMs (CycloneDX + SPDX), conventional-commits changelog, and an optional Homebrew tap publish step that skips silently when the tap token is absent.
  • Codecov configuration with the spec's core/ >80% gate, cmd//examples//docs//tests excluded.
  • Operator runbook at docs/ci.md covering workflow inventory, secret matrix, action-pinning policy, release process with verification commands, branch-protection setup, and troubleshooting.

Phase mapping

Phase Issue Branch Status
1. Repo skeleton & hygiene #2 feat/bootstrap-skeleton merged (#9)
2. CI & release pipeline (this PR) #3 feat/bootstrap-ci open
3. core/event + core/provider #4 feat/bootstrap-event-provider next
4. core/mcp + core/session #5 feat/bootstrap-mcp-session
5. core/loop + core/router + core/approval #6 feat/bootstrap-loop-router-approval
6. cmd/ask CLI #7 feat/bootstrap-ask
7. examples/acme-revenue + ADR + architecture doc #8 feat/bootstrap-acme-example

Workflow inventory

ci.yml

Pipeline: detect → (linttestbuild) → ci-pass.

  • detect runs find . -name '*.go' -not -path './.*' -not -path '*/testdata/*' -print -quit and exposes a has-source output. Downstream jobs use if: needs.detect.outputs.has-source == 'true' to gate themselves until phase 3 lands.
  • lint runs go 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.
  • test runs on ubuntu-latest and macos-latest with go test -race -shuffle=on -count=1 -covermode=atomic -coverprofile=coverage.out ./.... Coverage uploaded to Codecov from ubuntu-latest only with the core flag.
  • build cross-compiles on ubuntu-latest across darwin/{amd64,arm64}, linux/{amd64,arm64}, windows/amd64 with CGO_ENABLED=0 and -trimpath.
  • ci-pass is an aggregator that surfaces a single status check for branch protection. It tolerates skipped from test/build (empty-tree case) but not from lint.

security.yml

Four parallel scanners. All findings flow through SARIF into the GitHub security tab.

  • gosec with -fmt sarif -no-fail, uploaded via github/codeql-action/upload-sarif.
  • govulncheck invoked through go tool govulncheck ./...; runs even on the empty tree to validate the module graph.
  • semgrep runs in the official Semgrep container pinned to a multi-arch manifest digest, with p/security-audit, p/secrets, p/golang, and p/owasp-top-ten rulesets. SARIF upload via the CodeQL action.
  • trivy filesystem scan; exit-code: 0 so the SARIF gate (visible in the security tab) is the source of truth, not a forced PR failure.

codeql.yml

Go analysis with the security-extended and security-and-quality query packs. Init/autobuild/analyze gated behind the same detect step so the workflow is a no-op until source exists.

scorecard.yml

Weekly + push-to-main + branch_protection_rule triggers. 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.yml

PR-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.yml

Conventional 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.yml

Nightly cron at 04:00 UTC plus workflow_dispatch with a configurable duration input (default 5m). The detect job dynamically discovers Fuzz* targets via go test -list 'Fuzz.*' and emits a JSON matrix; the fuzz job fans out one runner per (package, FuzzXxx) pair. Failures upload testdata/fuzz/ as a workflow artifact for triage. No-op when no targets exist.

release.yml

Tag-driven (v*.*.*) with manual workflow_dispatch fallback. Single release job 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-provenance over dist/*.{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.yaml

  • homebrew_casks (post-brews-deprecation) with binaries: [ask] (post-binary-deprecation), pushing to plexara/homebrew-tap under Casks/. skip_upload: auto so an absent HOMEBREW_TAP_GITHUB_TOKEN doesn't fail the release.
  • 5-arch cross-compile (darwin/{amd64,arm64}, linux/{amd64,arm64}, windows/amd64) with CGO_ENABLED=0, -trimpath, -s -w, and -X main.{version,commit,date,builtBy} ldflags.
  • tar.gz archives (zip for Windows) carrying LICENSE, README.md, SECURITY.md.
  • cosign sign-blob over the SHA-256 checksums file with --yes (CI-safe; OIDC issuer is token.actions.githubusercontent.com).
  • Dual-format SBOMs per archive: CycloneDX + SPDX, both via Syft.
  • Conventional Commits changelog with grouped sections (Features / Bug fixes / Performance / Documentation / Other) and noisy categories (chore(deps):, chore(ci):, test:, ci:, merge commits) filtered out.

.github/codecov.yml

  • coverage.range: 70...95, precision: 2, round: down.
  • core flag scoped to core/ paths only, gated at 80% target with 1pp threshold on both project and patch checks. if_ci_failed: error so a CI failure also fails the coverage check.
  • cmd/, examples/, docs/, **/testdata/**, **/*_test.go, and **/mocks/** are ignored.
  • Comment layout: reach,diff,flags,files,footer.

docs/ci.md

Single source of truth for operating the pipeline. Sections:

  • Workflow inventory table (trigger × purpose).
  • Secrets matrix with exact names, criticality, and how to obtain each PAT (fine-grained scoping for the optional ones).
  • Action-pinning policy with example syntax and Dependabot's role.
  • Coverage gate explanation tying the codecov.yml back to spec §14.3.
  • Empty source-tree handling explanation.
  • Release process: tag → workflow → artifact verification commands (cosign + gh attestation verify).
  • Branch-protection setup checklist with the exact required-status-check names (ci pass, lint, analyze go, govulncheck, gosec, semgrep, trivy fs, review, conventional commit).
  • Troubleshooting for the most common failure modes.

Hard requirements verified

  • actionlint -color over all 8 workflows → exit 0
  • goreleaser check → 1 configuration file(s) validated, no deprecations
  • go tool golangci-lint config verify → exit 0
  • All 10 YAML files parse cleanly via python3 -c yaml.safe_load
  • 16 distinct third-party actions SHA-pinned with # vX.Y.Z trailing comments

Empty source-tree handling

Until phase 3 (#4) lands the first Go source file:

  • ci-pass aggregator returns success.
  • go mod verify, go mod tidy -diff, golangci-lint config verify, and govulncheck run unconditionally and stay green.
  • All other steps (lint, vet, test, build, CodeQL, gosec, Semgrep) skip themselves with a log message via the detect job's has-source output.

The first time core/event/event.go (or any other .go file) lands, the detect job flips and the rest of the pipeline activates without any workflow change.

Out of scope (deferred or manual)

  • Branch protection rules on main. Cannot be configured in workflow YAML; requires manual setup under repo Settings. docs/ci.md lists the exact required-status-check names and ancillary toggles.
  • Self-hosted integration runner. Phase 3+ may revisit if the integration test suite outgrows ubuntu-latest, but spec §14.3 explicitly defers integration tests behind a build tag and runner.
  • Container image publishing to ghcr.io. Spec §14.8 says "added when the project starts publishing images" — not v0.1.

Test plan

  • Push triggers ci.yml. detect job sets has-source=false. lint runs (go mod verify, go mod tidy -diff, golangci-lint config verify) and passes. test and build jobs skip with the empty-tree message. ci-pass aggregator is green.
  • security.yml runs. govulncheck passes. gosec, semgrep, and Trivy gate themselves out / produce empty SARIF. trivy fs runs on the source tree and reports any container/IaC findings (none expected).
  • codeql.yml runs. analyze go skips with empty-tree message; check stays green.
  • dependency-review.yml runs on this PR. Confirms allowlisted licenses, no high-severity vulns introduced.
  • pr-title.yml validates this PR's title against the Conventional Commits spec (ci(workflows): ...).
  • scorecard.yml runs on push to main after merge; confirm it shows up in the security tab.
  • After merge, set HOMEBREW_TAP_GITHUB_TOKEN (optional) and SCORECARD_READ_TOKEN (optional). Configure branch protection per docs/ci.md.
  • Phase 3 (Phase 3: core/event + core/provider #4) lands the first .go file; CI activates the gated steps automatically.

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.
@github-advanced-security
Copy link
Copy Markdown

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:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

cjimti added 2 commits May 5, 2026 19:50
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.
@cjimti cjimti merged commit cd7fe6e into main May 6, 2026
14 checks passed
@cjimti cjimti deleted the feat/bootstrap-ci branch May 6, 2026 03:09
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.

Phase 2: CI & release pipeline

2 participants