From 8d8649de7899049b587fde5a534c3268a6282a60 Mon Sep 17 00:00:00 2001 From: prode Date: Sun, 31 May 2026 13:28:49 -0300 Subject: [PATCH 1/3] ci(release): authenticate npm publish via NPM_TOKEN + add provenance OIDC trusted publishing can't perform the first publish (the package must already exist before a trusted publisher can be configured). Authenticate the npm-publish job with the NPM_TOKEN secret (an Automation token that bypasses 2FA) so the bootstrap release publishes from CI, and pass --provenance to keep a signed build attestation (id-token: write is retained for that). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5dc0072..4e2e611 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,8 +79,9 @@ jobs: retention-days: 7 # Publish to npm on tagged releases only (npm versions are immutable, so the - # rolling "latest" main builds are not published here). Uses Trusted - # Publishing (OIDC): no token in the repo, provenance generated automatically. + # rolling "latest" main builds are not published here). Authenticates with the + # NPM_TOKEN secret (an Automation token, which bypasses 2FA); id-token: write + # is kept so --provenance can attach a signed build attestation. npm-publish: name: publish npm packages needs: build @@ -88,7 +89,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write # required for OIDC trusted publishing + id-token: write # required for npm --provenance attestation steps: - uses: actions/checkout@v4 @@ -110,12 +111,17 @@ jobs: run: node npm/scripts/build-packages.mjs "${{ github.ref_name }}" artifacts - name: Publish (platform packages first, then the root) + env: + # Automation token (bypasses 2FA). setup-node wrote an .npmrc that + # reads this. --provenance still attaches a signed attestation via the + # job's id-token: write permission. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | set -euo pipefail for d in npm/dist/csdd-*/; do - npm publish "$d" --access public + npm publish "$d" --access public --provenance done - npm publish npm/dist/csdd --access public + npm publish npm/dist/csdd --access public --provenance release: name: publish release From eef966496682fa773ed1b31b605cf8c7eeaa2677 Mon Sep 17 00:00:00 2001 From: prode Date: Sun, 31 May 2026 14:02:41 -0300 Subject: [PATCH 2/3] feat(makefile): add Makefile for build, test, and npm release tasks --- Makefile | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e68cfe4 --- /dev/null +++ b/Makefile @@ -0,0 +1,101 @@ +# csdd — build, test, and npm release tasks. +# +# Day-to-day: +# make build # local binary -> ./csdd +# make check # gofmt + vet + race tests (the CI gate) +# +# Release (recommended — CI builds + publishes to npm on the pushed tag): +# make release VERSION=v0.2.0 +# +# Manual npm publish (bootstrap / fallback, when CI can't do it): +# make dist VERSION=v0.2.0 # cross-compile all 5 targets into dist/ +# make npm-build VERSION=v0.2.0 # assemble npm/dist/ from those artifacts +# make npm-dry-run # validate all 6 packages without publishing +# make npm-publish [OTP=123456] # publish the 6 packages (skips already-published) +# +# Auth for a manual publish: either an Automation token +# npm config set //registry.npmjs.org/:_authToken +# or pass a fresh 2FA code: make npm-publish OTP=123456 + +SHELL := bash +MODULE := github.com/protonspy/csdd +VERSION ?= dev +LDFLAGS := -s -w -X $(MODULE)/cmd.version=$(VERSION) +DIST ?= dist +ARTIFACTS ?= $(DIST) + +# GOOS/GOARCH targets shipped to npm — must match npm/scripts/build-packages.mjs. +PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 + +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show this help + @grep -hE '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) \ + | awk 'BEGIN{FS=":.*## "}{printf " \033[36m%-14s\033[0m %s\n",$$1,$$2}' + +.PHONY: build +build: ## Build the binary locally (-> ./csdd); set VERSION to stamp it + go build -trimpath -ldflags '$(LDFLAGS)' -o csdd . + +.PHONY: test +test: ## Run tests (race + coverage) + go test -race -coverprofile=coverage.out ./... + +.PHONY: fmt +fmt: ## Fail if any file is not gofmt-clean + @unformatted=$$(gofmt -l .); \ + if [ -n "$$unformatted" ]; then echo "not gofmt-clean:"; echo "$$unformatted"; exit 1; fi; \ + echo "gofmt clean" + +.PHONY: vet +vet: ## go vet ./... + go vet ./... + +.PHONY: check +check: fmt vet test ## Run the full CI gate (gofmt + vet + race tests) + +.PHONY: dist +dist: ## Cross-compile every npm target into $(DIST)/ (set VERSION=vX.Y.Z) + @rm -rf '$(DIST)' && mkdir -p '$(DIST)' + @set -euo pipefail; for p in $(PLATFORMS); do \ + goos=$${p%/*}; goarch=$${p#*/}; bin=csdd; [ "$$goos" = windows ] && bin=csdd.exe; \ + echo ">> $$goos/$$goarch"; \ + GOOS=$$goos GOARCH=$$goarch CGO_ENABLED=0 go build -trimpath -ldflags '$(LDFLAGS)' -o '$(DIST)'/$$bin .; \ + name=csdd_$(VERSION)_$${goos}_$${goarch}; \ + if [ "$$goos" = windows ]; then (cd '$(DIST)' && zip -q $$name.zip $$bin); \ + else (cd '$(DIST)' && tar -czf $$name.tar.gz $$bin); fi; \ + rm -f '$(DIST)'/$$bin; \ + done; \ + echo "artifacts in $(DIST)/" + +.PHONY: npm-build +npm-build: ## Assemble npm/dist/ from artifacts (set VERSION=vX.Y.Z; ARTIFACTS=dir) + node npm/scripts/build-packages.mjs '$(VERSION)' '$(ARTIFACTS)' + +.PHONY: npm-dry-run +npm-dry-run: ## Dry-run publish every assembled package + @set -euo pipefail; for d in npm/dist/csdd-*/ npm/dist/csdd/; do \ + echo "== $$d"; npm publish "$$d" --access public --dry-run; done + +.PHONY: npm-publish +npm-publish: ## Publish the 6 packages, platforms first (skips already-published; OTP=123456 if 2FA) + @set -euo pipefail; \ + otp=; if [ -n "$(OTP)" ]; then otp="--otp=$(OTP)"; fi; \ + for d in npm/dist/csdd-*/ npm/dist/csdd/; do \ + name=$$(cd "$$d" && node -p "require('./package.json').name"); \ + ver=$$(cd "$$d" && node -p "require('./package.json').version"); \ + if npm view "$$name@$$ver" version >/dev/null 2>&1; then \ + echo "skip $$name@$$ver (already published)"; continue; fi; \ + echo ">> publishing $$name@$$ver"; npm publish "$$d" --access public $$otp; \ + done + +.PHONY: release +release: ## Tag VERSION and push -> CI builds + publishes (set VERSION=vX.Y.Z) + @case '$(VERSION)' in v*.*.*) : ;; *) echo "VERSION must look like v1.2.3 (got '$(VERSION)')"; exit 1 ;; esac + git tag -a '$(VERSION)' -m 'csdd $(VERSION)' + git push origin '$(VERSION)' + +.PHONY: clean +clean: ## Remove build artifacts (dist/, npm/dist/, ./csdd, coverage) + rm -rf '$(DIST)' npm/dist csdd csdd.exe coverage.out From 4021ecfab2a1c52cab9cc4f3b66ec76c0beb0053 Mon Sep 17 00:00:00 2001 From: prode Date: Sun, 31 May 2026 14:35:17 -0300 Subject: [PATCH 3/3] fix(docs): update README for npx usage and add .gitignore entry for /dist --- .gitignore | 1 + README.md | 13 +++++++++++-- npm/csdd/README.md | 17 +++++++++++------ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 5934822..42ac2b4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ go.work.sum # Anchored with a leading slash so they do NOT match the npm/csdd/ package dir. /csdd /csdd.md +/dist \ No newline at end of file diff --git a/README.md b/README.md index d165103..c80bc35 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,26 @@ csdd # no args → interactive TUI ## Install -**npm** (cross-platform — installs the right prebuilt binary for your OS/arch): +**npx** (cross-platform — fetches the right prebuilt binary for your OS/arch, no install): + +```bash +npx @protonspy/csdd --help # run instantly, no install +npx @protonspy/csdd # interactive TUI +``` + +Prefer the short `csdd` command on your `PATH`? Install it globally: ```bash npm install -g @protonspy/csdd # then: csdd -npx @protonspy/csdd --help # or run without installing ``` **Prebuilt binaries:** grab the archive for your platform from the [releases page](https://github.com/protonspy/csdd/releases) and put `csdd` on your `PATH`. **From source:** `go install github.com/protonspy/csdd@latest`. +> The examples below call `csdd` directly. Running via npx? Prefix them with +> `npx @protonspy/csdd`, or alias it: `alias csdd='npx @protonspy/csdd'`. + --- ## The 5 resources csdd governs diff --git a/npm/csdd/README.md b/npm/csdd/README.md index 70c31fc..85850df 100644 --- a/npm/csdd/README.md +++ b/npm/csdd/README.md @@ -6,16 +6,19 @@ workflow for [Claude Code](https://claude.com/claude-code) into a contract that is validated mechanically — for humans *and* AI agents. -## Install +## Run + +No install needed — `npx` fetches the right prebuilt binary for your platform: ```bash -npm install -g @protonspy/csdd +npx @protonspy/csdd --help +npx @protonspy/csdd # interactive TUI ``` -Or run it without installing: +Prefer the short `csdd` command on your `PATH`? Install it globally: ```bash -npx @protonspy/csdd --help +npm install -g @protonspy/csdd # then: csdd ``` This package ships a thin launcher; the native binary for your platform is @@ -25,10 +28,12 @@ download at install time). Prebuilt for linux, macOS, and Windows on x64/arm64. ## Usage ```bash -csdd # interactive TUI -csdd spec generate photo-albums --artifact requirements # headless / CI +npx @protonspy/csdd # interactive TUI +npx @protonspy/csdd spec generate photo-albums --artifact requirements # headless / CI ``` +> Installed globally? Drop the `npx @protonspy/` prefix and just call `csdd`. + See the [full documentation](https://github.com/protonspy/csdd#readme). ## License