ci: harden release and review boundaries (#306)

Co-authored-by: Conor Bronsdon <conorbronsdon@users.noreply.github.com>
Co-authored-by: Sharad <sharadvc@users.noreply.github.com>
This commit is contained in:
Conor Bronsdon
2026-09-13 22:53:19 -07:00
committed by GitHub
parent fc979c6489
commit 989af1997f
8 changed files with 109 additions and 35 deletions
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: monthly
groups:
github-actions:
patterns:
- "*"
+2 -18
View File
@@ -20,25 +20,9 @@ on:
- 'bin/**'
- 'action.yml'
- '.pre-commit-hooks.yaml'
# This job is a required-check candidate, so it must report on every PR.
# Workflow-level path filters would leave unrelated PRs pending forever.
pull_request:
paths:
- "detector/**"
- "scripts/**"
- "examples/**"
- "evals/**"
- "package.json"
- ".github/workflows/detector-test.yml"
- "README.md"
- "SKILL.md"
- "SKILL.full.md"
- "references/**"
- "dist/**"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "PROOF.md"
- 'bin/**'
- 'action.yml'
- '.pre-commit-hooks.yaml'
permissions:
contents: read
+2
View File
@@ -35,6 +35,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# ssot-check resolves `../<repo>/...` copies against real directories on
# disk, so the surfaces have to be cloned as siblings of this checkout.
+50 -17
View File
@@ -5,6 +5,8 @@ on:
branches: [main]
paths:
- "CHANGELOG.md"
# Recovery dispatches must target main. The release environment enforces
# that boundary again outside this editable workflow file.
workflow_dispatch:
permissions:
@@ -14,36 +16,68 @@ permissions:
# `gh release view` before `gh release create`, and publish-npm reads
# `npm view` before `npm publish`. Overlapping runs can both read "not there
# yet" and then collide. Two runs carrying the same version turn one of them
# red; two runs carrying different versions can finish out of order and leave
# npm's `latest` pointing at the older one. Serializing puts the second run's
# checks after the first run's writes, so its `exists` guards see the truth and
# skip — the guards and this block do the job together, neither one alone.
# red; two runs carrying different versions can otherwise race their shared
# tags and registry state. Serializing puts each run's checks after the prior
# run's writes, so its `exists` guards see the truth. `queue: max` matters too:
# without it, a newer trigger replaces an already-pending release even when
# `cancel-in-progress` is false.
#
# The group omits `github.ref` on purpose: the registry and the release list are
# repo-global, so a workflow_dispatch has to queue behind a main push too.
# Never cancel — a cancelled run can die between its check and its publish, or
# leave a GitHub release standing with nothing published against it.
# repo-global. Never cancel — a cancelled run can die between its check and its
# publish, or leave a GitHub release standing with nothing published against it.
concurrency:
group: release
queue: max
cancel-in-progress: false
jobs:
release:
# Validate the exact release commit without a write token or OIDC access.
# Nothing public exists until this job has proved the version, tests, and
# package payload all succeed.
preflight:
if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
version: ${{ steps.changelog.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Parse latest version from CHANGELOG.md
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24
package-manager-cache: false
- name: Verify release versions
id: changelog
run: node scripts/verify-release-versions.js --github-output "$GITHUB_OUTPUT"
- name: Test the release commit
run: npm test
- name: Verify the npm package can be assembled
run: npm pack --dry-run --json
release:
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check if the release tag already exists
id: tagcheck
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="v${{ steps.changelog.outputs.version }}"
TAG="v${{ needs.preflight.outputs.version }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Release $TAG already exists — nothing to do."
@@ -54,7 +88,7 @@ jobs:
- name: Extract this version's changelog section
if: steps.tagcheck.outputs.exists == 'false'
run: |
VERSION="${{ steps.changelog.outputs.version }}"
VERSION="${{ needs.preflight.outputs.version }}"
awk -v heading="## [$VERSION]" '
$0 ~ /^## \[/ {
if (found) exit
@@ -73,7 +107,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="${{ steps.changelog.outputs.version }}"
VERSION="${{ needs.preflight.outputs.version }}"
gh release create "v$VERSION" \
--target "$GITHUB_SHA" \
--title "v$VERSION" \
@@ -87,14 +121,17 @@ jobs:
# the CHANGELOG heading the release job used — the same version guard as the
# release job must block a publish, not ride into the registry.
publish-npm:
needs: release
needs: [preflight, release]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
permissions:
contents: read
id-token: write # npm Trusted Publishing (OIDC) + --provenance
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
@@ -165,10 +202,6 @@ jobs:
fi
echo "HEAD is the commit tagged $TAG ($TAG_SHA) — safe to publish."
- name: Test before publish
if: steps.npmcheck.outputs.exists == 'false'
run: npm test
- name: Publish with provenance
if: steps.npmcheck.outputs.exists == 'false'
run: npm publish --provenance --access public
+3
View File
@@ -177,3 +177,6 @@ writing rule needs a minor version bump. Exempt changes need no version bump;
leave published release entries intact.
After changing either canonical file, run `bash scripts/sync-plugin-skill.sh && bash scripts/sync-cursor-rules.sh`. This regenerates both bundles, `SKILL.full.md`, and the portable paste/Cursor artifacts; CI checks parity. Do not edit generated copies.
Maintainers should follow [the release recovery procedure](docs/releasing.md)
instead of moving a tag or reusing a published version after a failed run.
+18
View File
@@ -0,0 +1,18 @@
# Security policy
## Supported versions
Security fixes target the current `main` branch and the latest
[release](https://github.com/conorbronsdon/avoid-ai-writing/releases). Older
releases are not routinely patched.
## Report a vulnerability
Do not file a public issue for an undisclosed security problem. Use this
repository's
[private vulnerability reporting](https://github.com/conorbronsdon/avoid-ai-writing/security/advisories/new)
form instead.
Include the affected version or commit, reproduction steps, realistic impact,
and any suggested mitigation. Reports about code execution, path traversal,
credential exposure, or release and package integrity are especially useful.
+3
View File
@@ -5,3 +5,6 @@ For bugs, routing problems, false positives, packaging issues, or ChatGPT and Co
https://github.com/conorbronsdon/avoid-ai-writing/issues
Include the plugin version, the host surface you used, the expected behavior, and a minimal reproduction. Do not post confidential drafts or personal information in a public issue.
For security vulnerabilities, do not use the public issue tracker. Follow
[SECURITY.md](SECURITY.md) and use GitHub private vulnerability reporting.
+21
View File
@@ -0,0 +1,21 @@
# Release recovery
The release workflow runs only after a version change reaches `main`. Its
read-only preflight checks the version agreement, test suite, and npm package
payload before either publishing job receives write authority.
GitHub serializes release runs. Keep `queue: max`: `cancel-in-progress: false`
protects the running job but does not retain more than one pending run by
itself.
## Recover a failed npm publication
If the GitHub release exists but npm publication fails, rerun the failed jobs
from the original workflow run. A rerun keeps the original commit SHA, sees the
existing GitHub release, verifies that its tag still names that SHA, and retries
only the missing npm version. Do not merge another version bump as a recovery
mechanism, move the release tag, or reuse a version that reached the registry.
If preflight fails, no release or package exists. Fix the failure through a new
pull request, keep the intended version unchanged, then manually dispatch the
release workflow from `main`. The release environment rejects another ref.