feat: standalone skills package + HTML render-annotate mode #1079
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| pull_request: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| dry-run: | |
| description: Validate build and npm publish without uploading | |
| type: boolean | |
| default: true | |
| permissions: | |
| contents: read | |
| env: | |
| DRY_RUN: ${{ !(startsWith(github.ref, 'refs/tags/') || inputs.dry-run == 'false') }} | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Generate Pi extension shared copies | |
| run: bash apps/pi-extension/vendor.sh | |
| - name: Type check | |
| run: bun run typecheck | |
| - name: Run tests | |
| run: bun test | |
| build: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| # Build job has NO id-token / attestations permissions. Compilation | |
| # itself doesn't need OIDC minting — those capabilities live in the | |
| # separate `attest` job below, which only runs on tag pushes. This | |
| # ensures PR dry-runs (which exercise `bun install` + compile) never | |
| # have OIDC minting available, closing the narrow-but-real | |
| # "trusted-contributor compromise lets a malicious build step mint | |
| # a repo-identity OIDC token" attack surface. | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: 1.3.11 | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Build UI | |
| run: | | |
| bun run build:review | |
| bun run build:hook | |
| - name: Compile binaries (cross-compile all targets) | |
| run: | | |
| # macOS ARM64 | |
| bun build apps/hook/server/index.ts --compile --target=bun-darwin-arm64 --outfile plannotator-darwin-arm64 | |
| sha256sum plannotator-darwin-arm64 > plannotator-darwin-arm64.sha256 | |
| # macOS x64 | |
| bun build apps/hook/server/index.ts --compile --target=bun-darwin-x64 --outfile plannotator-darwin-x64 | |
| sha256sum plannotator-darwin-x64 > plannotator-darwin-x64.sha256 | |
| # Linux x64 | |
| bun build apps/hook/server/index.ts --compile --target=bun-linux-x64 --outfile plannotator-linux-x64 | |
| sha256sum plannotator-linux-x64 > plannotator-linux-x64.sha256 | |
| # Linux ARM64 | |
| bun build apps/hook/server/index.ts --compile --target=bun-linux-arm64 --outfile plannotator-linux-arm64 | |
| sha256sum plannotator-linux-arm64 > plannotator-linux-arm64.sha256 | |
| # Windows x64 | |
| bun build apps/hook/server/index.ts --compile --target=bun-windows-x64 --outfile plannotator-win32-x64.exe | |
| sha256sum plannotator-win32-x64.exe > plannotator-win32-x64.exe.sha256 | |
| # Windows ARM64 (native, via bun-windows-arm64 — stable since Bun v1.3.10) | |
| bun build apps/hook/server/index.ts --compile --target=bun-windows-arm64 --outfile plannotator-win32-arm64.exe | |
| sha256sum plannotator-win32-arm64.exe > plannotator-win32-arm64.exe.sha256 | |
| # Paste service binaries | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-darwin-arm64 --outfile plannotator-paste-darwin-arm64 | |
| sha256sum plannotator-paste-darwin-arm64 > plannotator-paste-darwin-arm64.sha256 | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-darwin-x64 --outfile plannotator-paste-darwin-x64 | |
| sha256sum plannotator-paste-darwin-x64 > plannotator-paste-darwin-x64.sha256 | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-linux-x64 --outfile plannotator-paste-linux-x64 | |
| sha256sum plannotator-paste-linux-x64 > plannotator-paste-linux-x64.sha256 | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-linux-arm64 --outfile plannotator-paste-linux-arm64 | |
| sha256sum plannotator-paste-linux-arm64 > plannotator-paste-linux-arm64.sha256 | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-windows-x64 --outfile plannotator-paste-win32-x64.exe | |
| sha256sum plannotator-paste-win32-x64.exe > plannotator-paste-win32-x64.exe.sha256 | |
| bun build apps/paste-service/targets/bun.ts --compile --target=bun-windows-arm64 --outfile plannotator-paste-win32-arm64.exe | |
| sha256sum plannotator-paste-win32-arm64.exe > plannotator-paste-win32-arm64.exe.sha256 | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: binaries | |
| path: | | |
| plannotator-* | |
| !*.ts | |
| smoke-binaries: | |
| needs: build | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| binary: plannotator-linux-x64 | |
| - os: windows-latest | |
| binary: plannotator-win32-x64.exe | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Download binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: binaries | |
| path: artifacts | |
| - name: Smoke-test binary | |
| if: runner.os != 'Windows' | |
| env: | |
| BINARY: artifacts/${{ matrix.binary }} | |
| BROWSER: true | |
| run: | | |
| set -euo pipefail | |
| chmod +x "$BINARY" | |
| # 1. --help: proves binary loads and arg parsing works. | |
| "$BINARY" --help | |
| smoke_test_server() { | |
| local label="$1" port="$2" endpoint="$3" | |
| shift 3 | |
| PLANNOTATOR_PORT="$port" "$@" & | |
| local pid=$! | |
| local ok=0 | |
| for _ in $(seq 1 60); do | |
| if curl -sf "http://127.0.0.1:${port}${endpoint}" -o /dev/null 2>/dev/null; then | |
| ok=1 | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| kill "$pid" 2>/dev/null || true | |
| wait "$pid" 2>/dev/null || true | |
| if [ "$ok" = "0" ]; then | |
| echo "FAIL: ${label} did not respond on :${port}${endpoint}" | |
| exit 1 | |
| fi | |
| echo "OK: ${label} responded on :${port}${endpoint}" | |
| } | |
| # 2. review: exercises server startup, bundled HTML, git diff, and HTTP. | |
| smoke_test_server "plannotator review" 19500 "/api/diff" \ | |
| "$BINARY" review | |
| # 3. annotate: exercises annotate server startup with a real file. | |
| smoke_test_server "plannotator annotate" 19501 "/api/plan" \ | |
| "$BINARY" annotate README.md | |
| - name: Smoke-test binary | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| env: | |
| BINARY: artifacts/${{ matrix.binary }} | |
| BROWSER: true | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $binary = (Resolve-Path $env:BINARY).Path | |
| # 1. --help: proves binary loads and arg parsing works. | |
| & $binary --help | |
| function Test-PlannotatorServer { | |
| param( | |
| [string] $Label, | |
| [string] $Port, | |
| [string] $Endpoint, | |
| [string[]] $Arguments | |
| ) | |
| $env:PLANNOTATOR_PORT = $Port | |
| $stdout = New-TemporaryFile | |
| $stderr = New-TemporaryFile | |
| $process = Start-Process ` | |
| -FilePath $binary ` | |
| -ArgumentList $Arguments ` | |
| -PassThru ` | |
| -NoNewWindow ` | |
| -RedirectStandardOutput $stdout ` | |
| -RedirectStandardError $stderr | |
| $ok = $false | |
| try { | |
| for ($i = 0; $i -lt 60; $i++) { | |
| try { | |
| Invoke-WebRequest -Uri "http://127.0.0.1:$Port$Endpoint" -UseBasicParsing -TimeoutSec 1 | Out-Null | |
| $ok = $true | |
| break | |
| } catch { | |
| if ($process.HasExited) { | |
| break | |
| } | |
| Start-Sleep -Milliseconds 500 | |
| } | |
| } | |
| } finally { | |
| if (-not $process.HasExited) { | |
| Stop-Process -Id $process.Id -Force | |
| Wait-Process -Id $process.Id -ErrorAction SilentlyContinue | |
| } | |
| Remove-Item Env:\PLANNOTATOR_PORT -ErrorAction SilentlyContinue | |
| } | |
| if (-not $ok) { | |
| Write-Host "stdout:" | |
| Get-Content $stdout -ErrorAction SilentlyContinue | |
| Write-Host "stderr:" | |
| Get-Content $stderr -ErrorAction SilentlyContinue | |
| throw "FAIL: $Label did not respond on :$Port$Endpoint" | |
| } | |
| Write-Host "OK: $Label responded on :$Port$Endpoint" | |
| } | |
| # 2. review: exercises server startup, bundled HTML, git diff, and HTTP. | |
| Test-PlannotatorServer "plannotator review" "19500" "/api/diff" @("review") | |
| # 3. annotate: exercises annotate server startup with a real file. | |
| Test-PlannotatorServer "plannotator annotate" "19501" "/api/plan" @("annotate", "README.md") | |
| install-script-smoke: | |
| needs: build | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| artifact: plannotator-linux-x64 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Download binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: binaries | |
| path: artifacts | |
| - name: Verify installer writes Codex hook config | |
| env: | |
| ARTIFACT_NAME: ${{ matrix.artifact }} | |
| run: | | |
| set -euo pipefail | |
| tmp_home="$(mktemp -d)" | |
| fake_bin="$(mktemp -d)" | |
| artifact="$PWD/artifacts/$ARTIFACT_NAME" | |
| cat > "$fake_bin/codex" <<'SH' | |
| #!/usr/bin/env bash | |
| echo "codex stub" | |
| SH | |
| chmod +x "$fake_bin/codex" | |
| cat > "$fake_bin/curl" <<'SH' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| out="" | |
| url="" | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| -o|--output) | |
| out="$2" | |
| shift 2 | |
| ;; | |
| -*) | |
| shift | |
| ;; | |
| *) | |
| url="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| if [[ "$url" == *.sha256 ]]; then | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "$ARTIFACT" | |
| else | |
| shasum -a 256 "$ARTIFACT" | |
| fi | |
| exit 0 | |
| fi | |
| if [ -n "$out" ]; then | |
| cp "$ARTIFACT" "$out" | |
| else | |
| cat "$ARTIFACT" | |
| fi | |
| SH | |
| chmod +x "$fake_bin/curl" | |
| run_installer() { | |
| HOME="$tmp_home" \ | |
| PATH="$fake_bin:$PATH" \ | |
| SHELL=/bin/bash \ | |
| ARTIFACT="$artifact" \ | |
| bash scripts/install.sh --version v9.9.9 --skip-attestation | |
| } | |
| run_installer | |
| test -x "$tmp_home/.local/bin/plannotator" | |
| grep -q 'codex_hooks = true' "$tmp_home/.codex/config.toml" | |
| HOME="$tmp_home" node <<'NODE' | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const home = process.env.HOME; | |
| const hooksPath = path.join(home, ".codex", "hooks.json"); | |
| const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf8")); | |
| const command = hooks?.hooks?.Stop?.[0]?.hooks?.[0]?.command; | |
| const timeout = hooks?.hooks?.Stop?.[0]?.hooks?.[0]?.timeout; | |
| const expected = path.join(home, ".local", "bin", "plannotator"); | |
| if (command !== expected) { | |
| throw new Error(`Expected Stop hook command ${expected}, got ${command}`); | |
| } | |
| if (timeout !== 345600) { | |
| throw new Error(`Expected Stop hook timeout 345600, got ${timeout}`); | |
| } | |
| NODE | |
| cat > "$tmp_home/.codex/hooks.json" <<'JSON' | |
| { | |
| "hooks": { | |
| "Stop": [ | |
| { | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "PLANNOTATOR_BROWSER=/usr/bin/true plannotator", | |
| "timeout": 123 | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } | |
| JSON | |
| run_installer | |
| HOME="$tmp_home" node <<'NODE' | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const hooksPath = path.join(process.env.HOME, ".codex", "hooks.json"); | |
| const stop = JSON.parse(fs.readFileSync(hooksPath, "utf8"))?.hooks?.Stop; | |
| const hooks = stop?.flatMap((entry) => entry?.hooks ?? []) ?? []; | |
| if (hooks.length !== 1) { | |
| throw new Error(`Expected one preserved custom Stop hook, got ${hooks.length}`); | |
| } | |
| if (hooks[0].command !== "PLANNOTATOR_BROWSER=/usr/bin/true plannotator") { | |
| throw new Error(`Custom Stop hook command was changed to ${hooks[0].command}`); | |
| } | |
| NODE | |
| attest: | |
| # Isolated attestation job — runs on tag pushes only and holds the | |
| # OIDC minting + attestations-write capabilities that the build job | |
| # used to have. Splitting this out means PR builds and non-tag pushes | |
| # never get id-token: write granted, closing the trusted-contributor | |
| # compromise window where a malicious build step could mint a | |
| # repo-identity OIDC token. The attestation is produced against the | |
| # same binaries the build job uploaded; attest-build-provenance | |
| # publishes the signed bundle to GitHub's attestation store, so the | |
| # release job downstream doesn't need any new artifact handling. | |
| needs: | |
| - build | |
| - smoke-binaries | |
| - install-script-smoke | |
| if: startsWith(github.ref, 'refs/tags/') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: binaries | |
| - name: Generate SLSA build provenance attestation | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: | | |
| plannotator-darwin-arm64 | |
| plannotator-darwin-x64 | |
| plannotator-linux-x64 | |
| plannotator-linux-arm64 | |
| plannotator-win32-x64.exe | |
| plannotator-win32-arm64.exe | |
| plannotator-paste-darwin-arm64 | |
| plannotator-paste-darwin-x64 | |
| plannotator-paste-linux-x64 | |
| plannotator-paste-linux-arm64 | |
| plannotator-paste-win32-x64.exe | |
| plannotator-paste-win32-arm64.exe | |
| release: | |
| # Depends on `attest` so the signed provenance exists before the | |
| # GitHub Release is published — otherwise there'd be a window where | |
| # users could pull the binary and `gh attestation verify` would | |
| # race-fail. `needs: attest` implicitly requires `build` too. | |
| needs: attest | |
| if: startsWith(github.ref, 'refs/tags/') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Download artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: binaries | |
| path: artifacts | |
| - name: List artifacts | |
| run: ls -la artifacts/ | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 | |
| with: | |
| files: artifacts/* | |
| generate_release_notes: true | |
| draft: false | |
| prerelease: ${{ contains(github.ref, '-') }} | |
| npm-publish: | |
| needs: | |
| - build | |
| - smoke-binaries | |
| - install-script-smoke | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: 1.3.11 | |
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: 24 | |
| registry-url: https://registry.npmjs.org | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Build packages | |
| run: | | |
| bun run build:review | |
| bun run build:hook | |
| bun run build:opencode | |
| bun run build:pi | |
| - name: Publish @plannotator/opencode | |
| working-directory: apps/opencode-plugin | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| bun pm pack | |
| if [[ "$DRY_RUN" == "false" ]]; then | |
| npm publish *.tgz --provenance --access public | |
| fi | |
| - name: Publish @plannotator/pi-extension | |
| working-directory: apps/pi-extension | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| bun pm pack | |
| if [[ "$DRY_RUN" == "false" ]]; then | |
| npm publish *.tgz --provenance --access public | |
| fi |