Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11,244 changes: 11,156 additions & 88 deletions dist/main.js

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@actions/core": "^3.0.1",
"@actions/github": "^9.1.1",
"@actions/tool-cache": "^4.0.0",
"plist": "^5.0.0",
"semver": "^7.8.5"
},
"devDependencies": {
Expand Down
26 changes: 14 additions & 12 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createConfigurationFiles, getConfigurationInputs, joinPathList } from '
import { serveLogFilePathKey, servePIDKey, versionKey } from './shared';
import { exec, startDaemon } from './exec';
import { temporaryFileName } from './temporary';
import { collectServeArguments } from './serve';

interface Release {
tagName: string;
Expand Down Expand Up @@ -140,32 +141,33 @@ function archiveBaseName(name: string): string {
const installerStorePath = path.join(zbExtractedFolderPath, archiveBaseName(asset.name), 'store');
const objectNames = await fs.readdir(installerStorePath);
const zbStoreDirectory = core.platform.isWindows ? 'C:\\zb\\store' : '/opt/zb/store';
const zbBins = await Promise.all(
const zbStoreObjects = await Promise.all(
objectNames
.filter((name) => name.match(/-zb-/))
.map(async (name) => {
const binPath = path.join(zbStoreDirectory, name, 'bin');
const storePath = path.join(zbStoreDirectory, name);
const binPath = path.join(storePath, 'bin');
try {
await fs.lstat(binPath);
} catch {
return null;
}
return binPath;
return storePath;
})
);
for (const binPath of zbBins) {
if (binPath) {
core.addPath(binPath);
let firstZBStoreObject: string | undefined
for (const storePath of zbStoreObjects) {
if (storePath) {
core.addPath(path.join(storePath, 'bin'));
if (!firstZBStoreObject) {
firstZBStoreObject = storePath;
}
}
}

if (core.getBooleanInput('zb-serve') && zbBins[0]) {
if (core.getBooleanInput('zb-serve') && firstZBStoreObject) {
const logFilePath = temporaryFileName('zb-serve-*.txt');
const zbExe = path.join(zbBins[0], 'zb');
const serveArgs = [
'serve',
`--sandbox=${useRoot && core.platform.isLinux ? '1' : '0'}`,
];
const { command: zbExe, args: serveArgs } = await collectServeArguments(firstZBStoreObject, { useRoot });
let pid: number | undefined;
try {
pid = useRoot ?
Expand Down
151 changes: 151 additions & 0 deletions src/serve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Copyright 2026 The zb Authors
// SPDX-License-Identifier: MIT

import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import { after, before, describe, it } from 'node:test';

import { collectServeArguments, type ServeArgsOptions } from './serve';
import { mkdirTemp } from './temporary';

describe('collectServeArguments', () => {
let tempDir: string;
let emptyDir: string;
let linuxDir: string;
let macOSDir: string;
before(async () => {
tempDir = await mkdirTemp('setup-zb-serve-test-*');

emptyDir = path.join(tempDir, 'empty');
await fs.mkdir(emptyDir);

linuxDir = path.join(tempDir, 'linux');
const systemdDirectory = path.join(linuxDir, 'lib', 'systemd', 'system');
await fs.mkdir(systemdDirectory, { recursive: true });
await fs.writeFile(
path.join(systemdDirectory, 'zb-serve.service'),
'[Unit]\n' +
'Description=zb Store Server\n' +
'[Service]\n' +
`ExecStart=${linuxDir}/bin/zb serve --systemd --sandbox-path=/bin/sh=/opt/zb/store/hpsxd175dzfmjrg27pvvin3nzv3yi61k-busybox-1.36.1/bin/sh --implicit-system-dep=/bin/sh --build-users-group=zbld $ZB_SERVE_FLAGS\n`,
);

macOSDir = path.join(tempDir, 'macos');
const launchdDirectory = path.join(macOSDir, 'Library', 'LaunchDaemons');
await fs.mkdir(launchdDirectory, { recursive: true });
await fs.writeFile(
path.join(launchdDirectory, 'dev.zb-build.serve.plist'),
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' +
'<plist version="1.0">\n' +
'<dict>\n' +
'<key>Label</key>\n' +
'<string>dev.zb-build.serve</string>\n' +
'<key>KeepAlive</key>\n' +
'<true/>\n' +
'<key>RunAtLoad</key>\n' +
'<true/>\n' +
'<key>ProgramArguments</key>\n' +
'<array>\n' +
`<string>${macOSDir}/bin/zb</string>\n` +
'<string>serve</string>\n' +
'<string>--sandbox-path=/usr</string>\n' +
'<string>--sandbox-path=/bin</string>\n' +
'<string>--sandbox-path=/Library/Developer/CommandLineTools</string>\n' +
'</array>\n' +
'<key>StandardErrorPath</key>\n' +
'<string>/opt/zb/var/log/zb-serve.log</string>\n' +
'<key>StandardOutPath</key>\n' +
'<string>/dev/null</string>\n' +
'</dict>\n' +
'</plist>\n',
);
});

after(async () => {
await fs.rm(tempDir, { force: true, recursive: true });
});

describe('on Linux', () => {
const linuxServeArgs = (prefix: string, options: Omit<ServeArgsOptions, 'platform'>) =>
collectServeArguments(prefix, {
...options,
platform: {
isLinux: true,
isMacOS: false,
isWindows: false,
},
});

it('should use the zb in bin', async () => {
const got = await linuxServeArgs(emptyDir, { useRoot: true });
assert.equal(path.normalize(got.command), path.normalize(path.join(emptyDir, 'bin', 'zb')));
});

it('should sandbox with root', async () => {
const got = await linuxServeArgs(emptyDir, { useRoot: true });
assert.ok(got.args.includes('--sandbox=1'));
});

it('should not sandbox without root', async () => {
const got = await linuxServeArgs(emptyDir, { useRoot: false });
assert.ok(got.args.includes('--sandbox=0'));
});

it('should start with a serve argument', async () => {
const got = await linuxServeArgs(emptyDir, { useRoot: false });
assert.equal(got.args[0], 'serve');
});

it('should include sandbox arguments from systemd configuration', async () => {
const got = await linuxServeArgs(linuxDir, { useRoot: false });
assert.ok(got.args.includes('--sandbox-path=/bin/sh=/opt/zb/store/hpsxd175dzfmjrg27pvvin3nzv3yi61k-busybox-1.36.1/bin/sh'));
assert.ok(got.args.includes('--implicit-system-dep=/bin/sh'));
});

it('should not include --systemd', async () => {
const got = await linuxServeArgs(linuxDir, { useRoot: false });
assert.ok(!got.args.includes('--systemd'));
});
});

describe('on macOS', () => {
const macOSServeArgs = (prefix: string, options: Omit<ServeArgsOptions, 'platform'>) =>
collectServeArguments(prefix, {
...options,
platform: {
isLinux: false,
isMacOS: true,
isWindows: false,
},
});

it('should use the zb in bin', async () => {
const got = await macOSServeArgs(emptyDir, { useRoot: true });
assert.equal(path.normalize(got.command), path.normalize(path.join(emptyDir, 'bin', 'zb')));
});

it('should not sandbox with root', async () => {
const got = await macOSServeArgs(emptyDir, { useRoot: true });
assert.ok(got.args.includes('--sandbox=0'));
});

it('should not sandbox without root', async () => {
const got = await macOSServeArgs(emptyDir, { useRoot: false });
assert.ok(got.args.includes('--sandbox=0'));
});

it('should start with a serve argument', async () => {
const got = await macOSServeArgs(emptyDir, { useRoot: false });
assert.equal(got.args[0], 'serve');
});

it('should include sandbox arguments from launchd configuration', async () => {
const got = await macOSServeArgs(macOSDir, { useRoot: false });
assert.ok(got.args.includes('--sandbox-path=/usr'));
assert.ok(got.args.includes('--sandbox-path=/bin'));
assert.ok(got.args.includes('--sandbox-path=/Library/Developer/CommandLineTools'));
});
});
});
85 changes: 85 additions & 0 deletions src/serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2026 The zb Authors
// SPDX-License-Identifier: MIT

import fs from 'node:fs/promises';
import path from 'node:path';

import * as core from '@actions/core';
import { parse as parsePlist, type PlistValue } from 'plist';

export interface ServeArgsOptions {
useRoot: boolean
platform?: Platform
}

export interface Platform {
isLinux: boolean;
isMacOS: boolean;
isWindows: boolean;
}

export async function collectServeArguments(prefix: string, options: ServeArgsOptions): Promise<{ command: string, args: string[] }> {
const command = path.join(prefix, 'bin', 'zb');
const platform: Platform = options.platform || core.platform;
const args = ['serve'];
args.push(`--sandbox=${options.useRoot && platform.isLinux ? '1' : '0'}`);

if (platform.isLinux) {
let systemdUnit = '';
try {
systemdUnit = await fs.readFile(path.join(prefix, 'lib', 'systemd', 'system', 'zb-serve.service'), {
encoding: 'utf-8',
});
} catch {
}
const execStartPrefix = 'ExecStart=';
const execStartLine = systemdUnit.split('\n').find((line) => line.startsWith(execStartPrefix));
if (execStartLine) {
const argv = execStartLine.substring(execStartPrefix.length).split(/\s+/);
addUsefulArgs(args, argv.slice(1));
}
} else if (platform.isMacOS) {
let launchDaemonData: Buffer | undefined;
try {
launchDaemonData = await fs.readFile(path.join(prefix, 'Library', 'LaunchDaemons', 'dev.zb-build.serve.plist'));
} catch {
}
if (launchDaemonData) {
const launchDaemon = parseAnyPlist(launchDaemonData);
addUsefulArgs(args, launchDaemonProgramArguments(launchDaemon));
}
}

return { command, args };
}

function parseAnyPlist(data: string | Uint8Array): PlistValue {
return parsePlist(typeof data === 'string' || data.includes(0)
? data
: new TextDecoder().decode(data));
}

function addUsefulArgs(dst: string[], src: readonly string[]): void {
for (const arg of src) {
if (arg.startsWith('--sandbox-path=') || arg.startsWith('--implicit-system-dep')) {
dst.push(arg);
}
}
}

function launchDaemonProgramArguments(launchDaemon: PlistValue): string[] {
const stringArgs = [];
if (launchDaemon &&
typeof launchDaemon === 'object' &&
'ProgramArguments' in launchDaemon &&
launchDaemon.ProgramArguments instanceof Array) {
for (const arg of launchDaemon.ProgramArguments) {
if (typeof arg === 'string') {
stringArgs.push(arg);
} else {
stringArgs.push(Object.prototype.toString.call(arg));
}
}
}
return stringArgs;
}
8 changes: 5 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
],
"compilerOptions": {
"noEmit": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"noImplicitAny": true
"noImplicitAny": true,
"module": "nodenext",
"customConditions": ["import"],
"lib": ["ES2024"],
"target": "ES2024"
}
}