From b114df30a814f5d61797d830943511e7ccfa4684 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 18 Aug 2026 15:59:30 +0800 Subject: [PATCH 1/2] Publish Alpha CLI tarball and public Node test CI. Why: Strangers could clone the repo and run Node tests, but there was no tagged artifact to download and no public CI badge. The README still said there was no GitHub Release. Changed: Added CHANGELOG.md and set the package version to 0.1.0-alpha.1. scripts/package_cli.sh builds a versioned CLI tarball without the macOS app. .github/workflows/ci.yml runs verify:public-surface, test:broker-core, test:client, and test:harness-adoption on ubuntu-latest and does not run test:app. .github/workflows/release.yml attaches the CLI tarball when a matching version tag is pushed. README, getting-started, SECURITY, and specs record the Alpha release surface. Verification: npm run agent:verify -- --profile spec-only --paths CHANGELOG.md,README.md,SECURITY.md,CONTRIBUTING.md,docs/getting-started.md,docs/status.md,docs/test/front-door.test.mjs,.agents/manifests/specs.yaml,.agents/verify/spec-only.yaml,.github/workflows/ci.yml,.github/workflows/release.yml,scripts/package_cli.sh,package.json,package-lock.json,spec/README.md,spec/build-and-test.md,spec/project-structure.md --session-dir task-sessions/20260818-alpha-release-ci node --test docs/test/front-door.test.mjs npm run verify:public-surface npm run test:broker-core npm run test:client npm run test:harness-adoption Affected: CHANGELOG.md scripts/package_cli.sh .github/workflows/ci.yml .github/workflows/release.yml package.json package-lock.json README.md SECURITY.md CONTRIBUTING.md docs/getting-started.md docs/status.md docs/test/front-door.test.mjs .agents/manifests/specs.yaml .agents/verify/spec-only.yaml spec/README.md spec/build-and-test.md spec/project-structure.md Refs: https://github.com/fiveonecode/simulator-broker spec/build-and-test.md Session: task-sessions/20260818-alpha-release-ci --- .agents/manifests/specs.yaml | 6 ++ .agents/verify/spec-only.yaml | 4 +- .github/workflows/ci.yml | 39 +++++++++++++ .github/workflows/release.yml | 60 ++++++++++++++++++++ CHANGELOG.md | 34 +++++++++++ CONTRIBUTING.md | 3 +- README.md | 12 +++- SECURITY.md | 7 ++- docs/getting-started.md | 9 ++- docs/status.md | 9 +-- docs/test/front-door.test.mjs | 79 +++++++++++++++++++++++++- package-lock.json | 4 +- package.json | 3 +- scripts/package_cli.sh | 104 ++++++++++++++++++++++++++++++++++ spec/README.md | 1 + spec/build-and-test.md | 10 ++++ spec/project-structure.md | 7 ++- 17 files changed, 371 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100755 scripts/package_cli.sh diff --git a/.agents/manifests/specs.yaml b/.agents/manifests/specs.yaml index 2e029cd..b1035a6 100644 --- a/.agents/manifests/specs.yaml +++ b/.agents/manifests/specs.yaml @@ -1,6 +1,8 @@ id: specs globs: + - .github/workflows/** - .gitignore + - CHANGELOG.md - CODE_OF_CONDUCT.md - CONTRIBUTING.md - LICENSE @@ -11,6 +13,7 @@ globs: - examples/**/.simulator-broker/*.json - spec/** - references/** + - scripts/package_cli.sh owner: spec-steward required_skills: - harness-engineering @@ -24,7 +27,9 @@ primary_specs: verification_profile: spec-only commit_required: true allowed_paths: + - .github/workflows/** - .gitignore + - CHANGELOG.md - CODE_OF_CONDUCT.md - CONTRIBUTING.md - LICENSE @@ -35,6 +40,7 @@ allowed_paths: - examples/**/.simulator-broker/*.json - spec/** - references/** + - scripts/package_cli.sh forbidden_paths: [] required_evidence: - structured-git-commit diff --git a/.agents/verify/spec-only.yaml b/.agents/verify/spec-only.yaml index 6ff4a93..d49ec11 100644 --- a/.agents/verify/spec-only.yaml +++ b/.agents/verify/spec-only.yaml @@ -4,9 +4,9 @@ covers_scenarios: [] covers_boundaries: [] commands: - id: spec-diff-check - run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md docs spec .agents agent-harness package.json package-lock.json .gitignore .codex script scripts references app broker-core client && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md docs spec .agents agent-harness package.json package-lock.json .gitignore .codex script scripts references app broker-core client + run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client - id: public-front-door-check - run: node --test docs/test/front-door.test.mjs + run: node --test docs/test/*.test.mjs - id: instruction-parity-check run: cmp -s AGENTS.md CLAUDE.md || { echo "AGENTS.md and CLAUDE.md must stay identical." >&2; exit 1; } required_artifacts: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..93ff3dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +# Public Node test surface. This job is intentionally cheap: Ubuntu, no +# npm install (the repo has no runtime dependencies), and no macOS app suite. +name: Node tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: node-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + node: + name: broker-core client harness-adoption public-surface + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Public-surface scan + run: npm run verify:public-surface + + - name: broker-core tests + run: npm run test:broker-core + + - name: client tests + run: npm run test:client + + - name: harness-adoption tests + run: npm run test:harness-adoption diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5e77a3f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +# Publish the Alpha CLI tarball when a version tag is pushed. +# After merge, create the matching tag (example: v0.1.0-alpha.1). +# This workflow does not build or attach the macOS app. +name: Release CLI + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + name: attach CLI tarball + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Require tag to match package.json + run: | + expected="v$(node -p "require('./package.json').version")" + if [[ "${GITHUB_REF_NAME}" != "$expected" ]]; then + echo "Tag ${GITHUB_REF_NAME} does not match package.json (${expected})." >&2 + exit 1 + fi + + - name: Public Node test surface + run: | + npm run verify:public-surface + npm run test:broker-core + npm run test:client + npm run test:harness-adoption + + - name: Package CLI tarball + run: npm run package:cli + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version="$(node -p "require('./package.json').version")" + asset="artifacts/cli/simulator-broker-${version}-cli.tar.gz" + checksum="${asset}.sha256" + prerelease_args=() + if [[ "$version" == *alpha* || "$version" == *beta* || "$version" == *rc* ]]; then + prerelease_args+=(--prerelease) + fi + gh release create "${GITHUB_REF_NAME}" \ + --title "Simulator Broker ${GITHUB_REF_NAME}" \ + --notes "Alpha CLI tarball. Extract it and run \`./bin/simbroker --help\`. Node.js 20 or newer is required. macOS and Xcode are still required to create and run iOS Simulators. This release is not a Homebrew formula, notarized app, or npm package. See CHANGELOG.md." \ + "${prerelease_args[@]}" \ + "$asset" \ + "$checksum" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f5ca428 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to Simulator Broker are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0-alpha.1] - 2026-08-18 + +First tagged Alpha. The CLI, local `brokerd` service, and macOS operator app +already exist on `main`; this release names that surface and attaches a +downloadable CLI tarball. + +### Added + +- Public Node test workflow on GitHub-hosted Ubuntu for + `verify:public-surface`, `test:broker-core`, `test:client`, and + `test:harness-adoption`. That job does not run the macOS app suite. +- `scripts/package_cli.sh` (`npm run package:cli`) builds a versioned CLI + tarball without XcodeGen or an app build. +- Tag-driven GitHub Release workflow that attaches the CLI tarball and its + SHA-256 checksum. Alpha tags are published as pre-releases. +- CLI-only install through `bash scripts/install_local.sh --cli-only`, with + PATH persistence through a Homebrew prefix bin or one guarded login-profile + snippet. +- Human-readable `simbroker` help and `simbroker doctor`, with `--json` for + machine payloads. +- Public-patches contributing track: Node.js 20 and the Node test suites, with + no harness session required. + +### Notes + +- Alpha: macOS and Xcode are still required to create and run iOS Simulators. +- This release is not a Homebrew formula, notarized app, or npm package. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0a2b1d..a21d52c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,8 @@ npm run test:harness-adoption ``` App work also needs XcodeGen and `npm run test:app`. The full suite is -`npm test`. +`npm test`. The same Node suites run on GitHub-hosted Ubuntu CI. That job +does not run `npm run test:app`. You do not need to run `agent:context`, `agent:verify`, or `agent:complete`, and you do not need to create a task session directory. diff --git a/README.md b/README.md index ce4acd2..0a6a466 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A local control plane so humans, AI agents, and CI jobs can share iOS Simulators on one Mac without stealing devices from each other. **Status:** Alpha · macOS only · Xcode required +[![Node tests](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml/badge.svg)](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml) Simulator Broker leases simulator aliases by *purpose* (for example `agent-ui-session` or `manual-testing`) instead of hard-coding UDIDs. A local @@ -37,8 +38,15 @@ Otherwise the installer writes `~/.local/bin/simbroker` and one guarded login-shell PATH line. Open a new terminal if this shell still cannot resolve `simbroker`. `source .../env.sh` remains a fallback. -Xcode is still required to create and run iOS Simulators. There is no Homebrew -formula, npm package, or GitHub Release yet. +To try a tagged build without cloning, download +`simulator-broker--cli.tar.gz` from +[Releases](https://github.com/fiveonecode/simulator-broker/releases), extract +it, and run `./bin/simbroker --help`. Node.js 20+ is still required. + +Xcode is still required to create and run iOS Simulators. Alpha CLI tarballs +are attached to +[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases). +There is no Homebrew formula or npm package yet. `simbroker` help and `simbroker doctor` print human-readable text by default. Pass `--json` for machine-readable payloads. diff --git a/SECURITY.md b/SECURITY.md index e8ba3d5..fa8657c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,10 @@ ## Supported Versions -Security fixes are considered for the current `main` branch. Tagged release -support will be documented here when this project starts publishing versioned -binary or package releases. +Security fixes are considered for the current `main` branch and for the latest +tagged Alpha (`0.1.0-alpha.1`). Older Alpha tags are not supported. The +published artifact is the CLI tarball on GitHub Releases, not a Homebrew +formula, notarized app, or npm package. ## Reporting A Vulnerability diff --git a/docs/getting-started.md b/docs/getting-started.md index 54bfe48..e75076d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -28,6 +28,12 @@ simbroker --help This copies `broker-core`, `client`, and `package.json` into the install prefix and writes a `simbroker` wrapper. It does not run XcodeGen or build the app. +To install from a tagged Alpha without cloning, download +`simulator-broker--cli.tar.gz` from +[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases), +extract it, and run `./bin/simbroker --help`. That archive is the Node CLI +only. + - If Homebrew is present and `$(brew --prefix)/bin` is writable, the wrapper is installed there so a new login shell already has it on `PATH`. - Otherwise the wrapper is installed to `~/.local/bin` and the installer @@ -135,7 +141,8 @@ SIMBROKER_DISTRIBUTION_SIGNING_IDENTITY='Developer ID Application: Example (TEAM npm run package:distribution ``` -No signed build is published on GitHub Releases yet. +GitHub Releases attach the Alpha CLI tarball. A signed, notarized app is not +published there yet. ## What to read next diff --git a/docs/status.md b/docs/status.md index 8378222..e730212 100644 --- a/docs/status.md +++ b/docs/status.md @@ -24,6 +24,9 @@ This Alpha already includes: - app-driven first-run setup and per-repo onboarding commands - CLI-only install through `bash scripts/install_local.sh --cli-only`, plus the contributor app+CLI path `npm run install:local` +- tagged Alpha CLI tarball through `npm run package:cli` and GitHub Releases +- public Node test CI on GitHub-hosted Ubuntu for broker-core, client, + harness-adoption, and public-surface checks - local-debug packaging through `npm run package:local` - signed distribution packaging through `npm run package:distribution` - `simbroker project init` for `.simulator-broker/project.json` @@ -59,7 +62,5 @@ node client/bin/simbroker.mjs simulators boot --alias ui-1 ## Lower-priority public follow-through -- human-readable CLI help (JSON is still the default output) -- CLI-only install that does not build the app -- Homebrew, npm, and GitHub Releases -- public test CI and issue templates +- Homebrew formula, notarized app, and npm package +- issue templates and starter issues diff --git a/docs/test/front-door.test.mjs b/docs/test/front-door.test.mjs index f434646..0d4d817 100644 --- a/docs/test/front-door.test.mjs +++ b/docs/test/front-door.test.mjs @@ -1,5 +1,7 @@ import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; @@ -105,7 +107,9 @@ test("CONTRIBUTING keeps a labeled maintainer harness track", () => { test("README links newcomer docs and embeds a real screenshot file", () => { const readme = readRepoFile("README.md"); - const imageMatch = /!\[.*?]\((.*?)\)/.exec(readme); + const imageMatch = [...readme.matchAll(/!\[.*?]\((.*?)\)/g)] + .map((match) => match[1]) + .find((image) => image.endsWith(".png") && !/^https?:\/\//.test(image)); assert.ok(imageMatch, "README must embed a screenshot image"); assert.ok(readme.includes("[Getting started](docs/getting-started.md)")); @@ -113,7 +117,7 @@ test("README links newcomer docs and embeds a real screenshot file", () => { assert.equal(fs.existsSync(path.join(repoRoot, "docs/getting-started.md")), true); assert.equal(fs.existsSync(path.join(repoRoot, "docs/concepts.md")), true); - const relativeImage = imageMatch[1]; + const relativeImage = imageMatch; const imagePath = path.join(repoRoot, relativeImage); assert.equal(fs.existsSync(imagePath), true, `missing screenshot ${relativeImage}`); @@ -125,3 +129,74 @@ test("README links newcomer docs and embeds a real screenshot file", () => { "screenshot must be a PNG", ); }); + +test("README advertises GitHub Releases and the public Node CI badge", () => { + const readme = readRepoFile("README.md"); + + assert.ok(readme.includes("actions/workflows/ci.yml/badge.svg")); + assert.ok(readme.includes("github.com/fiveonecode/simulator-broker/releases")); + assert.ok(readme.includes("./bin/simbroker --help")); + assert.equal(readme.includes("or GitHub Release yet"), false); + assert.ok(readme.includes("There is no Homebrew formula or npm package yet.")); +}); + +test("CHANGELOG and package.json name the Alpha version", () => { + const changelog = readRepoFile("CHANGELOG.md"); + const packageJson = JSON.parse(readRepoFile("package.json")); + + assert.equal(packageJson.version, "0.1.0-alpha.1"); + assert.ok(changelog.includes("## [0.1.0-alpha.1]")); + assert.ok(changelog.includes("scripts/package_cli.sh")); +}); + +test("public CI runs the Node suites on Ubuntu and skips the macOS app suite", () => { + const ci = readRepoFile(".github/workflows/ci.yml"); + + assert.ok(ci.includes("runs-on: ubuntu-latest")); + assert.ok(ci.includes("npm run verify:public-surface")); + assert.ok(ci.includes("npm run test:broker-core")); + assert.ok(ci.includes("npm run test:client")); + assert.ok(ci.includes("npm run test:harness-adoption")); + assert.equal(ci.includes("test:app"), false); + assert.equal(ci.includes("macos-latest"), false); + assert.equal(/\n\s+run:\s*npm install\b/.test(ci), false); + assert.equal(/\n\s+run:\s*npm ci\b/.test(ci), false); +}); + +test("release workflow packages the CLI tarball on version tags", () => { + const release = readRepoFile(".github/workflows/release.yml"); + + assert.ok(release.includes("tags:")); + assert.ok(release.includes("npm run package:cli")); + assert.ok(release.includes("gh release create")); + assert.ok(release.includes("--prerelease")); + assert.equal(release.includes("test:app"), false); +}); + +test("package_cli.sh writes a runnable CLI tarball without tests or the app", () => { + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "simbroker-package-cli-")); + const result = spawnSync("bash", [path.join(repoRoot, "scripts/package_cli.sh"), "--output-dir", outputDir], { + encoding: "utf8", + cwd: repoRoot, + }); + + assert.equal(result.status, 0, result.stderr); + const tarball = path.join(outputDir, "simulator-broker-0.1.0-alpha.1-cli.tar.gz"); + const checksum = `${tarball}.sha256`; + assert.equal(fs.existsSync(tarball), true, result.stdout); + assert.equal(fs.existsSync(checksum), true, result.stdout); + + const extractDir = path.join(outputDir, "extract"); + fs.mkdirSync(extractDir); + const extract = spawnSync("tar", ["-xzf", tarball, "-C", extractDir], { encoding: "utf8" }); + assert.equal(extract.status, 0, extract.stderr); + + const root = path.join(extractDir, "simulator-broker-0.1.0-alpha.1-cli"); + const help = spawnSync(path.join(root, "bin/simbroker"), ["--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + assert.ok(help.stdout.includes("simbroker") || help.stderr.includes("simbroker")); + assert.equal(fs.existsSync(path.join(root, "broker-core/test")), false); + assert.equal(fs.existsSync(path.join(root, "client/test")), false); + assert.equal(fs.existsSync(path.join(root, "app")), false); + assert.equal(fs.existsSync(path.join(root, "LICENSE")), true); +}); diff --git a/package-lock.json b/package-lock.json index a9f66a7..e81ab14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simulator-broker-app", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simulator-broker-app", - "version": "0.1.0", + "version": "0.1.0-alpha.1", "license": "MIT", "engines": { "node": ">=20" diff --git a/package.json b/package.json index cb11757..907f031 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "agent:verify": "bash ./scripts/bootstrap_agent_harness.sh && npm --prefix agent-harness run verify --", "build:app": "bash ./scripts/generate_app_project.sh && xcodebuild -project ./app/SimulatorBrokerApp.xcodeproj -scheme SimulatorBrokerApp -derivedDataPath ./DerivedData/SimulatorBrokerApp -destination 'platform=macOS' build", "install:local": "bash ./scripts/install_local.sh", + "package:cli": "bash ./scripts/package_cli.sh", "package:distribution": "bash ./scripts/package_distribution.sh", "package:local": "bash ./scripts/package_local.sh", "test": "npm run verify:public-surface && npm run test:broker-core && npm run test:client && npm run test:harness-adoption && npm run test:app", @@ -30,7 +31,7 @@ "test:package-smoke": "bash ./scripts/package_smoke.sh", "verify:public-surface": "node client/public-surface.mjs" }, - "version": "0.1.0", + "version": "0.1.0-alpha.1", "description": "Local simulator broker and macOS operator app for coordinated iOS Simulator workflows", "license": "MIT", "repository": { diff --git a/scripts/package_cli.sh b/scripts/package_cli.sh new file mode 100755 index 0000000..9e1b238 --- /dev/null +++ b/scripts/package_cli.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +output_dir="$repo_root/artifacts/cli" + +usage() { + cat <<'EOF' +Usage: bash scripts/package_cli.sh [options] + +Package the Node CLI runtime into a versioned tarball. This path does not +build or include the macOS operator app. + +Options: + --output-dir Destination for the tarball and checksum. Default: + artifacts/cli + -h, --help Show this help text. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output-dir) + output_dir="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown package_cli.sh argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +version="$(node -e "const fs=require('node:fs'); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1],'utf8')).version)" "$repo_root/package.json")" + +if [[ -z "$version" || "$version" == "." || "$version" == ".." || "$version" == *"/"* || "$version" == *"\\"* ]]; then + echo "Refusing to package an invalid package.json version: ${version:-}" >&2 + exit 1 +fi + +stage="$(mktemp -d "${TMPDIR:-/tmp}/simbroker-package-cli.XXXXXX")" +cleanup() { + rm -rf "$stage" +} +trap cleanup EXIT + +archive_name="simulator-broker-${version}-cli" +bundle="$stage/$archive_name" +mkdir -p "$bundle/bin" "$bundle/broker-core" "$bundle/client" + +tar -C "$repo_root/broker-core" --exclude test --exclude '*.test.mjs' -cf - . \ + | tar -C "$bundle/broker-core" -xf - +tar -C "$repo_root/client" --exclude test --exclude '*.test.mjs' -cf - . \ + | tar -C "$bundle/client" -xf - + +cp "$repo_root/package.json" "$bundle/package.json" +cp "$repo_root/LICENSE" "$bundle/LICENSE" +cp "$repo_root/CHANGELOG.md" "$bundle/CHANGELOG.md" + +cat > "$bundle/README.md" < "$bundle/bin/simbroker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +root="$(cd "$(dirname "$0")/.." && pwd)" +exec node "$root/client/bin/simbroker.mjs" "$@" +EOF +chmod +x "$bundle/bin/simbroker" + +mkdir -p "$output_dir" +output_dir="$(cd "$output_dir" && pwd -P)" +tarball="$output_dir/${archive_name}.tar.gz" +checksum="$output_dir/${archive_name}.tar.gz.sha256" + +tar -C "$stage" -czf "$tarball" "$archive_name" + +if command -v shasum >/dev/null 2>&1; then + (cd "$output_dir" && shasum -a 256 "${archive_name}.tar.gz" > "${archive_name}.tar.gz.sha256") +elif command -v sha256sum >/dev/null 2>&1; then + (cd "$output_dir" && sha256sum "${archive_name}.tar.gz" > "${archive_name}.tar.gz.sha256") +else + echo "Neither shasum nor sha256sum is available; cannot write $checksum" >&2 + exit 1 +fi + +printf '%s\n' "$tarball" +printf '%s\n' "$checksum" diff --git a/spec/README.md b/spec/README.md index cea5cb3..c716f2c 100644 --- a/spec/README.md +++ b/spec/README.md @@ -54,6 +54,7 @@ This repo exists to develop a reusable local simulator broker: - local install, local-debug portable packaging, Release distribution packaging, and onboarding flows now exist through `install_local.sh`, `install_local.sh --cli-only`, `package_local.sh`, `package_distribution.sh`, `test:install-smoke`, `test:package-smoke`, `host init --bootstrap-config`, and `project init` - the published onboarding docs now distinguish CLI-only install, repo-local contributor app+CLI install, local-debug portable bundling, and signed distribution packaging; a new login shell should resolve `simbroker` after install without sourcing `env.sh` - `CONTRIBUTING.md` publishes a public-patch track (Node.js 20 and the Node test suites, no harness session) and a labeled maintainer/agent harness track; `agent:complete` enforcement is unchanged +- tagged Alpha `0.1.0-alpha.1` publishes a CLI tarball from `scripts/package_cli.sh` and runs the Node test surface on GitHub-hosted Ubuntu CI; the macOS app suite is not on that job - `host init --bootstrap-config` warns that it creates real Simulator devices before provisioning them - broker-aware sample consumer repo artifacts now cover manual human, interactive agent, unattended agent build-and-test, and CI patterns under `examples/harness-adoption/` - broker-aware build/test leases now support downstream process registration, memory ceiling containment, evidence bundles, and forced-abort cleanup for detached simulator-like processes diff --git a/spec/build-and-test.md b/spec/build-and-test.md index 81e15fe..1cfbf98 100644 --- a/spec/build-and-test.md +++ b/spec/build-and-test.md @@ -35,6 +35,13 @@ A first extracted implementation slice now exists: - CLI-only install through `bash scripts/install_local.sh --cli-only`, which copies the Node runtime and writes `simbroker` without XcodeGen or an app build - PATH persistence after install: Homebrew prefix bin when that is the install location, otherwise one guarded login-profile snippet for the default `~/.local/bin` location; `--profile` overrides the profile path so tests never edit the operator login rc - `host init --bootstrap-config` prints an honest warning that it creates real iOS Simulator devices before those devices are created +- `scripts/package_cli.sh` packages the Node CLI runtime into a versioned + tarball without XcodeGen or an app build +- public GitHub-hosted Ubuntu CI runs `verify:public-surface`, + `test:broker-core`, `test:client`, and `test:harness-adoption`; it does not + run `test:app` +- tagged versions such as `v0.1.0-alpha.1` attach the CLI tarball to a GitHub + Release through `.github/workflows/release.yml` - local-debug portable bundle support through a zip bundle plus package-smoke verification of the bundled install path and installed-app launch proof - a separate Release distribution packaging path that requires operator-supplied signing inputs, runs `codesign` plus `spctl`, optionally notarizes with `notarytool`, and writes a readiness summary JSON - executable `agent-harness/` changes now route through the implementation @@ -199,6 +206,7 @@ bash scripts/package_distribution.sh --team-id --signing-identity '"` when persist is skipped) - `bash scripts/install_local.sh --cli-only` installs the CLI runtime without invoking `xcodegen` or `xcodebuild` and without requiring an app bundle +- `npm run package:cli` writes `artifacts/cli/simulator-broker--cli.tar.gz` plus a SHA-256 checksum and does not invoke XcodeGen or `xcodebuild` +- `.github/workflows/ci.yml` runs the public Node suites on `ubuntu-latest` and does not run `npm run test:app` - `host init --bootstrap-config` writes a warning that real Simulator devices will be created before it calls `simctl` create - `npm run test:install-smoke` proves a fresh-machine-style install can bootstrap host config, scaffold a repo, start the service, acquire a lease, generate an app snapshot from the installed CLI, launch the installed app bundle against the smoke fixture, assert the `SimulatorBrokerApp` process stays alive, restore any preexisting default install metadata including symlink target contents, and clean up only the simulators provisioned by the smoke run afterward - `npm run package:distribution` builds the app in `Release`, requires operator-supplied `SIMBROKER_DISTRIBUTION_TEAM_ID` plus `SIMBROKER_DISTRIBUTION_SIGNING_IDENTITY`, optionally consumes `SIMBROKER_NOTARYTOOL_PROFILE`, and writes a machine-readable readiness summary under `artifacts/distribution/` diff --git a/spec/project-structure.md b/spec/project-structure.md index e8fb5d4..c6a3a98 100644 --- a/spec/project-structure.md +++ b/spec/project-structure.md @@ -8,6 +8,8 @@ Related: `spec/README.md`, `spec/global-simulator-broker.md`, `references/README - `.codex/environments/` — Codex environment bootstrap and Run actions - `WORKFLOW.md` — public-safe repo-owned Symphony execution, validation, protected-path, and handoff contract +- `CHANGELOG.md` — published version history for tagged releases +- `.github/workflows/` — public Node test CI and tag-driven CLI release - `spec/` — active source of truth, including worker-ready task specs under `spec/tasks/` - `docs/` — public newcomer docs (getting started, concepts, status) and the README screenshot; not source of truth @@ -19,8 +21,9 @@ Related: `spec/README.md`, `spec/global-simulator-broker.md`, `references/README - `script/` — canonical app run-loop entrypoints and shared macOS build preflight helpers such as `build_and_run.sh` - `scripts/` — repo-owned helper scripts including the canonical `validate.sh` full-repository gate, app generation, repo-local install, - CLI-only install (`install_local.sh --cli-only`), distribution install, - portable package creation, smoke verification, and harness bootstrap + CLI-only install (`install_local.sh --cli-only`), CLI tarball packaging + (`package_cli.sh`), distribution install, portable package creation, smoke + verification, and harness bootstrap ## Important rule From 8ebc91654429187797249d1e8d3be392a53aa67d Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 18 Aug 2026 17:34:19 +0800 Subject: [PATCH 2/2] Make Ubuntu Node CI pass snapshot tests and finish in budget. Why: The Alpha CI job on ubuntu-latest failed four broker-core snapshot tests because they called host xcrun simctl. The same run spent 17 minutes in verify:public-surface spawning one git cat-file per tracked file, so the 20-minute job budget could not cover the remaining suites. Changed: The four app-snapshot tests now pass the fixture simctl adapter through runtimeOptions. The default public-surface scan reads index blobs only for dirty or missing worktree files. CI and release jobs use a 30-minute budget. Front-door tests require timeout-minutes of at least 30. Verification: npm run agent:verify -- --profile spec-only --paths broker-core/test/broker-core.test.mjs,client/public-surface.mjs,client/test/public-surface.test.mjs,.github/workflows/ci.yml,.github/workflows/release.yml,docs/test/front-door.test.mjs,spec/build-and-test.md,CHANGELOG.md --session-dir task-sessions/20260818-alpha-ci-ubuntu-fix npm run agent:verify -- --profile implementation --paths broker-core/test/broker-core.test.mjs,client/public-surface.mjs,client/test/public-surface.test.mjs,.github/workflows/ci.yml,.github/workflows/release.yml,docs/test/front-door.test.mjs,spec/build-and-test.md,CHANGELOG.md --session-dir task-sessions/20260818-alpha-ci-ubuntu-fix Affected: broker-core/test/broker-core.test.mjs client/public-surface.mjs client/test/public-surface.test.mjs .github/workflows/ci.yml .github/workflows/release.yml docs/test/front-door.test.mjs spec/build-and-test.md CHANGELOG.md Refs: https://github.com/fiveonecode/simulator-broker/pull/6 https://github.com/fiveonecode/simulator-broker/actions/runs/32114159922 spec/build-and-test.md Session: task-sessions/20260818-alpha-ci-ubuntu-fix --- .github/workflows/ci.yml | 8 +++++--- .github/workflows/release.yml | 2 +- CHANGELOG.md | 3 +++ broker-core/test/broker-core.test.mjs | 10 +++++----- client/public-surface.mjs | 23 ++++++++++++++++++++++- client/test/public-surface.test.mjs | 23 +++++++++++++++++++++++ docs/test/front-door.test.mjs | 1 + spec/build-and-test.md | 7 +++++-- 8 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93ff3dd..ad4525b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,7 @@ -# Public Node test surface. This job is intentionally cheap: Ubuntu, no -# npm install (the repo has no runtime dependencies), and no macOS app suite. +# Public Node test surface. Ubuntu, no npm install (the repo has no runtime +# dependencies), and no macOS app suite. timeout-minutes is 30 because a +# prior 20-minute budget was consumed by per-file git cat-file in +# verify:public-surface on a clean checkout. name: Node tests on: @@ -18,7 +20,7 @@ jobs: node: name: broker-core client harness-adoption public-surface runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e77a3f..ea1533f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: release: name: attach CLI tarball runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ca428..ea7b0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ downloadable CLI tarball. - Public Node test workflow on GitHub-hosted Ubuntu for `verify:public-surface`, `test:broker-core`, `test:client`, and `test:harness-adoption`. That job does not run the macOS app suite. + Snapshot tests inject the fixture `simctl` adapter so the suite does not + call host `xcrun`. The default public-surface scan skips identical index + blobs on a clean worktree, and the job budget is 30 minutes. - `scripts/package_cli.sh` (`npm run package:cli`) builds a versioned CLI tarball without XcodeGen or an app build. - Tag-driven GitHub Release workflow that attaches the CLI tarball and its diff --git a/broker-core/test/broker-core.test.mjs b/broker-core/test/broker-core.test.mjs index fccbf3b..be855ab 100644 --- a/broker-core/test/broker-core.test.mjs +++ b/broker-core/test/broker-core.test.mjs @@ -9350,7 +9350,7 @@ test("events and app snapshots honor a zero event limit", () => { }); assert.equal(readEventsBroker(resolvedPaths, { limit: 0 }).events.length, 0); - assert.equal(appSnapshotBroker(resolvedPaths, { eventLimit: 0 }).recentEvents.length, 0); + assert.equal(appSnapshotBroker(resolvedPaths, runtimeOptions(paths, { eventLimit: 0 })).recentEvents.length, 0); }); test("app snapshot reuses one process sample for active lease checks", () => { @@ -9379,13 +9379,13 @@ test("app snapshot reuses one process sample for active lease checks", () => { const liveSampler = liveProcessSampler({ command: "node broker-core.test.mjs", pid: process.pid }); let sampleCount = 0; - const snapshot = appSnapshotBroker(resolvedPaths, { + const snapshot = appSnapshotBroker(resolvedPaths, runtimeOptions(paths, { processExists: (pid) => pid === process.pid, processSampler: () => { sampleCount += 1; return liveSampler(); }, - }); + })); assert.equal(snapshot.activeLeases.length, 2); assert.equal(sampleCount, 1); @@ -9422,7 +9422,7 @@ test("app snapshot reads only a bounded event tail for recent events", (t) => { return originalReadFileSync.call(this, target, ...args); }; - const snapshot = appSnapshotBroker(resolvedPaths, { eventLimit: 3 }); + const snapshot = appSnapshotBroker(resolvedPaths, runtimeOptions(paths, { eventLimit: 3 })); assert.deepEqual(snapshot.recentEvents.map((event) => event.eventId), [ "event-119", @@ -9661,7 +9661,7 @@ test("broker-owned state files are restricted to the current user", () => { purposeId: "agent-ui-session", simctlAdapter: paths.simctl.adapter, }); - writeAppSnapshotArtifact(resolvedPaths); + writeAppSnapshotArtifact(resolvedPaths, runtimeOptions(paths)); const statePaths = [ resolvedPaths.stateRoot, diff --git a/client/public-surface.mjs b/client/public-surface.mjs index e69abc3..83918b4 100644 --- a/client/public-surface.mjs +++ b/client/public-surface.mjs @@ -79,6 +79,24 @@ function defaultCandidateFiles(root) { return output.split("\0").filter(Boolean); } +// Clean checkouts match the index, so skip per-file `git cat-file`. A prior +// Ubuntu CI run spent 17 minutes spawning one process per tracked file. +function dirtyWorktreeFiles(root) { + try { + const output = execGit([ + "diff-files", + "-z", + "--name-only", + ], { + cwd: root, + encoding: "utf8", + }); + return new Set(output.split("\0").filter(Boolean)); + } catch { + return null; + } +} + function defaultCandidateIndexModes(root) { const output = execGit([ "ls-files", @@ -318,6 +336,7 @@ export function scanPublicSurface({ const candidateFiles = files ?? defaultCandidateFiles(resolvedRoot); const scanIndexBlobs = files === undefined; const indexModes = scanIndexBlobs ? defaultCandidateIndexModes(resolvedRoot) : new Map(); + const dirtyFiles = scanIndexBlobs ? dirtyWorktreeFiles(resolvedRoot) : new Set(); const resolvedDenylistPath = denylistPath ?? path.join(resolvedRoot, LOCAL_DENYLIST_NAME); const denylistRules = localDenylistRules(resolvedDenylistPath); const builtInRules = [ @@ -421,6 +440,7 @@ export function scanPublicSurface({ if (!absoluteFile.startsWith(`${resolvedRoot}${path.sep}`)) { continue; } + let worktreeMissing = false; try { const fileStats = fs.lstatSync(absoluteFile); if (fileStats.isSymbolicLink()) { @@ -438,8 +458,9 @@ export function scanPublicSurface({ if (error?.code !== "ENOENT") { throw error; } + worktreeMissing = true; } - if (scanIndexBlobs) { + if (scanIndexBlobs && (dirtyFiles === null || dirtyFiles.has(relativeFile) || worktreeMissing)) { scanText(normalizedRelativeFile, indexBlobContent(resolvedRoot, relativeFile)); } } diff --git a/client/test/public-surface.test.mjs b/client/test/public-surface.test.mjs index 1233d35..c7d50c1 100644 --- a/client/test/public-surface.test.mjs +++ b/client/test/public-surface.test.mjs @@ -546,6 +546,29 @@ test("default public surface candidates ignore untracked scratch files", () => { }]); }); +test("default public surface scan inspects staged blobs when the worktree file is missing", () => { + const root = makeTempDir(); + const localHome = path.join(root, "private-home"); + execFileSync("git", ["init"], { cwd: root, stdio: "ignore" }); + fs.writeFileSync(path.join(root, "README.md"), `machine path: ${localHome}/state\n`); + execFileSync("git", ["add", "README.md"], { cwd: root, stdio: "ignore" }); + fs.rmSync(path.join(root, "README.md")); + + const report = scanPublicSurface({ + homePath: localHome, + root, + }); + + assert.equal(report.ok, false); + assert.equal(report.filesScanned, 1); + assert.deepEqual(report.issues, [{ + line: 1, + path: "README.md", + rule: "local-home-path", + }]); + assert.equal(JSON.stringify(report).includes(localHome), false); +}); + test("default public surface scan inspects staged blobs even after worktree cleanup", () => { const root = makeTempDir(); const localHome = path.join(root, "private-home"); diff --git a/docs/test/front-door.test.mjs b/docs/test/front-door.test.mjs index 0d4d817..7d4a97f 100644 --- a/docs/test/front-door.test.mjs +++ b/docs/test/front-door.test.mjs @@ -153,6 +153,7 @@ test("public CI runs the Node suites on Ubuntu and skips the macOS app suite", ( const ci = readRepoFile(".github/workflows/ci.yml"); assert.ok(ci.includes("runs-on: ubuntu-latest")); + assert.match(ci, /timeout-minutes:\s*([3-9]\d|\d{3,})/); assert.ok(ci.includes("npm run verify:public-surface")); assert.ok(ci.includes("npm run test:broker-core")); assert.ok(ci.includes("npm run test:client")); diff --git a/spec/build-and-test.md b/spec/build-and-test.md index 1cfbf98..3e111c7 100644 --- a/spec/build-and-test.md +++ b/spec/build-and-test.md @@ -39,7 +39,10 @@ A first extracted implementation slice now exists: tarball without XcodeGen or an app build - public GitHub-hosted Ubuntu CI runs `verify:public-surface`, `test:broker-core`, `test:client`, and `test:harness-adoption`; it does not - run `test:app` + run `test:app`. The job budget is 30 minutes. Broker tests that build an + app snapshot must inject the fixture `simctl` adapter. The default + public-surface scan reads index blobs only for dirty or missing worktree + files so a clean checkout does not spawn one `git cat-file` per file. - tagged versions such as `v0.1.0-alpha.1` attach the CLI tarball to a GitHub Release through `.github/workflows/release.yml` - local-debug portable bundle support through a zip bundle plus package-smoke verification of the bundled install path and installed-app launch proof @@ -288,7 +291,7 @@ Add stronger profiles next for: - the installer prints the installed CLI path, app path when an app was installed, env helper path, any current-shell PATH warning, PATH persist result, and the next command (`command -v simbroker` after persist, or `source ""` when persist is skipped) - `bash scripts/install_local.sh --cli-only` installs the CLI runtime without invoking `xcodegen` or `xcodebuild` and without requiring an app bundle - `npm run package:cli` writes `artifacts/cli/simulator-broker--cli.tar.gz` plus a SHA-256 checksum and does not invoke XcodeGen or `xcodebuild` -- `.github/workflows/ci.yml` runs the public Node suites on `ubuntu-latest` and does not run `npm run test:app` +- `.github/workflows/ci.yml` runs the public Node suites on `ubuntu-latest` with a 30-minute budget and does not run `npm run test:app` - `host init --bootstrap-config` writes a warning that real Simulator devices will be created before it calls `simctl` create - `npm run test:install-smoke` proves a fresh-machine-style install can bootstrap host config, scaffold a repo, start the service, acquire a lease, generate an app snapshot from the installed CLI, launch the installed app bundle against the smoke fixture, assert the `SimulatorBrokerApp` process stays alive, restore any preexisting default install metadata including symlink target contents, and clean up only the simulators provisioned by the smoke run afterward - `npm run package:distribution` builds the app in `Release`, requires operator-supplied `SIMBROKER_DISTRIBUTION_TEAM_ID` plus `SIMBROKER_DISTRIBUTION_SIGNING_IDENTITY`, optionally consumes `SIMBROKER_NOTARYTOOL_PROFILE`, and writes a machine-readable readiness summary under `artifacts/distribution/`