From ef155d9163978caa3d606e8f230135de7b088756 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sat, 19 Sep 2026 23:52:13 +0500 Subject: [PATCH 1/4] feat(release): remove release channels from the release pipeline Release identity is the exact gds-v tag, monotonic sequence, artifact and manifest digests, SBOM and attestation (ADR 0038). Channel fields stay optional for decoding documents produced while the field existed; a legacy channel is still checked against a consumer policy that lists it. - bundle.Build and gds-release-builder no longer take a channel or harness evidence, and every release must build from its exact tag. - release_sequence alone classifies a bundle: 0 is a development projection, >= 1 is a release. - the release workflow runs on pushes to main; the privileged resolve job derives the next version and sequence from the latest published envelope over the GitHub API without checking out candidate source, creates the tag, and hands the identity to the unprivileged build. - harness evidence remains a separately produced and verified estate/runtime signal; it gates adoption, not publication. - default_bundle_channel is removed from the estate schema and example. --- .gds/bundle.lock.yaml | 7 +- .github/workflows/gds-ci.yml | 2 +- .github/workflows/release-bundle.yml | 230 ++++++++++-------- CHANGELOG.md | 23 ++ core/app/services.go | 9 +- core/assurance/generation.go | 2 +- core/bundle/build.go | 36 +-- core/bundle/bundle_test.go | 5 +- core/bundle/types.go | 25 +- core/bundle/verify.go | 2 +- core/cli/root.go | 1 - core/cli/root_test.go | 6 +- core/cmd/gds-release-builder/main.go | 3 - core/compiler/types.go | 4 +- core/context/provenance.go | 18 +- core/context/resolver_test.go | 4 +- core/estate/types.go | 5 +- core/harnessevidence/evidence.go | 87 +------ core/harnessevidence/evidence_test.go | 4 +- core/projections/generator.go | 5 +- core/projections/types.go | 9 +- core/releasebuilder/builder.go | 32 +-- core/releasebuilder/harness_evidence.go | 96 -------- core/releasebuilder/output.go | 8 +- core/releasebuilder/output_test.go | 25 +- core/releasebuilder/source_snapshot_test.go | 5 +- core/releasebuilder/types.go | 20 +- core/releasebuilder/workflow_contract_test.go | 26 +- core/releaseconsumer/install.go | 2 +- core/releaseconsumer/verify_test.go | 7 +- core/rollout/plan.go | 2 +- core/rollout/request.go | 2 +- docs/adr/0016-detached-release-envelope.md | 2 + ...038-release-identity-carries-no-channel.md | 88 +++++++ docs/adr/README.md | 3 +- docs/contracts/bundle-release-v1.md | 15 +- docs/contracts/projections-v1.md | 8 +- docs/runbooks/release-lifecycle.md | 57 ++--- docs/runbooks/release-promotion-policy.md | 46 ++-- docs/runbooks/seed-clean-device.md | 2 +- estate/estate.yaml | 1 - schemas/v1/bundle-lock.schema.json | 18 +- schemas/v1/bundle-manifest.schema.json | 18 +- schemas/v1/bundle-trust.schema.json | 2 +- schemas/v1/estate.schema.json | 5 +- .../v1/harness-runtime-manifest.schema.json | 2 +- schemas/v1/release-envelope.schema.json | 1 - .../v1/release-failure-envelope.schema.json | 1 - schemas/v1/release-installation.schema.json | 1 - schemas/v1/rollout-request.schema.json | 2 +- schemas/v1/rollout.schema.json | 2 +- .../v1/invalid-bundle-lock-output-digest.yaml | 1 - .../v1/invalid-bundle-lock-release.yaml | 3 +- ...nvalid-bundle-manifest-oversized-file.json | 1 - .../v1/invalid-estate-policy-order.yaml | 1 - .../v1/invalid-release-envelope-semver.json | 1 - ...d-release-failure-envelope-empty-jobs.json | 1 - .../v1/invalid-rollout-request-ring.yaml | 1 - .../v1/invalid-rollout-target-digest.json | 5 +- .../schemas/v1/valid-bundle-lock.yaml | 1 - .../schemas/v1/valid-bundle-manifest.json | 1 - tests/fixtures/schemas/v1/valid-estate.yaml | 1 - .../v1/valid-harness-runtime-manifest.json | 2 +- .../schemas/v1/valid-release-envelope.json | 1 - .../v1/valid-release-failure-envelope.json | 5 +- .../v1/valid-release-installation.json | 1 - .../schemas/v1/valid-rollout-request.yaml | 1 - tests/fixtures/schemas/v1/valid-rollout.json | 41 ++-- .../control-plane/.claude/CLAUDE.md | 2 +- .../control-plane/.gds/bundle.lock.yaml | 11 +- .../.github/workflows/gds-ci.yml | 2 +- .../projections/control-plane/AGENTS.md | 2 +- 72 files changed, 478 insertions(+), 593 deletions(-) delete mode 100644 core/releasebuilder/harness_evidence.go create mode 100644 docs/adr/0038-release-identity-carries-no-channel.md diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index adefe58..f9cd6ea 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -4,15 +4,14 @@ schema_version: 1 bundle: version: "0.9.7-dev" release_sequence: 0 - channel: "development" source_tree_digest: "sha256:1e873c46d68f27b01800a4ee64221b7f53b3b78ad7872374b565f0a08fc67ff1" digest: "sha256:522661293a809309328a9551766cb89d4589241afa78f507a4b6f50a53244160" projection: - input_digest: "sha256:6d7ec12102428e0d17daf7501ebec764be4361a90d5f81e4edf201b861bc4ee6" - output_digest: "sha256:d634e459c82a8d5605327d60a0d4167a79056698f3d12d566c957797aa4ddac1" + input_digest: "sha256:6261a99a0f9de321773be0c9ecb05d3c74adadfd58654c2dca4bc4f40a8d0ee4" + output_digest: "sha256:b1c3483631551b66835aaa8542df740b6f8cc7376f40c471ff1f05dd2088c793" files: - path: ".gds/compiled-policy.json" digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:15fd27bccb311133125e6bfe1d8659430b1e18940338efd3933546c93d3979fa" + digest: "sha256:d166a5011b5068da9cdb1529a6622c75043aed686780b4148cb758bd4e573c8b" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index df24abe..73d6427 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -2,7 +2,7 @@ # generator: gds # bundle: 0.9.7-dev # source-tree-digest: sha256:1e873c46d68f27b01800a4ee64221b7f53b3b78ad7872374b565f0a08fc67ff1 -# input-digest: sha256:6d7ec12102428e0d17daf7501ebec764be4361a90d5f81e4edf201b861bc4ee6 +# input-digest: sha256:6261a99a0f9de321773be0c9ecb05d3c74adadfd58654c2dca4bc4f40a8d0ee4 # output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38 # edit-source: # - .gds/repository.yaml diff --git a/.github/workflows/release-bundle.yml b/.github/workflows/release-bundle.yml index 3b6f071..544670a 100644 --- a/.github/workflows/release-bundle.yml +++ b/.github/workflows/release-bundle.yml @@ -1,35 +1,15 @@ name: gds-release-bundle on: + # One version line: every green main publishes the next patch release. + # Pull-request review and required checks are the authorization gate; the + # merge commit is released without a second human action. + push: + branches: [main] workflow_dispatch: inputs: version: - description: "Release SemVer without a v prefix" - required: true - type: string - channel: - description: "Immutable release channel" - required: true - default: canary - type: choice - options: - - canary - - stable - - frozen - release_sequence: - description: "Monotonic sequence greater than every previously accepted GDS release" - required: true - type: number - minimum_cli_version: - description: "Minimum compatible GDS CLI SemVer" - required: true - type: string - harness_evidence_bundle_base64: - description: "Base64 tar.gz containing manifest.json and seven isolated records (required for stable/frozen)" - required: false - type: string - harness_evidence_trust_policy_base64: - description: "Base64 public trust-policy/v1 JSON; digest must equal repository variable HARNESS_EVIDENCE_TRUST_POLICY_DIGEST" + description: "Optional explicit SemVer for a minor/major release; empty resolves the next patch" required: false type: string @@ -40,8 +20,102 @@ concurrency: cancel-in-progress: false jobs: + resolve: + name: resolve next release identity and tag + # This job holds ref-write authority only. It never checks out or executes + # candidate source: every read is a provider API call, and the single write + # is the exact version tag the build job must then produce byte-identically. + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + outputs: + version: ${{ steps.resolve.outputs.version }} + sequence: ${{ steps.resolve.outputs.sequence }} + minimum_cli_version: ${{ steps.resolve.outputs.minimum_cli_version }} + tag: ${{ steps.resolve.outputs.tag }} + released: ${{ steps.resolve.outputs.released }} + steps: + - name: Resolve next version, sequence, and tag + id: resolve + shell: bash + env: + GH_TOKEN: ${{ github.token }} + VERSION_OVERRIDE: ${{ inputs.version }} + run: | + set -euo pipefail + mapfile -t tags < <( + gh api "repos/$GITHUB_REPOSITORY/tags?per_page=100" --paginate --jq '.[].name' \ + | grep -x 'gds-v[0-9]*\.[0-9]*\.[0-9]*' \ + | sort -t. -k1.6nr -k2nr -k3nr + ) + if [ "${#tags[@]}" -eq 0 ]; then + echo "No gds-v* tag exists; the first automatic release requires one published baseline" >&2 + exit 1 + fi + latest_tag="${tags[0]}" + latest_commit="$(gh api "repos/$GITHUB_REPOSITORY/commits/$latest_tag" --jq '.sha')" + # A rerun of this workflow on an already-released merge commit must + # not mint a second release identity for the same tree. + if [ "$latest_commit" = "$GITHUB_SHA" ] && [ -z "$VERSION_OVERRIDE" ]; then + echo "released=false" >> "$GITHUB_OUTPUT" + echo "tag=$latest_tag" >> "$GITHUB_OUTPUT" + echo "version=${latest_tag#gds-v}" >> "$GITHUB_OUTPUT" + exit 0 + fi + # The published release envelope is the authoritative sequence + # ledger: it is written by this same pipeline and attached to the + # GitHub Release, so no repository file can drift from it. Walk the + # tag list newest-first until a release provides a readable envelope. + previous="" + work="$(mktemp -d)" + for candidate in "${tags[@]}"; do + if gh release download "$candidate" --repo "$GITHUB_REPOSITORY" \ + -p release-envelope.json -p manifest.json --dir "$work" --clobber >/dev/null 2>&1; then + previous="$candidate" + break + fi + done + if [ -z "$previous" ]; then + echo "No published release carries a readable release-envelope.json" >&2 + exit 1 + fi + previous_sequence="$(jq -r '.release_sequence' "$work/release-envelope.json")" + previous_floor="$(jq -r '.minimum_cli_version' "$work/manifest.json")" + [[ "$previous_sequence" =~ ^[0-9]+$ ]] || { echo "previous release sequence is not an integer" >&2; exit 1; } + previous_version="${previous#gds-v}" + if [ -n "$VERSION_OVERRIDE" ]; then + next_version="$VERSION_OVERRIDE" + else + next_version="$(awk -F. -v v="$previous_version" 'BEGIN{split(v,p,"."); printf "%d.%d.%d", p[1], p[2], p[3]+1}')" + fi + # Monotonicity is structural, not assumed: the new identity must be + # strictly greater than the release it follows. + highest="$(printf '%s\n' "$previous_version" "$next_version" | sort -t. -k1n -k2n -k3n | tail -1)" + if [ "$highest" != "$next_version" ] || [ "$next_version" = "$previous_version" ]; then + echo "next version $next_version is not greater than $previous_version" >&2 + exit 1 + fi + next_sequence=$((previous_sequence + 1)) + next_tag="gds-v$next_version" + if gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/$next_tag" >/dev/null 2>&1; then + echo "tag $next_tag already exists" >&2 + exit 1 + fi + gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f "ref=refs/tags/$next_tag" -f "sha=$GITHUB_SHA" --jq '.ref' >/dev/null + { + echo "released=true" + echo "version=$next_version" + echo "sequence=$next_sequence" + echo "minimum_cli_version=$previous_floor" + echo "tag=$next_tag" + } >> "$GITHUB_OUTPUT" + build: name: build immutable bundle (unprivileged) + needs: resolve + if: ${{ needs.resolve.outputs.released == 'true' }} # Privilege separation (RVR2-P1-001): all repository-controlled source, # scripts, dependencies, and the release builder run here with contents:read # only — no OIDC, no attestations, no release-write authority. The publish @@ -57,18 +131,16 @@ jobs: contents: read env: GOTOOLCHAIN: go1.27.1 - VERSION: ${{ inputs.version }} - RELEASE_SEQUENCE: ${{ inputs.release_sequence }} - CHANNEL: ${{ inputs.channel }} - MINIMUM_CLI_VERSION: ${{ inputs.minimum_cli_version }} - HARNESS_EVIDENCE_BUNDLE_BASE64: ${{ inputs.harness_evidence_bundle_base64 }} - HARNESS_EVIDENCE_TRUST_POLICY_BASE64: ${{ inputs.harness_evidence_trust_policy_base64 }} - HARNESS_EVIDENCE_TRUST_POLICY_DIGEST: ${{ vars.HARNESS_EVIDENCE_TRUST_POLICY_DIGEST }} + VERSION: ${{ needs.resolve.outputs.version }} + RELEASE_SEQUENCE: ${{ needs.resolve.outputs.sequence }} + MINIMUM_CLI_VERSION: ${{ needs.resolve.outputs.minimum_cli_version }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} steps: - name: Check out exact source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ needs.resolve.outputs.tag }} fetch-depth: 0 - name: Install exact Go toolchain @@ -77,44 +149,6 @@ jobs: go-version: "1.27.1" cache: true - - name: Materialize bounded signed harness evidence input - id: harness-evidence - shell: bash - env: - EVIDENCE_INPUT_ROOT: ${{ runner.temp }}/gds-harness-evidence - run: | - set -euo pipefail - if [ -z "$HARNESS_EVIDENCE_BUNDLE_BASE64" ] && [ -z "$HARNESS_EVIDENCE_TRUST_POLICY_BASE64" ]; then - [ "$CHANNEL" = canary ] || { echo "stable/frozen requires signed active-seven harness evidence" >&2; exit 1; } - echo "arguments=" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [ -z "$HARNESS_EVIDENCE_BUNDLE_BASE64" ] || [ -z "$HARNESS_EVIDENCE_TRUST_POLICY_BASE64" ]; then - echo "Signed harness evidence archive and trust policy must be supplied together" >&2 - exit 1 - fi - expected_digest="${HARNESS_EVIDENCE_TRUST_POLICY_DIGEST#sha256:}" - [[ "$expected_digest" =~ ^[0-9a-f]{64}$ ]] || { - echo "HARNESS_EVIDENCE_TRUST_POLICY_DIGEST must be an independent SHA-256 pin (64 lowercase hex digits, optionally prefixed with sha256:)" >&2 - exit 1 - } - install -d -m 0700 "$EVIDENCE_INPUT_ROOT" - printf '%s' "$HARNESS_EVIDENCE_BUNDLE_BASE64" | base64 --decode > "$EVIDENCE_INPUT_ROOT/evidence.tar.gz" - printf '%s' "$HARNESS_EVIDENCE_TRUST_POLICY_BASE64" | base64 --decode > "$EVIDENCE_INPUT_ROOT/trust.json" - actual_digest="$(sha256sum "$EVIDENCE_INPUT_ROOT/trust.json" | cut -d' ' -f1)" - if [ "$actual_digest" != "$expected_digest" ]; then - echo "Harness evidence trust policy does not match the independent repository pin" >&2 - exit 1 - fi - if ! go run ./core/cmd/gds-release-builder \ - --extract-harness-evidence-archive "$EVIDENCE_INPUT_ROOT/evidence.tar.gz" \ - --extract-harness-evidence-destination "$EVIDENCE_INPUT_ROOT/records" \ - > "$RUNNER_TEMP/gds-harness-evidence-materialization.json"; then - cat "$RUNNER_TEMP/gds-harness-evidence-materialization.json" >&2 - exit 1 - fi - echo "arguments=--harness-evidence-directory $EVIDENCE_INPUT_ROOT/records --harness-evidence-trust-policy $EVIDENCE_INPUT_ROOT/trust.json" >> "$GITHUB_OUTPUT" - - name: Run release gates shell: bash env: @@ -140,11 +174,9 @@ jobs: --output "$RELEASE_DIRECTORY" \ --version "$VERSION" \ --sequence "$RELEASE_SEQUENCE" \ - --channel "$CHANNEL" \ --minimum-cli-version "$MINIMUM_CLI_VERSION" \ - --source-ref "$GITHUB_REF" \ + --source-ref "refs/tags/$RELEASE_TAG" \ --go-binary "$GO_BINARY" \ - ${{ steps.harness-evidence.outputs.arguments }} \ > "$RUNNER_TEMP/gds-release-result.json" go run ./core/cmd/gds-release-builder \ --verify-directory "$RELEASE_DIRECTORY" \ @@ -168,7 +200,7 @@ jobs: - name: Upload immutable release artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: gds-release-${{ inputs.version }}-${{ github.sha }} + name: gds-release-${{ needs.resolve.outputs.version }}-${{ github.sha }} # Upload one owned staging root. Supplying unrelated absolute and # workspace-relative paths makes upload-artifact preserve their # runner-wide common ancestor, so download-artifact reconstructs @@ -181,7 +213,7 @@ jobs: attest: name: attest immutable bundle - needs: build + needs: [resolve, build] # This privileged job holds OIDC and attestation authority, but no release- # write authority. Publication is a separate job with no OIDC permission. # It never checks out candidate source and never executes candidate code. @@ -200,7 +232,7 @@ jobs: - name: Download immutable release artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: gds-release-${{ inputs.version }}-${{ github.sha }} + name: gds-release-${{ needs.resolve.outputs.version }}-${{ github.sha }} path: ${{ runner.temp }}/gds-release-output merge-multiple: true @@ -229,7 +261,7 @@ jobs: id: sbom uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-path: ${{ runner.temp }}/gds-release-output/release/gds-bundle-v${{ inputs.version }}.tar.gz + subject-path: ${{ runner.temp }}/gds-release-output/release/gds-bundle-v${{ needs.resolve.outputs.version }}.tar.gz sbom-path: ${{ runner.temp }}/gds-release-output/release/sbom.spdx.json - name: Materialize offline verification evidence @@ -262,7 +294,7 @@ jobs: - name: Upload immutable offline evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: gds-release-evidence-${{ inputs.version }}-${{ github.sha }} + name: gds-release-evidence-${{ needs.resolve.outputs.version }}-${{ github.sha }} path: ${{ runner.temp }}/gds-release-output/release-evidence if-no-files-found: error compression-level: 0 @@ -270,7 +302,7 @@ jobs: publish: name: publish immutable bundle - needs: attest + needs: [resolve, attest] # This job can create the GitHub Release but cannot request an OIDC token or # create attestations. It consumes only inert artifacts produced by the # preceding jobs and never checks out or executes candidate source. @@ -284,14 +316,14 @@ jobs: - name: Download immutable release artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: gds-release-${{ inputs.version }}-${{ github.sha }} + name: gds-release-${{ needs.resolve.outputs.version }}-${{ github.sha }} path: ${{ runner.temp }}/gds-release-output merge-multiple: true - name: Download immutable offline evidence uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: gds-release-evidence-${{ inputs.version }}-${{ github.sha }} + name: gds-release-evidence-${{ needs.resolve.outputs.version }}-${{ github.sha }} path: ${{ runner.temp }}/gds-release-output/release-evidence merge-multiple: true @@ -309,50 +341,40 @@ jobs: test -s "$EVIDENCE_DIRECTORY/sbom.sigstore.json" test -s "$EVIDENCE_DIRECTORY/trusted-root.jsonl" - # Publish a durable GitHub Release only for a tag-dispatched run (the - # stable/frozen channels require an exact gds-v tag; canary - # dispatches on a branch and keeps its bundle as the workflow artifact - # above). Attaches the governance-verified release assets AND the offline + # Every run that reaches publication is a real release on an exact tag. + # The release attaches the governance-verified assets AND the offline # verification evidence: the workflow artifact expires, so evidence kept # only there would leave a published release unverifiable offline. - name: Publish GitHub Release - if: ${{ startsWith(github.ref, 'refs/tags/') }} shell: bash env: GH_TOKEN: ${{ github.token }} RELEASE_DIRECTORY: ${{ runner.temp }}/gds-release-output/release EVIDENCE_DIRECTORY: ${{ runner.temp }}/gds-release-output/release-evidence - RELEASE_TAG: ${{ github.ref_name }} - RELEASE_CHANNEL: ${{ inputs.channel }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} run: | set -euo pipefail - release_flags=() - case "$RELEASE_CHANNEL" in - stable) release_flags=(--latest) ;; - canary) release_flags=(--prerelease) ;; - esac gh release create "$RELEASE_TAG" \ --repo "$GITHUB_REPOSITORY" \ --title "GDS $RELEASE_TAG" \ --verify-tag \ - "${release_flags[@]}" \ - --notes "Immutable GDS ${RELEASE_CHANNEL} bundle for ${RELEASE_TAG}. Multi-platform binaries + SPDX SBOM + SHA256SUMS, with keyless SLSA build provenance and SBOM attestations. Online: gh attestation verify -R ${GITHUB_REPOSITORY}. Offline: verify against provenance.sigstore.json and sbom.sigstore.json using trusted-root.jsonl, all attached here." \ + --latest \ + --notes "Immutable GDS bundle for ${RELEASE_TAG}. Multi-platform binaries + SPDX SBOM + SHA256SUMS, with keyless SLSA build provenance and SBOM attestations. Online: gh attestation verify -R ${GITHUB_REPOSITORY}. Offline: verify against provenance.sigstore.json and sbom.sigstore.json using trusted-root.jsonl, all attached here." \ "$RELEASE_DIRECTORY"/* \ "$EVIDENCE_DIRECTORY"/* record-failure: name: Record failed release evidence - needs: [build, attest, publish] - if: ${{ always() && startsWith(github.ref, 'refs/tags/') && (needs.build.result == 'failure' || needs.attest.result == 'failure' || needs.publish.result == 'failure') }} + needs: [resolve, build, attest, publish] + if: ${{ always() && needs.resolve.outputs.released == 'true' && (needs.build.result == 'failure' || needs.attest.result == 'failure' || needs.publish.result == 'failure') }} runs-on: ubuntu-latest permissions: contents: write env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ github.ref_name }} - RELEASE_VERSION: ${{ inputs.version }} - RELEASE_SEQUENCE: ${{ inputs.release_sequence }} - RELEASE_CHANNEL: ${{ inputs.channel }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + RELEASE_VERSION: ${{ needs.resolve.outputs.version }} + RELEASE_SEQUENCE: ${{ needs.resolve.outputs.sequence }} BUILD_RESULT: ${{ needs.build.result }} ATTEST_RESULT: ${{ needs.attest.result }} PUBLISH_RESULT: ${{ needs.publish.result }} @@ -372,12 +394,11 @@ jobs: jq -n \ --arg version "$RELEASE_VERSION" \ --argjson sequence "$RELEASE_SEQUENCE" \ - --arg channel "$RELEASE_CHANNEL" \ --arg commit "$GITHUB_SHA" \ --arg ref "$GITHUB_REF" \ --argjson run_id "$GITHUB_RUN_ID" \ --slurpfile failed "$failed_jobs" \ - '{schema_version:1,status:"failed",bundle_version:$version,release_sequence:$sequence,channel:$channel,source_commit:$commit,source_ref:$ref,workflow_run_id:$run_id,failed_jobs:$failed[0],superseded_by:null}' \ + '{schema_version:1,status:"failed",bundle_version:$version,release_sequence:$sequence,source_commit:$commit,source_ref:$ref,workflow_run_id:$run_id,failed_jobs:$failed[0],superseded_by:null}' \ > "$envelope" if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" "$envelope" --repo "$GITHUB_REPOSITORY" @@ -386,7 +407,6 @@ jobs: --repo "$GITHUB_REPOSITORY" \ --title "GDS $RELEASE_TAG (failed)" \ --verify-tag \ - --prerelease \ --notes "Release production failed. See release-failure-envelope.json for machine-readable evidence; retain this tag and release until a later release records it as superseded." \ "$envelope" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index c03ad9e..00c2cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ Versioning. ## [Unreleased] +- Remove release channels from the release pipeline (ADR 0038). Bundle + manifests, release envelopes, locks, rollout documents and trust policy no + longer carry or require `channel`; the fields remain optional for decoding + documents produced while the field existed, and a legacy channel is still + checked against a consumer policy that lists it. +- Classify bundles by `release_sequence` alone: `0` is a development + projection, `>= 1` is a release. Development locks no longer record a + `development` channel. +- Require every release build to come from the exact + `refs/tags/gds-v` ref. The release workflow now runs on pushes to + `main`: a privileged resolve job derives the next patch version and + monotonic sequence from the latest published envelope over the GitHub API + without checking out candidate source, creates the tag, and hands the exact + identity to the unprivileged build job. +- Drop harness evidence from the release builder's inputs and from + `bundle.Build` gating. Harness evidence stays a separately produced and + verified estate/runtime signal in `core/harnessevidence` and the module + release evidence path; it no longer decides whether an artifact may be + published. +- Remove `default_bundle_channel` from the estate schema and the example + estate, and remove the `--channel` flag and evidence inputs from + `gds-release-builder` and the release-candidate command. + ## [0.9.7] - 2026-09-19 - Skip hidden directories during workspace discovery so tool-state and diff --git a/core/app/services.go b/core/app/services.go index 5c56d9f..9ffa46a 100644 --- a/core/app/services.go +++ b/core/app/services.go @@ -105,7 +105,6 @@ type StateInspectionData struct { type ReleaseCandidateOptions struct { BundleVersion string ReleaseSequence int - Channel string MinimumCLIVersion string } @@ -591,14 +590,10 @@ func (services *Services) BuildReleaseCandidate( sourceRef := "refs/heads/" + status.Branch.Name candidate, findings := bundle.Build(info.WorktreeRoot, bundle.BuildOptions{ BundleVersion: options.BundleVersion, ReleaseSequence: options.ReleaseSequence, - Channel: options.Channel, SourceCommit: status.Head.OID, + SourceCommit: status.Head.OID, MinimumCLIVersion: options.MinimumCLIVersion, Workflow: trust.Source.AllowedWorkflows[0], SourceRef: sourceRef, - // The candidate command has no evidence input surface and performs no - // publication. Its canary is therefore explicitly provisional; the hosted - // stable/frozen builder still requires the signed active-seven manifest. - HarnessEvidenceProvisional: options.Channel == "canary", - TrackedSources: trackedSources, + TrackedSources: trackedSources, }, trust, services.Schemas) if len(findings) != 0 { return domain.NewEnvelope( diff --git a/core/assurance/generation.go b/core/assurance/generation.go index 316a3b5..5db7328 100644 --- a/core/assurance/generation.go +++ b/core/assurance/generation.go @@ -238,7 +238,7 @@ func exerciseRollout( RolloutID: "rollout_01J00000000000000000000000", CreatedAt: now, Envelope: bundle.ReleaseEnvelope{ SchemaVersion: domain.SchemaVersion, BundleVersion: "1.0.0", ReleaseSequence: 1, - Channel: "canary", SourceCommit: "0123456789abcdef0123456789abcdef01234567", + SourceCommit: "0123456789abcdef0123456789abcdef01234567", ManifestDigest: digestFixture("manifest", 0), ArtifactDigest: digestFixture("artifact", 0), }, diff --git a/core/bundle/build.go b/core/bundle/build.go index f5c45c3..ee22dc1 100644 --- a/core/bundle/build.go +++ b/core/bundle/build.go @@ -100,17 +100,15 @@ func Build( } manifest := Manifest{ SchemaVersion: domain.SchemaVersion, BundleVersion: options.BundleVersion, - ReleaseSequence: options.ReleaseSequence, Channel: options.Channel, - SourceCommit: options.SourceCommit, SourceRef: options.SourceRef, - MinimumCLIVersion: options.MinimumCLIVersion, - ContentSetDigest: digestJSON(records), - PolicyDigest: subsetDigest(records, "policies/"), - SkillSetDigest: subsetDigest(records, "skills/"), - HarnessProfilesDigest: subsetDigest(records, "harnesses/"), - HarnessEvidenceManifestDigest: options.HarnessEvidenceManifestDigest, - HarnessEvidenceProvisional: options.HarnessEvidenceProvisional, - Files: records, - SupplyChain: SupplyChain{AttestationRequired: true, SBOMRequiredForExecutables: true}, + ReleaseSequence: options.ReleaseSequence, + SourceCommit: options.SourceCommit, SourceRef: options.SourceRef, + MinimumCLIVersion: options.MinimumCLIVersion, + ContentSetDigest: digestJSON(records), + PolicyDigest: subsetDigest(records, "policies/"), + SkillSetDigest: subsetDigest(records, "skills/"), + HarnessProfilesDigest: subsetDigest(records, "harnesses/"), + Files: records, + SupplyChain: SupplyChain{AttestationRequired: true, SBOMRequiredForExecutables: true}, } manifestBytes, err := json.MarshalIndent(manifest, "", " ") if err != nil { @@ -131,8 +129,8 @@ func Build( }) envelope := ReleaseEnvelope{ SchemaVersion: domain.SchemaVersion, BundleVersion: options.BundleVersion, - ReleaseSequence: options.ReleaseSequence, Channel: options.Channel, - SourceCommit: options.SourceCommit, SourceRef: options.SourceRef, + ReleaseSequence: options.ReleaseSequence, + SourceCommit: options.SourceCommit, SourceRef: options.SourceRef, ExecutableFiles: executableFiles, ManifestDigest: digest(manifestBytes), ArtifactDigest: digest(artifact), ExpectedAttestationIdentityDigest: identityDigest, @@ -326,24 +324,14 @@ func writeArchive(files []sourceFile, manifest []byte) ([]byte, error) { func validateBuildOptions(options BuildOptions, trust TrustPolicy) *domain.Finding { if options.ReleaseSequence < trust.Release.MinimumReleaseSequence || - !contains(trust.Release.AllowedChannels, options.Channel) || !contains(trust.Source.AllowedWorkflows, options.Workflow) || !allowedRef(trust.Source.AllowedRefs, options.SourceRef) { finding := bundleFinding( "GDS_BUNDLE_BUILD_POLICY_BLOCKED", - fmt.Errorf("release sequence, channel, workflow, or ref is outside trust policy"), + fmt.Errorf("release sequence, workflow, or ref is outside trust policy"), ) return &finding } - if (options.Channel == "stable" || options.Channel == "frozen") && - (options.HarnessEvidenceManifestDigest == "" || options.HarnessEvidenceProvisional) { - finding := bundleFinding("GDS_HARNESS_EVIDENCE_REQUIRED", fmt.Errorf("stable and frozen releases require exact non-provisional harness evidence")) - return &finding - } - if options.Channel == "canary" && !options.HarnessEvidenceProvisional && options.HarnessEvidenceManifestDigest == "" { - finding := bundleFinding("GDS_HARNESS_EVIDENCE_IDENTITY_MISSING", fmt.Errorf("non-provisional canary requires a bound harness evidence manifest")) - return &finding - } return nil } diff --git a/core/bundle/bundle_test.go b/core/bundle/bundle_test.go index 369ea3d..d6b4690 100644 --- a/core/bundle/bundle_test.go +++ b/core/bundle/bundle_test.go @@ -347,11 +347,10 @@ func testTrust() TrustPolicy { func testBuildOptions(root string) BuildOptions { return BuildOptions{ - BundleVersion: "1.0.0", ReleaseSequence: 1, Channel: "canary", + BundleVersion: "1.0.0", ReleaseSequence: 1, SourceCommit: testSourceCommit, MinimumCLIVersion: "1.0.0", Workflow: ".github/workflows/release-bundle.yml", SourceRef: "refs/heads/main", - TrackedSources: fixtureTrackedSources(root), - HarnessEvidenceProvisional: true, + TrackedSources: fixtureTrackedSources(root), } } diff --git a/core/bundle/types.go b/core/bundle/types.go index aac2ef1..94302a6 100644 --- a/core/bundle/types.go +++ b/core/bundle/types.go @@ -47,7 +47,7 @@ type Manifest struct { SchemaVersion int `json:"schema_version"` BundleVersion string `json:"bundle_version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + Channel string `json:"channel,omitempty"` SourceCommit string `json:"source_commit"` SourceRef string `json:"source_ref"` MinimumCLIVersion string `json:"minimum_cli_version"` @@ -56,7 +56,7 @@ type Manifest struct { SkillSetDigest string `json:"skill_set_digest"` HarnessProfilesDigest string `json:"harness_profiles_digest"` HarnessEvidenceManifestDigest string `json:"harness_evidence_manifest_digest,omitempty"` - HarnessEvidenceProvisional bool `json:"harness_evidence_provisional"` + HarnessEvidenceProvisional bool `json:"harness_evidence_provisional,omitempty"` Files []FileRecord `json:"files"` SupplyChain SupplyChain `json:"supply_chain"` } @@ -87,7 +87,7 @@ type ReleaseEnvelope struct { SchemaVersion int `json:"schema_version"` BundleVersion string `json:"bundle_version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + Channel string `json:"channel,omitempty"` SourceCommit string `json:"source_commit"` SourceRef string `json:"source_ref"` ExecutableFiles int `json:"executable_files"` @@ -97,17 +97,14 @@ type ReleaseEnvelope struct { } type BuildOptions struct { - BundleVersion string - ReleaseSequence int - Channel string - SourceCommit string - MinimumCLIVersion string - Workflow string - SourceRef string - TrackedSources []string - AdditionalFiles []AdditionalFile - HarnessEvidenceManifestDigest string - HarnessEvidenceProvisional bool + BundleVersion string + ReleaseSequence int + SourceCommit string + MinimumCLIVersion string + Workflow string + SourceRef string + TrackedSources []string + AdditionalFiles []AdditionalFile } type AdditionalFile struct { diff --git a/core/bundle/verify.go b/core/bundle/verify.go index 78ec54e..1d38762 100644 --- a/core/bundle/verify.go +++ b/core/bundle/verify.go @@ -18,7 +18,7 @@ func Verify( now time.Time, ) (VerificationResult, []domain.Finding) { findings := []domain.Finding{} - if !contains(trust.Release.AllowedChannels, envelope.Channel) { + if envelope.Channel != "" && !contains(trust.Release.AllowedChannels, envelope.Channel) { findings = append(findings, verificationFinding( "GDS_BUNDLE_CHANNEL_BLOCKED", "Bundle channel is outside the consumer trust policy.", )) diff --git a/core/cli/root.go b/core/cli/root.go index 4aa5e8d..d17f103 100644 --- a/core/cli/root.go +++ b/core/cli/root.go @@ -965,7 +965,6 @@ func (executor *executor) releaseCommand() *cobra.Command { } candidate.Flags().StringVar(&values.BundleVersion, "version", "", "candidate semantic version") candidate.Flags().IntVar(&values.ReleaseSequence, "sequence", 0, "monotonic release sequence") - candidate.Flags().StringVar(&values.Channel, "channel", "canary", "release channel") candidate.Flags().StringVar( &values.MinimumCLIVersion, "minimum-cli-version", "0.1.0", "minimum compatible GDS CLI version", ) diff --git a/core/cli/root_test.go b/core/cli/root_test.go index cce395b..60abff6 100644 --- a/core/cli/root_test.go +++ b/core/cli/root_test.go @@ -233,10 +233,10 @@ func TestGenerateRepositoryConsumesVerifiedReleasedBundle(t *testing.T) { t.Fatal(err) } candidate, findings := bundle.Build(source, bundle.BuildOptions{ - BundleVersion: "9.0.0", ReleaseSequence: 900, Channel: "canary", + BundleVersion: "9.0.0", ReleaseSequence: 900, SourceCommit: sourceCommit, MinimumCLIVersion: "9.0.0", Workflow: ".github/workflows/release-bundle.yml", SourceRef: "refs/heads/main", - TrackedSources: tracked, HarnessEvidenceProvisional: true, + TrackedSources: tracked, }, bundle.TrustPolicy{ SchemaVersion: 1, TrustDomain: "gds-release", Source: bundle.TrustSource{Owner: "NDDev-OpenNetwork", Repository: "github-device-sync", @@ -282,7 +282,7 @@ func TestGenerateRepositoryConsumesVerifiedReleasedBundle(t *testing.T) { data := result.Data.(map[string]any) released := data["bundle"].(map[string]any) if released["version"] != "9.0.0" || released["release_sequence"] != float64(900) || - released["channel"] != "canary" || released["digest"] != candidate.Envelope.ArtifactDigest { + released["channel"] != nil || released["digest"] != candidate.Envelope.ArtifactDigest { t.Fatalf("released bundle identity = %#v", released) } } diff --git a/core/cmd/gds-release-builder/main.go b/core/cmd/gds-release-builder/main.go index d2ca692..b193a4a 100644 --- a/core/cmd/gds-release-builder/main.go +++ b/core/cmd/gds-release-builder/main.go @@ -36,11 +36,8 @@ func run(ctx context.Context, arguments []string, stdout, stderr io.Writer) int flags.StringVar(&request.OutputDirectory, "output", "", "new release output directory") flags.StringVar(&request.Version, "version", "", "release SemVer without a v prefix") flags.IntVar(&request.ReleaseSequence, "sequence", 0, "monotonic release sequence") - flags.StringVar(&request.Channel, "channel", "canary", "canary, stable, or frozen") flags.StringVar(&request.MinimumCLIVersion, "minimum-cli-version", "", "minimum compatible CLI SemVer") flags.StringVar(&request.SourceRef, "source-ref", "", "exact refs/heads/* or refs/tags/* source ref") - flags.StringVar(&request.HarnessEvidenceDirectory, "harness-evidence-directory", "", "directory containing manifest.json and isolated active-harness records") - flags.StringVar(&request.HarnessEvidenceTrustPolicy, "harness-evidence-trust-policy", "", "offline public trust policy for harness evidence") flags.StringVar( &request.GoBinary, "go-binary", "", "absolute Go executable (defaults to the running toolchain GOROOT when available)", diff --git a/core/compiler/types.go b/core/compiler/types.go index edf6cc3..937c509 100644 --- a/core/compiler/types.go +++ b/core/compiler/types.go @@ -10,8 +10,8 @@ import ( // DevelopmentBundleVersion stamps bundles compiled from policy sources on a // policy-owner checkout. It tracks the current release line with a -dev -// suffix so a development bundle is dated honestly; the development channel -// field, not this string, is what classifies the bundle. +// suffix so a development bundle is dated honestly; a zero release sequence, +// not this string, is what classifies the bundle. const DevelopmentBundleVersion = "0.9.7-dev" type PolicySource struct { diff --git a/core/context/provenance.go b/core/context/provenance.go index 9659b65..bb49123 100644 --- a/core/context/provenance.go +++ b/core/context/provenance.go @@ -68,7 +68,7 @@ func (prover *CanonicalPolicyProver) Verify( err, )} } - if document.Bundle.Channel != "development" { + if document.Bundle.ReleaseSequence != 0 { // Released candidates can only be produced from an archive that passed // full envelope, manifest and member verification. The committed lock // retains that artifact/content/attestation identity and the exact @@ -76,13 +76,14 @@ func (prover *CanonicalPolicyProver) Verify( // in every consumer repository. return nil } - // The channel alone classifies the bundle. This used to also require the - // version to equal the current DevelopmentBundleVersion, which meant that - // bumping that constant silently reclassified every not-yet-regenerated - // development lock as released and skipped its source verification — the - // opposite of what a version bump should do. A development bundle carrying - // an older dev version is still a development bundle and is verified as - // one. + // A zero release sequence alone classifies the bundle as development: the + // release builder rejects sequence < 1, so no published bundle can carry + // it. This used to also require the version to equal the current + // DevelopmentBundleVersion, which meant that bumping that constant + // silently reclassified every not-yet-regenerated development lock as + // released and skipped its source verification — the opposite of what a + // version bump should do. A development bundle carrying an older dev + // version is still a development bundle and is verified as one. if estateRoot == "" { return []domain.Finding{policyProvenanceFinding( "GDS_CONTEXT_POLICY_ESTATE_NOT_PROVEN", @@ -185,7 +186,6 @@ func (prover *CanonicalPolicyProver) Verify( // legitimately differ, which is the whole point. if bundle.Version != document.Bundle.Version || bundle.ReleaseSequence != document.Bundle.ReleaseSequence || - bundle.Channel != document.Bundle.Channel || bundle.Digest != document.Bundle.Digest { return []domain.Finding{{ Code: "GDS_CONTEXT_POLICY_BUNDLE_MISMATCH", diff --git a/core/context/resolver_test.go b/core/context/resolver_test.go index b89b6c1..5f68668 100644 --- a/core/context/resolver_test.go +++ b/core/context/resolver_test.go @@ -202,7 +202,7 @@ func TestCanonicalPolicyProverRejectsCommittedSelfConsistentReplacement(t *testi func TestCanonicalPolicyProverRejectsUncommittedReleasedProjection(t *testing.T) { resolver := newTestResolver(t) document := bundleLockDocument{} - document.Bundle.Channel = "stable" + document.Bundle.ReleaseSequence = 1 document.Bundle.Version = "1.0.0" findings := resolver.prover.Verify( context.Background(), t.TempDir(), t.TempDir(), domain.RepositoryAnchor{}, document, @@ -234,7 +234,7 @@ func TestCanonicalPolicyProverAcceptsCommittedReleasedProjectionIdentity(t *test runContextGit(t, root, "add", "--all") runContextGit(t, root, "commit", "--quiet", "-m", "released projection") document := bundleLockDocument{} - document.Bundle.Channel = "stable" + document.Bundle.ReleaseSequence = 1 document.Bundle.Version = "1.0.0" anchor := domain.RepositoryAnchor{ Repository: domain.RepositoryIdentity{Roles: []string{"module"}}, diff --git a/core/estate/types.go b/core/estate/types.go index 8f51ca4..7a5e3c7 100644 --- a/core/estate/types.go +++ b/core/estate/types.go @@ -51,9 +51,8 @@ type MutationGates struct { } type Estate struct { - ID string `json:"id"` - Name string `json:"name"` - DefaultBundleChannel string `json:"default_bundle_channel"` + ID string `json:"id"` + Name string `json:"name"` } type Rollout struct { diff --git a/core/harnessevidence/evidence.go b/core/harnessevidence/evidence.go index baa0bd9..bd9c86f 100644 --- a/core/harnessevidence/evidence.go +++ b/core/harnessevidence/evidence.go @@ -76,14 +76,16 @@ type ManifestEntry struct { } type ManifestPayload struct { - SchemaVersion int `json:"schema_version"` - ManifestID string `json:"manifest_id"` - HarnessRootSHA string `json:"harness_root_sha"` - Channel string `json:"channel"` - GeneratedAt time.Time `json:"generated_at"` - ExpiresAt time.Time `json:"expires_at"` - ActorID string `json:"actor_id"` - Evidence []ManifestEntry `json:"evidence"` + SchemaVersion int `json:"schema_version"` + ManifestID string `json:"manifest_id"` + HarnessRootSHA string `json:"harness_root_sha"` + // Channel is retained only so payloads signed while the field existed + // still re-digest to their recorded manifest digest. New manifests omit it. + Channel string `json:"channel,omitempty"` + GeneratedAt time.Time `json:"generated_at"` + ExpiresAt time.Time `json:"expires_at"` + ActorID string `json:"actor_id"` + Evidence []ManifestEntry `json:"evidence"` } type Manifest struct { @@ -93,7 +95,6 @@ type Manifest struct { } type Expectation struct { - Channel string HarnessRootSHA string ModuleSHAs map[string]string ExecutableVersions map[string]string @@ -104,14 +105,6 @@ type Expectation struct { type Verifier struct{ Trust trust.Verifier } -type GateResult struct { - Channel string `json:"channel"` - Status string `json:"status"` - Provisional bool `json:"provisional"` - Missing []string `json:"missing"` - AutoPromote bool `json:"auto_promote"` -} - func (verifier Verifier) Verify(record Record, expected Expectation) error { p := record.Payload digest, err := canonicaljson.Digest(p) @@ -138,10 +131,9 @@ func (verifier Verifier) VerifyManifest(manifest Manifest, records []Record, exp if err != nil || digest != manifest.ManifestDigest { return errors.New("harness evidence manifest digest mismatch") } - if p.SchemaVersion != 1 || p.HarnessRootSHA != expected.HarnessRootSHA || p.Channel != expected.Channel || + if p.SchemaVersion != 1 || p.HarnessRootSHA != expected.HarnessRootSHA || p.GeneratedAt.After(expected.Now) || !expected.Now.Before(p.ExpiresAt) || - !p.ExpiresAt.After(p.GeneratedAt) || p.ExpiresAt.Sub(p.GeneratedAt) > 72*time.Hour || - (p.Channel != "canary" && p.Channel != "stable" && p.Channel != "frozen") { + !p.ExpiresAt.After(p.GeneratedAt) || p.ExpiresAt.Sub(p.GeneratedAt) > 72*time.Hour { return errors.New("harness evidence manifest identity is invalid") } if err := verifier.Trust.Verify("gds-harness-runtime-manifest/v1", p.ActorID, "harness-evidence-aggregate", p.GeneratedAt, p, manifest.Signature); err != nil { @@ -171,58 +163,3 @@ func (verifier Verifier) VerifyManifest(manifest Manifest, records []Record, exp } return nil } - -// EvaluateChannel applies release-channel semantics. Canary may carry an -// explicitly provisional subset, but it can never auto-promote. Stable and -// frozen require the exact signed active-seven closure. -func (verifier Verifier) EvaluateChannel(manifest Manifest, records []Record, expected Expectation) (GateResult, error) { - if expected.Channel == "stable" || expected.Channel == "frozen" { - if err := verifier.VerifyManifest(manifest, records, expected); err != nil { - return GateResult{}, err - } - return GateResult{Channel: expected.Channel, Status: "pass"}, nil - } - if expected.Channel != "canary" || manifest.Payload.Channel != "canary" { - return GateResult{}, errors.New("release channel is invalid") - } - // Prove the aggregate identity even when its evidence list is incomplete. - p := manifest.Payload - digest, err := canonicaljson.Digest(p) - if err != nil || digest != manifest.ManifestDigest || p.HarnessRootSHA != expected.HarnessRootSHA || - p.GeneratedAt.After(expected.Now) || !expected.Now.Before(p.ExpiresAt) || - !p.ExpiresAt.After(p.GeneratedAt) || p.ExpiresAt.Sub(p.GeneratedAt) > 72*time.Hour { - return GateResult{}, errors.New("canary harness manifest identity is invalid") - } - if err := verifier.Trust.Verify("gds-harness-runtime-manifest/v1", p.ActorID, "harness-evidence-aggregate", p.GeneratedAt, p, manifest.Signature); err != nil { - return GateResult{}, err - } - byID := map[string]Record{} - for _, record := range records { - if _, duplicate := byID[record.Payload.HarnessID]; duplicate { - return GateResult{}, errors.New("canary repeats isolated harness evidence") - } - byID[record.Payload.HarnessID] = record - } - listed := map[string]string{} - for _, entry := range p.Evidence { - if !slices.Contains(ActiveHarnesses, entry.HarnessID) || listed[entry.HarnessID] != "" { - return GateResult{}, errors.New("canary manifest contains duplicate or non-active harness identity") - } - listed[entry.HarnessID] = entry.EvidenceDigest - } - missing := []string{} - for _, id := range ActiveHarnesses { - record, found := byID[id] - if !found || listed[id] == "" { - missing = append(missing, id) - continue - } - if listed[id] != record.EvidenceDigest { - return GateResult{}, fmt.Errorf("canary manifest digest differs for %s", id) - } - if err := verifier.Verify(record, expected); err != nil { - return GateResult{}, fmt.Errorf("verify canary %s evidence: %w", id, err) - } - } - return GateResult{Channel: "canary", Status: "provisional", Provisional: true, Missing: missing, AutoPromote: false}, nil -} diff --git a/core/harnessevidence/evidence_test.go b/core/harnessevidence/evidence_test.go index 6af5e45..605081b 100644 --- a/core/harnessevidence/evidence_test.go +++ b/core/harnessevidence/evidence_test.go @@ -21,7 +21,7 @@ func TestIsolatedEvidenceAndAggregateRequireExactActiveSeven(t *testing.T) { ValidFrom: now.Add(-time.Hour), ValidUntil: now.Add(7 * 24 * time.Hour), Status: "active", }}, }}}} - expected := Expectation{Channel: "stable", HarnessRootSHA: "root-sha", Now: now, + expected := Expectation{HarnessRootSHA: "root-sha", Now: now, ModuleSHAs: map[string]string{}, ExecutableVersions: map[string]string{}, ProfileDigests: map[string]string{}, BridgeDigests: map[string]string{}} records := make([]Record, 0, len(ActiveHarnesses)) @@ -42,7 +42,7 @@ func TestIsolatedEvidenceAndAggregateRequireExactActiveSeven(t *testing.T) { records = append(records, record) entries = append(entries, ManifestEntry{HarnessID: id, EvidenceDigest: digest}) } - payload := ManifestPayload{SchemaVersion: 1, ManifestID: "manifest-1", HarnessRootSHA: "root-sha", Channel: "stable", + payload := ManifestPayload{SchemaVersion: 1, ManifestID: "manifest-1", HarnessRootSHA: "root-sha", GeneratedAt: now.Add(-30 * time.Minute), ExpiresAt: now.Add(24 * time.Hour), ActorID: "nddev-harness-release", Evidence: entries} digest, _ := canonicaljson.Digest(payload) manifest := Manifest{Payload: payload, ManifestDigest: digest, Signature: sign(t, private, "gds-harness-runtime-manifest/v1", payload)} diff --git a/core/projections/generator.go b/core/projections/generator.go index 1bc90f9..71df3d7 100644 --- a/core/projections/generator.go +++ b/core/projections/generator.go @@ -197,7 +197,7 @@ func (generator *Generator) DevelopmentBundleFromSourceCommit( } return Bundle{ Version: compiler.DevelopmentBundleVersion, ReleaseSequence: 0, - Channel: "development", SourceCommit: sourceCommit, Digest: digest, + SourceCommit: sourceCommit, Digest: digest, }, nil } @@ -251,7 +251,7 @@ func (generator *Generator) DevelopmentBundle( } return Bundle{ Version: compiler.DevelopmentBundleVersion, ReleaseSequence: 0, - Channel: "development", SourceCommit: sourceCommit, + SourceCommit: sourceCommit, SourceTreeDigest: sourceTreeDigest, Digest: digest, }, nil } @@ -556,7 +556,6 @@ func renderLockYAML(document lockDocument) []byte { output.WriteString("bundle:\n") writeYAMLString(&output, 2, "version", document.Bundle.Version) output.WriteString(fmt.Sprintf(" release_sequence: %d\n", document.Bundle.ReleaseSequence)) - writeYAMLString(&output, 2, "channel", document.Bundle.Channel) // A content-addressed lock does not record the commit at all. Writing it // made the lock its own moving target: re-rendering after the commit that // carried it produced different bytes, so the lock went stale against diff --git a/core/projections/types.go b/core/projections/types.go index a29df7c..450e386 100644 --- a/core/projections/types.go +++ b/core/projections/types.go @@ -10,7 +10,9 @@ import ( type Bundle struct { Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + // Channel is accepted while decoding legacy v1 locks and manifests; it is + // never written and is not part of bundle identity. + Channel string `json:"channel,omitempty"` // SourceCommit is trace metadata: it records which commit carried the // canonical sources when the bundle was generated. It is deliberately not // part of the bundle identity, because a commit cannot contain its own SHA @@ -30,7 +32,6 @@ type Bundle struct { type BundleIdentity struct { Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` SourceTreeDigest string `json:"source_tree_digest,omitempty"` Digest string `json:"digest"` AttestationIdentityDigest string `json:"attestation_identity_digest,omitempty"` @@ -39,8 +40,8 @@ type BundleIdentity struct { func (bundle Bundle) Identity() BundleIdentity { return BundleIdentity{ Version: bundle.Version, ReleaseSequence: bundle.ReleaseSequence, - Channel: bundle.Channel, SourceTreeDigest: bundle.SourceTreeDigest, - Digest: bundle.Digest, AttestationIdentityDigest: bundle.AttestationIdentityDigest, + SourceTreeDigest: bundle.SourceTreeDigest, + Digest: bundle.Digest, AttestationIdentityDigest: bundle.AttestationIdentityDigest, } } diff --git a/core/releasebuilder/builder.go b/core/releasebuilder/builder.go index 9eaeaa0..9da1ec2 100644 --- a/core/releasebuilder/builder.go +++ b/core/releasebuilder/builder.go @@ -39,10 +39,6 @@ func Build(ctx context.Context, request Request, schemas *validation.Set) (resul if err != nil { return Result{}, err } - harnessDigest, harnessProvisional, err := verifyHarnessEvidence(request, root) - if err != nil { - return Result{}, err - } gitAuthority, err := gitauthority.Discover() if err != nil { return Result{}, err @@ -55,7 +51,7 @@ func Build(ctx context.Context, request Request, schemas *validation.Set) (resul if err != nil { return Result{}, err } - if err := validateReleaseRef(source.Ref, request.Version, request.Channel); err != nil { + if err := validateReleaseRef(source.Ref, request.Version); err != nil { return Result{}, err } goBinary := request.GoBinary @@ -120,12 +116,10 @@ func Build(ctx context.Context, request Request, schemas *validation.Set) (resul } options := bundle.BuildOptions{ BundleVersion: request.Version, ReleaseSequence: request.ReleaseSequence, - Channel: request.Channel, SourceCommit: source.Commit, + SourceCommit: source.Commit, MinimumCLIVersion: request.MinimumCLIVersion, Workflow: trust.Source.AllowedWorkflows[0], SourceRef: source.Ref, TrackedSources: trackedSources, AdditionalFiles: additional, - HarnessEvidenceManifestDigest: harnessDigest, - HarnessEvidenceProvisional: harnessProvisional, } first, findings := bundle.Build(root, options, trust, schemas) if len(findings) != 0 { @@ -187,11 +181,8 @@ func validateVerifierTargetCoverage(trust bundle.TrustPolicy) error { func validateRequest(request Request) (string, string, error) { if !semver.Valid(request.Version) || request.ReleaseSequence < 1 || - (request.Channel != "canary" && request.Channel != "stable" && request.Channel != "frozen") || - !semver.Valid(request.MinimumCLIVersion) || - ((request.HarnessEvidenceDirectory == "") != (request.HarnessEvidenceTrustPolicy == "")) || - ((request.Channel == "stable" || request.Channel == "frozen") && request.HarnessEvidenceDirectory == "") { - return "", "", errors.New("release request version, sequence, channel, or CLI floor is invalid") + !semver.Valid(request.MinimumCLIVersion) { + return "", "", errors.New("release request version, sequence, or CLI floor is invalid") } root, err := filepath.Abs(request.Root) if err != nil { @@ -306,19 +297,10 @@ func inspectGoVersion(ctx context.Context, root string, goBinary string, home st return fields[2], nil } -func validateReleaseRef(ref, version, channel string) error { +func validateReleaseRef(ref, version string) error { expectedTag := "refs/tags/gds-v" + version - if strings.HasPrefix(ref, "refs/tags/") && ref != expectedTag { - return errors.New("release tag does not match the requested bundle version") - } - if channel == "stable" || channel == "frozen" { - if ref != expectedTag { - return errors.New("stable and frozen releases require the exact version tag") - } - return nil - } - if channel == "canary" && ref != "refs/heads/main" && ref != expectedTag { - return errors.New("canary releases require main or the exact version tag") + if ref != expectedTag { + return errors.New("releases require the exact version tag") } return nil } diff --git a/core/releasebuilder/harness_evidence.go b/core/releasebuilder/harness_evidence.go deleted file mode 100644 index f840367..0000000 --- a/core/releasebuilder/harness_evidence.go +++ /dev/null @@ -1,96 +0,0 @@ -package releasebuilder - -import ( - "crypto/sha256" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "time" - - "github.com/NDDev-OpenNetwork/github-device-sync/core/harnessevidence" - "github.com/NDDev-OpenNetwork/github-device-sync/core/trust" -) - -func verifyHarnessEvidence(request Request, root string) (string, bool, error) { - if request.HarnessEvidenceDirectory == "" { - return "", request.Channel == "canary", nil - } - directory, err := filepath.Abs(request.HarnessEvidenceDirectory) - if err != nil || directory == root { - return "", false, errors.New("harness evidence directory is invalid") - } - policy, err := trust.LoadPolicy(request.HarnessEvidenceTrustPolicy) - if err != nil { - return "", false, fmt.Errorf("load harness evidence trust: %w", err) - } - producerCommit, moduleSHAs, err := harnessevidence.AnchoredIdentity(policy) - if err != nil { - return "", false, err - } - var manifest harnessevidence.Manifest - if err := readEvidenceJSON(filepath.Join(directory, "manifest.json"), &manifest); err != nil { - return "", false, err - } - expected := harnessevidence.Expectation{ - Channel: request.Channel, HarnessRootSHA: producerCommit, - ModuleSHAs: moduleSHAs, Now: time.Now().UTC(), - ExecutableVersions: map[string]string{}, ProfileDigests: map[string]string{}, BridgeDigests: map[string]string{}, - } - bridgeRaw, err := os.ReadFile(filepath.Join(root, "harnesses", "module-bridge.yaml")) - if err != nil { - return "", false, err - } - bridgeDigest := evidenceBytesDigest(bridgeRaw) - records := make([]harnessevidence.Record, 0, len(manifest.Payload.Evidence)) - for _, entry := range manifest.Payload.Evidence { - var record harnessevidence.Record - if err := readEvidenceJSON(filepath.Join(directory, entry.HarnessID+".json"), &record); err != nil { - return "", false, err - } - records = append(records, record) - expected.ExecutableVersions[entry.HarnessID] = record.Payload.ExecutableVersion - profileRaw, readErr := os.ReadFile(filepath.Join(root, "harnesses", entry.HarnessID, "profile.yaml")) - if readErr != nil { - return "", false, readErr - } - expected.ProfileDigests[entry.HarnessID] = evidenceBytesDigest(profileRaw) - expected.BridgeDigests[entry.HarnessID] = bridgeDigest - } - result, err := (harnessevidence.Verifier{Trust: trust.Verifier{Policy: policy}}).EvaluateChannel(manifest, records, expected) - if err != nil { - return "", false, fmt.Errorf("verify signed active harness evidence: %w", err) - } - return manifest.ManifestDigest, result.Provisional, nil -} - -func readEvidenceJSON(path string, target any) error { - info, err := os.Lstat(path) - if err != nil { - return fmt.Errorf("inspect harness evidence %s: %w", path, err) - } - if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() < 2 || info.Size() > 2<<20 { - return fmt.Errorf("harness evidence %s is not a bounded regular file", path) - } - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - decoder := json.NewDecoder(file) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return fmt.Errorf("decode harness evidence %s: %w", path, err) - } - var trailing any - if err := decoder.Decode(&trailing); err != io.EOF { - return fmt.Errorf("harness evidence %s has trailing JSON", path) - } - return nil -} - -func evidenceBytesDigest(raw []byte) string { - return fmt.Sprintf("sha256:%x", sha256.Sum256(raw)) -} diff --git a/core/releasebuilder/output.go b/core/releasebuilder/output.go index f99157e..091d790 100644 --- a/core/releasebuilder/output.go +++ b/core/releasebuilder/output.go @@ -56,8 +56,8 @@ func releaseOutputFiles( files[releaseChecksumsName] = checksums return Result{ SchemaVersion: domain.SchemaVersion, Version: request.Version, - ReleaseSequence: request.ReleaseSequence, Channel: request.Channel, - Source: source, GoVersion: goVersion, + ReleaseSequence: request.ReleaseSequence, + Source: source, GoVersion: goVersion, GitVersion: gitIdentity.Version, GitDigest: gitIdentity.Digest, ArtifactName: artifactName, ArtifactDigest: candidate.Envelope.ArtifactDigest, ManifestDigest: candidate.Envelope.ManifestDigest, @@ -276,8 +276,8 @@ func VerifyDirectory(directory string, schemas *validation.Set) (DirectoryVerifi } return DirectoryVerification{ SchemaVersion: domain.SchemaVersion, Status: "verified", Version: envelope.BundleVersion, - ReleaseSequence: envelope.ReleaseSequence, Channel: envelope.Channel, - SourceCommit: envelope.SourceCommit, SourceRef: envelope.SourceRef, + ReleaseSequence: envelope.ReleaseSequence, + SourceCommit: envelope.SourceCommit, SourceRef: envelope.SourceRef, ArtifactName: artifactName, ArtifactDigest: envelope.ArtifactDigest, ManifestDigest: envelope.ManifestDigest, SBOMDigest: digestBytes(contents[releaseSBOMName]), Reproducible: true, Files: files, diff --git a/core/releasebuilder/output_test.go b/core/releasebuilder/output_test.go index 68f90e6..b053414 100644 --- a/core/releasebuilder/output_test.go +++ b/core/releasebuilder/output_test.go @@ -148,23 +148,21 @@ func TestRemoveReleaseOutputIfSameRefusesForeignDirectory(t *testing.T) { } func TestValidateReleaseRef(t *testing.T) { - accepted := []struct{ ref, version, channel string }{ - {"refs/heads/main", "1.2.3-canary.1", "canary"}, - {"refs/tags/gds-v1.2.3", "1.2.3", "stable"}, - {"refs/tags/gds-v1.2.3", "1.2.3", "frozen"}, + accepted := []struct{ ref, version string }{ + {"refs/tags/gds-v1.2.3", "1.2.3"}, } for _, value := range accepted { - if err := validateReleaseRef(value.ref, value.version, value.channel); err != nil { + if err := validateReleaseRef(value.ref, value.version); err != nil { t.Fatalf("accepted ref rejected: %+v: %v", value, err) } } - rejected := []struct{ ref, version, channel string }{ - {"refs/heads/main", "1.2.3", "stable"}, - {"refs/heads/feature", "1.2.3-canary.1", "canary"}, - {"refs/tags/gds-v1.2.2", "1.2.3", "stable"}, + rejected := []struct{ ref, version string }{ + {"refs/heads/main", "1.2.3"}, + {"refs/heads/feature", "1.2.3-canary.1"}, + {"refs/tags/gds-v1.2.2", "1.2.3"}, } for _, value := range rejected { - if err := validateReleaseRef(value.ref, value.version, value.channel); err == nil { + if err := validateReleaseRef(value.ref, value.version); err == nil { t.Fatalf("unsafe ref accepted: %+v", value) } } @@ -272,13 +270,12 @@ verification: }, }, } - request := Request{Version: "1.2.3", ReleaseSequence: 7, Channel: "canary", MinimumCLIVersion: "1.2.3"} + request := Request{Version: "1.2.3", ReleaseSequence: 7, MinimumCLIVersion: "1.2.3"} candidate, findings := bundle.Build(root, bundle.BuildOptions{ - BundleVersion: "1.2.3", ReleaseSequence: 7, Channel: "canary", + BundleVersion: "1.2.3", ReleaseSequence: 7, SourceCommit: source.Commit, MinimumCLIVersion: "1.2.3", Workflow: trust.Source.AllowedWorkflows[0], SourceRef: source.Ref, - TrackedSources: tracked, - HarnessEvidenceProvisional: true, + TrackedSources: tracked, AdditionalFiles: []bundle.AdditionalFile{ {Path: "bin/linux/amd64/gds", Content: binary, Mode: "0755"}, {Path: "sbom/gds.spdx.json", Content: sbom, Mode: "0644"}, diff --git a/core/releasebuilder/source_snapshot_test.go b/core/releasebuilder/source_snapshot_test.go index cb831e6..0e2a6a4 100644 --- a/core/releasebuilder/source_snapshot_test.go +++ b/core/releasebuilder/source_snapshot_test.go @@ -70,10 +70,9 @@ func TestReleaseArtifactUsesInspectedCommitSnapshot(t *testing.T) { }, } candidate, findings := bundle.Build(snapshot, bundle.BuildOptions{ - BundleVersion: "1.2.3-canary.1", ReleaseSequence: 1, Channel: "canary", + BundleVersion: "1.2.3", ReleaseSequence: 1, SourceCommit: source.Commit, SourceRef: source.Ref, MinimumCLIVersion: "1.2.3", Workflow: trust.Source.AllowedWorkflows[0], TrackedSources: tracked, - HarnessEvidenceProvisional: true, }, trust, schemas) if len(findings) != 0 { t.Fatalf("build snapshot artifact: %+v", findings) @@ -138,7 +137,7 @@ func TestValidateRequestRejectsOutputInsideSourceRoot(t *testing.T) { }) _, _, err := validateRequest(Request{ Root: repository, OutputDirectory: filepath.Join(repository, "release-output"), - Version: "1.2.3", ReleaseSequence: 1, Channel: "canary", + Version: "1.2.3", ReleaseSequence: 1, MinimumCLIVersion: "1.2.3", }) if err == nil { diff --git a/core/releasebuilder/types.go b/core/releasebuilder/types.go index 8606de3..95e2509 100644 --- a/core/releasebuilder/types.go +++ b/core/releasebuilder/types.go @@ -17,17 +17,13 @@ func defaultTargets() []Target { } type Request struct { - Root string - OutputDirectory string - Version string - ReleaseSequence int - Channel string - MinimumCLIVersion string - SourceRef string - GoBinary string - HarnessEvidenceManifestDigest string - HarnessEvidenceDirectory string - HarnessEvidenceTrustPolicy string + Root string + OutputDirectory string + Version string + ReleaseSequence int + MinimumCLIVersion string + SourceRef string + GoBinary string } type Source struct { @@ -46,7 +42,6 @@ type Result struct { SchemaVersion int `json:"schema_version"` Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` Source Source `json:"source"` GoVersion string `json:"go_version"` GitVersion string `json:"git_version"` @@ -65,7 +60,6 @@ type DirectoryVerification struct { Status string `json:"status"` Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` SourceCommit string `json:"source_commit"` SourceRef string `json:"source_ref"` ArtifactName string `json:"artifact_name"` diff --git a/core/releasebuilder/workflow_contract_test.go b/core/releasebuilder/workflow_contract_test.go index df96f62..2227893 100644 --- a/core/releasebuilder/workflow_contract_test.go +++ b/core/releasebuilder/workflow_contract_test.go @@ -93,11 +93,10 @@ func TestHostedReleaseWorkflowUsesOutputOutsideSourceRoot(t *testing.T) { `> "$RELEASE_OUTPUT_ROOT/attestation-subjects.sha256"`, `path: ${{ runner.temp }}/gds-release-output`, `${{ runner.temp }}/gds-release-output/release-evidence`, - `HARNESS_EVIDENCE_TRUST_POLICY_DIGEST: ${{ vars.HARNESS_EVIDENCE_TRUST_POLICY_DIGEST }}`, - `stable/frozen requires signed active-seven harness evidence`, - `--harness-evidence-directory $EVIDENCE_INPUT_ROOT/records`, - `RELEASE_SEQUENCE: ${{ inputs.release_sequence }}`, - `canary) release_flags=(--prerelease) ;;`, + `RELEASE_SEQUENCE: ${{ needs.resolve.outputs.sequence }}`, + `--source-ref "refs/tags/$RELEASE_TAG"`, + `ref: ${{ needs.resolve.outputs.tag }}`, + `--latest`, `name: Record failed release evidence`, `release-failure-envelope.json`, `superseded_by:null`, @@ -106,8 +105,19 @@ func TestHostedReleaseWorkflowUsesOutputOutsideSourceRoot(t *testing.T) { t.Fatalf("hosted workflow is missing output contract %q", required) } } - if strings.Contains(content, "RELEASE_SEQUENCE: ${{ github.run_number }}") { - t.Fatal("release sequence regressed to repository-local workflow run numbering") + for _, forbidden := range []string{ + `inputs.channel`, + `CHANNEL`, + `harness_evidence`, + `HARNESS_EVIDENCE`, + `--channel`, + `--prerelease`, + "RELEASE_SEQUENCE: ${{ github.run_number }}", + "RELEASE_SEQUENCE: ${{ inputs.release_sequence }}", + } { + if strings.Contains(content, forbidden) { + t.Fatalf("hosted workflow still carries retired release input %q", forbidden) + } } if strings.Contains(content, `$GITHUB_WORKSPACE/$RELEASE_DIRECTORY`) { t.Fatal("hosted workflow still writes release output beneath the source root") @@ -120,7 +130,7 @@ func TestHostedReleaseWorkflowUsesOutputOutsideSourceRoot(t *testing.T) { outputParent := t.TempDir() _, output, err := validateRequest(Request{ Root: repositoryRoot, OutputDirectory: filepath.Join(outputParent, "release"), - Version: "1.2.3", ReleaseSequence: 1, Channel: "canary", MinimumCLIVersion: "1.0.0", + Version: "1.2.3", ReleaseSequence: 1, MinimumCLIVersion: "1.0.0", }) if err != nil { t.Fatalf("workflow-equivalent output rejected by builder: %v", err) diff --git a/core/releaseconsumer/install.go b/core/releaseconsumer/install.go index bbf0470..b4359e3 100644 --- a/core/releaseconsumer/install.go +++ b/core/releaseconsumer/install.go @@ -32,7 +32,7 @@ type InstallRecord struct { ReleaseKey string `json:"release_key"` BundleVersion string `json:"bundle_version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + Channel string `json:"channel,omitempty"` ArtifactDigest string `json:"artifact_digest"` ManifestDigest string `json:"manifest_digest"` AttestationIdentityDigest string `json:"attestation_identity_digest"` diff --git a/core/releaseconsumer/verify_test.go b/core/releaseconsumer/verify_test.go index 2398500..00031a1 100644 --- a/core/releaseconsumer/verify_test.go +++ b/core/releaseconsumer/verify_test.go @@ -198,12 +198,11 @@ verification: bundle.AdditionalFile{Path: "trust/bundle-trust.yaml", Content: trustRaw, Mode: "0644"}, ) candidate, findings := bundle.Build(root, bundle.BuildOptions{ - BundleVersion: version, ReleaseSequence: sequence, Channel: "stable", + BundleVersion: version, ReleaseSequence: sequence, SourceCommit: strings.Repeat("a", 40), SourceRef: "refs/tags/gds-v" + version, MinimumCLIVersion: "1.0.0", Workflow: trust.Source.AllowedWorkflows[0], - HarnessEvidenceManifestDigest: "sha256:" + strings.Repeat("e", 64), - TrackedSources: []string{"skills/registry.yaml"}, - AdditionalFiles: additional, + TrackedSources: []string{"skills/registry.yaml"}, + AdditionalFiles: additional, }, trust, schemas) if len(findings) != 0 { t.Fatalf("bundle findings: %+v", findings) diff --git a/core/rollout/plan.go b/core/rollout/plan.go index 29deda0..3f7ca4f 100644 --- a/core/rollout/plan.go +++ b/core/rollout/plan.go @@ -33,7 +33,7 @@ type Plan struct { type Bundle struct { Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + Channel string `json:"channel,omitempty"` ArtifactDigest string `json:"artifact_digest"` ManifestDigest string `json:"manifest_digest"` } diff --git a/core/rollout/request.go b/core/rollout/request.go index 404d8f4..b13fec8 100644 --- a/core/rollout/request.go +++ b/core/rollout/request.go @@ -25,7 +25,7 @@ type Request struct { type RequestBundle struct { Version string `json:"version"` ReleaseSequence int `json:"release_sequence"` - Channel string `json:"channel"` + Channel string `json:"channel,omitempty"` ArtifactDigest string `json:"artifact_digest"` ManifestDigest string `json:"manifest_digest"` } diff --git a/docs/adr/0016-detached-release-envelope.md b/docs/adr/0016-detached-release-envelope.md index 8dc05a1..2632299 100644 --- a/docs/adr/0016-detached-release-envelope.md +++ b/docs/adr/0016-detached-release-envelope.md @@ -2,6 +2,8 @@ Status: Accepted +Superseded in part by: ADR 0038 (the envelope's `channel` binding only) + Date: 2026-07-11 ## Context diff --git a/docs/adr/0038-release-identity-carries-no-channel.md b/docs/adr/0038-release-identity-carries-no-channel.md new file mode 100644 index 0000000..5266637 --- /dev/null +++ b/docs/adr/0038-release-identity-carries-no-channel.md @@ -0,0 +1,88 @@ +# ADR 0038: Release identity carries no channel + +Status: Accepted + +Date: 2026-09-19 + +Supersedes: ADR 0016 (the envelope's `channel` binding only) + +## Context + +The release pipeline carried a `channel` field — `canary`, `stable`, `frozen` +for published artifacts and `development` for projections — and used it as +release identity and as a gate: stable and frozen required signed +active-seven harness evidence at build time, canary tolerated it as +provisional, and the consumer trust policy rejected envelopes whose channel +was not in `allowed_channels`. + +That model conflated two different facts. A release's identity is established +by its exact source tag, version, monotonic sequence, artifact and manifest +digests, SBOM and attestation. Harness evidence is a fleet and runtime +signal: it proves the seven harness repositories were green on a specific +device within a bounded window, which is a property of where the release is +adopted, not of what was published. Gating publication on it pushed a private +signing key into the automatic release path and made the artifact's identity +depend on evidence that expires. + +Channels also failed as a promotion mechanism: nothing promoted an artifact +between channels, so `stable` was only ever a label asserted at build time, +and every published artifact was in practice `canary`. + +## Decision + +Release documents carry no channel. Concretely: + +1. `release_sequence` alone classifies a bundle: `0` is a development + projection, `>= 1` is a release. No `development` channel exists. +2. Every release builds from the exact `refs/tags/gds-v` tag. The + release workflow resolves the next version and sequence from the latest + published envelope, creates that tag, and only then builds from it. +3. `channel` fields remain as optional decode-compatible members in schemas + and types that must read documents produced while the field existed, and + in the signed harness-evidence manifest payload where removing it would + change re-digests of already-signed manifests. They are never written by + current producers and never gate anything. +4. `allowed_channels` is removed from the required trust-policy surface; the + consumer check applies only to legacy envelopes that still carry a + channel. +5. Harness evidence remains a separately produced, separately verified + estate signal (module release evidence, runtime validation). It is not an + input to `bundle.Build` or the release builder. +6. Rollout rings keep their cohort names (`canary`, `representative`, + `early`, `general`): a deployment wave label is not a release channel and + ADR 0013 is unaffected. + +## Consequences + +- The automatic release path needs no device signing key; release integrity + is tag + sequence + reproducible artifact + attestation. +- Legacy v1 artifacts decode and verify exactly as before; their channel is + still checked against a consumer policy that lists it. +- Version bumps never reclassify a development lock, because classification + is the sequence, not a version string. + +## Alternatives considered + +- Keep channels but stop gating on evidence: rejected because a label that + carries no mechanism is worse than no label — it implied promotion semantics + that did not exist. +- Promote artifacts between channels by rewriting envelopes: rejected because + it mutates published identity, which the immutability contract forbids. + +## Verification + +- `bundle.Build` and `gds-release-builder` reject every source ref other than + the exact version tag and every release sequence below 1. +- The workflow contract test forbids channel inputs and harness-evidence + inputs on the release workflow and forbids candidate checkout in the + privileged resolve job. +- Schema fixtures: current valid documents omit channel; a trust policy that + still lists `allowed_channels` validates; an envelope with a channel value + outside the legacy enum is still rejected. + +## Rollback + +Reintroducing a channel would require a new ADR, schema version negotiation +for the restored required field, and a signing-key story for the evidence +gate. The decode-compatible fields can be dropped in a future schema version +once no consumer needs to read a pre-0038 document. diff --git a/docs/adr/README.md b/docs/adr/README.md index f0dc294..acfbe14 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,6 +8,7 @@ clause stops being normative. | ADR | Title | Status | Supersedes | Superseded by | |---|---|---|---|---| +| [0038](0038-release-identity-carries-no-channel.md) | Release identity carries no channel | Accepted | ADR 0016 | — | | [0037](0037-seven-harnesses-one-per-setup-system.md) | Seven harnesses, one per setup system | Accepted | ADR 0011 | — | | [0036](0036-harness-identity-follows-the-consumer.md) | Harness identity follows the consumer contract | Accepted | — | — | | [0035](0035-agent-first-explicit-control-plane.md) | Agent-first explicit control plane and evidence-bound mutation | Accepted | — | — | @@ -27,7 +28,7 @@ clause stops being normative. | [0020](0020-single-controller-runtime-and-retention.md) | Single-controller runtime, loopback ingress, and retention | Accepted | — | — | | [0019](0019-portable-secret-references-and-device-runtime.md) | Portable secret references and device-local GitHub runtime | Accepted | — | — | | [0018](0018-device-workspaces-and-metadata-repository-retirement.md) | Use device workspaces instead of metadata repositories | Accepted | — | ADR 0027, ADR 0032 (in part) | -| [0016](0016-detached-release-envelope.md) | Bind release artifacts with a detached envelope | Accepted | — | — | +| [0016](0016-detached-release-envelope.md) | Bind release artifacts with a detached envelope | Accepted | — | ADR 0038 (in part) | | [0015](0015-projection-digest-layers.md) | Separate projection body, file, and aggregate digests | Accepted | — | — | | [0014](0014-go-production-core.md) | Implement the production GDS core in Go | Accepted | — | — | | [0013](0013-canary-and-wave-rollouts.md) | Roll out immutable changes through canaries and waves | Accepted | — | — | diff --git a/docs/contracts/bundle-release-v1.md b/docs/contracts/bundle-release-v1.md index 0cfee9b..fba27f7 100644 --- a/docs/contracts/bundle-release-v1.md +++ b/docs/contracts/bundle-release-v1.md @@ -27,9 +27,8 @@ markers, and unexpected executable content fail closed. `gds-release-builder` requires a fully tracked clean Git worktree, exact source ref resolving to `HEAD`, Go `1.27.1`, read-only modules, CGO disabled, portable CPU baselines, and an isolated build environment without ambient credentials or -Git configuration. Stable and frozen channels require -`refs/tags/gds-v`; canary accepts only `refs/heads/main` or that exact -tag. +Git configuration. Every release requires the exact `refs/tags/gds-v` +source ref; the release workflow creates that tag before the build runs. The Darwin binaries require macOS 13 or later, matching Go 1.27's supported platform baseline. Linux and Darwin each retain amd64 and arm64 targets. @@ -71,8 +70,8 @@ bundle-trust.yaml SHA256SUMS ``` -Identical source, version, sequence, channel, toolchain, and ref produce -byte-identical output. Tracked outputs contain no wall-clock timestamp. +Identical source, version, sequence, toolchain, and ref produce byte-identical +output. Tracked outputs contain no wall-clock timestamp. Publication of that directory is atomic and identity-checked. Staged files are renamed onto the destination, and any failure after that commit point rolls the @@ -100,7 +99,7 @@ from full portable-bundle completeness. ## Detached identity ADR 0016 defines the non-self-referential layers. The detached envelope binds -artifact digest, manifest digest, version, monotonic sequence, channel, source +artifact digest, manifest digest, version, monotonic sequence, source commit, exact source ref, executable count, and expected attestation identity. The six-file directory verifier rejects any missing, extra, symlinked, renamed, oversized, or digest-mismatched member. @@ -111,7 +110,7 @@ The independent local `bundle-trust.yaml` binds: - source owner and repository; - one exact workflow and allowed refs; -- channels and minimum release sequence; +- minimum release sequence; - mandatory provenance and executable SBOM attestations; - mandatory offline evidence; - the exact SHA-256 digest of `trusted-root.jsonl`; @@ -268,7 +267,7 @@ result JSON belongs to neither verifier input directory. A tag-triggered build, attestation, or publication failure is retained as a GitHub prerelease with `release-failure-envelope.json`. The schema binds the -attempted version, monotonic sequence, channel, source commit/ref, workflow run, +attempted version, monotonic sequence, source commit/ref, workflow run, failed job names, and an initially null `superseded_by`. A later accepted release may name the failed tag as superseded; failed tags are never rewritten or silently deleted. diff --git a/docs/contracts/projections-v1.md b/docs/contracts/projections-v1.md index 866693f..182d47d 100644 --- a/docs/contracts/projections-v1.md +++ b/docs/contracts/projections-v1.md @@ -67,9 +67,9 @@ pull-request merge result. A positive fixed depth is not an accepted substitute: the number of source-equivalent commits between the merge and the lock author is unbounded. -Development locks use channel `development` and sequence `0`. Canary, stable, -or frozen locks require a positive sequence and attestation identity digest. -The development lock is test evidence, not a released immutable bundle. +Development locks use sequence `0`. A released lock requires a positive +sequence and attestation identity digest. The development lock is test +evidence, not a released immutable bundle. Standalone public modules consume released policy without copying its source tree into every repository: @@ -85,7 +85,7 @@ Both inputs are required together. GDS verifies the complete archive against the detached envelope and embedded schemas, materializes only policy, schema, template and public exception inputs in an owned temporary directory, and requires the executing binary's embedded templates to match the release. The -resulting lock records release version, sequence, channel, artifact digest, +resulting lock records release version, sequence, artifact digest, content-set digest and attestation identity. Plan/apply/verify require the same two immutable input paths so precondition re-observation cannot change source. Private targets cannot use this standalone boundary. diff --git a/docs/runbooks/release-lifecycle.md b/docs/runbooks/release-lifecycle.md index 400482f..9449af2 100644 --- a/docs/runbooks/release-lifecycle.md +++ b/docs/runbooks/release-lifecycle.md @@ -24,23 +24,21 @@ out-of-band source. ## Hosted workflow preconditions -Do not dispatch `.github/workflows/release-bundle.yml` until both conditions are -proven: +`.github/workflows/release-bundle.yml` runs automatically on every push to +`main`. Before relying on it, prove: - `scripts/validate_release.sh` passes; -- `stable` and `frozen` receive an isolated signed evidence archive for exactly - `antigravity-cli`, `claude-code`, `codex`, `cursor-cli`, `grok-build`, - `opencode`, and `pi`, plus its public - trust policy; -- repository variable `HARNESS_EVIDENCE_TRUST_POLICY_DIGEST` pins the exact - `sha256:` digest of that independently distributed public trust policy; - the repository visibility and active GitHub plan support artifact attestations. -The dispatch must provide an explicit `release_sequence` greater than every -sequence already accepted in the consumer ledger. Repository transfer or -republication never resets that ledger, and `github.run_number` is local to one -workflow lineage, so it is not a release sequence. +The privileged resolve job reads the latest published `release-envelope.json` +through the GitHub API — never by checking out candidate source — derives the +next patch version and the next monotonic release sequence, refuses +non-monotonic or already-tagged identities, and creates the exact +`refs/tags/gds-v` ref. The unprivileged build job then checks out +that tag, so the built identity is the tagged identity. The sequence ledger is +never reset by a repository transfer or republication, and `github.run_number` +is local to one workflow lineage, so it is not a release sequence. The whole release chain runs on GitHub-hosted runners. The unprivileged build job executes the exact source and release gates with `contents: read`; a @@ -55,26 +53,25 @@ attestations for public repositories; private/internal attestations require GitHub Enterprise Cloud. A permission such as `id-token: write` does not establish plan eligibility. See [GitHub artifact attestation availability](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations). -Canary may omit active-seven evidence only as explicitly provisional and cannot -auto-promote. Stable/frozen require an exact version tag and verify the aggregate -signature, every isolated record, anchored module/root identity, GDS profile and -bridge digests, freshness (maximum 72 hours), and the complete active set. This -gate is enforced independently of individual `runtime_tests.required` profile -settings. Only signed records and public trust material enter workflow inputs; -private signing keys never do. - -The evidence producer is independently managed. Its deterministic flat archive -is encoded in `harness_evidence_bundle_base64`; the independently distributed -public policy is encoded in `harness_evidence_trust_policy_base64`. The policy -identity needs both `harness-evidence` and `harness-evidence-aggregate` roles. -Before dispatch, decode both into a temporary directory and exercise the local -`gds-release-builder` against the exact target source/tag and evidence. Producer -self-consistency is not a substitute for compatibility with the GDS verifier. +Every release builds from its exact `refs/tags/gds-v` tag. Harness +evidence — the signed active-seven record set with aggregate signature, +anchored module/root identity, profile and bridge digests, and bounded +freshness — is a separately produced estate and runtime signal. It gates +adoption decisions, not publication: it never enters release workflow inputs, +and private signing keys never leave their approved stores. + +The evidence producer is independently managed and its output is consumed by +estate adoption flows, not by the release workflow. Its deterministic flat +archive and its independently distributed public trust policy are produced and +signed outside CI; the policy identity needs both `harness-evidence` and +`harness-evidence-aggregate` roles. Producer self-consistency is not a +substitute for compatibility with the GDS verifier — exercise the consumer +verification path against the produced archive before relying on it. Published releases and past workflow runs are historical evidence. They do not -prove that a new source commit, sequence, channel or active-seven record set is -eligible, and do not authorize a later publication. Bind publication approval to -the concrete release identity through checkpoint `A5`. +prove that a new source commit, sequence or active-seven record set is +eligible, and do not authorize a later publication. Bind publication approval +to the concrete release identity through checkpoint `A5`. ## Read-only verification diff --git a/docs/runbooks/release-promotion-policy.md b/docs/runbooks/release-promotion-policy.md index 8c9227a..37dd62e 100644 --- a/docs/runbooks/release-promotion-policy.md +++ b/docs/runbooks/release-promotion-policy.md @@ -17,38 +17,39 @@ or installed runtime. The public GDS engine selects `release.mode: bundle`. A release binds: -- a clean exact source commit and permitted source ref; -- SemVer, channel and a monotonic release sequence; +- a clean exact source commit and the exact `refs/tags/gds-v` ref; +- SemVer and a monotonic release sequence; - the supported Go toolchain and pinned dependency inputs; - byte-identical builds and the exact six-file release directory; - artifact digests, SPDX SBOM and Sigstore provenance; -- independently distributed consumer trust and offline verification material; -- signed active-seven harness evidence when the channel requires it. +- independently distributed consumer trust and offline verification material. The sequence must exceed the applicable consumer acceptance floor. A repository transfer, workflow run number, version label or fresh tag does not reset that floor. Conflicting published identities must not be overwritten. -## Channel requirements +## Release requirements -| Channel | Source ref | Harness evidence | Meaning | -| --- | --- | --- | --- | -| canary | `refs/heads/main` or exact `refs/tags/gds-v` | May be absent only as provisional | Candidate for bounded evaluation; no automatic promotion | -| stable | Exact `refs/tags/gds-v` | Signed complete active-seven set | Nonprovisional release; consumer acceptance is still separate | -| frozen | Exact `refs/tags/gds-v` | Signed complete active-seven set | Immutable identity retained under the consumer's rollback policy | +There are no release channels (ADR 0038). Every published release has the same +requirements: -The builder verifies `antigravity-cli`, `claude-code`, `codex`, `cursor-cli`, -`grok-build`, `opencode` and `pi`, including aggregate/record signatures, anchored -producer/module identities, profile and bridge digests, and at most 72-hour -freshness. Individual profile flags do not waive stable/frozen aggregate proof. -`HARNESS_EVIDENCE_TRUST_POLICY_DIGEST` binds the workflow input to its independent -public trust policy. See `core/releasebuilder/harness_evidence.go` and -`core/harnessevidence` for the executable contract. +| Requirement | Value | +| --- | --- | +| Source ref | Exact `refs/tags/gds-v`, created by the resolve job before the build | +| Sequence | Strictly greater than the latest published envelope | +| Identity | Version, sequence, artifact and manifest digests, source commit and ref, attestation identity | + +Harness evidence is a separate estate and runtime signal, not a release input. +The signed active-seven record set — `antigravity-cli`, `claude-code`, `codex`, +`cursor-cli`, `grok-build`, `opencode` and `pi` with aggregate and record +signatures, anchored producer/module identities, profile and bridge digests, +and at most 72-hour freshness — is produced and verified by the adoption path +(see `core/harnessevidence` and `core/app/module_release_evidence.go`), never +by `gds-release-builder`. Publishing an immutable artifact does not install it. A consumer's rollout -policy decides eligible channels/rings, canary and rollback evidence, and later -promotion. It must not treat a provisional canary as a verified stable release -or mutate a published artifact to change its channel. +policy decides rings, canary cohorts and rollback evidence, and later +promotion. A published artifact is final: nothing mutates it after release. ## Publication and installation @@ -58,9 +59,8 @@ Use the existing owner authorization and signed operation contract; a previous release or this runbook does not independently authorize a new provider write. Before publication, require `scripts/validate_release.sh`, the builder's -independent rebuild and directory verification, and the target channel's signed -evidence. Build, attest and publish run on GitHub-hosted runners with separate -permissions. The repository is public; do not infer private attestation support +independent rebuild and directory verification. Build, attest and publish run +on GitHub-hosted runners with separate permissions. The repository is public; do not infer private attestation support from OIDC permissions alone. [GitHub documents the availability boundary](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations). Before installation, `gds release verify` must succeed against independent local diff --git a/docs/runbooks/seed-clean-device.md b/docs/runbooks/seed-clean-device.md index 2b12486..c9b5ed9 100644 --- a/docs/runbooks/seed-clean-device.md +++ b/docs/runbooks/seed-clean-device.md @@ -12,7 +12,7 @@ intent or user credentials. The OS bootstrap does not install GDS. ## Select immutable inputs The owner supplies the estate repository and exact commit, device descriptor, -release version/sequence/channel, independent consumer trust policy, canonical +release version/sequence, independent consumer trust policy, canonical installation/state paths and a trusted seed verifier. Use the device's declared profile: Ubuntu GUI build work uses desktop-builds; headless server and macOS desktop profiles have different Docker and execution contracts. diff --git a/estate/estate.yaml b/estate/estate.yaml index 0bf0350..dccfea2 100644 --- a/estate/estate.yaml +++ b/estate/estate.yaml @@ -10,7 +10,6 @@ schema_version: 1 estate: id: "estate_01JEXAMPZ00000000000000000" name: "example-estate" - default_bundle_channel: "stable" installations: - "installation:github-personal" diff --git a/schemas/v1/bundle-lock.schema.json b/schemas/v1/bundle-lock.schema.json index f3c4556..c026ce1 100644 --- a/schemas/v1/bundle-lock.schema.json +++ b/schemas/v1/bundle-lock.schema.json @@ -19,7 +19,6 @@ "required": [ "version", "release_sequence", - "channel", "digest" ], "properties": { @@ -55,27 +54,18 @@ { "if": { "properties": { - "channel": { - "enum": [ - "canary", - "stable", - "frozen" - ] + "release_sequence": { + "minimum": 1 } }, "required": [ - "channel" + "release_sequence" ] }, "then": { "required": [ "attestation_identity_digest" - ], - "properties": { - "release_sequence": { - "minimum": 1 - } - } + ] } } ] diff --git a/schemas/v1/bundle-manifest.schema.json b/schemas/v1/bundle-manifest.schema.json index 998bcce..43ce0c2 100644 --- a/schemas/v1/bundle-manifest.schema.json +++ b/schemas/v1/bundle-manifest.schema.json @@ -8,7 +8,6 @@ "schema_version", "bundle_version", "release_sequence", - "channel", "source_commit", "source_ref", "minimum_cli_version", @@ -16,7 +15,6 @@ "policy_digest", "skill_set_digest", "harness_profiles_digest", - "harness_evidence_provisional", "files", "supply_chain" ], @@ -32,7 +30,8 @@ "minimum": 1 }, "channel": { - "enum": ["canary", "stable", "frozen"] + "enum": ["canary", "stable", "frozen"], + "deprecated": true }, "source_commit": { "$ref": "common.schema.json#/$defs/sourceCommit" @@ -98,16 +97,5 @@ } } } - }, - "allOf": [{ - "if": {"properties": {"channel": {"enum": ["stable", "frozen"]}}}, - "then": { - "required": ["harness_evidence_manifest_digest"], - "properties": {"harness_evidence_provisional": {"const": false}} - }, - "else": { - "if": {"properties": {"harness_evidence_provisional": {"const": false}}}, - "then": {"required": ["harness_evidence_manifest_digest"]} - } - }] + } } diff --git a/schemas/v1/bundle-trust.schema.json b/schemas/v1/bundle-trust.schema.json index a24f1cd..3d15ab9 100644 --- a/schemas/v1/bundle-trust.schema.json +++ b/schemas/v1/bundle-trust.schema.json @@ -45,7 +45,7 @@ "release": { "type": "object", "additionalProperties": false, - "required": ["minimum_release_sequence", "allowed_channels"], + "required": ["minimum_release_sequence"], "properties": { "minimum_release_sequence": { "type": "integer", diff --git a/schemas/v1/estate.schema.json b/schemas/v1/estate.schema.json index b6c0a36..8928911 100644 --- a/schemas/v1/estate.schema.json +++ b/schemas/v1/estate.schema.json @@ -21,16 +21,13 @@ "estate": { "type": "object", "additionalProperties": false, - "required": ["id", "name", "default_bundle_channel"], + "required": ["id", "name"], "properties": { "id": { "$ref": "common.schema.json#/$defs/estateId" }, "name": { "$ref": "common.schema.json#/$defs/kebabId" - }, - "default_bundle_channel": { - "enum": ["canary", "stable", "frozen"] } } }, diff --git a/schemas/v1/harness-runtime-manifest.schema.json b/schemas/v1/harness-runtime-manifest.schema.json index 72b32f9..5eaddb8 100644 --- a/schemas/v1/harness-runtime-manifest.schema.json +++ b/schemas/v1/harness-runtime-manifest.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/NDDev-OpenNetwork/github-device-sync/schemas/v1/harness-runtime-manifest.schema.json","title":"Signed active harness evidence manifest v1","type":"object","additionalProperties":false,"required":["payload","manifest_digest","signature"],"properties":{"payload":{"type":"object","additionalProperties":false,"required":["schema_version","manifest_id","harness_root_sha","channel","generated_at","expires_at","actor_id","evidence"],"properties":{"schema_version":{"const":1},"manifest_id":{"type":"string","minLength":1},"harness_root_sha":{"$ref":"common.schema.json#/$defs/sourceCommit"},"channel":{"enum":["canary","stable","frozen"]},"generated_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"actor_id":{"type":"string","minLength":1},"evidence":{"type":"array","maxItems":7,"uniqueItems":true,"items":{"type":"object","additionalProperties":false,"required":["harness_id","evidence_digest"],"properties":{"harness_id":{"enum":["antigravity","claude-code","codex","cursor","grok-build","opencode","pi"]},"evidence_digest":{"$ref":"common.schema.json#/$defs/sha256Digest"}}}}}},"manifest_digest":{"$ref":"common.schema.json#/$defs/sha256Digest"},"signature":{"$ref":"device-evidence.schema.json#/$defs/signature"}}} +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/NDDev-OpenNetwork/github-device-sync/schemas/v1/harness-runtime-manifest.schema.json","title":"Signed active harness evidence manifest v1","type":"object","additionalProperties":false,"required":["payload","manifest_digest","signature"],"properties":{"payload":{"type":"object","additionalProperties":false,"required":["schema_version","manifest_id","harness_root_sha","generated_at","expires_at","actor_id","evidence"],"properties":{"schema_version":{"const":1},"manifest_id":{"type":"string","minLength":1},"harness_root_sha":{"$ref":"common.schema.json#/$defs/sourceCommit"},"channel":{"enum":["canary","stable","frozen"]},"generated_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"actor_id":{"type":"string","minLength":1},"evidence":{"type":"array","maxItems":7,"uniqueItems":true,"items":{"type":"object","additionalProperties":false,"required":["harness_id","evidence_digest"],"properties":{"harness_id":{"enum":["antigravity","claude-code","codex","cursor","grok-build","opencode","pi"]},"evidence_digest":{"$ref":"common.schema.json#/$defs/sha256Digest"}}}}}},"manifest_digest":{"$ref":"common.schema.json#/$defs/sha256Digest"},"signature":{"$ref":"device-evidence.schema.json#/$defs/signature"}}} diff --git a/schemas/v1/release-envelope.schema.json b/schemas/v1/release-envelope.schema.json index f75809e..521e953 100644 --- a/schemas/v1/release-envelope.schema.json +++ b/schemas/v1/release-envelope.schema.json @@ -8,7 +8,6 @@ "schema_version", "bundle_version", "release_sequence", - "channel", "source_commit", "source_ref", "executable_files", diff --git a/schemas/v1/release-failure-envelope.schema.json b/schemas/v1/release-failure-envelope.schema.json index 8a673ed..06a35ea 100644 --- a/schemas/v1/release-failure-envelope.schema.json +++ b/schemas/v1/release-failure-envelope.schema.json @@ -9,7 +9,6 @@ "status", "bundle_version", "release_sequence", - "channel", "source_commit", "source_ref", "workflow_run_id", diff --git a/schemas/v1/release-installation.schema.json b/schemas/v1/release-installation.schema.json index b5d2e8a..d048ca0 100644 --- a/schemas/v1/release-installation.schema.json +++ b/schemas/v1/release-installation.schema.json @@ -10,7 +10,6 @@ "release_key", "bundle_version", "release_sequence", - "channel", "artifact_digest", "manifest_digest", "attestation_identity_digest", diff --git a/schemas/v1/rollout-request.schema.json b/schemas/v1/rollout-request.schema.json index 0bf27e8..f5de267 100644 --- a/schemas/v1/rollout-request.schema.json +++ b/schemas/v1/rollout-request.schema.json @@ -19,7 +19,7 @@ "bundle": { "type": "object", "additionalProperties": false, - "required": ["version", "release_sequence", "channel", "artifact_digest", "manifest_digest"], + "required": ["version", "release_sequence", "artifact_digest", "manifest_digest"], "properties": { "version": { "$ref": "common.schema.json#/$defs/semanticVersion" diff --git a/schemas/v1/rollout.schema.json b/schemas/v1/rollout.schema.json index 51e08d3..01757fd 100644 --- a/schemas/v1/rollout.schema.json +++ b/schemas/v1/rollout.schema.json @@ -30,7 +30,7 @@ "bundle": { "type": "object", "additionalProperties": false, - "required": ["version", "release_sequence", "channel", "artifact_digest", "manifest_digest"], + "required": ["version", "release_sequence", "artifact_digest", "manifest_digest"], "properties": { "version": { "$ref": "common.schema.json#/$defs/semanticVersion" diff --git a/tests/fixtures/schemas/v1/invalid-bundle-lock-output-digest.yaml b/tests/fixtures/schemas/v1/invalid-bundle-lock-output-digest.yaml index 82e65f3..2695ab2 100644 --- a/tests/fixtures/schemas/v1/invalid-bundle-lock-output-digest.yaml +++ b/tests/fixtures/schemas/v1/invalid-bundle-lock-output-digest.yaml @@ -3,7 +3,6 @@ schema_version: 1 bundle: version: "0.1.0-dev" release_sequence: 0 - channel: "development" source_commit: "0123456789abcdef0123456789abcdef01234567" digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" diff --git a/tests/fixtures/schemas/v1/invalid-bundle-lock-release.yaml b/tests/fixtures/schemas/v1/invalid-bundle-lock-release.yaml index 6da7be3..32909fe 100644 --- a/tests/fixtures/schemas/v1/invalid-bundle-lock-release.yaml +++ b/tests/fixtures/schemas/v1/invalid-bundle-lock-release.yaml @@ -2,8 +2,7 @@ schema_version: 1 bundle: version: "1.0.0" - release_sequence: 0 - channel: "stable" + release_sequence: 1 source_commit: "0123456789abcdef0123456789abcdef01234567" digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" diff --git a/tests/fixtures/schemas/v1/invalid-bundle-manifest-oversized-file.json b/tests/fixtures/schemas/v1/invalid-bundle-manifest-oversized-file.json index 55ca6d7..e796bac 100644 --- a/tests/fixtures/schemas/v1/invalid-bundle-manifest-oversized-file.json +++ b/tests/fixtures/schemas/v1/invalid-bundle-manifest-oversized-file.json @@ -2,7 +2,6 @@ "schema_version": 1, "bundle_version": "1.0.0", "release_sequence": 1, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v1.0.0", "minimum_cli_version": "1.0.0", diff --git a/tests/fixtures/schemas/v1/invalid-estate-policy-order.yaml b/tests/fixtures/schemas/v1/invalid-estate-policy-order.yaml index 963de55..9248be8 100644 --- a/tests/fixtures/schemas/v1/invalid-estate-policy-order.yaml +++ b/tests/fixtures/schemas/v1/invalid-estate-policy-order.yaml @@ -2,7 +2,6 @@ schema_version: 1 estate: id: "estate_01KX7BV07RHD6KRA4Z4J0KCHGS" name: "example-estate" - default_bundle_channel: "stable" installations: ["installation:github-personal"] mutation_capabilities: ["mutation:github-personal"] policy_order: diff --git a/tests/fixtures/schemas/v1/invalid-release-envelope-semver.json b/tests/fixtures/schemas/v1/invalid-release-envelope-semver.json index 6690506..326fbb4 100644 --- a/tests/fixtures/schemas/v1/invalid-release-envelope-semver.json +++ b/tests/fixtures/schemas/v1/invalid-release-envelope-semver.json @@ -2,7 +2,6 @@ "schema_version": 1, "bundle_version": "1.0.0-01", "release_sequence": 1, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v1.0.0-01", "executable_files": 0, diff --git a/tests/fixtures/schemas/v1/invalid-release-failure-envelope-empty-jobs.json b/tests/fixtures/schemas/v1/invalid-release-failure-envelope-empty-jobs.json index 1567a7c..17bcef9 100644 --- a/tests/fixtures/schemas/v1/invalid-release-failure-envelope-empty-jobs.json +++ b/tests/fixtures/schemas/v1/invalid-release-failure-envelope-empty-jobs.json @@ -3,7 +3,6 @@ "status": "failed", "bundle_version": "0.7.0-rc.1", "release_sequence": 47, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v0.7.0-rc.1", "workflow_run_id": 123456789, diff --git a/tests/fixtures/schemas/v1/invalid-rollout-request-ring.yaml b/tests/fixtures/schemas/v1/invalid-rollout-request-ring.yaml index 1393f8f..3d1e1fa 100644 --- a/tests/fixtures/schemas/v1/invalid-rollout-request-ring.yaml +++ b/tests/fixtures/schemas/v1/invalid-rollout-request-ring.yaml @@ -4,7 +4,6 @@ created_at: "2026-07-11T00:00:00Z" bundle: version: "1.0.0" release_sequence: 1 - channel: "canary" artifact_digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" manifest_digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" repository_ids: diff --git a/tests/fixtures/schemas/v1/invalid-rollout-target-digest.json b/tests/fixtures/schemas/v1/invalid-rollout-target-digest.json index 18d3760..4439d4a 100644 --- a/tests/fixtures/schemas/v1/invalid-rollout-target-digest.json +++ b/tests/fixtures/schemas/v1/invalid-rollout-target-digest.json @@ -5,7 +5,6 @@ "bundle": { "version": "1.0.0", "release_sequence": 1, - "channel": "canary", "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "manifest_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, @@ -15,7 +14,9 @@ { "id": "canary", "ordinal": 0, - "repository_ids": ["repo_01J00000000000000000000001"], + "repository_ids": [ + "repo_01J00000000000000000000001" + ], "status": "pending" } ], diff --git a/tests/fixtures/schemas/v1/valid-bundle-lock.yaml b/tests/fixtures/schemas/v1/valid-bundle-lock.yaml index bca9c3a..3a49b2f 100644 --- a/tests/fixtures/schemas/v1/valid-bundle-lock.yaml +++ b/tests/fixtures/schemas/v1/valid-bundle-lock.yaml @@ -3,7 +3,6 @@ schema_version: 1 bundle: version: "0.1.0-dev" release_sequence: 0 - channel: "development" source_commit: "0123456789abcdef0123456789abcdef01234567" digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" diff --git a/tests/fixtures/schemas/v1/valid-bundle-manifest.json b/tests/fixtures/schemas/v1/valid-bundle-manifest.json index f157b33..8397c39 100644 --- a/tests/fixtures/schemas/v1/valid-bundle-manifest.json +++ b/tests/fixtures/schemas/v1/valid-bundle-manifest.json @@ -2,7 +2,6 @@ "schema_version": 1, "bundle_version": "1.0.0", "release_sequence": 1, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v1.0.0", "minimum_cli_version": "1.0.0", diff --git a/tests/fixtures/schemas/v1/valid-estate.yaml b/tests/fixtures/schemas/v1/valid-estate.yaml index 6de3cf1..e061a7c 100644 --- a/tests/fixtures/schemas/v1/valid-estate.yaml +++ b/tests/fixtures/schemas/v1/valid-estate.yaml @@ -3,7 +3,6 @@ schema_version: 1 estate: id: "estate_01KX7BV07RHD6KRA4Z4J0KCHGS" name: "example-estate" - default_bundle_channel: "stable" installations: - "installation:github-personal" diff --git a/tests/fixtures/schemas/v1/valid-harness-runtime-manifest.json b/tests/fixtures/schemas/v1/valid-harness-runtime-manifest.json index fd84708..9ebbca4 100644 --- a/tests/fixtures/schemas/v1/valid-harness-runtime-manifest.json +++ b/tests/fixtures/schemas/v1/valid-harness-runtime-manifest.json @@ -1 +1 @@ -{"payload":{"schema_version":1,"manifest_id":"manifest-1","harness_root_sha":"0123456789abcdef0123456789abcdef01234567","channel":"stable","generated_at":"2026-08-09T18:00:00Z","expires_at":"2026-08-10T18:00:00Z","actor_id":"automation:harness-evidence","evidence":[{"harness_id":"codex","evidence_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111"}]},"manifest_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","signature":{"algorithm":"ed25519","key_id":"harness-2026","value":"AA"}} +{"payload":{"schema_version":1,"manifest_id":"manifest-1","harness_root_sha":"0123456789abcdef0123456789abcdef01234567","generated_at":"2026-08-09T18:00:00Z","expires_at":"2026-08-10T18:00:00Z","actor_id":"automation:harness-evidence","evidence":[{"harness_id":"codex","evidence_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111"}]},"manifest_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","signature":{"algorithm":"ed25519","key_id":"harness-2026","value":"AA"}} diff --git a/tests/fixtures/schemas/v1/valid-release-envelope.json b/tests/fixtures/schemas/v1/valid-release-envelope.json index 76bdad3..1cdb661 100644 --- a/tests/fixtures/schemas/v1/valid-release-envelope.json +++ b/tests/fixtures/schemas/v1/valid-release-envelope.json @@ -2,7 +2,6 @@ "schema_version": 1, "bundle_version": "1.0.0", "release_sequence": 1, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v1.0.0", "executable_files": 0, diff --git a/tests/fixtures/schemas/v1/valid-release-failure-envelope.json b/tests/fixtures/schemas/v1/valid-release-failure-envelope.json index d5e3043..01dd6bb 100644 --- a/tests/fixtures/schemas/v1/valid-release-failure-envelope.json +++ b/tests/fixtures/schemas/v1/valid-release-failure-envelope.json @@ -3,10 +3,11 @@ "status": "failed", "bundle_version": "0.7.0-rc.1", "release_sequence": 47, - "channel": "canary", "source_commit": "0123456789abcdef0123456789abcdef01234567", "source_ref": "refs/tags/gds-v0.7.0-rc.1", "workflow_run_id": 123456789, - "failed_jobs": ["build"], + "failed_jobs": [ + "build" + ], "superseded_by": "gds-v0.7.0-rc.2" } diff --git a/tests/fixtures/schemas/v1/valid-release-installation.json b/tests/fixtures/schemas/v1/valid-release-installation.json index bf64037..aac3792 100644 --- a/tests/fixtures/schemas/v1/valid-release-installation.json +++ b/tests/fixtures/schemas/v1/valid-release-installation.json @@ -4,7 +4,6 @@ "release_key": "0000000007--1.2.3--aaaaaaaaaaaaaaaa", "bundle_version": "1.2.3", "release_sequence": 7, - "channel": "stable", "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "manifest_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "attestation_identity_digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", diff --git a/tests/fixtures/schemas/v1/valid-rollout-request.yaml b/tests/fixtures/schemas/v1/valid-rollout-request.yaml index 098276b..ea6538b 100644 --- a/tests/fixtures/schemas/v1/valid-rollout-request.yaml +++ b/tests/fixtures/schemas/v1/valid-rollout-request.yaml @@ -4,7 +4,6 @@ created_at: "2026-07-11T00:00:00Z" bundle: version: "1.0.0" release_sequence: 1 - channel: "canary" artifact_digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" manifest_digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" repository_ids: diff --git a/tests/fixtures/schemas/v1/valid-rollout.json b/tests/fixtures/schemas/v1/valid-rollout.json index 956c780..7d4638a 100644 --- a/tests/fixtures/schemas/v1/valid-rollout.json +++ b/tests/fixtures/schemas/v1/valid-rollout.json @@ -1,32 +1,33 @@ { - "schema_version": 1, - "rollout_id": "rollout_01J00000000000000000000000", - "created_at": "2026-07-11T00:00:00Z", "bundle": { - "version": "1.0.0", - "release_sequence": 1, - "channel": "canary", "artifact_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "manifest_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "manifest_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "release_sequence": 1, + "version": "1.0.0" }, - "target_set_digest": "sha256:4bb6ebf0c8849935f2aa9bebcce2126d5fa94686dedbf1325172661c1586083e", + "created_at": "2026-07-11T00:00:00Z", + "gates": { + "max_failure_rate": 0.02, + "required_check_failure_tolerance": 0, + "security_failure_tolerance": 0 + }, + "mutation": { + "auto_merge": false, + "mode": "pull-request" + }, + "plan_digest": "sha256:ac4814bf4f176f21380c6e18563cb4510d63e387d6a93799897710c182effb52", + "rollout_id": "rollout_01J00000000000000000000000", + "schema_version": 1, "target_count": 1, + "target_set_digest": "sha256:4bb6ebf0c8849935f2aa9bebcce2126d5fa94686dedbf1325172661c1586083e", "waves": [ { "id": "canary", "ordinal": 0, - "repository_ids": ["repo_01J00000000000000000000001"], + "repository_ids": [ + "repo_01J00000000000000000000001" + ], "status": "pending" } - ], - "gates": { - "max_failure_rate": 0.02, - "security_failure_tolerance": 0, - "required_check_failure_tolerance": 0 - }, - "mutation": { - "mode": "pull-request", - "auto_merge": false - }, - "plan_digest": "sha256:bbdd3855b966acc7be39af14607b93637837d41fbf49843dc3f633b7df8c433d" + ] } diff --git a/tests/golden/projections/control-plane/.claude/CLAUDE.md b/tests/golden/projections/control-plane/.claude/CLAUDE.md index 2a08bc1..7d3b258 100644 --- a/tests/golden/projections/control-plane/.claude/CLAUDE.md +++ b/tests/golden/projections/control-plane/.claude/CLAUDE.md @@ -3,7 +3,7 @@ GENERATED FILE - DO NOT EDIT DIRECTLY generator: gds bundle: 0.9.7-dev source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -input-digest: sha256:bd9d930d1470b169905eeae756a0e747c53f4825ca9e501c1db62b5835755df3 +input-digest: sha256:4eefca123b3b75f390ee5794f5ba1ec518777a267aeb5bce7bc0bf4fd9fe21f8 output-digest: sha256:88cb57297d8d713287872a8afaca8d42f7146ecf7a091e4996e65eee8f962665 edit-source: - .gds/repository.yaml diff --git a/tests/golden/projections/control-plane/.gds/bundle.lock.yaml b/tests/golden/projections/control-plane/.gds/bundle.lock.yaml index 27a347c..3ffa6db 100644 --- a/tests/golden/projections/control-plane/.gds/bundle.lock.yaml +++ b/tests/golden/projections/control-plane/.gds/bundle.lock.yaml @@ -4,19 +4,18 @@ schema_version: 1 bundle: version: "0.9.7-dev" release_sequence: 0 - channel: "development" source_tree_digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001" digest: "sha256:fe686e5956e8cd0e9904ebdbf70a5abd14998e3ae04993781fcb93e48a09e6ab" projection: - input_digest: "sha256:bd9d930d1470b169905eeae756a0e747c53f4825ca9e501c1db62b5835755df3" - output_digest: "sha256:f6cb1598086a808d2bdcce071c57a32eb6f8f98d54a097bf0fa056fea42c6a4b" + input_digest: "sha256:4eefca123b3b75f390ee5794f5ba1ec518777a267aeb5bce7bc0bf4fd9fe21f8" + output_digest: "sha256:cd1bc0444d6f250dda92bf1d498c354c0319a3713c40f74349bb493f7a35c54f" files: - path: ".claude/CLAUDE.md" - digest: "sha256:befd575a59d93d8fba1fccd4429c1721acb754590aca15d257441e03298a3179" + digest: "sha256:44191b6f7a597c0257fda7cc4b813ecc21810d4d8b03d0ecdee985b4cbffe729" - path: ".gds/compiled-policy.json" digest: "sha256:f86f7e2eb77664de1960f9dd25835b4e7341ce02378803df34372c50c94c86fb" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:0ba2611284363a39901b9253bfac0b68a9c483df09c618731b7864d69ca9e9b6" + digest: "sha256:a751158efad9987a1524e7f85436d39af07a1fd94f8308013b46ff4ab053e657" - path: "AGENTS.md" - digest: "sha256:731b5fa7835e88a873dd7090b488139c4746d489f9536417be5b36276e2087cf" + digest: "sha256:9553b54acaa8f11027433ef79c339c1c14e90877a1a402bf8be7ed65dbdcedac" diff --git a/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml b/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml index 30f3b19..05f9726 100644 --- a/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml +++ b/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml @@ -2,7 +2,7 @@ # generator: gds # bundle: 0.9.7-dev # source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -# input-digest: sha256:bd9d930d1470b169905eeae756a0e747c53f4825ca9e501c1db62b5835755df3 +# input-digest: sha256:4eefca123b3b75f390ee5794f5ba1ec518777a267aeb5bce7bc0bf4fd9fe21f8 # output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38 # edit-source: # - .gds/repository.yaml diff --git a/tests/golden/projections/control-plane/AGENTS.md b/tests/golden/projections/control-plane/AGENTS.md index 0f41953..c231431 100644 --- a/tests/golden/projections/control-plane/AGENTS.md +++ b/tests/golden/projections/control-plane/AGENTS.md @@ -3,7 +3,7 @@ GENERATED FILE - DO NOT EDIT DIRECTLY generator: gds bundle: 0.9.7-dev source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -input-digest: sha256:bd9d930d1470b169905eeae756a0e747c53f4825ca9e501c1db62b5835755df3 +input-digest: sha256:4eefca123b3b75f390ee5794f5ba1ec518777a267aeb5bce7bc0bf4fd9fe21f8 output-digest: sha256:c9674389b139e2ea844844d3e0bb9227a1528f4a5af9347e5dd43c563f8e1bd4 edit-source: - .gds/repository.yaml From 41fff6c161af63dd2f483e50df5630f6a5f71bdf Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sat, 19 Sep 2026 23:52:47 +0500 Subject: [PATCH 2/4] chore: restamp development lock after channel removal --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index f9cd6ea..0b0c5d2 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -4,14 +4,14 @@ schema_version: 1 bundle: version: "0.9.7-dev" release_sequence: 0 - source_tree_digest: "sha256:1e873c46d68f27b01800a4ee64221b7f53b3b78ad7872374b565f0a08fc67ff1" - digest: "sha256:522661293a809309328a9551766cb89d4589241afa78f507a4b6f50a53244160" + source_tree_digest: "sha256:864917988777e8b9f61f87f08e4f08d94173e37c1352373332b5e02e76562f97" + digest: "sha256:eb2048f08b6e1dbc84d89792a6e3f658f6258ff7c7e3b11c87fd5c2fbd48b0de" projection: - input_digest: "sha256:6261a99a0f9de321773be0c9ecb05d3c74adadfd58654c2dca4bc4f40a8d0ee4" - output_digest: "sha256:b1c3483631551b66835aaa8542df740b6f8cc7376f40c471ff1f05dd2088c793" + input_digest: "sha256:f2fb932fd0598eaf673a8cd2b3232f73f7c5b3cfad377fb807dda30ec134e977" + output_digest: "sha256:0caadd7bca7495045f29598f2908a15bab55a2c421b10202095ff99e3c833d69" files: - path: ".gds/compiled-policy.json" digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:d166a5011b5068da9cdb1529a6622c75043aed686780b4148cb758bd4e573c8b" + digest: "sha256:275145c60b6f99f946085685a66b7eabb4a693043588cc55a87250401af28612" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 73d6427..20051f0 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.9.7-dev -# source-tree-digest: sha256:1e873c46d68f27b01800a4ee64221b7f53b3b78ad7872374b565f0a08fc67ff1 -# input-digest: sha256:6261a99a0f9de321773be0c9ecb05d3c74adadfd58654c2dca4bc4f40a8d0ee4 +# source-tree-digest: sha256:864917988777e8b9f61f87f08e4f08d94173e37c1352373332b5e02e76562f97 +# input-digest: sha256:f2fb932fd0598eaf673a8cd2b3232f73f7c5b3cfad377fb807dda30ec134e977 # output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38 # edit-source: # - .gds/repository.yaml From edf62017dcbf83a1cf5574d311687faea70a5921 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sat, 19 Sep 2026 23:56:10 +0500 Subject: [PATCH 3/4] test(release): drop hosted evidence-input step tests The workflow step that materialized signed harness evidence into the release build was removed with the channel gate; the file tested only that step. --- tests/test_release_evidence_input.py | 100 --------------------------- 1 file changed, 100 deletions(-) delete mode 100644 tests/test_release_evidence_input.py diff --git a/tests/test_release_evidence_input.py b/tests/test_release_evidence_input.py deleted file mode 100644 index 137fe32..0000000 --- a/tests/test_release_evidence_input.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Execute the hosted input step against the native bounded archive reader.""" -from pathlib import Path -import base64 -import hashlib -import io -import json -import os -import subprocess -import tarfile - -import pytest -import yaml - -ROOT = Path(__file__).resolve().parents[1] - - -def evidence_step() -> str: - doc = yaml.safe_load((ROOT / ".github/workflows/release-bundle.yml").read_text()) - steps = doc["jobs"]["build"]["steps"] - names = [step.get("name") for step in steps] - name = "Materialize bounded signed harness evidence input" - assert names.index(name) < names.index("Run release gates") - return steps[names.index(name)]["run"] - - -@pytest.fixture(scope="module") -def native_builder(tmp_path_factory: pytest.TempPathFactory) -> Path: - binary = tmp_path_factory.mktemp("release-evidence-builder") / "gds-release-builder" - subprocess.run(["go", "build", "-trimpath", "-o", str(binary), "./core/cmd/gds-release-builder"], - cwd=ROOT, check=True, capture_output=True, text=True) - return binary - - -def run_input(tmp_path: Path, native_builder: Path, pin: str, *, channel="stable", - archive=True, policy=True, malformed_archive=False): - records = ["manifest", "antigravity", "claude-code", "codex", "cursor", "grok-build", "opencode", "pi"] - stream = io.BytesIO() - with tarfile.open(fileobj=stream, mode="w:gz", format=tarfile.USTAR_FORMAT) as tar: - for name in records: - info = tarfile.TarInfo(name + ".json") - info.size, info.mode = 3, 0o600 - tar.addfile(info, io.BytesIO(b"{}\n")) - raw_archive = b"invalid archive" if malformed_archive else stream.getvalue() - trust = b'{"schema_version":1}\n' - output = tmp_path / "outputs" - step = evidence_step() - command = "go run ./core/cmd/gds-release-builder" - assert step.count(command) == 1 - step = step.replace(command, '"$TEST_RELEASE_BUILDER"') - result = subprocess.run(["bash", "-c", step], cwd=ROOT, capture_output=True, text=True, - env={**os.environ, "TEST_RELEASE_BUILDER": str(native_builder), - "RUNNER_TEMP": str(tmp_path), "EVIDENCE_INPUT_ROOT": str(tmp_path / "input"), - "GITHUB_OUTPUT": str(output), "CHANNEL": channel, - "HARNESS_EVIDENCE_BUNDLE_BASE64": base64.b64encode(raw_archive).decode() if archive else "", - "HARNESS_EVIDENCE_TRUST_POLICY_BASE64": base64.b64encode(trust).decode() if policy else "", - "HARNESS_EVIDENCE_TRUST_POLICY_DIGEST": pin}, timeout=30) - return result, output - - -@pytest.mark.parametrize("prefix", ["", "sha256:"]) -def test_both_exact_sha256_notations_reach_native_materialization(tmp_path: Path, native_builder: Path, prefix: str): - digest = hashlib.sha256(b'{"schema_version":1}\n').hexdigest() - result, output = run_input(tmp_path, native_builder, prefix + digest) - assert result.returncode == 0, result.stderr - assert "--harness-evidence-directory" in output.read_text() - assert len(list((tmp_path / "input/records").glob("*.json"))) == 8 - - -@pytest.mark.parametrize("pin,message", [ - ("", "must be an independent SHA-256 pin"), - ("sha512:" + "a" * 64, "must be an independent SHA-256 pin"), - ("sha256:not-a-digest", "must be an independent SHA-256 pin"), - ("sha256:" + "0" * 64, "does not match the independent repository pin"), -]) -def test_missing_malformed_and_mismatched_independent_pins_fail(tmp_path: Path, native_builder: Path, pin: str, message: str): - result, output = run_input(tmp_path, native_builder, pin) - assert result.returncode != 0 and message in result.stderr - assert not output.exists() - assert not (tmp_path / "input/records").exists() - - -@pytest.mark.parametrize("archive,policy", [(True, False), (False, True), (False, False)]) -def test_stable_requires_both_signed_inputs(tmp_path: Path, native_builder: Path, archive: bool, policy: bool): - result, output = run_input(tmp_path, native_builder, "", archive=archive, policy=policy) - assert result.returncode != 0 - assert not output.exists() - - -def test_canary_can_explicitly_omit_both_inputs(tmp_path: Path, native_builder: Path): - result, output = run_input(tmp_path, native_builder, "", channel="canary", archive=False, policy=False) - assert result.returncode == 0, result.stderr - assert output.read_text() == "arguments=\n" - - -def test_native_archive_refusal_is_visible_in_the_step_log(tmp_path: Path, native_builder: Path): - digest = hashlib.sha256(b'{"schema_version":1}\n').hexdigest() - result, output = run_input(tmp_path, native_builder, digest, malformed_archive=True) - assert result.returncode != 0 - assert json.loads(result.stderr)["code"] == "GDS_HARNESS_EVIDENCE_ARCHIVE_INVALID" - assert not output.exists() From 17db8edd5e498e7c7d875cce3add117a91ac251c Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sun, 20 Sep 2026 00:03:48 +0500 Subject: [PATCH 4/4] test(ci): point ci-feedback contract at the ci-workflows home main repointed the caller to NDDev-OpenNetwork/ci-workflows in 3125062 but left the pin regex expecting the old github-actions path. --- tests/test_ci_feedback_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ci_feedback_contract.py b/tests/test_ci_feedback_contract.py index bba135e..c872c52 100644 --- a/tests/test_ci_feedback_contract.py +++ b/tests/test_ci_feedback_contract.py @@ -32,7 +32,7 @@ def test_exact_completed_attempt_is_forwarded(self): def test_reusable_reference_is_immutable(self): text = CALLER.read_text() - self.assertRegex(text, r"uses: NDDev-OpenNetwork/github-actions/\.github/workflows/ci-feedback\.yml@[0-9a-f]{40}(?: +# commit:[0-9a-f]{40})?\n") + self.assertRegex(text, r"uses: NDDev-OpenNetwork/ci-workflows/\.github/workflows/ci-feedback\.yml@[0-9a-f]{40}(?: +# commit:[0-9a-f]{40})?\n") self.assertNotIn("@main", text) def test_no_project_execution_or_secret_inheritance(self):