Skip to content

Commit ce49716

Browse files
balzssclaude
andcommitted
feat(ui-scripts): allow pr-snapshot to publish at an operator-supplied prerelease version
Adds two optional inputs to the pr-snapshot workflow_dispatch path: custom_version (e.g. 11.7.3-SECURITY.0) and dist_tag (e.g. security). When set, they override the auto-computed snapshot version and the default pr-snapshot dist-tag. Use case: mirror a previously-published private security release onto the public registry under a non-latest dist-tag, so open-source consumers who pinned to a prerelease version from the private registry can switch their resolution to npmjs without changing package.json. Workflow plumbing: - release_to_npm.yml: two new optional inputs forwarded to the pr-release job - _pr-release-reusable.yml: accepts the inputs, validates them, and forwards as --customVersion / --distTag (via env vars to avoid shell-injection from workflow_dispatch input values) publish.js: - new --customVersion / --distTag flags - publishSnapshotVersion uses customVersion when supplied, else falls back to calculateNextSnapshotVersion as today - validateCustomVersionInputs() enforces guards: valid semver, prerelease only (refuses stable versions so we can never take over a future stable slot), distTag not 'latest', distTag required when customVersion is set Existing pr-snapshot behavior is unchanged when the new inputs are blank. OIDC auth + --provenance preserved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d5c1198 commit ce49716

3 files changed

Lines changed: 116 additions & 12 deletions

File tree

.github/workflows/_pr-release-reusable.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
name: PR release to npm (Reusable)
22
on:
33
workflow_call:
4+
inputs:
5+
custom_version:
6+
description: 'Optional: exact semver prerelease version to publish, overriding the auto-computed snapshot version.'
7+
required: false
8+
type: string
9+
default: ''
10+
dist_tag:
11+
description: 'Optional: npm dist-tag override. Required when custom_version is set. Cannot be "latest".'
12+
required: false
13+
type: string
14+
default: ''
415

516
permissions:
617
id-token: write
@@ -11,6 +22,20 @@ jobs:
1122
runs-on: ubuntu-latest
1223
name: Release to npm
1324
steps:
25+
- name: Validate custom-version inputs
26+
if: inputs.custom_version != ''
27+
env:
28+
CUSTOM_VERSION: ${{ inputs.custom_version }}
29+
DIST_TAG: ${{ inputs.dist_tag }}
30+
run: |
31+
if [ -z "$DIST_TAG" ]; then
32+
echo "::error::dist_tag is required when custom_version is set"
33+
exit 1
34+
fi
35+
if [ "$DIST_TAG" = "latest" ]; then
36+
echo "::error::dist_tag cannot be 'latest'"
37+
exit 1
38+
fi
1439
- uses: actions/checkout@v4
1540
with:
1641
fetch-depth: 0
@@ -25,7 +50,15 @@ jobs:
2550
- name: Set up project
2651
run: pnpm run bootstrap
2752
- name: Release to NPM
28-
run: pnpm run release --prRelease
53+
env:
54+
CUSTOM_VERSION: ${{ inputs.custom_version }}
55+
DIST_TAG: ${{ inputs.dist_tag }}
56+
run: |
57+
if [ -n "$CUSTOM_VERSION" ]; then
58+
pnpm run release --prRelease --customVersion="$CUSTOM_VERSION" --distTag="$DIST_TAG"
59+
else
60+
pnpm run release --prRelease
61+
fi
2962
- name: Get commit message
3063
run: | # puts the first line of the last commit message to the commmit_message env var
3164
echo "commmit_message=$(git log --format=%B -n 1 ${{ github.event.after }} | head -n 1)" >> $GITHUB_ENV

.github/workflows/release_to_npm.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ on:
1414
- manual
1515
- pr-snapshot
1616
default: 'manual'
17+
custom_version:
18+
description: 'Optional: exact semver prerelease version to publish (pr-snapshot only). Overrides the auto-computed snapshot version. Example: 11.7.3-SECURITY.0'
19+
required: false
20+
type: string
21+
default: ''
22+
dist_tag:
23+
description: 'Optional: npm dist-tag override (pr-snapshot only). Required when custom_version is set. Cannot be "latest".'
24+
required: false
25+
type: string
26+
default: ''
1727

1828
permissions:
1929
id-token: write # Required for OIDC token generation
@@ -38,10 +48,15 @@ jobs:
3848
contents: write
3949
secrets: inherit
4050

41-
# PR snapshot release
51+
# PR snapshot release. Optionally accepts custom_version + dist_tag to mirror
52+
# a previously-published private security release onto the public registry
53+
# under a non-latest dist-tag.
4254
pr-release:
4355
if: github.event_name == 'workflow_dispatch' && inputs.release_type == 'pr-snapshot'
4456
uses: ./.github/workflows/_pr-release-reusable.yml
57+
with:
58+
custom_version: ${{ inputs.custom_version }}
59+
dist_tag: ${{ inputs.dist_tag }}
4560
permissions:
4661
id-token: write
4762
contents: write

packages/ui-scripts/lib/commands/publish.js

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,32 @@ export default {
4848
describe: 'If true pnpm publish will use vXYZ-pr-snapshot as version',
4949
default: false
5050
})
51+
52+
yargs.option('customVersion', {
53+
type: 'string',
54+
describe:
55+
'Publish all packages at this exact semver prerelease version (e.g. 11.7.3-SECURITY.0) instead of the version in package.json. Must be combined with --distTag.',
56+
default: ''
57+
})
58+
59+
yargs.option('distTag', {
60+
type: 'string',
61+
describe:
62+
'Override the npm dist-tag used for this release. Required with --customVersion. Cannot be "latest".',
63+
default: ''
64+
})
5165
},
5266
handler: async (argv) => {
53-
const { isMaintenance, prRelease } = argv
67+
const { isMaintenance, prRelease, customVersion, distTag } = argv
5468
try {
5569
const pkgJSON = pkgUtils.getPackageJSON()
5670
await publish({
5771
packageName: pkgJSON.name,
5872
version: pkgJSON.version,
5973
isMaintenance,
60-
prRelease
74+
prRelease,
75+
customVersion,
76+
distTag
6177
})
6278
} catch (err) {
6379
error(err)
@@ -66,9 +82,18 @@ export default {
6682
}
6783
}
6884

69-
async function publish({ packageName, version, isMaintenance, prRelease }) {
85+
async function publish({
86+
packageName,
87+
version,
88+
isMaintenance,
89+
prRelease,
90+
customVersion,
91+
distTag
92+
}) {
7093
const isRegularRelease = isReleaseCommit(version)
7194

95+
validateCustomVersionInputs({ customVersion, distTag })
96+
7297
checkNpmAuth()
7398

7499
try {
@@ -87,21 +112,51 @@ async function publish({ packageName, version, isMaintenance, prRelease }) {
87112
packages
88113
})
89114
} else {
90-
const tag = prRelease ? 'pr-snapshot' : 'snapshot'
115+
const tag = distTag || (prRelease ? 'pr-snapshot' : 'snapshot')
91116
info(`📦 Version: ${version}, Tag: ${tag}`)
92117
await publishSnapshotVersion({
93118
version,
94119
packageName,
95120
packages,
96121
tag,
97-
prRelease
122+
prRelease,
123+
customVersion
98124
})
99125
}
100126
} finally {
101127
cleanupNPMRCFile()
102128
}
103129
}
104130

131+
/**
132+
* Validates the optional --customVersion / --distTag pair used to mirror a
133+
* private security release onto the public registry under a non-latest tag.
134+
* Throws if the inputs are inconsistent or unsafe.
135+
*/
136+
function validateCustomVersionInputs({ customVersion, distTag }) {
137+
if (!customVersion && !distTag) return
138+
139+
if (customVersion) {
140+
if (!semver.valid(customVersion)) {
141+
throw new Error(
142+
`--customVersion must be a valid semver, got: "${customVersion}"`
143+
)
144+
}
145+
if (!semver.prerelease(customVersion)) {
146+
throw new Error(
147+
`--customVersion must be a prerelease (e.g. 11.7.3-SECURITY.0); got "${customVersion}". Refusing to take over a stable version slot.`
148+
)
149+
}
150+
if (!distTag) {
151+
throw new Error('--distTag is required when --customVersion is set.')
152+
}
153+
}
154+
155+
if (distTag === 'latest') {
156+
throw new Error('--distTag cannot be "latest".')
157+
}
158+
}
159+
105160
/**
106161
* Publishes each package to pnpm.
107162
*/
@@ -113,13 +168,14 @@ async function publishRegularVersion(arg) {
113168
}
114169

115170
/**
116-
* Calculates the new snapshot version based on the latest tag
117-
* and the current commit, then publishes each package to npm
118-
* with the new snapshot version.
171+
* Bumps packages to a snapshot/prerelease version (either operator-supplied
172+
* via --customVersion, or auto-computed from the commit history) and
173+
* publishes them under the given dist-tag.
119174
*/
120175
async function publishSnapshotVersion(arg) {
121-
const { version, packageName, packages, tag, prRelease } = arg
122-
const snapshotVersion = calculateNextSnapshotVersion(version, prRelease)
176+
const { version, packageName, packages, tag, prRelease, customVersion } = arg
177+
const snapshotVersion =
178+
customVersion || calculateNextSnapshotVersion(version, prRelease)
123179

124180
info(`applying new snapshot version (${snapshotVersion}) to each package`)
125181

0 commit comments

Comments
 (0)