diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml index f3c7d4884..784703bd2 100644 --- a/.github/workflows/squad-ci.yml +++ b/.github/workflows/squad-ci.yml @@ -71,3 +71,29 @@ jobs: - name: Run tests run: npm test + + publish-policy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Enforce workspace-scoped npm publish + run: | + echo "Scanning workflow files for bare npm publish commands..." + VIOLATIONS=0 + for wf in .github/workflows/*.yml; do + # Strip comment lines, then check for npm publish without -w/--workspace + BARE=$(grep -n 'npm.*publish' "$wf" | grep -v '#' | grep -v '\-w ' | grep -v '\-\-workspace' | grep -v 'echo ' | grep -v 'grep ' | grep -v 'name:' || true) + if [ -n "$BARE" ]; then + echo "::error file=$wf::Bare npm publish found (missing -w/--workspace):" + echo "$BARE" + VIOLATIONS=1 + fi + done + if [ "$VIOLATIONS" -eq 1 ]; then + echo "" + echo "::error::PUBLISH POLICY VIOLATION — all npm publish commands must be workspace-scoped (-w or --workspace)" + echo "See: https://github.com/bradygaster/squad/issues/557" + exit 1 + fi + echo "✅ All npm publish commands are workspace-scoped" diff --git a/.squad/agents/eecom/history.md b/.squad/agents/eecom/history.md index 2d26c73ba..bc9b56b6d 100644 --- a/.squad/agents/eecom/history.md +++ b/.squad/agents/eecom/history.md @@ -4,6 +4,16 @@ ## Learnings +### Init scaffolding: casting dir + no-remote stderr (#579) (2025-07-18) + +**Context:** `squad init` in a fresh `git init` repo (no remote) printed `error: No such remote 'origin'` to stderr and `squad doctor` reported `casting/registry.json` missing. Two independent bugs in `packages/squad-sdk/src/config/init.ts`. + +**Fix 1 — Stderr leak:** Three `execFileSync('git', ['remote', 'get-url', 'origin'])` calls in `initSquad()` were missing `stdio: ['pipe','pipe','pipe']`. The try/catch caught the error but git's stderr still leaked to the console. Added stdio piping to all three call sites (lines ~713, ~732, ~1039). + +**Fix 2 — Missing casting files:** The init flow created the `.squad/casting/` directory but never populated it. Added a scaffolding block after directory creation that copies `casting-policy.json`, `casting-registry.json`, and `casting-history.json` from SDK templates (with inline fallbacks). Respects `skipExisting` — never overwrites user files. + +**Pattern:** When calling `execFileSync` for a git command inside a try/catch, always add `stdio: ['pipe','pipe','pipe']` to suppress stderr. The catch prevents a crash, but without piped stdio the error message still prints to the user's terminal. + ### CLI Version Subcommand Pattern (2026-03-23 Release Incident) **Context:** `squad version` returned "Unknown command: version" even though `squad --version` and `squad -v` worked fine. Classic "unwired command" bug but for a flag-to-subcommand gap rather than a missing import. diff --git a/.squad/agents/fido/history.md b/.squad/agents/fido/history.md index f84d20286..7381edbc7 100644 --- a/.squad/agents/fido/history.md +++ b/.squad/agents/fido/history.md @@ -172,3 +172,9 @@ Commit: 7660a27 on branch squad/579-init-scaffolding-hardening. **Key finding:** `resolvePersonalSquadDir()` is install-method-agnostic — it resolves from env vars and `os.homedir()`, never from `process.argv`. The npx issue (#576) is therefore NOT in path resolution but likely in the CLI command wiring or the `--global` flag routing. Tests confirm the SDK layer works correctly. **Commit:** c307187 on branch squad/576-personal-squad-init-npx +### Publish Policy CI Gate (#557) + +Added `publish-policy` job to squad-ci.yml — lightweight lint that scans all `.github/workflows/*.yml` for bare `npm publish` commands missing `-w`/`--workspace`. Catches the incident class where root package.json gets published instead of a workspace package. Also wrote `test/publish-policy.test.ts` (36 tests) covering: workspace-scoped passes, bare publish fails, comment/echo/grep/YAML-name line skipping, findViolations line numbering, and live validation of all 15 workflow files. Key pattern: meta-references (echo, grep, YAML name keys containing "npm publish") must be excluded from lint — the CI script's own text would otherwise self-trigger. + +📌 **Team update (2026-03-24T06-release-hardening):** Publish policy CI gate (#557) implemented. Added `publish-policy` job to squad-ci.yml: lightweight lint scans `.github/workflows/*.yml` for bare `npm publish` commands, rejects non-workspace-scoped invocations. Wrote test/publish-policy.test.ts (36 tests) validating: workspace-scoped passes, bare publish fails, meta-reference (echo/grep/YAML-name) skipping, live validation of 15 workflow files. Pattern: catch "publish root package.json" incident class before merge. Both lint + playbook docs create enforcement + education loop. + diff --git a/.squad/agents/flight/history.md b/.squad/agents/flight/history.md index 93c13e2b3..053b15bd2 100644 --- a/.squad/agents/flight/history.md +++ b/.squad/agents/flight/history.md @@ -135,3 +135,20 @@ Community contributor joniba filed #525 identifying that Squad has full worktree **Decision:** P2 — important but not v1-blocking. Broke into 5 sub-issues: (1) doc fix for missing issue-lifecycle.md (quick win → Procedures), (2) worktree variant in ralph-commands.ts (EECOM), (3) coordinator pre-spawn logic (Procedures + EECOM), (4) post-merge cleanup (EECOM), (5) architecture decision on heuristic (Flight). Sub-issue #1 ships immediately; #2–5 queue post-Wave-1 alongside SubSquads work where parallel execution becomes a hard requirement. **Backlog priority recommendation:** Top 5 for v1 = #508 (Ambient Personal Squad), #498 (remove .squad/ from VCS), #485 (Agent Spec & Validation), #481 (Typed StorageProvider), #347 (shore up init --sdk). Quick wins: #525 doc fix, #347. Deprioritize: manual verification debt (#418–421), long-term exploratory. A2A (#332–336) stays shelved per existing decision. + +### Release Hardening Plan — Finalized (2026-07-22) + +Brady approved scope for remaining v0.9.1 incident hardening. Three issues to execute, three deferred into umbrella: + +**DO:** #564 (rewrite PUBLISH-README.md as living playbook — absorbs #558, #559, #560), #557 (CI lint rule rejecting non-workspace `npm publish` in workflow YAML), #562 (delete ghost workflow `publish-npm.yml` ID 250121956). + +**DEFERRED into #564:** #560 (pre-flight checklist → playbook section), #559 (fallback protocol → playbook section), #558 (422 race docs → playbook section). + +**Key findings:** +- GitHub REST API has NO "Delete a workflow" endpoint. Ghost workflows only disappear when all their runs are deleted (GitHub GC). Procedure: `gh api` to list+delete all runs for workflow ID 250121956, then wait for GC. +- The lint rule goes in `squad-ci.yml` as a `publish-policy` job: scans `.github/workflows/*.yml` for `npm publish` without `-w` flag. Blocks PR merge if violated. +- PUBLISH-README.md playbook has 11 sections covering pre-flight, CI publish, manual fallback, 422 race conditions, insider channel, workspace policy, post-publish verification, and version bumping. Replaces the stale v0.8.22 stub entirely. + +**Execution order:** #562 (Brady, manual API call) and #557 (FIDO/Procedures, CI change) run in parallel. #564 (Procedures+Surgeon, playbook) goes last so it can reference the lint rule. + +Decision written to `.squad/decisions/inbox/flight-release-hardening-plan.md`. diff --git a/.squad/agents/pao/history.md b/.squad/agents/pao/history.md index 7d6d7cab0..abbb323e2 100644 --- a/.squad/agents/pao/history.md +++ b/.squad/agents/pao/history.md @@ -162,3 +162,33 @@ Teams MCP critical update: Office 365 Connectors retired Dec 2024 → Power Auto **Pattern observed:** Feature-release timing + follow-up responses critical for community trust. v0.9.1 directly addressed 5+ discussions (models, skills, human members) that were open 2-4 weeks. Community triage now operational: 14 discussions reviewed, 6 closed, 8 kept active = 43% closure rate on resolved items. **Key insight:** Retirement of Microsoft Office 365 Connectors (Dec 2024) caught users mid-setup. Proactive notification of Teams Workflows alternative + Power Automate guidance essential for Teams MCP users. + +### Release Playbook Rewrite (#564, 2026-07-22) + +**Task:** Rewrite PUBLISH-README.md from a v0.8.22 version-specific stub (58 lines) into a living, version-agnostic release playbook. + +**Outcome:** 232-line playbook replacing entirely with 11 sections per Flight's spec: +1. Overview — two publish channels, package order (SDK → CLI) +2. Pre-Flight Checklist — runnable checklist with `grep`/`npm` commands +3. Publish via CI (Recommended Path) — GitHub Release workflow +4. Publish via workflow_dispatch — manual trigger fallback +5. Insider Channel — insider branch + `@insider` tag for testing +6. Workspace Publish Policy — reference to CI lint rule #557 (enforces `-w` flag) +7. Manual Local Publish — emergency fallback with step-by-step commands +8. 422 Race Condition & npm Errors — v0.9.1 incident + troubleshooting +9. Post-Publish Verification — `npm view` + npx cold-install test +10. Version Bump After Publish — preview version increment pattern +11. Legacy Publish Scripts — deprecation notice for PowerShell scripts + +**Key decisions:** +- Microsoft Style Guide enforced: sentence-case headings, active voice, "you" not "we", present tense +- Version-agnostic: `` placeholder, no hardcoded version numbers +- Scannability: checklist format, code blocks (bash not PowerShell for portability), tables for error reference +- Accuracy: pulled from actual workflows (`squad-npm-publish.yml`, `squad-insider-publish.yml`) — preflight job, smoke test, publish stages, registry propagation retry logic (5× 15-second intervals) +- Runnable: all commands copy-pasteable (e.g., `npm -w packages/squad-sdk pack --dry-run`) + +**Pattern:** Living playbook absorbs three related issues (#558 race conditions, #559 manual publish, #560 pre-flight checklist) into unified reference. No separate documents; all under one decision tree: try CI first, use manual only if CI broken. Workspace publish policy section references CI lint rule #557 (being added in parallel by FIDO); both docs + lint create enforcement + education. + +**Commit:** `docs: rewrite PUBLISH-README.md as release playbook (#564)` on squad/release-hardening branch. + +📌 **Team update (2026-03-24T06-release-hardening):** Release playbook rewrite (#564) completed. PUBLISH-README.md transformed from v0.8.22 stub to living 232-line playbook with 11 sections: Overview, Pre-Flight Checklist, Publish via CI (recommended), Publish via workflow_dispatch, Insider Channel, Workspace Publish Policy, Manual Local Publish (emergency fallback), 422 Race Condition & npm Errors, Post-Publish Verification, Version Bump After Publish, Legacy Publish Scripts. Absorbed issues #558, #559, #560 into unified decision tree. Microsoft Style Guide enforced; version-agnostic; all commands runnable. Scannability: checklist format, bash code blocks, error reference table. Committed to squad/release-hardening. diff --git a/.squad/decisions.md b/.squad/decisions.md index 5a954af0c..ded855710 100644 --- a/.squad/decisions.md +++ b/.squad/decisions.md @@ -7288,3 +7288,297 @@ Prioritize squad.config.ts sync fixes over new commands. Implement in this order **Why:** Agents were claiming "done" without completing all checklist items. The verification step enforces the checklist as a contract. Opt-in by structure — zero overhead for issues without checkboxes. **PR:** #473 **Issue:** #472 + +--- + +# Decision: Release Hardening Plan + +**By:** Flight +**Date:** 2026-07-22 +**Status:** Approved (Brady-approved scope) +**Issues:** #564 (umbrella), #557, #562. Deferred into #564: #558, #559, #560. + +--- + +## Summary + +Three concrete work items remain to close out v0.9.1 release incident hardening. This plan specifies exactly what gets built, in what order, and who does what. No fluff. + +--- + +## 1. #564 — Rewrite PUBLISH-README.md as Release Playbook + +**What:** Replace the stale 58-line v0.8.22 stub with a living, version-agnostic release playbook. +**Owner:** Procedures (formal playbook structure) + Surgeon (publish-specific content) +**Reviewer:** Flight +**File:** `PUBLISH-README.md` (root — same location, new content) +**Absorbs:** #558, #559, #560 (each becomes a section below) + +### Exact Sections + +``` +# Release Playbook + +## Overview +- What this document is (living playbook, not version-specific instructions) +- Two publish channels: stable (squad-npm-publish.yml) and insider (squad-insider-publish.yml) +- Package order: SDK first, CLI second (CLI depends on SDK) + +## Pre-Flight Checklist (absorbs #560) +- [ ] All tests pass on dev branch (`npm test` — expect 3900+ tests) +- [ ] No `file:` references in any packages/*/package.json dependencies +- [ ] All package.json versions are valid semver (no -preview suffix for release) +- [ ] SDK dependency in squad-cli is a version range, not `file:../squad-sdk` +- [ ] `npm run build` succeeds clean (no TypeScript errors) +- [ ] `npm -w packages/squad-sdk pack --dry-run` and `npm -w packages/squad-cli pack --dry-run` both succeed +- [ ] Git tag matches package.json versions +- [ ] CHANGELOG.md updated for this version +- [ ] GitHub Release draft created (triggers squad-npm-publish.yml on publish) + +## Publish via CI (Recommended Path) +- Create GitHub Release → triggers `squad-npm-publish.yml` +- Pipeline stages: preflight → smoke-test → publish-sdk → publish-cli +- Each stage has version match verification and npm registry propagation checks +- SDK publishes first; CLI job depends on successful SDK publish +- Provenance attestation is automatic (--provenance flag) +- Monitor: Actions tab → "Squad npm Publish" workflow + +## Publish via workflow_dispatch (Manual Trigger) +- Go to Actions → "Squad npm Publish" → Run workflow +- Input: version string (e.g., "0.9.2") +- Same pipeline as release-triggered publish +- Use when: re-publishing after a failed attempt, or publishing without a GitHub Release + +## Insider Channel +- Pushes to `insider` branch auto-trigger `squad-insider-publish.yml` +- Publishes both packages with `--tag insider` +- No preflight job (insider is for testing, not production) +- Install: `npm install @bradygaster/squad-cli@insider` + +## Workspace Publish Policy +- NEVER use `npm publish` from the repo root (publishes the wrong package) +- ALWAYS use `npm -w packages/squad-sdk publish` or `npm -w packages/squad-cli publish` +- CI enforces this — see lint rule (#557) +- Manual local publish is a fallback, not the default path + +## Manual Local Publish (Emergency Fallback) (absorbs #559) +- When to use: CI is broken, npm is having issues, or you need to publish NOW +- Prerequisites: `NPM_TOKEN` or `npm login` with 2FA, build succeeds locally +- Steps: + 1. `npm ci && npm run build` + 2. Run pre-flight checklist above manually + 3. `cd packages/squad-sdk && npm publish --access public --otp=` + 4. Verify: `npm view @bradygaster/squad-sdk@ version` + 5. `cd ../squad-cli && npm publish --access public --otp=` + 6. Verify: `npm view @bradygaster/squad-cli@ version` +- ALWAYS publish SDK before CLI +- If CLI publish fails after SDK succeeds: SDK is already live, fix CLI and re-publish (do NOT unpublish SDK) + +## 422 Race Condition & npm Errors (absorbs #558) +- **What happened:** During v0.9.1, npm returned 422 because the package version already existed +- **Root cause:** `file:` dependency caused SDK to resolve locally instead of from registry. When CI tried to publish, the version check saw the wrong state. +- **If you get 422 "Version already exists":** + 1. Check if the package IS actually published: `npm view @bradygaster/squad-@` + 2. If yes — it succeeded, the 422 was a race. Move on. + 3. If no — bump the version, fix the issue, re-publish +- **If you get 403 "Forbidden":** NPM_TOKEN is expired or missing. Regenerate at npmjs.com → Access Tokens. +- **If you get ETARGET "No matching version":** You published SDK but CLI's dependency hasn't propagated yet. Wait 60s and retry. +- **npm registry propagation:** Takes 15-60 seconds. The CI workflow retries 5 times with 15s intervals. + +## Post-Publish Verification +- `npm view @bradygaster/squad-sdk@ version` +- `npm view @bradygaster/squad-cli@ version` +- `npx @bradygaster/squad-cli@ --version` (cold install test) +- Check GitHub Release is marked as "Latest" + +## Version Bump After Publish +- After stable publish, bump all package.json to next preview: `X.Y.(Z+1)-preview.1` +- Files to update: root `package.json`, `packages/squad-sdk/package.json`, `packages/squad-cli/package.json` +- Commit to dev branch, not main + +## Legacy Publish Scripts (Deprecated) +- `publish-0.8.21.ps1`, `publish-0.8.22.ps1`, `publish-0.9.1.ps1` exist in repo root +- These are version-specific and superseded by CI publish +- Do NOT create new version-specific publish scripts +- Existing scripts may be deleted in a future cleanup +``` + +### What Gets Deleted from Current PUBLISH-README.md + +Everything. The current content is a v0.8.22-specific stub. The new playbook replaces it entirely. + +--- + +## 2. #557 — CI Lint Rule: Reject `npm -w ... publish` in Workflow YAML + +**What:** A CI check that fails if any workflow YAML contains bare `npm ... publish` without using the workspace-scoped pattern correctly — specifically, it prevents someone from adding `npm publish` (without `-w`) in a workflow file, which would publish the root package instead of the correct workspace package. + +**Owner:** FIDO (CI/lint domain) or Procedures (governance) +**Reviewer:** Flight +**Where it runs:** New job in `squad-ci.yml`, runs on every PR and push to dev/insider +**What it checks:** Scans `.github/workflows/*.yml` for `npm publish` invocations that are NOT workspace-scoped. + +### Exact Implementation + +Add a new job to `.github/workflows/squad-ci.yml`: + +```yaml + publish-policy: + name: Workspace publish policy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Reject non-workspace npm publish in workflows + run: | + echo "Checking workflow files for non-workspace npm publish commands..." + VIOLATIONS=0 + for f in .github/workflows/*.yml; do + # Find lines with 'npm publish' or 'npm ... publish' that do NOT have '-w' flag + # Exclude comments (lines starting with #) + while IFS= read -r line; do + # Skip comment lines + [[ "$line" =~ ^[[:space:]]*# ]] && continue + # Match 'npm publish' without '-w' or '--workspace' + if echo "$line" | grep -qP 'npm\s+publish' && ! echo "$line" | grep -qP 'npm\s+-w\s|npm\s+--workspace'; then + echo "::error file=$f::Found non-workspace 'npm publish' — use 'npm -w packages/ publish' instead" + echo " → $line" + VIOLATIONS=$((VIOLATIONS + 1)) + fi + done < <(grep -n 'npm.*publish' "$f" || true) + done + if [ "$VIOLATIONS" -gt 0 ]; then + echo "" + echo "::error::BLOCKED — $VIOLATIONS workflow file(s) use 'npm publish' without workspace scope." + echo "Policy: Always use 'npm -w packages/squad-sdk publish' or 'npm -w packages/squad-cli publish'." + echo "See PUBLISH-README.md → Workspace Publish Policy." + exit 1 + fi + echo "✅ All npm publish commands are workspace-scoped" +``` + +### What This Catches + +- `npm publish` (bare, would publish root package.json) +- `npm publish --access public` (bare with flags) +- `run: npm publish --tag insider` (insider without workspace) + +### What This Allows + +- `npm -w packages/squad-sdk publish --access public --provenance` ✅ +- `npm -w packages/squad-cli publish --tag insider --access public` ✅ + +### Documentation + +Add the policy to PUBLISH-README.md under "Workspace Publish Policy" (already in the outline above). + +--- + +## 3. #562 — Delete Ghost Workflow `publish-npm.yml` (ID 250121956) + +**What:** The file `.github/workflows/publish-npm.yml` was already deleted from disk, but the workflow ghost persists in GitHub Actions UI with state `disabled_manually`. + +**Owner:** Brady (requires repo admin + API token) +**Why Brady:** This is a one-time API operation requiring admin-level access, not a code change. + +### Research Finding + +GitHub has NO `DELETE /repos/{owner}/{repo}/actions/workflows/{id}` endpoint. The Workflows REST API only supports List, Get, Disable, and Enable. **You cannot directly delete a workflow.** + +### The Actual Path to Clear It + +GitHub auto-garbage-collects a workflow entry once it has **zero workflow runs**. The procedure: + +1. **List all runs for the ghost workflow:** + ```bash + gh api repos/bradygaster/squad/actions/workflows/250121956/runs \ + --paginate -q '.workflow_runs[].id' + ``` + +2. **Delete every run:** + ```bash + gh api repos/bradygaster/squad/actions/workflows/250121956/runs \ + --paginate -q '.workflow_runs[].id' | \ + while read run_id; do + echo "Deleting run $run_id" + gh api -X DELETE repos/bradygaster/squad/actions/runs/$run_id + done + ``` + +3. **Verify the workflow is gone:** + ```bash + gh api repos/bradygaster/squad/actions/workflows/250121956 + ``` + If all runs are deleted, this should eventually return 404 (GitHub may take a few minutes to GC). + +4. **If it still shows:** GitHub's GC is not instant. Wait 24 hours and check again. If it persists after 24h with zero runs, contact GitHub Support — this is a known limitation. + +### Alternative: If Zero Runs Already Exist + +If the ghost workflow has no runs at all and still shows, this is a GitHub bug. The only path is GitHub Support. Document this in the issue and close with "waiting on GitHub GC" status. + +--- + +## Execution Order + +| Order | Issue | Work | Owner | Depends On | +|-------|-------|------|-------|------------| +| 1 | #562 | Delete ghost workflow runs via `gh api` | Brady (manual) | Nothing | +| 2 | #557 | Add `publish-policy` job to squad-ci.yml | FIDO or Procedures | Nothing | +| 3 | #564 | Rewrite PUBLISH-README.md | Procedures + Surgeon | #557 (so playbook can reference the lint rule) | + +Items 1 and 2 are independent and can execute in parallel. Item 3 should go last so it references the lint rule that already exists. + +--- + +## What We Are NOT Doing + +- No new publish scripts (CI is the path) +- No unpublishing or republishing anything +- No changes to `squad-npm-publish.yml` or `squad-insider-publish.yml` (they're working correctly) +- No separate documents for #558, #559, #560 (absorbed into #564 playbook sections) + + +--- + +# Decision: Publish Policy CI Gate + +**By:** FIDO +**Date:** 2025-07-24 +**Issue:** #557 + +## What + +All `npm publish` commands in `.github/workflows/*.yml` must be workspace-scoped (`-w` or `--workspace`). A CI job (`publish-policy`) now enforces this on every PR and push to dev/insider. + +## Why + +Bare `npm publish` would publish the root `package.json` instead of a workspace package — a critical incident vector. This gate catches it before merge. + +## Pattern + +Meta-references to "npm publish" in echo, grep, and YAML `name:` lines are excluded from the lint to prevent self-triggering. The test suite (`test/publish-policy.test.ts`) validates both the lint logic and all live workflow files. + + +--- + +# Decision: `init --global` bootstraps personal-squad/ directory + +**By:** EECOM +**Date:** 2026-07-23 +**Issue:** #576 + +## What + +`init --global` now also creates the `personal-squad/` directory (via `ensurePersonalSquadDir()`) alongside the full `.squad/` structure. Repo-level `init` detects and acknowledges existing personal squads. + +## Why + +`resolveGlobalSquadPath()` returns `~/.config/squad/` — the container. But `resolvePersonalSquadDir()` looks for `~/.config/squad/personal-squad/`. Without the bridge, `init --global` never created the subdirectory that the rest of the personal squad system depends on. + +## Impact + +- `ensurePersonalSquadDir()` is a new SDK export — any code that needs to guarantee the personal squad directory exists should use it. +- `init --global` now suppresses GitHub workflows (they're meaningless in the global config dir). +- `RunInitOptions` has a new `isGlobal` field. + diff --git a/.squad/skills/release-process/SKILL.md b/.squad/skills/release-process/SKILL.md index a11f3ad18..28d62b5ed 100644 --- a/.squad/skills/release-process/SKILL.md +++ b/.squad/skills/release-process/SKILL.md @@ -117,9 +117,15 @@ Set this environment variable in all CI build steps to prevent the build script | Draft GitHub Releases | npm publish workflow doesn't trigger | Never create drafts | | User npm tokens with 2FA | EOTP errors in CI | Use Automation token type | +## CI Gate: Workspace Publish Policy + +The `publish-policy` job in `squad-ci.yml` scans all workflow files for bare `npm publish` commands that are missing `-w`/`--workspace` flags. Any workflow that attempts a non-workspace-scoped publish will fail CI. This prevents accidental root-level publishes that would push the wrong `package.json` to npm. + +See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation details. + ## Related - Issues: #556–#564 (release:next) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Playbook (for Brady's review): session files/release-playbook.md +- Playbook: `PUBLISH-README.md` (repo root) diff --git a/PUBLISH-README.md b/PUBLISH-README.md index c4c455d08..b7d8de1ae 100644 --- a/PUBLISH-README.md +++ b/PUBLISH-README.md @@ -1,57 +1,231 @@ -# Publish Instructions for v0.8.22 +# Release Playbook -## Quick Publish (Recommended) +This living playbook documents how to publish Squad releases to npm. Follow the recommended CI path whenever possible; use manual publish only as an emergency fallback. -```powershell -.\publish-0.8.22.ps1 -OTP +## Overview + +Squad publishes two npm packages: +- `@bradygaster/squad-sdk` — the core SDK +- `@bradygaster/squad-cli` — the CLI tool (depends on the SDK) + +**Package order matters:** always publish the SDK first, then the CLI. The CLI declares a version range dependency on the SDK, and npm registry needs time to propagate the SDK before the CLI can reference it safely. + +Two publish channels exist: +- **Stable:** triggered by GitHub Release (recommended path) +- **Insider:** triggered by push to `insider` branch (for testing pre-release builds) + +## Pre-Flight Checklist + +Complete this before any publish attempt: + +- [ ] All tests pass: `npm test` (expect 3,900+ tests) +- [ ] No `file:` references in any `packages/*/package.json` + - Check: `grep -r "file:" packages/*/package.json` should return nothing +- [ ] All `package.json` versions use valid semver (no `-preview` suffix for release) + - Release versions: `1.2.3` + - Pre-release versions: `1.2.3-preview.1` +- [ ] SDK dependency in `packages/squad-cli/package.json` is a version range, not `file:../squad-sdk` + - Example valid: `"@bradygaster/squad-sdk": "^0.9.2"` +- [ ] Build succeeds clean: `npm run build` (no TypeScript errors) +- [ ] Package validation passes: + ```bash + npm -w packages/squad-sdk pack --dry-run + npm -w packages/squad-cli pack --dry-run + ``` +- [ ] Git tag matches version: `git tag` shows `v` +- [ ] `CHANGELOG.md` updated for this version +- [ ] GitHub Release draft created with notes + +## Publish via CI (Recommended Path) + +**This is the standard path for all releases.** + +### Steps + +1. In the GitHub UI, go to **Releases** → **Draft** +2. Finalize the release notes and click **Publish Release** +3. This triggers `squad-npm-publish.yml` automatically + +### What the workflow does + +The workflow runs four jobs in sequence: + +1. **Preflight** — validates no `file:` dependencies exist +2. **Smoke test** — runs `npm pack --dry-run` and CLI integration tests +3. **Publish SDK** — publishes `@bradygaster/squad-sdk` with provenance attestation +4. **Publish CLI** — waits for SDK to succeed, then publishes `@bradygaster/squad-cli` + +Each publish step: +- Verifies version matches the release tag +- Uses `npm -w packages/ publish --access public --provenance` +- Retries registry verification up to 5 times (15-second intervals) to account for propagation delay + +### Monitor the workflow + +Go to **Actions** → **Squad npm Publish** → select the latest run. All jobs must pass. If any job fails, see "Troubleshooting" below. + +## Publish via workflow_dispatch (Manual Trigger) + +**Use this when you need to republish without creating a new GitHub Release.** + +### Steps + +1. Go to **Actions** → **Squad npm Publish** +2. Click **Run workflow** +3. Enter the version string (e.g., `0.9.2`) +4. Click **Run workflow** + +Same workflow runs as above. Use this to retry after a transient npm registry issue. + +## Insider Channel + +**For pre-release testing only.** + +Pushes to the `insider` branch auto-trigger `squad-insider-publish.yml`, which: +- Publishes both SDK and CLI with `--tag insider` +- Skips the preflight job (insider builds may have experimental dependencies) +- Uses tag `insider` instead of `latest` + +Install insider builds with: +```bash +npm install -g @bradygaster/squad-cli@insider +``` + +Users see the latest `latest` tag by default; they opt in to `insider` explicitly. + +## Workspace Publish Policy + +**NEVER use `npm publish` from the repo root.** + +Using `npm publish` without workspace scope publishes the root `package.json` instead of the intended package. This breaks everything. + +**ALWAYS use:** +```bash +npm -w packages/squad-sdk publish --access public +npm -w packages/squad-cli publish --access public +``` + +The CI workflows enforce this automatically via a lint rule. If you add any publish step to a workflow, it will be caught at CI gate. + +For more details on this policy, see `.github/workflows/squad-ci.yml` → `publish-policy` job. + +## Manual Local Publish (Emergency Fallback) + +**Use this only when CI is broken and you must publish NOW.** + +Prerequisites: +- `npm login` completed (or `NPM_TOKEN` environment variable set with a token from npmjs.com) +- Build succeeds locally: `npm run build` +- Pre-flight checklist passes + +### Steps + +1. Install dependencies and build: + ```bash + npm ci && npm run build + ``` + +2. Run the pre-flight checklist manually (above) + +3. Publish SDK: + ```bash + cd packages/squad-sdk + npm publish --access public --otp= + cd ../.. + ``` + Replace `` with your 2FA code from the authenticator app. + +4. Verify SDK is live (wait up to 60 seconds for registry propagation): + ```bash + npm view @bradygaster/squad-sdk@ version + ``` + +5. Publish CLI: + ```bash + cd packages/squad-cli + npm publish --access public --otp= + cd ../.. + ``` + +6. Verify CLI is live: + ```bash + npm view @bradygaster/squad-cli@ version + ``` + +### Critical rule + +**Always publish SDK before CLI.** The CLI declares a dependency on the SDK, and npm needs the SDK version to exist in the registry. + +### If SDK succeeds but CLI fails + +Do NOT unpublish the SDK. Fix the CLI issue and republish the CLI. Both versions are already incremented; re-running the publish is safe. + +## 422 Race Condition & npm Errors + +During the v0.9.1 release, npm returned a 422 error ("Version already exists") even though the version hadn't been published yet. This was caused by `file:` dependencies confusing the version check. + +### Error: 422 "Version already exists" + +**First, verify whether the package is actually published:** +```bash +npm view @bradygaster/squad-@ version ``` -This script will: -1. ✅ Publish squad-sdk@0.8.22 to npm -2. ✅ Publish squad-cli@0.8.22 to npm -3. ✅ Verify both packages are live -4. ✅ Bump all package.json to 0.8.23-preview.1 -5. ✅ Commit and push to dev +- **If npm returns the version:** The publish succeeded. The 422 was a race condition. Move on. +- **If npm returns "404 Not Found":** The publish failed. Bump the version, fix the root issue, and re-publish. + +### Error: 403 "Forbidden" -## Manual Publish (If you prefer) +Your `NPM_TOKEN` is expired or missing. Regenerate it at npmjs.com → **Access Tokens** → **Generate New Token** (Automation, no expiration). -```powershell -# Get OTP from your authenticator app, then: +### Error: ETARGET "No matching version" -# 1. Publish SDK -cd packages\squad-sdk -npm publish --access public --otp= -cd ..\.. +You published the SDK, but the CLI can't find the SDK version in the registry yet. **Wait 60 seconds** and retry. The CI workflow automatically retries 5 times with 15-second intervals. -# 2. Publish CLI -cd packages\squad-cli -npm publish --access public --otp= -cd ..\.. +### npm registry propagation -# 3. Verify both live -npm view @bradygaster/squad-sdk version # should show 0.8.22 -npm view @bradygaster/squad-cli version # should show 0.8.22 +npm takes 15–60 seconds to propagate a new package version to all edge caches. The CI workflow accounts for this with retry logic. If manual publishing, retry the CLI publish after waiting. -# 4. Bump to next preview version -# (Run publish-0.8.22.ps1 -OTP dummy -SkipBump to skip the actual publish -# but do the version bump, OR manually edit 3 package.json files) +## Post-Publish Verification + +After publish completes (via CI or manual), verify both packages are live: + +```bash +npm view @bradygaster/squad-sdk@ version +npm view @bradygaster/squad-cli@ version +npx @bradygaster/squad-cli@ --version ``` -## What's Ready +The third command does a fresh install and tests the CLI from npm. This confirms the build, packaging, and CLI entrypoint are all working. + +Check GitHub Releases and confirm the release is marked as **Latest**. + +## Version Bump After Publish + +After a stable release, bump the repository versions for continued development: + +1. Update all `package.json` files to the next preview version: + - `package.json` (root) + - `packages/squad-sdk/package.json` + - `packages/squad-cli/package.json` + - Format: `..-preview.1` + +2. Commit to the dev branch (NOT main): + ```bash + git add package.json packages/squad-sdk/package.json packages/squad-cli/package.json + git commit -m "chore: bump to next preview version" + git push origin dev + ``` -- ✅ Version 0.8.22 in all package.json files -- ✅ Build verified clean (3,768 tests pass) -- ✅ Git tag v0.8.22 created and pushed -- ✅ GitHub Release created -- ⏸️ Waiting on: 2FA code for npm publish +Example: if you just released `0.9.2`, bump to `0.9.3-preview.1`. -## After Publish +## Legacy Publish Scripts (Deprecated) -Both packages will be live at: -- https://www.npmjs.com/package/@bradygaster/squad-sdk/v/0.8.22 -- https://www.npmjs.com/package/@bradygaster/squad-cli/v/0.8.22 +The repo contains version-specific publish scripts: +- `publish-0.8.21.ps1` +- `publish-0.8.22.ps1` +- `publish-0.9.1.ps1` -Repository will be bumped to v0.8.23-preview.1 for continued development. +These are **deprecated.** They are version-specific and no longer maintained. ---- -*Prepared by: Rabin (Distribution)* +**Do NOT create new version-specific publish scripts.** The CI workflows are the standard path. Existing scripts may be deleted in a future cleanup. diff --git a/packages/squad-cli/templates/skills/release-process/SKILL.md b/packages/squad-cli/templates/skills/release-process/SKILL.md index a11f3ad18..28d62b5ed 100644 --- a/packages/squad-cli/templates/skills/release-process/SKILL.md +++ b/packages/squad-cli/templates/skills/release-process/SKILL.md @@ -117,9 +117,15 @@ Set this environment variable in all CI build steps to prevent the build script | Draft GitHub Releases | npm publish workflow doesn't trigger | Never create drafts | | User npm tokens with 2FA | EOTP errors in CI | Use Automation token type | +## CI Gate: Workspace Publish Policy + +The `publish-policy` job in `squad-ci.yml` scans all workflow files for bare `npm publish` commands that are missing `-w`/`--workspace` flags. Any workflow that attempts a non-workspace-scoped publish will fail CI. This prevents accidental root-level publishes that would push the wrong `package.json` to npm. + +See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation details. + ## Related - Issues: #556–#564 (release:next) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Playbook (for Brady's review): session files/release-playbook.md +- Playbook: `PUBLISH-README.md` (repo root) diff --git a/packages/squad-sdk/templates/skills/release-process/SKILL.md b/packages/squad-sdk/templates/skills/release-process/SKILL.md index a11f3ad18..28d62b5ed 100644 --- a/packages/squad-sdk/templates/skills/release-process/SKILL.md +++ b/packages/squad-sdk/templates/skills/release-process/SKILL.md @@ -117,9 +117,15 @@ Set this environment variable in all CI build steps to prevent the build script | Draft GitHub Releases | npm publish workflow doesn't trigger | Never create drafts | | User npm tokens with 2FA | EOTP errors in CI | Use Automation token type | +## CI Gate: Workspace Publish Policy + +The `publish-policy` job in `squad-ci.yml` scans all workflow files for bare `npm publish` commands that are missing `-w`/`--workspace` flags. Any workflow that attempts a non-workspace-scoped publish will fail CI. This prevents accidental root-level publishes that would push the wrong `package.json` to npm. + +See `.github/workflows/squad-ci.yml` → `publish-policy` job for implementation details. + ## Related - Issues: #556–#564 (release:next) - Retro: `.squad/decisions/inbox/surgeon-v091-retrospective.md` - CI audit: `.squad/decisions/inbox/booster-ci-audit.md` -- Playbook (for Brady's review): session files/release-playbook.md +- Playbook: `PUBLISH-README.md` (repo root) diff --git a/test/publish-policy.test.ts b/test/publish-policy.test.ts new file mode 100644 index 000000000..7b29ecbe5 --- /dev/null +++ b/test/publish-policy.test.ts @@ -0,0 +1,146 @@ +/** + * Publish Policy Lint Test (#557) + * + * Validates that all npm publish commands in workflow files are + * workspace-scoped (-w or --workspace). Bare npm publish would + * publish the root package.json — a critical incident vector. + * + * Also tests the grep logic itself with known good/bad inputs. + */ + +import { describe, it, expect } from 'vitest'; +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// ─── Core lint function (mirrors CI shell logic) ──────────────────────── + +/** + * Returns true if the line contains a workspace-scoped npm publish + * or is not a publish command at all. Returns false for bare publishes. + */ +function isCompliant(line: string): boolean { + const trimmed = line.trim(); + // Skip comment lines + if (trimmed.startsWith('#')) return true; + // If no npm publish pattern, line is fine + if (!/npm.*publish/.test(trimmed)) return true; + // Skip meta-references: echo output, grep patterns, YAML name keys + if (/echo\s/.test(trimmed)) return true; + if (/grep\s/.test(trimmed)) return true; + if (/^\s*-?\s*name:/.test(line)) return true; + // Must have -w or --workspace flag + return /\s-w[\s]/.test(trimmed) || /\s-w$/.test(trimmed) || /\s--workspace[\s]/.test(trimmed) || /\s--workspace$/.test(trimmed); +} + +/** + * Scans a YAML file's lines and returns violations (bare npm publish). + */ +function findViolations(content: string): { line: number; text: string }[] { + const violations: { line: number; text: string }[] = []; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (!isCompliant(lines[i])) { + violations.push({ line: i + 1, text: lines[i].trim() }); + } + } + return violations; +} + +// ─── Unit tests for the lint logic ────────────────────────────────────── + +describe('publish-policy lint logic', () => { + describe('should PASS (workspace-scoped)', () => { + const goodLines = [ + 'npm -w packages/squad-sdk publish --access public --provenance', + 'npm -w packages/squad-cli publish --tag insider --access public', + 'run: npm -w packages/squad-sdk publish --access public --provenance', + 'run: npm -w packages/squad-cli publish --tag insider --access public', + 'npm --workspace packages/squad-sdk publish --access public', + 'npm --workspace packages/squad-cli publish --tag insider', + ]; + + for (const line of goodLines) { + it(`allows: ${line}`, () => { + expect(isCompliant(line)).toBe(true); + }); + } + }); + + describe('should FAIL (bare publish)', () => { + const badLines = [ + 'npm publish', + 'npm publish --access public', + 'run: npm publish --tag insider', + 'run: npm publish --access public --provenance', + 'npm publish --tag insider --access public', + ]; + + for (const line of badLines) { + it(`rejects: ${line}`, () => { + expect(isCompliant(line)).toBe(false); + }); + } + }); + + describe('should skip non-publish and comment lines', () => { + const neutralLines = [ + '# npm publish --access public', + ' # run: npm publish', + 'npm install', + 'npm test', + 'echo "npm publish is dangerous"', + 'npm ci', + ' - name: Enforce workspace-scoped npm publish', + 'BARE=$(grep -n \'npm.*publish\' "$wf" || true)', + ]; + + for (const line of neutralLines) { + it(`ignores: ${line}`, () => { + expect(isCompliant(line)).toBe(true); + }); + } + }); + + it('findViolations returns correct line numbers', () => { + const content = [ + 'name: test', + '# npm publish bare in comment', + 'run: npm -w packages/sdk publish --access public', + 'run: npm publish --access public', + 'run: npm publish', + ].join('\n'); + + const violations = findViolations(content); + expect(violations).toHaveLength(2); + expect(violations[0].line).toBe(4); + expect(violations[0].text).toBe('run: npm publish --access public'); + expect(violations[1].line).toBe(5); + expect(violations[1].text).toBe('run: npm publish'); + }); +}); + +// ─── Live workflow file validation ────────────────────────────────────── + +describe('publish-policy: live workflow files', () => { + const workflowDir = join(process.cwd(), '.github', 'workflows'); + const workflowFiles = readdirSync(workflowDir).filter(f => f.endsWith('.yml')); + + it('workflow directory has files to check', () => { + expect(workflowFiles.length).toBeGreaterThan(0); + }); + + for (const file of workflowFiles) { + it(`${file} — no bare npm publish`, () => { + const content = readFileSync(join(workflowDir, file), 'utf-8'); + const violations = findViolations(content); + if (violations.length > 0) { + const details = violations + .map(v => ` line ${v.line}: ${v.text}`) + .join('\n'); + expect.fail( + `Bare npm publish found in ${file} (missing -w/--workspace):\n${details}`, + ); + } + }); + } +});