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
85 changes: 85 additions & 0 deletions .github/scripts/has-unpublished-packages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { appendFileSync, existsSync } from 'node:fs';
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';

const ignoredDirectories = new Set(['.git', 'dist', 'node_modules']);
const workspaceRoot = process.cwd();
const hasWorkspaceManifest = existsSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'));

async function findPackageManifests(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const manifests = [];

for (const entry of entries) {
const fullPath = path.join(directory, entry.name);

if (entry.isDirectory()) {
if (!ignoredDirectories.has(entry.name)) {
manifests.push(...(await findPackageManifests(fullPath)));
}
} else if (entry.isFile() && entry.name === 'package.json') {
if (hasWorkspaceManifest && fullPath === path.join(workspaceRoot, 'package.json')) {
continue;
}

const pkg = JSON.parse(await readFile(fullPath, 'utf8'));

if (!pkg.private && pkg.name && pkg.version) {
manifests.push({ name: pkg.name, version: pkg.version });
}
}
}

return manifests;
}

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);
}

async function main() {
const packages = (await findPackageManifests(process.cwd())).sort((a, b) =>
a.name.localeCompare(b.name)
);
let hasUnpublished = false;

for (const pkg of packages) {
const isPublished = await hasPublishedVersion(pkg);

if (isPublished) {
console.log(`${pkg.name}@${pkg.version} is already published`);
} else {
console.log(`${pkg.name}@${pkg.version} is not published yet`);
hasUnpublished = true;
}
}

const output =
[`has_unpublished=${String(hasUnpublished)}`, `should_publish=${String(hasUnpublished)}`].join(
'\n'
) + '\n';

if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, output);
} else {
process.stdout.write(output);
}
}

main().catch(error => {
console.error(error);
process.exit(1);
});
155 changes: 155 additions & 0 deletions .github/scripts/stage-packages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, join, relative } from "node:path";

const root = process.cwd();
const config = JSON.parse(readFileSync(join(root, ".changeset/config.json"), "utf8"));
const ignored = new Set(config.ignore || []);
const access = config.access || "public";

function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}

function packageJsonPathsFromWorkspace() {
const paths = [];

function visit(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (
entry.name === ".git" ||
entry.name === "node_modules" ||
entry.name === ".pnpm" ||
entry.name === "dist" ||
entry.name === "coverage"
) {
continue;
}

const path = join(dir, entry.name);
if (entry.isDirectory()) {
visit(path);
} else if (entry.isFile() && entry.name === "package.json") {
paths.push(path);
}
}
}

visit(root);
return paths;
}

function packageJsonPaths() {
return [...new Set(packageJsonPathsFromWorkspace())].filter(existsSync);
}

function versionExists(name, version) {
const result = spawnSync("npm", ["view", `${name}@${version}`, "version", "--json"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});

if (result.status === 0) return true;
const output = `${result.stdout}
${result.stderr}`;
if (output.includes("E404") || output.includes("No match found")) return false;

process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
throw new Error(`Could not check npm version for ${name}@${version}`);
}

function distTag(version) {
const prerelease = version.match(/^[^-]+-([0-9A-Za-z-]+)/);
return prerelease ? prerelease[1] : "latest";
}

function runGit(args) {
const result = spawnSync("git", args, {
cwd: root,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
process.stdout.write(result.stdout);
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}`],
{
cwd: root,
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}`);
}

const staged = [];
for (const packageJsonPath of packageJsonPaths()) {
const pkg = readJson(packageJsonPath);
if (!pkg.name || !pkg.version || pkg.private || ignored.has(pkg.name)) continue;
if (versionExists(pkg.name, pkg.version)) {
console.log(`Skipping ${pkg.name}@${pkg.version}; already published.`);
continue;
}

const packageDir = dirname(packageJsonPath);
const tag = distTag(pkg.version);
const args = [
"stage",
"publish",
packageDir,
"--provenance",
"--access",
pkg.publishConfig?.access || access,
"--tag",
tag,
"--json",
];

console.log(`Staging ${pkg.name}@${pkg.version} with dist-tag ${tag}...`);
const result = spawnSync("pnpm", args, {
cwd: root,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
if (result.status !== 0) process.exit(result.status || 1);

const stageId = result.stdout.match(/"stageId"\s*:\s*"([^"]+)"/)?.[1];
const tagName = `${pkg.name}@${pkg.version}`;
createGitTag(tagName);

staged.push({
name: pkg.name,
version: pkg.version,
path: relative(root, packageDir) || ".",
stageId,
});
}

if (staged.length === 0) {
console.log("No unpublished packages to stage.");
} else {
console.log("Staged packages:");
for (const pkg of staged) {
console.log(`- ${pkg.name}@${pkg.version}${pkg.stageId ? ` (${pkg.stageId})` : ""}`);
}
console.log("Approve staged packages with `npm stage approve <stage-id>` after review.");
}
Loading
Loading