From a1e84ca67a41c10d030c5469e5cda26ebc4d8f85 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:53:51 -0400 Subject: [PATCH] fix(opensource): permit only untracked reports at publish gate --- manifests/context-packs/skill-triggers@1.json | 2 +- skills/opensource-pipeline/SKILL.md | 9 +++++++-- tests/skills/opensource-forker-history.test.js | 18 ++++++++++++++++++ .../opensource-pipeline-publish-guard.test.js | 11 ++++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/manifests/context-packs/skill-triggers@1.json b/manifests/context-packs/skill-triggers@1.json index cf1ef48de..34be199f4 100644 --- a/manifests/context-packs/skill-triggers@1.json +++ b/manifests/context-packs/skill-triggers@1.json @@ -10,7 +10,7 @@ "id": "hand-seeded", "source": "manual-curation-pending-regeneration" }, - "registryDigest": "4fc25981407c964d970cb203db86c094524ee0da4abd3dafe6cf13c6e386b92a", + "registryDigest": "846ac8826dd91e81cd838c2b55d7be536af427eb453efa127501f891e5778ff8", "schemaVersion": 1, "triggers": { "skill:agent-runtime-gateway-smoke-test": [ diff --git a/skills/opensource-pipeline/SKILL.md b/skills/opensource-pipeline/SKILL.md index 47dd03644..3be84b1f4 100644 --- a/skills/opensource-pipeline/SKILL.md +++ b/skills/opensource-pipeline/SKILL.md @@ -229,11 +229,16 @@ Proceed with GitHub creation? (yes/no/review first) Push only the commit the sanitizer passed. If the tree or the commit changed since Step 5b (for example the user edited files after "review first" and amended), go back to Step 5b instead of -pushing. +pushing. The two generated reports may remain untracked even when a source `.gitignore` negates +their names. Allow only those two untracked files; a report in the index or scanned commit, or any +other worktree change, still stops publication. ```bash cd "{STAGING_PATH}" -test -z "$(git status --porcelain)" && test "$(git rev-list --count HEAD)" = 1 \ +test -z "$(git ls-files -- FORK_REPORT.md SANITIZATION_REPORT.md)" \ + && test -z "$(git ls-tree -r --name-only HEAD -- FORK_REPORT.md SANITIZATION_REPORT.md)" \ + && test -z "$(git status --porcelain --untracked-files=all -- . ':(exclude)FORK_REPORT.md' ':(exclude)SANITIZATION_REPORT.md')" \ + && test "$(git rev-list --count HEAD)" = 1 \ && test "$(git rev-parse HEAD)" = "{SCANNED_COMMIT}" \ || { echo "Uncommitted changes, extra commits, or HEAD is not the scanned commit: return to Step 5b" >&2; exit 1; } gh repo create "{github_org}/{github_repo}" --public --source=. --push --description "{description}" diff --git a/tests/skills/opensource-forker-history.test.js b/tests/skills/opensource-forker-history.test.js index 5339309d9..b8ac8a984 100644 --- a/tests/skills/opensource-forker-history.test.js +++ b/tests/skills/opensource-forker-history.test.js @@ -51,6 +51,7 @@ if (process.platform === 'win32') { 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 publishBlock = bashBlock('skills/opensource-pipeline/SKILL.md', '#### Step 7: GitHub Publish'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'oss-forker-')); let failed = 0; @@ -68,6 +69,23 @@ try { const files = spawnSync('git', ['ls-tree', '-r', '--name-only', 'HEAD'], { cwd: dir, encoding: 'utf8' }).stdout.trim(); assert.strictEqual(files, '.gitignore\nCLAUDE.md\nREADME.md', `commit must not hold the reports, got: ${files}`); + const scanned = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf8' }).stdout.trim(); + const bin = path.join(dir, '.git', 'test-bin'); + const published = path.join(dir, '.git', 'published-marker'); + fs.mkdirSync(bin); + fs.writeFileSync(path.join(bin, 'gh'), `#!/bin/sh\ntouch "${published}"\n`, { mode: 0o755 }); + const publish = publishBlock + .replaceAll('{STAGING_PATH}', dir) + .replaceAll('{SCANNED_COMMIT}', scanned); + const env = { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}` }; + const clean = spawnSync('bash', ['-ec', publish], { cwd: dir, encoding: 'utf8', env }); + assert.strictEqual(clean.status, 0, clean.stderr); + assert.ok(fs.existsSync(published), 'clean scanned commit with only untracked reports should publish'); + fs.rmSync(published); + fs.writeFileSync(path.join(dir, 'extra.txt'), 'unreviewed\n'); + const dirty = spawnSync('bash', ['-ec', publish], { cwd: dir, encoding: 'utf8', env }); + assert.notStrictEqual(dirty.status, 0, 'other untracked files must block publication'); + assert.ok(!fs.existsSync(published), 'dirty tree must not invoke gh'); console.log(` ✓ ${NAME}`); } catch (err) { failed = 1; diff --git a/tests/skills/opensource-pipeline-publish-guard.test.js b/tests/skills/opensource-pipeline-publish-guard.test.js index a91e46b79..7c96411d1 100644 --- a/tests/skills/opensource-pipeline-publish-guard.test.js +++ b/tests/skills/opensource-pipeline-publish-guard.test.js @@ -44,7 +44,7 @@ function git(cwd, ...args) { } // One-commit staging repo; returns the commit the sanitizer "scanned". -function withStaging(fn) { +function withStaging(fn, { committedReport = false } = {}) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'oss-publish-')); try { const staging = path.join(root, 'staging'); @@ -56,6 +56,7 @@ function withStaging(fn) { git(staging, 'config', 'user.email', 'test@example.com'); git(staging, 'config', 'user.name', 'test'); fs.writeFileSync(path.join(staging, 'README.md'), 'clean\n'); + if (committedReport) fs.writeFileSync(path.join(staging, 'FORK_REPORT.md'), 'must not publish\n'); git(staging, 'add', '-A'); git(staging, 'commit', '-q', '-m', 'init'); const scanned = git(staging, 'rev-parse', 'HEAD'); @@ -106,6 +107,14 @@ function runTests() { }); })) passed++; else failed++; + if (test('refuses to publish a commit containing a report', () => { + withStaging(({ publish }) => { + const r = publish(); + assert.notStrictEqual(r.status, 0); + assert.ok(!r.published, 'gh repo create ran with a tracked report'); + }, { committedReport: true }); + })) passed++; else failed++; + console.log(`\nPassed: ${passed}`); console.log(`Failed: ${failed}`); process.exitCode = failed > 0 ? 1 : 0;