Skip to content

Commit be5cd5f

Browse files
committed
fix(release): pin workspace:* sibling deps before JSR publish
1 parent d5e3e5b commit be5cd5f

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,9 @@ jobs:
459459
concurrency:
460460
group: release-${{ github.ref }}
461461
cancel-in-progress: false
462+
outputs:
463+
partial_failure: ${{ steps.release.outputs.partial_failure }}
464+
partial_failure_reason: ${{ steps.release.outputs.partial_failure_reason }}
462465
# Run on master branch pushes, only if all CI jobs succeeded
463466
if: |
464467
github.event_name == 'push' &&
@@ -538,6 +541,20 @@ jobs:
538541
title: 'Canary Release'
539542
status: 'failure'
540543

544+
# Fires when the canary job exits green but JSR or the gotrue-js legacy mirror
545+
# publish failed. release-canary.ts swallows those errors so npm canary still
546+
# ships; this notification ensures the breakage doesn't go silent for weeks.
547+
notify-canary-partial-failure:
548+
name: Notify Slack for Canary partial failure
549+
needs: release-canary
550+
if: ${{ always() && github.event_name == 'push' && needs.release-canary.result == 'success' && needs.release-canary.outputs.partial_failure == 'true' }}
551+
uses: ./.github/workflows/slack-notify.yml
552+
secrets:
553+
SLACK_CLIENT_LIBS_WEBHOOK: ${{ secrets.SLACK_CLIENT_LIBS_WEBHOOK }}
554+
with:
555+
title: 'Canary Release (partial failure: ${{ needs.release-canary.outputs.partial_failure_reason }})'
556+
status: 'failure'
557+
541558
notify-next-failure:
542559
name: Notify Slack for Next failure
543560
needs: release-next

scripts/publish-to-jsr.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ function cleanupTracingSnapshot(packagePath: string, pkg: string): void {
2929
rmSync(join(packagePath, TRACING_INTERNAL_SUBPATH), { recursive: true, force: true })
3030
}
3131

32+
// JSR rejects pnpm's `workspace:*` protocol as an unpinned npm specifier. Before
33+
// publishing, swap every `workspace:<range>` entry in package.json for the
34+
// package's own version (fixed-release mode means all siblings share it), then
35+
// restore the original file in the finally block.
36+
function rewriteWorkspaceDeps(packagePath: string, version: string): string {
37+
const pkgJsonPath = join(packagePath, 'package.json')
38+
const original = readFileSync(pkgJsonPath, 'utf-8')
39+
const pkgJson = JSON.parse(original)
40+
let touched = false
41+
for (const section of ['dependencies', 'devDependencies'] as const) {
42+
const deps = pkgJson[section]
43+
if (!deps) continue
44+
for (const [name, value] of Object.entries(deps)) {
45+
if (typeof value === 'string' && value.startsWith('workspace:')) {
46+
deps[name] = version
47+
touched = true
48+
}
49+
}
50+
}
51+
if (touched) {
52+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n')
53+
}
54+
return original
55+
}
56+
57+
function restorePackageJson(packagePath: string, original: string): void {
58+
writeFileSync(join(packagePath, 'package.json'), original)
59+
}
60+
3261
function getArg(name: string): string | undefined {
3362
const idx = process.argv.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`))
3463
if (idx === -1) return undefined
@@ -86,6 +115,9 @@ async function publishToJsr() {
86115
jsrConfig.version = version
87116
writeFileSync(jsrPath, JSON.stringify(jsrConfig, null, 2) + '\n')
88117

118+
// Pin workspace:* sibling deps so JSR sees a real version constraint.
119+
const originalPackageJson = rewriteWorkspaceDeps(packagePath, version)
120+
89121
// Snapshot any internal workspace deps that JSR would otherwise reject for
90122
// missing version constraints. See TRACING_SNAPSHOT_SOURCES for details.
91123
copyTracingSnapshot(packagePath, pkg)
@@ -113,6 +145,8 @@ async function publishToJsr() {
113145
} finally {
114146
// Restore original jsr.json to keep working directory clean
115147
writeFileSync(jsrPath, originalJsrContent)
148+
// Restore package.json (workspace:* sibling deps)
149+
restorePackageJson(packagePath, originalPackageJson)
116150
// Remove any internal snapshot copies created above
117151
cleanupTracingSnapshot(packagePath, pkg)
118152
}

scripts/release-canary.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { releaseVersion, releaseChangelog, releasePublish } from 'nx/release'
22
import { execSync } from 'child_process'
3+
import { appendFileSync } from 'fs'
34
import { getLastStableTag, getArg } from './utils'
45

56
// Optional CLI flags for overriding default behavior (used by develop branch for v3 prereleases):
@@ -146,6 +147,10 @@ const tagArg = getArg('tag') ?? 'canary'
146147
verbose: true,
147148
})
148149

150+
// Track non-fatal publish failures (legacy mirror + JSR) so the workflow can
151+
// notify Slack instead of letting them disappear into a green job log.
152+
const partialFailures: string[] = []
153+
149154
// Publish gotrue-js as legacy mirror of auth-js
150155
console.log('\n📦 Publishing @supabase/gotrue-js (legacy mirror)...')
151156
try {
@@ -154,6 +159,7 @@ const tagArg = getArg('tag') ?? 'canary'
154159
console.error('❌ Failed to publish gotrue-js legacy package:', error)
155160
// Don't fail the entire release if gotrue-js fails
156161
console.log('⚠️ Continuing with release despite gotrue-js publish failure')
162+
partialFailures.push('gotrue-js')
157163
}
158164

159165
// Publish all packages to JSR
@@ -164,6 +170,14 @@ const tagArg = getArg('tag') ?? 'canary'
164170
console.error('❌ Failed to publish to JSR:', error)
165171
// Don't fail the entire release if JSR publishing fails
166172
console.log('⚠️ Continuing with release despite JSR publish failure')
173+
partialFailures.push('jsr')
174+
}
175+
176+
if (partialFailures.length > 0 && process.env.GITHUB_OUTPUT) {
177+
appendFileSync(
178+
process.env.GITHUB_OUTPUT,
179+
`partial_failure=true\npartial_failure_reason=${partialFailures.join(',')}\n`
180+
)
167181
}
168182

169183
process.exit(Object.values(publishResult).every((result) => result.code === 0) ? 0 : 1)

0 commit comments

Comments
 (0)