Skip to content

Commit c1d95d5

Browse files
committed
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
1 parent 14d5703 commit c1d95d5

File tree

3 files changed

+228
-72
lines changed

3 files changed

+228
-72
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: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,126 @@
11
name: Create Release PR
2+
23
on:
34
workflow_dispatch:
45
inputs:
5-
semver:
6-
description: "Select version bump type"
6+
version:
7+
description: 'Version type'
78
required: true
8-
default: "patch"
99
type: choice
1010
options:
1111
- patch
1212
- minor
1313
- major
14-
permissions:
15-
contents: write
16-
pull-requests: write
14+
1715
jobs:
1816
create-release-pr:
1917
runs-on: ubuntu-latest
18+
permissions:
19+
contents: write
20+
pull-requests: write
2021
steps:
2122
- name: Checkout
22-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
23+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
2324
with:
24-
fetch-depth: 0
2525
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+
2632
- name: Install pnpm
2733
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
34+
2835
- name: Setup Node.js
29-
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
36+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
3037
with:
31-
cache: "pnpm"
32-
node-version: lts/*
33-
- name: Git config
34-
run: |
35-
git config user.name "github-actions[bot]"
36-
git config user.email "github-actions[bot]@users.noreply.github.com"
37-
- name: Bump version
38-
run: npm version ${{ inputs.semver }} --no-git-tag-version
39-
- name: Get new version
38+
node-version: 'lts/*'
39+
40+
# No need to install dependencies - npm version works without them
41+
- name: Version bump
4042
id: version
41-
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
42-
- name: Generate release notes
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
4351
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
4492
env:
45-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46-
TAG_NAME: v${{ steps.version.outputs.version }}
93+
GH_TOKEN: ${{ github.token }}
94+
VERSION: ${{ steps.version.outputs.version }}
95+
GITHUB_REPOSITORY: ${{ github.repository }}
96+
97+
- name: Commit version bump
4798
run: |
48-
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
49-
echo "notes<<$EOF" >> "$GITHUB_OUTPUT"
50-
gh api repos/${{ github.repository }}/releases/generate-notes \
51-
-f tag_name="$TAG_NAME" \
52-
--jq '.body' >> "$GITHUB_OUTPUT"
53-
echo "$EOF" >> "$GITHUB_OUTPUT"
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+
54112
- name: Create Pull Request
55-
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
56-
with:
57-
token: ${{ secrets.GITHUB_TOKEN }}
58-
commit-message: "chore: bump version to ${{ steps.version.outputs.version }}"
59-
title: "v${{ steps.version.outputs.version }}"
60-
body: |
61-
## Release v${{ steps.version.outputs.version }}
62-
63-
${{ steps.release-notes.outputs.notes }}
64-
branch: "release/${{ steps.version.outputs.version }}"
65-
labels: "Type: Release"
66-
draft: true
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: 119 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,142 @@
11
name: Release
2+
23
on:
34
pull_request:
4-
types: [closed]
5+
branches:
6+
- master
7+
- main
8+
types:
9+
- closed
10+
511
jobs:
612
release:
13+
if: |
14+
github.event.pull_request.merged == true &&
15+
contains(github.event.pull_request.labels.*.name, 'Type: Release')
716
runs-on: ubuntu-latest
8-
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'Type: Release')
17+
environment:
18+
name: npm
919
permissions:
1020
contents: write
11-
id-token: write
12-
pull-requests: 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 }}
1327
steps:
1428
- name: Checkout
15-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
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+
1652
- name: Install pnpm
53+
if: steps.tag-check.outputs.exists == 'false'
1754
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
55+
1856
- name: Setup Node.js
19-
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
57+
if: steps.tag-check.outputs.exists == 'false'
58+
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
2059
with:
21-
cache: "pnpm"
22-
node-version: lts/*
23-
registry-url: "https://registry.npmjs.org"
60+
node-version: 'lts/*'
61+
registry-url: 'https://registry.npmjs.org'
62+
63+
- name: Install latest npm
64+
if: steps.tag-check.outputs.exists == 'false'
65+
run: |
66+
echo "Current npm version: $(npm -v)"
67+
npm install -g npm@latest
68+
echo "Updated npm version: $(npm -v)"
69+
2470
- name: Install dependencies
25-
run: pnpm install
26-
- name: Build
71+
if: steps.tag-check.outputs.exists == 'false'
72+
run: pnpm install --frozen-lockfile
73+
74+
- name: Build package
75+
if: steps.tag-check.outputs.exists == 'false'
2776
run: pnpm run build
28-
- name: Publish to npm
29-
run: npm publish --provenance --access public
30-
env:
31-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32-
- name: Get version
33-
id: version
34-
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"
35-
- name: Create GitHub Release
77+
78+
- name: Publish to npm with provenance
79+
if: steps.tag-check.outputs.exists == 'false'
80+
run: pnpm publish --access public
81+
82+
- name: Create GitHub Release with tag
83+
id: create-release
84+
if: steps.tag-check.outputs.exists == 'false'
85+
run: |
86+
RELEASE_URL=$(gh release create "v$VERSION" \
87+
--title "v$VERSION" \
88+
--target "$SHA" \
89+
--notes "$PR_BODY")
90+
echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
3691
env:
37-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38-
TAG_NAME: v${{ steps.version.outputs.version }}
92+
GH_TOKEN: ${{ github.token }}
93+
VERSION: ${{ steps.package.outputs.version }}
94+
SHA: ${{ github.sha }}
95+
PR_BODY: ${{ github.event.pull_request.body }}
96+
97+
comment:
98+
needs: release
99+
if: |
100+
always() &&
101+
github.event_name == 'pull_request' &&
102+
needs.release.outputs.released == 'true'
103+
runs-on: ubuntu-latest
104+
permissions:
105+
pull-requests: write
106+
steps:
107+
- name: Comment on PR - Success
108+
if: needs.release.result == 'success'
39109
run: |
40-
gh release create "$TAG_NAME" --generate-notes
41-
- name: Comment on PR (success)
42-
if: success()
110+
gh pr comment "$PR_NUMBER" \
111+
--repo "$REPOSITORY" \
112+
--body "✅ **Release v$VERSION completed successfully!**
113+
114+
- 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
115+
- 🏷️ GitHub Release: $RELEASE_URL
116+
- 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
43117
env:
44-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118+
GH_TOKEN: ${{ github.token }}
45119
PR_NUMBER: ${{ github.event.pull_request.number }}
46-
VERSION: ${{ steps.version.outputs.version }}
120+
VERSION: ${{ needs.release.outputs.version }}
121+
PACKAGE_NAME: ${{ needs.release.outputs.package-name }}
122+
RELEASE_URL: ${{ needs.release.outputs.release-url }}
123+
SERVER_URL: ${{ github.server_url }}
124+
REPOSITORY: ${{ github.repository }}
125+
RUN_ID: ${{ github.run_id }}
126+
127+
- name: Comment on PR - Failure
128+
if: needs.release.result == 'failure'
47129
run: |
48-
gh pr comment "$PR_NUMBER" --body "Released as v${VERSION}"
49-
- name: Comment on PR (failure)
50-
if: failure()
130+
gh pr comment "$PR_NUMBER" \
131+
--repo "$REPOSITORY" \
132+
--body "❌ **Release v$VERSION failed**
133+
134+
Please check the workflow logs for details.
135+
🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
51136
env:
52-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137+
GH_TOKEN: ${{ github.token }}
53138
PR_NUMBER: ${{ github.event.pull_request.number }}
54-
run: |
55-
gh pr comment "$PR_NUMBER" --body "Release failed. Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details."
139+
VERSION: ${{ needs.release.outputs.version }}
140+
SERVER_URL: ${{ github.server_url }}
141+
REPOSITORY: ${{ github.repository }}
142+
RUN_ID: ${{ github.run_id }}

0 commit comments

Comments
 (0)