mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
ci: add exact-source Gitea backend builds
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
name: Backend candidate (Gitea)
|
||||
|
||||
# Private Linux candidate only. The reviewed dispatcher supplies the exact Builds
|
||||
# head. Fleet admits its exact canonical GitHub source parent before dispatch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expected_sha:
|
||||
description: Exact reviewed Builds main commit
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: never
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
EXPECTED_BUILD_SHA: ${{ inputs.expected_sha }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux candidate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-bookworm@sha256:0557ac14e0d45d02ed563067b82856ca5e7aa3437fa28d98d4350ea9c3d9494a
|
||||
env:
|
||||
RUSTUP_HOME: ${{ runner.temp }}/rustup-backend-candidate
|
||||
CARGO_HOME: ${{ runner.temp }}/cargo-backend-candidate
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.expected_sha }}
|
||||
persist-credentials: false
|
||||
- name: Require reviewed source selectors before setup
|
||||
run: |
|
||||
test "$GITHUB_REPOSITORY" = Builds/smirk-backend-core || { echo 'This repository is not admitted for backend Builds.' >&2; exit 1; }
|
||||
test "$(git rev-parse HEAD)" = "$EXPECTED_BUILD_SHA" || { echo 'Checkout differs from the reviewed event or dispatch head.' >&2; exit 1; }
|
||||
- name: Install isolated container build dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends python3 build-essential pkg-config libssl-dev binutils
|
||||
- name: Admit reviewed source and exact dispatch head
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
|
||||
canonical_source=$(git rev-parse HEAD)
|
||||
else
|
||||
read -r -a parents <<< "$(git show -s --format=%P HEAD)"
|
||||
test "${#parents[@]}" -eq 2 || { echo 'Builds main requires a two-parent source wrapper.' >&2; exit 1; }
|
||||
canonical_source=${parents[1]}
|
||||
fi
|
||||
git fetch --no-tags https://github.com/Such-Software/smirk-backend-core.git "$canonical_source"
|
||||
python3 tools/check-build-source.py
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build Linux candidate
|
||||
run: cargo build --locked --release --target x86_64-unknown-linux-gnu --bin smirk-backend-core
|
||||
- name: Stage binary and provenance
|
||||
run: python3 tools/stage-backend-candidate.py --output "$RUNNER_TEMP/backend-candidate"
|
||||
- name: Retain private candidate
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: smirk-backend-core-linux-x86_64-${{ inputs.expected_sha }}
|
||||
path: ${{ runner.temp }}/backend-candidate/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Backend CI (Gitea)
|
||||
|
||||
# GitHub remains canonical source. Builds admits an explicitly reviewed commit;
|
||||
# this workflow is the fundless CI gate, not deployment or public publication.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: never
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
EXPECTED_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: backend checks
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-bookworm@sha256:0557ac14e0d45d02ed563067b82856ca5e7aa3437fa28d98d4350ea9c3d9494a
|
||||
env:
|
||||
RUSTUP_HOME: ${{ runner.temp }}/rustup-backend-ci
|
||||
CARGO_HOME: ${{ runner.temp }}/cargo-backend-ci
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- name: Require reviewed source selectors before setup
|
||||
run: |
|
||||
test "$GITHUB_REPOSITORY" = Builds/smirk-backend-core || { echo 'This repository is not admitted for backend Builds.' >&2; exit 1; }
|
||||
test "$(git rev-parse HEAD)" = "$EXPECTED_BUILD_SHA" || { echo 'Checkout differs from the reviewed event or dispatch head.' >&2; exit 1; }
|
||||
- name: Install isolated container dependencies
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends python3 build-essential pkg-config libssl-dev postgresql postgresql-client
|
||||
- name: Admit reviewed canonical source
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then
|
||||
canonical_source=$(git rev-parse HEAD)
|
||||
else
|
||||
read -r -a parents <<< "$(git show -s --format=%P HEAD)"
|
||||
test "${#parents[@]}" -eq 2 || { echo 'Builds main requires a two-parent source wrapper.' >&2; exit 1; }
|
||||
canonical_source=${parents[1]}
|
||||
fi
|
||||
git fetch --no-tags https://github.com/Such-Software/smirk-backend-core.git "$canonical_source"
|
||||
python3 tools/check-build-source.py
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Source admission and artifact guard regressions
|
||||
run: python3 -m unittest discover -s tools/tests -v
|
||||
- name: Secret shapes
|
||||
run: ./tools/secret-scan.sh
|
||||
- name: Rust format and clippy
|
||||
run: |
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --locked --all-targets -- -D warnings
|
||||
- name: All fundless tests with disposable PostgreSQL
|
||||
run: ./tools/ci-fundless.sh
|
||||
- name: OpenAPI source agreement
|
||||
run: |
|
||||
cargo run --locked --quiet --bin gen_openapi > "$RUNNER_TEMP/openapi.json"
|
||||
diff openapi.json "$RUNNER_TEMP/openapi.json"
|
||||
- name: Embedded console source agreement
|
||||
working-directory: admin-ui
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
git diff --exit-code -- dist
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24.0'
|
||||
- name: Redacted history credential scan
|
||||
run: |
|
||||
go install github.com/zricethezav/gitleaks/v8@v8.18.4
|
||||
"$(go env GOPATH)/bin/gitleaks" detect --no-banner --redact --source .
|
||||
|
||||
supply-chain:
|
||||
name: dependency policy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
gate:
|
||||
name: backend gate
|
||||
if: always()
|
||||
needs: [verify, supply-chain]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require every mandatory check
|
||||
env:
|
||||
VERIFY_RESULT: ${{ needs.verify.result }}
|
||||
DEPENDENCY_RESULT: ${{ needs.supply-chain.result }}
|
||||
run: |
|
||||
if [ "$VERIFY_RESULT" != success ] || [ "$DEPENDENCY_RESULT" != success ]; then
|
||||
echo 'Backend gate refused: required checks failed, were cancelled, or did not run.' >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -158,3 +158,6 @@ funded or otherwise sensitive wallet seed.
|
||||
- [ ] Regenerate OpenAPI and the console bundle from their sources and check drift.
|
||||
- [ ] Verify each enabled chain against its configured source.
|
||||
- [ ] Use reviewed Fleet plan/apply procedures for company production deployments.
|
||||
|
||||
Backend CI and private Linux candidate provenance are described in
|
||||
[Building the backend](docs/operations/BUILDING.md).
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Backend builds and candidate provenance
|
||||
|
||||
> Status: implemented, enrollment pending · Updated: 2026-09-27 · Applies to: backend maintainers and release operators
|
||||
|
||||
GitHub `Such-Software/smirk-backend-core` remains canonical source. Gitea
|
||||
`Builds/smirk-backend-core` is private CI and candidate ingress. A workflow file
|
||||
in source does not prove that the repository, runner, branch protection or
|
||||
required status has been enrolled. Those are reviewed Fleet operations.
|
||||
|
||||
`.gitea/workflows/ci.yml` owns the fundless backend gate: formatting, strict
|
||||
Clippy, every integration target with disposable PostgreSQL, documentation
|
||||
tests, generated OpenAPI and embedded-console agreement, redacted credential
|
||||
scanning and dependency policy. Its final gate refuses failed, skipped or
|
||||
cancelled mandatory jobs. GitHub Actions is not the build authority.
|
||||
|
||||
`tools/ci-fundless.sh` creates a temporary PostgreSQL cluster with a private Unix
|
||||
socket and no TCP listener. It never accepts a database URL or a test-command
|
||||
override, refuses a local `.env`, supplies `TEST_DATABASE_URL`, and removes its
|
||||
cluster on exit. Without a database, ordinary `cargo test` can skip integration
|
||||
targets and is not equivalent evidence. Tests use only unfunded fixtures.
|
||||
|
||||
## Private Linux candidate
|
||||
|
||||
`.gitea/workflows/backend-build.yml` accepts only the required `expected_sha`
|
||||
input. The reviewed Fleet dispatcher verifies the admitted canonical GitHub
|
||||
commit before dispatch. The workflow checks out that exact Builds commit and
|
||||
requires a two-parent merge wrapper with a tree identical to its second parent.
|
||||
That parent is recorded as canonical source. It is fetched from the declared
|
||||
GitHub repository; an arbitrary tree with different ancestry is refused.
|
||||
|
||||
The workflow builds with Rust 1.98.0 in a digest-pinned Debian 12 container.
|
||||
`tools/stage-backend-candidate.py` refuses an unexpected binary architecture,
|
||||
missing GNU libc evidence, or a binary requiring GNU libc newer than 2.36.
|
||||
The private artifact contains the executable and `manifest.json`, which records
|
||||
source and Builds commits, source tree, workflow and lockfile digests, compiler,
|
||||
target, GNU libc requirements, executable digest and size. Artifact identity is
|
||||
`smirk-backend-core-linux-x86_64-<Builds commit>`.
|
||||
|
||||
This is a candidate, not a deployment or public release. Fleet must independently
|
||||
admit its source, workflow, artifact digest and host compatibility before a
|
||||
reviewed plan. A successful upload does not prove production health. The current
|
||||
configuration adapter does not install this binary or prepare a new host.
|
||||
|
||||
## Reusable build checklist
|
||||
|
||||
- [ ] Exact canonical source and Builds wrapper are admitted by reviewed Fleet authority.
|
||||
- [ ] Actual Gitea required CI status is green for the selected source.
|
||||
- [ ] Every fundless PostgreSQL target ran against disposable data.
|
||||
- [ ] Candidate manifest, executable digest and target match the reviewed build.
|
||||
- [ ] Host compatibility and deployment evidence are separate from build success.
|
||||
- [ ] No credential values, funded wallets or production database were used.
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
channel = "1.98.0"
|
||||
components = ["rustfmt", "clippy"]
|
||||
|
||||
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