mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-11-13 03:33:55 +01:00
ci: add exact-source Gitea backend builds
This commit is contained in:
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Admit one reviewed canonical source or its exact Builds merge wrapper."""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class Refusal(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def git(*args):
|
||||
return subprocess.check_output(["git", *args], text=True, stderr=subprocess.PIPE).strip()
|
||||
|
||||
|
||||
def commit(value, label):
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", value or ""):
|
||||
raise Refusal(f"{label} must be a reviewed full lowercase commit SHA")
|
||||
return value
|
||||
|
||||
|
||||
def source_for_checkout(require_wrapper):
|
||||
if not require_wrapper:
|
||||
return git("rev-parse", "HEAD")
|
||||
parents = git("show", "-s", "--format=%P", "HEAD").split()
|
||||
if len(parents) != 2:
|
||||
raise Refusal("release or main CI requires a two-parent Builds wrapper")
|
||||
return commit(parents[1], "canonical source parent")
|
||||
|
||||
|
||||
def admit(source, expected_head=None, require_wrapper=False):
|
||||
source = commit(source, "canonical source")
|
||||
head = git("rev-parse", "HEAD")
|
||||
if expected_head is not None and head != commit(expected_head, "expected_sha"):
|
||||
raise Refusal("checkout HEAD differs from the reviewed dispatch head")
|
||||
if git("rev-parse", f"{source}^{{commit}}") != source:
|
||||
raise Refusal("reviewed canonical source is not a commit")
|
||||
tree = git("rev-parse", f"{source}^{{tree}}")
|
||||
if git("rev-parse", "HEAD^{tree}") != tree:
|
||||
raise Refusal("Builds checkout tree differs from reviewed canonical source")
|
||||
if head != source or require_wrapper:
|
||||
parents = git("show", "-s", "--format=%P", "HEAD").split()
|
||||
if len(parents) != 2 or parents[1] != source:
|
||||
raise Refusal("Builds HEAD is neither canonical source nor its two-parent wrapper")
|
||||
if git("status", "--porcelain", "--untracked-files=all"):
|
||||
raise Refusal("Builds checkout contains local changes or untracked files")
|
||||
return {"source_commit": source, "build_commit": head, "source_tree": tree}
|
||||
|
||||
|
||||
def main():
|
||||
if os.environ.get("GITHUB_REPOSITORY") != "Builds/smirk-backend-core":
|
||||
raise Refusal("this workflow is admitted only on Builds/smirk-backend-core")
|
||||
event = os.environ.get("GITHUB_EVENT_NAME")
|
||||
expected = os.environ.get("EXPECTED_BUILD_SHA")
|
||||
if not expected:
|
||||
raise Refusal("this build requires the exact reviewed event or dispatch head")
|
||||
if event not in {"push", "pull_request", "workflow_dispatch"}:
|
||||
raise Refusal("this build event is not admitted")
|
||||
wrapper = event != "pull_request"
|
||||
canonical = source_for_checkout(wrapper)
|
||||
result = admit(canonical, expected, require_wrapper=wrapper)
|
||||
print(f"Admitted canonical source {result['source_commit']} at Builds {result['build_commit']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Refusal as error:
|
||||
sys.exit(f"Build source refused: {error}")
|
||||
except subprocess.CalledProcessError:
|
||||
sys.exit("Build source refused: Git could not resolve the reviewed source or checkout")
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Own a disposable Unix-socket PostgreSQL cluster. Never use a caller's database.
|
||||
set -euo pipefail
|
||||
set +x
|
||||
if [[ $# != 0 ]]; then
|
||||
echo 'Fundless CI does not accept a database or test-command override.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f .env ]]; then
|
||||
echo 'Fundless CI refuses a checkout containing .env.' >&2
|
||||
exit 1
|
||||
fi
|
||||
pg_bin=$(pg_config --bindir)
|
||||
for tool in initdb pg_ctl createdb psql; do
|
||||
[[ -x "$pg_bin/$tool" ]] || { echo "Fundless CI requires PostgreSQL $tool." >&2; exit 1; }
|
||||
done
|
||||
ci_pg_dir=$(mktemp -d /tmp/smirk-ci-pg.XXXXXXXX)
|
||||
chmod 700 "$ci_pg_dir"
|
||||
pg_user=$(id -un)
|
||||
if [[ $(id -u) == 0 ]]; then
|
||||
pg_user=postgres
|
||||
id "$pg_user" >/dev/null 2>&1 || { echo 'The isolated CI container requires the postgres service account.' >&2; exit 1; }
|
||||
chown "$pg_user" "$ci_pg_dir"
|
||||
fi
|
||||
pg_exec() {
|
||||
if [[ $(id -u) == 0 ]]; then
|
||||
runuser -u "$pg_user" -- "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
cleanup() {
|
||||
pg_exec "$pg_bin/pg_ctl" -D "$ci_pg_dir/data" -m immediate -w stop >/dev/null 2>&1 || true
|
||||
rm -rf "$ci_pg_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
pg_exec "$pg_bin/initdb" -D "$ci_pg_dir/data" --auth-local=trust --auth-host=reject --no-locale >/dev/null
|
||||
# No TCP listener and a private socket directory: no password or shared service.
|
||||
pg_exec "$pg_bin/pg_ctl" -D "$ci_pg_dir/data" -l "$ci_pg_dir/postgres.log" \
|
||||
-o "-k $ci_pg_dir -c listen_addresses='' -c unix_socket_permissions=0700" -w start >/dev/null
|
||||
pg_exec "$pg_bin/createdb" -h "$ci_pg_dir" smirk_ci
|
||||
export TEST_DATABASE_URL="postgresql:///smirk_ci?host=$ci_pg_dir&user=$pg_user"
|
||||
# A failing or missing DB is an error; tests must not quietly take their no-DB path.
|
||||
pg_exec "$pg_bin/psql" -h "$ci_pg_dir" -d smirk_ci -Atc 'SELECT 1' | grep -qx 1
|
||||
cargo test --locked --tests
|
||||
cargo test --locked --doc
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage a private Linux candidate with exact source and artifact provenance."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
spec = importlib.util.spec_from_file_location("build_source", ROOT / "tools/check-build-source.py")
|
||||
source = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(source)
|
||||
|
||||
|
||||
def sha256(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def elf_requirements(path):
|
||||
header = Path(path).read_bytes()[:20]
|
||||
if len(header) != 20 or header[:6] != b"\x7fELF\x02\x01" or header[18:20] != b"\x3e\x00":
|
||||
raise source.Refusal("candidate must be a 64-bit little-endian x86_64 ELF binary")
|
||||
versions = subprocess.check_output(["readelf", "--version-info", str(path)], text=True)
|
||||
required = sorted(set(re.findall(r"\bGLIBC_([0-9]+(?:\.[0-9]+)+)\b", versions)),
|
||||
key=lambda value: tuple(map(int, value.split("."))))
|
||||
if not required:
|
||||
raise source.Refusal("candidate has no readable GNU libc compatibility evidence")
|
||||
if tuple(map(int, required[-1].split("."))) > (2, 36):
|
||||
raise source.Refusal("candidate requires newer GNU libc than the admitted Debian 12 baseline")
|
||||
return required
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
os.chdir(ROOT)
|
||||
admitted = source.admit(source.source_for_checkout(True),
|
||||
os.environ.get("EXPECTED_BUILD_SHA", ""), require_wrapper=True)
|
||||
binary = ROOT / "target/x86_64-unknown-linux-gnu/release/smirk-backend-core"
|
||||
required = elf_requirements(binary)
|
||||
rustc = subprocess.check_output(["rustc", "--version"], text=True).strip()
|
||||
if not rustc.startswith("rustc 1.98.0 "):
|
||||
raise source.Refusal("candidate compiler differs from the reviewed Rust 1.98.0 pin")
|
||||
args.output.mkdir(mode=0o700, parents=False, exist_ok=False)
|
||||
staged = args.output / "smirk-backend-core"
|
||||
shutil.copyfile(binary, staged)
|
||||
staged.chmod(0o755)
|
||||
manifest = {
|
||||
"schema_version": 1,
|
||||
"product": "smirk-backend-core",
|
||||
"publication": "private_candidate_only",
|
||||
**admitted,
|
||||
"canonical_repository": "https://github.com/Such-Software/smirk-backend-core",
|
||||
"build_repository": "Builds/smirk-backend-core",
|
||||
"workflow_path": ".gitea/workflows/backend-build.yml",
|
||||
"workflow_sha256": sha256(ROOT / ".gitea/workflows/backend-build.yml"),
|
||||
"cargo_lock_sha256": sha256(ROOT / "Cargo.lock"),
|
||||
"rust_toolchain_sha256": sha256(ROOT / "rust-toolchain.toml"),
|
||||
"rustc": rustc,
|
||||
"target": "x86_64-unknown-linux-gnu",
|
||||
"glibc_versions": required,
|
||||
"artifact": {"name": staged.name, "sha256": sha256(staged), "size": staged.stat().st_size},
|
||||
}
|
||||
(args.output / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
|
||||
print(f"Staged private candidate for {admitted['source_commit']}; publication requires separate admission.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except source.Refusal as error:
|
||||
sys.exit(f"Candidate refused: {error}")
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
sys.exit("Candidate refused: required build output, compiler, or ELF evidence is unavailable")
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Fundless Git fixtures prove source admission and binary compatibility guards."""
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
TOOLS = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
source = load("source", TOOLS / "check-build-source.py")
|
||||
stage = load("stage", TOOLS / "stage-backend-candidate.py")
|
||||
|
||||
|
||||
class Admission(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="smirk-git-admission-")
|
||||
self.previous = Path.cwd()
|
||||
os.chdir(self.temp.name)
|
||||
self.git("init", "-q")
|
||||
self.git("config", "user.name", "Fundless fixture")
|
||||
self.git("config", "user.email", "fixture@example.invalid")
|
||||
Path("source.txt").write_text("baseline\n")
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "baseline")
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
Path("source.txt").write_text("candidate\n")
|
||||
self.git("commit", "-qam", "candidate")
|
||||
self.candidate = self.git("rev-parse", "HEAD")
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(self.previous)
|
||||
self.temp.cleanup()
|
||||
|
||||
def git(self, *args, input=None):
|
||||
return subprocess.check_output(["git", *args], input=input, text=True,
|
||||
stderr=subprocess.PIPE).strip()
|
||||
|
||||
def wrapper(self, second=None, tree=None):
|
||||
tree = tree or self.git("rev-parse", self.candidate + "^{tree}")
|
||||
value = self.git("commit-tree", tree, "-p", self.base, "-p", second or self.candidate,
|
||||
input="wrapper\n")
|
||||
self.git("reset", "--hard", value)
|
||||
return value
|
||||
|
||||
def test_canonical_and_tree_identical_wrapper_admitted(self):
|
||||
self.assertEqual(source.admit(self.candidate)["build_commit"], self.candidate)
|
||||
wrapper = self.wrapper()
|
||||
admitted = source.admit(self.candidate, wrapper)
|
||||
self.assertEqual(admitted["source_commit"], self.candidate)
|
||||
self.assertEqual(admitted["source_tree"], self.git("rev-parse", "HEAD^{tree}"))
|
||||
|
||||
def test_release_requires_wrapper_and_derives_its_source(self):
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.source_for_checkout(True)
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit(self.candidate, self.candidate, require_wrapper=True)
|
||||
wrapper = self.wrapper()
|
||||
self.assertEqual(source.source_for_checkout(True), self.candidate)
|
||||
self.assertEqual(source.admit(self.candidate, wrapper, require_wrapper=True)["build_commit"], wrapper)
|
||||
|
||||
def test_moving_dispatch_head_refused(self):
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit(self.candidate, self.base)
|
||||
|
||||
def test_tree_change_refused(self):
|
||||
self.wrapper(tree=self.git("rev-parse", self.base + "^{tree}"))
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit(self.candidate)
|
||||
|
||||
def test_tree_identity_without_source_parent_refused(self):
|
||||
tree = self.git("rev-parse", self.candidate + "^{tree}")
|
||||
unrelated = self.git("commit-tree", tree, "-p", self.base, input="unrelated\n")
|
||||
self.wrapper(second=unrelated)
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit(self.candidate)
|
||||
|
||||
def test_dirty_surface_and_missing_pin_refused(self):
|
||||
Path("unknown.txt").write_text("unreviewed\n")
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit(self.candidate)
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.admit("")
|
||||
|
||||
def test_dispatch_missing_head_or_wrong_repository_refused(self):
|
||||
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "Builds/smirk-backend-core",
|
||||
"GITHUB_EVENT_NAME": "workflow_dispatch"}, clear=True):
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.main()
|
||||
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "unreviewed/repository"}, clear=True):
|
||||
with self.assertRaises(source.Refusal):
|
||||
source.main()
|
||||
|
||||
|
||||
class BinaryCompatibility(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="smirk-elf-fixture-")
|
||||
self.binary = Path(self.temp.name) / "binary"
|
||||
self.binary.write_bytes(b"\x7fELF\x02\x01" + bytes(12) + b"\x3e\x00")
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def test_compatible_versions_recorded_in_numeric_order(self):
|
||||
with patch.object(subprocess, "check_output", return_value="GLIBC_2.3 GLIBC_2.36 GLIBC_2.9"):
|
||||
result = stage.elf_requirements(self.binary)
|
||||
self.assertEqual(result[-1], "2.36")
|
||||
self.assertIn("2.9", result)
|
||||
|
||||
def test_incompatible_or_unknown_libc_refused(self):
|
||||
for output in ("GLIBC_2.38", "unreadable"):
|
||||
with self.subTest(output=output), patch.object(subprocess, "check_output", return_value=output):
|
||||
with self.assertRaises(stage.source.Refusal):
|
||||
stage.elf_requirements(self.binary)
|
||||
|
||||
def test_wrong_binary_format_refused_before_tool_invocation(self):
|
||||
self.binary.write_bytes(b"not ELF")
|
||||
with patch.object(subprocess, "check_output") as probe:
|
||||
with self.assertRaises(stage.source.Refusal):
|
||||
stage.elf_requirements(self.binary)
|
||||
probe.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user