Skip to content
Closed
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
59 changes: 47 additions & 12 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,40 +1,75 @@
# Keeps SHA-pinned GitHub Actions (SWR-SEC-ACTION-PINNING) and lockfiles
# (SWR-SEC-FROZEN-INSTALL) fresh so immutable pins do not rot into stale,
# vulnerable dependencies. See docs/specs/supply-chain-security.md.
# templates/gh-actions/dependabot.yml is the copy shipped to downstream products.
# Dependabot — keep SHA-pinned actions (SWR-SEC-ACTION-PINNING) and frozen
# lockfiles (SWR-SEC-FROZEN-INSTALL) fresh WITHOUT PR spam on `main` and WITHOUT
# burning the build matrix on one-line bumps. See
# docs/specs/supply-chain-security.md [SWR-SEC-DEPENDABOT-STAGING].
# templates/gh-actions/dependabot.yml is the copy shipped to downstream products;
# Basilisk is the reference implementation.
#
# Every bump ends up on the long-lived `dependabot-upgrades` STAGING branch,
# swept there by .github/workflows/dependabot-automerge.yml (latest bump
# clobbers previous). Review happens ONCE, at the `dependabot-upgrades -> main`
# consolidation PR:
# * VERSION updates -> `target-branch: dependabot-upgrades`; opened against
# the staging branch, so ci.yml (pull_request: [main]) never fires on them.
# * SECURITY updates -> GitHub IGNORES `target-branch` and always opens them
# against `main`; the auto-merge workflow also fires on `main` and folds the
# bump into the SAME staging branch, then retires the PR. The `*-security`
# groups (`applies-to: security-updates`) collapse CVE bumps one-per-ecosystem.
#
# REQUIREMENT: the `dependabot-upgrades` branch must exist, cut from `main` AFTER
# this file + dependabot-automerge.yml are on `main` (the staging branch must
# carry the auto-merge workflow; for pull_request it is read from the base ref).
version: 2
updates:
# Keep every `uses:` SHA pin current. Dependabot bumps the SHA while preserving
# the `# vX.Y.Z` comment. (GitHub Actions have no security-advisory feed, so a
# single version group — no `*-security` split — is correct here.)
- package-ecosystem: github-actions
directory: "/"
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
github-actions:
patterns: ["*"]
open-pull-requests-limit: 5

# Rust dependency graph (Cargo.lock).
- package-ecosystem: cargo
directory: "/"
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
cargo:
applies-to: version-updates
patterns: ["*"]
cargo-security:
applies-to: security-updates
patterns: ["*"]
open-pull-requests-limit: 5

# npm lives in three independent trees: the root pnpm workspace ("/"
# covers every clients/ts/* package), the standalone Eleventy site
# (website/, own pnpm-lock.yaml), and the AJV validator
# (tools/validate-manifest/, own package-lock.json). "/" alone never
# touches the latter two, so their pins silently rot — list all three.
# npm lives in three independent trees: the root pnpm workspace ("/" covers
# every clients/ts/* package), the standalone Eleventy site (website/, own
# pnpm-lock.yaml), and the AJV validator (tools/validate-manifest/, own
# package-lock.json). "/" alone never touches the latter two, so their pins
# silently rot — list all three.
- package-ecosystem: npm
directories:
- "/"
- "/website"
- "/tools/validate-manifest"
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
npm:
applies-to: version-updates
patterns: ["*"]
npm-security:
applies-to: security-updates
patterns: ["*"]
open-pull-requests-limit: 5
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ concurrency:
jobs:
ci:
name: CI
# Dependabot PRs are swept into `dependabot-upgrades` and never merge into
# main directly, so the heavy matrix would only burn minutes on a bump we
# discard. CI runs once, on the consolidation PR. (SWR-SEC-DEPENDABOT-STAGING)
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down Expand Up @@ -78,6 +82,9 @@ jobs:

ts-tests:
name: TypeScript tests (${{ matrix.os }})
# Skip Dependabot bumps — swept into `dependabot-upgrades`, tested once on
# the consolidation PR. (SWR-SEC-DEPENDABOT-STAGING)
if: github.actor != 'dependabot[bot]'
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
Expand Down
102 changes: 102 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Dependabot auto-merge — sweeps EVERY Dependabot PR into the long-lived
# `dependabot-upgrades` staging branch, no questions asked.
# See docs/specs/supply-chain-security.md [SWR-SEC-DEPENDABOT-STAGING].
# Basilisk is the reference implementation; templates/gh-actions/dependabot-automerge.yml
# is the copy shipped to downstream products.
#
# Two kinds of PR land here:
# * VERSION updates -> Dependabot opens them against `dependabot-upgrades`
# directly (.github/dependabot.yml `target-branch`).
# * SECURITY updates -> GitHub IGNORES `target-branch` for these and ALWAYS
# opens them against the default branch (`main`). So this workflow also
# triggers on `main` and folds the security bump into the SAME staging
# branch — nothing is ever left sitting on `main` waiting for a human.
#
# Merge strategy: the incoming branch ALWAYS clobbers what is already staged
# (`git merge -X theirs`). Successive bumps of the same lock-file never conflict-
# stall: the latest bump wins, every time. Nothing reaches `main` this way — the
# full build/test (ci.yml) gates the single `dependabot-upgrades -> main`
# consolidation PR, which is where review and the expensive matrix actually run.
# ci.yml deliberately SKIPS Dependabot PRs (it would only burn the matrix on a
# bump we immediately sweep away).
#
# Lives at the repo root so it is present on `dependabot-upgrades` (cut from
# main): for `pull_request` the workflow is read from the PR's base branch, so
# BOTH `main` and the staging branch must carry this file.
#
# SECURITY INVARIANT — the trigger MUST stay `pull_request`, NEVER
# `pull_request_target`. Under `pull_request` a fork PR runs with a READ-ONLY
# token and no secrets, so merging fork-controlled content here is inert.
# `pull_request_target` would hand the write token + secrets to that content and
# turn this merge bot into a real RCE/exfiltration sink — exactly the
# actions/untrusted-checkout finding, made true. Do not change it.
# (SWR-SEC-CODE-SCANNING)
name: Dependabot auto-merge

on:
pull_request:
branches:
- dependabot-upgrades
- main

permissions:
contents: write
pull-requests: write

jobs:
sweep:
name: Clobber-merge into dependabot-upgrades
# Two independent gates, both required: the (unforgeable) Dependabot actor
# AND a `dependabot/*` source branch. actor-AND-source keeps the trust
# assumption explicit and resilient if the trigger set is ever widened.
if: github.actor == 'dependabot[bot]' && startsWith(github.head_ref, 'dependabot/')
# Deliberately the standard runner, NOT a larger/paid one: a trivial merge
# bot must not consume CI minutes meant for the real build matrix.
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check out the staging branch
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: dependabot-upgrades
fetch-depth: 0

- name: Clobber-merge the bump and retire the PR
env:
PR_URL: ${{ github.event.pull_request.html_url }}
PR_HEAD: ${{ github.event.pull_request.head.ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Defense-in-depth: disable git hooks for every git op in this job, so
# merging attacker-influenced tree content can never execute a hook by
# construction. No hook is reachable today (hooks live in .git, not the
# tree), but this holds regardless of any future change.
git config --global core.hooksPath /dev/null
# Pull the bump branch into a stable local ref we can re-merge.
git fetch origin "+refs/heads/${PR_HEAD}:refs/remotes/origin/${PR_HEAD}"
# Re-merge onto the LIVE staging tip and retry: concurrent Dependabot
# PRs race to push here, so each run rebases on whatever already landed
# and the incoming branch always wins conflicts (-X theirs).
for attempt in 1 2 3 4 5; do
git fetch origin "+refs/heads/dependabot-upgrades:refs/remotes/origin/dependabot-upgrades"
git reset --hard "origin/dependabot-upgrades"
git merge -X theirs --no-edit "origin/${PR_HEAD}" \
-m "build(deps): clobber-merge ${PR_HEAD} into dependabot-upgrades"
if git push origin "HEAD:dependabot-upgrades"; then
break
fi
if [ "$attempt" = "5" ]; then
echo "::error::could not push to dependabot-upgrades after 5 attempts"
exit 1
fi
sleep 5
done
# Retire the PR + its branch: the bump is already staged, so the PR
# (whether it targeted main or the staging branch) has served its
# purpose. `|| true` — GitHub may have auto-closed it on the push.
gh pr close "$PR_URL" --delete-branch \
--comment "Swept into \`dependabot-upgrades\` (latest bump clobbers previous)." \
|| git push origin --delete "$PR_HEAD" || true
6 changes: 6 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
name: Deploy Pages

on:
push:
branches: [main]
paths:
- 'docs/specs/**'
- 'website/**'
- '.github/workflows/deploy-pages.yml'
workflow_dispatch:

permissions:
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"crates/shipwright-manifest",
"crates/shipwright-zed",
"tools/shipwright-version-stamp",
"tools/shipwright-release-scope",
]

[workspace.package]
Expand All @@ -25,6 +26,7 @@ readme = "README.md"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
glob = "0.3"

# Workspace lints — deny-by-default. See REPO-STANDARDS-SPEC [LINT-RUST].
# Inherit in every member crate with `[lints] workspace = true`.
Expand Down
3 changes: 3 additions & 0 deletions coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
},
"tools/shipwright-version-stamp": {
"threshold": 100
},
"tools/shipwright-release-scope": {
"threshold": 100
}
}
}
3 changes: 3 additions & 0 deletions crates/shipwright-zed/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
//! language server starts. This crate re-exports the pure host resolver and
//! adds small helpers for representing deferred LSP checks and validating the
//! `serverInfo` payload returned from LSP `initialize`.
//!
//! Implements the Zed deployment contract SWR-IDE-ZED: digest-verified
//! `github-release` resolution and LSP-`initialize` version checks.

#![forbid(unsafe_code)]

Expand Down
43 changes: 38 additions & 5 deletions docs/agents/shipwright-compliance/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ Spec source (cite these URLs, never local paths — this skill runs on repos tha
| Library architecture | `.../docs/specs/library-architecture.md` (`SWR-ARCH-*`) |
| Source projects & survey | `.../docs/specs/source-projects.md` (`SWR-SRC-*`) |
| Release pipeline plan | `.../docs/plans/release-pipeline.md` (`SWR-REL-*`) |
| **Release change detection** (CI cost gate) | `.../docs/specs/release-change-detection.md` (`SWR-REL-CHANGES-*`) |

Reusable workflow templates (fetch the raw file and adapt — do not hand-roll from memory):
`https://raw.githubusercontent.com/Nimblesite/Shipwright/main/templates/gh-actions/<file>`
where `<file>` ∈ `release-binary-multiplatform.yml`, `publish-brew-tap.yml`,
`publish-scoop-bucket.yml`, `publish-vsix-per-platform.yml`.
`publish-scoop-bucket.yml`, `publish-vsix-per-platform.yml`,
`release-change-detection.yml`.

## Workflow

Expand All @@ -45,7 +47,8 @@ Shipwright Compliance Progress:
- [ ] Phase A: Emit the audit report (conformity + security holes)
- [ ] Phase B: Implement — manifest, version stamping, libraries, release.yml (see reference/implement-release.md)
- [ ] Phase B: Wire GitHub Release + Homebrew + Scoop + per-platform VSIX + registries as applicable
- [ ] Phase B: Close the supply-chain holes — pinned actions, least-priv tokens, frozen installs, provenance, SBOM, signed checksums, OIDC publishing, per-channel verification
- [ ] Phase B: Add the release change-detection cost gate (release-scope.json + scope job + per-surface `if:`)
- [ ] Phase B: Close the supply-chain holes — pinned actions, Dependabot staging branch + auto-merge sweep, least-priv tokens, frozen installs, provenance, SBOM, signed checksums, OIDC publishing, per-channel verification
- [ ] Phase C: Verify locally (manifest validates, `--version` matches, CI gate green)
- [ ] Emit the change summary
```
Expand Down Expand Up @@ -110,6 +113,17 @@ order. The implementation playbook is the authoritative step list; the high-leve
staged under `bin/<vsceTarget>/`, verified package contents, Marketplace publish on tag. `[SWR-VSIX-*]`.
8. **Acceptance gates in CI** — validate the manifest, run `--version` / `--version --json`, and verify
the produced package against the manifest. `[SWR-GATE-*]`.
9. **Release change-detection cost gate** — add `.github/release-scope.json` (a per-repo ruleset mapping
path globs to `binary`/`vsix`/`jetbrains`/`website`/`ignore`, validated by `release-scope.schema.json`),
copy in `release-change-detection.yml`, and add a `scope:` job as the FIRST job of `release.yml`. Then
gate every other job on its outputs: the native binary matrix `if: needs.scope.outputs.build_matrix == 'true'`,
the standalone binary release + Homebrew + Scoop on `full`, VSIX jobs on `vsix`, JetBrains on `jetbrains`,
website on `website`. CONTRACT (`[SWR-REL-CHANGES-CONTRACT]`): a tag stamps ONE version and host
activation-verify is `onMismatch:error`, so a published VSIX/JetBrains plugin MUST bundle binaries built
at the new version — therefore a vsix/jetbrains-only change STILL runs the binary matrix (do NOT reuse a
prior release's binary). Only a website-only change skips the matrix. Pin `shipwright_rev` to a full
Shipwright commit SHA. `[SWR-REL-CHANGES-*]`. Tailor the ruleset to the repo's actual layout — `binary`
MUST cover every compiled source + lockfile (fail-safe favours over-releasing, never under-releasing).

Reuse the canonical templates (fetch the raw URLs above and adapt to this repo's binary/extension
names) instead of writing workflows from scratch. Make the **smallest diff that achieves conformity**.
Expand All @@ -130,7 +144,10 @@ Prove the changes locally before declaring done:

- **No PATH / package-manager runtime fallback.** A normal startup that reads or mutates PATH, shells
out to `which`/`where`, or launches a Homebrew/Scoop/npm-global/cargo/dotnet-tool binary is FAIL.
Bundled or explicit-override sources only. `[SWR-IDE-RESOLUTION]`, `[SWR-SEC-CONTROLS]`.
Bundled or explicit-override sources only. On Zed this also bars `worktree.which` and a `~/.cargo/bin`
default: a silent PATH/preinstalled fallback is FAIL; the Zed default is the verified `github-release`
download, and a download branch left unreachable behind a never-true guard (dead-download) is also
FAIL. `[SWR-IDE-RESOLUTION]`, `[SWR-IDE-ZED]`, `[SWR-SEC-CONTROLS]`.
- **One VSIX per target.** Native-binary extensions MUST package `npx vsce package --target <vsceTarget>`.
A single all-platform native VSIX is FAIL. `[SWR-VSIX-PACKAGE]`.
- **Verify package contents.** The release MUST inspect each produced artifact: exact `bin/<target>/`
Expand All @@ -144,9 +161,25 @@ Prove the changes locally before declaring done:
install`, never crash on missing .NET, never hand-roll a download. `[SWR-IDE-DOTNET-RUNTIME]`.
- **Supply-chain integrity is non-negotiable.** Mutable action tags (`@v4`/`@stable`), a missing or
over-broad top-level `permissions:`, `npm install` (vs `npm ci`) in a release/VSIX job, a release
with no provenance/SBOM/cosign-signed `SHA256SUMS`, a downloader (Neovim/Zed/host) that executes a
fetched binary without verifying its checksum AND signature, or a long-lived registry/marketplace
with no provenance/SBOM/cosign-signed `SHA256SUMS`, a host/Neovim/brew/scoop downloader that execs a
fetched binary without verifying its digest AND cosign signature, a Zed extension that execs a
downloaded binary with no in-extension SHA-256 digest check (its cosign signature is a release-boundary
check — the WASM sandbox cannot run cosign; `[SWR-IDE-ZED]`), or a long-lived registry/marketplace
token outside a protected environment are all FAIL. `[SWR-SEC-*]`, `[SWR-SIGN-*]`.
- **Dependabot must stage, not spam.** A repo whose Dependabot bumps land one-by-one on `main` — no
`target-branch: dependabot-upgrades`, no `dependabot-automerge.yml` sweep workflow, or no
`dependabot-upgrades` branch — is FAIL: pins rot or PRs pile up and every bump burns the full matrix.
Each ecosystem targets the staging branch with paired `version-updates`/`security-updates` groups; the
sweep workflow triggers on `pull_request` (NEVER `pull_request_target` — that is an RCE sink) for both
`dependabot-upgrades` and `main` (security bumps ignore `target-branch`), clobber-merges (`-X theirs`),
and retires the PR; `ci.yml`/`codeql.yml` skip Dependabot PRs. Review happens once, at the
`dependabot-upgrades → main` consolidation PR. Basilisk is the reference. `[SWR-SEC-DEPENDABOT-STAGING]`.
- **No blind full-matrix releases.** A tag-triggered `release.yml` that rebuilds the macOS/Windows binary
matrix on every tag — even a website-only change — is a cost FAIL. Gate the costly jobs on
`release-change-detection.yml` outputs. The cascade is mandatory: a binary change releases EVERYTHING; an
unclassified change releases EVERYTHING (fail-safe); a vsix/jetbrains change still builds the binary matrix
(single-version contract). Only a website-only change may skip the matrix. Never gate so aggressively that a
real binary change ships without a rebuild. `[SWR-REL-CHANGES-CASCADE]`, `[SWR-REL-CHANGES-FAILSAFE]`, `[SWR-REL-CHANGES-CONTRACT]`.
- **License must be honest.** A package's declared SPDX license (default single `MIT`) MUST match a
LICENSE file that actually ships; declaring `MIT OR Apache-2.0` (or any expression) without the
second license's text present is FAIL. `[SWR-REL-LICENSE]`.
Expand Down
Loading