Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/bootstrap-feature/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ This is the pattern that lets the fake-runner half of L1 stay fast and hermetic.
| Pure logic + fakes | L1 | none | `<pkg>/<feature>_test.go` |
| Real subprocess in temp dir | L1 | none | `test/integration/<feature>_integration_test.go` |
| Compiled binary, no installs | L3 | `e2e` | `test/e2e/...` |
| Real installs on macOS | L4/L5 | `e2e,vm,destructive` | `test/e2e/...` |
| Real installs on macOS | L4 (VM) | `e2e,vm` | `test/e2e/...` |

Default to faked-runner L1 unless the thing you're testing only exists when a real
brew/git/npm is on the path — then add an integration test under `test/integration/`
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/ship-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ not** use this for:
`gh pr create --draft` and stop.
- Changes to `.github/workflows/` or branch protection rules. Those need
a human to also poke the GitHub UI.
- Release tags (`v*.*.*`). Releases follow `make test-vm-release` first.
- Release tags (`v*.*.*`). Releases follow `make test-vm` first (see CONTRIBUTING.md "VM E2E setup").

## The flow

Expand Down
80 changes: 69 additions & 11 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
name: Auto Release

# Sensor: when unreleased commits on main trip a threshold, automatically tag
# the next version and dispatch release.yml. Closes the "agent forgot to cut
# a release" loop — see docs/HARNESS.md.
# Sensor: when unreleased commits on main trip a threshold, either auto-tag
# (patch fast lane) or open a release-ready issue (feat threshold).
# See docs/HARNESS.md.
#
# Triggers (any one fires):
# - >=5 feat/fix commits since last stable tag
# - >=7 days since last stable tag AND new commits exist
# - any fix: present (patch fast lane)
#
# Version bump:
# - any feat: present -> minor (X.Y+1.0)
# - else -> patch (X.Y.Z+1)
# - any feat: present -> minor (X.Y+1.0) — opens issue, does NOT tag
# - else -> patch (X.Y.Z+1) — auto-tags + dispatches release.yml
#
# Why we dispatch release.yml instead of relying on the tag-push trigger:
# GitHub deliberately suppresses workflow events fired by GITHUB_TOKEN, so a
# tag pushed from here would NOT start release.yml. We push the tag and then
# explicitly `gh workflow run release.yml -f version=<tag>`.
# Why the split: feat: changes carry more risk than fix: patches and benefit
# from a human running the local Tart VM e2e suite before tag. The issue is
# a nudge, not a hard gate — a human can tag without running it.
#
# Why we dispatch release.yml on patch instead of relying on the tag-push
# trigger: GitHub deliberately suppresses workflow events fired by
# GITHUB_TOKEN, so a tag pushed from here would NOT start release.yml.
# We push the tag and then explicitly
# `gh workflow run release.yml -f version=<tag>`.

on:
push:
Expand All @@ -35,6 +40,7 @@ concurrency:
permissions:
contents: write
actions: write
issues: write

jobs:
evaluate:
Expand Down Expand Up @@ -119,8 +125,11 @@ jobs:
echo "dry_run=true" >> "$GITHUB_OUTPUT"
fi

- name: Tag and dispatch release
if: steps.decide.outputs.release == 'true' && steps.decide.outputs.dry_run != 'true'
- name: Tag and dispatch release (patch fast lane)
if: |
steps.decide.outputs.release == 'true'
&& steps.decide.outputs.dry_run != 'true'
&& steps.decide.outputs.bump == 'patch'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_TAG: ${{ steps.decide.outputs.new_tag }}
Expand All @@ -133,3 +142,52 @@ jobs:
git push origin "$NEW_TAG"
gh workflow run release.yml -f version="$NEW_TAG"
echo "::notice::tagged $NEW_TAG and dispatched release.yml — $REASON"

- name: Open release-ready issue (feat threshold)
if: |
steps.decide.outputs.release == 'true'
&& steps.decide.outputs.dry_run != 'true'
&& steps.decide.outputs.bump == 'minor'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NEW_TAG: ${{ steps.decide.outputs.new_tag }}
REASON: ${{ steps.decide.outputs.reason }}
run: |
set -euo pipefail

# If an open release-ready issue already exists for this tag, skip
# (we don't want to spam the queue every push to main).
existing=$(gh issue list \
--label release-ready \
--state open \
--search "in:title ${NEW_TAG}" \
--json number --jq '.[0].number' || true)
if [ -n "$existing" ]; then
echo "::notice::release-ready issue #${existing} already open for ${NEW_TAG} — skipping"
exit 0
fi

body=$(cat <<EOF
Auto-release sensor wants to cut **${NEW_TAG}** (minor bump).

**Why:** ${REASON}

Because this release includes \`feat:\` changes, the sensor did
**not** auto-tag. Run the destructive e2e suite locally, then
cut the release manually:

- [ ] \`make test-vm\` passes (Apple Silicon + Tart required — see scripts/vm/README.md)
- [ ] sanity-check the curl|bash smoke and cli-compat results in the most recent test.yml run on main
- [ ] \`git tag -a ${NEW_TAG} -m "..."\` and \`git push origin ${NEW_TAG}\`
- [ ] close this issue

Skipping \`make test-vm\` is allowed (it is not a hard gate),
but \`feat:\` changes carry more risk than \`fix:\` patches.
EOF
)

gh issue create \
--title "Release ready: ${NEW_TAG}" \
--label release-ready \
--body "$body"
echo "::notice::opened release-ready issue for ${NEW_TAG} — ${REASON}"
51 changes: 1 addition & 50 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ jobs:
- name: Unit tests
run: make test-unit

- name: Destructive tests
run: make test-destructive

build:
needs: [detect-release-type, gate-tests]
runs-on: ubuntu-latest
Expand Down Expand Up @@ -101,54 +98,8 @@ jobs:
name: openboot-${{ matrix.os }}-${{ matrix.arch }}
path: openboot-${{ matrix.os }}-${{ matrix.arch }}

smoke-test:
needs: [detect-release-type, build]
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Download arm64 artifact
uses: actions/download-artifact@v4
with:
name: openboot-darwin-arm64
path: artifacts

- name: Prepare release binary for testing
run: |
chmod +x artifacts/openboot-darwin-arm64
cp artifacts/openboot-darwin-arm64 ./openboot

- name: Verify binary version string
run: |
VERSION_OUTPUT=$(./openboot version 2>&1 || true)
echo "Binary version output: ${VERSION_OUTPUT}"
EXPECTED="${{ needs.detect-release-type.outputs.version_clean }}"
if [[ "${VERSION_OUTPUT}" != *"${EXPECTED}"* ]]; then
echo "ERROR: Binary version '${VERSION_OUTPUT}' does not contain expected '${EXPECTED}'"
exit 1
fi

- name: Run smoke tests against release binary
run: make test-smoke-prebuilt

- name: Upload snapshot artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-results
path: |
/tmp/openboot-smoke-*.json
retention-days: 7
if-no-files-found: ignore

release:
needs: [detect-release-type, build, smoke-test]
needs: [detect-release-type, build]
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
49 changes: 0 additions & 49 deletions .github/workflows/smoke-test.yml

This file was deleted.

40 changes: 0 additions & 40 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ on:
repository_dispatch:
types: [contract-updated]
workflow_dispatch:
inputs:
run_destructive:
description: 'Run destructive (real install) tests'
required: false
type: boolean
default: false

jobs:
lint:
Expand Down Expand Up @@ -139,40 +133,6 @@ jobs:
if: always()
run: kill $(cat /tmp/mock-pid) 2>/dev/null || true

macos-e2e:
name: macos e2e (L4)
runs-on: macos-latest
# Only on release tags or manual dispatch — these are slow and destructive.
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Run macOS E2E (release tier)
run: make test-vm-release

destructive:
name: destructive (L5)
runs-on: macos-latest
if: ${{ inputs.run_destructive == true }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"

- name: Run destructive tests
run: make test-destructive

cli-compat:
name: old-cli compat
runs-on: macos-latest
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ human, you probably want [README.md](README.md) or [CONTRIBUTING.md](CONTRIBUTIN

- **[CLAUDE.md](CLAUDE.md)** — project conventions and where-to-look table.
Treat this as authoritative; this file is a summary index for agents.
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — test layering L1–L5, Runner
- **[CONTRIBUTING.md](CONTRIBUTING.md)** — test layering L1–L4, Runner
interface, hook setup.
- **[docs/HARNESS.md](docs/HARNESS.md)** — the steering meta-doc: when a
class of issue recurs, which file do you edit to prevent it next time.
Expand Down Expand Up @@ -81,7 +81,7 @@ These are loaded automatically when Claude runs in this repo.
- `git push --force` against `main` or release tags.
- `git commit --amend` on commits already pushed.
- `git reset --hard` discarding uncommitted work.
- Running `make test-destructive` or `make test-vm-*` outside an ephemeral
- Running `make test-vm` (or any other `test-vm-*` target) outside an ephemeral
VM — these install real packages.
- Anything that modifies the user's `~/.zshrc`, Homebrew install, or
macOS `defaults`.
Expand Down
9 changes: 4 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ OpenBoot is a **macOS-only** Go 1.24 CLI that automates dev-environment setup: H
Entry point: `cmd/openboot/main.go` → `internal/cli.Execute()`.
Core flow: `openboot install` runs a 7-step wizard in `internal/installer/installer.go`.

For full contribution guide (test layering L1–L5, Runner interface, hook setup) see @CONTRIBUTING.md.
For full contribution guide (test layering L1–L4, Runner interface, hook setup) see @CONTRIBUTING.md.
For AI agents: @AGENTS.md indexes invariants enforced by `internal/archtest`; @docs/HARNESS.md is the steering meta-doc for where to encode new rules.

## Working in parallel
Expand All @@ -24,8 +24,7 @@ make build-release VERSION=0.25.0 # optimized + UPX
# Test — full tier table in CONTRIBUTING.md
make test-unit # L1 (~75s) — unit + integration + contract; pre-push hook
make test-e2e # L3 compiled binary
make test-vm-release # L4 destructive macOS (~20m) — before tagging
make test-destructive # L5 — actually installs
make test-vm # L4 (~30m) — destructive e2e in a local Tart VM; before tagging
make test-coverage # coverage.out + coverage.html

# Single test
Expand Down Expand Up @@ -62,7 +61,7 @@ internal/
system/ # RunCommand / RunCommandSilent, arch, git config
ui/ # bubbletea Model pattern, lipgloss styling
updater/ # Auto-update: check GitHub → download → replace
test/{integration,e2e}/ # integration runs as part of L1; e2e gated by build tags (e2e, vm, destructive, smoke)
test/{integration,e2e}/ # integration runs as part of L1; e2e gated by build tags (e2e, vm)
testutil/ # shared helpers + MacHost (destructive E2E on real macOS)
scripts/
install.sh # curl|bash installer
Expand All @@ -85,7 +84,7 @@ scripts/
| Change publish flow | `internal/cli/snapshot_publish.go` (`publishSnapshot`) | Slug resolution |
| Source resolution (install) | `internal/cli/install.go` (`resolvePositionalArg`) | file / user-slug / preset / alias detection |
| HTTP with retry | `internal/httputil/ratelimit.go` | Use `httputil.Do()` — handles 429 + Retry-After (archtest: `no-raw-http`) |
| Test tier / when to run | `CONTRIBUTING.md` "Test Layering" | L1–L5 table |
| Test tier / when to run | `CONTRIBUTING.md` "Test Layering" | L1–L4 table |
| Release process | `.github/workflows/` | Tag-driven, release-notes template |

## Project-specific conventions
Expand Down
Loading
Loading