diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 25c06cd..e307a87 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -19,4 +19,4 @@ jobs: name: actionlint permissions: contents: read - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/actionlint.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/actionlint.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7399240..3a37908 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,7 @@ jobs: contents: read security-events: write # Publish CodeQL analysis to code scanning. actions: read # Read workflow metadata for the actions language. - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-codeql.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-codeql.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 with: languages: '["actions"]' queries: security-and-quality diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d4cc0ce..6b59fb5 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -16,4 +16,4 @@ jobs: permissions: contents: read pull-requests: write # Comment when dependency policy rejects a change. - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-dependency-review.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-dependency-review.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3457ec..6502581 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,21 +135,24 @@ jobs: run: | set -euo pipefail python3 -I <<'BUNDLE' + import datetime as dt + import hashlib import json import pathlib import re + import subprocess import sys + EVIDENCE_PATH = "build/release-evidence.json" + def fail(message): print(f"::error::{message}", file=sys.stderr) raise SystemExit(1) try: - evidence = json.loads( - pathlib.Path("build/release-evidence.json").read_text(encoding="utf-8") - ) + evidence = json.loads(pathlib.Path(EVIDENCE_PATH).read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: - fail(f"cannot read build/release-evidence.json: {exc}") + fail(f"cannot read {EVIDENCE_PATH}: {exc}") if not isinstance(evidence, dict): fail("release-evidence must be a JSON object") if evidence.get("schema_version") != 1: @@ -160,6 +163,29 @@ jobs: digest = module.get("setup_digest") if not isinstance(digest, str) or re.fullmatch(r"sha256:[0-9a-f]{64}", digest) is None: fail("release-evidence.module.setup_digest must be a sha256 content digest") + + # Bind the recorded digest to the exact tagged content: hash every + # tracked blob by path and id, excluding the evidence file itself + # (identical to scripts/evidence.compute_setup_digest in the harness). + listing = subprocess.run( + ["git", "ls-files", "-s"], capture_output=True, text=True, check=True + ).stdout + entries = [] + for line in listing.splitlines(): + metadata, _, path = line.partition("\t") + fields = metadata.split() + if not path or len(fields) != 3: + fail(f"unexpected git ls-files entry: {line!r}") + if path == EVIDENCE_PATH: + continue + entries.append(f"{path} {fields[1]}") + recomputed = "sha256:" + hashlib.sha256("\n".join(sorted(entries)).encode("utf-8")).hexdigest() + if digest != recomputed: + fail( + "release-evidence.module.setup_digest does not match the tagged content " + f"(recorded {digest}, recomputed {recomputed})" + ) + harness = evidence.get("harness") if not isinstance(harness, dict) or re.fullmatch(r"[0-9a-f]{40}", str(harness.get("commit"))) is None: fail("release-evidence.harness.commit must be a full harness commit SHA") @@ -167,9 +193,40 @@ jobs: version_file = pathlib.Path("VERSION").read_text(encoding="utf-8").strip() if not isinstance(adapter, dict) or adapter.get("version") != version_file: fail("release-evidence.adapter.version must equal VERSION") + if not isinstance(evidence.get("vendor"), dict) or not evidence["vendor"]: + fail("release-evidence.vendor must be a non-empty object") + platforms = evidence.get("platforms") + if not isinstance(platforms, list) or not platforms: + fail("release-evidence.platforms must be a non-empty array") + checks = evidence.get("checks") + if not isinstance(checks, list) or not checks or not all(isinstance(item, str) and item for item in checks): + fail("release-evidence.checks must be a non-empty array of strings") + + # Freshness: the bundle must be inside its validated window at tag time. + def parse_utc(value): + if not isinstance(value, str) or not value.endswith("Z"): + return None + try: + parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + return parsed if parsed.utcoffset() == dt.timedelta(0) else None + + generated_at = parse_utc(evidence.get("generated_at_utc")) + expires_at = parse_utc(evidence.get("expires_at_utc")) + if generated_at is None or expires_at is None: + fail("release-evidence timestamps must be ISO-8601 UTC ending in Z") + now = dt.datetime.now(dt.timezone.utc) + if generated_at >= expires_at: + fail("release-evidence.generated_at_utc must precede expires_at_utc") + if now >= expires_at: + fail("release-evidence has expired; regenerate after re-validation") + if now < generated_at: + fail("release-evidence.generated_at_utc is in the future") + if evidence.get("promotion", {}).get("decision") != "approved": fail("release-evidence.promotion.decision must be approved") - print(f"ok: release-evidence bundle present and consistent for {version_file}") + print(f"ok: release-evidence bundle recomputed and consistent for {version_file}") BUNDLE publish: @@ -179,7 +236,8 @@ jobs: contents: write # Create the immutable release and its exact assets. id-token: write # Exchange workflow identity for attestations. attestations: write # Publish provenance and SBOM attestations. - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/release-supply-chain.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + artifact-metadata: write # Record the actions/attest artifact storage entry (required at v4.1.1). + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/release-supply-chain.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 with: version: ${{ github.ref_name }} package_name: nddev-zcode-app diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3b44406..e606dbe 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,4 +19,4 @@ jobs: contents: read actions: read # Inspect workflow metadata for Scorecard checks. id-token: write # Obtain OIDC identity for the Scorecard artifact. - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-scorecard-json.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/public-scorecard-json.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 6ca2cc1..7dcb4d0 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -16,4 +16,4 @@ concurrency: jobs: secret-scan: name: Secret scan - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/secret-scan.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/secret-scan.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 86e7636..110cd24 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -21,7 +21,7 @@ jobs: permissions: contents: read security-events: write # Publish workflow-analysis results to code scanning. - uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/zizmor-sarif.yml@eda8ff7eb970c05a219238b6e5fb7faad8f67c40 # 0.7.0 + uses: NDDev-it-com/nddev-ci-workflows/.github/workflows/zizmor-sarif.yml@8b8e3ea6321c912b68eec831c2072a0173203433 # 0.8.1 with: persona: pedantic min_severity: low diff --git a/build/release-evidence.json b/build/release-evidence.json index e5d8d65..061f2c4 100644 --- a/build/release-evidence.json +++ b/build/release-evidence.json @@ -2,11 +2,11 @@ "schema_version": 1, "module": { "repository": "NDDev-it-com/nddev-zcode-app", - "setup_digest": "sha256:024e07675c09c94849000f18275727a6e4a1624be663264021aa7025ba64f08b" + "setup_digest": "sha256:1868c8955f0656cf02522ee177d04c53f08aa2a8d6cdb62d7b1610bcfe9ce54c" }, "harness": { "repository": "NDDev-it-com/nddev-harnesses", - "commit": "d432aae15a3a3fe48dab5cb447ccc03abe5c78f8" + "commit": "e17e5f403914a248a3b7d53a251ee5136d94f449" }, "adapter": { "id": "zcode", @@ -37,8 +37,8 @@ "benchmark:macos", "benchmark:ubuntu" ], - "generated_at_utc": "2026-07-11T19:33:20Z", - "expires_at_utc": "2027-01-07T19:33:20Z", + "generated_at_utc": "2026-07-11T22:04:47Z", + "expires_at_utc": "2027-01-07T22:04:47Z", "promotion": { "decision": "approved" }