1+ # This workflow will run tests using node and then publish a package to the npm registry when a release is created
2+ # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3+ #
4+ # Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret is
5+ # required. Each package must have a trusted publisher configured on npmjs.com
6+ # pointing at this workflow file. See:
7+ # https://docs.npmjs.com/trusted-publishers
8+
19name : Node.js Package
210
311on :
@@ -7,40 +15,107 @@ jobs:
715 publish-npm :
816 runs-on : ubuntu-latest
917 permissions :
10- contents : write
11- id-token : write
18+ contents : write # for the atomic version-bump push (branch + tag)
19+ id-token : write # for npm OIDC trusted publishing
1220 steps :
1321 - uses : actions/setup-node@v6
1422 with :
23+ # OIDC trusted publishing needs npm >= 11.5.1, which requires
24+ # Node >= 20.17.0. setup-node's `20` resolves to the latest
25+ # 20.x, which satisfies that.
1526 node-version : 20
1627 registry-url : https://registry.npmjs.org/
1728 - name : Upgrade npm to >=11.5.1 (required for trusted publishing)
1829 run : npm install -g npm@latest
19- - uses : actions/checkout@v6
30+ - name : Check out Etherpad core
31+ uses : actions/checkout@v6
32+ with :
33+ repository : ether/etherpad-lite
34+ - uses : pnpm/action-setup@v6
35+ name : Install pnpm
36+ with :
37+ version : 10
38+ run_install : false
39+ - name : Get pnpm store directory
40+ shell : bash
41+ run : |
42+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
43+ - uses : actions/cache@v5
44+ name : Setup pnpm cache
45+ with :
46+ path : ${{ env.STORE_PATH }}
47+ key : ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
48+ restore-keys : |
49+ ${{ runner.os }}-pnpm-store-
50+ -
51+ uses : actions/checkout@v6
2052 with :
2153 fetch-depth : 0
22- - run : npm ci
23- - run : npm test
24- - name : Bump version (patch)
54+ -
55+ name : Bump version (patch)
56+ id : bump
2557 run : |
2658 LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1
2759 NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1
28- [ "${NEW_COMMITS}" -gt 0 ] || exit 0
60+ # No new commits since the last tag → nothing to publish.
61+ # Setting `bumped=false` lets the publish step below skip
62+ # itself instead of trying to republish the existing version
63+ # (which fails with "cannot publish over previously published
64+ # versions"). Only manual `gh workflow run` invocations on a
65+ # tag-current branch hit this path; on:push always sees ≥1
66+ # new commit because the push event is what triggered us.
67+ if [ "${NEW_COMMITS}" -le 0 ]; then
68+ echo "bumped=false" >> "${GITHUB_OUTPUT}"
69+ exit 0
70+ fi
2971 git config user.name 'github-actions[bot]'
3072 git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
31- npm version patch
32- NEW_TAG="v$(node -p "require('./package.json').version")"
33- # Use --atomic so branch + tag succeed or fail together.
34- # If a concurrent run already pushed this tag, the push fails
35- # cleanly and the next run will retry.
73+ pnpm i --frozen-lockfile
74+ # `pnpm version patch` bumps package.json, makes a commit, and creates
75+ # a `v<new-version>` tag. Capture the new tag name from package.json
76+ # rather than parsing pnpm's output, which has historically varied.
77+ # Bump the patch component directly with Node. pnpm/action-setup@v6
78+ # sometimes installs pnpm 11 pre-releases even when version: 10.x is
79+ # requested (pnpm/action-setup#225); those pre-releases either skip
80+ # the git commit/tag or reject --no-git-tag-version as unknown.
81+ # Doing the bump in Node sidesteps both failure modes.
82+ NEW_VERSION=$(node -e "const fs=require('fs');const p=require('./package.json');const v=p.version.split('.');v[2]=String(Number(v[2])+1);p.version=v.join('.');fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\n');console.log(p.version);")
83+ NEW_TAG="v${NEW_VERSION}"
84+ git add package.json
85+ git commit -m "${NEW_TAG}"
86+ git tag -a "${NEW_TAG}" -m "${NEW_TAG}"
87+ # CRITICAL: use --atomic so the branch update and the tag update
88+ # succeed (or fail) as a single transaction on the server. The old
89+ # `git push --follow-tags` was non-atomic per ref: if a concurrent
90+ # publish run won the race, the branch fast-forward would be rejected
91+ # but the tag push would still land — leaving a dangling tag with no
92+ # matching commit on the branch. Subsequent runs would then forever
93+ # try to bump to the same already-existing tag and fail with
94+ # `tag 'vN+1' already exists`. With --atomic, a rejected branch push
95+ # rejects the tag push too, and the next workflow tick can retry
96+ # cleanly against the up-to-date refs.
3697 git push --atomic origin "${GITHUB_REF_NAME}" "${NEW_TAG}"
98+ echo "bumped=true" >> "${GITHUB_OUTPUT}"
99+ # This is required if the package has a prepare script that uses something
100+ # in dependencies or devDependencies.
101+ -
102+ if : steps.bump.outputs.bumped == 'true'
103+ run : pnpm i
104+ # `npm publish` must come after `git push` otherwise there is a race
105+ # condition: If two PRs are merged back-to-back then master/main will be
106+ # updated with the commits from the second PR before the first PR's
107+ # workflow has a chance to push the commit generated by `npm version
108+ # patch`. This causes the first PR's `git push` step to fail after the
109+ # package has already been published, which in turn will cause all future
110+ # workflow runs to fail because they will all attempt to use the same
111+ # already-used version number. By running `npm publish` after `git push`,
112+ # back-to-back merges will cause the first merge's workflow to fail but
113+ # the second's will succeed.
114+ #
115+ # Use `npm publish` directly (not `pnpm publish`) because OIDC trusted
116+ # publishing requires npm CLI >= 11.5.1 and `pnpm publish` shells out to
117+ # whichever `npm` is on PATH; calling `npm` directly avoids any shim
118+ # ambiguity.
37119 - name : Publish to npm via OIDC
38- run : |
39- # Only publish if the version bump succeeded (tag was pushed)
40- CURRENT_VERSION=$(node -p "require('./package.json').version")
41- PUBLISHED=$(npm view ep_plugin_helpers version 2>/dev/null || echo "none")
42- if [ "${CURRENT_VERSION}" = "${PUBLISHED}" ]; then
43- echo "Version ${CURRENT_VERSION} already published, skipping"
44- exit 0
45- fi
46- npm publish --provenance --access public
120+ if : steps.bump.outputs.bumped == 'true'
121+ run : npm publish --provenance --access public
0 commit comments