mirror of
https://github.com/hanzoai/jose.git
synced 2026-09-24 17:00:01 +00:00
ci: track staged npm release availability
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
name: npm availability
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to monitor
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to monitor
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
wait:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 65
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Wait for npm availability
|
||||
run: node tools/wait-for-npm.cjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
@@ -91,3 +91,13 @@ jobs:
|
||||
- run: node tools/release-notes.cjs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
npm-availability:
|
||||
needs:
|
||||
- github
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
uses: ./.github/workflows/npm-availability.yml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
@@ -48,6 +48,7 @@ All notable changes to this project will be documented in this file. See [commit
|
||||
* unify base64 decode errors ([3a91833](https://github.com/panva/jose/commit/3a918336051e7b60122327af62baf16b19f4e1d7))
|
||||
* validate each token once rather than once per layer ([bbdae09](https://github.com/panva/jose/commit/bbdae094ca0acaabf33688f56971991d07523714))
|
||||
* write each algorithm identifier once ([73d83b5](https://github.com/panva/jose/commit/73d83b5854a797eb6cac8e3d4c8481a9d0207830))
|
||||
|
||||
## [6.2.4](https://github.com/panva/jose/compare/v6.2.3...v6.2.4) (2026-07-21)
|
||||
|
||||
|
||||
|
||||
+26
-14
@@ -1,20 +1,32 @@
|
||||
const fs = require('fs')
|
||||
const { execSync } = require('child_process')
|
||||
const { addPendingNotice } = require('./wait-for-npm.cjs')
|
||||
|
||||
execSync('git show HEAD -- CHANGELOG.md > CHANGELOG.diff')
|
||||
function extractReleaseNotes(changelog, version) {
|
||||
const releaseHeading = `## [${version}]`
|
||||
const releaseStart = changelog.indexOf(releaseHeading)
|
||||
|
||||
const tag = execSync('git tag --points-at HEAD').toString().trim()
|
||||
if (releaseStart === -1) {
|
||||
throw new Error(`could not find ${releaseHeading} in CHANGELOG.md`)
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
'notes.md',
|
||||
fs
|
||||
.readFileSync('CHANGELOG.diff')
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((line) => line.startsWith('+') && !line.startsWith('+++'))
|
||||
.map((line) => line.slice(1))
|
||||
.slice(3)
|
||||
.join('\n'),
|
||||
)
|
||||
const notesStart = changelog.indexOf('\n', releaseStart) + 1
|
||||
const nextRelease = changelog.indexOf('\n## [', notesStart)
|
||||
return changelog.slice(notesStart, nextRelease === -1 ? undefined : nextRelease).trim()
|
||||
}
|
||||
|
||||
execSync(`gh release create ${tag} -F notes.md --title ${tag} --discussion-category Releases`)
|
||||
function main() {
|
||||
const tag = execSync('git tag --points-at HEAD').toString().trim()
|
||||
const version = tag.replace(/^v/, '')
|
||||
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8')
|
||||
const notes = extractReleaseNotes(changelog, version)
|
||||
|
||||
fs.writeFileSync('notes.md', addPendingNotice(notes))
|
||||
execSync(`gh release create ${tag} -F notes.md --title ${tag} --discussion-category Releases`)
|
||||
}
|
||||
|
||||
module.exports = { extractReleaseNotes }
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
const { execFileSync } = require('child_process')
|
||||
const { name } = require('../package.json')
|
||||
|
||||
const NOTICE_START = '<!-- npm-publish-pending:start -->'
|
||||
const NOTICE_END = '<!-- npm-publish-pending:end -->'
|
||||
const MAX_WAIT = 60 * 60_000
|
||||
const RETRY_INTERVAL = 60_000
|
||||
const REQUEST_TIMEOUT = 15_000
|
||||
|
||||
const notice = `${NOTICE_START}
|
||||
> [!NOTE]
|
||||
> The npm package has been staged and is pending npm approval. It will become available after publish-time scanning completes.
|
||||
${NOTICE_END}`
|
||||
|
||||
function addPendingNotice(body) {
|
||||
if (body.includes(NOTICE_START)) {
|
||||
return body
|
||||
}
|
||||
|
||||
return `${notice}\n\n${body}`
|
||||
}
|
||||
|
||||
function removePendingNotice(body) {
|
||||
const start = body.indexOf(NOTICE_START)
|
||||
if (start === -1) {
|
||||
return body
|
||||
}
|
||||
|
||||
const end = body.indexOf(NOTICE_END, start)
|
||||
if (end === -1) {
|
||||
throw new Error('npm pending notice is missing its closing marker')
|
||||
}
|
||||
|
||||
const afterNotice = end + NOTICE_END.length
|
||||
const separatorLength = body.startsWith('\n\n', afterNotice)
|
||||
? 2
|
||||
: Number(body.startsWith('\n', afterNotice))
|
||||
|
||||
return `${body.slice(0, start)}${body.slice(afterNotice + separatorLength)}`
|
||||
}
|
||||
|
||||
function ghApi(endpoint, options = {}) {
|
||||
const args = ['api']
|
||||
if (options.method) {
|
||||
args.push('--method', options.method)
|
||||
}
|
||||
if (options.input) {
|
||||
args.push('--input', '-')
|
||||
}
|
||||
args.push(endpoint)
|
||||
|
||||
return execFileSync('gh', args, {
|
||||
encoding: 'utf8',
|
||||
input: options.input,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
})
|
||||
}
|
||||
|
||||
function updateBody(endpoint, body) {
|
||||
ghApi(endpoint, {
|
||||
method: 'PATCH',
|
||||
input: JSON.stringify({ body }),
|
||||
})
|
||||
}
|
||||
|
||||
function discussionEndpoint(repository, discussionUrl) {
|
||||
const url = new URL(discussionUrl)
|
||||
const match = /^\/[^/]+\/[^/]+\/discussions\/(\d+)$/.exec(url.pathname)
|
||||
if (!match) {
|
||||
throw new Error(`unexpected release discussion URL: ${discussionUrl}`)
|
||||
}
|
||||
|
||||
return `repos/${repository}/discussions/${match[1]}`
|
||||
}
|
||||
|
||||
function updateReleaseBodies(repository, tag, transform) {
|
||||
const releaseEndpoint = `repos/${repository}/releases/tags/${encodeURIComponent(tag)}`
|
||||
const release = JSON.parse(ghApi(releaseEndpoint))
|
||||
if (!release.discussion_url) {
|
||||
throw new Error(`release ${tag} does not have a linked discussion`)
|
||||
}
|
||||
|
||||
const linkedDiscussionEndpoint = discussionEndpoint(repository, release.discussion_url)
|
||||
const discussion = JSON.parse(ghApi(linkedDiscussionEndpoint))
|
||||
const releaseBody = transform(release.body || '')
|
||||
const discussionBody = transform(discussion.body || '')
|
||||
|
||||
if (releaseBody !== release.body) {
|
||||
updateBody(`repos/${repository}/releases/${release.id}`, releaseBody)
|
||||
}
|
||||
if (discussionBody !== discussion.body) {
|
||||
updateBody(linkedDiscussionEndpoint, discussionBody)
|
||||
}
|
||||
}
|
||||
|
||||
function markReleasePending(repository, tag) {
|
||||
updateReleaseBodies(repository, tag, addPendingNotice)
|
||||
}
|
||||
|
||||
function finalizeRelease(repository, tag) {
|
||||
updateReleaseBodies(repository, tag, removePendingNotice)
|
||||
}
|
||||
|
||||
async function isPublished(version) {
|
||||
const response = await fetch(
|
||||
`https://registry.npmjs.org/${encodeURIComponent(name)}/${encodeURIComponent(version)}`,
|
||||
{
|
||||
headers: { accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
||||
},
|
||||
)
|
||||
|
||||
if (response.status === 404) {
|
||||
return false
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`npm registry returned ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const manifest = await response.json()
|
||||
return manifest.version === version
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const repository = process.env.GITHUB_REPOSITORY
|
||||
const tag = process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME
|
||||
if (!repository || !tag) {
|
||||
throw new Error('GITHUB_REPOSITORY and RELEASE_TAG or GITHUB_REF_NAME must be set')
|
||||
}
|
||||
|
||||
const match = /^v(\d+\.\d+\.\d+)$/.exec(tag)
|
||||
if (!match) {
|
||||
throw new Error(`unexpected release tag: ${tag}`)
|
||||
}
|
||||
const version = match[1]
|
||||
|
||||
const deadline = Date.now() + MAX_WAIT
|
||||
let attempt = 1
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
let published = false
|
||||
try {
|
||||
published = await isPublished(version)
|
||||
} catch (error) {
|
||||
console.warn(`npm availability check failed: ${error.message}`)
|
||||
}
|
||||
|
||||
if (published) {
|
||||
finalizeRelease(repository, tag)
|
||||
console.log(`${name}@${version} is available on npm; removed the pending notice`)
|
||||
return
|
||||
}
|
||||
|
||||
if (attempt === 1) {
|
||||
markReleasePending(repository, tag)
|
||||
console.log(`npm pending notice is present on release ${tag} and its discussion`)
|
||||
}
|
||||
|
||||
const remaining = deadline - Date.now()
|
||||
if (remaining > 0) {
|
||||
console.log(`${name}@${version} is not available on npm; retrying in one minute`)
|
||||
await wait(Math.min(RETRY_INTERVAL, remaining))
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${name}@${version} was not available on npm after ${attempt} attempts in one hour`,
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPendingNotice,
|
||||
discussionEndpoint,
|
||||
removePendingNotice,
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user