From ff10deb88f58ce483f337c36e36b76812543a36e Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 13:56:16 +0530 Subject: [PATCH 1/6] feat: integrate Pages into self-contained alab binaries --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++ .github/workflows/release.yml | 38 ++++++++++++++++++++++++ .gitignore | 3 ++ .gitmodules | 3 ++ AGENTS.md | 26 ++++++++++++++++ README.md | 50 +++++++++++++++++++++++++++++-- USAGE.md | 56 +++++++++++++++++++++++++++++++++++ bun.lock | 29 ++++++++++++++++++ docs/integration.md | 27 +++++++++++++++++ docs/spec.md | 27 +++++++++++++++++ package.json | 23 ++++++++++++++ pins.json | 8 +++++ scripts/build-all.ts | 16 ++++++++++ scripts/smoke-binary.sh | 40 +++++++++++++++++++++++++ scripts/verify-pins.ts | 19 ++++++++++++ src/cli.ts | 50 +++++++++++++++++++++++++++++++ tests/cli.test.ts | 9 ++++++ vendor/pages | 1 + 18 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 AGENTS.md create mode 100644 USAGE.md create mode 100644 bun.lock create mode 100644 docs/integration.md create mode 100644 docs/spec.md create mode 100644 package.json create mode 100644 pins.json create mode 100644 scripts/build-all.ts create mode 100644 scripts/smoke-binary.sh create mode 100644 scripts/verify-pins.ts create mode 100644 src/cli.ts create mode 100644 tests/cli.test.ts create mode 160000 vendor/pages diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7523e1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + verify: + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + - os: ubuntu-24.04-arm + target: bun-linux-arm64 + - os: macos-14 + target: bun-darwin-arm64 + - os: macos-15-intel + target: bun-darwin-x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 + - run: bun install --frozen-lockfile + - run: bun run verify:pins + - run: bun test + - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts + - run: bun run test:binary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5961a50 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.10 + - run: bun install --frozen-lockfile + - run: bun run verify:pins + - run: bun test + - run: bun run build:all + - name: Package binaries + run: | + cd dist + for binary in alab-*; do + chmod +x "$binary" + cp "$binary" alab + tar -czf "$binary.tar.gz" alab + rm alab + done + sha256sum *.tar.gz > SHA256SUMS + - uses: softprops/action-gh-release@v2 + with: + files: | + dist/*.tar.gz + dist/SHA256SUMS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba34fb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a1a0b6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/pages"] + path = vendor/pages + url = https://github.com/agentlab-in/pages.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cacc5f6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# Alab CLI + +This repository owns command routing, exact tool pins, combined executable builds, and release automation for the `alab` CLI. + +## Boundaries + +- Tool repositories own their implementations. Do not fork tool behavior here. +- Integrations must use an exact version and commit SHA. +- A released artifact must not download a runtime or helper executable when used. +- Preserve child command arguments, standard input, standard output, standard error, and exit status. +- Add only platforms exercised by CI with a real compiled executable. +- Do not publish releases or tags without explicit authorization. + +## Workflow + +Use Bun 1.3.10 for dependency installation, tests, and compilation. After changing a pin or router, run: + +```bash +bun install --frozen-lockfile +bun run verify:pins +bun run test +bun run build +bun run test:binary +``` + +Run Git commands from this repository. Preserve unrelated work. Never commit credentials, generated `dist/` files, or local Alab state. diff --git a/README.md b/README.md index c3c3301..134e73e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,51 @@ # Alab CLI -Alab is the self-contained command line entry point for AgentLab tools. +Alab is one self-contained executable for AgentLab tools. The current release integrates [AgentLab Pages](https://github.com/agentlab-in/pages), a static site publisher backed by your own Cloudflare account. -The first integrated tool is [AgentLab Pages](https://github.com/agentlab-in/pages). Downloadable binaries and usage documentation are being prepared. +No separate Node.js, npm, Bun, Python, Go, Wrangler, or helper CLI installation is required to run a downloaded binary. + +## Install + +Download the archive matching your system from the repository's Releases page, verify it with `SHA256SUMS`, extract `alab`, and put it on your `PATH`. + +Supported release targets: + +| Operating system | CPU | +| --- | --- | +| macOS | Apple silicon, Intel | +| Linux | x86-64, ARM64 | + +Windows is not currently built or tested. + +```bash +alab --version +alab pages --help +alab pages setup +alab pages put ./site +``` + +`alab pages setup` guides Cloudflare API token validation, account selection, and project creation or reuse. Pages maintains authoritative state on one machine. A repeated `put` of the same directory retains its ID and URL. Cloudflare's default URL is used unless configuration explicitly supplies another base URL. + +See [USAGE.md](./USAGE.md) for setup, command behavior, and limitations. See [docs/integration.md](./docs/integration.md) for the internal tool interface and update procedure. + +## Build + +The repository pins Bun and every tool revision. Bun is a build dependency only. + +```bash +bun install --frozen-lockfile +bun run verify:pins +bun run test +bun run build +bun run test:binary +``` + +`bun run build:all` cross-compiles the four declared release targets into `dist/`. + +## Security + +Every published page is public. The optional Pages root password only obscures the root listing and does not protect direct page URLs. Never publish credentials or private content. + +## License + +MIT diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..263c472 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,56 @@ +# Usage + +## Consumer usage + +### Prerequisites + +Downloaded Alab binaries have no language runtime or helper CLI prerequisite. Publishing requires a Cloudflare account and an API token with `Account`, `Cloudflare Pages`, `Edit` permission. + +### Installation + +Download the archive for your operating system and CPU from Releases. Verify the archive against `SHA256SUMS`, extract it, and place `alab` in a directory on `PATH`. + +### Configuration + +Run: + +```bash +alab pages setup +``` + +The command links to Cloudflare's token creation page, accepts token input without echoing it, validates access, selects an account, and creates or reuses the configured Pages project. On macOS, the token is stored in Keychain. Other settings and Linux token storage use files under `~/.alab/` with restricted permissions. + +For noninteractive use, provide `CLOUDFLARE_API_TOKEN` and the required setup options. Missing input fails instead of waiting for a prompt. + +### Commands + +```text +alab pages setup [options] +alab pages put [dir] [--id ] [--dry-run] [--skip-deploy] [--json] +alab pages remove [id] [--dry-run] [--skip-deploy] [--json] +alab pages read [id] [--dir ] [--json] +alab pages list [--json] +alab pages open [id] +alab pages info +``` + +`delete` aliases `remove`, and `ls` aliases `list`. + +`put` copies a static directory into the local full snapshot and deploys it. Repeating it for the same directory uses `.alab/pages.json` to retain the same ID and URL. `remove` deletes the locally known page and redeploys the remaining snapshot. A deployment failure does not roll back the preceding local change, so retry the same command after fixing access. + +`--dry-run` skips Cloudflare deployment but still changes local state. `--skip-deploy` changes only local state. Use isolated `ALAB_HOME` and `ALAB_PAGES_CONTENT` directories for experiments. + +### Availability and limitations + +Release automation builds macOS binaries for Apple silicon and Intel, plus Linux binaries for x86-64 and ARM64. CI runs the compiled command smoke test natively on all four operating system and CPU combinations. + +Pages accepts ready-made static files only. It does not build frameworks, run servers, recover state across machines, download deployed pages, or manage custom domains. The local machine is authoritative. Every page URL is public. + +## Enterprise usage + +Alab can be integrated into internal agent workflows that need a stable, inspectable command surface for publishing static artifacts through organization-owned Cloudflare infrastructure. + +If you want to deploy this internally, integrate it with your existing systems, +adapt it to your environment, or discuss adjacent engineering work, book a call: + +https://cal.com/harshitsinghbhandari/30min diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..51e23ba --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@agentlab/alab-cli", + "dependencies": { + "@noble/hashes": "2.4.0", + "commander": "13.1.0", + }, + "devDependencies": { + "@types/bun": "1.3.10", + }, + }, + }, + "packages": { + "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/node": ["@types/node@26.5.1", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + } +} diff --git a/docs/integration.md b/docs/integration.md new file mode 100644 index 0000000..b378ed8 --- /dev/null +++ b/docs/integration.md @@ -0,0 +1,27 @@ +# Tool integration interface + +## Current interface + +The router imports `runCli(argv)` from the exact Pages `src/program.ts` revision and calls it in the same process when the first argument is `pages`. Pages receives the original `process.argv` array. + +This preserves every remaining argument and option, standard input for hidden interactive prompts, standard output including JSON, standard error and subprocess progress, the working directory and environment, and success or failure through the process exit status. + +No extraction is used. Bun embeds the router, Pages source, and JavaScript dependencies in the executable. Pages v2.0.0 deploys through the Cloudflare HTTP API and does not invoke Wrangler. + +## Pin and integrity policy + +`vendor/pages` is a Git submodule pinned by the parent commit. `pins.json` repeats its version, repository, commit SHA, and expected `package.json` SHA-256. `scripts/verify-pins.ts` rejects any mismatch before build or release. + +JavaScript packages use exact versions and a committed Bun lockfile. The release workflow uses a pinned Bun version and emits `SHA256SUMS` for downloadable archives. + +## Updating Pages + +1. Fetch tags inside `vendor/pages`. +2. Check out the intended immutable release commit. +3. Update the Pages version, commit, and package hash in `pins.json`. +4. Review the Pages CLI entry point and dependencies for interface or runtime changes. +5. Run every command in `AGENTS.md`, including the compiled binary smoke test. +6. Update public docs and the Alab version when behavior changes. +7. Commit the submodule pointer, pins, lockfile changes, code, and docs together. + +Never pin a moving branch or fetch tool code at runtime. diff --git a/docs/spec.md b/docs/spec.md new file mode 100644 index 0000000..44a9677 --- /dev/null +++ b/docs/spec.md @@ -0,0 +1,27 @@ +# Alab Pages integration specification + +## Outcome + +Distribute one `alab` executable per supported operating system and CPU. The executable routes `alab pages ...` to the pinned Pages implementation without requiring or downloading a runtime or helper CLI. + +## Decisions + +- Bun compile produces native executables and is used only in development and CI. +- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at commit `15355cdde65a8f536c48264b3239a097ecec8492`. +- Runtime dependencies are exact versions in `package.json` and `bun.lock`. +- Pages executes in-process. Arguments and process streams remain unchanged. Its rejection reaches one top-level error handler and sets exit code 1. +- `alab --version` reports the router version and every integrated tool version and SHA. +- Releases contain compressed binaries and a `SHA256SUMS` integrity manifest. +- CI runs native binary smoke tests on macOS ARM64, macOS Intel, Linux x86-64, and Linux ARM64. + +## Excluded + +Review integration, a plugin system, Pages recovery or pull behavior, framework builds, and custom-domain management are not part of this integration. + +## Completion checks + +1. Verify the Pages submodule SHA and package version. +2. Run router tests. +3. Compile the host executable. +4. Run Pages help, info, put, read, list, update, and remove against isolated temporary state with development tools absent from `PATH`. +5. Build all release targets and produce deterministic artifact names and checksums. diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e18ed7 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@agentlab/alab-cli", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "bun build --compile --minify --outfile dist/alab src/cli.ts", + "build:all": "bun scripts/build-all.ts", + "test": "bun test tests/cli.test.ts", + "test:binary": "bash scripts/smoke-binary.sh dist/alab", + "verify:pins": "bun scripts/verify-pins.ts" + }, + "dependencies": { + "@noble/hashes": "2.4.0", + "commander": "13.1.0" + }, + "devDependencies": { + "@types/bun": "1.3.10" + }, + "engines": { + "bun": "1.3.10" + } +} diff --git a/pins.json b/pins.json new file mode 100644 index 0000000..edb2735 --- /dev/null +++ b/pins.json @@ -0,0 +1,8 @@ +{ + "pages": { + "version": "2.0.0", + "repository": "https://github.com/agentlab-in/pages.git", + "commit": "15355cdde65a8f536c48264b3239a097ecec8492", + "packageSha256": "33359d14b47a323d4447890b4e5edf8700b25956e1a46f3843c6d0ab52287125" + } +} diff --git a/scripts/build-all.ts b/scripts/build-all.ts new file mode 100644 index 0000000..41cf14f --- /dev/null +++ b/scripts/build-all.ts @@ -0,0 +1,16 @@ +import { mkdirSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const root = resolve(import.meta.dir, ".."); +const dist = resolve(root, "dist"); +const targets = ["bun-darwin-arm64", "bun-darwin-x64", "bun-linux-x64", "bun-linux-arm64"]; + +rmSync(dist, { recursive: true, force: true }); +mkdirSync(dist, { recursive: true }); + +for (const target of targets) { + const name = target.replace(/^bun-/, ""); + const result = spawnSync("bun", ["build", "--compile", "--minify", `--target=${target}`, `--outfile=${resolve(dist, `alab-${name}`)}`, resolve(root, "src/cli.ts")], { stdio: "inherit" }); + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh new file mode 100644 index 0000000..bb98259 --- /dev/null +++ b/scripts/smoke-binary.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +binary="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT + +mkdir -p "$scratch/site" "$scratch/empty-path" +printf '

first

\n' > "$scratch/site/index.html" + +run_clean() { + env -i PATH="$scratch/empty-path" ALAB_HOME="$scratch/home" ALAB_PAGES_CONTENT="$scratch/content" "$binary" "$@" +} + +version="$(run_clean --version)" +[[ "$version" == *'pages 2.0.0 (15355cdde65a8f536c48264b3239a097ecec8492)'* ]] +run_clean pages --help >/dev/null +run_clean pages info >/dev/null + +first="$(run_clean pages put "$scratch/site" --skip-deploy --json)" +[[ "$first" =~ \"id\"[[:space:]]*:[[:space:]]*\"([a-z0-9]+)\" ]] +id="${BASH_REMATCH[1]}" +test -n "$id" +read_result="$(run_clean pages read --dir "$scratch/site" --json)" +[[ "$read_result" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] +list_result="$(run_clean pages list --json)" +[[ "$list_result" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] + +printf '

updated

\n' > "$scratch/site/index.html" +second="$(run_clean pages put "$scratch/site" --skip-deploy --json)" +[[ "$second" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] +removed="$(run_clean pages remove "$id" --skip-deploy --json)" +[[ "$removed" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] + +if run_clean unknown >/dev/null 2>&1; then + echo "unknown tool unexpectedly succeeded" >&2 + exit 1 +fi + +echo "compiled binary smoke test passed" diff --git a/scripts/verify-pins.ts b/scripts/verify-pins.ts new file mode 100644 index 0000000..6eb556d --- /dev/null +++ b/scripts/verify-pins.ts @@ -0,0 +1,19 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { spawnSync } from "node:child_process"; +import pins from "../pins.json"; +import pagesPackage from "../vendor/pages/package.json"; + +const root = resolve(import.meta.dir, ".."); +const packagePath = resolve(root, "vendor/pages/package.json"); +const revision = spawnSync("git", ["-C", resolve(root, "vendor/pages"), "rev-parse", "HEAD"], { encoding: "utf8" }); + +if (revision.status !== 0) throw new Error(revision.stderr.trim() || "Could not read Pages revision"); +if (revision.stdout.trim() !== pins.pages.commit) throw new Error(`Pages revision mismatch: ${revision.stdout.trim()}`); +if (pagesPackage.version !== pins.pages.version) throw new Error(`Pages version mismatch: ${pagesPackage.version}`); + +const packageHash = createHash("sha256").update(readFileSync(packagePath)).digest("hex"); +if (packageHash !== pins.pages.packageSha256) throw new Error(`Pages package hash mismatch: ${packageHash}`); + +console.log(`pages ${pins.pages.version} ${pins.pages.commit} sha256:${packageHash}`); diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..2b637dc --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +import alabPackage from "../package.json"; +import pins from "../pins.json"; +import pagesPackage from "../vendor/pages/package.json"; +import { runCli as runPagesCli } from "../vendor/pages/src/program.ts"; + +const HELP = `Alab ${alabPackage.version} + +Usage: alab [command] [options] + +Tools: + pages Publish static sites with AgentLab Pages + +Commands: + alab pages --help Show Pages commands and first-use guidance + alab --version Show Alab and integrated tool versions + alab help Show this help +`; + +export function versionText(): string { + return `alab ${alabPackage.version}\npages ${pagesPackage.version} (${pins.pages.commit})`; +} + +export async function main(argv = process.argv): Promise { + const args = argv.slice(2); + if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") { + process.stdout.write(HELP); + return 0; + } + if (args[0] === "--version" || args[0] === "-V") { + console.log(versionText()); + return 0; + } + if (args[0] !== "pages") { + console.error(`Error: unknown tool '${args[0]}'. Run 'alab --help'.`); + return 1; + } + + await runPagesCli(argv); + return process.exitCode ?? 0; +} + +if (import.meta.main) { + main().then((code) => { + process.exitCode = code; + }).catch((error: unknown) => { + console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + }); +} diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 0000000..57c10d7 --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from "bun:test"; +import { versionText } from "../src/cli.ts"; +import pins from "../pins.json"; + +describe("Alab router", () => { + test("reports combined and pinned Pages versions", () => { + expect(versionText()).toBe(`alab 0.1.0\npages 2.0.0 (${pins.pages.commit})`); + }); +}); diff --git a/vendor/pages b/vendor/pages new file mode 160000 index 0000000..15355cd --- /dev/null +++ b/vendor/pages @@ -0,0 +1 @@ +Subproject commit 15355cdde65a8f536c48264b3239a097ecec8492 From 8df7d5b4e7c1628f1f438152820faf1e12f73253 Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 13:57:06 +0530 Subject: [PATCH 2/6] fix: scope bun tests to integration suite --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7523e1..c28bb45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,6 @@ jobs: bun-version: 1.3.10 - run: bun install --frozen-lockfile - run: bun run verify:pins - - run: bun test + - run: bun run test - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts - run: bun run test:binary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5961a50..37323a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: bun-version: 1.3.10 - run: bun install --frozen-lockfile - run: bun run verify:pins - - run: bun test + - run: bun run test - run: bun run build:all - name: Package binaries run: | From 820d20feb19e8afc86da6a4b4f335347bfaadfcb Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 14:03:45 +0530 Subject: [PATCH 3/6] ci: retain verified platform binaries --- .github/workflows/ci.yml | 5 +++++ README.md | 2 ++ docs/spec.md | 1 + 3 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28bb45..d1e07cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,3 +34,8 @@ jobs: - run: bun run test - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts - run: bun run test:binary + - uses: actions/upload-artifact@v4 + with: + name: alab-${{ matrix.target }} + path: dist/alab + if-no-files-found: error diff --git a/README.md b/README.md index 134e73e..308e0e9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ bun run test:binary `bun run build:all` cross-compiles the four declared release targets into `dist/`. +Every CI run retains its four verified platform binaries as downloadable workflow artifacts. Tagged releases package the same target set into archives with a `SHA256SUMS` manifest. + ## Security Every published page is public. The optional Pages root password only obscures the root listing and does not protect direct page URLs. Never publish credentials or private content. diff --git a/docs/spec.md b/docs/spec.md index 44a9677..f1de1af 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -25,3 +25,4 @@ Review integration, a plugin system, Pages recovery or pull behavior, framework 3. Compile the host executable. 4. Run Pages help, info, put, read, list, update, and remove against isolated temporary state with development tools absent from `PATH`. 5. Build all release targets and produce deterministic artifact names and checksums. +6. Retain each natively tested binary as a downloadable CI artifact. From 3cae502194933c99b70dcad3a6fc074667674c57 Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 14:17:02 +0530 Subject: [PATCH 4/6] fix: harden binary release verification --- .github/workflows/ci.yml | 6 +-- .github/workflows/release.yml | 55 +++++++++++++++------ docs/spec.md | 2 +- package.json | 2 +- pins.json | 2 +- scripts/smoke-binary.sh | 90 +++++++++++++++++++++++++++-------- tests/cli.test.ts | 9 ---- tests/router.test.ts | 37 ++++++++++++++ vendor/pages | 2 +- 9 files changed, 154 insertions(+), 51 deletions(-) delete mode 100644 tests/cli.test.ts create mode 100644 tests/router.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1e07cf..791066f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,10 +23,10 @@ jobs: target: bun-darwin-x64 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: submodules: true - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.10 - run: bun install --frozen-lockfile @@ -34,7 +34,7 @@ jobs: - run: bun run test - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts - run: bun run test:binary - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: alab-${{ matrix.target }} path: dist/alab diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37323a5..f1dee68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,33 +5,60 @@ on: tags: ["v*"] permissions: - contents: write + contents: read jobs: build: - runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + - os: ubuntu-24.04-arm + target: bun-linux-arm64 + - os: macos-14 + target: bun-darwin-arm64 + - os: macos-15-intel + target: bun-darwin-x64 + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: submodules: true - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.10 - run: bun install --frozen-lockfile - run: bun run verify:pins - run: bun run test - - run: bun run build:all - - name: Package binaries + - run: bun build --compile --minify --target=${{ matrix.target }} --outfile=dist/alab src/cli.ts + - run: bun run test:binary + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: alab-${{ matrix.target }} + path: dist/alab + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + path: artifacts + - name: Package exact tested binaries run: | - cd dist - for binary in alab-*; do - chmod +x "$binary" - cp "$binary" alab - tar -czf "$binary.tar.gz" alab - rm alab + mkdir -p dist staging + for artifact_dir in artifacts/alab-bun-*; do + target="${artifact_dir##*/alab-bun-}" + install -m 0755 "$artifact_dir/alab" staging/alab + touch -d '@0' staging/alab + tar --sort=name --format=ustar --owner=0 --group=0 --numeric-owner --mtime='@0' -C staging -cf - alab | gzip -n > "dist/alab-$target.tar.gz" done - sha256sum *.tar.gz > SHA256SUMS - - uses: softprops/action-gh-release@v2 + LC_ALL=C sha256sum dist/*.tar.gz > dist/SHA256SUMS + - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: | dist/*.tar.gz diff --git a/docs/spec.md b/docs/spec.md index f1de1af..03c94c0 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -7,7 +7,7 @@ Distribute one `alab` executable per supported operating system and CPU. The exe ## Decisions - Bun compile produces native executables and is used only in development and CI. -- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at commit `15355cdde65a8f536c48264b3239a097ecec8492`. +- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at commit `33c4c004ec9e854f3521750ab6e26ad935758c1b`. - Runtime dependencies are exact versions in `package.json` and `bun.lock`. - Pages executes in-process. Arguments and process streams remain unchanged. Its rejection reaches one top-level error handler and sets exit code 1. - `alab --version` reports the router version and every integrated tool version and SHA. diff --git a/package.json b/package.json index 3e18ed7..f0edcd5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "bun build --compile --minify --outfile dist/alab src/cli.ts", "build:all": "bun scripts/build-all.ts", - "test": "bun test tests/cli.test.ts", + "test": "bun test tests/router.test.ts", "test:binary": "bash scripts/smoke-binary.sh dist/alab", "verify:pins": "bun scripts/verify-pins.ts" }, diff --git a/pins.json b/pins.json index edb2735..c7c2581 100644 --- a/pins.json +++ b/pins.json @@ -2,7 +2,7 @@ "pages": { "version": "2.0.0", "repository": "https://github.com/agentlab-in/pages.git", - "commit": "15355cdde65a8f536c48264b3239a097ecec8492", + "commit": "33c4c004ec9e854f3521750ab6e26ad935758c1b", "packageSha256": "33359d14b47a323d4447890b4e5edf8700b25956e1a46f3843c6d0ab52287125" } } diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh index bb98259..83c0c1a 100644 --- a/scripts/smoke-binary.sh +++ b/scripts/smoke-binary.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash set -euo pipefail +trap 'echo "smoke assertion failed at line $LINENO: status=$status stdout=$stdout stderr=$stderr" >&2' ERR binary="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +status=0 +stdout="" +stderr="" scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT @@ -12,29 +16,73 @@ run_clean() { env -i PATH="$scratch/empty-path" ALAB_HOME="$scratch/home" ALAB_PAGES_CONTENT="$scratch/content" "$binary" "$@" } -version="$(run_clean --version)" -[[ "$version" == *'pages 2.0.0 (15355cdde65a8f536c48264b3239a097ecec8492)'* ]] -run_clean pages --help >/dev/null -run_clean pages info >/dev/null +capture() { + set +e + run_clean "$@" >"$scratch/stdout" 2>"$scratch/stderr" + status=$? + set -e + stdout="$(<"$scratch/stdout")" + stderr="$(<"$scratch/stderr")" +} + +assert_success() { + [[ "$status" -eq 0 ]] + [[ -z "$stderr" ]] +} + +assert_json() { + [[ "$stdout" == \{*\} || "$stdout" == \[*\] ]] +} + +capture --version +assert_success +[[ "$stdout" == $'alab 0.1.0\npages 2.0.0 (33c4c004ec9e854f3521750ab6e26ad935758c1b)' ]] + +capture pages --help +assert_success +[[ "$stdout" == *'Usage: alab pages [options] [command]'* ]] +[[ "$stdout" == *'setup [options]'* ]] +[[ "$stdout" == *'put [options] [dir]'* ]] -first="$(run_clean pages put "$scratch/site" --skip-deploy --json)" -[[ "$first" =~ \"id\"[[:space:]]*:[[:space:]]*\"([a-z0-9]+)\" ]] -id="${BASH_REMATCH[1]}" -test -n "$id" -read_result="$(run_clean pages read --dir "$scratch/site" --json)" -[[ "$read_result" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] -list_result="$(run_clean pages list --json)" -[[ "$list_result" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] +capture pages info +assert_success +[[ "$stdout" == *'CF token missing'* ]] + +capture pages put "$scratch/site" --id Alpha9 --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] +[[ "${stdout#*\"id\"}" != *'"id"'* ]] + +capture pages read --dir "$scratch/site" --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages list --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] printf '

updated

\n' > "$scratch/site/index.html" -second="$(run_clean pages put "$scratch/site" --skip-deploy --json)" -[[ "$second" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] -removed="$(run_clean pages remove "$id" --skip-deploy --json)" -[[ "$removed" =~ \"id\"[[:space:]]*:[[:space:]]*\"$id\" ]] - -if run_clean unknown >/dev/null 2>&1; then - echo "unknown tool unexpectedly succeeded" >&2 - exit 1 -fi +capture pages put "$scratch/site" --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages remove alpha9 --skip-deploy --json +assert_success +assert_json +[[ "$stdout" =~ \"id\"[[:space:]]*:[[:space:]]*\"alpha9\" ]] + +capture pages read missing --json +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == 'Error: Page missing is not in the local store' ]] + +capture unknown +[[ "$status" -eq 1 ]] +[[ -z "$stdout" ]] +[[ "$stderr" == "Error: unknown tool 'unknown'. Run 'alab --help'." ]] echo "compiled binary smoke test passed" diff --git a/tests/cli.test.ts b/tests/cli.test.ts deleted file mode 100644 index 57c10d7..0000000 --- a/tests/cli.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { versionText } from "../src/cli.ts"; -import pins from "../pins.json"; - -describe("Alab router", () => { - test("reports combined and pinned Pages versions", () => { - expect(versionText()).toBe(`alab 0.1.0\npages 2.0.0 (${pins.pages.commit})`); - }); -}); diff --git a/tests/router.test.ts b/tests/router.test.ts new file mode 100644 index 0000000..aef9753 --- /dev/null +++ b/tests/router.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; +import { versionText } from "../src/cli.ts"; +import pins from "../pins.json"; + +function run(args: string[]) { + return Bun.spawnSync([process.execPath, "src/cli.ts", ...args], { + cwd: import.meta.dir + "/..", + env: {}, + stdout: "pipe", + stderr: "pipe", + }); +} + +describe("Alab router", () => { + test("reports combined and pinned Pages versions", () => { + expect(versionText()).toBe(`alab 0.1.0\npages 2.0.0 (${pins.pages.commit})`); + }); + + test("routes Pages help with exact successful streams", () => { + const result = run(["pages", "--help"]); + const stdout = result.stdout.toString(); + + expect(result.exitCode).toBe(0); + expect(result.stderr.toString()).toBe(""); + expect(stdout.match(/^Usage:/gm)).toHaveLength(1); + expect(stdout).toContain("Usage: alab pages [options] [command]"); + expect(stdout).toContain("put [options] [dir]"); + }); + + test("returns an exact error status without polluting stdout", () => { + const result = run(["unknown"]); + + expect(result.exitCode).toBe(1); + expect(result.stdout.toString()).toBe(""); + expect(result.stderr.toString()).toBe("Error: unknown tool 'unknown'. Run 'alab --help'.\n"); + }); +}); diff --git a/vendor/pages b/vendor/pages index 15355cd..33c4c00 160000 --- a/vendor/pages +++ b/vendor/pages @@ -1 +1 @@ -Subproject commit 15355cdde65a8f536c48264b3239a097ecec8492 +Subproject commit 33c4c004ec9e854f3521750ab6e26ad935758c1b From 28c036b02f0d72de1d85841629f7083803318e3f Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 14:18:22 +0530 Subject: [PATCH 5/6] chore: pin merged Pages revision --- docs/spec.md | 2 +- pins.json | 2 +- scripts/smoke-binary.sh | 2 +- vendor/pages | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index 03c94c0..83ffaca 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -7,7 +7,7 @@ Distribute one `alab` executable per supported operating system and CPU. The exe ## Decisions - Bun compile produces native executables and is used only in development and CI. -- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at commit `33c4c004ec9e854f3521750ab6e26ad935758c1b`. +- Pages v2.0.0 plus its side-effect-free integration entry point is pinned as a Git submodule at merged commit `c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8`. - Runtime dependencies are exact versions in `package.json` and `bun.lock`. - Pages executes in-process. Arguments and process streams remain unchanged. Its rejection reaches one top-level error handler and sets exit code 1. - `alab --version` reports the router version and every integrated tool version and SHA. diff --git a/pins.json b/pins.json index c7c2581..dcfcd37 100644 --- a/pins.json +++ b/pins.json @@ -2,7 +2,7 @@ "pages": { "version": "2.0.0", "repository": "https://github.com/agentlab-in/pages.git", - "commit": "33c4c004ec9e854f3521750ab6e26ad935758c1b", + "commit": "c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8", "packageSha256": "33359d14b47a323d4447890b4e5edf8700b25956e1a46f3843c6d0ab52287125" } } diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh index 83c0c1a..6d7d4f9 100644 --- a/scripts/smoke-binary.sh +++ b/scripts/smoke-binary.sh @@ -36,7 +36,7 @@ assert_json() { capture --version assert_success -[[ "$stdout" == $'alab 0.1.0\npages 2.0.0 (33c4c004ec9e854f3521750ab6e26ad935758c1b)' ]] +[[ "$stdout" == $'alab 0.1.0\npages 2.0.0 (c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8)' ]] capture pages --help assert_success diff --git a/vendor/pages b/vendor/pages index 33c4c00..c6a71e8 160000 --- a/vendor/pages +++ b/vendor/pages @@ -1 +1 @@ -Subproject commit 33c4c004ec9e854f3521750ab6e26ad935758c1b +Subproject commit c6a71e826bfecb9f7c65c88c1f1d28ba23f72bd8 From 5d9f73d4ee0fa3d4264208d85d485420f67d4f3a Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Mon, 14 Sep 2026 14:19:42 +0530 Subject: [PATCH 6/6] test: assert actual Pages unknown id error --- scripts/smoke-binary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/smoke-binary.sh b/scripts/smoke-binary.sh index 6d7d4f9..591d1ac 100644 --- a/scripts/smoke-binary.sh +++ b/scripts/smoke-binary.sh @@ -78,7 +78,7 @@ assert_json capture pages read missing --json [[ "$status" -eq 1 ]] [[ -z "$stdout" ]] -[[ "$stderr" == 'Error: Page missing is not in the local store' ]] +[[ "$stderr" == 'Error: Unknown page id "missing". Run agentlab-pages list.' ]] capture unknown [[ "$status" -eq 1 ]]