@steve31415/baselib 3.36.0 → 3.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,20 @@ import { acquireLock, clearHold, publishRetainedAssets, readHold, readReleaseRec
15
15
  import { consistentBaseSha, readServingState, SHA40 } from './plan.js';
16
16
  const RELEASE_DEADLINE_MS = 25 * 60_000;
17
17
  const IMAGE_DIGEST = /@sha256:[0-9a-f]{64}$/;
18
+ const MAX_DIRTY_PATHS = 20;
19
+ /** The clean-worktree refusal, naming what made the tree dirty (`git status
20
+ * --porcelain` lines) so the reader need not go and look (2026-10-11: games'
21
+ * deploy checkout held untracked Cloudflare-era build outputs). */
22
+ function dirtyWorktreeMessage(porcelain) {
23
+ const lines = porcelain.split('\n').filter((l) => l.trim() !== '');
24
+ const shown = lines.slice(0, MAX_DIRTY_PATHS);
25
+ const more = lines.length - shown.length;
26
+ return [
27
+ 'deploys require a clean worktree; uncommitted or untracked files:',
28
+ ...shown.map((l) => ` ${l}`),
29
+ ...(more > 0 ? [` … and ${more} more`] : []),
30
+ ].join('\n');
31
+ }
18
32
  export async function runDeploy(options) {
19
33
  const log = options.log ?? ((m) => console.error(`[pw-deploy] ${m}`));
20
34
  const runner = options.runner;
@@ -37,9 +51,9 @@ export async function runDeploy(options) {
37
51
  throw new Error('deploys run from main');
38
52
  const dirty = (await must(runner, 'git', ['status', '--porcelain=v1', '--untracked-files=all'], {
39
53
  cwd: options.repoRoot,
40
- })).stdout.trim();
41
- if (dirty)
42
- throw new Error('deploys require a clean worktree');
54
+ })).stdout;
55
+ if (dirty.trim())
56
+ throw new Error(dirtyWorktreeMessage(dirty));
43
57
  const sha = (await must(runner, 'git', ['rev-parse', 'HEAD'], { cwd: options.repoRoot })).stdout.trim();
44
58
  const originMain = (await must(runner, 'git', ['rev-parse', 'origin/main'], { cwd: options.repoRoot })).stdout.trim();
45
59
  if (sha !== originMain)
@@ -9,32 +9,65 @@
9
9
  //
10
10
  // Scope is `.ts` only; `.tsx` is out of scope by decision (docs/TESTING.md,
11
11
  // "The src -> test mapping"). `.d.ts` files are declarations, not modules.
12
+ //
13
+ // Files git ignores are not part of the repo and are skipped in both
14
+ // directions. A long-lived deploy checkout can hold leftovers of a build step
15
+ // that no longer exists — games' clone kept a generated `src/build-version.ts`
16
+ // from 2026-07-15, gitignored once its generator was deleted, and verify
17
+ // failed on it in every deploy while passing in every fresh worktree
18
+ // (2026-10-12, Coder task 37e13f8e). Untracked files that are NOT ignored still
19
+ // count, so a new module is checked before its first `git add`. Outside a git
20
+ // work tree (or without git) nothing is filtered.
21
+ import { execFile } from 'node:child_process';
12
22
  import { readdir } from 'node:fs/promises';
13
23
  import { join } from 'node:path';
14
24
  const MODULE_EXT = '.ts';
15
25
  const TEST_EXT = '.test.ts';
16
- /** Files under `dir` (recursively) ending in `ext`, as paths relative to
17
- * `dir` with forward slashes. A missing directory yields no files. */
18
- async function listFiles(dir, ext) {
26
+ /** The subset of `paths` (relative to `root`) that git ignores. Tracked
27
+ * files are never reported, even when a pattern matches them. Empty when
28
+ * `root` is not in a git work tree or git is unavailable. */
29
+ function gitIgnored(root, paths) {
30
+ if (paths.length === 0)
31
+ return Promise.resolve(new Set());
32
+ return new Promise((resolve, reject) => {
33
+ const child = execFile('git', ['check-ignore', '-z', '--stdin'], { cwd: root, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
34
+ // Exit 1 means "none ignored"; 128 (not a repo) or a missing git
35
+ // binary means there is nothing to filter against.
36
+ const code = err?.code;
37
+ if (err && code !== 1 && code !== 128 && code !== 'ENOENT') {
38
+ reject(err);
39
+ return;
40
+ }
41
+ resolve(new Set(err ? [] : stdout.split('\0').filter((p) => p !== '')));
42
+ });
43
+ child.stdin?.on('error', () => { }); // git exited before reading: the callback reports it
44
+ child.stdin?.end(paths.join('\0') + '\0');
45
+ });
46
+ }
47
+ /** Files under `root/dir` (recursively) ending in `ext` and not ignored by
48
+ * git, as paths relative to `dir` with forward slashes. A missing directory
49
+ * yields no files. */
50
+ async function listFiles(root, dir, ext) {
19
51
  let entries;
20
52
  try {
21
- entries = await readdir(dir, { recursive: true });
53
+ entries = await readdir(join(root, dir), { recursive: true });
22
54
  }
23
55
  catch (err) {
24
56
  if (err.code === 'ENOENT')
25
57
  return [];
26
58
  throw err;
27
59
  }
28
- return entries
60
+ const files = entries
29
61
  .map((e) => e.split('\\').join('/'))
30
- .filter((e) => e.endsWith(ext) && !e.endsWith('.d.ts'))
31
- .sort();
62
+ .filter((e) => e.endsWith(ext) && !e.endsWith('.d.ts'));
63
+ const ignored = await gitIgnored(root, files.map((f) => `${dir}/${f}`));
64
+ return files.filter((f) => !ignored.has(`${dir}/${f}`)).sort();
32
65
  }
33
66
  export async function checkTestOwners(opts) {
34
67
  const srcDir = opts.srcDir ?? 'src';
35
68
  const testDir = opts.testDir ?? 'test';
36
- const modules = (await listFiles(join(opts.root, srcDir), MODULE_EXT)).filter((m) => !m.endsWith(TEST_EXT));
37
- const tests = new Set(await listFiles(join(opts.root, testDir), TEST_EXT));
69
+ const modules = (await listFiles(opts.root, srcDir, MODULE_EXT)).filter((m) => !m.endsWith(TEST_EXT));
70
+ const tests = new Set(await listFiles(opts.root, testDir, TEST_EXT));
38
71
  const missing = [];
39
72
  for (const mod of modules) {
40
73
  const expected = `${mod.slice(0, -MODULE_EXT.length)}${TEST_EXT}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.36.0",
3
+ "version": "3.36.2",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",