Skip to content

Commit 48a05c0

Browse files
azuclaude
andauthored
CI: add npm Trusted Publisher workflows and security configuration (#5)
* chore: add release workflows for GitHub Actions - create-release-pr.yml: manually triggered workflow for version bump and release PR creation - release.yml: automated npm publish with provenance on release PR merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * CI: add npm Trusted Publisher workflows and security configuration - create-release-pr.yml: Creates release PRs with version bump and release notes - release.yml: Publishes to npm using Trusted Publisher (OIDC) when PR is merged - CODEOWNERS: Protects critical workflow files from unauthorized changes - No npm tokens required - uses GitHub OIDC for authentication * chore: remove npm installation step from release workflow --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 84ef8c4 commit 48a05c0

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed

.github/CODEOWNERS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# GitHub CODEOWNERS
2+
# This file defines code ownership for automatic review requests
3+
4+
# Critical workflow files must be reviewed by repository owner
5+
/.github/workflows/release.yml @textlint-rule
6+
/.github/workflows/create-release-pr.yml @textlint-rule
7+
8+
# CODEOWNERS file itself requires review to prevent bypassing protections
9+
/.github/CODEOWNERS @textlint-rule
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Create Release PR
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Version type'
8+
required: true
9+
type: choice
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
15+
jobs:
16+
create-release-pr:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
pull-requests: write
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
24+
with:
25+
persist-credentials: false
26+
27+
- name: Configure Git
28+
run: |
29+
git config user.name "github-actions[bot]"
30+
git config user.email "github-actions[bot]@users.noreply.github.com"
31+
32+
- name: Install pnpm
33+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
34+
35+
- name: Setup Node.js
36+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
37+
with:
38+
node-version: 'lts/*'
39+
40+
# No need to install dependencies - npm version works without them
41+
- name: Version bump
42+
id: version
43+
run: |
44+
npm version "$VERSION_TYPE" --no-git-tag-version
45+
VERSION=$(jq -r '.version' package.json)
46+
echo "version=$VERSION" >> $GITHUB_OUTPUT
47+
env:
48+
VERSION_TYPE: ${{ github.event.inputs.version }}
49+
50+
- name: Get release notes
51+
id: release-notes
52+
run: |
53+
# Get the default branch
54+
DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch')
55+
56+
# Get the latest release tag using GitHub API
57+
# Use the exit code to determine if a release exists
58+
if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then
59+
echo "Previous release found: $LAST_TAG"
60+
else
61+
LAST_TAG=""
62+
echo "No previous releases found - this will be the first release"
63+
fi
64+
65+
# Generate release notes - only include previous_tag_name if we have a valid previous tag
66+
echo "Generating release notes for tag: v$VERSION"
67+
if [ -n "$LAST_TAG" ]; then
68+
echo "Using previous tag: $LAST_TAG"
69+
RELEASE_NOTES=$(gh api \
70+
--method POST \
71+
-H "Accept: application/vnd.github+json" \
72+
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
73+
-f "tag_name=v$VERSION" \
74+
-f "target_commitish=$DEFAULT_BRANCH" \
75+
-f "previous_tag_name=$LAST_TAG" \
76+
--jq '.body')
77+
else
78+
echo "Generating notes from all commits"
79+
RELEASE_NOTES=$(gh api \
80+
--method POST \
81+
-H "Accept: application/vnd.github+json" \
82+
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
83+
-f "tag_name=v$VERSION" \
84+
-f "target_commitish=$DEFAULT_BRANCH" \
85+
--jq '.body')
86+
fi
87+
88+
# Set release notes as environment variable
89+
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
90+
echo "$RELEASE_NOTES" >> $GITHUB_ENV
91+
echo "EOF" >> $GITHUB_ENV
92+
env:
93+
GH_TOKEN: ${{ github.token }}
94+
VERSION: ${{ steps.version.outputs.version }}
95+
GITHUB_REPOSITORY: ${{ github.repository }}
96+
97+
- name: Commit version bump
98+
run: |
99+
git add .
100+
git commit -m "chore: release v$VERSION"
101+
env:
102+
VERSION: ${{ steps.version.outputs.version }}
103+
104+
- name: Push branch
105+
run: |
106+
git push -u "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:release/v$VERSION"
107+
env:
108+
VERSION: ${{ steps.version.outputs.version }}
109+
GH_TOKEN: ${{ github.token }}
110+
GITHUB_REPOSITORY: ${{ github.repository }}
111+
112+
- name: Create Pull Request
113+
run: |
114+
echo "$RELEASE_NOTES" | gh pr create \
115+
--title "Release v$VERSION" \
116+
--body-file - \
117+
--base "$BASE_BRANCH" \
118+
--head "release/v$VERSION" \
119+
--label "Type: Release" \
120+
--assignee "$ACTOR" \
121+
--draft
122+
env:
123+
GH_TOKEN: ${{ github.token }}
124+
VERSION: ${{ steps.version.outputs.version }}
125+
BASE_BRANCH: ${{ github.ref_name }}
126+
ACTOR: ${{ github.actor }}

.github/workflows/release.yml

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
name: Release
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
- main
8+
types:
9+
- closed
10+
11+
jobs:
12+
release:
13+
if: |
14+
github.event.pull_request.merged == true &&
15+
contains(github.event.pull_request.labels.*.name, 'Type: Release')
16+
runs-on: ubuntu-latest
17+
environment:
18+
name: npm
19+
permissions:
20+
contents: write
21+
id-token: write # OIDC
22+
outputs:
23+
released: ${{ steps.tag-check.outputs.exists == 'false' }}
24+
version: ${{ steps.package.outputs.version }}
25+
package-name: ${{ steps.package.outputs.name }}
26+
release-url: ${{ steps.create-release.outputs.url }}
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
30+
with:
31+
persist-credentials: false
32+
33+
- name: Get package info
34+
id: package
35+
run: |
36+
VERSION=$(jq -r '.version' package.json)
37+
PACKAGE_NAME=$(jq -r '.name' package.json)
38+
echo "version=$VERSION" >> $GITHUB_OUTPUT
39+
echo "name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
40+
41+
- name: Check if tag exists
42+
id: tag-check
43+
run: |
44+
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
45+
echo "exists=true" >> $GITHUB_OUTPUT
46+
else
47+
echo "exists=false" >> $GITHUB_OUTPUT
48+
fi
49+
env:
50+
VERSION: ${{ steps.package.outputs.version }}
51+
52+
- name: Install pnpm
53+
if: steps.tag-check.outputs.exists == 'false'
54+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
55+
56+
- name: Setup Node.js
57+
if: steps.tag-check.outputs.exists == 'false'
58+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
59+
with:
60+
node-version: 'lts/*'
61+
registry-url: 'https://registry.npmjs.org'
62+
63+
- name: Install dependencies
64+
if: steps.tag-check.outputs.exists == 'false'
65+
run: pnpm install --frozen-lockfile
66+
67+
- name: Build package
68+
if: steps.tag-check.outputs.exists == 'false'
69+
run: pnpm run build
70+
71+
- name: Publish to npm with provenance
72+
if: steps.tag-check.outputs.exists == 'false'
73+
run: pnpm publish --access public
74+
75+
- name: Create GitHub Release with tag
76+
id: create-release
77+
if: steps.tag-check.outputs.exists == 'false'
78+
run: |
79+
RELEASE_URL=$(gh release create "v$VERSION" \
80+
--title "v$VERSION" \
81+
--target "$SHA" \
82+
--notes "$PR_BODY")
83+
echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
84+
env:
85+
GH_TOKEN: ${{ github.token }}
86+
VERSION: ${{ steps.package.outputs.version }}
87+
SHA: ${{ github.sha }}
88+
PR_BODY: ${{ github.event.pull_request.body }}
89+
90+
comment:
91+
needs: release
92+
if: |
93+
always() &&
94+
github.event_name == 'pull_request' &&
95+
needs.release.outputs.released == 'true'
96+
runs-on: ubuntu-latest
97+
permissions:
98+
pull-requests: write
99+
steps:
100+
- name: Comment on PR - Success
101+
if: needs.release.result == 'success'
102+
run: |
103+
gh pr comment "$PR_NUMBER" \
104+
--repo "$REPOSITORY" \
105+
--body "✅ **Release v$VERSION completed successfully!**
106+
107+
- 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
108+
- 🏷️ GitHub Release: $RELEASE_URL
109+
- 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
110+
env:
111+
GH_TOKEN: ${{ github.token }}
112+
PR_NUMBER: ${{ github.event.pull_request.number }}
113+
VERSION: ${{ needs.release.outputs.version }}
114+
PACKAGE_NAME: ${{ needs.release.outputs.package-name }}
115+
RELEASE_URL: ${{ needs.release.outputs.release-url }}
116+
SERVER_URL: ${{ github.server_url }}
117+
REPOSITORY: ${{ github.repository }}
118+
RUN_ID: ${{ github.run_id }}
119+
120+
- name: Comment on PR - Failure
121+
if: needs.release.result == 'failure'
122+
run: |
123+
gh pr comment "$PR_NUMBER" \
124+
--repo "$REPOSITORY" \
125+
--body "❌ **Release v$VERSION failed**
126+
127+
Please check the workflow logs for details.
128+
🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
129+
env:
130+
GH_TOKEN: ${{ github.token }}
131+
PR_NUMBER: ${{ github.event.pull_request.number }}
132+
VERSION: ${{ needs.release.outputs.version }}
133+
SERVER_URL: ${{ github.server_url }}
134+
REPOSITORY: ${{ github.repository }}
135+
RUN_ID: ${{ github.run_id }}

0 commit comments

Comments
 (0)