diff --git a/.github/workflows/archived/release.yml.legacy b/.github/workflows/archived/release.yml.legacy new file mode 100644 index 00000000..d3c95dd3 --- /dev/null +++ b/.github/workflows/archived/release.yml.legacy @@ -0,0 +1,686 @@ +# ⚠️ 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 d3c95dd3..f70eef63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,28 @@ -# ⚠️ 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. +# 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. +# +# Stack (per RFC-0013 §Adoption Matrix + §References): +# - goreleaser-action → binary build, archive, checksums +# - anchore/sbom-action → CycloneDX SBOM (in-repo, post-build) +# - sigstore/cosign-installer → keyless signing of release blobs +# - slsa-framework/slsa-github-generator +# → SLSA v1.0 provenance reusable workflow +# - actions/attest-build-provenance +# → GitHub-native attestation alongside SLSA +# +# Pinning policy: every third-party action is pinned by commit SHA per +# the repo's zizmor `unpinned-uses` gate (see .github/zizmor.yml). +# Exception: the SLSA reusable workflow (`generator_generic_slsa3.yml`) +# is invoked by tag — SLSA's own security model requires the workflow +# subject in the OIDC token to be a known tag ref so verifiers can +# pin against it. Pinning by SHA breaks `slsa-verifier`'s identity check. +# See https://github.com/slsa-framework/slsa-github-generator/blob/main/SECURITY.md +# §"Workflow Pinning". name: release @@ -21,38 +40,30 @@ on: 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) + # --------------------------------------------------------------------------- + # goreleaser: build linux/{amd64,arm64} binaries, tar.gz archives, sha256 + # checksums, and per-archive CycloneDX SBOMs (via goreleaser's `sboms:` + # stanza which shells out to syft — same engine as anchore/sbom-action). + # Produces `dist/` artifacts that downstream jobs consume. + # --------------------------------------------------------------------------- + goreleaser: + name: goreleaser (build + archive + sbom) 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 + contents: write # goreleaser publishes to GitHub Releases on tag push outputs: - digest: ${{ steps.digest.outputs.value }} + hashes: ${{ steps.hashes.outputs.value }} tag: ${{ steps.tag.outputs.value }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 + fetch-depth: 0 # goreleaser needs full history for changelog + git describe - 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. + go-version-file: go.mod cache: true # zizmor: ignore[cache-poisoning] - name: Resolve tag @@ -73,614 +84,205 @@ jobs: 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: Install syft (for goreleaser sboms stanza) + # anchore/sbom-action ships syft, but the goreleaser sboms: stanza + # invokes the `syft` binary directly. Install it on PATH so the + # build pass produces SBOMs in dist/ alongside the archives. + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - - 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 + - name: Run goreleaser + id: goreleaser + uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 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 + distribution: goreleaser + version: "~> v2" + args: release --clean 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 + # goreleaser uses this for the GitHub Release creation step. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # 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). + GORELEASER_CURRENT_TAG: ${{ steps.tag.outputs.value }} + + - name: Compute base64-encoded sha256 hashes for SLSA provenance + id: hashes + # The SLSA generic generator expects a base64-encoded list of + # ` ` lines (the contents of the goreleaser + # checksums.txt file, encoded). This becomes the `subjects` input + # to the reusable workflow below. run: | set -euo pipefail - digest=$(sha256sum "release/$BINARY_BASENAME" | awk '{print $1}') - echo "value=$digest" >> "$GITHUB_OUTPUT" + cd dist + hashes=$(base64 -w0 < checksums.txt) + echo "value=$hashes" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: binary - path: release/${{ env.BINARY_BASENAME }} + name: dist + path: | + dist/*.tar.gz + dist/*.sbom.cdx.json + dist/checksums.txt if-no-files-found: error retention-days: 7 + # --------------------------------------------------------------------------- + # sbom: re-run anchore/sbom-action on the source tree to publish a + # canonical, repo-level SBOM alongside the per-archive SBOMs that + # goreleaser emits. Belt + suspenders: per-archive SBOMs are pinned to + # the binary contents; the source-tree SBOM is what `gh release view` + # consumers expect as a single discoverable artifact. + # --------------------------------------------------------------------------- sbom: - name: sbom (cyclonedx) + name: sbom (anchore/syft, source-tree) runs-on: ubuntu-latest - needs: build - timeout-minutes: 15 + needs: goreleaser + timeout-minutes: 10 permissions: - contents: read + contents: write # uploads SBOM to the existing GitHub Release 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. + # Pin by commit SHA so a force-push to the tag between goreleaser + # and sbom cannot produce an SBOM for a different tree than the + # binaries that goreleaser already shipped. 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 + - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: - name: sbom - path: tracecore.sbom.cdx.json - if-no-files-found: error - retention-days: 7 - + 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-release-assets: true + upload-artifact: true + upload-artifact-retention: 7 + + # --------------------------------------------------------------------------- + # sign: cosign-keyless sign-blob of each goreleaser archive + the + # checksums file. Bundle (cert + signature + Rekor inclusion proof) + # ships as a release asset so third-party verifiers can validate + # offline. + # --------------------------------------------------------------------------- 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. + needs: goreleaser timeout-minutes: 10 permissions: - id-token: write - contents: read + id-token: write # OIDC token for keyless cosign + contents: write # upload bundles to the GitHub Release steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: binary + name: dist + path: dist - 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) + - name: Sign blobs (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 + # Sign each archive AND the checksums file. The checksums file + # is the trust anchor that lets a verifier check every other + # artifact's integrity, so it gets its own bundle. + shopt -s nullglob + for f in dist/*.tar.gz dist/checksums.txt; do + cosign sign-blob --yes \ + --bundle "$f.cosign.bundle" \ + "$f" + done + + - name: Verify blob signatures (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 }} + TAG: ${{ needs.goreleaser.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 + shopt -s nullglob + for f in dist/*.tar.gz dist/checksums.txt; do + cosign verify-blob \ + --bundle "$f.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' \ + "$f" + done + + - name: Upload cosign bundles to the GitHub Release 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 }} + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.goreleaser.outputs.tag }} 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)" + shopt -s nullglob + bundles=(dist/*.cosign.bundle) + if [ ${#bundles[@]} -eq 0 ]; then + echo "::error::no cosign bundles produced" 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) + gh release upload "$TAG" "${bundles[@]}" --clobber + + # --------------------------------------------------------------------------- + # provenance: SLSA v1.0 generic provenance via the reusable workflow. + # Operates on the base64-encoded checksums.txt produced by goreleaser + # so a single provenance attestation covers every archive published. + # + # NOTE: reusable workflow is referenced by TAG (not SHA) per SLSA's + # security model — see header comment. zizmor's `unpinned-uses` audit + # recognises this pattern as an exception for slsa-github-generator. + # --------------------------------------------------------------------------- + provenance: + name: provenance (SLSA v1.0) + needs: goreleaser + permissions: + id-token: write # OIDC for Fulcio + contents: write # upload provenance to GitHub Release + actions: read # reusable-workflow inspection of caller + # SLSA's security model requires the reusable-workflow OIDC subject + # to be a known tag ref so `slsa-verifier` can pin against it. + # Pinning by SHA breaks the identity check. See + # https://github.com/slsa-framework/slsa-github-generator/blob/main/SECURITY.md + # §"Workflow Pinning". + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 # zizmor: ignore[unpinned-uses] + with: + base64-subjects: ${{ needs.goreleaser.outputs.hashes }} + upload-assets: true + upload-tag-name: ${{ needs.goreleaser.outputs.tag }} + + # --------------------------------------------------------------------------- + # attest: GitHub-native build-provenance attestation as a second + # independent provenance trail. Keeps `gh attestation verify` working + # for users who consume attestations through the GitHub API rather + # than via the SLSA bundle on the release. + # --------------------------------------------------------------------------- + attest: + name: attest (github-native build-provenance) 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 + needs: goreleaser + timeout-minutes: 10 permissions: + id-token: write 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 }} + attestations: write 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' + name: dist + path: dist - 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 + # Attest every release archive; one attestation per subject. + # The checksums file itself is not attested here — its + # integrity is already covered by `sign` above (cosign-bundle) + # AND by the SLSA provenance attestation that takes + # checksums.txt as its subject input. + subject-path: | + dist/*.tar.gz diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..3cff84ad --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,136 @@ +# tracecore goreleaser config — RFC-0013 PR-C. +# +# Replaces the bespoke .github/workflows/release.yml (archived to +# .github/workflows/archived/release.yml.legacy). The release pipeline +# now delegates binary build + archive + checksum to goreleaser, and +# wires SBOM / signing / provenance through goreleaser-adjacent steps +# in .github/workflows/release.yml. +# +# Scope: this config builds the legacy `cmd/tracecore` binary. Post +# PR-A (#171) tracecore also has an OCB build path (`make build-ocb` → +# ./_build/tracecore). Migrating goreleaser to publish the OCB output +# is tracked under PR-D-adjacent work in RFC-0013 §migration; doing +# both in one PR would conflate "switch release tooling" with "switch +# build path" and obscure the diff. The legacy binary stays the source +# 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. +# - -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. + +version: 2 + +project_name: tracecore + +dist: ./dist + +before: + hooks: + # Verify module hashes against go.sum before building. Mirrors the + # `go mod download && go mod verify` step in the archived release.yml. + # If go.sum is poisoned at this tag commit, this catches it before + # any binary leaves the runner. + - go mod download + - go mod verify + +builds: + - id: tracecore + main: ./cmd/tracecore + binary: tracecore + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + flags: + - -trimpath + # SOURCE_DATE_EPOCH from `before:` env / runner env. goreleaser sets + # build timestamps from this when reproducible mode is enabled below. + mod_timestamp: "{{ .CommitTimestamp }}" + ldflags: + - -s + - -w + - -X github.com/tracecoreai/tracecore/internal/version.Version={{ .Version }} + - -X github.com/tracecoreai/tracecore/internal/version.Revision={{ .ShortCommit }} + - -X github.com/tracecoreai/tracecore/internal/version.Branch={{ .Branch }} + - -X github.com/tracecoreai/tracecore/internal/version.BuildDate={{ .Date }} + +archives: + - id: tracecore + formats: + - tar.gz + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + files: + - LICENSE + - README.md + +checksum: + name_template: "checksums.txt" + algorithm: sha256 + +# SBOM (CycloneDX JSON) generated per-archive via Syft (anchore/sbom-action +# uses the same engine). Output filename pattern matches the archive so +# verifiers can pair binary↔SBOM by name. cosign integration lives in the +# release.yml workflow (sign step) since signing needs OIDC at job scope. +sboms: + - id: tracecore + artifacts: archive + documents: + - "${artifact}.sbom.cdx.json" + cmd: syft + args: + - "$artifact" + - "--output" + - "cyclonedx-json=$document" + +changelog: + use: git + sort: asc + # Conventional-commits filter — surfaces feat / fix / perf and drops + # noise (chore, ci, docs, test, style, refactor). RFC-0013 release + # notes will be hand-edited for v0.1.0 anyway; this gives a sane + # default for patch releases. + filters: + exclude: + - "^chore:" + - "^chore\\(" + - "^ci:" + - "^ci\\(" + - "^docs:" + - "^docs\\(" + - "^test:" + - "^test\\(" + - "^style:" + - "^style\\(" + - "^refactor:" + - "^refactor\\(" + groups: + - title: Features + regexp: "^.*feat[(\\w)]*:+.*$" + order: 0 + - title: Bug fixes + regexp: "^.*fix[(\\w)]*:+.*$" + order: 1 + - title: Performance + regexp: "^.*perf[(\\w)]*:+.*$" + order: 2 + - title: Other + order: 999 + +release: + # The release.yml workflow owns release-note composition (it appends + # the docs/reproducibility.md walkthrough + Rekor logIndex pointer); + # goreleaser just stages assets + checksums. Setting `draft: false` + # mirrors the archived workflow's behaviour. + draft: false + prerelease: auto + mode: append + +snapshot: + version_template: "{{ incpatch .Version }}-next" diff --git a/CHANGELOG.md b/CHANGELOG.md index a8a47376..7b50b581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ User-visible changes are documented here. Format: [Keep a Changelog](https://kee Pre-alpha. **Distribution-first pivot adopted ([RFC-0013](docs/rfcs/0013-distro-first-pivot.md))** - binary now assembled via the OpenTelemetry Collector Builder (OCB) from upstream + contrib components plus a thin `tracecoreai/tracecore-components` module containing only the moat (NCCL FlightRecorder receiver, OTTL processors with windowed semantics, pattern detectors). The M1 in-tree pipeline runtime + factory-based assembly is queued for deletion at v0.1.0 in favor of the OCB-generated boot path; the canonical `clockreceiver` + `stdoutexporter` examples ship for one PR cycle and then exit. Targeting v0.1.0 / v0.2.0 / v0.3.0 release boundaries per RFC-0013 §4. +Pivot wave 1 landed across: #166 (RFC-0013 doc accepted), #168 (delete kueue + kineto receivers), #169 (pre-PR-A drift sweep + Helm security tighten), #170 (PR-G RFC supersession headers + RFC-0004 archive), #171 (PR-H top-level doc reconciliation), #172 (clockreceiver removal + stdoutexporter quarantine), #173 (release-doc + CHANGELOG wave-1 reconciliation). PR-A (OCB `builder-config.yaml` + `make build-ocb` side-by-side) is next. + ### Changed (RFC-0013 pivot) - **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. diff --git a/NORTHSTARS.md b/NORTHSTARS.md index 1cb6d85e..d3377e20 100644 --- a/NORTHSTARS.md +++ b/NORTHSTARS.md @@ -78,7 +78,7 @@ Seven lines of work. Each has one accountable owner role, one hero KPI, supporti **Caveats:** - v0/v1 is pain-weighted (the ~30 layers mapping to the 15 patterns), not breadth-max. - Frontier layers (L38-L42: power/grid coupling, SDC, RLHF, MoE, FP8 numerics) are v2/v3 strategic differentiators, not coverage-completeness work. -- Assumes own-binary architecture, with receivers structured so they could be upstreamed to `opentelemetry-collector-contrib` later. See RFC closing this question; revisit O1 if it lands differently. +- Assumes the OCB-assembled distribution posture per [RFC-0013](docs/rfcs/0013-distro-first-pivot.md): vendor / orchestrator coverage is delivered by adopting upstream + contrib receivers (`prometheusreceiver` against `dcgm-exporter` / ROCm / Intel / Habana; `filelogreceiver`; `journaldreceiver`; `k8sobjectsreceiver`) wired through the bundled recipe, not by building in-tree receivers. In-house code adds coverage only inside the four moat scopes (RFC-0013 §6) — primarily the pattern detectors and NCCL FlightRecorder receiver. Layer-count and "receivers at stable" KPIs above are measured against the OCB manifest's bundled receivers plus the moat components, not in-tree directories. - A non-NVIDIA stack gating the trajectory in a given quarter re-sequences the matrix with explicit written reason. No silent slip. --- @@ -392,7 +392,7 @@ Seven lines of work. Each has one accountable owner role, one hero KPI, supporti These will be written as RFCs as the work demands; each blocks specific KPI lock-in: -1. **Own-binary vs. extend `opentelemetry-collector-contrib`** - closes RFC 0001's open question; gates O1 architectural assumptions. *(In flight - RFC-0002.)* +1. **Own-binary vs. extend `opentelemetry-collector-contrib`** - closed by [RFC-0013](docs/rfcs/0013-distro-first-pivot.md): tracecore is an OCB-assembled distribution from upstream + contrib + a thin moat module. RFC-0002 (originally an own-binary recommendation) was revised by RFC-0013; RFC-0001's "open question" closes the same way. 2. **Auto-update boundary** - closed: operator-pulled releases, no in-binary self-update mechanism. *(Resolved - [RFC-0008](docs/rfcs/0008-auto-update-boundary.md). A superseding RFC opens if a production-operator ask appears that operator-side delivery automation cannot serve.)* 3. **eBPF integration scope** - consume eBPF data from other tools (v0-v1) or ship our own programs (v2+). Already in RFC 0001 as an open question. 4. **Helm vs. Operator deployment** - Helm for v0; Operator (CRDs) RFC-worthy when operator demand emerges. diff --git a/bench/install/tracecore-values.yaml b/bench/install/tracecore-values.yaml index 07419538..9157553c 100644 --- a/bench/install/tracecore-values.yaml +++ b/bench/install/tracecore-values.yaml @@ -9,8 +9,17 @@ # RFC-0013 note: the `clockreceiver` and `stdoutexporter` values # keys below refer to the v0.0.x in-tree components. At v0.1.0 they # map to recipe equivalents per RFC-0013 §7 (Deletion list): -# clockreceiver -> telemetrygeneratorreceiver (OCB-bundled) -# stdoutexporter -> debugexporter (OCB-bundled) +# clockreceiver -> telemetrygeneratorreceiver (OCB-bundled) [BLOCKED] +# stdoutexporter -> debugexporter (OCB-bundled) +# PR-E status (2026-05-30): the clockreceiver -> telemetrygeneratorreceiver +# swap is DEFERRED. The receiver does not exist in +# opentelemetry-collector-contrib at v0.110.0 or main (verified against +# the GH tree API; receiver/telemetrygeneratorreceiver path 404s on every +# tag from v0.95.0 through v0.130.0). The bench keeps using the in-tree +# clockreceiver until PR-F's deletion forces a rewire — at which point +# the bench will switch to a non-clock load source (likely +# hostmetricsreceiver on a 1s scrape, or an OCB-bundled equivalent if one +# lands upstream). See builder-config.yaml TODO(RFC-0013 PR-E) block. # The chart's compat map will keep these values keys working for one # minor with a `NOTES.txt` deprecation warning per RFC-0013 §8. image: diff --git a/builder-config.yaml b/builder-config.yaml index 4345d936..db6a3b23 100644 --- a/builder-config.yaml +++ b/builder-config.yaml @@ -11,7 +11,19 @@ receivers: - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/journaldreceiver v0.110.0 - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/k8sobjectsreceiver v0.110.0 - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.110.0 - # telemetrygeneratorreceiver: contrib pseudo-module, no v0.110.0 tag. Added in PR-E (clockreceiver swap). + # TODO(RFC-0013 PR-E): swap clockreceiver -> telemetrygeneratorreceiver. + # BLOCKER (verified 2026-05-30 against opentelemetry-collector-contrib + # v0.110.0 + main): receiver does NOT exist in the OTel contrib repo at + # any path (receiver/telemetrygeneratorreceiver, loadgenreceiver, + # mockreceiver, dummyreceiver all 404). RFC-0013 §1's example shape + # referenced it speculatively — the receiver was never landed upstream. + # Decision: keep clockreceiver alive in cmd/tracecore/components.go + # legacy boot path until either (a) an upstream replacement lands and we + # bump OCB to that release, or (b) PR-F's deletion ships and the bench + # rewires to a different load source (e.g. hostmetricsreceiver on a + # short scrape interval, or otlpreceiver fed by a sibling loader pod). + # Re-evaluate on every OCB version bump. Tracked in RFC-0013 OQ + # follow-up (cannot edit RFC inline per PR-E constraint). processors: - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.110.0