Skip to content

Commit fdfae28

Browse files
authored
Merge pull request #1 from cloudinary-devs/add-release-workflow
chore: add release workflow
2 parents 5a361d4 + ee6b661 commit fdfae28

6 files changed

Lines changed: 892 additions & 7 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
dry_run:
7+
description: 'Dry run only (no git push, no tags, no npm publish)'
8+
required: false
9+
default: true
10+
type: boolean
11+
12+
permissions:
13+
contents: write
14+
id-token: write
15+
issues: write
16+
pull-requests: write
17+
18+
# Allow GitHub Actions to bypass branch protection
19+
# This is required for semantic-release to push version updates
20+
21+
jobs:
22+
release:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Verify admin permissions
26+
run: |
27+
# Use the repository's permission endpoint which works for both personal and org repos
28+
RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
29+
-H "Accept: application/vnd.github.v3+json" \
30+
"https://api.github.com/repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission")
31+
32+
# Extract permission using jq if available, otherwise use grep
33+
if command -v jq &> /dev/null; then
34+
PERMISSION=$(echo "$RESPONSE" | jq -r '.permission // empty')
35+
else
36+
PERMISSION=$(echo "$RESPONSE" | grep -o '"permission":"[^"]*"' | head -1 | cut -d'"' -f4)
37+
fi
38+
39+
if [ -z "$PERMISSION" ]; then
40+
echo "Warning: Could not determine permission level. Response: $RESPONSE"
41+
echo "Note: workflow_dispatch requires write access, proceeding..."
42+
exit 0
43+
fi
44+
45+
if [ "$PERMISSION" != "admin" ]; then
46+
echo "Error: Only repository admins can trigger releases. Current permission: $PERMISSION"
47+
exit 1
48+
fi
49+
50+
echo "✓ Verified admin permission for ${{ github.actor }}"
51+
52+
- uses: actions/checkout@v4
53+
with:
54+
ref: main
55+
fetch-depth: 0
56+
fetch-tags: true
57+
token: ${{ secrets.RELEASE_TOKEN }}
58+
59+
- name: Setup git branch
60+
run: |
61+
git fetch --all --tags --force
62+
git fetch origin '+refs/notes/*:refs/notes/*' || true
63+
git checkout -B main
64+
git branch --set-upstream-to=origin/main main
65+
66+
- name: Ensure master branch exists (for semantic-release validation)
67+
run: |
68+
# semantic-release requires at least one release branch that exists; repo uses main, we declare "master"
69+
if ! git ls-remote --heads origin master 2>/dev/null | grep -q .; then
70+
git checkout -b master
71+
git push origin master
72+
git checkout main
73+
fi
74+
75+
- uses: actions/setup-node@v4
76+
with:
77+
node-version: '20'
78+
registry-url: 'https://registry.npmjs.org'
79+
always-auth: true
80+
81+
- run: npm ci
82+
83+
- run: npm test --if-present
84+
85+
- name: Configure git
86+
run: |
87+
git config user.name "github-actions[bot]"
88+
git config user.email "github-actions[bot]@users.noreply.github.com"
89+
90+
- name: Add semantic-release note (dry run only)
91+
if: github.event.inputs.dry_run == 'true'
92+
run: |
93+
set -e
94+
if ! git rev-parse --verify v1.0.0-beta.1 >/dev/null 2>&1; then exit 0; fi
95+
git notes --ref semantic-release-v1.0.0-beta.1 add -f -m '{"channels":["beta"]}' v1.0.0-beta.1
96+
echo "Added semantic-release note to v1.0.0-beta.1 (local only for dry run)."
97+
98+
- name: Dry run mode notice
99+
if: github.event.inputs.dry_run == 'true'
100+
run: |
101+
echo "=============================================="
102+
echo " DRY RUN MODE - No commits, tags, or publish"
103+
echo "=============================================="
104+
echo "Semantic-release will show what WOULD happen."
105+
echo "To perform a real release, run again and uncheck 'Dry run only'."
106+
echo ""
107+
108+
- name: Ensure v1.0.0-beta.1 exists locally (dry run only)
109+
if: github.event.inputs.dry_run == 'true'
110+
run: |
111+
if ! git rev-parse --verify "v1.0.0-beta.1" >/dev/null 2>&1; then
112+
# So semantic-release sees a "previous release" and suggests 1.0.0-beta.2 for new commits
113+
PARENT=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
114+
git tag -a "v1.0.0-beta.1" "$PARENT" -m "chore: initial beta release (dry-run placeholder)"
115+
echo "Created local tag v1.0.0-beta.1 at $PARENT so semantic-release can compute next version (1.0.0-beta.2)."
116+
else
117+
echo "Tag v1.0.0-beta.1 already exists."
118+
fi
119+
120+
- name: Get version before semantic-release
121+
id: version-before
122+
run: |
123+
VERSION_BEFORE=$(node -p "require('./package.json').version")
124+
echo "version=$VERSION_BEFORE" >> $GITHUB_OUTPUT
125+
echo "Current version: $VERSION_BEFORE"
126+
127+
- name: Release with semantic-release
128+
id: release
129+
env:
130+
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
131+
run: |
132+
set -e
133+
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
134+
echo "Running semantic-release in DRY RUN mode..."
135+
npx semantic-release --dry-run 2>&1 | tee semantic-release.log || true
136+
grep -oE "The next release version is [^[:space:]]+" semantic-release.log 2>/dev/null | sed 's/The next release version is //' > next-version.txt || echo "" > next-version.txt
137+
else
138+
npx semantic-release
139+
fi
140+
141+
# npm publish uses OIDC (id-token: write + --provenance). No NPM_TOKEN needed.
142+
# Require on npmjs.com: Package → Package settings → Trusted publishers →
143+
# Add: GitHub Actions, org cloudinary-devs, repo create-cloudinary-next, workflow release.yml
144+
# npm trusted publishing (OIDC) requires npm CLI 11.5.1+; Node 20 ships with npm 9.x.
145+
# Force OIDC-only: override NPM_CONFIG_USERCONFIG so npm ignores setup-node's .npmrc (which may reference a stale token).
146+
- name: Publish to npm using trusted publishing
147+
if: github.event.inputs.dry_run != 'true'
148+
env:
149+
NODE_AUTH_TOKEN: ''
150+
NPM_TOKEN: ''
151+
NPM_CONFIG_USERCONFIG: '${{ runner.temp }}/.npmrc-oidc'
152+
run: |
153+
echo "=== Publishing to npm with trusted publishing (OIDC) ==="
154+
unset NODE_AUTH_TOKEN NPM_TOKEN 2>/dev/null || true
155+
# Config that has only registry — no _authToken — so npm uses OIDC
156+
echo "registry=https://registry.npmjs.org/" > "$NPM_CONFIG_USERCONFIG"
157+
# OIDC for publish requires npm 11.5.1+ (Node 20 ships with npm 9.x)
158+
npm install -g npm@latest
159+
npm --version
160+
161+
# Get versions
162+
VERSION_BEFORE="${{ steps.version-before.outputs.version }}"
163+
VERSION_AFTER=$(node -p "require('./package.json').version")
164+
165+
echo "Version before: $VERSION_BEFORE"
166+
echo "Version after: $VERSION_AFTER"
167+
168+
# Only publish if semantic-release created a new version
169+
if [ "$VERSION_BEFORE" != "$VERSION_AFTER" ]; then
170+
echo "✓ New version detected: $VERSION_AFTER"
171+
echo "Publishing to npm..."
172+
173+
# Publish using npm publish which supports OIDC/trusted publishing
174+
# --tag latest so installers get the most recent version (npm i create-cloudinary-next / npx create-cloudinary-next)
175+
npm publish --provenance --access public --tag latest
176+
echo "✓ Published $VERSION_AFTER to npm"
177+
else
178+
echo "No version change detected (version: $VERSION_AFTER)"
179+
echo "Skipping npm publish - no new release was created"
180+
fi
181+
182+
- name: Dry run - skip npm publish
183+
if: github.event.inputs.dry_run == 'true'
184+
run: |
185+
echo "=============================================="
186+
echo " DRY RUN - Skipping npm publish"
187+
echo "=============================================="
188+
NEXT_VERSION=$(cat next-version.txt 2>/dev/null || echo "")
189+
if [ -n "$NEXT_VERSION" ]; then
190+
echo "Version that WOULD have been published: $NEXT_VERSION"
191+
else
192+
echo "Version that WOULD have been published: (check semantic-release output above; might be no new release)"
193+
echo "Current package.json: $(node -p "require('./package.json').version")"
194+
fi
195+
echo ""
196+
echo "To publish for real, run the workflow again with 'Dry run only' unchecked."

‎.husky/commit-msg‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npx --no -- commitlint --edit ${1}

‎.releaserc.json‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"branches": [
3+
"master",
4+
{
5+
"name": "main",
6+
"prerelease": "beta"
7+
}
8+
],
9+
"tagFormat": "v${version}",
10+
"plugins": [
11+
"@semantic-release/commit-analyzer",
12+
"@semantic-release/release-notes-generator",
13+
"@semantic-release/changelog",
14+
[
15+
"@semantic-release/npm",
16+
{
17+
"npmPublish": false,
18+
"tarballDir": "dist"
19+
}
20+
],
21+
[
22+
"@semantic-release/git",
23+
{
24+
"assets": ["package.json", "CHANGELOG.md"],
25+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
26+
}
27+
],
28+
"@semantic-release/github"
29+
]
30+
}

‎commitlint.config.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export default {
2+
extends: ['@commitlint/config-conventional'],
3+
rules: {
4+
'type-enum': [
5+
2,
6+
'always',
7+
[
8+
'feat', // New feature
9+
'fix', // Bug fix
10+
'docs', // Documentation changes
11+
'style', // Code style changes (formatting, etc.)
12+
'refactor', // Code refactoring
13+
'perf', // Performance improvements
14+
'test', // Adding or updating tests
15+
'build', // Build system changes
16+
'ci', // CI configuration changes
17+
'chore', // Other changes that don't modify src or test files
18+
'revert', // Revert a previous commit
19+
],
20+
],
21+
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
22+
},
23+
};

0 commit comments

Comments
 (0)