fix(opensource-pipeline): drop copied reports from the index even when .gitignore re-includes them

A .gitignore rule outranks .git/info/exclude, so a source project that ships !FORK_REPORT.md made git add -A stage the report. Remove both reports from the index after staging, in the forker's first commit and in every fold-in amend. The history test now runs both documented blocks against a negating .gitignore, prints the Passed/Failed summary for run-all.js, and skips on Windows like the publish-guard test.
This commit is contained in:
Wen committed 2026-10-19 07:23:00 -03:00
1 parent 35909e548e
commit f493d9ff66
3 files changed
+58 -20

No files matched your search

+4 -1
View File
@@ -147,6 +147,7 @@ cd TARGET_DIR
git init
printf '%s\n' FORK_REPORT.md SANITIZATION_REPORT.md >> .git/info/exclude
git add -A
git rm -q --cached --ignore-unmatch FORK_REPORT.md SANITIZATION_REPORT.md
git commit -m "Initial open-source release
Forked from private source. All secrets stripped, internal references
@@ -156,7 +157,9 @@ replaced with configurable placeholders. See .env.example for configuration."
This commit is provisional. The pipeline amends it after sanitizer fixes and packaging, so the
published history stays one commit (see the opensource-pipeline skill, Step 3). The reports never
go into it: `.git/info/exclude` is written before `git add -A`, so even a report already present in
the source project (copied by Step 2) stays untracked.
the source project (copied by Step 2) stays untracked. A `.gitignore` rule outranks
`.git/info/exclude`, so a copied `!FORK_REPORT.md` would re-include it; `git rm --cached` drops
the reports from the index in that case.
### Step 7: Generate Fork Report
+2 -1
View File
@@ -91,8 +91,9 @@ sanitizer passed. The reports stay out of it. Fold changes in with:
cd "{STAGING_PATH}"
grep -qxF SANITIZATION_REPORT.md .git/info/exclude || printf '%s\n' FORK_REPORT.md SANITIZATION_REPORT.md >> .git/info/exclude
git add -A
git rm -q --cached --ignore-unmatch FORK_REPORT.md SANITIZATION_REPORT.md
git commit --amend --no-edit
git status --porcelain # must print nothing
git status --porcelain -uno # must print nothing
git rev-list --count HEAD # must print 1
```
+52 -18
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env node
/**
* Runs the documented Step 6 of agents/opensource-forker.md in a throwaway dir that already
* holds FORK_REPORT.md / SANITIZATION_REPORT.md (as if the source project shipped them), and
* checks they stay out of the first commit.
* Runs the documented Step 6 of agents/opensource-forker.md, then the opensource-pipeline fold-in
* block, in a throwaway dir that already holds FORK_REPORT.md / SANITIZATION_REPORT.md (as if the
* source project shipped them, with a .gitignore that re-includes them), and checks they stay out
* of the single published commit.
*
* Run with: node tests/skills/opensource-forker-history.test.js
*/
@@ -15,19 +16,20 @@ const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const AGENT_MD = path.join(__dirname, '..', '..', 'agents', 'opensource-forker.md');
const ROOT = path.join(__dirname, '..', '..');
const NAME = 'pre-existing reports stay out of the published fork commit';
const step = fs.readFileSync(AGENT_MD, 'utf8').split('### Step 6: Clean Git History')[1];
assert.ok(step, 'opensource-forker.md is missing "### Step 6: Clean Git History"');
const block = step.match(/```bash\n([\s\S]*?)```/);
assert.ok(block, 'no bash block in Step 6');
function bashBlock(file, after) {
const section = fs.readFileSync(path.join(ROOT, file), 'utf8').split(after)[1];
assert.ok(section, `${file} is missing "${after}"`);
const block = section.match(/```bash\n([\s\S]*?)```/);
assert.ok(block, `no bash block after "${after}" in ${file}`);
return block[1];
}
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oss-forker-'));
try {
for (const f of ['README.md', 'FORK_REPORT.md', 'SANITIZATION_REPORT.md']) {
fs.writeFileSync(path.join(dir, f), 'x\n');
}
const r = spawnSync('bash', ['-ec', block[1].replaceAll('TARGET_DIR', dir)], {
function run(script, dir) {
const r = spawnSync('bash', ['-ec', script], {
cwd: dir,
encoding: 'utf8',
env: {
...process.env,
@@ -36,13 +38,45 @@ try {
},
});
assert.strictEqual(r.status, 0, r.stderr);
return r.stdout;
}
if (process.platform === 'win32') {
// The documented blocks are bash; a Windows temp path does not survive `cd`.
console.log(` - skipped: ${NAME} (bash test on Windows)`);
console.log('\nPassed: 0');
console.log('Failed: 0');
process.exit(0);
}
const step6 = bashBlock('agents/opensource-forker.md', '### Step 6: Clean Git History');
const foldIn = bashBlock('skills/opensource-pipeline/SKILL.md', 'Fold changes in with:');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oss-forker-'));
let failed = 0;
try {
for (const f of ['README.md', 'FORK_REPORT.md', 'SANITIZATION_REPORT.md']) {
fs.writeFileSync(path.join(dir, f), 'x\n');
}
// A .gitignore rule outranks .git/info/exclude, so a negation would re-include the reports.
fs.writeFileSync(path.join(dir, '.gitignore'), '!FORK_REPORT.md\n!SANITIZATION_REPORT.md\n');
run(step6.replaceAll('TARGET_DIR', dir), dir);
fs.writeFileSync(path.join(dir, 'CLAUDE.md'), 'packaged\n');
const out = run(foldIn.replaceAll('{STAGING_PATH}', dir), dir).trim().split('\n');
assert.strictEqual(out.at(-1), '1', `fold-in must keep one commit, got: ${out.at(-1)}`);
const files = spawnSync('git', ['ls-tree', '-r', '--name-only', 'HEAD'], { cwd: dir, encoding: 'utf8' }).stdout.trim();
assert.strictEqual(files, 'README.md', `first commit must hold only README.md, got: ${files}`);
console.log(' ✓ pre-existing reports stay out of the first fork commit');
assert.strictEqual(files, '.gitignore\nCLAUDE.md\nREADME.md', `commit must not hold the reports, got: ${files}`);
console.log(` ✓ ${NAME}`);
} catch (err) {
console.log(' ✗ pre-existing reports stay out of the first fork commit');
failed = 1;
console.log(` ✗ ${NAME}`);
console.log(` Error: ${err.message}`);
process.exitCode = 1;
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
console.log(`\nPassed: ${1 - failed}`);
console.log(`Failed: ${failed}`);
process.exitCode = failed;