scripts/strip-pr-footer.mjs
66 lines and 5 functions in DrDustinEdwards/devkit, at commit 505cda4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| stripFooter | 18 to 27 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| gh | 29 to 36 | 3 | 0% | WatchCRAP 12 | No data: mutation testing has not run on this repo yet |
| fix | 38 to 43 | 2 | 0% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| main | 45 to 65 | 9 | 0% | CriticalCRAP 90 | No data: mutation testing has not run on this repo yet |
| <anonymous:68> | 68 to 71 | 2 | 0% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. No module of this repo imports it.
Source
// Strips the "Generated by Claude Code" footer from pull request descriptions, the no-AI-trailer rule ai-author.yml holds
// commits to, for descriptions. A cloud session's GitHub connection appends the footer to every description it writes and
// puts it back when the session removes it, so the repository removes it instead. pr-footer.yml
// carries this file inline (scripts/inline-rollback-guard.mjs) and runs it on each opened or edited pull request, and by
// hand (workflow_dispatch) over every pull request. An edit made with the workflow's GITHUB_TOKEN starts no new run, so
// there is no loop.
//
// NO node_modules IMPORTS: the job runs without npm ci.
// The footer as the connection writes it: a rule, then one italic line linking Claude
// Code, at the end of the description. Also the "Generated with Claude Code" wording.
const FOOTER = /\s*(?:^|\n)-{3,}\s*\n+\s*_?(?:🤖\s*)?Generated (?:by|with) \[Claude Code\]\([^)]*\)_?\s*$/u;
// The bare line, with no rule above it, and a session link left on its own.
const BARE = /\s*\n(?:🤖\s*)?_?Generated (?:by|with) \[Claude Code\]\([^)]*\)_?\s*$/u;
const SESSION_LINK = /\s*\nhttps:\/\/claude\.ai\/code\/session_[A-Za-z0-9]+\s*$/u;
/** The description without the footer, or null when it carries none. @param {unknown} body @returns {string | null} */
export function stripFooter(body) {
if (typeof body !== "string") return null;
let out = body;
for (let i = 0; i < 4; i++) {
const next = out.replace(FOOTER, "").replace(BARE, "").replace(SESSION_LINK, "");
if (next === out) break;
out = next;
}
return out === body ? null : `${out.trimEnd()}\n`;
}
async function gh(path, init = {}) {
const res = await fetch(`https://api.github.com${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, Accept: "application/vnd.github+json", "Content-Type": "application/json" },
});
if (!res.ok) throw new Error(`${init.method ?? "GET"} ${path}: ${res.status} ${(await res.text()).slice(0, 200)}`);
return res.json();
}
async function fix(repo, number, body) {
const stripped = stripFooter(body);
if (stripped === null) return false;
await gh(`/repos/${repo}/pulls/${number}`, { method: "PATCH", body: JSON.stringify({ body: stripped }) });
return true;
}
async function main() {
const repo = process.env.GITHUB_REPOSITORY;
if (!repo || !process.env.GITHUB_TOKEN) throw new Error("GITHUB_REPOSITORY and GITHUB_TOKEN are required");
const one = process.env.PR_NUMBER;
if (one) {
const pr = await gh(`/repos/${repo}/pulls/${one}`);
console.log(`#${one}: ${(await fix(repo, one, pr.body)) ? "footer removed" : "no footer"}`);
return;
}
let read = 0;
let fixed = 0;
for (let page = 1; page <= 50; page++) {
const prs = await gh(`/repos/${repo}/pulls?state=all&per_page=100&page=${page}`);
for (const pr of prs) {
read++;
if (await fix(repo, pr.number, pr.body)) fixed++;
}
if (prs.length < 100) break;
}
console.log(`${read} pull request(s) read, ${fixed} footer(s) removed`);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
});
}