Skip to the content

Capsid menu

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

The functions of strip-pr-footer.mjs
FunctionLinesComplexityCoveredCRAPMutation
stripFooter18 to 275100%OkCRAP 5No data: mutation testing has not run on this repo yet
gh29 to 3630%WatchCRAP 12No data: mutation testing has not run on this repo yet
fix38 to 4320%WatchCRAP 6No data: mutation testing has not run on this repo yet
main45 to 6590%CriticalCRAP 90No data: mutation testing has not run on this repo yet
<anonymous:68>68 to 7120%WatchCRAP 6No 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);
  });
}