Compare commits
7 Commits
v0.0.9-beta.1
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bf791a829 | |||
| c88c0d02c8 | |||
| f43ba63f0f | |||
| 4a29c4aaa3 | |||
| 78ec8f6d6a | |||
| 5c5773e4fd | |||
| ece7377fc8 |
@@ -6,8 +6,8 @@
|
||||
|
||||
1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request
|
||||
2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議)
|
||||
3. 讀取所有未解決的舊問題(問題檔案 `.gitea/ai-review/findings.json` 存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案
|
||||
4. 讀取排除問題檔案(`.gitea/ai-review/exclusions.json` 存在於使用此 Action 的專案固定位置),用來過濾PR問題表格中不需要處理的問題
|
||||
3. 讀取來源分支中的所有未解決舊問題(問題檔案 `.gitea/ai-review/findings.json`)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案
|
||||
4. 讀取來源分支中的排除問題檔案(`.gitea/ai-review/exclusions.json`),用來過濾PR問題表格中不需要處理的問題
|
||||
5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request
|
||||
6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request
|
||||
7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request
|
||||
@@ -23,7 +23,7 @@
|
||||
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
||||
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
||||
7. 讀取 Git Diff 時排除 `.gitea/`、`.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/` 資料夾,以及 `CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
|
||||
8. 階段七驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||
8. 階段七驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
||||
|
||||
# 使用說明
|
||||
@@ -227,4 +227,4 @@ Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||
|
||||
### 版本包含
|
||||
|
||||
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
||||
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。`findings.json` 與 `exclusions.json` 都從使用此 action 的存取庫來源分支讀取,而不是從 action 本地 workspace 讀取。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
||||
|
||||
+9
-3
@@ -16,13 +16,19 @@ function buildTable(findings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入 findings.json 到 workspace
|
||||
* 寫入 findings.json。
|
||||
* 預設寫到 workspace;若提供 mirrorDir,則同步寫入另一份供 repo commit 使用。
|
||||
*/
|
||||
export function saveFindings(workspace, findings) {
|
||||
const fullPath = path.join(workspace, FINDINGS_PATH);
|
||||
export function saveFindings(workspace, findings, mirrorDir = null) {
|
||||
const targets = [workspace];
|
||||
if (mirrorDir && mirrorDir !== workspace) targets.push(mirrorDir);
|
||||
|
||||
for (const targetDir of targets) {
|
||||
const fullPath = path.join(targetDir, FINDINGS_PATH);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2) + '\n', 'utf8');
|
||||
console.log(` ✅ findings 寫入: ${fullPath} (${findings.length} 筆)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { saveFindings } from './comments.js';
|
||||
import { FINDINGS_PATH } from './config.js';
|
||||
|
||||
describe('saveFindings', () => {
|
||||
const tempDirs = [];
|
||||
const makeTempDir = prefix => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
};
|
||||
|
||||
it('writes findings to workspace and mirror dirs when provided', () => {
|
||||
const workspace = makeTempDir('findings-ws-');
|
||||
const mirrorDir = makeTempDir('findings-mirror-');
|
||||
const findings = [{ level: 'warning', role: 'Leo', location: 'file.js:1', suggestion: 'test' }];
|
||||
|
||||
saveFindings(workspace, findings, mirrorDir);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
const mirrorText = fs.readFileSync(path.join(mirrorDir, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n');
|
||||
assert.equal(mirrorText, JSON.stringify(findings, null, 2) + '\n');
|
||||
});
|
||||
|
||||
it('writes only to workspace when mirrorDir is omitted', () => {
|
||||
const workspace = makeTempDir('findings-ws-');
|
||||
const findings = [{ level: 'info', role: 'Maya', location: 'file.js:2', suggestion: 'note' }];
|
||||
|
||||
saveFindings(workspace, findings);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, JSON.stringify(findings, null, 2) + '\n');
|
||||
});
|
||||
|
||||
it('does not duplicate writes when mirrorDir matches workspace', () => {
|
||||
const workspace = makeTempDir('findings-same-');
|
||||
const findings = [];
|
||||
const writeCalls = [];
|
||||
const originalWriteFileSync = fs.writeFileSync;
|
||||
|
||||
fs.writeFileSync = (...args) => {
|
||||
writeCalls.push(args[0]);
|
||||
return originalWriteFileSync(...args);
|
||||
};
|
||||
|
||||
try {
|
||||
saveFindings(workspace, findings, workspace);
|
||||
} finally {
|
||||
fs.writeFileSync = originalWriteFileSync;
|
||||
}
|
||||
|
||||
assert.equal(writeCalls.length, 1);
|
||||
assert.equal(writeCalls[0], path.join(workspace, FINDINGS_PATH));
|
||||
});
|
||||
|
||||
it('writes an empty JSON array when findings is empty', () => {
|
||||
const workspace = makeTempDir('findings-empty-');
|
||||
|
||||
saveFindings(workspace, []);
|
||||
|
||||
const workspaceText = fs.readFileSync(path.join(workspace, FINDINGS_PATH), 'utf8');
|
||||
assert.equal(workspaceText, '[]\n');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
+23
-3
@@ -34,8 +34,14 @@ function readJSONArray(fullPath, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExclusions(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.excluded_findings)) return data.excluded_findings;
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取舊 findings(從 workspace 的 FINDINGS_PATH)
|
||||
* 讀取舊 findings(從來源分支的 cloned repoDir 中的 FINDINGS_PATH)
|
||||
*/
|
||||
export function loadOldFindings(workspace) {
|
||||
const old = readJSONArray(path.join(workspace, FINDINGS_PATH), '舊 findings ').map(f => ({ ...f, is_new: false }));
|
||||
@@ -104,10 +110,24 @@ export async function deduplicateWithAI(findings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH)
|
||||
* 讀取排除問題檔案(從來源分支的 cloned repoDir 中的 EXCLUSIONS_PATH)
|
||||
*/
|
||||
export function loadExclusions(workspace) {
|
||||
const exclusions = readJSONArray(path.join(workspace, EXCLUSIONS_PATH), '排除問題');
|
||||
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(' 排除問題檔案不存在,視為空');
|
||||
console.log(' 讀取排除問題: 0 筆');
|
||||
return [];
|
||||
}
|
||||
|
||||
let exclusions = [];
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
exclusions = normalizeExclusions(data);
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ 讀取排除問題失敗: ${e.message},視為空`);
|
||||
exclusions = [];
|
||||
}
|
||||
console.log(` 讀取排除問題: ${exclusions.length} 筆`);
|
||||
return exclusions;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { loadExclusions, applyExclusions } from './findings.js';
|
||||
import { EXCLUSIONS_PATH } from './config.js';
|
||||
|
||||
describe('findings exclusions', () => {
|
||||
let workspace;
|
||||
|
||||
beforeEach(() => {
|
||||
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'findings-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads excluded_findings wrapper format', () => {
|
||||
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, JSON.stringify({
|
||||
excluded_findings: [
|
||||
{ location: 'entrypoint.sh:180', title: 'fetch_package_versions jq overhead' },
|
||||
],
|
||||
}, null, 2));
|
||||
|
||||
const exclusions = loadExclusions(workspace);
|
||||
|
||||
assert.equal(exclusions.length, 1);
|
||||
assert.equal(exclusions[0].location, 'entrypoint.sh:180');
|
||||
assert.equal(exclusions[0].title, 'fetch_package_versions jq overhead');
|
||||
});
|
||||
|
||||
it('applies exclusions loaded from wrapper format', () => {
|
||||
const findings = [
|
||||
{ location: 'entrypoint.sh:180', role: 'Maya', suggestion: 'keep' },
|
||||
{ location: 'README.md:12', role: 'Maya', suggestion: 'keep' },
|
||||
];
|
||||
const exclusions = [
|
||||
{ location: 'entrypoint.sh:180', title: 'fetch_package_versions jq overhead' },
|
||||
];
|
||||
|
||||
const filtered = applyExclusions(findings, exclusions);
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].location, 'README.md:12');
|
||||
});
|
||||
});
|
||||
+19
-1
@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
|
||||
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
|
||||
|
||||
const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
|
||||
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
|
||||
export const SYNC_PATHS = [
|
||||
FINDINGS_PATH,
|
||||
'.amazonq/rules/triage-findings.md',
|
||||
'.codex/skills/triage-findings/SKILL.md',
|
||||
'.codex/skills/triage-findings/agents/openai.yaml',
|
||||
@@ -68,6 +68,10 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
||||
await withAskpass(workspace, async credEnv => {
|
||||
run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir);
|
||||
run(['config', 'user.name', 'AI Review Bot'], repoDir);
|
||||
if (PR_HEAD_BRANCH) {
|
||||
run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv);
|
||||
run(['reset', '--hard', `origin/${PR_HEAD_BRANCH}`], repoDir);
|
||||
}
|
||||
|
||||
const existingSyncPaths = [];
|
||||
|
||||
@@ -86,6 +90,16 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
||||
if (existingSyncPaths.length > 0) {
|
||||
run(['add', ...existingSyncPaths], repoDir);
|
||||
}
|
||||
const generatedSyncPaths = GENERATED_SYNC_PATHS.filter(relPath => fs.existsSync(path.join(workspace, relPath)));
|
||||
if (generatedSyncPaths.length > 0) {
|
||||
for (const relPath of generatedSyncPaths) {
|
||||
const src = path.join(workspace, relPath);
|
||||
const dest = path.join(repoDir, relPath);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
run(['add', ...generatedSyncPaths], repoDir);
|
||||
}
|
||||
|
||||
const status = run(['status', '--porcelain'], repoDir);
|
||||
if (!status) {
|
||||
@@ -95,8 +109,12 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
||||
|
||||
const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir);
|
||||
const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown';
|
||||
try {
|
||||
run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv);
|
||||
console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`);
|
||||
} catch (pushErr) {
|
||||
console.log(` ⚠️ Step7 commit 成功但 push 失敗: commit=${commitHash} push=${PR_HEAD_BRANCH} error=${pushErr.message}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`);
|
||||
|
||||
+50
-13
@@ -94,20 +94,31 @@ describe('commitAndPush', () => {
|
||||
});
|
||||
|
||||
it('adds skill and entry files together with findings', async () => {
|
||||
const repoDir = path.join(workspace, 'repo');
|
||||
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
fs.mkdirSync(path.join(repoDir, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
const spawn = makeSpawn();
|
||||
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
|
||||
const addCall = spawn.calls.find(c => c.args[0] === 'add');
|
||||
assert.ok(addCall, 'expected git add to run');
|
||||
assert.ok(addCall.args.includes('.github/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(addCall.args.includes('.codex/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(addCall.args.includes('.codex/skills/triage-findings/agents/openai.yaml'));
|
||||
assert.ok(addCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(addCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(addCall.args.includes('.github/copilot-instructions.md'));
|
||||
assert.ok(addCall.args.includes('.amazonq/rules/triage-findings.md'));
|
||||
assert.ok(addCall.args.includes('CLAUDE.md'));
|
||||
assert.ok(addCall.args.includes('GEMINI.md'));
|
||||
assert.ok(!addCall.args.includes('README.md'));
|
||||
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
||||
const addCalls = spawn.calls.filter(c => c.args[0] === 'add');
|
||||
const skillAddCall = addCalls.find(c => c.args.includes('.github/skills/triage-findings/SKILL.md'));
|
||||
const generatedAddCall = addCalls.find(c => c.args.includes('.gitea/ai-review/exclusions.json'));
|
||||
assert.ok(skillAddCall, 'expected git add for synced skill files');
|
||||
assert.ok(generatedAddCall, 'expected git add for generated review files');
|
||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(skillAddCall.args.includes('.codex/skills/triage-findings/agents/openai.yaml'));
|
||||
assert.ok(skillAddCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(skillAddCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
||||
assert.ok(skillAddCall.args.includes('.github/copilot-instructions.md'));
|
||||
assert.ok(skillAddCall.args.includes('.amazonq/rules/triage-findings.md'));
|
||||
assert.ok(skillAddCall.args.includes('CLAUDE.md'));
|
||||
assert.ok(skillAddCall.args.includes('GEMINI.md'));
|
||||
assert.ok(!skillAddCall.args.includes('README.md'));
|
||||
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/findings.json'));
|
||||
assert.ok(generatedAddCall.args.includes('.gitea/ai-review/exclusions.json'));
|
||||
});
|
||||
|
||||
it('keeps repo copies when the source sync file is missing', async () => {
|
||||
@@ -139,6 +150,32 @@ describe('commitAndPush', () => {
|
||||
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
||||
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn, sourceRoot));
|
||||
});
|
||||
|
||||
it('logs push failures separately from commit failures', async () => {
|
||||
const repoDir = path.join(workspace, 'repo');
|
||||
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
fs.mkdirSync(path.join(repoDir, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
|
||||
const spawn = makeSpawn({
|
||||
push: () => ({ status: 1, stdout: '', stderr: 'remote: error: pre-receive hook declined', error: null }),
|
||||
});
|
||||
const logs = [];
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => { logs.push(args.join(' ')); };
|
||||
|
||||
try {
|
||||
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
||||
assert.ok(logs.some(line => line.includes('Step7 commit 成功但 push 失敗')));
|
||||
assert.ok(logs.some(line => line.includes('pre-receive hook declined')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneRepo', () => {
|
||||
|
||||
+4
-3
@@ -88,7 +88,8 @@ async function main() {
|
||||
|
||||
// Step6: 寫入 findings.json,依序發布 comment
|
||||
console.log('\n📝 Step5: Findings 寫入與 Comment 發布');
|
||||
saveFindings(WORKSPACE, filtered);
|
||||
const reviewDir = repoDir || WORKSPACE;
|
||||
saveFindings(WORKSPACE, filtered, reviewDir);
|
||||
try {
|
||||
await postOldFindingsComment(filtered);
|
||||
await postNewNonCriticalComment(filtered);
|
||||
@@ -102,7 +103,7 @@ async function main() {
|
||||
console.log('\n🔎 Step6: JSON 格式驗證');
|
||||
const missingPaths = [];
|
||||
for (const relPath of [FINDINGS_PATH, EXCLUSIONS_PATH]) {
|
||||
const fullPath = path.join(repoDir || WORKSPACE, relPath);
|
||||
const fullPath = path.join(reviewDir, relPath);
|
||||
try {
|
||||
const result = await validateJSONArrayFile(fullPath, relPath);
|
||||
if (!result.exists) missingPaths.push({ fullPath, relPath });
|
||||
@@ -117,7 +118,7 @@ async function main() {
|
||||
|
||||
// Step7: commit/push findings.json 到來源分支
|
||||
console.log('\n💾 Step7: 記憶區 Commit/Push');
|
||||
await commitAndPush(WORKSPACE, repoDir);
|
||||
await commitAndPush(WORKSPACE, repoDir || WORKSPACE);
|
||||
|
||||
// Step9: 有 critical 問題則 exit 1
|
||||
console.log('\n🚦 Step8: 嚴重問題檢查');
|
||||
|
||||
Reference in New Issue
Block a user