@@ -38,13 +38,20 @@ jobs:
3838 echo "::error::Timed out waiting for CI test suite"
3939 exit 1
4040
41- # Determine what bumpy will do — only run expensive build/notarize when publishing
41+ # Determine what bumpy will do — only run expensive build/notarize when publishing.
42+ # Also probe the native-binary caches: if this exact source (+ build scripts +
43+ # workflows) already produced a signed/notarized binary on a previous run, we
44+ # reuse it instead of rebuilding and — for macOS — re-notarizing.
4245 plan :
4346 needs : wait-for-tests
4447 runs-on : ubuntu-latest
4548 outputs :
4649 mode : ${{ steps.plan.outputs.mode }}
4750 includes-varlock : ${{ contains(fromJSON(steps.plan.outputs.json).packageNames, 'varlock') }}
51+ macos-cache-key : ${{ steps.keys.outputs.macos-cache-key }}
52+ rust-source-hash : ${{ steps.keys.outputs.rust-source-hash }}
53+ macos-cache-hit : ${{ steps.macos-cache.outputs.cache-hit }}
54+ rust-cache-hit : ${{ steps.rust-cache.outputs.cache-hit }}
4855 steps :
4956 - uses : actions/checkout@v6
5057 - name : Setup Bun
@@ -54,39 +61,139 @@ jobs:
5461 env :
5562 GH_TOKEN : ${{ github.token }}
5663
57- # Build and sign the macOS native binary (cache hit if already built in CI)
64+ # Comprehensive content hashes for the native binaries. These cover the
65+ # native source, the build/bundle scripts, AND the workflows that drive
66+ # them — anything that can change the produced binary. The leading `v1`
67+ # salt forces a full rebuild when something hashFiles can't see changes
68+ # (runner image / Xcode / Rust / UPX toolchain) — bump it to invalidate.
69+ #
70+ # Note: the macOS .app bundle embeds a fixed constant version (hardcoded in
71+ # build-swift.ts, not the npm version), so the notarized bundle is
72+ # byte-stable across releases — which is what makes cross-release reuse
73+ # valid. Do not reintroduce a per-release version there.
74+ - id : keys
75+ run : |
76+ MACOS_HASH=${{ hashFiles('packages/encryption-binary-swift/swift/**', 'packages/encryption-binary-swift/scripts/**', 'packages/encryption-binary-swift/package.json', '.github/workflows/build-native-macos.yaml', '.github/workflows/notarize-native-macos.yaml') }}
77+ RUST_HASH=${{ hashFiles('packages/encryption-binary-rust/Cargo.lock', 'packages/encryption-binary-rust/Cargo.toml', 'packages/encryption-binary-rust/src/**', 'packages/encryption-binary-rust/scripts/**', '.github/workflows/build-native-rust.yaml') }}
78+ echo "macos-cache-key=native-bin-macos-notarized-v1-$MACOS_HASH" >> "$GITHUB_OUTPUT"
79+ echo "rust-source-hash=v1-$RUST_HASH" >> "$GITHUB_OUTPUT"
80+
81+ # Probe caches (lookup-only, no download). cache-hit is 'true' only on an
82+ # exact key match within this branch's scope (main + its base) — feature
83+ # branch caches are not visible here, so a release can only reuse a binary
84+ # produced by a prior run on main.
85+ - name : Probe macOS notarized binary cache
86+ id : macos-cache
87+ if : steps.plan.outputs.mode == 'publish'
88+ uses : actions/cache/restore@v5
89+ with :
90+ path : packages/varlock/native-bins/darwin/VarlockEnclave.app
91+ key : ${{ steps.keys.outputs.macos-cache-key }}
92+ lookup-only : true
93+ - name : Probe Rust cache - linux-x64
94+ id : rust-cache-linux-x64
95+ if : steps.plan.outputs.mode == 'publish'
96+ uses : actions/cache/restore@v5
97+ with :
98+ path : packages/varlock/native-bins/linux-x64/
99+ key : native-bin-rust-linux-x64-${{ steps.keys.outputs.rust-source-hash }}
100+ lookup-only : true
101+ - name : Probe Rust cache - linux-arm64
102+ id : rust-cache-linux-arm64
103+ if : steps.plan.outputs.mode == 'publish'
104+ uses : actions/cache/restore@v5
105+ with :
106+ path : packages/varlock/native-bins/linux-arm64/
107+ key : native-bin-rust-linux-arm64-${{ steps.keys.outputs.rust-source-hash }}
108+ lookup-only : true
109+ - name : Probe Rust cache - win32-x64
110+ id : rust-cache-win32-x64
111+ if : steps.plan.outputs.mode == 'publish'
112+ uses : actions/cache/restore@v5
113+ with :
114+ path : packages/varlock/native-bins/win32-x64/
115+ key : native-bin-rust-win32-x64-${{ steps.keys.outputs.rust-source-hash }}
116+ lookup-only : true
117+ - name : Aggregate Rust cache status
118+ id : rust-cache
119+ if : steps.plan.outputs.mode == 'publish'
120+ run : |
121+ if [[ "${{ steps.rust-cache-linux-x64.outputs.cache-hit }}" == "true" \
122+ && "${{ steps.rust-cache-linux-arm64.outputs.cache-hit }}" == "true" \
123+ && "${{ steps.rust-cache-win32-x64.outputs.cache-hit }}" == "true" ]]; then
124+ echo "cache-hit=true" >> "$GITHUB_OUTPUT"
125+ else
126+ echo "cache-hit=false" >> "$GITHUB_OUTPUT"
127+ fi
128+
129+ # Build + sign the macOS native binary. Skipped on a cache hit — the previously
130+ # notarized bundle is reused and re-verified (see verify-native-macos) instead.
58131 build-native-macos :
59132 needs : plan
60- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
133+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.macos-cache-hit != 'true'
61134 uses : ./.github/workflows/build-native-macos.yaml
62135 with :
63136 mode : release
64137 artifact-name : native-bin-macos-signed
65138 secrets :
66139 OP_CI_TOKEN : ${{ secrets.OP_CI_TOKEN }}
67140
68- # Notarize for production npm distribution
141+ # Notarize for production npm distribution and cache the notarized bundle so
142+ # future releases with unchanged source can reuse it.
69143 notarize-native-macos :
70144 needs : [plan, build-native-macos]
71- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
145+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.macos-cache-hit != 'true'
72146 uses : ./.github/workflows/notarize-native-macos.yaml
73147 with :
74148 source-artifact-name : native-bin-macos-signed
75149 artifact-name : native-bin-macos-npm
150+ cache-key : ${{ needs.plan.outputs.macos-cache-key }}
76151 secrets :
77152 OP_CI_TOKEN : ${{ secrets.OP_CI_TOKEN }}
78153
79- # Build Rust native binaries for Linux and Windows
154+ # When the notarized bundle is reused from cache (build + notarize skipped),
155+ # re-verify it on a macOS runner before we trust it in the publish step. This
156+ # is the safety net for "never publish the wrong thing": a corrupted or
157+ # un-notarized cached bundle fails here and blocks the release.
158+ verify-native-macos :
159+ needs : plan
160+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.macos-cache-hit == 'true'
161+ runs-on : macos-latest
162+ steps :
163+ - name : Restore notarized macOS binary from cache
164+ uses : actions/cache/restore@v5
165+ with :
166+ path : packages/varlock/native-bins/darwin/VarlockEnclave.app
167+ key : ${{ needs.plan.outputs.macos-cache-key }}
168+ fail-on-cache-miss : true
169+ - name : Verify signature, notarization, and function
170+ run : |
171+ APP="packages/varlock/native-bins/darwin/VarlockEnclave.app"
172+ BIN="$APP/Contents/MacOS/varlock-local-encrypt"
173+ echo "=== codesign verify ==="
174+ codesign --verify --deep --strict --verbose=2 "$APP"
175+ codesign -dvv "$APP" 2>&1 || true
176+ echo "=== notarization staple ==="
177+ xcrun stapler validate "$APP"
178+ echo "=== gatekeeper assessment (non-fatal) ==="
179+ spctl -a -t exec -vv "$APP" || true
180+ echo "=== functional smoke ==="
181+ chmod +x "$BIN"
182+ "$BIN" status | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['ok'], 'status not ok'"
183+ echo "Cached notarized binary verified"
184+
185+ # Build Rust native binaries for Linux and Windows. Skipped on a cache hit.
80186 build-native-rust :
81187 needs : plan
82- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
188+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit != 'true'
83189 uses : ./.github/workflows/build-native-rust.yaml
84190 with :
85191 artifact-name : native-bin-rust
192+ source-hash : ${{ needs.plan.outputs.rust-source-hash }}
86193
87194 release :
88195 name : Release
89- needs : [plan, notarize-native-macos, build-native-rust]
196+ needs : [plan, notarize-native-macos, verify-native-macos, build-native-rust]
90197 if : always() && !failure() && !cancelled()
91198 runs-on : ubuntu-latest
92199 permissions :
@@ -117,42 +224,86 @@ jobs:
117224 - name : Update npm
118225 run : npm install -g npm@latest
119226
120- # Download signed macOS native binary so it's included in the npm package
121- - name : Download macOS native binary
122- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
227+ # macOS native binary: download the freshly-notarized artifact (cache miss),
228+ # or restore the previously-notarized bundle from cache (cache hit, already
229+ # re-verified by verify-native-macos).
230+ - name : Download macOS native binary (built this run)
231+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.macos-cache-hit != 'true'
123232 uses : actions/download-artifact@v8
124233 with :
125234 name : native-bin-macos-npm
126235 path : packages/varlock/native-bins/darwin/VarlockEnclave.app
236+ - name : Restore macOS native binary (from cache)
237+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.macos-cache-hit == 'true'
238+ uses : actions/cache/restore@v5
239+ with :
240+ path : packages/varlock/native-bins/darwin/VarlockEnclave.app
241+ key : ${{ needs.plan.outputs.macos-cache-key }}
242+ fail-on-cache-miss : true
127243 - name : Restore native binary execute permission
128244 if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
129245 run : chmod +x packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt
130246
131- # Download Rust native binaries for Linux and Windows
132- - name : Download Linux x64 native binary
133- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
247+ # Rust native binaries: download artifacts (cache miss) or restore from cache (hit)
248+ - name : Download Rust native binaries (built this run)
249+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit != 'true'
134250 uses : actions/download-artifact@v8
135251 with :
136252 name : native-bin-rust-linux-x64
137253 path : packages/varlock/native-bins/linux-x64
138- - name : Download Linux arm64 native binary
139- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
254+ - name : Download Rust native binaries - linux- arm64 (built this run)
255+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit != 'true'
140256 uses : actions/download-artifact@v8
141257 with :
142258 name : native-bin-rust-linux-arm64
143259 path : packages/varlock/native-bins/linux-arm64
144- - name : Download Windows x64 native binary
145- if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
260+ - name : Download Rust native binaries - win32- x64 (built this run)
261+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit != 'true'
146262 uses : actions/download-artifact@v8
147263 with :
148264 name : native-bin-rust-win32-x64
149265 path : packages/varlock/native-bins/win32-x64
266+ - name : Restore Rust native binaries (from cache)
267+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit == 'true'
268+ uses : actions/cache/restore@v5
269+ with :
270+ path : packages/varlock/native-bins/linux-x64/
271+ key : native-bin-rust-linux-x64-${{ needs.plan.outputs.rust-source-hash }}
272+ fail-on-cache-miss : true
273+ - name : Restore Rust native binaries - linux-arm64 (from cache)
274+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit == 'true'
275+ uses : actions/cache/restore@v5
276+ with :
277+ path : packages/varlock/native-bins/linux-arm64/
278+ key : native-bin-rust-linux-arm64-${{ needs.plan.outputs.rust-source-hash }}
279+ fail-on-cache-miss : true
280+ - name : Restore Rust native binaries - win32-x64 (from cache)
281+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true' && needs.plan.outputs.rust-cache-hit == 'true'
282+ uses : actions/cache/restore@v5
283+ with :
284+ path : packages/varlock/native-bins/win32-x64/
285+ key : native-bin-rust-win32-x64-${{ needs.plan.outputs.rust-source-hash }}
286+ fail-on-cache-miss : true
150287 - name : Restore Rust binary execute permissions
151288 if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
152289 run : |
153290 chmod +x packages/varlock/native-bins/linux-x64/varlock-local-encrypt
154291 chmod +x packages/varlock/native-bins/linux-arm64/varlock-local-encrypt
155292
293+ # Fail loudly if any native binary is missing before we build the npm package,
294+ # rather than silently publishing an incomplete package.
295+ - name : Verify native binaries present
296+ if : needs.plan.outputs.mode == 'publish' && needs.plan.outputs.includes-varlock == 'true'
297+ run : |
298+ missing=0
299+ check() { if [ ! -f "$1" ]; then echo "::error::missing native binary: $1"; missing=1; fi; }
300+ check packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt
301+ check packages/varlock/native-bins/linux-x64/varlock-local-encrypt
302+ check packages/varlock/native-bins/linux-arm64/varlock-local-encrypt
303+ check packages/varlock/native-bins/win32-x64/varlock-local-encrypt.exe
304+ if [ "$missing" = "1" ]; then exit 1; fi
305+ echo "All native binaries present"
306+
156307 # ------------------------------------------------------------
157308 - name : Build libraries
158309 run : bun run build:libs
0 commit comments