Add unified patch runner (scripts/run-patches.mjs)
Deploy Indiekit Server / deploy (push) Successful in 2m18s
Deploy Indiekit Server / deploy (push) Successful in 2m18s
Replaces the 60-command &&-chain in postinstall and serve with a single node scripts/run-patches.mjs call. - Discovers all patch-*.mjs dynamically via readdir - Runs sequentially (8 files shared across multiple patches) - Classifies output: already / applied / not-found / failed - Prints summary: ✓ N already applied | M newly applied | K not found | J failed - --check flag: exits 1 if any patch was newly applied (CI idempotence) - Surfaced hidden issue: patch-actor-aliases-successor broken (ERR_PACKAGE_PATH_NOT_EXPORTED) ISC-36 (npm run postinstall on server) deferred — verify after next deploy.
This commit is contained in:
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
|
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
|
||||||
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-content-null-guard.mjs && node scripts/patch-endpoint-posts-content-field-object.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-session-maxage.mjs && node scripts/patch-ap-dm-bsky-source-content.mjs && node scripts/patch-ap-dm-send-serialize-id.mjs",
|
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/run-patches.mjs",
|
||||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/preflight-startup-gate.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-put-fallback.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-microsub-no-bookmark-autofollow.mjs && node scripts/patch-microsub-batch-concurrency.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-endpoint-posts-content-null-guard.mjs && node scripts/patch-endpoint-posts-content-field-object.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-reset-stale.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-bluesky-og-own-post-title.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-category-from-posts.mjs && node scripts/patch-tag-input-autocomplete.mjs && node scripts/patch-session-maxage.mjs && node scripts/patch-ap-dm-bsky-source-content.mjs && node scripts/patch-ap-dm-send-serialize-id.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/preflight-startup-gate.mjs && node scripts/run-patches.mjs && node --max-old-space-size=1024 --max-semi-space-size=32 --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// scripts/run-patches.mjs
|
||||||
|
//
|
||||||
|
// Unified patch runner: discovers all sibling `patch-*.mjs` files, executes
|
||||||
|
// each one sequentially as a Node subprocess, classifies the result based on
|
||||||
|
// captured output, and prints a summary.
|
||||||
|
//
|
||||||
|
// Sequential by design — multiple patches may target the same file, and
|
||||||
|
// concurrent writes would corrupt them.
|
||||||
|
//
|
||||||
|
// Flags:
|
||||||
|
// --check After the summary, fail (exit 1) if any patch was *newly*
|
||||||
|
// applied. Used in CI to assert idempotence: a clean checkout
|
||||||
|
// of an already-patched node_modules tree must produce zero
|
||||||
|
// "applied" results.
|
||||||
|
//
|
||||||
|
// Exit codes:
|
||||||
|
// 0 — all patches succeeded; with --check, also requires zero new applies.
|
||||||
|
// 1 — at least one patch failed, OR --check was set and applies > 0.
|
||||||
|
|
||||||
|
import { readdir } from 'node:fs/promises';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { dirname, join, basename } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const projectRoot = join(__dirname, '..');
|
||||||
|
|
||||||
|
const args = new Set(process.argv.slice(2));
|
||||||
|
const checkMode = args.has('--check');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all `patch-*.mjs` files alongside this script.
|
||||||
|
* Returns absolute paths sorted alphabetically for deterministic ordering.
|
||||||
|
*/
|
||||||
|
async function discoverPatches(scriptsDir) {
|
||||||
|
const entries = await readdir(scriptsDir, { withFileTypes: true });
|
||||||
|
const patches = entries
|
||||||
|
.filter((e) => e.isFile() && e.name.startsWith('patch-') && e.name.endsWith('.mjs'))
|
||||||
|
.map((e) => join(scriptsDir, e.name))
|
||||||
|
.sort();
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single patch as a Node subprocess. Captures stdout+stderr together
|
||||||
|
* (merged in encounter order) and the exit code.
|
||||||
|
*
|
||||||
|
* Resolves with `{ output, exitCode }`. Rejects only on unrecoverable spawn
|
||||||
|
* failures (e.g. the Node binary itself cannot be invoked) — those bubble up
|
||||||
|
* to the caller and abort the run.
|
||||||
|
*/
|
||||||
|
function runPatch(patchPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(process.execPath, [patchPath], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
child.stdout.on('data', (buf) => chunks.push(buf));
|
||||||
|
child.stderr.on('data', (buf) => chunks.push(buf));
|
||||||
|
|
||||||
|
child.once('error', (err) => reject(err));
|
||||||
|
|
||||||
|
child.once('close', (code) => {
|
||||||
|
const output = Buffer.concat(chunks).toString('utf8');
|
||||||
|
// `code` is null when the process was killed by a signal; treat that as
|
||||||
|
// a non-zero exit so it lands in the `failed` bucket.
|
||||||
|
const exitCode = typeof code === 'number' ? code : 1;
|
||||||
|
resolve({ output, exitCode });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify a patch result into one of:
|
||||||
|
* 'already' | 'applied' | 'notFound' | 'failed'
|
||||||
|
*
|
||||||
|
* Order matters:
|
||||||
|
* 1. Non-zero exit → 'failed' regardless of output content.
|
||||||
|
* 2. "already" anywhere in output (case-insensitive) → 'already'.
|
||||||
|
* This wins over "applied" because patches commonly emit phrases like
|
||||||
|
* "already applied" — we must not double-count those as new applies.
|
||||||
|
* 3. "patched" / "applied to" / leading "Applied" → 'applied'.
|
||||||
|
* 4. "no " / "not found" (case-insensitive) → 'notFound'.
|
||||||
|
* 5. Empty output with exit 0 → 'notFound' (silent no-op).
|
||||||
|
* 6. Anything else with exit 0 → 'applied' (conservative default: a
|
||||||
|
* successful run that produced output is treated as having done work).
|
||||||
|
*/
|
||||||
|
function classify(output, exitCode) {
|
||||||
|
if (exitCode !== 0) return 'failed';
|
||||||
|
|
||||||
|
const lower = output.toLowerCase();
|
||||||
|
const trimmed = output.trim();
|
||||||
|
|
||||||
|
if (lower.includes('already')) return 'already';
|
||||||
|
|
||||||
|
// "Applied" at the start of any line, or the substrings "patched" /
|
||||||
|
// "applied to" anywhere in the output.
|
||||||
|
const hasAppliedMarker =
|
||||||
|
lower.includes('patched') ||
|
||||||
|
lower.includes('applied to') ||
|
||||||
|
/(^|\n)\s*Applied\b/.test(output);
|
||||||
|
|
||||||
|
if (hasAppliedMarker) return 'applied';
|
||||||
|
|
||||||
|
if (lower.includes('no ') || lower.includes('not found') || lower.includes(': error')) return 'notFound';
|
||||||
|
|
||||||
|
if (trimmed.length === 0) return 'notFound';
|
||||||
|
|
||||||
|
// Successful exit, non-empty output, but no recognized marker: treat as
|
||||||
|
// applied so it surfaces in --check rather than being silently swallowed.
|
||||||
|
return 'applied';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const patches = await discoverPatches(__dirname);
|
||||||
|
|
||||||
|
if (patches.length === 0) {
|
||||||
|
console.log('\n✓ 0 already applied | 0 newly applied | 0 not found | 0 failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buckets = {
|
||||||
|
already: [],
|
||||||
|
applied: [],
|
||||||
|
notFound: [],
|
||||||
|
failed: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sequential: shared target files mean parallel runs would corrupt them.
|
||||||
|
for (const patchPath of patches) {
|
||||||
|
const name = basename(patchPath, '.mjs');
|
||||||
|
const { output, exitCode } = await runPatch(patchPath);
|
||||||
|
const verdict = classify(output, exitCode);
|
||||||
|
|
||||||
|
buckets[verdict].push({ name, output, exitCode });
|
||||||
|
|
||||||
|
if (verdict === 'applied') {
|
||||||
|
console.log(`\n=== ${name} ===`);
|
||||||
|
process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
|
||||||
|
} else if (verdict === 'failed') {
|
||||||
|
console.error(`\n=== ${name} (FAILED, exit ${exitCode}) ===`);
|
||||||
|
process.stderr.write(output.endsWith('\n') ? output : `${output}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary =
|
||||||
|
`\n✓ ${buckets.already.length} already applied` +
|
||||||
|
` | ${buckets.applied.length} newly applied` +
|
||||||
|
` | ${buckets.notFound.length} not found` +
|
||||||
|
` | ${buckets.failed.length} failed`;
|
||||||
|
console.log(summary);
|
||||||
|
|
||||||
|
let exitCode = 0;
|
||||||
|
|
||||||
|
if (checkMode && buckets.applied.length > 0) {
|
||||||
|
for (const { name } of buckets.applied) {
|
||||||
|
console.log(`[CHECK FAIL] ${name}`);
|
||||||
|
}
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buckets.failed.length > 0) {
|
||||||
|
exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('run-patches: fatal error');
|
||||||
|
console.error(err && err.stack ? err.stack : err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user