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
|
### 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.
|
Purpose: produce Production 1.0 release evidence outside GitHub public mirror.
|
||||||
|
|
||||||
Reason: `docs/PROJECT_STATUS_RU.md`, `docs/QUALITY_STATUS_RU.md` and
|
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;
|
- `rustc` / `cargo` versions;
|
||||||
- git commit;
|
- git commit;
|
||||||
- tag/version;
|
- tag/version;
|
||||||
|
- resolved 40-character Git commit SHA;
|
||||||
|
- `SOURCE_DATE_EPOCH`;
|
||||||
|
- deterministic `BUILD_TIME` derived from `SOURCE_DATE_EPOCH`;
|
||||||
- checks;
|
- checks;
|
||||||
- artifacts;
|
- artifacts;
|
||||||
- SBOM;
|
- SBOM;
|
||||||
|
|||||||
@@ -34,6 +34,19 @@ produced on the Russian build-runner.
|
|||||||
17. Сохранить logs.
|
17. Сохранить logs.
|
||||||
18. Сохранить final report.
|
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
|
Если smoke требует live stand, не удалять и не скрывать его. В release report
|
||||||
фиксировать: `skipped: requires live stand`.
|
фиксировать: `skipped: requires live stand`.
|
||||||
|
|
||||||
@@ -63,6 +76,8 @@ product/runtime release candidate.
|
|||||||
- `RELEASE_EVIDENCE_REPORT_RU.md`.
|
- `RELEASE_EVIDENCE_REPORT_RU.md`.
|
||||||
- `SHA256SUMS`.
|
- `SHA256SUMS`.
|
||||||
- `logs/`.
|
- `logs/`.
|
||||||
|
- `logs/source-date-epoch.log`.
|
||||||
|
- `logs/build-time-utc.log`.
|
||||||
- Source archive.
|
- Source archive.
|
||||||
- Binary artifacts archive или documented skip только для `DOCS_ONLY=1`.
|
- Binary artifacts archive или documented skip только для `DOCS_ONLY=1`.
|
||||||
- `cargo-metadata.json` или documented skip.
|
- `cargo-metadata.json` или documented skip.
|
||||||
|
|||||||
@@ -129,7 +129,16 @@ trap cleanup_on_failure EXIT
|
|||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
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 fmt --manifest-path adk-rust/Cargo.toml --all -- --check
|
||||||
cargo test --manifest-path adk-rust/Cargo.toml --workspace
|
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"
|
git -c filter.lfs.smudge= -c filter.lfs.process= -c filter.lfs.required=false checkout --detach "$RELEASE_COMMIT"
|
||||||
fi
|
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
|
git remote -v
|
||||||
} > "$LOG_DIR/git-remotes.log" 2>&1
|
} > "$LOG_DIR/git-remotes.log" 2>&1
|
||||||
git status --short > "$LOG_DIR/git-status-short.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
|
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
|
git log --oneline -20 > "$LOG_DIR/git-log-oneline-20.log" 2>&1
|
||||||
capture_command rustc-version rustc --version
|
capture_command rustc-version rustc --version
|
||||||
capture_command cargo-version cargo --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"
|
record_check "source_archive" "ok" "$SOURCE_ARCHIVE"
|
||||||
|
|
||||||
BINARY_ARCHIVE="$ARTIFACT_DIR/${RELEASE_VERSION}-binaries.tar.gz"
|
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
|
if [[ "$DOCS_ONLY" == "1" ]]; then
|
||||||
printf 'skipped: DOCS_ONLY=1\n' > "$BINARY_ARCHIVE.skip"
|
printf 'skipped: DOCS_ONLY=1\n' > "$BINARY_ARCHIVE.skip"
|
||||||
skip_check "binary_archive" "DOCS_ONLY=1"
|
skip_check "binary_archive" "DOCS_ONLY=1"
|
||||||
elif [[ -d target/release ]]; then
|
elif [[ -d "$BINARY_RELEASE_DIR" ]]; then
|
||||||
tar -czf "$BINARY_ARCHIVE" target/release
|
tar -czf "$BINARY_ARCHIVE" -C "$BINARY_RELEASE_DIR" .
|
||||||
record_check "binary_archive" "ok" "$BINARY_ARCHIVE"
|
record_check "binary_archive" "ok" "$BINARY_ARCHIVE"
|
||||||
else
|
else
|
||||||
skip_check "binary_archive" "target/release missing"
|
skip_check "binary_archive" "$BINARY_RELEASE_DIR missing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v cargo-cyclonedx >/dev/null 2>&1; then
|
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)"
|
BUILD_RUNNER_HOST="$(hostname 2>/dev/null || printf unknown)"
|
||||||
PRIMARY_SOURCE_REPOSITORY="https://git.iri1968.dpdns.org/awatch-rus/AWatch-rus"
|
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 json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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
|
root = Path(manifest_path).parent
|
||||||
checks = []
|
checks = []
|
||||||
checks_file = root / "checks.tsv"
|
checks_file = root / "checks.tsv"
|
||||||
@@ -248,7 +275,10 @@ for path in sorted((root / "artifacts").glob("*")):
|
|||||||
data = {
|
data = {
|
||||||
"product": "AWatch-rus",
|
"product": "AWatch-rus",
|
||||||
"release_version": release_version,
|
"release_version": release_version,
|
||||||
|
"release_commit_input": release_commit_input,
|
||||||
"release_commit": release_commit,
|
"release_commit": release_commit,
|
||||||
|
"source_date_epoch": int(source_date_epoch),
|
||||||
|
"build_time_utc": build_time_utc,
|
||||||
"build_runner": build_runner,
|
"build_runner": build_runner,
|
||||||
"primary_source_repository": primary_source,
|
"primary_source_repository": primary_source,
|
||||||
"github_role": "public_mirror_only",
|
"github_role": "public_mirror_only",
|
||||||
@@ -269,6 +299,12 @@ Release version: ${RELEASE_VERSION}
|
|||||||
|
|
||||||
Release commit: ${RELEASE_COMMIT}
|
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}
|
Generated at: ${GENERATED_AT}
|
||||||
|
|
||||||
Build runner: ${BUILD_RUNNER_HOST}
|
Build runner: ${BUILD_RUNNER_HOST}
|
||||||
@@ -300,7 +336,10 @@ EOF
|
|||||||
|
|
||||||
(
|
(
|
||||||
cd "$OUTPUT_DIR"
|
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
|
if [[ -f scripts/check_release_evidence.sh ]]; then
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ if [[ -s "$EVIDENCE_DIR/release-evidence-manifest.json" ]]; then
|
|||||||
jq -e '
|
jq -e '
|
||||||
.product == "AWatch-rus"
|
.product == "AWatch-rus"
|
||||||
and (.release_version | type == "string" and length > 0)
|
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 (.build_runner | type == "string" and length > 0)
|
||||||
and .primary_source_repository == "https://git.iri1968.dpdns.org/awatch-rus/AWatch-rus"
|
and .primary_source_repository == "https://git.iri1968.dpdns.org/awatch-rus/AWatch-rus"
|
||||||
and .github_role == "public_mirror_only"
|
and .github_role == "public_mirror_only"
|
||||||
@@ -72,9 +75,13 @@ required = {
|
|||||||
for key, value in required.items():
|
for key, value in required.items():
|
||||||
if data.get(key) != value:
|
if data.get(key) != value:
|
||||||
raise SystemExit(f"{key} mismatch")
|
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]:
|
if not isinstance(data.get(key), str) or not data[key]:
|
||||||
raise SystemExit(f"{key} missing")
|
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):
|
if not isinstance(data.get("checks"), list):
|
||||||
raise SystemExit("checks missing")
|
raise SystemExit("checks missing")
|
||||||
if not isinstance(data.get("artifacts"), list):
|
if not isinstance(data.get("artifacts"), list):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import tarfile
|
import tarfile
|
||||||
@@ -84,6 +85,18 @@ def create_compatibility_aliases(out_dir: Path, archive: Path) -> None:
|
|||||||
write_archive_checksum(archive_alias)
|
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:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--release-dir", type=Path, required=True)
|
parser.add_argument("--release-dir", type=Path, required=True)
|
||||||
@@ -127,7 +140,8 @@ def main() -> None:
|
|||||||
"commit": args.commit,
|
"commit": args.commit,
|
||||||
"ref": args.ref,
|
"ref": args.ref,
|
||||||
"run_id": args.run_id,
|
"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,
|
"binaries": manifest_binaries,
|
||||||
}
|
}
|
||||||
write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
|
write(out_dir / "BUILD_MANIFEST.json", json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user