Skip to content
Open
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
37 changes: 34 additions & 3 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,40 @@ jobs:
fi

echo "Publishing to npm with tag: $NPM_TAG"
# Using OIDC trusted publisher - no token needed
# --provenance adds verified build attestation
npm publish --tag $NPM_TAG --provenance --access public
# Using OIDC trusted publisher — no NPM_TOKEN required.
# --provenance attaches a verified build attestation to the tarball.
#
# If this step 404s, npm has not registered this repo+workflow as a
# trusted publisher. Fix path (one-time):
# https://www.npmjs.com/package/glin-profanity/access
# → Publishing access → Add trusted publisher → GitHub Actions:
# Organization: glincker
# Repository: glin-profanity
# Workflow: auto-release.yml
# Environment: npm-publish
# See docs/RELEASING.md for the full walkthrough.
if ! npm publish --tag "$NPM_TAG" --provenance --access public; then
cat >&2 <<'EOF'

=============================================================
npm publish failed. This is almost always because the npm
trusted-publisher registration on this package does not match
the workflow that just ran.
Comment on lines +303 to +309

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trusted-publisher guidance banner is printed for any npm publish failure, but the surrounding comments/PR description frame it as 404-specific. Consider capturing npm publish output and only printing this banner when the output indicates a 404/E404; otherwise, emit a generic failure message so 403/network/other errors don’t get misdiagnosed.

Copilot uses AI. Check for mistakes.

Fix:
1. Visit https://www.npmjs.com/package/glin-profanity/access
2. Add a GitHub Actions trusted publisher with exactly:
Organization: glincker
Repository: glin-profanity
Workflow: auto-release.yml
Environment: npm-publish
3. Re-run this workflow (gh run rerun <id> --failed).

See docs/RELEASING.md for the full one-time setup.
=============================================================
EOF
exit 1
fi
Comment on lines +303 to +324

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Banner fires for every publish failure, not just 404.

The fix instructions in the emitted banner assume a trusted-publisher mismatch (404), but this branch runs on any non-zero exit — including 403 (2FA/ownership), network errors, registry outages, or the "version already exists" race (if the pre-check at Line 275 missed it). Operators hitting a 403 will be pointed at the wrong remediation. The "almost always" hedge helps but the explicit 4-step fix is still misleading.

Consider either (a) scoping the banner to 404 by capturing npm's exit or stderr, or (b) restructuring the banner so the 404 fix is clearly one branch alongside the 403/other guidance already documented in docs/RELEASING.md.

♻️ Sketch of a more discriminating variant
-          if ! npm publish --tag "$NPM_TAG" --provenance --access public; then
-            cat >&2 <<'EOF'
-
-=============================================================
-npm publish failed. This is almost always because the npm
-trusted-publisher registration on this package does not match
-the workflow that just ran.
-
-Fix:
-  1. Visit https://www.npmjs.com/package/glin-profanity/access
-  2. Add a GitHub Actions trusted publisher with exactly:
-       Organization: glincker
-       Repository:   glin-profanity
-       Workflow:     auto-release.yml
-       Environment:  npm-publish
-  3. Re-run this workflow (gh run rerun <id> --failed).
-
-See docs/RELEASING.md for the full one-time setup.
-=============================================================
-EOF
-            exit 1
-          fi
+          PUBLISH_LOG=$(mktemp)
+          if ! npm publish --tag "$NPM_TAG" --provenance --access public 2> >(tee "$PUBLISH_LOG" >&2); then
+            cat >&2 <<'EOF'
+
+=============================================================
+npm publish failed. See docs/RELEASING.md#diagnosing-publish-failures.
+EOF
+            if grep -qE '404|Not Found' "$PUBLISH_LOG"; then
+              cat >&2 <<'EOF'
+
+Likely cause: trusted-publisher mismatch. Fix:
+  1. https://www.npmjs.com/package/glin-profanity/access
+  2. Add a GitHub Actions trusted publisher with exactly:
+       Organization: glincker
+       Repository:   glin-profanity
+       Workflow:     auto-release.yml
+       Environment:  npm-publish
+  3. gh run rerun <id> --failed
+EOF
+            elif grep -qE '403|Two-factor|OTP' "$PUBLISH_LOG"; then
+              echo "Likely cause: 2FA/ownership. See RELEASING.md §'403'." >&2
+            fi
+            echo "=============================================================" >&2
+            exit 1
+          fi

Note: the actionlint/yamllint errors on lines 306–307 are false positives — the heredoc contents live inside a run: | bash string, so YAML doesn't parse them.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 306-306: could not parse as YAML: could not find expected ':'

(syntax-check)

🪛 YAMLlint (1.38.0)

[error] 307-307: syntax error: could not find expected ':'

(syntax)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/auto-release.yml around lines 303 - 324, The npm publish
failure banner currently triggers on any non-zero exit from the npm publish
command; change the logic around the npm publish invocation (the if ! npm
publish --tag "$NPM_TAG" --provenance --access public; then ... fi block) to
capture both the exit status and stderr/stdout (e.g., run npm publish ... 2>&1
into a variable or temp file and save $?), then only emit the "trusted-publisher
/ 404" banner when the captured output or status matches a 404/ENOENT/not found
pattern indicating a trusted-publisher mismatch; otherwise emit a generic
failure message pointing to docs/RELEASING.md and include the captured npm
output for troubleshooting (this keeps the existing heredoc for the 404 case but
replaces the unconditional banner with a discriminating branch based on the npm
publish exit code/output).


# Publish to PyPI
publish-pypi:
Expand Down
144 changes: 144 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Releasing

glin-profanity publishes to **npm** (JS/TS), **PyPI** (Python), and cuts a **GitHub Release** with a signed tag. All three fire automatically from a single merge to `release` via `.github/workflows/auto-release.yml`.

Comment on lines +3 to +4

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc states releases fire from a merge/push to the release branch, but the workflow is configured to run on pushes to both main and release (on.push.branches: [ main, release ]). Either update the doc to mention main as well, or adjust the workflow triggers if release is intended to be the sole release branch.

Copilot uses AI. Check for mistakes.
This document covers the one-time setup needed for the automated pipeline to work end-to-end, plus fallback paths when something breaks.

---

## TL;DR

- Merge a conventional-commit PR to `release`, or push a commit whose subject starts with `release: minor`, `release: patch`, etc.
- The workflow detects the bump, builds, publishes to npm + PyPI, and creates a GitHub Release.
- **No tokens in GitHub Secrets.** Trust is established via OIDC.

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc says "No tokens in GitHub Secrets" but the release workflow still uses ${{ secrets.PYPI_TOKEN }} for the PyPI publish job. This is misleading—consider narrowing this to "No NPM_TOKEN in GitHub Secrets" (OIDC for npm), or explicitly call out that PyPI currently still uses a secret until/if it’s migrated to trusted publishing.

Suggested change
- **No tokens in GitHub Secrets.** Trust is established via OIDC.
- **No `NPM_TOKEN` in GitHub Secrets.** npm publishing uses OIDC; PyPI may still use `PYPI_TOKEN` until trusted publishing is configured there.

Copilot uses AI. Check for mistakes.

---

## Why OIDC (and why you don't want NPM_TOKEN)

- No long-lived credentials in the repo → nothing to leak, nothing to rotate.
- Every published tarball carries a **sigstore provenance attestation** — consumers can verify the exact commit + workflow that built it.
- Works with strict package-level 2FA (`Require 2FA: Authorization and writes`).

OIDC issues a short-lived token that names the `repo × workflow × environment` that requested it. npm checks that triple against the package's trusted-publisher list and either accepts or rejects. There's no secret passing through the workflow.

---

## One-time setup (npm side)

This is the step that the automated workflow **cannot** do for you. Do it once per package.

1. Sign in to npm as a package owner.
2. Open: `https://www.npmjs.com/package/glin-profanity/access`
3. Scroll to **Publishing access** → **Add trusted publisher** → **GitHub Actions**
4. Fill in **exactly** these values (all four fields are part of the identity check):

| Field | Value |
|---|---|
| Organization or user | `glincker` |
| Repository name | `glin-profanity` |
| Workflow filename | `auto-release.yml` |
| Environment name | `npm-publish` |

5. Save.

Repeat for `glin-profanity-mcp` and `openclaw-profanity` if you want those on OIDC too (their workflows are `release-mcp.yml` and `release-openclaw.yml`, same org/repo).

### Why the "Environment" field matters

The workflow declares `environment: npm-publish` on the `publish-npm` job. GitHub Actions only mints an OIDC token that claims an environment when a job actually declares one — and npm validates the claim. If you leave the Environment field blank on npm's side, the claim with `environment=npm-publish` will **not** match an unspecified expectation and npm returns 404. Must match character-for-character.

---

## One-time setup (PyPI side)

PyPI supports the same trusted-publisher pattern. If `hatch publish` ever starts 404-ing, the equivalent setup is:

1. `https://pypi.org/manage/project/glin-profanity/settings/publishing/`
2. Add a GitHub publisher: org `glincker`, repo `glin-profanity`, workflow `auto-release.yml`, environment `pypi-publish` (if the workflow adopts one).

PyPI has been working so this is informational only.

---

## Triggering a release

### Automatic on merge

The workflow triggers on every push to `release`. `scripts/sync-versions.js detect` parses the latest commit subject. Patterns that trigger a bump:

| Commit subject starts with | Result |
|---|---|
| `release: patch` | patch bump |
| `release: minor` | minor bump |
| `release: major` | major bump |
| `release: beta-minor` | minor bump on beta channel |
| `release: alpha-patch` | patch bump on alpha channel |
Comment on lines +72 to +76

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trigger table lists patterns like release: patch / release: minor without the trailing space that the current detection code expects (it matches release: patch etc.). Consider updating the table entries to release: patch <...> / release: minor <...> (or loosen the detection regex) to avoid false expectations when someone copies these literals.

Suggested change
| `release: patch` | patch bump |
| `release: minor` | minor bump |
| `release: major` | major bump |
| `release: beta-minor` | minor bump on beta channel |
| `release: alpha-patch` | patch bump on alpha channel |
| `release: patch <...>` | patch bump |
| `release: minor <...>` | minor bump |
| `release: major <...>` | major bump |
| `release: beta-minor <...>` | minor bump on beta channel |
| `release: alpha-patch <...>` | patch bump on alpha channel |

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +76

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc says commit subjects starting with release: patch, release: minor, etc. trigger bumps, but the current detection regex in scripts/sync-versions.js requires a trailing space after the type (e.g. ^release: patch ). A subject exactly release: patch would not match—update the doc examples to include an additional message after the type, or adjust the regex to accept end-of-line.

Suggested change
The workflow triggers on every push to `release`. `scripts/sync-versions.js detect` parses the latest commit subject. Patterns that trigger a bump:
| Commit subject starts with | Result |
|---|---|
| `release: patch` | patch bump |
| `release: minor` | minor bump |
| `release: major` | major bump |
| `release: beta-minor` | minor bump on beta channel |
| `release: alpha-patch` | patch bump on alpha channel |
The workflow triggers on every push to `release`. `scripts/sync-versions.js detect` parses the latest commit subject. Example commit subjects that trigger a bump:
| Commit subject | Result |
|---|---|
| `release: patch cut release` | patch bump |
| `release: minor cut release` | minor bump |
| `release: major cut release` | major bump |
| `release: beta-minor cut release` | minor bump on beta channel |
| `release: alpha-patch cut release` | patch bump on alpha channel |

Copilot uses AI. Check for mistakes.

Patterns that explicitly **skip** a release (so merges don't cascade into junk releases):

- `chore(deps): ...` — Dependabot
- `chore: bump version ...` — the workflow's own version-bump commit
- Anything containing `[skip ci]`
- Merge commits from Dependabot PRs

### Manual

```bash
gh workflow run auto-release.yml -f release_type=minor -f channel=stable
```

---

## Diagnosing publish failures

### npm publish returns 404

**Always** means the trusted-publisher config on npm doesn't match the running job's OIDC claim. Compare npm's config (see above) to the currently-running workflow's `repo`, `workflow filename`, and `environment`. Any mismatch → 404.

You will see this in the job log before the 404:

```
npm notice publish Signed provenance statement with source and build information from GitHub Actions
npm notice Provenance statement published to transparency log: https://search.sigstore.dev/...
```

If sigstore writes succeed but the PUT to npm returns 404, the problem is definitively on npm's side (not yours). The workflow has added explicit diagnostic output for this case.

### npm publish returns 403 with "Two-factor authentication is required"

The token is being sent as an automation token but the package's `Require 2FA` setting is the strictest mode. OIDC bypasses this entirely. If you see this error, the workflow is not using OIDC (likely `NPM_TOKEN` was accidentally added to the environment). Check `env:` on the publish step.

### npm publish returns 403 without OTP context

User/token does not own the package. Verify with `npm owner ls glin-profanity`.

### "Version already exists, skipping publish"

An earlier run in the cascade already published this version. Bump the version in source to something unused and merge again.

---

## Fallback: manual publish

If CI is blocked and you need to ship immediately:

```bash
cd packages/js
# ensure version in package.json is the version you want to publish
npm run build
# verify dist/scanners/ exists
ls dist/scanners/
# publish (prompts for OTP)
npm publish --access public
```

This does **not** attach provenance attestation — use only for emergency / one-off fixes.

---

## Adding a new workspace package to the pipeline

1. Create its own `release-<name>.yml` in `.github/workflows/`, modeled on `release-mcp.yml` (trigger on `push` to `release` with path filter `packages/<name>/**`).
2. Add a `publish-npm` job with `environment: npm-publish-<name>` (distinct per package for least-privilege auditability).
3. Register the matching trusted publisher on npm.
Loading