-
-
Notifications
You must be signed in to change notification settings - Fork 1
126 lines (122 loc) · 5.39 KB
/
Copy pathbuildroot-guest.yml
File metadata and controls
126 lines (122 loc) · 5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
name: buildroot-guest
on:
workflow_dispatch:
workflow_call:
push:
branches: [main]
paths:
- "scripts/buildroot/**"
- ".github/workflows/buildroot-guest.yml"
permissions:
contents: read
jobs:
reproduce:
name: reproduce-${{ matrix.profile }}-${{ matrix.slot }}
runs-on: ubuntu-24.04
timeout-minutes: 300
strategy:
fail-fast: false
matrix:
profile: [linux, node, python]
slot: [a, b]
env:
# Matrix slots have isolated runners. Node records its cross-toolchain
# path, so independent builds must use the same absolute workspace.
PYPROC_BUILDROOT_WORKSPACE: ${{ github.workspace }}/../pyproc-buildroot-${{ matrix.profile }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build pinned Buildroot guest and legal material
run: npm run assets:buildroot -- --profile ${{ matrix.profile }}
- name: Normalize evidence paths
id: evidence
run: |
echo "dist=$(realpath "$PYPROC_BUILDROOT_WORKSPACE/dist")" >> "$GITHUB_OUTPUT"
echo "legal=$(realpath "$PYPROC_BUILDROOT_WORKSPACE/output/legal-info")" >> "$GITHUB_OUTPUT"
- name: Upload independent build evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: pyproc-buildroot-guest-${{ matrix.profile }}-${{ matrix.slot }}
path: ${{ steps.evidence.outputs.dist }}/
if-no-files-found: error
retention-days: 30
- name: Upload complete legal material
if: matrix.slot == 'a'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: pyproc-buildroot-legal-info-${{ matrix.profile }}
path: ${{ steps.evidence.outputs.legal }}/
if-no-files-found: error
compression-level: 0
retention-days: 30
verify:
name: verify-byte-identical-${{ matrix.profile }}
needs: reproduce
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
profile: [linux, node, python]
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: pyproc-buildroot-guest-${{ matrix.profile }}-a
path: .cache/repro/a
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: pyproc-buildroot-guest-${{ matrix.profile }}-b
path: .cache/repro/b
- name: Verify image, manifest, SBOM, and legal gate
run: |
set -euo pipefail
cmp .cache/repro/a/build-manifest.json .cache/repro/b/build-manifest.json
cmp .cache/repro/a/buildroot.cyclonedx.json .cache/repro/b/buildroot.cyclonedx.json
mkdir -p .cache/repro/verified
cp .cache/repro/a/* .cache/repro/verified/
node --input-type=module <<'NODE'
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const base = ".cache/repro/verified";
const manifest = JSON.parse(await readFile(`${base}/build-manifest.json`, "utf8"));
const left = await readFile(`.cache/repro/a/${manifest.output.name}`);
const right = await readFile(`.cache/repro/b/${manifest.output.name}`);
if (!left.equals(right)) throw new Error("independent guest image bytes differ");
const image = await readFile(`${base}/${manifest.output.name}`);
const actualSha256 = createHash("sha256").update(image).digest("hex");
if (actualSha256 !== manifest.output.sha256) throw new Error("verified image SHA-256 mismatch");
if (image.byteLength !== manifest.output.byteLength) throw new Error("verified image byteLength mismatch");
if (manifest.evidence.legalWarnings.length) throw new Error("verified legal-info warnings are not empty");
if (manifest.runtime) {
const expectedVersion = manifest.runtime.name === "node"
? `v${manifest.runtime.version}`
: manifest.runtime.version;
if (!manifest.runtime.name || manifest.runtimeOracle?.version !== expectedVersion) {
throw new Error(`verified ${manifest.runtime.name} runtime oracle mismatch`);
}
if (manifest.runtimeOracle.sha256 !== manifest.runtime.oracle.sha256) {
throw new Error(`verified ${manifest.runtime.name} workload digest mismatch`);
}
}
const receipt = {
schemaVersion: 1,
recipe: manifest.recipe,
githubRunId: process.env.GITHUB_RUN_ID,
headSha: process.env.GITHUB_SHA,
independentBuilds: ["a", "b"],
byteIdentical: true,
output: manifest.output,
};
await writeFile(`${base}/reproducibility-manifest.json`, `${JSON.stringify(receipt, null, 2)}\n`);
NODE
- name: Upload verified reproducible guest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: pyproc-buildroot-guest-verified-${{ matrix.profile }}
path: .cache/repro/verified/
if-no-files-found: error
retention-days: 90