ci: add exact-source Gitea backend builds

This commit is contained in:
2026-09-27 10:20:32 -04:00
parent d90a57bb11
commit 6ec84f3e4d
9 changed files with 567 additions and 1 deletions
+70
View File
@@ -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
+110
View File
@@ -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
+3
View File
@@ -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).
+51
View File
@@ -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
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "stable"
channel = "1.98.0"
components = ["rustfmt", "clippy"]
+72
View File
@@ -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")
+46
View File
@@ -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
+80
View File
@@ -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")
+134
View File
@@ -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()