Repository navigation
206 lines (180 loc) · 8.62 KB
/
Copy pathrelease.yml
File metadata and controls
206 lines (180 loc) · 8.62 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
defaults:
run:
shell: bash
jobs:
release:
name: build + publish opclib
runs-on: ubuntu-latest
# Job-level so the signing material is visible to step-level `if:`
# expressions. The `secrets` context is NOT available in `steps.*.if`, and a
# step cannot read its own `env:` block there either — the previous
# `if: env.OPCLIB_SIGNING_KEY != ''` on the restore step therefore always
# evaluated false and signing could never run.
env:
OPCLIB_SIGNING_KEY: ${{ secrets.OPCLIB_SIGNING_KEY }}
OPCLIB_KEY_ID: ${{ vars.OPCLIB_KEY_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.0
# npm runs prepare scripts respecting topological dep order; bun
# parallelises and races on nested github: peer-deps.
- name: Install dependencies
run: npm install --no-package-lock --no-save
- name: Typecheck
run: bun run typecheck
- name: Test
run: bun test
- name: Validate library source (strict / release)
run: bun tools/validate.ts --release --strict
# The three gates below run on every PR via validate.yml. They were absent
# here, which made the release path weaker than the PR path — a bad 3D
# orientation or a dead datasheet link could ship in a tagged release
# while being blocked on the PR that introduced it.
- name: 3D orientation gate
run: bun tools/audit-3d-placement.ts --release
- name: Component audit (headless)
run: bun tools/audit-components.ts --no-render
# Structural only — no network. Manufacturer WAFs block datacenter egress,
# so reachability cannot be a gate. Run `--network` locally for link rot.
- name: Check datasheet links (structural)
run: bun tools/check-datasheet-links.ts
# A tagged release MUST be signed: OpenPCB/scripts/fetch-core-library.ts
# verifies the Ed25519 signature against resources/keys/*.pub before it
# will install a pack. Publishing unsigned silently breaks that path, so
# fail loudly here instead of degrading.
- name: Require signing material
run: |
MISSING=""
[ -n "${OPCLIB_SIGNING_KEY:-}" ] || MISSING="${MISSING} secrets.OPCLIB_SIGNING_KEY"
[ -n "${OPCLIB_KEY_ID:-}" ] || MISSING="${MISSING} vars.OPCLIB_KEY_ID"
if [ -n "$MISSING" ]; then
echo "::error::Cannot publish an unsigned release. Missing:${MISSING}"
exit 1
fi
echo "Signing material present (keyId=${OPCLIB_KEY_ID})"
- name: Restore signing key
run: |
install -m 700 -d "$RUNNER_TEMP/keys"
KEY_PATH="$RUNNER_TEMP/keys/opclib-signing.pem"
printf '%s' "$OPCLIB_SIGNING_KEY" > "$KEY_PATH"
chmod 600 "$KEY_PATH"
echo "OPCLIB_SIGN_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV"
- name: Pack release
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "Packing version ${VERSION} (keyId=${OPCLIB_KEY_ID})"
bun tools/pack.ts --version="${VERSION}" \
--sign-key="${OPCLIB_SIGN_KEY_PATH}" --key-id="${OPCLIB_KEY_ID}"
# GLB-only core pack (the app renders GLB; STEP is MCAD-export only and
# was over half the payload) plus the STEP companion archive. Written to
# a separate directory so it does not overwrite the full pack above,
# which keeps STEP embedded for consumers that want a single artifact.
- name: Pack GLB-only core + STEP companion
run: |
VERSION="${GITHUB_REF_NAME#v}"
bun tools/pack.ts --version="${VERSION}" --no-step --out=dist/glb-only \
--sign-key="${OPCLIB_SIGN_KEY_PATH}" --key-id="${OPCLIB_KEY_ID}"
mv "dist/glb-only/openpcb-core-library-${VERSION}.opclib" \
"dist/openpcb-core-library-${VERSION}-glb.opclib"
mv "dist/glb-only/openpcb-core-library-${VERSION}-step.zip" dist/
rmdir dist/glb-only
- name: Generate SHA256SUMS
run: |
VERSION="${GITHUB_REF_NAME#v}"
cd dist
sha256sum \
"openpcb-core-library-${VERSION}.opclib" \
"openpcb-core-library-${VERSION}-glb.opclib" \
"openpcb-core-library-${VERSION}-step.zip" > SHA256SUMS
cat SHA256SUMS
# Verifies BOTH published packs, and checks the Ed25519 signature against
# the committed keys/openpcb-core.pub — the same key the desktop app ships
# in resources/keys/. That catches a CI signing key that has drifted from
# the published public key BEFORE the release goes out, rather than as an
# install failure on every user's machine.
- name: Verify packages
run: |
VERSION="${GITHUB_REF_NAME#v}"
VERIFY_SCRIPT="$(mktemp ./verify-opclib.XXXXXX.mjs)"
trap 'rm -f "$VERIFY_SCRIPT"' EXIT
cat > "$VERIFY_SCRIPT" <<'EOF'
import { readOpclibFromPath, verifyManifest } from "@openpcb/opclib-pack";
import { unzipSync } from "fflate";
import { readFileSync } from "node:fs";
const version = process.env.VERSION;
const keyId = process.env.OPCLIB_KEY_ID;
if (!version || !keyId) throw new Error("missing env");
const publicKey = readFileSync("keys/openpcb-core.pub", "utf8");
const forbidden = [/^apps\//, /^packages\//, /\.kicad_sym$/, /\.kicad_mod$/, /^node_modules\//, /^\.playwright-cli\//, /^\.github\/workflows\//];
for (const [label, artifact] of [
["full", `dist/openpcb-core-library-${version}.opclib`],
["glb-only", `dist/openpcb-core-library-${version}-glb.opclib`],
]) {
const pkg = await readOpclibFromPath(artifact);
const manifest = pkg.manifest;
if (manifest.library.id !== "openpcb.core") throw new Error(`${label}: invalid library id`);
if (manifest.library.version !== version) throw new Error(`${label}: version mismatch`);
if (!/^[a-f0-9]{64}$/.test(manifest.integrity.packageSha256)) throw new Error(`${label}: invalid package sha`);
if (!manifest.signature) throw new Error(`${label}: pack is unsigned`);
if (manifest.signature.keyId !== keyId) {
throw new Error(`${label}: signed with keyId ${manifest.signature.keyId}, expected ${keyId}`);
}
const verdict = verifyManifest(manifest, {
resolveKey: (id) => (id === manifest.signature.keyId ? publicKey : undefined),
});
if (!verdict.valid) {
throw new Error(`${label}: signature does not verify against keys/openpcb-core.pub (${verdict.reason ?? "unknown"})`);
}
const bytes = new Uint8Array(await Bun.file(artifact).arrayBuffer());
const entries = Object.keys(unzipSync(bytes));
const matches = entries.filter((entry) => forbidden.some((pattern) => pattern.test(entry)));
if (matches.length > 0) throw new Error(`${label}: forbidden package entries: ${matches.join(", ")}`);
console.log(
`verified ${label}:`, manifest.library.id, manifest.library.version,
`${manifest.components.length} components,`, `${entries.length} entries,`,
`signed by ${manifest.signature.keyId}`,
);
}
EOF
VERSION="$VERSION" bun "$VERIFY_SCRIPT"
- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
ARTIFACT="dist/openpcb-core-library-${VERSION}.opclib"
SUMS="dist/SHA256SUMS"
# The public key is not optional: consumers verify the signature with
# it, and the verify step above already proved it matches the signer.
test -f keys/openpcb-core.pub
ASSETS=(
"$ARTIFACT"
"dist/openpcb-core-library-${VERSION}-glb.opclib"
"dist/openpcb-core-library-${VERSION}-step.zip"
"$SUMS"
"keys/openpcb-core.pub"
)
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
gh release upload "${GITHUB_REF_NAME}" "${ASSETS[@]}" --clobber
else
gh release create "${GITHUB_REF_NAME}" \
--verify-tag \
--title "${GITHUB_REF_NAME}" \
--notes "OpenPCB Core Library ${VERSION}" \
"${ASSETS[@]}"
fi