Add build provenance to release evidence
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user