Bump ggml-org/llama.cpp from a576442 to 51570a4 in /services/inference-worker
#441
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: bluebuild | |
| on: | |
| schedule: | |
| - cron: | |
| "00 06 * * *" | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| source-prep: | |
| name: "Stage 1: Source Prep" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 | |
| with: | |
| go-version: "1.26.5" | |
| cache: false | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Verify supported Fedora base pin | |
| run: | | |
| python3 - <<'PY' | |
| import re | |
| import yaml | |
| with open("recipes/recipe.yml", encoding="utf-8") as handle: | |
| recipe = yaml.safe_load(handle) | |
| version = str(recipe.get("image-version", "")) | |
| match = re.fullmatch(r"44@(sha256:[0-9a-f]{64})", version) | |
| if not match: | |
| raise SystemExit( | |
| "recipes/recipe.yml must use Fedora 44 with a canonical digest pin" | |
| ) | |
| print(f"Configured Fedora 44 base digest: {match.group(1)}") | |
| PY | |
| configured_digest=$(python3 -c ' | |
| import yaml | |
| with open("recipes/recipe.yml", encoding="utf-8") as handle: | |
| print(str(yaml.safe_load(handle)["image-version"]).split("@", 1)[1]) | |
| ') | |
| current_digest=$( | |
| skopeo inspect docker://ghcr.io/ublue-os/silverblue-main:44 | | |
| jq -er '.Digest' | |
| ) | |
| if [ "$configured_digest" != "$current_digest" ]; then | |
| echo "::error::Fedora 44 base tag moved. Review the new image and update the recipe digest." | |
| echo "Configured: $configured_digest" | |
| echo "Current: $current_digest" | |
| exit 1 | |
| fi | |
| - name: Verify Fedora 44 package availability | |
| run: | | |
| base_ref=$( | |
| python3 - <<'PY' | |
| import yaml | |
| with open("recipes/recipe.yml", encoding="utf-8") as handle: | |
| recipe = yaml.safe_load(handle) | |
| version = str(recipe["image-version"]) | |
| digest = version.split("@", 1)[1] | |
| print(f'{recipe["base-image"]}@{digest}') | |
| PY | |
| ) | |
| mapfile -t packages < <( | |
| python3 - <<'PY' | |
| import yaml | |
| with open("recipes/recipe.yml", encoding="utf-8") as handle: | |
| recipe = yaml.safe_load(handle) | |
| for module in recipe.get("modules", []): | |
| if module.get("type") == "rpm-ostree": | |
| for package in module.get("install", []): | |
| print(package) | |
| PY | |
| ) | |
| if [ "${#packages[@]}" -eq 0 ]; then | |
| echo "::error::Recipe contains no RPM package requirements" | |
| exit 1 | |
| fi | |
| docker pull "$base_ref" | |
| docker run --rm --entrypoint /bin/bash "$base_ref" \ | |
| -s -- "${packages[@]}" <<'BASH' | |
| set -euo pipefail | |
| dnf5 -q makecache --refresh | |
| missing=0 | |
| for package in "$@"; do | |
| if rpm -q --quiet -- "$package" || | |
| dnf5 -q repoquery --available "$package" | grep -q .; then | |
| echo "OK: ${package}" | |
| else | |
| echo "MISSING: ${package}" >&2 | |
| missing=$((missing + 1)) | |
| fi | |
| done | |
| if [ "$missing" -ne 0 ]; then | |
| echo "Fedora package resolution failed for ${missing} package(s)" >&2 | |
| exit 1 | |
| fi | |
| BASH | |
| - name: Materialize verified Go dependency trees | |
| run: | | |
| while IFS= read -r module; do | |
| service_dir=$(dirname "$module") | |
| echo "Vendoring ${service_dir}" | |
| ( | |
| cd "$service_dir" | |
| go mod verify | |
| go mod vendor | |
| ) | |
| done < <(find services -mindepth 2 -maxdepth 2 -name go.mod -print | sort) | |
| - name: Materialize Python wheelhouse | |
| run: | | |
| mkdir -p vendor/wheels | |
| find vendor/wheels -maxdepth 1 -type f -name '*.whl' -delete | |
| python3 -m pip download \ | |
| --dest vendor/wheels \ | |
| --require-hashes \ | |
| --only-binary=:all: \ | |
| -r vendor/application-requirements.lock | |
| ( | |
| cd vendor/wheels | |
| find . -maxdepth 1 -type f -name '*.whl' -print0 | | |
| sort -z | | |
| xargs -0 sha256sum > SHA256SUMS | |
| test -s SHA256SUMS | |
| sha256sum --check --strict SHA256SUMS | |
| ) | |
| - name: Fetch checksum-pinned external source | |
| run: | | |
| python3 - <<'PY' | |
| import hashlib | |
| import io | |
| import pathlib | |
| import shutil | |
| import tarfile | |
| import urllib.request | |
| import yaml | |
| with open(".upstreams.lock.yaml", encoding="utf-8") as handle: | |
| lock = yaml.safe_load(handle) | |
| for name, entry in sorted(lock.get("upstreams", {}).items()): | |
| commit = str(entry["pinned_commit"]) | |
| expected = str(entry["archive_sha256"]) | |
| if len(commit) != 40 or len(expected) != 64: | |
| raise SystemExit(f"{name}: invalid source pin") | |
| url = entry["upstream_url"].removesuffix(".git") | |
| archive_url = f"{url}/archive/{commit}.tar.gz" | |
| print(f"Fetching {name}@{commit}") | |
| with urllib.request.urlopen(archive_url, timeout=60) as response: | |
| content = response.read() | |
| actual = hashlib.sha256(content).hexdigest() | |
| if actual != expected: | |
| raise SystemExit( | |
| f"{name}: archive mismatch: expected {expected}, got {actual}" | |
| ) | |
| destination = pathlib.Path(entry["local_path"]) | |
| shutil.rmtree(destination, ignore_errors=True) | |
| destination.mkdir(parents=True) | |
| with tarfile.open(fileobj=io.BytesIO(content), mode="r:gz") as archive: | |
| members = archive.getmembers() | |
| prefix = members[0].name.split("/", 1)[0] + "/" | |
| for member in members: | |
| if member.issym() or member.islnk(): | |
| raise SystemExit(f"{name}: archive contains a link") | |
| if not member.name.startswith(prefix): | |
| raise SystemExit(f"{name}: malformed archive root") | |
| relative = pathlib.PurePosixPath(member.name.removeprefix(prefix)) | |
| if relative.is_absolute() or ".." in relative.parts: | |
| raise SystemExit(f"{name}: unsafe archive path") | |
| member.name = relative.as_posix() | |
| if member.name and member.name != ".": | |
| archive.extract(member, destination, filter="data") | |
| PY | |
| - name: Download and verify llama.cpp tarball | |
| run: | | |
| mkdir -p .source-prep | |
| # Read pinned version + checksum from build-services.sh | |
| LLAMA_CPP_VERSION=$(grep -oP 'LLAMA_CPP_VERSION:-\K[^}]+' files/scripts/build-services.sh | head -1) | |
| LLAMA_CPP_SHA256=$(grep -oP 'LLAMA_CPP_SHA256:-\K[^}]+' files/scripts/build-services.sh | head -1) | |
| echo "Downloading llama.cpp ${LLAMA_CPP_VERSION}..." | |
| TARBALL="llama-cpp-${LLAMA_CPP_VERSION}.tar.gz" | |
| curl -fsSL -o "/tmp/${TARBALL}" \ | |
| "https://github.com/ggml-org/llama.cpp/archive/refs/tags/${LLAMA_CPP_VERSION}.tar.gz" | |
| echo "Verifying checksum..." | |
| ACTUAL=$(sha256sum "/tmp/${TARBALL}" | awk '{print $1}') | |
| if [ "$ACTUAL" != "$LLAMA_CPP_SHA256" ]; then | |
| echo "::error::llama.cpp checksum mismatch: expected ${LLAMA_CPP_SHA256}, got ${ACTUAL}" | |
| echo "Update LLAMA_CPP_SHA256 in build-services.sh if the version was bumped." | |
| exit 1 | |
| fi | |
| echo "OK: llama.cpp checksum verified" | |
| echo "TARBALL_SHA256=${ACTUAL}" >> "$GITHUB_ENV" | |
| echo "LLAMA_CPP_VERSION=${LLAMA_CPP_VERSION}" >> "$GITHUB_ENV" | |
| mv "/tmp/${TARBALL}" ".source-prep/llama-cpp-staged.tar.gz" | |
| - name: Emit SOURCE_PREP_MANIFEST.json | |
| run: | | |
| python3 -c " | |
| import json, hashlib, os | |
| from pathlib import Path | |
| from datetime import datetime | |
| def digest(path): | |
| with open(path, 'rb') as handle: | |
| return hashlib.sha256(handle.read()).hexdigest() | |
| manifest = { | |
| 'schema_version': 1, | |
| 'timestamp': datetime.utcnow().isoformat() + 'Z', | |
| 'commit_sha': os.environ.get('GITHUB_SHA', 'unknown'), | |
| 'llama_cpp_version': os.environ.get('LLAMA_CPP_VERSION', 'unknown'), | |
| 'llama_cpp_tarball_sha256': os.environ.get('TARBALL_SHA256', 'unknown'), | |
| } | |
| required_files = [ | |
| Path('vendor/wheels/SHA256SUMS'), | |
| Path('vendor/application-requirements.lock'), | |
| Path('.upstreams.lock.yaml'), | |
| ] | |
| missing = [str(path) for path in required_files if not path.is_file()] | |
| if missing: | |
| raise SystemExit(f'missing source-prep inputs: {missing}') | |
| wheel_lines = [ | |
| line for line in required_files[0].read_text().splitlines() if line.strip() | |
| ] | |
| if not wheel_lines: | |
| raise SystemExit('wheelhouse checksum manifest is empty') | |
| manifest['wheelhouse_sha256sums_digest'] = digest(required_files[0]) | |
| manifest['application_requirements_lock_digest'] = digest(required_files[1]) | |
| manifest['upstreams_lock_digest'] = digest(required_files[2]) | |
| manifest['wheel_count'] = len(wheel_lines) | |
| manifest['application_dependency_mode'] = 'staged-offline' | |
| with open('.source-prep/SOURCE_PREP_MANIFEST.json', 'w') as f: | |
| json.dump(manifest, f, indent=2) | |
| f.write('\n') | |
| print('--- SOURCE_PREP_MANIFEST.json ---') | |
| print(json.dumps(manifest, indent=2)) | |
| " | |
| - name: Upload staged artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: source-prep | |
| path: | | |
| .source-prep/ | |
| .upstreams.lock.yaml | |
| upstreams/ | |
| vendor/wheels/ | |
| services/*/vendor/ | |
| if-no-files-found: error | |
| retention-days: 1 | |
| bluebuild_pr: | |
| name: "Stage 2: Build Custom Image (Unprivileged PR)" | |
| if: github.event_name == 'pull_request' | |
| needs: [source-prep] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| recipe: | |
| - recipe.yml | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download verified source-prep inputs | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: source-prep | |
| path: . | |
| - name: Build Custom Image Without Publishing | |
| uses: blue-build/github-action@24d146df25adc2cf579e918efe2d9bff6adea408 # v1.11.1 | |
| with: | |
| recipe: ${{ matrix.recipe }} | |
| skip_checkout: true | |
| verify_install: true | |
| # The action declares this input required, but push=false never signs. | |
| # Pass an explicit non-secret empty value to keep forked PRs isolated. | |
| cosign_private_key: "" | |
| push: false | |
| registry_token: "" | |
| pr_event_number: ${{ github.event.number }} | |
| maximize_build_space: true | |
| bluebuild_publish: | |
| name: "Stage 2: Build, Sign, and Publish Custom Image" | |
| if: github.event_name != 'pull_request' | |
| needs: [source-prep] | |
| runs-on: ubuntu-latest | |
| environment: release | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| recipe: | |
| # BlueBuild resolves recipe paths relative to the recipes/ directory. | |
| # "recipe.yml" maps to "recipes/recipe.yml" by convention. | |
| - recipe.yml | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download verified source-prep inputs | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: source-prep | |
| path: . | |
| - name: Build Custom Image | |
| id: build | |
| uses: blue-build/github-action@24d146df25adc2cf579e918efe2d9bff6adea408 # v1.11.1 | |
| with: | |
| recipe: ${{ matrix.recipe }} | |
| skip_checkout: true | |
| verify_install: true | |
| cosign_private_key: ${{ secrets.SIGNING_SECRET }} | |
| push: true | |
| registry_token: ${{ github.token }} | |
| pr_event_number: ${{ github.event.number }} | |
| maximize_build_space: true | |
| - name: Set lowercase image ref | |
| if: github.event_name != 'pull_request' | |
| run: echo "IMAGE_REF=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" | |
| # Publish the image digest so users can pin installs to an exact build. | |
| # The digest appears in the workflow summary and as an artifact. | |
| - name: Resolve and verify the built image | |
| if: github.event_name != 'pull_request' | |
| id: digest | |
| run: | | |
| inspect_json=$(skopeo inspect "docker://${IMAGE_REF}:latest") | |
| digest=$(jq -er '.Digest' <<<"$inspect_json") | |
| revision=$(jq -er '.Labels["org.opencontainers.image.revision"]' <<<"$inspect_json") | |
| if ! [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then | |
| echo "::error::Registry returned a non-canonical image digest" | |
| exit 1 | |
| fi | |
| if [ "$revision" != "$GITHUB_SHA" ]; then | |
| echo "::error::The published image was built from ${revision}, not ${GITHUB_SHA}" | |
| exit 1 | |
| fi | |
| pinned_ref="${IMAGE_REF}@${digest}" | |
| cosign verify --key cosign.pub "$pinned_ref" >/dev/null | |
| echo "$digest" > IMAGE_DIGEST | |
| echo "$pinned_ref" > IMAGE_REF_PINNED | |
| { | |
| echo "digest=$digest" | |
| echo "pinned_ref=$pinned_ref" | |
| echo "image_ref=$IMAGE_REF" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "## Verified image" | |
| echo "" | |
| echo "Source commit: \`${GITHUB_SHA}\`" | |
| echo "Pinned image: \`${pinned_ref}\`" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Generate final-image SBOM | |
| if: github.event_name != 'pull_request' | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| image: ${{ steps.digest.outputs.pinned_ref }} | |
| format: cyclonedx-json | |
| output-file: sbom.cdx.json | |
| upload-artifact: false | |
| - name: Validate final-image SBOM | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| component_count=$(jq -er '(.components // []) | length' sbom.cdx.json) | |
| if [ "$component_count" -lt 100 ]; then | |
| echo "::error::Final-image SBOM is implausibly small (${component_count} components)" | |
| exit 1 | |
| fi | |
| jq -e '.bomFormat == "CycloneDX" and (.metadata.component.name | length > 0)' \ | |
| sbom.cdx.json >/dev/null | |
| - name: Extract release-bound integrity baseline | |
| if: github.event_name != 'pull_request' | |
| env: | |
| PINNED_REF: ${{ steps.digest.outputs.pinned_ref }} | |
| run: | | |
| docker pull "$PINNED_REF" | |
| docker run --rm --entrypoint /bin/bash "$PINNED_REF" -c \ | |
| 'rpm -q cosign && command -v cosign >/dev/null && cosign version' | |
| docker run --rm --entrypoint /bin/bash "$PINNED_REF" -c ' | |
| for package in golang cmake gcc-c++ gcc git git-core git-core-doc perl-Git python3-pip; do | |
| if rpm -q --quiet -- "$package"; then | |
| echo "FATAL: build-only package remains in final image: $package" >&2 | |
| exit 1 | |
| fi | |
| done | |
| for command_name in go cmake gcc g++ git pip pip3; do | |
| if command -v "$command_name" >/dev/null 2>&1; then | |
| echo "FATAL: build-only command remains in final image: $command_name" >&2 | |
| exit 1 | |
| fi | |
| done | |
| ' | |
| container_id=$(docker create "$PINNED_REF") | |
| cleanup() { | |
| docker rm -f "$container_id" >/dev/null 2>&1 || true | |
| } | |
| trap cleanup EXIT | |
| docker cp \ | |
| "${container_id}:/usr/share/secure-ai/integrity/release-baseline.json" \ | |
| RELEASE_BASELINE.json | |
| mkdir -p image-root/usr/lib/systemd image-root/usr image-root/etc | |
| docker cp "${container_id}:/usr/lib/systemd/system" \ | |
| image-root/usr/lib/systemd/ | |
| docker cp "${container_id}:/usr/libexec" image-root/usr/ | |
| docker cp "${container_id}:/etc/greenboot" image-root/etc/ | |
| python3 .github/scripts/check-assembled-execstart.py \ | |
| --rootfs image-root | |
| jq -e \ | |
| --arg source_commit "$GITHUB_SHA" \ | |
| '.version == 1 | |
| and .source_commit == $source_commit | |
| and (.files | type == "array" and length > 0) | |
| and all(.files[]; | |
| (.path | startswith("/")) | |
| and (.sha256 | test("^[0-9a-f]{64}$")) | |
| and (.size | type == "number" and . >= 0))' \ | |
| RELEASE_BASELINE.json >/dev/null | |
| - name: Create and attach image attestations | |
| if: github.event_name != 'pull_request' | |
| env: | |
| COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }} | |
| IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} | |
| PINNED_REF: ${{ steps.digest.outputs.pinned_ref }} | |
| run: | | |
| baseline_sha256=$(sha256sum RELEASE_BASELINE.json | awk '{print $1}') | |
| base_digest=$( | |
| python3 - <<'PY' | |
| import re | |
| from pathlib import Path | |
| recipe = Path("recipes/recipe.yml").read_text(encoding="utf-8") | |
| match = re.search( | |
| r'^image-version:\s*["\x27]?44@(sha256:[0-9a-f]{64})["\x27]?\s*$', | |
| recipe, | |
| re.MULTILINE, | |
| ) | |
| if not match: | |
| raise SystemExit("unable to derive immutable Fedora base digest") | |
| print(match.group(1)) | |
| PY | |
| ) | |
| jq -n \ | |
| --arg commit "$GITHUB_SHA" \ | |
| --arg repository "https://github.com/${GITHUB_REPOSITORY}" \ | |
| --arg workflow "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | |
| --arg recipe "recipes/recipe.yml" \ | |
| --arg base_digest "$base_digest" \ | |
| --arg baseline_sha256 "$baseline_sha256" \ | |
| '{ | |
| buildDefinition: { | |
| buildType: "https://blue-build.org/secai-os/v1", | |
| externalParameters: { | |
| source_repository: $repository, | |
| source_commit: $commit, | |
| recipe: $recipe | |
| }, | |
| internalParameters: {}, | |
| resolvedDependencies: [ | |
| {uri: "pkg:oci/ublue-os/silverblue-main@44", digest: {sha256: ($base_digest | sub("^sha256:"; ""))}}, | |
| {uri: "file:/usr/share/secure-ai/integrity/release-baseline.json", digest: {sha256: $baseline_sha256}} | |
| ] | |
| }, | |
| runDetails: { | |
| builder: {id: $workflow}, | |
| metadata: {invocationId: $workflow} | |
| } | |
| }' > image-provenance.json | |
| cosign attest --yes --type cyclonedx \ | |
| --predicate sbom.cdx.json \ | |
| --key env://COSIGN_PRIVATE_KEY \ | |
| "$PINNED_REF" | |
| cosign attest --yes --type slsaprovenance \ | |
| --predicate image-provenance.json \ | |
| --key env://COSIGN_PRIVATE_KEY \ | |
| "$PINNED_REF" | |
| cosign verify-attestation --type cyclonedx --key cosign.pub \ | |
| "$PINNED_REF" >/dev/null | |
| cosign verify-attestation --type slsaprovenance --key cosign.pub \ | |
| "$PINNED_REF" >/dev/null | |
| - name: Generate GitHub image provenance | |
| if: github.event_name != 'pull_request' | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ${{ steps.digest.outputs.image_ref }} | |
| subject-digest: ${{ steps.digest.outputs.digest }} | |
| push-to-registry: true | |
| - name: Upload image digest artifact | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: image-digest | |
| path: | | |
| IMAGE_DIGEST | |
| IMAGE_REF_PINNED | |
| RELEASE_BASELINE.json | |
| sbom.cdx.json | |
| image-provenance.json | |
| if-no-files-found: error | |
| retention-days: 30 | |
| bluebuild: | |
| name: "Stage 2: BlueBuild Gate" | |
| if: >- | |
| always() && | |
| ((github.event_name == 'pull_request' && | |
| needs.bluebuild_pr.result == 'success') || | |
| (github.event_name != 'pull_request' && | |
| needs.bluebuild_publish.result == 'success')) | |
| needs: [bluebuild_pr, bluebuild_publish] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Confirm the event-appropriate build completed | |
| run: echo "BlueBuild completed without crossing the PR trust boundary." | |
| smoke-test: | |
| name: Tier 1 Smoke Test (Artifact Verification) | |
| needs: [bluebuild] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install locked validation dependencies | |
| run: python -m pip install --require-hashes -r requirements-ci.lock | |
| - name: Validate recipe systemd units | |
| run: | | |
| python3 .github/scripts/check-assembled-execstart.py | |
| python3 -c " | |
| import yaml, sys | |
| with open('recipes/recipe.yml') as f: | |
| recipe = yaml.safe_load(f) | |
| for module in recipe.get('modules', []): | |
| if module.get('type') != 'systemd': | |
| continue | |
| enabled = set(module.get('system', {}).get('enabled', [])) | |
| disabled = set(module.get('system', {}).get('disabled', [])) | |
| overlap = enabled & disabled | |
| if overlap: | |
| print(f'FAIL: services in both enabled and disabled: {overlap}') | |
| sys.exit(1) | |
| # Diffusion must be disabled by default | |
| if 'secure-ai-diffusion.service' in enabled: | |
| print('FAIL: secure-ai-diffusion.service must be in disabled list') | |
| sys.exit(1) | |
| if 'secure-ai-diffusion.service' not in disabled: | |
| print('FAIL: secure-ai-diffusion.service missing from disabled list') | |
| sys.exit(1) | |
| # Core services must be enabled | |
| core = [ | |
| 'secure-ai-registry.service', | |
| 'secure-ai-tool-firewall.service', | |
| 'secure-ai-ui.service', | |
| 'secure-ai-policy-engine.service', | |
| 'nftables.service', | |
| ] | |
| for svc in core: | |
| if svc not in enabled: | |
| print(f'FAIL: core service {svc} not in enabled list') | |
| sys.exit(1) | |
| print(f'OK: {len(enabled)} enabled, {len(disabled)} disabled, no overlap') | |
| " | |
| - name: Validate YAML config files | |
| run: | | |
| python3 -c " | |
| import yaml, sys, glob | |
| errors = 0 | |
| for pattern in ['files/system/etc/secure-ai/**/*.yaml', 'recipes/*.yml']: | |
| for f in glob.glob(pattern, recursive=True): | |
| try: | |
| with open(f) as fh: | |
| yaml.safe_load(fh) | |
| print(f'OK: {f}') | |
| except Exception as e: | |
| print(f'FAIL: {f}: {e}') | |
| errors += 1 | |
| sys.exit(errors) | |
| " | |
| - name: Verify build script is hermetic-ready | |
| run: | | |
| echo "=== Checking build-services.sh for network fetch patterns ===" | |
| SCRIPT="files/scripts/build-services.sh" | |
| # Must have hermetic guard | |
| grep -q "HERMETIC_BUILD" "$SCRIPT" || { echo "FAIL: no HERMETIC_BUILD guard"; exit 1; } | |
| echo "OK: HERMETIC_BUILD guard present" | |
| # Must have LLAMA_CPP_SHA256 | |
| grep -q "LLAMA_CPP_SHA256" "$SCRIPT" || { echo "FAIL: no LLAMA_CPP_SHA256"; exit 1; } | |
| echo "OK: LLAMA_CPP_SHA256 checksum present" | |
| # Must have GOPROXY=off in hermetic mode | |
| grep -q "GOPROXY=off" "$SCRIPT" || { echo "FAIL: no GOPROXY=off"; exit 1; } | |
| echo "OK: GOPROXY=off in hermetic mode" | |
| # Must not have --clone in locate_source calls | |
| if grep -n "locate_source.*--clone" "$SCRIPT"; then | |
| echo "FAIL: locate_source still uses --clone" | |
| exit 1 | |
| fi | |
| echo "OK: no --clone in locate_source" | |
| # Must not have dnf install | |
| if grep -n "dnf install" "$SCRIPT" | grep -v "^#" | grep -v "dnf remove"; then | |
| echo "FAIL: dnf install found in build script" | |
| exit 1 | |
| fi | |
| echo "OK: no dnf install" | |
| echo "=== Build script hermetic checks passed ===" | |
| - name: Verify systemd units use wrappers | |
| run: | | |
| echo "=== Checking systemd units ===" | |
| UNITS_DIR="files/system/usr/lib/systemd/system" | |
| # UI must use wrapper, not python3 directly | |
| if grep -q "ExecStart=/usr/bin/python3" "${UNITS_DIR}/secure-ai-ui.service"; then | |
| echo "FAIL: UI service still uses python3 directly" | |
| exit 1 | |
| fi | |
| echo "OK: UI uses wrapper" | |
| # Diffusion must not use python3 directly | |
| if grep -q "ExecStart=/usr/bin/python3" "${UNITS_DIR}/secure-ai-diffusion.service"; then | |
| echo "FAIL: Diffusion service still uses python3 directly" | |
| exit 1 | |
| fi | |
| echo "OK: Diffusion uses wrapper/placeholder" | |
| echo "=== Systemd unit checks passed ===" |