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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ jobs:

run-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: ${{ matrix.node }}
cache: yarn
cache-dependency-path: yarn.lock

Expand All @@ -40,6 +43,12 @@ jobs:
- name: Run tests
run: yarn test

- name: Check publishable tarballs
run: yarn tsx bin/check-tarballs.ts

- name: Smoke-test installed tarballs
run: yarn tsx bin/smoke-tarballs.ts

build-web:
runs-on: ubuntu-latest
steps:
Expand Down
70 changes: 70 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Publish tagged packages
on:
push:
branches: [main]
workflow_dispatch:
inputs:
dry-run:
description: "Run npm publish with --dry-run"
type: boolean
default: true

permissions:
contents: read
id-token: write

# lerna.json sets no commit message; adding "[skip ci]" there would
# silently stop this workflow from publishing.

jobs:
check:
runs-on: ubuntu-latest
outputs:
tagged: ${{ steps.tags.outputs.tagged }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- id: tags
run: |
if git tag --points-at HEAD | grep -q '^@ethdebug/'; then
echo tagged=true >> "$GITHUB_OUTPUT"
else
echo tagged=false >> "$GITHUB_OUTPUT"
fi

publish:
needs: check
if: >-
needs.check.outputs.tagged == 'true' ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: publish
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
cache-dependency-path: yarn.lock

- name: Use npm 11 (trusted publishing)
run: npm install -g npm@11

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Run tests
run: yarn test

- name: Publish
run: >-
yarn tsx bin/publish-tagged.ts
${{ (github.event_name == 'workflow_dispatch' && inputs.dry-run) && '--dry-run' || '' }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ dist/
*.tsbuildinfo
coverage/
.worktrees/
.nx/
23 changes: 23 additions & 0 deletions bin/check-tarballs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { fileURLToPath } from "node:url";
import { checkPackList, packList } from "./packlist.js";
import { readWorkspaces } from "./publish-tagged.js";

const root = fileURLToPath(new URL("..", import.meta.url));

let failed = false;
for (const workspace of readWorkspaces(root)) {
if (workspace.private) {
continue;
}
const bad = checkPackList(packList(workspace.dir));
if (bad.length > 0) {
failed = true;
console.error(`${workspace.name}: disallowed files in tarball:`);

Check warning on line 15 in bin/check-tarballs.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected console statement
for (const path of bad) {
console.error(` ${path}`);

Check warning on line 17 in bin/check-tarballs.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected console statement
}
} else {
console.log(`${workspace.name}: ok`);

Check warning on line 20 in bin/check-tarballs.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected console statement
}
}
process.exit(failed ? 1 : 0);
49 changes: 49 additions & 0 deletions bin/packlist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { checkPackList, parsePackOutput } from "./packlist.js";

describe("checkPackList", () => {
it("accepts the allowed shape", () => {
expect(
checkPackList([
"package.json",
"README.md",
"LICENSE",
"dist/src/index.js",
"dist/src/a/b.d.ts",
"dist/bin/bugc.js",
]),
).toEqual([]);
});

it("rejects src, tests, buildinfo, and dist/test", () => {
expect(
checkPackList([
"src/index.ts",
"dist/src/x.test.js",
"dist/tsconfig.build.tsbuildinfo",
"dist/test/helper.js",
"dist/vitest.config.js",
]),
).toEqual([
"src/index.ts",
"dist/src/x.test.js",
"dist/tsconfig.build.tsbuildinfo",
"dist/test/helper.js",
"dist/vitest.config.js",
]);
});
});

describe("parsePackOutput", () => {
it("takes the last JSON array after script noise", () => {
const out = [
"yarn run v1.22.22",
"$ node ./bin/generate-schema-yamls.js",
"Done in 0.46s.",
"[",
' { "files": [ { "path": "dist/src/index.js" } ] }',
"]",
].join("\n");
expect(parsePackOutput(out)).toEqual(["dist/src/index.js"]);
});
});
40 changes: 40 additions & 0 deletions bin/packlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { execFileSync } from "node:child_process";

const allowed = [
/^package\.json$/,
/^README[^/]*$/,
/^LICENSE[^/]*$/,
/^dist\/src\//,
/^dist\/bin\//,
];

const rejected = [/\.test\./, /\.tsbuildinfo$/];

export function checkPackList(files: string[]): string[] {
return files.filter(
(path) =>
!allowed.some((re) => re.test(path)) ||
rejected.some((re) => re.test(path)),
);
}

export function parsePackOutput(stdout: string): string[] {
const lines = stdout.split("\n");
const start = lines.lastIndexOf("[");
if (start < 0) {
throw new Error("npm pack --json: no JSON array in output");
}
const parsed = JSON.parse(lines.slice(start).join("\n")) as {
files: { path: string }[];
}[];
return parsed.flatMap((entry) => entry.files.map((file) => file.path));
}

export function packList(packageDir: string): string[] {
const stdout = execFileSync("npm", ["pack", "--dry-run", "--json"], {
cwd: packageDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
});
return parsePackOutput(stdout);
}
Loading
Loading