|
| 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." |
0 commit comments