From 2871f2737dd6e74bd0cd041c7765e2b5eb498c32 Mon Sep 17 00:00:00 2001 From: Tri Lam Date: Sat, 30 May 2026 18:14:13 -0700 Subject: [PATCH] =?UTF-8?q?chore(pivot):=20wave-1=20self-review=20fixes=20?= =?UTF-8?q?=E2=80=94=20delete=20archive=20+=20drift=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of PR #174 found drift between docs and the new release.yml, plus a half-measure "archive" that violates the no-bloat directive. Fixes: - DELETE `.github/workflows/archived/release.yml.legacy`. Archive is a half-measure — git history is the archive. Removes the empty `.github/workflows/archived/` directory too. - DROP `workflow_dispatch` trigger from release.yml. Tag-push is the only legitimate release path; manual dispatch was a bypass scenario that complicated signing identity verification (cosign verify-blob hardcodes `--certificate-github-workflow-trigger 'push'`). - FIX `docs/reproducibility.md` workflow path drift: 4 refs to non-existent `goreleaser.yml` → correct `release.yml`. - FIX `.goreleaser.yaml` comment: removed reference to non-existent "reproducible flag" — determinism comes from `mod_timestamp` (already in the config). - UPDATE prose refs in CHANGELOG.md, MILESTONES.md, docs/notes/ci.md, release.yml header — "archived under .github/workflows/archived/" → "preserved in git history". Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Tri Lam --- .github/workflows/archived/release.yml.legacy | 686 ------------------ .github/workflows/release.yml | 33 +- .goreleaser.yaml | 5 +- CHANGELOG.md | 2 +- MILESTONES.md | 4 +- docs/notes/ci.md | 10 +- docs/reproducibility.md | 6 +- 7 files changed, 21 insertions(+), 725 deletions(-) delete mode 100644 .github/workflows/archived/release.yml.legacy diff --git a/.github/workflows/archived/release.yml.legacy b/.github/workflows/archived/release.yml.legacy deleted file mode 100644 index d3c95dd3..00000000 --- a/.github/workflows/archived/release.yml.legacy +++ /dev/null @@ -1,686 +0,0 @@ -# ⚠️ STATUS (2026-05-22): scheduled for replacement at v0.1.0. -# RFC-0013 §2 + §Migration: this workflow will be rewritten using -# goreleaser + slsa-github-generator + sigstore/cosign-installer + -# anchore/sbom-action + actions/attest-build-provenance. The old -# workflow will be archived under .github/workflows/archived/. -# Do not invest in hand-rolled additions here. - -name: release - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - tag: - description: 'Tag to build (workflow_dispatch only; uses github.ref otherwise)' - required: false - type: string - -permissions: - contents: read - -env: - GO_VERSION_FILE: go.mod - BINARY_BASENAME: tracecore_linux_amd64 - CYCLONEDX_GOMOD_VERSION: v1.10.0 - # Image publish destination. Chart's values.yaml `image.repository` - # default points here; drift between the two is the contract this - # job upholds. Lowercase per OCI distribution spec. - IMAGE_REPO: ghcr.io/tracecoreai/tracecore - -jobs: - build: - name: build (reproducible verify) - runs-on: ubuntu-latest - # Hard ceiling on a stuck job. The two cold-cache Go builds + diffoscope - # comparison fit comfortably under 10 minutes on ubuntu-latest; 20 leaves - # headroom for apt mirror weather without letting a hang burn billing. - timeout-minutes: 20 - permissions: - contents: read - outputs: - digest: ${{ steps.digest.outputs.value }} - tag: ${{ steps.tag.outputs.value }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: ${{ env.GO_VERSION_FILE }} - # Cache keyed on go.sum — the same trust root M3's reproducible - # rebuild + cosign + SLSA provenance already validates. - cache: true # zizmor: ignore[cache-poisoning] - - - name: Resolve tag - id: tag - env: - # Pass via env so the shell quotes the value — direct - # `${{ inputs.tag }}` in `run:` is a template-injection vector. - INPUT_TAG: ${{ inputs.tag }} - run: | - set -euo pipefail - tag="$INPUT_TAG" - if [ -z "$tag" ]; then - tag="${GITHUB_REF#refs/tags/}" - fi - if [ -z "$tag" ] || [ "$tag" = "$GITHUB_REF" ]; then - echo "::error::no tag resolvable from ref=$GITHUB_REF or input" - exit 1 - fi - echo "value=$tag" >> "$GITHUB_OUTPUT" - - - name: Verify dispatch ref matches tag (pre-flight) - if: github.event_name == 'workflow_dispatch' - env: - INPUT_TAG: ${{ inputs.tag }} - run: | - set -euo pipefail - # On workflow_dispatch with `inputs.tag` set, github.ref MUST be - # refs/tags/$inputs.tag. Otherwise the OIDC ref claim is the - # dispatched branch ref (refs/heads/) and both the - # cosign verify and gh-attestation-verify smoke checks will - # refuse the artifact 15-30 minutes into the run. Fail fast - # here so the operator sees the misuse + workaround in seconds. - if [ -n "$INPUT_TAG" ] && [ "$GITHUB_REF" != "refs/tags/$INPUT_TAG" ]; then - echo "::error::workflow_dispatch with inputs.tag=$INPUT_TAG must be triggered from --ref refs/tags/$INPUT_TAG (got $GITHUB_REF)" - echo "::error::workaround: gh workflow run release.yml --ref refs/tags/$INPUT_TAG" - exit 1 - fi - - - name: Compute SOURCE_DATE_EPOCH - id: sde - env: - TAG: ${{ steps.tag.outputs.value }} - run: | - set -euo pipefail - epoch=$(git log -1 --pretty=%ct "$TAG") - build_date=$(date -u -d "@$epoch" +%Y-%m-%dT%H:%M:%SZ) - echo "epoch=$epoch" >> "$GITHUB_OUTPUT" - echo "build_date=$build_date" >> "$GITHUB_OUTPUT" - - - name: Install diffoscope - run: | - set -euo pipefail - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends diffoscope-minimal - diffoscope --version - - - name: Verify module hashes (go mod download + verify) - run: | - set -euo pipefail - # Defense in depth against a compromised GOPROXY mirror returning - # contents that don't match go.sum. `download` re-verifies on fetch; - # `verify` catches an already-poisoned cache. Both are idempotent. - # Trust root: the go.sum at this tag commit. A poisoned go.sum - # itself is a separate threat addressed by a tag-protection - # ruleset (tracked in FOLLOWUPS.md §M3). - go mod download - go mod verify - - - name: Build #1 - env: - # Canonical reproducible-builds.org stanza (LC_ALL/TZ). -trimpath + - # SOURCE_DATE_EPOCH already carry the determinism load for Go-only - # output; the locale/timezone pins are cheap insurance against - # future cgo additions or non-Go release artifacts. - SOURCE_DATE_EPOCH: ${{ steps.sde.outputs.epoch }} - CGO_ENABLED: '0' - GOOS: linux - GOARCH: amd64 - LC_ALL: C - TZ: UTC - VERSION: ${{ steps.tag.outputs.value }} - run: | - set -euo pipefail - umask 022 - # mktemp dir outside the worktree keeps `git status --porcelain` - # empty, which keeps Go's `-buildvcs=true` from flipping - # vcs.modified=true and changing NT_GNU_BUILD_ID. Isolated GOCACHE - # forces a cold compile so build #2 can't replay #1. - out_dir=$(mktemp -d -t build1.XXXXXX) - GOCACHE=$(mktemp -d -t gocache1.XXXXXX) \ - make build BIN="$out_dir/$BINARY_BASENAME" - sha256sum "$out_dir/$BINARY_BASENAME" | tee "$out_dir/$BINARY_BASENAME.sha256" - echo "BUILD1_DIR=$out_dir" >> "$GITHUB_ENV" - - - name: Build #2 (cold rebuild) - env: - SOURCE_DATE_EPOCH: ${{ steps.sde.outputs.epoch }} - CGO_ENABLED: '0' - GOOS: linux - GOARCH: amd64 - LC_ALL: C - TZ: UTC - VERSION: ${{ steps.tag.outputs.value }} - run: | - set -euo pipefail - umask 022 - out_dir=$(mktemp -d -t build2.XXXXXX) - GOCACHE=$(mktemp -d -t gocache2.XXXXXX) \ - make build BIN="$out_dir/$BINARY_BASENAME" - sha256sum "$out_dir/$BINARY_BASENAME" | tee "$out_dir/$BINARY_BASENAME.sha256" - echo "BUILD2_DIR=$out_dir" >> "$GITHUB_ENV" - - - name: Assert byte-identical (sha256 + diffoscope) - run: | - set -uo pipefail - d1=$(awk '{print $1}' "$BUILD1_DIR/$BINARY_BASENAME.sha256") - d2=$(awk '{print $1}' "$BUILD2_DIR/$BINARY_BASENAME.sha256") - if [ "$d1" = "$d2" ]; then - echo "sha256 match: $d1" - # diffoscope --exit-code is unsupported on diffoscope-minimal in - # ubuntu-latest's apt repo; its default exit status (0 = no diff, - # 1 = diff) carries the same signal. - diffoscope "$BUILD1_DIR/$BINARY_BASENAME" "$BUILD2_DIR/$BINARY_BASENAME" - exit $? - fi - echo "::error::reproducibility broken: build1=$d1 build2=$d2" - diffoscope --max-text-report-size 1048576 "$BUILD1_DIR/$BINARY_BASENAME" "$BUILD2_DIR/$BINARY_BASENAME" || true - exit 1 - - - name: Stage canonical binary - run: | - set -euo pipefail - mkdir -p release - cp "$BUILD1_DIR/$BINARY_BASENAME" "release/$BINARY_BASENAME" - - - name: Upload both builds on failure for offline triage - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: failed-build-pair - path: | - ${{ env.BUILD1_DIR }}/${{ env.BINARY_BASENAME }} - ${{ env.BUILD2_DIR }}/${{ env.BINARY_BASENAME }} - if-no-files-found: error - retention-days: 7 - - - name: Assert -trimpath + SOURCE_DATE_EPOCH honored - env: - EXPECTED_BUILD_DATE: ${{ steps.sde.outputs.build_date }} - run: | - set -euo pipefail - grep -F -- '-trimpath' Makefile >/dev/null - grep -F -- 'SOURCE_DATE_EPOCH' Makefile >/dev/null - if ! strings "release/$BINARY_BASENAME" | grep -F -q -- "$EXPECTED_BUILD_DATE"; then - echo "::error::BuildDate=$EXPECTED_BUILD_DATE not embedded in binary" - exit 1 - fi - - - name: Digest of release artifact - id: digest - run: | - set -euo pipefail - digest=$(sha256sum "release/$BINARY_BASENAME" | awk '{print $1}') - echo "value=$digest" >> "$GITHUB_OUTPUT" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: binary - path: release/${{ env.BINARY_BASENAME }} - if-no-files-found: error - retention-days: 7 - - sbom: - name: sbom (cyclonedx) - runs-on: ubuntu-latest - needs: build - timeout-minutes: 15 - permissions: - contents: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # Pin by commit SHA, not tag, so a force-push to the tag - # between the build and sbom jobs cannot produce an SBOM - # for a different tree than the binary that was signed. - ref: ${{ github.sha }} - - - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: ${{ env.GO_VERSION_FILE }} - # Cache keyed on go.sum — the same trust root M3's reproducible - # rebuild + cosign + SLSA provenance already validates. - cache: true # zizmor: ignore[cache-poisoning] - - - name: Install cyclonedx-gomod - env: - VERSION: ${{ env.CYCLONEDX_GOMOD_VERSION }} - run: | - set -euo pipefail - go install "github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@$VERSION" - cyclonedx-gomod version - - - name: Generate SBOM - env: - OUT: tracecore.sbom.cdx.json - run: | - set -euo pipefail - cyclonedx-gomod mod -licenses -json -output "$OUT" - test -s "$OUT" - jq -e '.bomFormat == "CycloneDX"' "$OUT" >/dev/null - # Coverage check per MILESTONES §M21 NF rubric — every direct - # go.mod require that actually ships in the binary must appear - # as a component. cyclonedx-gomod's `mod` excludes test-only - # direct requires (testify, goleak) because they don't reach - # the main module's import graph, so we filter the expected - # set against `go list -deps ./cmd/tracecore` to avoid demanding - # SBOM entries for code that isn't in the linked binary. - runtime_mods=$(GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ - go list -deps -f '{{if .Module}}{{.Module.Path}}{{end}}' ./cmd/tracecore \ - | sort -u) - missing="" - while IFS= read -r path; do - [ -z "$path" ] && continue - printf '%s\n' "$runtime_mods" | grep -qx "$path" || continue - jq -e --arg p "$path" \ - '.components[] | select(.purl | startswith("pkg:golang/" + $p + "@"))' \ - "$OUT" >/dev/null \ - || missing="${missing}${path}\n" - done < <(go mod edit -json | jq -r '.Require[] | select(.Indirect != true) | .Path') - if [ -n "$missing" ]; then - echo "::error::SBOM is missing components for runtime direct modules:" - printf '%b' "$missing" - exit 1 - fi - components=$(jq '.components | length' "$OUT") - echo "SBOM components: $components (runtime direct modules all covered)" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: sbom - path: tracecore.sbom.cdx.json - if-no-files-found: error - retention-days: 7 - - sign: - name: sign (cosign keyless) - runs-on: ubuntu-latest - needs: build - # OIDC token + Fulcio + Rekor round-trip; usually <2min. Tighter cap so a - # Sigstore service degradation fails fast instead of stalling the release. - timeout-minutes: 10 - permissions: - id-token: write - contents: read - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: binary - - - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - - name: Verify binary digest matches build job - env: - EXPECTED: ${{ needs.build.outputs.digest }} - run: | - set -euo pipefail - actual=$(sha256sum "$BINARY_BASENAME" | awk '{print $1}') - if [ "$actual" != "$EXPECTED" ]; then - echo "::error::artifact digest drift: build=$EXPECTED downloaded=$actual" - exit 1 - fi - - - name: Sign blob (keyless) - env: - COSIGN_EXPERIMENTAL: '1' - run: | - set -euo pipefail - cosign sign-blob --yes \ - --bundle "$BINARY_BASENAME.cosign.bundle" \ - "$BINARY_BASENAME" - - - name: Verify blob signature smoke check - env: - # Pin the OIDC subject to this exact workflow file on a tag-ref — - # without this, any workflow on any branch in the repo could - # produce a bundle that passes the verifier's identity check. - IDENTITY_REGEXP: "^https://github.com/${{ github.repository }}/\\.github/workflows/release\\.yml@refs/tags/" - TAG: ${{ needs.build.outputs.tag }} - run: | - set -euo pipefail - # --certificate-github-workflow-ref pins the verify to this exact - # tag (not any tag-ref the regex would also match). --trigger pins - # the OIDC claim that this fired from a tag push, not workflow_dispatch. - # Both narrow strictly inside what IDENTITY_REGEXP already accepts. - cosign verify-blob \ - --bundle "$BINARY_BASENAME.cosign.bundle" \ - --certificate-identity-regexp "$IDENTITY_REGEXP" \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-github-workflow-ref "refs/tags/$TAG" \ - --certificate-github-workflow-trigger 'push' \ - "$BINARY_BASENAME" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cosign-bundle - path: ${{ env.BINARY_BASENAME }}.cosign.bundle - if-no-files-found: error - retention-days: 7 - - provenance: - name: provenance (SLSA v1.0) - runs-on: ubuntu-latest - needs: build - timeout-minutes: 10 - permissions: - id-token: write - attestations: write - contents: read - steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: binary - - - name: Verify binary digest matches build job - env: - EXPECTED: ${{ needs.build.outputs.digest }} - run: | - set -euo pipefail - actual=$(sha256sum "$BINARY_BASENAME" | awk '{print $1}') - if [ "$actual" != "$EXPECTED" ]; then - echo "::error::artifact digest drift: build=$EXPECTED downloaded=$actual" - exit 1 - fi - - - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - id: attest - with: - subject-path: ${{ env.BINARY_BASENAME }} - - - name: Export attestation bundle + assert SLSA v1.0 predicateType - env: - BUNDLE_PATH: ${{ steps.attest.outputs.bundle-path }} - run: | - set -euo pipefail - if [ -z "$BUNDLE_PATH" ] || [ ! -f "$BUNDLE_PATH" ]; then - echo "::error::attest-build-provenance produced no bundle" - exit 1 - fi - cp "$BUNDLE_PATH" "$BINARY_BASENAME.intoto.jsonl" - payload=$(jq -r .dsseEnvelope.payload < "$BINARY_BASENAME.intoto.jsonl" | base64 -d) - predicate=$(echo "$payload" | jq -r '.predicateType') - if [ "$predicate" != "https://slsa.dev/provenance/v1" ]; then - echo "::error::predicateType=$predicate (want https://slsa.dev/provenance/v1)" - exit 1 - fi - subject_digest=$(echo "$payload" | jq -r '.subject[0].digest.sha256') - if [ "$subject_digest" != "${{ needs.build.outputs.digest }}" ]; then - echo "::error::provenance subject digest=$subject_digest mismatch" - exit 1 - fi - - - name: Smoke-check gh attestation verify (tag + source-digest pinned) - env: - TAG: ${{ needs.build.outputs.tag }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - # Local-bundle verify (the .intoto.jsonl carries the Fulcio cert, - # SCT, and Rekor inclusion proof, so verification works offline; - # no Sigstore-API round-trip = no propagation flake at release - # time). Live certificate revocation isn't reachable mid-workflow - # anyway — third-party verifiers run the full stack against the - # published artifacts per docs/reproducibility.md step 6. - # - # Flag set matches docs/reproducibility.md so a verifier running - # the documented walkthrough exercises the same identity binding: - # * --signer-workflow pins the OIDC subject path to - # release.yml, not any workflow in the repo with attestations - # write permission. - # * --predicate-type pins SLSA v1 explicitly (today's default, - # but explicit beats default-drift). - # * --source-ref / --source-digest filter against Fulcio cert - # OIDs 1.3.6.1.4.1.57264.1.14 (SourceRepositoryRef) and .13 - # (SourceRepositoryDigest), populated by GitHub Actions' OIDC - # token claims `ref` and `sha`. For the canonical tag-push - # trigger those equal refs/tags/$TAG and $GITHUB_SHA, so the - # smoke check refuses any attestation whose OIDC identity - # disagrees with this run's tag. - # - # Scope note: a workflow_dispatch run from a non-tag ref (using - # the inputs.tag override) records OIDC ref=refs/heads/, - # so this check will refuse it — same property the cosign - # `--certificate-github-workflow-ref refs/tags/$TAG` filter - # already enforces. To re-release a tag via dispatch, point the - # dispatch at the tag itself (gh workflow run --ref refs/tags/$TAG). - gh attestation verify "$BINARY_BASENAME" \ - --bundle "$BINARY_BASENAME.intoto.jsonl" \ - --repo "$REPO" \ - --signer-workflow "$REPO/.github/workflows/release.yml" \ - --predicate-type 'https://slsa.dev/provenance/v1' \ - --source-ref "refs/tags/$TAG" \ - --source-digest "$GITHUB_SHA" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: provenance - path: ${{ env.BINARY_BASENAME }}.intoto.jsonl - if-no-files-found: error - retention-days: 7 - - image: - name: image (build + push + sign + attest) - runs-on: ubuntu-latest - needs: build - # Buildkit cold pull of distroless + COPY layer + push to ghcr + 2× cosign - # OIDC round-trips. Real-world ~3-5min; 20 leaves slack for ghcr.io weather - # without letting a Sigstore stall block the release indefinitely. - timeout-minutes: 20 - permissions: - contents: read - packages: write # push to ghcr.io/tracecoreai/tracecore - id-token: write # cosign keyless + attest-build-provenance OIDC - attestations: write # attest-build-provenance writes a bundle - outputs: - digest: ${{ steps.push.outputs.digest }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - # Pin by commit SHA so the Dockerfile + workflow this job - # reads match the commit that ran. The binary downloaded - # below has its own digest guard (see "Verify binary digest - # matches build job"), which catches the case of a binary - # built from a different tree than the Dockerfile read here. - # Together these close the force-push window: a force-push - # to the tag between `build` and `image` cannot smuggle in - # either a different binary or a different Dockerfile. - ref: ${{ github.sha }} - persist-credentials: false - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: binary - path: release - - - name: Verify binary digest matches build job - env: - EXPECTED: ${{ needs.build.outputs.digest }} - run: | - set -euo pipefail - actual=$(sha256sum "release/$BINARY_BASENAME" | awk '{print $1}') - if [ "$actual" != "$EXPECTED" ]; then - echo "::error::artifact digest drift: build=$EXPECTED downloaded=$actual" - exit 1 - fi - - - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Compute image tags - id: meta - env: - TAG: ${{ needs.build.outputs.tag }} - IS_PRERELEASE: ${{ contains(needs.build.outputs.tag, '-') }} - run: | - set -euo pipefail - # Always tag with the release version. Only float `:latest` - # for stable releases (no `-` in the SemVer pre-release - # field); a pre-release that takes `:latest` would silently - # promote alpha bits to the chart's default-pull surface. - tags="$IMAGE_REPO:$TAG" - if [ "$IS_PRERELEASE" != "true" ]; then - tags="$tags"$'\n'"$IMAGE_REPO:latest" - fi - { - echo "tags<> "$GITHUB_OUTPUT" - - - name: Compute SOURCE_DATE_EPOCH for image layer - id: sde - env: - TAG: ${{ needs.build.outputs.tag }} - run: | - set -euo pipefail - # Same epoch as the binary build so the image layer carrying - # the binary has a deterministic mtime. Without this, two - # builds at the same SHA produce different image digests - # purely from `now()` in the COPY layer. - epoch=$(git log -1 --pretty=%ct "$TAG") - echo "epoch=$epoch" >> "$GITHUB_OUTPUT" - - - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 - id: push - env: - # SOURCE_DATE_EPOCH must reach buildkit through the build - # environment (not just as a --build-arg) so buildkit's - # layer-timestamp rewrite kicks in. The Dockerfile also - # declares `ARG SOURCE_DATE_EPOCH` so the value is visible - # to a reader of the Dockerfile alone, and so a local - # `docker buildx build` reproduces the CI image bit-for-bit. - SOURCE_DATE_EPOCH: ${{ steps.sde.outputs.epoch }} - with: - context: . - file: Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.meta.outputs.tags }} - build-args: | - BINARY_PATH=release/${{ env.BINARY_BASENAME }} - SOURCE_DATE_EPOCH=${{ steps.sde.outputs.epoch }} - provenance: false # we attest below with GitHub's reusable attester - sbom: false # SBOM ships as a release artifact, not a manifest sub-attestation - - - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - - name: Sign image (keyless) - env: - DIGEST: ${{ steps.push.outputs.digest }} - run: | - set -euo pipefail - # Sign the manifest by digest, not tag — a registry rebuild - # of `:latest` would otherwise let an attacker replace what - # `cosign verify` resolves. The digest is the trust root. - cosign sign --yes "${IMAGE_REPO}@${DIGEST}" - - - name: Verify image signature smoke check - env: - DIGEST: ${{ steps.push.outputs.digest }} - IDENTITY_REGEXP: "^https://github.com/${{ github.repository }}/\\.github/workflows/release\\.yml@refs/tags/" - TAG: ${{ needs.build.outputs.tag }} - run: | - set -euo pipefail - # Same identity-binding pattern as the binary blob verify: - # pin to release.yml on a tag ref. Mismatch fails closed. - cosign verify "${IMAGE_REPO}@${DIGEST}" \ - --certificate-identity-regexp "$IDENTITY_REGEXP" \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-github-workflow-ref "refs/tags/$TAG" \ - --certificate-github-workflow-trigger 'push' - - - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-name: ${{ env.IMAGE_REPO }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true - - - name: Verify image attestation smoke check - env: - DIGEST: ${{ steps.push.outputs.digest }} - IDENTITY_REGEXP: "^https://github.com/${{ github.repository }}/\\.github/workflows/release\\.yml@refs/tags/" - TAG: ${{ needs.build.outputs.tag }} - run: | - set -euo pipefail - # `cosign sign` covered the manifest signature. `attest-build-provenance` - # pushed a SLSA v1 provenance attestation alongside the manifest in the - # registry (push-to-registry: true). Verify that attestation now, by - # digest + by predicate type + by the same identity binding, so a - # third-party verifier replaying the docs/reproducibility.md walkthrough - # won't be the first to discover a broken or missing attestation. - cosign verify-attestation "${IMAGE_REPO}@${DIGEST}" \ - --type slsaprovenance1 \ - --certificate-identity-regexp "$IDENTITY_REGEXP" \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --certificate-github-workflow-ref "refs/tags/$TAG" \ - --certificate-github-workflow-trigger 'push' - - release: - name: release - runs-on: ubuntu-latest - needs: [build, sbom, sign, provenance, image] - timeout-minutes: 10 - permissions: - contents: write - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: artifacts - - - name: Stage release files - env: - TAG: ${{ needs.build.outputs.tag }} - run: | - set -euo pipefail - mkdir -p release - cp "artifacts/binary/$BINARY_BASENAME" "release/tracecore_${TAG}_linux_amd64" - cp "artifacts/sbom/tracecore.sbom.cdx.json" "release/tracecore_${TAG}.sbom.cdx.json" - cp "artifacts/cosign-bundle/$BINARY_BASENAME.cosign.bundle" \ - "release/tracecore_${TAG}.cosign.bundle" - cp "artifacts/provenance/$BINARY_BASENAME.intoto.jsonl" \ - "release/tracecore_${TAG}.intoto.jsonl" - ls -la release/ - - - name: Create or update GitHub Release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.build.outputs.tag }} - IS_PRERELEASE: ${{ contains(needs.build.outputs.tag, '-') }} - run: | - set -euo pipefail - prerelease_flag="" - if [ "$IS_PRERELEASE" = "true" ]; then - prerelease_flag="--prerelease" - fi - # Compose release notes: reproducibility walkthrough + a Rekor - # transparency-log pointer for after-the-fact audit. Extracting - # logIndex from the bundle in-job beats hand-pulling it later. - notes=$(mktemp) - cat docs/reproducibility.md > "$notes" - bundle="release/tracecore_${TAG}.cosign.bundle" - if [ -f "$bundle" ]; then - logIndex=$(jq -r '.rekorBundle.Payload.logIndex // empty' "$bundle") - if [ -n "$logIndex" ]; then - printf '\n## Transparency log\n\nRekor entry: https://search.sigstore.dev/?logIndex=%s\n' "$logIndex" >> "$notes" - fi - fi - if gh release view "$TAG" >/dev/null 2>&1; then - gh release upload "$TAG" --clobber release/* - else - gh release create "$TAG" $prerelease_flag \ - --title "$TAG" \ - --notes-file "$notes" \ - release/* - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f70eef63..6d2c5f04 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,10 @@ # Release pipeline — RFC-0013 PR-C rewrite. # -# Replaces the bespoke release workflow (archived at -# .github/workflows/archived/release.yml.legacy). Build + archive + -# checksum are now delegated to goreleaser via .goreleaser.yaml at the -# repo root; SBOM / signing / SLSA provenance / GitHub attestation are -# layered on top using OpenSSF / Sigstore-blessed actions. +# Replaces the bespoke release workflow that previously lived here +# (preserved in git history). Build + archive + checksum are delegated +# to goreleaser via .goreleaser.yaml at the repo root; SBOM / signing / +# SLSA provenance / GitHub attestation are layered on top using OpenSSF / +# Sigstore-blessed actions. # # Stack (per RFC-0013 §Adoption Matrix + §References): # - goreleaser-action → binary build, archive, checksums @@ -30,12 +30,6 @@ on: push: tags: - 'v*' - workflow_dispatch: - inputs: - tag: - description: 'Tag to build (workflow_dispatch only; uses github.ref otherwise)' - required: false - type: string permissions: contents: read @@ -68,18 +62,11 @@ jobs: - name: Resolve tag id: tag - env: - # Pass via env so the shell quotes the value — direct - # `${{ inputs.tag }}` in `run:` is a template-injection vector. - INPUT_TAG: ${{ inputs.tag }} run: | set -euo pipefail - tag="$INPUT_TAG" - if [ -z "$tag" ]; then - tag="${GITHUB_REF#refs/tags/}" - fi + tag="${GITHUB_REF#refs/tags/}" if [ -z "$tag" ] || [ "$tag" = "$GITHUB_REF" ]; then - echo "::error::no tag resolvable from ref=$GITHUB_REF or input" + echo "::error::no tag resolvable from ref=$GITHUB_REF" exit 1 fi echo "value=$tag" >> "$GITHUB_OUTPUT" @@ -103,7 +90,7 @@ jobs: # Honour the tag commit's timestamp so `mod_timestamp: # "{{ .CommitTimestamp }}"` in .goreleaser.yaml yields a # deterministic build (matches Makefile lines 36-39 + - # archived release.yml SOURCE_DATE_EPOCH stanza). + # the prior release.yml's SOURCE_DATE_EPOCH stanza). GORELEASER_CURRENT_TAG: ${{ steps.tag.outputs.value }} - name: Compute base64-encoded sha256 hashes for SLSA provenance @@ -154,9 +141,7 @@ jobs: with: format: cyclonedx-json output-file: tracecore.source.sbom.cdx.json - # Upload to the GitHub Release created by goreleaser. Tag is - # carried through from the goreleaser job's resolved value so - # this works for both push:tags and workflow_dispatch paths. + # Upload to the GitHub Release created by goreleaser. upload-release-assets: true upload-artifact: true upload-artifact-retention: 7 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3cff84ad..7724ae4f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,11 +15,10 @@ # of release truth until PR-D lands. # # Determinism contract (matches Makefile lines 12-17 + 36-45): -# - SOURCE_DATE_EPOCH honored via mod_timestamp + reproducible flag. +# - SOURCE_DATE_EPOCH honored via mod_timestamp. # - -trimpath + ldflags `-s -w` + the four version vars # (Version/Revision/Branch/BuildDate) match the Makefile LDFLAGS. -# - CGO disabled to match the existing CI build matrix and the -# archived release.yml's `CGO_ENABLED: '0'` env. +# - CGO disabled to match the existing CI build matrix. version: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b50b581..f1511ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Pivot wave 1 landed across: #166 (RFC-0013 doc accepted), #168 (delete kueue + k - **Adopt > build posture replaces in-tree receivers for GPU telemetry, container stdout, kernel events, K8s events, Kueue, Python profiling, heartbeat, self-telemetry, release pipeline, and image publish.** Adoption matrix lives in [RFC-0013 §2](docs/rfcs/0013-distro-first-pivot.md#2-adoption-matrix). Vendors: NVIDIA (`dcgm-exporter`), AMD (`ROCm/device-metrics-exporter`), Intel (`intel/xpumanager`), Habana (Habana Prometheus Metric Exporter) - all scraped via upstream `prometheusreceiver`. CNCF: `filelogreceiver` + container stanza + `file_storage`; `journaldreceiver`; `k8sobjectsreceiver`; `telemetrygeneratorreceiver`. CNCF Profiles: `parca-agent` via OTLP profiles sink. Self-telemetry: upstream `componentstatus` + `service/telemetry` + standard `otelcol_*` metrics. - **Customer-stable telemetry contracts preserved across the pivot** via the OTTL `transform` processor in the bundled Helm-chart recipe ([RFC-0013 §3](docs/rfcs/0013-distro-first-pivot.md#3-customer-stable-telemetry-contracts)). Stable surfaces: `k8s.event.hint` 11-entry enum (pod_evicted, mount_failure, backoff, oom_killed, node_unhealthy, schedule_failure, create_failure, volume_attach_failure, container_status_unknown, node_pressure, image_pull_failure); `kernelevents.xid` (NVRM Xid code); `gpu.id` (PCI BDF); `gpu.vendor` (nvidia | amd | intel | habana - upstream-contribution target to OTel `hw.*` semconv); `gen_ai.training.rank` and `gen_ai.training.job_id` (cross-receiver join keys); NCCL FlightRecorder span schema; pattern detector outputs (M17/M18/M19). Operator alerts written against these survive the receiver swap. - **Deletions scheduled** (RFC-0013 §7): - - **v0.1.0:** `components/receivers/clockreceiver/` (→ `telemetrygeneratorreceiver`), `components/receivers/dcgm/` (cgo stub never shipped real path; → `dcgm-exporter` + `prometheusreceiver` recipe), `components/receivers/kueue/` (never shipped; → `prometheusreceiver` recipe), `internal/componentstatus/`, `internal/selftelemetry/`, `internal/telemetry/`. Hand-rolled `.github/workflows/release.yml` archived under `.github/workflows/archived/`. Operator-visible breaks: self-tel metric rename `tracecore.*` → `otelcol_*`; release-artifact provenance shape change (documented once). + - **v0.1.0:** `components/receivers/clockreceiver/` (→ `telemetrygeneratorreceiver`), `components/receivers/dcgm/` (cgo stub never shipped real path; → `dcgm-exporter` + `prometheusreceiver` recipe), `components/receivers/kueue/` (never shipped; → `prometheusreceiver` recipe), `internal/componentstatus/`, `internal/selftelemetry/`, `internal/telemetry/`. Hand-rolled `.github/workflows/release.yml` rewritten onto the goreleaser stack (prior workflow preserved in git history). Operator-visible breaks: self-tel metric rename `tracecore.*` → `otelcol_*`; release-artifact provenance shape change (documented once). - **v0.2.0:** `components/receivers/kernelevents/` (→ `journaldreceiver` + `filelogreceiver` + OTTL Xid transform), `components/receivers/k8sevents/` (→ `k8sobjectsreceiver` + OTTL `k8s.event.hint` transform), `components/receivers/kineto/` (deferred; re-eval at OTel Profiles GA), plus `.github/workflows/kernelevents-integration.yml`. Operator-visible breaks: ALL recipe-side receiver swaps, batched into one migration guide; Helm values keys map old→new for one minor release with `NOTES.txt` deprecation warning. - **v0.3.0:** `components/receivers/pyspy/` (→ `parca-agent` via separate chart), `python/tracecore_pyspy/`, `tools/pyspy-lint/`, `.github/workflows/{pyspy-integration,python-publish}.yml`. Operator-visible breaks: PyPI helper deleted; security posture changes (CAP_SYS_PTRACE → CAP_SYS_ADMIN/BPF - operator review window). - **Upstream contributions become first-class policy.** Tracecore patches upstream first; forks only when upstream rejects ([RFC-0013 §5](docs/rfcs/0013-distro-first-pivot.md#5-upstream-contribution-policy)). When a contribution is in-flight, tracecore ships against a `replace` directive in `go.mod` pointing at the contribution branch; the replace is removed when the upstream tag lands. Likely contribution slots opened by the pivot: `k8sobjectsreceiver` (`k8s.event.hint` derived attribute), `filelogreceiver` / container stanza (PyTorch rank + dataloader-timing presets), `journaldreceiver` (`_TRACE_ID`/`_SPAN_ID` propagation), cross-vendor `gpu.vendor` semconv extension, OTel Profiles Kineto adapter, OCB reproducibility flags, `telemetrygeneratorreceiver` rate-limit knobs. diff --git a/MILESTONES.md b/MILESTONES.md index 42913146..495e1657 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -17,7 +17,7 @@ Per [RFC-0013 §4](docs/rfcs/0013-distro-first-pivot.md#4-migration-timeline-rel | Release | Deletions | Adoptions | |---|---|---| -| **v0.1.0** | `clockreceiver`, `dcgm` (cgo stub never shipped real path), `kueue` (never shipped), self-tel internal packages (`internal/componentstatus`, `internal/selftelemetry`, `internal/telemetry`); legacy `release.yml` archived under `.github/workflows/archived/` | OCB skeleton + `builder-config.yaml`; upstream `componentstatus` + `service/telemetry`; goreleaser + SLSA stack; `ko` for image build; `telemetrygeneratorreceiver` for heartbeat | +| **v0.1.0** | `clockreceiver`, `dcgm` (cgo stub never shipped real path), `kueue` (never shipped), self-tel internal packages (`internal/componentstatus`, `internal/selftelemetry`, `internal/telemetry`); legacy `release.yml` rewritten onto goreleaser stack (prior workflow in git history) | OCB skeleton + `builder-config.yaml`; upstream `componentstatus` + `service/telemetry`; goreleaser + SLSA stack; `ko` for image build; `telemetrygeneratorreceiver` for heartbeat | | **v0.2.0** | `kernelevents`, `k8sevents`, `kineto` receivers + integration workflows (`.github/workflows/kernelevents-integration.yml`) | Recipes: `filelogreceiver`+container stanza+`file_storage`; `journaldreceiver`+`filelogreceiver`+OTTL transform; `k8sobjectsreceiver`+transform; `prometheusreceiver` (Kueue + dcgm-exporter / ROCm / Intel / Habana); `tracecoreai/tracecore-components` Go module split | | **v0.3.0** | `pyspy` receiver + `python/tracecore_pyspy/` PyPI helper + `tools/pyspy-lint/`; `.github/workflows/{pyspy-integration,python-publish}.yml` | `parca-agent` adoption recipe (deployed via separate chart). Kineto re-evaluated against OTel Profiles GA | @@ -166,7 +166,7 @@ Critical path to v0.1.0; the only lane in which a single milestone (M21) gates e ### M3. Reproducible-build CI - **Status:** ☑ delivered (PR #28) -- **Status (RFC-0013):** hand-rolled `release.yml` REWRITTEN at v0.1.0 - replaced by `goreleaser` + `anchore/sbom-action` + `slsa-framework/slsa-github-generator` + `sigstore/cosign-installer` + `actions/attest-build-provenance`. Image build moves to `ko`; image-pull-side updates to `Renovate` (PR-based bump of chart `image.tag`). The old `release.yml` archives under `.github/workflows/archived/`. Lane 1 owns the integration glue, not a hand-rolled signing/SBOM/provenance pipeline. +- **Status (RFC-0013):** hand-rolled `release.yml` REWRITTEN at v0.1.0 - replaced by `goreleaser` + `anchore/sbom-action` + `slsa-framework/slsa-github-generator` + `sigstore/cosign-installer` + `actions/attest-build-provenance`. Image build moves to `ko`; image-pull-side updates to `Renovate` (PR-based bump of chart `image.tag`). The prior `release.yml` is preserved in git history. Lane 1 owns the integration glue, not a hand-rolled signing/SBOM/provenance pipeline. - **Depends on:** none - **Landed:** `.github/workflows/release.yml` + `docs/reproducibility.md`. diff --git a/docs/notes/ci.md b/docs/notes/ci.md index 2f31eac4..588ff989 100644 --- a/docs/notes/ci.md +++ b/docs/notes/ci.md @@ -11,12 +11,10 @@ v0.3.0 migration window per [RFC-0013 §Migration](../rfcs/0013-distro-first-pivot.md#migration--rollout) and §7 (Deletion list). Concrete schedule: -- **v0.1.0** - `.github/workflows/release.yml` is rewritten (or - replaced by a new `goreleaser.yml`) on top of the goreleaser + - `slsa-github-generator` + `cosign-installer` + `sbom-action` - stack (RFC-0013 §Migration PR-C). The old hand-rolled - `release.yml` is archived under - `.github/workflows/archived/` for one minor before deletion. +- **v0.1.0** - `.github/workflows/release.yml` is rewritten on top + of the goreleaser + `slsa-github-generator` + `cosign-installer` + + `sbom-action` stack (RFC-0013 §Migration PR-C). The prior + hand-rolled workflow is preserved in git history. - **v0.2.0** - `.github/workflows/kernelevents-integration.yml` is deleted alongside `components/receivers/kernelevents/` (RFC-0013 §7). Recipe validation for `journaldreceiver` + diff --git a/docs/reproducibility.md b/docs/reproducibility.md index cb2710b8..c8af21df 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -122,7 +122,7 @@ gh attestation verify "$PUBLISHED" \ --bundle "$ATTEST" \ --repo "$REPO" \ --predicate-type 'https://slsa.dev/provenance/v1' \ - --signer-workflow "${REPO}/.github/workflows/goreleaser.yml" \ + --signer-workflow "${REPO}/.github/workflows/release.yml" \ --source-ref "refs/tags/$TAG" \ --source-digest "$(git -C src rev-parse HEAD)" ``` @@ -159,7 +159,7 @@ cosign verify "$DIGEST" \ # attestation verify fetches it inline; no separate download. gh attestation verify "oci://$DIGEST" \ --repo "$REPO" \ - --signer-workflow "${REPO}/.github/workflows/goreleaser.yml" \ + --signer-workflow "${REPO}/.github/workflows/release.yml" \ --predicate-type 'https://slsa.dev/provenance/v1' \ --source-ref "refs/tags/$TAG" \ --source-digest "$(git -C src rev-parse HEAD)" @@ -171,7 +171,7 @@ gh attestation verify "oci://$DIGEST" \ |---|---|---| | 4 | Byte-identical rebuild at the same SHA | [PRINCIPLES.md §12](../PRINCIPLES.md) | | 5 | Binary signature traces to a GitHub Actions OIDC identity, no long-lived key | [Sigstore keyless](https://docs.sigstore.dev/cosign/verifying/verify/) | -| 6 | Binary provenance matches `predicateType: https://slsa.dev/provenance/v1`, signed by this repo's `goreleaser.yml` on a tag-ref | [SLSA v1.0 Build L1](https://slsa.dev/spec/v1.0/levels#build-l1) + [GitHub artifact attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations) | +| 6 | Binary provenance matches `predicateType: https://slsa.dev/provenance/v1`, signed by this repo's `release.yml` on a tag-ref | [SLSA v1.0 Build L1](https://slsa.dev/spec/v1.0/levels#build-l1) + [GitHub artifact attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations) | | 7 | SBOM coverage of every direct module | [CycloneDX spec](https://cyclonedx.org/specification/overview/) | | 8 | Image manifest signature traces to the same OIDC identity as the binary | [Sigstore keyless](https://docs.sigstore.dev/cosign/verifying/verify/) | | 9 | Image provenance matches `predicateType: https://slsa.dev/provenance/v1`, attestation stored in the OCI registry | [SLSA v1.0 Build L1](https://slsa.dev/spec/v1.0/levels#build-l1) |