Skip to content

Commit 809fe45

Browse files
auxesisclaude
andcommitted
ci(ffi): check the C library of each Linux protect-ffi binary before packing
Only ffi-preflight.yml checked which C library the Linux protect-ffi binaries link, and nothing runs that dry run automatically. release.yml builds through _build-ffi-artifacts.yml, so a release could publish a musl binary that links glibc. The suite published a glibc-linked @cipherstash/auth-linux-x64-musl 0.44.0 that way. Each Linux leg now runs scripts/check-c-library.sh on its binary after placing it and before packing it, as _build-auth-artifacts.yml does since #1018. scripts/__tests__/ffi-build-artifacts.test.mjs pins the step's place, its Linux condition and the binary it reads. Refs: CIP-4282 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
1 parent 55c1058 commit 809fe45

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

‎.github/workflows/_build-ffi-artifacts.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,16 @@ jobs:
269269
-o "platforms/${PLATFORM}/index.node" < "${BUILD_LOG}"
270270
test -s "platforms/${PLATFORM}/index.node"
271271
272+
# Every build checks which C library its binary links, so a release
273+
# cannot publish a musl binary that links glibc even if nobody ran
274+
# ffi-preflight first. The rules are in scripts/check-c-library.sh, which
275+
# every workflow that checks a Linux binary runs.
276+
- name: Check which C library the binary links
277+
if: ${{ runner.os == 'Linux' }}
278+
env:
279+
PLATFORM: ${{ matrix.cfg.platform }}
280+
run: scripts/check-c-library.sh "$PLATFORM" "languages/typescript/packages/protect-ffi/platforms/${PLATFORM}/index.node"
281+
272282
# `pnpm pack` writes into the packed package's own directory by default,
273283
# and `--pack-destination` resolves relative to `--dir` rather than to the
274284
# CWD (verified). Packing to the default location and moving the result

‎scripts/__tests__/check-c-library.test.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ describe.skipIf(!hasReadelf && !process.env.CI)('check-c-library.sh', () => {
297297
describe('the workflows that check a Linux binary', () => {
298298
const CALLERS = [
299299
'.github/workflows/_build-auth-artifacts.yml',
300+
'.github/workflows/_build-ffi-artifacts.yml',
300301
'.github/workflows/auth-preflight.yml',
301302
'.github/workflows/ffi-preflight.yml',
302303
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { readWorkflow } from './lib/workflows.mjs'
3+
4+
/**
5+
* `_build-ffi-artifacts.yml` builds the seven `@cipherstash/protect-ffi`
6+
* tarballs that `release.yml`'s `publish-ffi` uploads. Every Linux binary's C
7+
* library is checked before it is packed. Without the check, only a hand-run
8+
* ffi-preflight sees a glibc-linked musl binary, which fails to load on musl
9+
* systems such as Alpine Linux, and a release would publish it.
10+
*/
11+
12+
const workflow = readWorkflow('.github/workflows/_build-ffi-artifacts.yml')
13+
const steps = workflow?.jobs?.binaries?.steps ?? []
14+
const runOf = (step) => String(step?.run ?? '')
15+
const named = (name) => steps.findIndex((step) => step?.name === name)
16+
17+
/** A GitHub Actions expression, as the parsed workflow holds it. */
18+
const gha = (expression) => `\${{ ${expression} }}`
19+
20+
describe('_build-ffi-artifacts.yml', () => {
21+
it('checks the C library of every Linux binary before it is packed', () => {
22+
const check = steps.findIndex((step) =>
23+
runOf(step).includes('scripts/check-c-library.sh'),
24+
)
25+
const place = named('Place the binding in its platform package')
26+
const pack = named('Pack the platform package')
27+
expect(place).toBeGreaterThan(-1)
28+
expect(check).toBeGreaterThan(place)
29+
expect(check).toBeLessThan(pack)
30+
expect(String(steps[check].if)).toContain("runner.os == 'Linux'")
31+
// The binary that is packed, under the leg's own platform name.
32+
expect(steps[check].env?.PLATFORM).toBe(gha('matrix.cfg.platform'))
33+
expect(runOf(steps[check])).toMatch(
34+
/check-c-library\.sh "\$PLATFORM" "languages\/typescript\/packages\/protect-ffi\/platforms\/\$\{PLATFORM\}\/index\.node"/,
35+
)
36+
})
37+
})

0 commit comments

Comments
 (0)