Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions .github/scripts/stage-package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { appendFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';

async function hasPublishedVersion(pkg) {
const response = await fetch(
`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`,
{ headers: { accept: 'application/vnd.npm.install-v1+json' } }
);

if (response.status === 404) {
return false;
}

if (!response.ok) {
throw new Error(
`Failed to query ${pkg.name}: ${response.status} ${response.statusText}`
);
}

const metadata = await response.json();
return Object.prototype.hasOwnProperty.call(
metadata.versions ?? {},
pkg.version
);
}

function getDistTag(version) {
const prerelease = version.match(/-([0-9A-Za-z-]+)(?:\.|$)/);
return prerelease ? prerelease[1] : 'latest';
}

function collectStageIds(value, ids = new Set()) {
if (!value || typeof value !== 'object') return ids;

for (const [key, child] of Object.entries(value)) {
if (/stage[-_]?id/i.test(key) && typeof child === 'string') {
ids.add(child);
} else {
collectStageIds(child, ids);
}
}

return ids;
}

function runGit(args) {
const result = spawnSync('git', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});

if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.status !== 0) process.exit(result.status ?? 1);
}

function hasLocalGitTag(tagName) {
const result = spawnSync(
'git',
['rev-parse', '--verify', '--quiet', `refs/tags/${tagName}`],
{ stdio: 'ignore' }
);
return result.status === 0;
}

function createGitTag(tagName) {
if (hasLocalGitTag(tagName)) {
console.log(`Git tag ${tagName} already exists locally.`);
} else {
runGit(['tag', tagName, '-m', tagName]);
}

// changesets/action parses this line, then pushes the tag and creates the GitHub release.
console.log(`New tag: ${tagName}`);
}

async function main() {
const pkg = JSON.parse(await readFile('package.json', 'utf8'));
const changesetConfig = JSON.parse(
await readFile('.changeset/config.json', 'utf8')
);

if (pkg.private) {
console.log(`${pkg.name}@${pkg.version} is private; skipping staging`);
return;
}

if (await hasPublishedVersion(pkg)) {
console.log(
`${pkg.name}@${pkg.version} is already published; skipping staging`
);
return;
}

const access =
pkg.publishConfig?.access ?? changesetConfig.access ?? 'public';
const tag = getDistTag(pkg.version);
const args = [
'stage',
'publish',
'.',
'--access',
access,
'--tag',
tag,
'--json'
];

console.log(`Staging ${pkg.name}@${pkg.version} with dist-tag ${tag}`);
const result = spawnSync('npm', args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
});

if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);

if (result.status !== 0) {
process.exit(result.status ?? 1);
}

createGitTag(`v${pkg.version}`);

let stageIds = [];
try {
stageIds = [...collectStageIds(JSON.parse(result.stdout))];
} catch {
// Keep the raw npm output above as the source of truth if the JSON shape changes.
}

if (stageIds.length > 0) {
const message = [
`Staged ${pkg.name}@${pkg.version}.`,
...stageIds.map((id) => `Stage ID: ${id}`),
'Approve with `npm stage approve <stage-id>` after reviewing the staged package.'
].join('\n');

console.log(message);

if (process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${message}\n`);
}
}
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
11 changes: 6 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
registry-url: "https://registry.npmjs.org"

- name: Update npm
run: npm install -g npm@11.11.1
run: npm install -g npm@^11.15.0

- name: Install dependencies
run: npm ci --ignore-scripts
Expand All @@ -54,7 +54,7 @@ jobs:
run: node .github/scripts/has-unpublished-packages.mjs

publish:
name: Publish
name: Stage package
needs: release
if: needs.release.outputs.should_publish == 'true'
environment:
Expand All @@ -78,18 +78,19 @@ jobs:
registry-url: "https://registry.npmjs.org"

- name: Update npm
run: npm install -g npm@11.11.1
run: npm install -g npm@^11.15.0

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Build package
run: npm run build

- name: Publish packages
- name: Stage package
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
with:
publish: npm exec -- changeset publish
publish: node .github/scripts/stage-package.mjs
commitMode: github-api
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading