Skip to content

Commit 8cf9bde

Browse files
committed
ci: add GitHub Actions workflow for multi-platform testing
- Add CI workflow with lint job (C++, TypeScript, types, markdown) - Add build jobs for Linux glibc (Rocky 8), Linux musl (Alpine 3.20) - Add build jobs for macOS ARM64 (macos-14), macOS x64 (macos-13) - Test on Node.js 20 and 22 (matching engines field) - Use system FFmpeg via package managers for CI validation - Add @pproenca/webcodecs-ffmpeg-* as optionalDependencies - Update gyp/ffmpeg-paths-lib.ts to resolve FFmpeg from npm package - Resolution order: FFMPEG_ROOT > npm package > ./ffmpeg-install > system The CI uses --omit=optional to validate the system FFmpeg fallback works. Developers get FFmpeg automatically on npm install, or can opt-out to use system FFmpeg with npm install --omit=optional.
1 parent ca693e7 commit 8cf9bde

10 files changed

Lines changed: 730 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# CI workflow for node-webcodecs
2+
# Runs linting, building, and testing on all supported platforms
3+
#
4+
# Platforms:
5+
# - Linux x64 glibc (Rocky Linux 8 container)
6+
# - Linux x64 musl (Alpine 3.20 container)
7+
# - macOS ARM64 (macos-14 runner)
8+
# - macOS x64 (macos-13 runner)
9+
#
10+
# Node.js versions: 20, 22 (matching engines field)
11+
12+
name: CI
13+
14+
on:
15+
push:
16+
branches: [master]
17+
pull_request:
18+
branches: [master]
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
lint:
25+
name: Lint
26+
runs-on: ubuntu-24.04
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Setup Node.js
32+
uses: actions/setup-node@v4
33+
with:
34+
node-version: 22
35+
36+
- name: Install dependencies
37+
run: npm ci --omit=optional
38+
39+
- name: Lint C++
40+
run: npm run lint:cpp
41+
42+
- name: Lint TypeScript
43+
run: npm run lint:ts
44+
45+
- name: Lint types
46+
run: npm run lint:types
47+
48+
- name: Lint markdown
49+
run: npm run lint:md
50+
51+
build-linux-glibc:
52+
name: Linux glibc (Node ${{ matrix.node }})
53+
runs-on: ubuntu-24.04
54+
container: rockylinux:8
55+
strategy:
56+
matrix:
57+
node: [20, 22]
58+
steps:
59+
- name: Install system dependencies
60+
run: |
61+
dnf install -y --enablerepo=devel gcc-c++ make python3 git
62+
dnf install -y --enablerepo=epel,crb ffmpeg-free-devel
63+
64+
- name: Checkout
65+
uses: actions/checkout@v4
66+
67+
- name: Install Node.js
68+
uses: actions/setup-node@v4
69+
with:
70+
node-version: ${{ matrix.node }}
71+
72+
- name: Install dependencies
73+
run: npm ci --omit=optional
74+
75+
- name: Build
76+
run: npm run build
77+
78+
- name: Test
79+
run: npm test
80+
81+
build-linux-musl:
82+
name: Linux musl (Node ${{ matrix.node }})
83+
runs-on: ubuntu-24.04
84+
container: alpine:3.20
85+
strategy:
86+
matrix:
87+
node: [20, 22]
88+
steps:
89+
- name: Install system dependencies
90+
run: apk add --no-cache ffmpeg-dev build-base python3 pkgconfig git nodejs npm
91+
92+
- name: Checkout
93+
uses: actions/checkout@v4
94+
95+
- name: Install dependencies
96+
run: npm ci --omit=optional
97+
98+
- name: Build
99+
run: npm run build
100+
101+
- name: Test
102+
run: npm test
103+
104+
build-macos-arm64:
105+
name: macOS ARM64 (Node ${{ matrix.node }})
106+
runs-on: macos-14
107+
strategy:
108+
matrix:
109+
node: [20, 22]
110+
steps:
111+
- name: Checkout
112+
uses: actions/checkout@v4
113+
114+
- name: Install FFmpeg
115+
run: brew install ffmpeg
116+
117+
- name: Setup Node.js
118+
uses: actions/setup-node@v4
119+
with:
120+
node-version: ${{ matrix.node }}
121+
122+
- name: Install dependencies
123+
run: npm ci --omit=optional
124+
125+
- name: Build
126+
run: npm run build
127+
128+
- name: Test
129+
run: npm test
130+
131+
build-macos-x64:
132+
name: macOS x64 (Node ${{ matrix.node }})
133+
runs-on: macos-13
134+
strategy:
135+
matrix:
136+
node: [20, 22]
137+
steps:
138+
- name: Checkout
139+
uses: actions/checkout@v4
140+
141+
- name: Install FFmpeg
142+
run: brew install ffmpeg
143+
144+
- name: Setup Node.js
145+
uses: actions/setup-node@v4
146+
with:
147+
node-version: ${{ matrix.node }}
148+
149+
- name: Install dependencies
150+
run: npm ci --omit=optional
151+
152+
- name: Build
153+
run: npm run build
154+
155+
- name: Test
156+
run: npm test

gyp/ffmpeg-paths-lib.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
// Resolve FFmpeg paths for node-gyp binding.
66
//
77
// Resolution order:
8-
// 1. FFMPEG_ROOT env var (set by CI from deps-v* release artifacts)
9-
// 2. ./ffmpeg-install directory (local development)
10-
// 3. System pkg-config (fallback)
8+
// 1. FFMPEG_ROOT env var (explicit override)
9+
// 2. @pproenca/webcodecs-ffmpeg npm package (if installed)
10+
// 3. ./ffmpeg-install directory (local development)
11+
// 4. System pkg-config (fallback)
1112
//
1213
// The FFmpeg static libraries are built from:
1314
// - Linux: docker/Dockerfile.linux-x64 (Alpine musl, fully static)
@@ -33,6 +34,7 @@ exports.resolveProjectRoot = resolveProjectRoot;
3334
const node_fs_1 = require("node:fs");
3435
const node_child_process_1 = require("node:child_process");
3536
const node_path_1 = require("node:path");
37+
const node_os_1 = require("node:os");
3638
const FFMPEG_LIBS = 'libavcodec libavformat libavutil libswscale libswresample libavfilter';
3739
function filterFrameworkFlags(flags) {
3840
const tokens = flags.split(/\s+/);
@@ -46,19 +48,46 @@ function filterFrameworkFlags(flags) {
4648
}
4749
return result.join(' ');
4850
}
51+
function tryResolveFromNpmPackage() {
52+
// Build platform-specific package name (e.g., @pproenca/webcodecs-ffmpeg-darwin-arm64)
53+
const pkgName = `@pproenca/webcodecs-ffmpeg-${(0, node_os_1.platform)()}-${(0, node_os_1.arch)()}`;
54+
try {
55+
// Resolve the pkgconfig export from the platform package
56+
// The package exports "./pkgconfig" pointing to "./lib/pkgconfig/index.js"
57+
const pkgconfigIndex = require.resolve(`${pkgName}/pkgconfig`);
58+
const pkgconfig = (0, node_path_1.dirname)(pkgconfigIndex);
59+
if ((0, node_fs_1.existsSync)(pkgconfig)) {
60+
// The root is two levels up from lib/pkgconfig
61+
const root = (0, node_path_1.dirname)((0, node_path_1.dirname)(pkgconfig));
62+
return { root, pkgconfig };
63+
}
64+
}
65+
catch {
66+
// Package not installed - continue to next fallback
67+
}
68+
return null;
69+
}
4970
function getFfmpegRoot(projectRoot, env) {
71+
// 1. FFMPEG_ROOT env var (explicit override)
5072
if (env.FFMPEG_ROOT) {
5173
const root = env.FFMPEG_ROOT;
5274
const pkgconfig = (0, node_path_1.join)(root, 'lib', 'pkgconfig');
5375
if ((0, node_fs_1.existsSync)(pkgconfig)) {
5476
return { root, pkgconfig };
5577
}
5678
}
79+
// 2. @pproenca/webcodecs-ffmpeg npm package (if installed)
80+
const npmPackage = tryResolveFromNpmPackage();
81+
if (npmPackage) {
82+
return npmPackage;
83+
}
84+
// 3. ./ffmpeg-install directory (local development)
5785
const ffmpegInstall = (0, node_path_1.join)(projectRoot, 'ffmpeg-install');
5886
const pkgconfig = (0, node_path_1.join)(ffmpegInstall, 'lib', 'pkgconfig');
5987
if ((0, node_fs_1.existsSync)(pkgconfig)) {
6088
return { root: ffmpegInstall, pkgconfig };
6189
}
90+
// 4. System pkg-config will be used as fallback by the caller
6291
return null;
6392
}
6493
function runPkgConfig(args, ffmpegRoot, pkgConfigPath, env) {

gyp/ffmpeg-paths-lib.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
// Resolve FFmpeg paths for node-gyp binding.
55
//
66
// Resolution order:
7-
// 1. FFMPEG_ROOT env var (set by CI from deps-v* release artifacts)
8-
// 2. ./ffmpeg-install directory (local development)
9-
// 3. System pkg-config (fallback)
7+
// 1. FFMPEG_ROOT env var (explicit override)
8+
// 2. @pproenca/webcodecs-ffmpeg npm package (if installed)
9+
// 3. ./ffmpeg-install directory (local development)
10+
// 4. System pkg-config (fallback)
1011
//
1112
// The FFmpeg static libraries are built from:
1213
// - Linux: docker/Dockerfile.linux-x64 (Alpine musl, fully static)
@@ -25,7 +26,8 @@
2526

2627
import {existsSync} from 'node:fs';
2728
import {execSync} from 'node:child_process';
28-
import {join, resolve} from 'node:path';
29+
import {join, resolve, dirname} from 'node:path';
30+
import {platform, arch} from 'node:os';
2931

3032
const FFMPEG_LIBS = 'libavcodec libavformat libavutil libswscale libswresample libavfilter';
3133

@@ -47,7 +49,29 @@ export function filterFrameworkFlags(flags: string): string {
4749
return result.join(' ');
4850
}
4951

52+
function tryResolveFromNpmPackage(): FfmpegRoot | null {
53+
// Build platform-specific package name (e.g., @pproenca/webcodecs-ffmpeg-darwin-arm64)
54+
const pkgName = `@pproenca/webcodecs-ffmpeg-${platform()}-${arch()}`;
55+
56+
try {
57+
// Resolve the pkgconfig export from the platform package
58+
// The package exports "./pkgconfig" pointing to "./lib/pkgconfig/index.js"
59+
const pkgconfigIndex = require.resolve(`${pkgName}/pkgconfig`);
60+
const pkgconfig = dirname(pkgconfigIndex);
61+
62+
if (existsSync(pkgconfig)) {
63+
// The root is two levels up from lib/pkgconfig
64+
const root = dirname(dirname(pkgconfig));
65+
return {root, pkgconfig};
66+
}
67+
} catch {
68+
// Package not installed - continue to next fallback
69+
}
70+
return null;
71+
}
72+
5073
export function getFfmpegRoot(projectRoot: string, env: NodeJS.ProcessEnv): FfmpegRoot | null {
74+
// 1. FFMPEG_ROOT env var (explicit override)
5175
if (env.FFMPEG_ROOT) {
5276
const root = env.FFMPEG_ROOT;
5377
const pkgconfig = join(root, 'lib', 'pkgconfig');
@@ -56,12 +80,20 @@ export function getFfmpegRoot(projectRoot: string, env: NodeJS.ProcessEnv): Ffmp
5680
}
5781
}
5882

83+
// 2. @pproenca/webcodecs-ffmpeg npm package (if installed)
84+
const npmPackage = tryResolveFromNpmPackage();
85+
if (npmPackage) {
86+
return npmPackage;
87+
}
88+
89+
// 3. ./ffmpeg-install directory (local development)
5990
const ffmpegInstall = join(projectRoot, 'ffmpeg-install');
6091
const pkgconfig = join(ffmpegInstall, 'lib', 'pkgconfig');
6192
if (existsSync(pkgconfig)) {
6293
return {root: ffmpegInstall, pkgconfig};
6394
}
6495

96+
// 4. System pkg-config will be used as fallback by the caller
6597
return null;
6698
}
6799

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-01-11

0 commit comments

Comments
 (0)