build: fail closed on existing local release artifacts
This commit is contained in:
+48
-14
@@ -7,18 +7,33 @@ OUT=$(realpath -m -- "$OUT_INPUT")
|
||||
VERSION=${RELEASE_VERSION:-local-development}
|
||||
|
||||
case "$OUT" in
|
||||
"$ROOT"/*) ;;
|
||||
"$ROOT"/dist/*) ;;
|
||||
*)
|
||||
echo "release output must stay below project root: $OUT" >&2
|
||||
echo "release output must stay below project dist/: $OUT" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [[ -e "$OUT" || -L "$OUT" ]]; then
|
||||
echo "release output already exists; refusing to replace it: $OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf -- "$OUT"
|
||||
mkdir -p -- "$OUT"
|
||||
|
||||
go_version=$(go version)
|
||||
case "$go_version" in
|
||||
"go version go1.27.1 "*) ;;
|
||||
*) echo "release requires Go 1.27.1, got: $go_version" >&2; exit 1 ;;
|
||||
esac
|
||||
source_ref=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || printf 'unavailable')
|
||||
source_dirty=false
|
||||
if [[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=all -- . 2>/dev/null)" ]]; then
|
||||
source_dirty=true
|
||||
fi
|
||||
cd -- "$ROOT"
|
||||
"$ROOT/scripts/check-proto.sh" > /dev/null
|
||||
"$ROOT/scripts/check-contracts.sh" > /dev/null
|
||||
go mod verify
|
||||
mkdir -p -- "$(dirname -- "$OUT")"
|
||||
mkdir -- "$OUT"
|
||||
go build -trimpath -buildvcs=false -o "$OUT/sip-go-agent" ./cmd/sip-go-agent
|
||||
cp -- go.mod go.sum "$OUT/"
|
||||
(
|
||||
@@ -26,23 +41,27 @@ cp -- go.mod go.sum "$OUT/"
|
||||
sha256sum sip-go-agent go.mod go.sum > SHA256SUMS
|
||||
)
|
||||
|
||||
source_ref=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || printf 'unavailable')
|
||||
source_dirty=false
|
||||
if [[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=all -- . 2>/dev/null)" ]]; then
|
||||
source_dirty=true
|
||||
fi
|
||||
|
||||
go_version=$(go version)
|
||||
python3 - "$OUT/manifest.json" "$VERSION" "$source_ref" "$source_dirty" "$go_version" "$OUT" <<'PY'
|
||||
python3 - "$OUT/manifest.json" "$VERSION" "$source_ref" "$source_dirty" "$go_version" "$OUT" "$ROOT" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
manifest_path, version, source_ref, source_dirty, go_version, out = sys.argv[1:]
|
||||
manifest_path, version, source_ref, source_dirty, go_version, out, project_root = sys.argv[1:]
|
||||
root = pathlib.Path(out)
|
||||
project = pathlib.Path(project_root)
|
||||
def sha256(name):
|
||||
return hashlib.sha256((root / name).read_bytes()).hexdigest()
|
||||
def project_sha256(path):
|
||||
return hashlib.sha256((project / path).read_bytes()).hexdigest()
|
||||
|
||||
v01 = json.loads((project / "docs/contracts/local-contract-manifest-v0.1.json").read_text())
|
||||
v02 = json.loads((project / "docs/contracts/local-contract-manifest-v0.2.json").read_text())
|
||||
topology = json.loads((project / "docs/contracts/mq-topology-v0.1-proposal.json").read_text())
|
||||
if (v01["manifest_version"], v02["manifest_version"], topology["contract_version"]) != (
|
||||
"local-contract-manifest.v0.1", "local-contract-manifest.v0.2", "project-saas-dispatcher.v0.1"
|
||||
):
|
||||
raise SystemExit("unexpected project-local contract or queue topology version")
|
||||
|
||||
manifest = {
|
||||
"manifest_version": 1,
|
||||
@@ -56,6 +75,21 @@ manifest = {
|
||||
"go.mod": sha256("go.mod"),
|
||||
"go.sum": sha256("go.sum"),
|
||||
},
|
||||
"contract_attestation": {
|
||||
"local_business": {
|
||||
"version": v01["manifest_version"],
|
||||
"manifest_sha256": project_sha256("docs/contracts/local-contract-manifest-v0.1.json"),
|
||||
},
|
||||
"task_discovery": {
|
||||
"version": v02["manifest_version"],
|
||||
"manifest_sha256": project_sha256("docs/contracts/local-contract-manifest-v0.2.json"),
|
||||
},
|
||||
"mq_topology": {
|
||||
"version": topology["contract_version"],
|
||||
"manifest_sha256": project_sha256("docs/contracts/mq-topology-v0.1-proposal.json"),
|
||||
},
|
||||
"proto_manifest_sha256": project_sha256("proto/manifest.json"),
|
||||
},
|
||||
"security": {
|
||||
"credentials_embedded": False,
|
||||
"production_approval": False,
|
||||
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
mkdir -p -- "$ROOT/dist"
|
||||
WORK=$(mktemp -d "$ROOT/dist/.f06-local.XXXXXXXX")
|
||||
trap 'rm -rf -- "$WORK"' EXIT
|
||||
PACKAGE_VERSION="f06-$(basename -- "$WORK" | tr -cd '[:alnum:]')"
|
||||
PACKAGE_RELEASE="$ROOT/dist/release-$PACKAGE_VERSION"
|
||||
PACKAGE_STAGE="$ROOT/dist/package-$PACKAGE_VERSION"
|
||||
PACKAGE_ARCHIVE="$ROOT/dist/packages/sip-go-agent-$PACKAGE_VERSION-linux-amd64.tar.gz"
|
||||
for path in "$PACKAGE_RELEASE" "$PACKAGE_STAGE" "$PACKAGE_ARCHIVE" "$PACKAGE_ARCHIVE.sha256"; do
|
||||
if [[ -e "$path" || -L "$path" ]]; then
|
||||
echo "local package test path already exists: $path" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
mkdir -- "$PACKAGE_RELEASE"
|
||||
trap 'rm -rf -- "$WORK" "$PACKAGE_RELEASE" "$PACKAGE_STAGE"; rm -f -- "$PACKAGE_ARCHIVE" "$PACKAGE_ARCHIVE.sha256"' EXIT
|
||||
printf 'keep existing package output\n' > "$PACKAGE_RELEASE/sentinel"
|
||||
|
||||
protected="$WORK/preexisting"
|
||||
mkdir -- "$protected"
|
||||
printf 'keep existing release output\n' > "$protected/sentinel"
|
||||
if RELEASE_VERSION=local-development "$ROOT/scripts/build-release.sh" "$protected" > "$WORK/rejected.log" 2>&1; then
|
||||
echo 'release build accepted a preexisting output directory' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$protected/sentinel" ]] || [[ $(<"$protected/sentinel") != 'keep existing release output' ]]; then
|
||||
echo 'release build removed or changed a preexisting file' >&2
|
||||
exit 1
|
||||
fi
|
||||
if "$ROOT/deploys/build-package.sh" "$PACKAGE_VERSION" > "$WORK/package-rejected.log" 2>&1; then
|
||||
echo 'package build accepted a preexisting release directory' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$PACKAGE_RELEASE/sentinel" ]] || [[ $(<"$PACKAGE_RELEASE/sentinel") != 'keep existing package output' ]]; then
|
||||
echo 'package build removed or changed a preexisting file' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$ROOT/scripts/check-proto.sh" > "$WORK/proto.log"
|
||||
"$ROOT/scripts/check-contracts.sh" > "$WORK/contracts.log"
|
||||
RELEASE_VERSION=local-development "$ROOT/scripts/build-release.sh" "$WORK/release" > "$WORK/build.log" 2>&1 || {
|
||||
tail -n 25 "$WORK/build.log" >&2
|
||||
exit 1
|
||||
}
|
||||
(cd -- "$WORK/release" && sha256sum --check SHA256SUMS > /dev/null)
|
||||
python3 - "$ROOT" "$WORK/release" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root, release = map(pathlib.Path, sys.argv[1:])
|
||||
manifest = json.loads((release / "manifest.json").read_text())
|
||||
assert manifest["manifest_version"] == 1
|
||||
assert manifest["scope"] == manifest["version"] == "local-development"
|
||||
assert manifest["security"] == {"credentials_embedded": False, "production_approval": False}
|
||||
assert manifest["binary"]["sha256"] == hashlib.sha256((release / "sip-go-agent").read_bytes()).hexdigest()
|
||||
assert manifest["go_version"].startswith("go version go1.27.1 ")
|
||||
|
||||
v01 = json.loads((root / "docs/contracts/local-contract-manifest-v0.1.json").read_text())
|
||||
v02 = json.loads((root / "docs/contracts/local-contract-manifest-v0.2.json").read_text())
|
||||
topology = json.loads((root / "docs/contracts/mq-topology-v0.1-proposal.json").read_text())
|
||||
assert v01["manifest_version"] == "local-contract-manifest.v0.1"
|
||||
assert v02["manifest_version"] == "local-contract-manifest.v0.2"
|
||||
assert v02["source"]["path"] == "docs/thirds/v0.2.md"
|
||||
assert "docs/contracts/task-discovery-v0.2-proposal.schema.json" in {item["path"] for item in v02["artifacts"]}
|
||||
assert all("task-discovery-v0.1" not in item["path"] for item in v02["artifacts"])
|
||||
assert topology["contract_version"] == "project-saas-dispatcher.v0.1"
|
||||
assert all(topology["queues"][kind]["owner"] == "saas" for kind in ("task", "control", "result"))
|
||||
assert all(topology["queues"][kind]["queue_name"].endswith(".v3") for kind in ("task", "control", "result"))
|
||||
expected_attestation = {
|
||||
"local_business": {
|
||||
"version": v01["manifest_version"],
|
||||
"manifest_sha256": hashlib.sha256((root / "docs/contracts/local-contract-manifest-v0.1.json").read_bytes()).hexdigest(),
|
||||
},
|
||||
"task_discovery": {
|
||||
"version": v02["manifest_version"],
|
||||
"manifest_sha256": hashlib.sha256((root / "docs/contracts/local-contract-manifest-v0.2.json").read_bytes()).hexdigest(),
|
||||
},
|
||||
"mq_topology": {
|
||||
"version": topology["contract_version"],
|
||||
"manifest_sha256": hashlib.sha256((root / "docs/contracts/mq-topology-v0.1-proposal.json").read_bytes()).hexdigest(),
|
||||
},
|
||||
"proto_manifest_sha256": hashlib.sha256((root / "proto/manifest.json").read_bytes()).hexdigest(),
|
||||
}
|
||||
assert manifest["contract_attestation"] == expected_attestation
|
||||
print("local artifact SHA-256:", manifest["binary"]["sha256"])
|
||||
print("task discovery source SHA-256:", v02["source"]["sha256"])
|
||||
print("source dirty:", manifest["source_dirty"])
|
||||
print("production approved:", manifest["security"]["production_approval"])
|
||||
PY
|
||||
|
||||
if "$WORK/release/sip-go-agent" dispatcher --help | grep -Eq -- '--tenant-key|--consume'; then
|
||||
echo 'release exposes deprecated Dispatcher queue/tenant switches' >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -- "$PACKAGE_RELEASE/sentinel"
|
||||
rmdir -- "$PACKAGE_RELEASE"
|
||||
"$ROOT/deploys/build-package.sh" "$PACKAGE_VERSION" > "$WORK/package.log" 2>&1 || {
|
||||
tail -n 25 "$WORK/package.log" >&2
|
||||
exit 1
|
||||
}
|
||||
(cd -- "$(dirname -- "$PACKAGE_ARCHIVE")" && sha256sum --check "$(basename -- "$PACKAGE_ARCHIVE").sha256" > /dev/null)
|
||||
mkdir -- "$WORK/extracted"
|
||||
tar -C "$WORK/extracted" -xzf "$PACKAGE_ARCHIVE"
|
||||
(cd -- "$WORK/extracted" && sha256sum --check package.SHA256SUMS > /dev/null)
|
||||
python3 - "$WORK/extracted" "$PACKAGE_VERSION" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
package, version = pathlib.Path(sys.argv[1]), sys.argv[2]
|
||||
manifest = json.loads((package / "manifest.json").read_text())
|
||||
assert manifest["version"] == version
|
||||
assert manifest["scope"] == "local-development"
|
||||
assert manifest["security"]["production_approval"] is False
|
||||
assert manifest["contract_attestation"]["task_discovery"]["version"] == "local-contract-manifest.v0.2"
|
||||
PY
|
||||
echo 'F06 local release/package artifact and contract/queue gates passed; no deployment or dial attempted'
|
||||
Reference in New Issue
Block a user