name: Build SilverMetal Linux ISO (reproducibility-gated) # M1.1 exit-criterion check. Builds the ISO twice from a clean checkout in # isolated directories and gates on byte-identical SHA256. On a tag push, the # verified ISO and its SHA256SUMS are attached to a Gitea release. # # The release-upload pattern (create-if-not-exists then attach asset) is # lifted from SilverLABS/SilverVPN/.gitea/workflows/build-linux-client.yaml # lines 77-117. Keep them in sync if either changes. on: push: branches: [main] paths: - 'linux/**' - 'shared/branding/linux-iso-meta.yaml' - '.gitea/workflows/build-iso-linux.yaml' tags: - 'v*' pull_request: branches: [main] paths: - 'linux/**' - 'shared/branding/linux-iso-meta.yaml' - '.gitea/workflows/build-iso-linux.yaml' workflow_dispatch: # Two reproducibility-gated builds in flight at once would just compete for # loop devices on the privileged runner. Serialise per ref. concurrency: group: build-iso-linux-${{ github.ref }} cancel-in-progress: true jobs: build-and-verify: # Self-hosted, privileged-capable. Setup procedure documented in # linux/build/README.md ("Self-hosted runner setup"). runs-on: silvermetal-builder timeout-minutes: 240 steps: - name: Checkout (with submodules) uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 # need history so SOURCE_DATE_EPOCH = HEAD commit time - name: Show pinned inputs run: | set -eu echo "commit=$(git rev-parse HEAD)" cat linux/build/config/snapshot-pin.env echo "builder image:" grep -E '^FROM |^ARG APT_SNAPSHOT_URL' linux/build/docker/Dockerfile.builder - name: Build A env: BUILD_DIR: ${{ github.workspace }}/build-a run: linux/build/scripts/build.sh - name: Build B (clean second build) env: BUILD_DIR: ${{ github.workspace }}/build-b run: linux/build/scripts/build.sh - name: Compare SHA256 id: compare run: | set -eu A=$(sha256sum "${{ github.workspace }}/build-a"/*.iso | cut -d' ' -f1) B=$(sha256sum "${{ github.workspace }}/build-b"/*.iso | cut -d' ' -f1) echo "A=${A}" echo "B=${B}" echo "iso_sha256=${A}" >> "${GITHUB_OUTPUT}" if [ "${A}" != "${B}" ]; then echo "::error::ISO SHA256 mismatch — A=${A} B=${B}" ISO_A="$(ls "${{ github.workspace }}/build-a"/*.iso | head -n1)" \ ISO_B="$(ls "${{ github.workspace }}/build-b"/*.iso | head -n1)" \ REPORT_DIR="${{ github.workspace }}/divergence" \ linux/build/scripts/diagnose-divergence.sh exit 1 fi echo "Reproducibility gate PASSED at ${A}" - name: Upload divergence report on failure if: failure() uses: actions/upload-artifact@v3 with: name: divergence-report-${{ github.run_id }} path: ${{ github.workspace }}/divergence if-no-files-found: ignore retention-days: 14 - name: Stage release artefacts if: startsWith(github.ref, 'refs/tags/') run: | set -eu mkdir -p release cp "${{ github.workspace }}/build-a"/*.iso release/ cp "${{ github.workspace }}/build-a"/SHA256SUMS release/ cp "${{ github.workspace }}/build-a"/BUILD_INFO release/ cp "${{ github.workspace }}/build-a"/snapshot-pin.env release/snapshot-pin.env ls -la release/ - name: Upload to Gitea release (tag only) if: startsWith(github.ref, 'refs/tags/') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu TAG="${{ github.ref_name }}" API="${{ github.server_url }}/api/v1" REPO="${{ github.repository }}" # Create-if-not-exists, then attach assets. Pattern lifted from # SilverLABS/SilverVPN build-linux-client.yaml:89-115. RELEASE_ID=$(curl -s "${API}/repos/${REPO}/releases/tags/${TAG}" \ -H "Authorization: token ${GITHUB_TOKEN}" \ | jq -r '.id // empty' 2>/dev/null || true) if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "0" ]; then RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API}/repos/${REPO}/releases" \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"${TAG}\",\"name\":\"SilverMetal Linux ${TAG}\",\"body\":\"Reproducibility-verified ISO. SHA256 in SHA256SUMS asset.\",\"draft\":false,\"prerelease\":true}") HTTP_CODE=$(echo "${RESPONSE}" | tail -1) BODY=$(echo "${RESPONSE}" | sed '$d') if [ "${HTTP_CODE}" -ge 400 ]; then echo "ERROR: Failed to create release (${HTTP_CODE}): ${BODY}" exit 1 fi RELEASE_ID=$(echo "${BODY}" | jq -r '.id') echo "Created release ID: ${RELEASE_ID}" else echo "Found existing release ID: ${RELEASE_ID}" fi for asset in release/*; do name=$(basename "${asset}") echo "Attaching ${name}" curl -sf -X POST \ "${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${name}" \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/octet-stream" \ --data-binary "@${asset}" done