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
104 changes: 104 additions & 0 deletions .github/workflows/cli-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,107 @@ jobs:

- name: Run tests
run: cargo test -q --manifest-path cli/Cargo.toml

package-smoke:
name: package-smoke-${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
platform_package: linux-x64-musl
bin_name: af
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
platform_package: linux-arm64-musl
bin_name: af
- os: macos-15-intel
target: x86_64-apple-darwin
platform_package: darwin-x64
bin_name: af
- os: macos-15
target: aarch64-apple-darwin
platform_package: darwin-arm64
bin_name: af
- os: windows-latest
target: x86_64-pc-windows-msvc
platform_package: win32-x64-msvc
bin_name: af.exe
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Setup Zig for linux-musl cross build
if: contains(matrix.target, 'linux-musl')
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.13.0

- name: Install cargo-zigbuild
if: contains(matrix.target, 'linux-musl')
uses: taiki-e/install-action@v2
with:
tool: cargo-zigbuild

- name: Build (linux-musl)
if: contains(matrix.target, 'linux-musl')
run: cargo zigbuild --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}

- name: Build (native)
if: ${{ !contains(matrix.target, 'linux-musl') }}
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}

- name: Prepare npm smoke workroot (unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
WORKROOT="$RUNNER_TEMP/af-npm-smoke"
VERSION="0.0.0-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
node cli/npm/scripts/prepare-smoke-workroot.mjs \
--workroot "$WORKROOT" \
--version "$VERSION" \
--platform-package "${{ matrix.platform_package }}" \
--binary "cli/target/${{ matrix.target }}/release/${{ matrix.bin_name }}"
PLATFORM_TARBALL="$(cd "$WORKROOT/platforms/${{ matrix.platform_package }}" && npm pack | tail -n1)"
cp "$WORKROOT/platforms/${{ matrix.platform_package }}/$PLATFORM_TARBALL" "$WORKROOT/meta/vendor/$PLATFORM_TARBALL"
META_TARBALL="$(cd "$WORKROOT/meta" && npm pack | tail -n1)"
npm install -g "$WORKROOT/meta/$META_TARBALL"
PREFIX="$(npm prefix -g)"
"$PREFIX/bin/af" --help > /dev/null

- name: Prepare npm smoke workroot (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$workroot = Join-Path $env:RUNNER_TEMP "af-npm-smoke"
$version = "0.0.0-ci-$env:GITHUB_RUN_ID-$env:GITHUB_RUN_ATTEMPT"
node cli/npm/scripts/prepare-smoke-workroot.mjs `
--workroot $workroot `
--version $version `
--platform-package "${{ matrix.platform_package }}" `
--binary "cli/target/${{ matrix.target }}/release/${{ matrix.bin_name }}"
$platformDir = Join-Path $workroot "platforms/${{ matrix.platform_package }}"
Push-Location $platformDir
$platformTarball = (npm pack | Select-Object -Last 1).Trim()
Pop-Location
Copy-Item (Join-Path $platformDir $platformTarball) (Join-Path $workroot "meta/vendor/$platformTarball")
$metaDir = Join-Path $workroot "meta"
Push-Location $metaDir
$metaTarball = (npm pack | Select-Object -Last 1).Trim()
Pop-Location
npm install -g (Join-Path $metaDir $metaTarball)
$prefix = npm prefix -g
& (Join-Path $prefix "af.cmd") --help | Out-Null
111 changes: 109 additions & 2 deletions .github/workflows/cli-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,116 @@ jobs:
path: cli/${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.ext }}
if-no-files-found: error

release:
smoke-npm:
if: startsWith(github.ref, 'refs/tags/')
name: smoke-npm-${{ matrix.target }}
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
platform_package: linux-x64-musl
bin_name: af
ext: tar.gz
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
platform_package: linux-arm64-musl
bin_name: af
ext: tar.gz
- os: macos-15-intel
target: x86_64-apple-darwin
platform_package: darwin-x64
bin_name: af
ext: tar.gz
- os: macos-15
target: aarch64-apple-darwin
platform_package: darwin-arm64
bin_name: af
ext: tar.gz
- os: windows-latest
target: x86_64-pc-windows-msvc
platform_package: win32-x64-msvc
bin_name: af.exe
ext: zip
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Download artifact
uses: actions/download-artifact@v7
with:
name: cli-${{ matrix.target }}
path: artifacts

- name: Extract binary (unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
ARCHIVE="$(find artifacts -type f -name "${GITHUB_REF_NAME}-${{ matrix.target }}.${{ matrix.ext }}" | head -n1)"
if [[ -z "$ARCHIVE" ]]; then
echo "Artifact for target '${{ matrix.target }}' not found"
exit 1
fi
TMPDIR="$(mktemp -d)"
tar -xzf "$ARCHIVE" -C "$TMPDIR"
BINARY_PATH="$TMPDIR/${{ matrix.bin_name }}"
WORKROOT="$RUNNER_TEMP/af-npm-smoke"
VERSION="${GITHUB_REF_NAME#cli-v}"
node cli/npm/scripts/prepare-smoke-workroot.mjs \
--workroot "$WORKROOT" \
--version "$VERSION" \
--platform-package "${{ matrix.platform_package }}" \
--binary "$BINARY_PATH"
PLATFORM_TARBALL="$(cd "$WORKROOT/platforms/${{ matrix.platform_package }}" && npm pack | tail -n1)"
cp "$WORKROOT/platforms/${{ matrix.platform_package }}/$PLATFORM_TARBALL" "$WORKROOT/meta/vendor/$PLATFORM_TARBALL"
META_TARBALL="$(cd "$WORKROOT/meta" && npm pack | tail -n1)"
npm install -g "$WORKROOT/meta/$META_TARBALL"
PREFIX="$(npm prefix -g)"
"$PREFIX/bin/af" --help > /dev/null

- name: Extract binary (windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$archive = Get-ChildItem -Path artifacts -Recurse -Filter "${env:GITHUB_REF_NAME}-${{ matrix.target }}.${{ matrix.ext }}" | Select-Object -First 1
if (-not $archive) { throw "Artifact for target '${{ matrix.target }}' not found" }
$tmpdir = Join-Path $env:RUNNER_TEMP "af-smoke-artifact"
if (Test-Path $tmpdir) { Remove-Item -Recurse -Force $tmpdir }
New-Item -ItemType Directory -Path $tmpdir | Out-Null
Expand-Archive -Path $archive.FullName -DestinationPath $tmpdir -Force
$binary = Join-Path $tmpdir "${{ matrix.bin_name }}"
$workroot = Join-Path $env:RUNNER_TEMP "af-npm-smoke"
$version = $env:GITHUB_REF_NAME.Substring(5)
node cli/npm/scripts/prepare-smoke-workroot.mjs `
--workroot $workroot `
--version $version `
--platform-package "${{ matrix.platform_package }}" `
--binary $binary
$platformDir = Join-Path $workroot "platforms/${{ matrix.platform_package }}"
Push-Location $platformDir
$platformTarball = (npm pack | Select-Object -Last 1).Trim()
Pop-Location
Copy-Item (Join-Path $platformDir $platformTarball) (Join-Path $workroot "meta/vendor/$platformTarball")
$metaDir = Join-Path $workroot "meta"
Push-Location $metaDir
$metaTarball = (npm pack | Select-Object -Last 1).Trim()
Pop-Location
npm install -g (Join-Path $metaDir $metaTarball)
$prefix = npm prefix -g
& (Join-Path $prefix "af.cmd") --help | Out-Null

release:
if: startsWith(github.ref, 'refs/tags/')
needs: [build, smoke-npm]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
Expand All @@ -134,7 +241,7 @@ jobs:

npm-publish:
if: startsWith(github.ref, 'refs/tags/')
needs: [build, release]
needs: [build, smoke-npm, release]
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
120 changes: 120 additions & 0 deletions cli/npm/scripts/prepare-smoke-workroot.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env node

import fs from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';

function usage() {
console.error(
'Usage: node prepare-smoke-workroot.mjs --workroot <dir> --version <version> --platform-package <name> --binary <path>',
);
}

function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('--')) {
throw new Error(`unexpected argument: ${arg}`);
}
const key = arg.slice(2);
const value = argv[i + 1];
if (!value || value.startsWith('--')) {
throw new Error(`missing value for --${key}`);
}
args[key] = value;
i += 1;
}
return args;
}

async function rewritePackageVersions(rootDir, version) {
const entries = await fs.readdir(rootDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
await rewritePackageVersions(fullPath, version);
continue;
}
if (entry.isFile() && entry.name === 'package.json') {
const raw = await fs.readFile(fullPath, 'utf8');
await fs.writeFile(fullPath, raw.replaceAll('__VERSION__', version));
}
}
}

async function main() {
const args = parseArgs(process.argv.slice(2));
const workroot = args.workroot;
const version = args.version;
const platformPackage = args['platform-package'];
const binaryPath = args.binary;

if (!workroot || !version || !platformPackage || !binaryPath) {
usage();
process.exit(1);
}

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const npmRoot = path.resolve(scriptDir, '..');
const metaTemplateDir = path.join(npmRoot, 'meta');
const platformsTemplateDir = path.join(npmRoot, 'platforms');
const selectedPlatformDir = path.join(platformsTemplateDir, platformPackage);

await fs.access(metaTemplateDir);
await fs.access(selectedPlatformDir);
await fs.access(binaryPath);

await fs.rm(workroot, { recursive: true, force: true });
await fs.mkdir(workroot, { recursive: true });

const metaDir = path.join(workroot, 'meta');
const platformsDir = path.join(workroot, 'platforms');
await fs.cp(metaTemplateDir, metaDir, { recursive: true });
await fs.mkdir(platformsDir, { recursive: true });
await fs.cp(selectedPlatformDir, path.join(platformsDir, platformPackage), {
recursive: true,
});

await rewritePackageVersions(workroot, version);

const platformPkgPath = path.join(platformsDir, platformPackage, 'package.json');
const platformPkg = JSON.parse(await fs.readFile(platformPkgPath, 'utf8'));

const metaPkgPath = path.join(metaDir, 'package.json');
const metaPkg = JSON.parse(await fs.readFile(metaPkgPath, 'utf8'));
const tarballName = `${platformPkg.name.replace(/^@/, '').replace(/\//g, '-')}-${version}.tgz`;
const files = new Set(Array.isArray(metaPkg.files) ? metaPkg.files : []);
files.add('vendor');
metaPkg.files = [...files];
metaPkg.optionalDependencies = {
[platformPkg.name]: `file:./vendor/${tarballName}`,
};
await fs.writeFile(`${metaPkgPath}`, `${JSON.stringify(metaPkg, null, 2)}\n`);
await fs.mkdir(path.join(metaDir, 'vendor'), { recursive: true });

const binName = path.basename(binaryPath);
const platformBinDir = path.join(platformsDir, platformPackage, 'bin');
const platformBinPath = path.join(platformBinDir, binName);
await fs.mkdir(platformBinDir, { recursive: true });
await fs.copyFile(binaryPath, platformBinPath);
if (!binName.endsWith('.exe')) {
await fs.chmod(platformBinPath, 0o755);
}

console.log(
JSON.stringify({
workroot,
metaDir,
platformDir: path.join(platformsDir, platformPackage),
binary: platformBinPath,
platformTarball: tarballName,
}),
);
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
Loading