Skip to content
Draft
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
117 changes: 117 additions & 0 deletions .github/scripts/check-packages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Builds packages with their publish config, packs them, and checks the tarballs we would publish.
// Run it through `npm run check-packages [-- <package>...]`, which puts the pinned publint and attw on PATH.
// Package names match the CI matrix: "testcontainers" or a directory name under packages/modules.
import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../..");

const testOnlyFiles = [
/\.(test|spec)\.[^/]+$/,
/(^|[/._-])test[-_]?(helpers?|utils?)[^/]*$/i,
/(^|\/)(fixtures|__tests__|__mocks__)\//,
];

const workspacePath = (name) => (name === "testcontainers" ? "packages/testcontainers" : `packages/modules/${name}`);

const allPackages = () => {
const modulesDir = resolve(rootDir, "packages/modules");
const modules = readdirSync(modulesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.filter((entry) => existsSync(resolve(modulesDir, entry.name, "package.json")))
.map((entry) => entry.name);
return ["testcontainers", ...modules.sort()];
};

const run = (command, args, { capture = false } = {}) => {
const result = spawnSync(command, args, {
cwd: rootDir,
encoding: "utf8",
stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
});
if (result.error) {
throw result.error;
}
return { ok: result.status === 0, stdout: result.stdout };
};

const build = (name) => {
const packageDir = resolve(rootDir, workspacePath(name));

// Start clean: `check-compiles` emits tests into the same build directory, and tsc does not re-emit
// files that its tsbuildinfo considers up to date.
rmSync(resolve(packageDir, "build"), { recursive: true, force: true });
for (const file of readdirSync(packageDir).filter((file) => file.endsWith(".tsbuildinfo"))) {
rmSync(resolve(packageDir, file));
}

if (!run("npm", ["run", "build", "--workspace", workspacePath(name)]).ok) {
throw new Error(`Failed to build ${name}`);
}
};

const check = (name, packDir) => {
const errors = [];

const pack = run("npm", ["pack", "--workspace", workspacePath(name), "--pack-destination", packDir, "--json"], {
capture: true,
});
if (!pack.ok) {
return ["npm pack failed"];
}
const [{ filename, files }] = JSON.parse(pack.stdout);
const tarball = join(packDir, filename);

const testFiles = files.map((file) => file.path).filter((path) => testOnlyFiles.some((re) => re.test(path)));
if (testFiles.length > 0) {
console.error(`Test-only files in the tarball:\n${testFiles.map((path) => ` ${path}`).join("\n")}`);
errors.push("test-only files are packed (exclude them in tsconfig.build.json)");
}

if (!run("publint", ["run", tarball, "--strict"]).ok) {
errors.push("publint");
}

// Check only the types we ship, not any @types package of the same name on the registry.
if (!run("attw", [tarball, "--no-definitely-typed"]).ok) {
errors.push("attw");
}

return errors;
};

const packages = process.argv.length > 2 ? process.argv.slice(2) : allPackages();
const unknown = packages.filter((name) => !existsSync(resolve(rootDir, workspacePath(name), "package.json")));
if (unknown.length > 0) {
console.error(`Unknown packages: ${unknown.join(", ")}`);
process.exit(1);
}

// Modules compile against the core build output through project references, so core is built first.
for (const name of new Set(["testcontainers", ...packages])) {
build(name);
}

const packDir = mkdtempSync(join(tmpdir(), "testcontainers-packages-"));
const failures = [];
try {
for (const name of packages) {
console.log(`\n=== ${name} ===`);
const errors = check(name, packDir);
if (errors.length > 0) {
failures.push(`${name}: ${errors.join(", ")}`);
}
}
} finally {
rmSync(packDir, { recursive: true, force: true });
}

if (failures.length > 0) {
console.error(`\nPackage checks failed:\n${failures.map((failure) => ` ${failure}`).join("\n")}`);
process.exit(1);
}
console.log(`\nPackage checks passed for ${packages.length} package(s).`);
26 changes: 26 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ jobs:
npm run build --ignore-scripts --workspace ${{ steps.npm-install.outputs.workspace_path }} -- --project tsconfig.json --noEmit
fi

package:
if: ${{ needs.detect-modules.outputs.modules_count != '0' }}
name: Package
needs:
- detect-modules
- lint
strategy:
fail-fast: false
matrix:
module: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
runs-on: ubuntu-24.04
steps:
- name: Code checkout
uses: actions/checkout@v7
- name: Install Node and Dependencies
uses: ./.github/actions/npm-setup
with:
runner: ubuntu-24.04
node-version: 24.x
workspace: "${{ matrix.module }}"
- name: Check package
env:
MODULE: ${{ matrix.module }}
run: npm run check-packages -- "${MODULE}"

smoke-test:
if: ${{ needs.detect-modules.outputs.has_testcontainers == 'true' }}
needs:
Expand Down Expand Up @@ -208,6 +233,7 @@ jobs:
- detect-modules
- lint
- compile
- package
- smoke-test
- smoke-test-bun
- test
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ It captures practical rules that prevent avoidable CI and PR churn.
- Apply the implementation change, rerun the same test, and confirm it passes.
- Report the red-green evidence in the PR verification summary.
- Test-only helper files under `src` (for example `*-test-utils.ts`) must be explicitly excluded from package `tsconfig.build.json` so they are not emitted into `build` and accidentally published.
- `npm run check-packages [-- <package>...]` (the `Package` CI job) enforces this: it builds each package from a clean `build` directory, packs it, fails if the tarball contains test-only files, and runs `publint` and `@arethetypeswrong/cli` against the tarball.
- `npm run check-compiles` emits tests into the same `build` directories, so do not `npm pack` from a working tree after running it.
- For substantial changes to GitHub Actions, runner images, Node/npm versions, or release/publish automation, consider running the manual `Node.js Package` workflow as a dry-run publish sanity check.
- Select the PR branch as the workflow ref to test publish workflow changes before merging.
- Use a representative version input, for example the next planned semver.
Expand Down Expand Up @@ -86,7 +88,7 @@ reviewers can follow the reasoning.
1. Start from `main`.
2. Create a branch prefixed with `<agent-name>/` (for example `claude/fix-exec-output-truncation`). The PR title must not carry such prefixes (see step 9).
3. Implement scoped changes only.
4. Run required checks: `npm run format`, `npm run lint`, `npm run check-compiles` when touching `packages/testcontainers` APIs consumed by modules, and targeted tests.
4. Run required checks: `npm run format`, `npm run lint`, `npm run check-compiles` when touching `packages/testcontainers` APIs consumed by modules, `npm run check-packages -- <package>` when touching a package's `package.json`, `tsconfig.build.json` or build scripts, and targeted tests.
- When working in a fresh git worktree, dependencies are not installed (`node_modules` is absent), so verification commands (tests, `lint`, `format`, `check-compiles`) will fail with "Cannot find module" errors. Run `npm ci` once before verifying. `npm ci` only populates `node_modules` and must not modify `package-lock.json`; if it does, treat that as drift to investigate.
5. Verify git diff only contains intended files.
6. Never commit, push, or post on GitHub (issues, PRs, or comments) without first sharing the proposed diff/message and getting explicit user approval.
Expand Down
Loading
Loading