Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5afe8a2119 | |||
| 09584f4f93 | |||
| ed061f85ce | |||
| b4c54124ec | |||
| b51ab78a5e | |||
| 1129f37384 |
@@ -154,6 +154,11 @@
|
|||||||
"location": "app/main.js",
|
"location": "app/main.js",
|
||||||
"suggestion": "main.js 中的 Step 標題註解為 pipeline 流程說明,非待整理的 TODO,不需要轉換為具體任務"
|
"suggestion": "main.js 中的 Step 標題註解為 pipeline 流程說明,非待整理的 TODO,不需要轉換為具體任務"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"role": "Maya",
|
||||||
|
"location": "app/log.test.js",
|
||||||
|
"suggestion": "`log.test.js` 的新增非常棒,提供了良好的覆蓋率。為了進一步提升測試的完整性,建議考慮為 `line`, `ok`, `warn`, `error` 函數新增測試案例,以驗證當傳入空字串時的行為。雖然這些函數的行為相對簡單,但測試空字串可以確保邊界情況下的輸出符合預期。"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "Rex",
|
"role": "Rex",
|
||||||
"location": "app/package.json",
|
"location": "app/package.json",
|
||||||
|
|||||||
@@ -1,16 +1 @@
|
|||||||
[
|
[]
|
||||||
{
|
|
||||||
"level": "info",
|
|
||||||
"role": "Maya",
|
|
||||||
"location": "app/log.js",
|
|
||||||
"suggestion": "log.js 檔案中的 ok, warn, error 函數是應用程式的日誌工具。雖然功能簡單,但為這些工具函數編寫單元測試是一個良好的實踐,以確保它們正確地呼叫 console 對應的方法(如 console.log, console.warn, console.error)並輸出預期的格式。這有助於防止未來意外的行為變更。",
|
|
||||||
"is_new": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "info",
|
|
||||||
"role": "Maya",
|
|
||||||
"location": "app/log.test.js",
|
|
||||||
"suggestion": "`log.test.js` 的新增非常棒,提供了良好的覆蓋率。為了進一步提升測試的完整性,建議考慮為 `line`, `ok`, `warn`, `error` 函數新增測試案例,以驗證當傳入空字串時的行為。雖然這些函數的行為相對簡單,但測試空字串可以確保邊界情況下的輸出符合預期。",
|
|
||||||
"is_new": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
+60
-10
@@ -20,6 +20,17 @@ export const SYNC_PATHS = [
|
|||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
'GEMINI.md',
|
'GEMINI.md',
|
||||||
];
|
];
|
||||||
|
const FORCE_SYNC_FILE_PATHS = [
|
||||||
|
'.github/copilot-instructions.md',
|
||||||
|
'CLAUDE.md',
|
||||||
|
'GEMINI.md',
|
||||||
|
];
|
||||||
|
const SYNC_TREE_PATHS = [
|
||||||
|
'.codex/skills/triage-findings',
|
||||||
|
'.claude/skills/triage-findings',
|
||||||
|
'.gemini/skills/triage-findings',
|
||||||
|
'.github/skills/triage-findings',
|
||||||
|
];
|
||||||
|
|
||||||
function makeRunner(spawn) {
|
function makeRunner(spawn) {
|
||||||
return function run(args, cwd, env) {
|
return function run(args, cwd, env) {
|
||||||
@@ -51,6 +62,35 @@ function readGitOutput(run, args, cwd, env) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyTree(sourceRoot, repoDir, relDir) {
|
||||||
|
const srcDir = path.join(sourceRoot, relDir);
|
||||||
|
if (!fs.existsSync(srcDir)) return [];
|
||||||
|
|
||||||
|
const copied = [];
|
||||||
|
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
||||||
|
const relPath = path.join(relDir, entry.name);
|
||||||
|
const src = path.join(sourceRoot, relPath);
|
||||||
|
const dest = path.join(repoDir, relPath);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
copied.push(...copyTree(sourceRoot, repoDir, relPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
copied.push(relPath);
|
||||||
|
}
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFileOverwrite(sourceRoot, repoDir, relPath) {
|
||||||
|
const src = path.join(sourceRoot, relPath);
|
||||||
|
if (!fs.existsSync(src)) return null;
|
||||||
|
const dest = path.join(repoDir, relPath);
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
|
||||||
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
||||||
const run = makeRunner(_spawnSync);
|
const run = makeRunner(_spawnSync);
|
||||||
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
const headSha = readGitOutput(run, ['rev-parse', 'HEAD'], repoDir);
|
||||||
@@ -101,21 +141,31 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
|||||||
run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
|
run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSyncPaths = [];
|
const existingSyncPaths = new Set();
|
||||||
|
|
||||||
// Copy action skill files into the target repo. Existing files are overwritten;
|
// Copy action skill trees into the target repo. Existing files are overwritten;
|
||||||
// missing source files are ignored so we do not delete target repo content.
|
// missing source files are ignored so we do not delete target repo content.
|
||||||
for (const relPath of SYNC_PATHS) {
|
for (const relDir of SYNC_TREE_PATHS) {
|
||||||
const src = path.join(sourceRoot, relPath);
|
for (const relPath of copyTree(sourceRoot, repoDir, relDir)) {
|
||||||
const dest = path.join(repoDir, relPath);
|
existingSyncPaths.add(relPath);
|
||||||
if (fs.existsSync(src)) {
|
|
||||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
existingSyncPaths.push(relPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingSyncPaths.length > 0) {
|
// Force overwrite the direct instruction files first so the target repo always
|
||||||
|
// receives the action-owned versions even if the repo has drifted.
|
||||||
|
for (const relPath of FORCE_SYNC_FILE_PATHS) {
|
||||||
|
const copied = copyFileOverwrite(sourceRoot, repoDir, relPath);
|
||||||
|
if (copied) existingSyncPaths.add(copied);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy standalone action files into the target repo. Existing files are overwritten.
|
||||||
|
for (const relPath of SYNC_PATHS) {
|
||||||
|
if (FORCE_SYNC_FILE_PATHS.includes(relPath)) continue;
|
||||||
|
const copied = copyFileOverwrite(sourceRoot, repoDir, relPath);
|
||||||
|
if (copied) existingSyncPaths.add(copied);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingSyncPaths.size > 0) {
|
||||||
run(['add', ...existingSyncPaths], repoDir);
|
run(['add', ...existingSyncPaths], repoDir);
|
||||||
}
|
}
|
||||||
const generatedSyncPaths = GENERATED_SYNC_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
|
const generatedSyncPaths = GENERATED_SYNC_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
|
||||||
|
|||||||
@@ -159,11 +159,30 @@ describe('commitAndPush', () => {
|
|||||||
const repoDir = path.join(workspace, 'repo');
|
const repoDir = path.join(workspace, 'repo');
|
||||||
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');
|
||||||
|
fs.writeFileSync(path.join(repoDir, 'GEMINI.md'), 'stale');
|
||||||
|
fs.writeFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'stale');
|
||||||
|
|
||||||
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
||||||
|
|
||||||
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');
|
||||||
|
assert.equal(fs.readFileSync(path.join(repoDir, 'GEMINI.md'), 'utf8'), 'GEMINI.md');
|
||||||
|
assert.equal(fs.readFileSync(path.join(repoDir, '.github/copilot-instructions.md'), 'utf8'), '.github/copilot-instructions.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recursively overwrites skill tree files from the action source', async () => {
|
||||||
|
const repoDir = path.join(workspace, 'repo');
|
||||||
|
const nestedRelPath = '.codex/skills/triage-findings/assets/example.txt';
|
||||||
|
const sourceNestedPath = path.join(sourceRoot, nestedRelPath);
|
||||||
|
const repoNestedPath = path.join(repoDir, nestedRelPath);
|
||||||
|
fs.mkdirSync(path.dirname(sourceNestedPath), { recursive: true });
|
||||||
|
fs.writeFileSync(sourceNestedPath, 'fresh');
|
||||||
|
fs.mkdirSync(path.dirname(repoNestedPath), { recursive: true });
|
||||||
|
fs.writeFileSync(repoNestedPath, 'stale');
|
||||||
|
|
||||||
|
await commitAndPush(workspace, repoDir, makeSpawn(), sourceRoot);
|
||||||
|
|
||||||
|
assert.equal(fs.readFileSync(repoNestedPath, 'utf8'), 'fresh');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw when git command fails', async () => {
|
it('does not throw when git command fails', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user