commit d223c0e20c7e6da333178232c42e1acf5be76bb9
parent d85aafb4b2f2f415f6e30c0cb77a25cbf0049273
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 10:17:45 -0400
common: a withdrawal never fails over the render cache — a cache it cannot remove is one masked line, and the refusal's exit 1 and the withdrawn source stand (re-review); test with a read-only cache dir
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/common/publish/source.test.ts b/common/publish/source.test.ts
@@ -914,3 +914,36 @@ test("history: pages that would take the publish over the step's file limit are
assert.ok(existsSync(path.join(o.publicDir!, "source", MIRROR_DIR, "info", "refs")), "the rest is published");
assert.ok(!("history" in JSON.parse(readFileSync(path.join(o.publicDir!, "source", "manifest.json"), "utf8"))));
});
+
+test("history: a refusal with a render cache it cannot remove still exits 1 and withdraws the source; one masked line says so (re-review)", async () => {
+ const repo = sourceRepo();
+ const planted = "plantedinhistory";
+ const files = operatorFiles("", "");
+ const logs: string[] = [];
+ const log = path.join(dir("stagit-log"), "calls");
+ const cacheDir = cacheDirIn(dir("cache-home"));
+ const o = opts(repo, files, logs, {
+ filterRepo: ["true"],
+ stagit: fakeStagit(log, { extra: `said ${planted} here` }),
+ tokensFile: TOKENS_FILE,
+ historyCacheDir: cacheDir,
+ });
+ assert.equal(await publishSource(o), 0, logs.join("\n"));
+ assert.ok(existsSync(path.join(cacheDir, "key.json")));
+ // The cache becomes read-only; then a rule denies what its pages say.
+ chmodSync(cacheDir, 0o555);
+ try {
+ writeFileSync(files.denylistFile, `${planted}\n`);
+ logs.length = 0;
+ assert.equal(await publishSource(o), 1, "the refusal's exit stands");
+ const text = logs.join("\n");
+ assert.match(text, /AUDIT REFUSED/);
+ assert.match(text, /the previous publish was WITHDRAWN/);
+ assert.match(text, /\[source\] the history's render cache .* could not be removed \(EACCES\); remove it by hand/);
+ assert.ok(!text.includes(planted));
+ assert.ok(!existsSync(path.join(o.publicDir!, "source")), "the source is withdrawn");
+ assert.ok(!existsSync(path.join(path.dirname(o.publicDir!), ".source-publish.json")));
+ } finally {
+ chmodSync(cacheDir, 0o755);
+ }
+});
diff --git a/common/publish/source.ts b/common/publish/source.ts
@@ -712,8 +712,21 @@ export async function publishSource(opts: SourcePublishOpts = {}): Promise<numbe
"[source] the previous publish was WITHDRAWN (mirror, tree, history, tarball): it was audited under rules that may not be today's. /source shows its empty state until a publish passes.",
);
}
- // The history's render cache was rendered under those rules too.
- if (progress.historyCache) await dropHistoryCache(progress.historyCache);
+ // The history's render cache was rendered under those rules too. It is
+ // never why a withdrawal fails: one line, and the refusal stands.
+ if (progress.historyCache) {
+ try {
+ await dropHistoryCache(progress.historyCache);
+ } catch (err) {
+ const why = (err as NodeJS.ErrnoException)?.code ?? (err as Error)?.message ?? String(err);
+ onLog(
+ maskLiterals(
+ `[source] the history's render cache ${tildify(progress.historyCache)} could not be removed (${why}); remove it by hand`,
+ ctx.literals,
+ ),
+ );
+ }
+ }
};
let code: number;
try {