Skip to content
Draft
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
88 changes: 88 additions & 0 deletions .github/scripts/smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# The compiled binary must behave exactly like the same sources under bun.
#
# A --version check would exercise almost none of the compiled surface: the
# binary embeds a JavaScript engine for linkedom and fetch, and the port had to
# hand-roll argument parsing, DOM traversal, the TSV renderer and the --where
# evaluator. So every output mode is compared byte-for-byte against the bun
# entry point, which the test suite already covers.
set -uo pipefail

AX=${AX:-./ax}
pass=0
fail=0

work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

cat >"$work/page.html" <<'HTML'
<html><body><article>
<h1>Title</h1><p>Hello <a href="/x">link</a> world.</p>
<ul><li class="item">one</li><li class="item">two</li></ul>
<table class="stats"><tr><th>Name</th><th>Stars</th></tr>
<tr><td>a</td><td>100</td></tr><tr><td>b</td><td>5</td></tr></table>
</article></body></html>
HTML

printf '<table><tr><th colspan=2>H</th></tr><tr><td>a</td><td>b</td></tr></table>' \
>"$work/spans.html"

check() {
local desc=$1
shift
local got want
got=$("$AX" "$@" 2>&1)
want=$(bun src/index.ts "$@" 2>&1)
if [ "$got" = "$want" ]; then
pass=$((pass + 1))
echo "ok $desc"
else
fail=$((fail + 1))
echo "FAIL $desc"
diff <(printf '%s\n' "$want") <(printf '%s\n' "$got") || true
fi
}

page=$work/page.html

check version --version
check help --help
check agent-context agent-context
check text "$page" '.item' --text
check html "$page" '.item' --html
check attr "$page" a --attr href
check count "$page" '.item' --count
check markdown "$page" --md
check outline "$page" --outline
check locate "$page" --locate two
check table "$page" 'table.stats' --table
check table-json "$page" 'table.stats' --table --json
check colspan "$work/spans.html" --table
check where "$page" 'table.stats' --table --where 'Stars >= 50'
check where-regex "$page" 'table.stats' --table --where 'Name ~ /a/'
check where-and "$page" 'table.stats' --table --where 'Stars > 50 && Name == "a"'
check json "$page" '.item' --json
check envelope "$page" '.item' --json-envelope
check row "$page" '.item' --row 'v=, c=@class'
check paging "$page" '.item' --text --limit 1 --offset 1
check budget "$page" --md --budget 5
check bad-selector "$page" '>>>' --text
check no-such-file "$work/missing.html" --text
check no-selector "$page"
check unknown-flag "$page" '.item' --text --bogus

# stdin has no flag to compare through check()
got=$(printf '<p class=x>hi</p>' | "$AX" - '.x' --text 2>&1)
want=$(printf '<p class=x>hi</p>' | bun src/index.ts - '.x' --text 2>&1)
if [ "$got" = "$want" ]; then
pass=$((pass + 1))
echo "ok stdin"
else
fail=$((fail + 1))
echo "FAIL stdin"
diff <(printf '%s\n' "$want") <(printf '%s\n' "$got") || true
fi

echo
echo "pass=$pass fail=$fail"
[ "$fail" -eq 0 ]
79 changes: 77 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

# A push to a branch with an open PR would otherwise run this twice.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -19,7 +23,78 @@ jobs:
- run: bun install --frozen-lockfile
- name: nix/bun.nix must be in sync with bun.lock
run: git diff --exit-code nix/bun.nix
- name: src/agent-context.gen.ts must be in sync with agent-context.txt
run: bun run gen && git diff --exit-code src/agent-context.gen.ts
- run: bunx tsc --noEmit
- run: bunx oxfmt --check .
- run: bun test
- run: bun build src/index.ts --compile --outfile ax && ./ax --version

# A job per OS: a scriptc --dynamic binary is host-native (the embedded engine
# archive is built per target, and cross-compiling one is not supported), so
# each platform needs its own runner. The first build on a runner compiles
# that engine from source, which takes minutes and needs a C toolchain the
# `test` job has no other use for.
build:
name: compile (${{ matrix.target }}, ${{ matrix.builder }})
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, target: linux-x64, builder: scriptc }
- { os: ubuntu-24.04-arm, target: linux-arm64, builder: scriptc }
- { os: macos-latest, target: darwin-arm64, builder: scriptc }
- { os: macos-15-intel, target: darwin-x64, builder: scriptc }
# Windows builds with bun instead: scriptc has not ported the socket
# stack (net/http/https/tls), which is where ax's fetch lives. The
# sources are the same, and the bun path keeps the full-fidelity
# lib/platform.ts (TextDecoder labels, the stdout drain) that the
# compiled builds trade away.
- { os: windows-latest, target: windows-x64, builder: bun, ext: .exe }
defaults:
run:
# One script for every OS; Windows runners ship Git Bash.
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
# scriptc's compiler runs on Node, not Bun: typescript@7's synchronous RPC
# channel reads `stdout._handle.fd` off a spawned child.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: matrix.builder == 'scriptc'
with:
node-version: 22
- run: bun install --frozen-lockfile

- name: Install the C toolchain scriptc drives (Linux)
if: matrix.builder == 'scriptc' && runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y clang cmake

- name: Install the C toolchain scriptc drives (macOS)
if: matrix.builder == 'scriptc' && runner.os == 'macOS'
run: brew install cmake

- name: Compile (scriptc)
if: matrix.builder == 'scriptc'
run: bun run build ax${{ matrix.ext }}

- name: Compile (bun)
if: matrix.builder == 'bun'
run: bun build src/index.ts --compile --outfile ax${{ matrix.ext }}

- name: Smoke test — output must match the bun entry point
env:
AX: ./ax${{ matrix.ext }}
run: bash .github/scripts/smoke.sh

- name: Report binary size
run: ls -l ax${{ matrix.ext }} | awk '{print $5, $9}'

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ax-${{ matrix.target }}
path: ax${{ matrix.ext }}
if-no-files-found: error
12 changes: 10 additions & 2 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ name: Nix flake

on:
push:
branches: [main]
paths:
- '**.nix'
- 'flake.lock'
Expand Down Expand Up @@ -39,7 +38,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand All @@ -60,8 +59,17 @@ jobs:
- name: nix flake check --all-systems
run: nix flake check --all-systems --no-build

# Compiles the embedded JavaScript engine from the vendored sources, so
# this is minutes rather than seconds.
- name: nix build .#default
run: nix build .#default --print-build-logs

- name: nix run .#default -- --version
run: nix run .#default -- --version

- name: nix run .#default -- --md
run: |
printf '<article><h1>Title</h1><p>Hello</p></article>' > page.html
got=$(nix run .#default -- page.html --md)
want=$(printf '# Title\n\nHello')
[ "$got" = "$want" ] || { printf 'got:\n%s\nwant:\n%s\n' "$got" "$want"; exit 1; }
89 changes: 79 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,75 @@ permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
# One runner per target: ax needs scriptc's --dynamic (linkedom and fetch run
# in the embedded JavaScript engine), and a --dynamic build is host-native —
# the engine archive is built per target and cross-compiling one is not
# supported. That rules out the old single-runner --target sweep.
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- { os: macos-latest, target: darwin-arm64, builder: scriptc }
- { os: macos-15-intel, target: darwin-x64, builder: scriptc }
- { os: ubuntu-latest, target: linux-x64, builder: scriptc }
- { os: ubuntu-24.04-arm, target: linux-arm64, builder: scriptc }
# Windows ships a bun-compiled binary: scriptc has not ported the
# socket stack (net/http/https/tls), which is where ax's fetch lives.
- { os: windows-latest, target: windows-x64, builder: bun, ext: .exe }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14

# scriptc's compiler runs on Node, not Bun.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: matrix.builder == 'scriptc'
with:
node-version: 22
- run: bun install --frozen-lockfile

- name: Install the C toolchain scriptc drives (Linux)
if: matrix.builder == 'scriptc' && runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y clang cmake

- name: Install the C toolchain scriptc drives (macOS)
if: matrix.builder == 'scriptc' && runner.os == 'macOS'
run: brew install cmake

- name: Compile (scriptc)
if: matrix.builder == 'scriptc'
run: bun run build "ax-${{ matrix.target }}${{ matrix.ext }}"

- name: Compile (bun)
if: matrix.builder == 'bun'
run: bun build src/index.ts --compile --outfile "ax-${{ matrix.target }}${{ matrix.ext }}"

# Never publish a binary that has not answered correctly on its own
# platform.
- name: Smoke test
env:
AX: ./ax-${{ matrix.target }}${{ matrix.ext }}
run: bash .github/scripts/smoke.sh

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ax-${{ matrix.target }}
path: ax-${{ matrix.target }}${{ matrix.ext }}
if-no-files-found: error

release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Tag must match the version in package.json
run: |
v=$(jq -r .version package.json)
Expand All @@ -28,15 +86,26 @@ jobs:
exit 1
fi

- name: Build binaries for all targets
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
merge-multiple: true

- name: Collect binaries
run: |
mkdir -p dist
for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 windows-x64; do
out="dist/ax-$t"
[ "$t" = "windows-x64" ] && out="$out.exe"
bun build src/index.ts --compile --target="bun-$t" --outfile "$out"
done
find artifacts -type f -name 'ax-*' -exec mv {} dist/ \;
chmod +x dist/ax-*
ls -lh dist/
missing=
for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do
[ -f "dist/ax-$t" ] || missing="$missing ax-$t"
done
[ -f dist/ax-windows-x64.exe ] || missing="$missing ax-windows-x64.exe"
if [ -n "$missing" ]; then
echo "missing release assets:$missing" >&2
exit 1
fi

- name: Generate checksums
run: |
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
# compiled single-file binary
# compiled single-file binaries
/ax
/ax-scriptc
*.log
dist/
content.gen.ts
Expand All @@ -15,3 +16,4 @@ website/src/apple-touch-icon.png
# Nix build result symlinks
/result
/result-*
.scriptc-build/
Loading
Loading