Add build provenance to release evidence

This commit is contained in:
igor04091968
2026-07-01 16:49:35 +03:00
parent b9dca66d1b
commit e6378e87f5
7 changed files with 102 additions and 10 deletions
+5
View File
@@ -106,6 +106,11 @@ Expected benefit: release and production state become auditable.
### P0-2. Controlled release evidence build
Status: addressed by TASK_070. Existing release evidence tooling now records
resolved Git SHA, deterministic `SOURCE_DATE_EPOCH` / `BUILD_TIME`, runner
environment and artifact checksums, and the evidence checker enforces the
required provenance fields.
Purpose: produce Production 1.0 release evidence outside GitHub public mirror.
Reason: `docs/PROJECT_STATUS_RU.md`, `docs/QUALITY_STATUS_RU.md` and
@@ -58,6 +58,9 @@ checks only. They are not primary registry release evidence.
- `rustc` / `cargo` versions;
- git commit;
- tag/version;
- resolved 40-character Git commit SHA;
- `SOURCE_DATE_EPOCH`;
- deterministic `BUILD_TIME` derived from `SOURCE_DATE_EPOCH`;
- checks;
- artifacts;
- SBOM;
@@ -34,6 +34,19 @@ produced on the Russian build-runner.
17. Сохранить logs.
18. Сохранить final report.
Build provenance requirements:
- `RELEASE_COMMIT` may be a SHA or tag, but manifest `release_commit` must be
the resolved full 40-character Git SHA.
- `GIT_COMMIT` must be exported during Rust build so `/version` and other
build/version probes can report the source revision.
- `SOURCE_DATE_EPOCH` defaults to the resolved commit timestamp and must be
recorded in release evidence.
- `BUILD_TIME` must be derived from `SOURCE_DATE_EPOCH`, not from wall-clock
build time.
- Wall-clock `generated_at` is evidence generation time only; it is not the
embedded build timestamp.
Если smoke требует live stand, не удалять и не скрывать его. В release report
фиксировать: `skipped: requires live stand`.
@@ -63,6 +76,8 @@ product/runtime release candidate.
- `RELEASE_EVIDENCE_REPORT_RU.md`.
- `SHA256SUMS`.
- `logs/`.
- `logs/source-date-epoch.log`.
- `logs/build-time-utc.log`.
- Source archive.
- Binary artifacts archive или documented skip только для `DOCS_ONLY=1`.
- `cargo-metadata.json` или documented skip.
+10 -1
View File
@@ -129,7 +129,16 @@ trap cleanup_on_failure EXIT
mkdir -p "$OUT_DIR"
git rev-parse HEAD > "$OUT_DIR/git-commit.txt"
GIT_COMMIT="$(git rev-parse HEAD)"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct "$GIT_COMMIT")}"
BUILD_TIME="$(date -u -d "@${SOURCE_DATE_EPOCH}" +%Y-%m-%dT%H:%M:%SZ)"
export GIT_COMMIT
export SOURCE_DATE_EPOCH
export BUILD_TIME
printf '%s\n' "$GIT_COMMIT" > "$OUT_DIR/git-commit.txt"
printf '%s\n' "$SOURCE_DATE_EPOCH" > "$OUT_DIR/source-date-epoch.txt"
printf '%s\n' "$BUILD_TIME" > "$OUT_DIR/build-time-utc.txt"
cargo fmt --manifest-path adk-rust/Cargo.toml --all -- --check
cargo test --manifest-path adk-rust/Cargo.toml --workspace
+45 -6
View File
@@ -118,11 +118,21 @@ if [[ "$RELEASE_COMMIT" != "$(git rev-parse HEAD)" ]]; then
git -c filter.lfs.smudge= -c filter.lfs.process= -c filter.lfs.required=false checkout --detach "$RELEASE_COMMIT"
fi
RELEASE_COMMIT_INPUT="$RELEASE_COMMIT"
RELEASE_COMMIT="$(git rev-parse HEAD)"
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct "$RELEASE_COMMIT")}"
BUILD_TIME_UTC="$(date -u -d "@${SOURCE_DATE_EPOCH}" +%Y-%m-%dT%H:%M:%SZ)"
export GIT_COMMIT="$RELEASE_COMMIT"
export BUILD_TIME="$BUILD_TIME_UTC"
export SOURCE_DATE_EPOCH
{
git remote -v
} > "$LOG_DIR/git-remotes.log" 2>&1
git status --short > "$LOG_DIR/git-status-short.log" 2>&1
git rev-parse HEAD > "$LOG_DIR/git-rev-parse-head.log" 2>&1
printf '%s\n' "$SOURCE_DATE_EPOCH" > "$LOG_DIR/source-date-epoch.log"
printf '%s\n' "$BUILD_TIME_UTC" > "$LOG_DIR/build-time-utc.log"
git log --oneline -20 > "$LOG_DIR/git-log-oneline-20.log" 2>&1
capture_command rustc-version rustc --version
capture_command cargo-version cargo --version
@@ -198,14 +208,21 @@ git -c filter.lfs.smudge= -c filter.lfs.process= -c filter.lfs.required=false ar
record_check "source_archive" "ok" "$SOURCE_ARCHIVE"
BINARY_ARCHIVE="$ARTIFACT_DIR/${RELEASE_VERSION}-binaries.tar.gz"
if [[ -n "${CARGO_TARGET_DIR:-}" ]]; then
BINARY_RELEASE_DIR="${CARGO_TARGET_DIR%/}/release"
elif [[ -n "$CARGO_WORKSPACE_DIR" ]]; then
BINARY_RELEASE_DIR="$CARGO_WORKSPACE_DIR/target/release"
else
BINARY_RELEASE_DIR="$ROOT/target/release"
fi
if [[ "$DOCS_ONLY" == "1" ]]; then
printf 'skipped: DOCS_ONLY=1\n' > "$BINARY_ARCHIVE.skip"
skip_check "binary_archive" "DOCS_ONLY=1"
elif [[ -d target/release ]]; then
tar -czf "$BINARY_ARCHIVE" target/release
elif [[ -d "$BINARY_RELEASE_DIR" ]]; then
tar -czf "$BINARY_ARCHIVE" -C "$BINARY_RELEASE_DIR" .
record_check "binary_archive" "ok" "$BINARY_ARCHIVE"
else
skip_check "binary_archive" "target/release missing"
skip_check "binary_archive" "$BINARY_RELEASE_DIR missing"
fi
if command -v cargo-cyclonedx >/dev/null 2>&1; then
@@ -226,12 +243,22 @@ GENERATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
BUILD_RUNNER_HOST="$(hostname 2>/dev/null || printf unknown)"
PRIMARY_SOURCE_REPOSITORY="https://git.iri1968.dpdns.org/awatch-rus/AWatch-rus"
python3 - "$OUTPUT_DIR/release-evidence-manifest.json" "$RELEASE_VERSION" "$RELEASE_COMMIT" "$GENERATED_AT" "$BUILD_RUNNER_HOST" "$PRIMARY_SOURCE_REPOSITORY" <<'PY'
python3 - "$OUTPUT_DIR/release-evidence-manifest.json" "$RELEASE_VERSION" "$RELEASE_COMMIT_INPUT" "$RELEASE_COMMIT" "$SOURCE_DATE_EPOCH" "$BUILD_TIME_UTC" "$GENERATED_AT" "$BUILD_RUNNER_HOST" "$PRIMARY_SOURCE_REPOSITORY" <<'PY'
import json
import sys
from pathlib import Path
manifest_path, release_version, release_commit, generated_at, build_runner, primary_source = sys.argv[1:]
(
manifest_path,
release_version,
release_commit_input,
release_commit,
source_date_epoch,
build_time_utc,
generated_at,
build_runner,
primary_source,
) = sys.argv[1:]
root = Path(manifest_path).parent
checks = []
checks_file = root / "checks.tsv"
@@ -248,7 +275,10 @@ for path in sorted((root / "artifacts").glob("*")):
data = {
"product": "AWatch-rus",
"release_version": release_version,
"release_commit_input": release_commit_input,
"release_commit": release_commit,
"source_date_epoch": int(source_date_epoch),
"build_time_utc": build_time_utc,
"build_runner": build_runner,
"primary_source_repository": primary_source,
"github_role": "public_mirror_only",
@@ -269,6 +299,12 @@ Release version: ${RELEASE_VERSION}
Release commit: ${RELEASE_COMMIT}
Release commit input: ${RELEASE_COMMIT_INPUT}
SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH}
Build time UTC: ${BUILD_TIME_UTC}
Generated at: ${GENERATED_AT}
Build runner: ${BUILD_RUNNER_HOST}
@@ -300,7 +336,10 @@ EOF
(
cd "$OUTPUT_DIR"
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
sums_tmp="$(mktemp)"
trap 'rm -f "$sums_tmp"' EXIT
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > "$sums_tmp"
mv "$sums_tmp" SHA256SUMS
)
if [[ -f scripts/check_release_evidence.sh ]]; then
+9 -2
View File
@@ -48,7 +48,10 @@ if [[ -s "$EVIDENCE_DIR/release-evidence-manifest.json" ]]; then
jq -e '
.product == "AWatch-rus"
and (.release_version | type == "string" and length > 0)
and (.release_commit | type == "string" and length > 0)
and (.release_commit | type == "string" and test("^[0-9a-f]{40}$"))
and (.release_commit_input | type == "string" and length > 0)
and (.source_date_epoch | type == "number")
and (.build_time_utc | type == "string" and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
and (.build_runner | type == "string" and length > 0)
and .primary_source_repository == "https://git.iri1968.dpdns.org/awatch-rus/AWatch-rus"
and .github_role == "public_mirror_only"
@@ -72,9 +75,13 @@ required = {
for key, value in required.items():
if data.get(key) != value:
raise SystemExit(f"{key} mismatch")
for key in ("release_version", "release_commit", "build_runner", "generated_at"):
for key in ("release_version", "release_commit", "release_commit_input", "build_time_utc", "build_runner", "generated_at"):
if not isinstance(data.get(key), str) or not data[key]:
raise SystemExit(f"{key} missing")
if len(data["release_commit"]) != 40 or any(char not in "0123456789abcdef" for char in data["release_commit"]):
raise SystemExit("release_commit must be a full lowercase git SHA")
if not isinstance(data.get("source_date_epoch"), int):
raise SystemExit("source_date_epoch missing")
if not isinstance(data.get("checks"), list):
raise SystemExit("checks missing")
if not isinstance(data.get("artifacts"), list):
+15 -1
View File
@@ -6,6 +6,7 @@ from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import stat
import tarfile
@@ -84,6 +85,18 @@ def create_compatibility_aliases(out_dir: Path, archive: Path) -> None:
write_archive_checksum(archive_alias)
def build_time_utc() -> str:
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH")
if source_date_epoch:
try:
stamp = datetime.fromtimestamp(int(source_date_epoch), timezone.utc)
except ValueError as exc:
raise SystemExit("SOURCE_DATE_EPOCH must be an integer Unix timestamp") from exc
else:
stamp = datetime.now(timezone.utc)
return stamp.isoformat(timespec="seconds").replace("+00:00", "Z")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--release-dir", type=Path, required=True)
@@ -127,7 +140,8 @@ def main() -> None:
"commit": args.commit,
"ref": args.ref,
"run_id": args.run_id,
"build_time_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"source_date_epoch": os.environ.get("SOURCE_DATE_EPOCH", ""),
"build_time_utc": build_time_utc(),
"binaries": manifest_binaries,
}
write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")