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..ad4525b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# 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: + 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: 30 + 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..ea1533f --- /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: 30 + 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..ea7b0cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# 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. + 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 + 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/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/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..7d4a97f 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,75 @@ 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.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")); + 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..3e111c7 100644 --- a/spec/build-and-test.md +++ b/spec/build-and-test.md @@ -35,6 +35,16 @@ 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`. 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 - 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 +209,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` 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/` 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