|
|
@@ -8,18 +8,14 @@ import { commitAndPush, cloneRepo, SYNC_PATHS } from './git.js';
|
|
|
|
// --- helpers ---
|
|
|
|
// --- helpers ---
|
|
|
|
function makeTmpWorkspace() {
|
|
|
|
function makeTmpWorkspace() {
|
|
|
|
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
|
|
|
|
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
|
|
|
|
|
|
|
|
// Pre-create repo dir so clone branch is skipped
|
|
|
|
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
|
|
|
|
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
|
|
|
|
return ws;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function makeActionSource() {
|
|
|
|
|
|
|
|
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'git-source-'));
|
|
|
|
|
|
|
|
for (const relPath of SYNC_PATHS) {
|
|
|
|
for (const relPath of SYNC_PATHS) {
|
|
|
|
const fullPath = path.join(sourceRoot, relPath);
|
|
|
|
const fullPath = path.join(ws, relPath);
|
|
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
|
|
fs.writeFileSync(fullPath, relPath);
|
|
|
|
fs.writeFileSync(fullPath, relPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return sourceRoot;
|
|
|
|
return ws;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Default stub: all commands succeed, status returns changes
|
|
|
|
// Default stub: all commands succeed, status returns changes
|
|
|
@@ -39,12 +35,9 @@ function makeSpawn(overrides = {}) {
|
|
|
|
|
|
|
|
|
|
|
|
describe('commitAndPush', () => {
|
|
|
|
describe('commitAndPush', () => {
|
|
|
|
let workspace;
|
|
|
|
let workspace;
|
|
|
|
let sourceRoot;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
before(() => { workspace = makeTmpWorkspace(); });
|
|
|
|
before(() => { workspace = makeTmpWorkspace(); });
|
|
|
|
after(() => { fs.rmSync(workspace, { recursive: true, force: true }); });
|
|
|
|
after(() => { fs.rmSync(workspace, { recursive: true, force: true }); });
|
|
|
|
before(() => { sourceRoot = makeActionSource(); });
|
|
|
|
|
|
|
|
after(() => { fs.rmSync(sourceRoot, { recursive: true, force: true }); });
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
beforeEach(() => {
|
|
|
|
for (const f of fs.readdirSync(workspace)) {
|
|
|
|
for (const f of fs.readdirSync(workspace)) {
|
|
|
|
if (f.endsWith('.git-askpass.sh')) fs.unlinkSync(path.join(workspace, f));
|
|
|
|
if (f.endsWith('.git-askpass.sh')) fs.unlinkSync(path.join(workspace, f));
|
|
|
@@ -53,7 +46,7 @@ describe('commitAndPush', () => {
|
|
|
|
|
|
|
|
|
|
|
|
it('does not embed token in any git command argument', async () => {
|
|
|
|
it('does not embed token in any git command argument', async () => {
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
|
|
|
|
|
|
|
|
|
|
|
for (const { args } of spawn.calls) {
|
|
|
|
for (const { args } of spawn.calls) {
|
|
|
|
assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`);
|
|
|
|
assert.ok(!args.join(' ').includes('test-token'), `Token leaked in git args: ${args.join(' ')}`);
|
|
|
@@ -62,7 +55,7 @@ describe('commitAndPush', () => {
|
|
|
|
|
|
|
|
|
|
|
|
it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => {
|
|
|
|
it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => {
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
|
|
|
|
|
|
|
|
|
|
|
const networkOps = ['fetch', 'push', 'clone'];
|
|
|
|
const networkOps = ['fetch', 'push', 'clone'];
|
|
|
|
const networkCalls = spawn.calls.filter(c => networkOps.includes(c.args[0]));
|
|
|
|
const networkCalls = spawn.calls.filter(c => networkOps.includes(c.args[0]));
|
|
|
@@ -74,28 +67,28 @@ describe('commitAndPush', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('cleans up askpass script after successful run', async () => {
|
|
|
|
it('cleans up askpass script after successful run', async () => {
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), makeSpawn(), sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), makeSpawn());
|
|
|
|
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
|
|
|
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
|
|
|
assert.equal(leftover.length, 0, 'askpass script was not cleaned up');
|
|
|
|
assert.equal(leftover.length, 0, 'askpass script was not cleaned up');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('cleans up askpass script even when git fails', async () => {
|
|
|
|
it('cleans up askpass script even when git fails', async () => {
|
|
|
|
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
|
|
|
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn);
|
|
|
|
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
|
|
|
const leftover = fs.readdirSync(workspace).filter(f => f.endsWith('.git-askpass.sh'));
|
|
|
|
assert.equal(leftover.length, 0, 'askpass script was not cleaned up after failure');
|
|
|
|
assert.equal(leftover.length, 0, 'askpass script was not cleaned up after failure');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('skips commit when status shows no changes', async () => {
|
|
|
|
it('skips commit when status shows no changes', async () => {
|
|
|
|
const spawn = makeSpawn({ status: () => ({ status: 0, stdout: '', stderr: '', error: null }) });
|
|
|
|
const spawn = makeSpawn({ status: () => ({ status: 0, stdout: '', stderr: '', error: null }) });
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
|
|
|
const commitCalled = spawn.calls.some(c => c.args[0] === 'commit');
|
|
|
|
const commitCalled = spawn.calls.some(c => c.args[0] === 'commit');
|
|
|
|
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
|
|
|
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('adds skill and entry files together with findings', async () => {
|
|
|
|
it('adds skill and entry files together with findings', async () => {
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
|
|
|
const addCall = spawn.calls.find(c => c.args[0] === 'add');
|
|
|
|
const addCall = spawn.calls.find(c => c.args[0] === 'add');
|
|
|
|
assert.ok(addCall, 'expected git add to run');
|
|
|
|
assert.ok(addCall, 'expected git add to run');
|
|
|
|
assert.ok(addCall.args.includes('.github/skills/triage-findings/SKILL.md'));
|
|
|
|
assert.ok(addCall.args.includes('.github/skills/triage-findings/SKILL.md'));
|
|
|
@@ -109,13 +102,13 @@ describe('commitAndPush', () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('keeps repo copies when the source sync file is missing', async () => {
|
|
|
|
it('keeps repo copies when the source sync file is missing', async () => {
|
|
|
|
const missingPath = path.join(sourceRoot, '.amazonq/rules/triage-findings.md');
|
|
|
|
const missingPath = path.join(workspace, '.amazonq/rules/triage-findings.md');
|
|
|
|
fs.rmSync(missingPath, { force: true });
|
|
|
|
fs.rmSync(missingPath, { force: true });
|
|
|
|
const repoPath = path.join(workspace, 'repo', '.amazonq/rules/triage-findings.md');
|
|
|
|
const repoPath = path.join(workspace, 'repo', '.amazonq/rules/triage-findings.md');
|
|
|
|
fs.writeFileSync(repoPath, 'stale');
|
|
|
|
fs.writeFileSync(repoPath, 'stale');
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
const spawn = makeSpawn();
|
|
|
|
|
|
|
|
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
|
|
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
|
|
|
|
|
|
|
|
|
|
|
const rmCall = spawn.calls.find(c => c.args[0] === 'rm');
|
|
|
|
const rmCall = spawn.calls.find(c => c.args[0] === 'rm');
|
|
|
|
assert.equal(rmCall, undefined, 'git rm should not run for missing source files');
|
|
|
|
assert.equal(rmCall, undefined, 'git rm should not run for missing source files');
|
|
|
@@ -127,7 +120,7 @@ describe('commitAndPush', () => {
|
|
|
|
fs.writeFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'stale');
|
|
|
|
fs.writeFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'stale');
|
|
|
|
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'stale');
|
|
|
|
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'stale');
|
|
|
|
|
|
|
|
|
|
|
|
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
|
|
|
await commitAndPush(workspace, repoDir, makeSpawn());
|
|
|
|
|
|
|
|
|
|
|
|
assert.equal(fs.readFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'utf8'), '.github/skills/triage-findings/SKILL.md');
|
|
|
|
assert.equal(fs.readFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'utf8'), '.github/skills/triage-findings/SKILL.md');
|
|
|
|
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), 'CLAUDE.md');
|
|
|
|
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), 'CLAUDE.md');
|
|
|
@@ -135,7 +128,7 @@ describe('commitAndPush', () => {
|
|
|
|
|
|
|
|
|
|
|
|
it('does not throw when git command fails', async () => {
|
|
|
|
it('does not throw when git command fails', async () => {
|
|
|
|
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
|
|
|
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
|
|
|
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn, sourceRoot));
|
|
|
|
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|