Skip to content

Commit c8b0285

Browse files
authored
CI: add npm Trusted Publisher workflows and security configuration (#12)
- 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 74b5b49 commit c8b0285

File tree

3 files changed

+255
-0
lines changed

3 files changed

+255
-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-ja
6+
/.github/workflows/create-release-pr.yml @textlint-ja
7+
8+
# CODEOWNERS file itself requires review to prevent bypassing protections
9+
/.github/CODEOWNERS @textlint-ja
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
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+
33+
- name: Setup Node.js
34+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
35+
with:
36+
node-version: 'lts/*'
37+
38+
# No need to install dependencies - npm version works without them
39+
- name: Version bump
40+
id: version
41+
run: |
42+
npm version "$VERSION_TYPE" --no-git-tag-version
43+
VERSION=$(jq -r '.version' package.json)
44+
echo "version=$VERSION" >> $GITHUB_OUTPUT
45+
env:
46+
VERSION_TYPE: ${{ github.event.inputs.version }}
47+
48+
- name: Get release notes
49+
id: release-notes
50+
run: |
51+
# Get the default branch
52+
DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.default_branch')
53+
54+
# Get the latest release tag using GitHub API
55+
# Use the exit code to determine if a release exists
56+
if LAST_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' 2>/dev/null); then
57+
echo "Previous release found: $LAST_TAG"
58+
else
59+
LAST_TAG=""
60+
echo "No previous releases found - this will be the first release"
61+
fi
62+
63+
# Generate release notes - only include previous_tag_name if we have a valid previous tag
64+
echo "Generating release notes for tag: v$VERSION"
65+
if [ -n "$LAST_TAG" ]; then
66+
echo "Using previous tag: $LAST_TAG"
67+
RELEASE_NOTES=$(gh api \
68+
--method POST \
69+
-H "Accept: application/vnd.github+json" \
70+
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
71+
-f "tag_name=v$VERSION" \
72+
-f "target_commitish=$DEFAULT_BRANCH" \
73+
-f "previous_tag_name=$LAST_TAG" \
74+
--jq '.body')
75+
else
76+
echo "Generating notes from all commits"
77+
RELEASE_NOTES=$(gh api \
78+
--method POST \
79+
-H "Accept: application/vnd.github+json" \
80+
"/repos/$GITHUB_REPOSITORY/releases/generate-notes" \
81+
-f "tag_name=v$VERSION" \
82+
-f "target_commitish=$DEFAULT_BRANCH" \
83+
--jq '.body')
84+
fi
85+
86+
# Set release notes as environment variable
87+
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
88+
echo "$RELEASE_NOTES" >> $GITHUB_ENV
89+
echo "EOF" >> $GITHUB_ENV
90+
env:
91+
GH_TOKEN: ${{ github.token }}
92+
VERSION: ${{ steps.version.outputs.version }}
93+
GITHUB_REPOSITORY: ${{ github.repository }}
94+
95+
- name: Create Pull Request
96+
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
97+
with:
98+
branch: release/v${{ steps.version.outputs.version }}
99+
delete-branch: true
100+
title: "Release v${{ steps.version.outputs.version }}"
101+
body: |
102+
${{ env.RELEASE_NOTES }}
103+
commit-message: "chore: release v${{ steps.version.outputs.version }}"
104+
labels: |
105+
Type: Release
106+
assignees: ${{ github.actor }}
107+
draft: true

.github/workflows/release.yml

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
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+
53+
- name: Setup Node.js
54+
if: steps.tag-check.outputs.exists == 'false'
55+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
56+
with:
57+
node-version: 'lts/*'
58+
registry-url: 'https://registry.npmjs.org'
59+
60+
- name: Install latest npm
61+
if: steps.tag-check.outputs.exists == 'false'
62+
run: |
63+
echo "Current npm version: $(npm -v)"
64+
npm install -g npm@latest
65+
echo "Updated npm version: $(npm -v)"
66+
67+
- name: Install dependencies
68+
if: steps.tag-check.outputs.exists == 'false'
69+
run: yarn install --frozen-lockfile
70+
71+
- name: Build package
72+
if: steps.tag-check.outputs.exists == 'false'
73+
run: yarn run build
74+
75+
- name: Publish to npm with provenance
76+
if: steps.tag-check.outputs.exists == 'false'
77+
run: npm publish --access public
78+
79+
- name: Create GitHub Release with tag
80+
id: create-release
81+
if: steps.tag-check.outputs.exists == 'false'
82+
run: |
83+
RELEASE_URL=$(gh release create "v$VERSION" \
84+
--title "v$VERSION" \
85+
--target "$SHA" \
86+
--notes "$PR_BODY")
87+
echo "url=$RELEASE_URL" >> $GITHUB_OUTPUT
88+
env:
89+
GH_TOKEN: ${{ github.token }}
90+
VERSION: ${{ steps.package.outputs.version }}
91+
SHA: ${{ github.sha }}
92+
PR_BODY: ${{ github.event.pull_request.body }}
93+
94+
comment:
95+
needs: release
96+
if: |
97+
always() &&
98+
github.event_name == 'pull_request' &&
99+
needs.release.outputs.released == 'true'
100+
runs-on: ubuntu-latest
101+
permissions:
102+
pull-requests: write
103+
steps:
104+
- name: Comment on PR - Success
105+
if: needs.release.result == 'success'
106+
run: |
107+
gh pr comment "$PR_NUMBER" \
108+
--repo "$REPOSITORY" \
109+
--body "✅ **Release v$VERSION completed successfully!**
110+
111+
- 📦 npm package: https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION
112+
- 🏷️ GitHub Release: $RELEASE_URL
113+
- 🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
114+
env:
115+
GH_TOKEN: ${{ github.token }}
116+
PR_NUMBER: ${{ github.event.pull_request.number }}
117+
VERSION: ${{ needs.release.outputs.version }}
118+
PACKAGE_NAME: ${{ needs.release.outputs.package-name }}
119+
RELEASE_URL: ${{ needs.release.outputs.release-url }}
120+
SERVER_URL: ${{ github.server_url }}
121+
REPOSITORY: ${{ github.repository }}
122+
RUN_ID: ${{ github.run_id }}
123+
124+
- name: Comment on PR - Failure
125+
if: needs.release.result == 'failure'
126+
run: |
127+
gh pr comment "$PR_NUMBER" \
128+
--repo "$REPOSITORY" \
129+
--body "❌ **Release v$VERSION failed**
130+
131+
Please check the workflow logs for details.
132+
🔗 Workflow run: $SERVER_URL/$REPOSITORY/actions/runs/$RUN_ID"
133+
env:
134+
GH_TOKEN: ${{ github.token }}
135+
PR_NUMBER: ${{ github.event.pull_request.number }}
136+
VERSION: ${{ needs.release.outputs.version }}
137+
SERVER_URL: ${{ github.server_url }}
138+
REPOSITORY: ${{ github.repository }}
139+
RUN_ID: ${{ github.run_id }}

0 commit comments

Comments
 (0)