mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(ci): reduce redundant Actions runs and disable PR notifications
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { detectIssueCommand, handleIssueCommand } from "./issue-commands.mjs";
|
||||
import { writeContributorSnapshot } from "../../docs/scripts/contributor-snapshot.mjs";
|
||||
|
||||
function workflow(name) {
|
||||
return readFileSync(new URL(`../workflows/${name}.yml`, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
function job(content, name) {
|
||||
const jobs = content.slice(content.indexOf("\njobs:\n") + 7);
|
||||
const definitions = [...jobs.matchAll(/^ ([\w-]+):\s*$/gm)];
|
||||
const index = definitions.findIndex((match) => match[1] === name);
|
||||
assert.ok(index >= 0, `missing job ${name}`);
|
||||
return jobs.slice(definitions[index].index, definitions[index + 1]?.index ?? jobs.length);
|
||||
}
|
||||
|
||||
function evaluateCondition(content, github) {
|
||||
const folded = content.match(/^ if: >-\n((?: [^\n]*\n)+)/m);
|
||||
const expression = (folded?.[1] ?? content.match(/^ if: (.+)$/m)?.[1])?.trim();
|
||||
assert.ok(expression, "missing job condition");
|
||||
const evaluate = new Function("github", "contains", `return (${expression.replace(/^\$\{\{\s*|\s*\}\}$/g, "")});`);
|
||||
return evaluate(github, (search, item) => String(search ?? "").toLowerCase().includes(String(item).toLowerCase()));
|
||||
}
|
||||
|
||||
function commentEvent(body, { userType = "User", login = "contributor", pullRequest = false } = {}) {
|
||||
return { repository_owner: "t8y2", event: {
|
||||
issue: { pull_request: pullRequest ? { url: "https://api.github.com/repos/t8y2/dbx/pulls/123" } : undefined },
|
||||
comment: { body, user: { type: userType, login } },
|
||||
} };
|
||||
}
|
||||
|
||||
test("issue command prefilter admits every supported command and skips unrelated comments", async () => {
|
||||
const content = job(workflow("issue-claim"), "claim");
|
||||
for (const body of ["/claim", "please /claim this", "\t/claim\n", "/unclaim", " /unclaimed\n", "\t/close\r\n"]) {
|
||||
assert.ok(detectIssueCommand(body), body);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), true, body);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body, { userType: "Bot" })), false, body);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body, { pullRequest: true })), false, body);
|
||||
}
|
||||
for (const body of ["Thanks!", "Fixed in abc123", "", null]) {
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), false);
|
||||
}
|
||||
for (const body of ["/claimable", "please /close", "/unclaim more", "/CLAIM"]) {
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), true);
|
||||
assert.deepEqual(await handleIssueCommand({ commentBody: body, commentUserType: "User", client: {} }), { status: "ignored" });
|
||||
}
|
||||
});
|
||||
|
||||
test("spam prefilter retains all existing malicious-link cases and never bypasses the matcher", () => {
|
||||
const content = job(workflow("spam-comment-guard"), "delete-malicious-link");
|
||||
const pattern = content.match(/grep -qiP '([^']+)'/)[1];
|
||||
const matcher = new RegExp(pattern.replaceAll("[:space:]", "\\s"), "i");
|
||||
for (const body of ["https://evil.example/update.zip", "https://evil.example/update.ZIP?download=1",
|
||||
"[update.zip](//evil.example/download)", "[update.ZIP](https://evil%2Eexample/download)",
|
||||
"[update.7z](https://evil.example/download)", "[update.exe](https://evil.example/download)",
|
||||
"[update.SCR](//evil.example/download)"]) {
|
||||
assert.equal(matcher.test(body), true, body);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), true, body);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body, { userType: "Bot" })), false);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body, { login: "t8y2" })), false);
|
||||
assert.equal(evaluateCondition(content, commentEvent(body, { pullRequest: true })), true);
|
||||
}
|
||||
for (const body of ["Thanks!", "https://example.com/docs", "", null]) {
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), false);
|
||||
}
|
||||
for (const body of ["Please upload a .zip", "https://github.com/t8y2/dbx/archive/main.zip",
|
||||
"https://user-images.githubusercontent.com/files/logs.zip", "[logs.exe](https://github.com/user-attachments/files/123/logs.exe)",
|
||||
"[logs.7z](//raw.githubusercontent.com/t8y2/dbx/main/logs.7z)"]) {
|
||||
assert.equal(evaluateCondition(content, commentEvent(body)), true, body);
|
||||
assert.equal(matcher.test(body), false, body);
|
||||
}
|
||||
});
|
||||
|
||||
test("PR notification automation stays disabled while its configuration is retained", () => {
|
||||
const content = workflow("notify");
|
||||
assert.match(content, /# pull_request_target:\s+#\s+types: \[closed\]/);
|
||||
assert.match(content, /^ workflow_dispatch:/m);
|
||||
assert.doesNotMatch(content, /^ (?:pull_request_target|pull_request|issues|release|push|schedule):/m);
|
||||
assert.equal(evaluateCondition(job(content, "notify"), { event: { pull_request: { merged: true } } }), true);
|
||||
assert.equal(evaluateCondition(job(content, "notify"), { event: { pull_request: { merged: false } } }), false);
|
||||
assert.equal(evaluateCondition(job(content, "notify"), { event: { pull_request: {} } }), false);
|
||||
});
|
||||
|
||||
test("plugin checks run once before unchanged cross-platform verification", () => {
|
||||
const content = workflow("plugin-dev-host");
|
||||
const checks = job(content, "checks");
|
||||
const tests = job(content, "test");
|
||||
assert.match(checks, /runs-on: ubuntu-24\.04/);
|
||||
assert.match(checks, /npm run lint --prefix plugins\/sdk\/dev-host/);
|
||||
assert.match(checks, /npm run fmt:check --prefix plugins\/sdk\/dev-host/);
|
||||
assert.doesNotMatch(checks, /cargo|setup-go|rust-toolchain/);
|
||||
assert.match(tests, /needs: checks/);
|
||||
assert.match(tests, /os: \[ubuntu-24\.04, macos-15, windows-2022\]/);
|
||||
assert.doesNotMatch(tests, /npm run (?:lint|fmt:check)/);
|
||||
for (const command of ["npm test --prefix plugins/sdk/dev-host", "cargo nextest run", "cargo test --doc",
|
||||
"npm test --prefix packages/plugin-cli", "node scripts/verify-plugin-cli-package.mjs", "DBX_PLUGIN_CLI_VERIFY_NATIVE: '1'"]) {
|
||||
assert.ok(tests.includes(command), command);
|
||||
}
|
||||
assert.match(content, /group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.ref \}\}\s+cancel-in-progress: true/);
|
||||
});
|
||||
|
||||
test("contributor refresh is weekly and remains manually available", () => {
|
||||
const content = workflow("contributors");
|
||||
assert.match(content, /cron: "0 18 \* \* 0"/);
|
||||
assert.match(content, /workflow_dispatch:/);
|
||||
});
|
||||
|
||||
test("GitHub script selection excludes planner tests only when fast checks own them", () => {
|
||||
const content = job(workflow("ci"), "github-scripts");
|
||||
const script = content.match(/run: \|\n((?: [^\n]*\n)+)/)[1].replace(/^ /gm, "");
|
||||
const repository = new URL("../../", import.meta.url);
|
||||
const files = readdirSync(new URL(".github/scripts/", repository)).filter((file) => file.endsWith(".test.mjs"));
|
||||
for (const fast of ["true", "false", ""]) {
|
||||
const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", `node() { printf '%s\\n' "$@"; }\n${script}`], {
|
||||
cwd: repository, encoding: "utf8", env: { ...process.env, FAST_CHECKS: fast },
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const selected = result.stdout.trim().split("\n").slice(1).sort();
|
||||
const expected = files.filter((file) => fast !== "true" || !file.startsWith("ci-")).map((file) => `.github/scripts/${file}`).sort();
|
||||
assert.deepEqual(selected, expected);
|
||||
assert.ok(selected.includes(".github/scripts/actions-efficiency.test.mjs"));
|
||||
}
|
||||
});
|
||||
|
||||
async function snapshotFixture(context) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "dbx-contributor-snapshot-"));
|
||||
context.after(() => rm(directory, { recursive: true, force: true }));
|
||||
return join(directory, "data", "contributors.json");
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
return { repository: "t8y2/dbx", generatedAt: "2026-10-01T18:00:00.000Z", stars: 10,
|
||||
contributors: [{ login: "contributor", commits: 3, mergedPullRequests: 1 }] };
|
||||
}
|
||||
|
||||
test("a missing contributor snapshot is created and timestamp-only refreshes preserve exact bytes", async (context) => {
|
||||
const outputPath = await snapshotFixture(context);
|
||||
const original = snapshot();
|
||||
assert.equal(await writeContributorSnapshot(outputPath, original), true);
|
||||
const originalBytes = await readFile(outputPath, "utf8");
|
||||
assert.equal(originalBytes, `${JSON.stringify(original, null, 2)}\n`);
|
||||
const refreshed = { ...original, generatedAt: "2026-10-08T18:00:00.000Z" };
|
||||
assert.equal(await writeContributorSnapshot(outputPath, refreshed), false);
|
||||
assert.equal(await readFile(outputPath, "utf8"), originalBytes);
|
||||
assert.equal(refreshed.generatedAt, "2026-10-08T18:00:00.000Z");
|
||||
});
|
||||
|
||||
test("actual contributor, star and repository changes update the snapshot", async (context) => {
|
||||
const outputPath = await snapshotFixture(context);
|
||||
const original = snapshot();
|
||||
for (const updated of [{ ...original, stars: 11 }, { ...original, repository: "other/dbx" },
|
||||
{ ...original, contributors: [{ ...original.contributors[0], commits: 4 }] },
|
||||
{ ...original, contributors: [] }]) {
|
||||
await writeContributorSnapshot(outputPath, original);
|
||||
const refreshed = { ...updated, generatedAt: "2026-10-08T18:00:00.000Z" };
|
||||
assert.equal(await writeContributorSnapshot(outputPath, refreshed), true);
|
||||
assert.deepEqual(JSON.parse(await readFile(outputPath, "utf8")), refreshed);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid contributor snapshots and filesystem errors are not silently overwritten", async (context) => {
|
||||
const outputPath = await snapshotFixture(context);
|
||||
await writeContributorSnapshot(outputPath, snapshot());
|
||||
await writeFile(outputPath, "invalid JSON");
|
||||
await assert.rejects(writeContributorSnapshot(outputPath, snapshot()), SyntaxError);
|
||||
assert.equal(await readFile(outputPath, "utf8"), "invalid JSON");
|
||||
await assert.rejects(writeContributorSnapshot(join(outputPath, "invalid"), snapshot()), { code: "ENOTDIR" });
|
||||
});
|
||||
@@ -35,7 +35,24 @@ whose development dependency is `dbx-core`.
|
||||
Windows DuckDB packaging is selected by its own inputs or shared build/lock
|
||||
changes, not every change to its development dependency's source.
|
||||
- Existing frontend, package, Windows, JDBC, offline-payload and Nix jobs retain
|
||||
their commands and path filters.
|
||||
their commands and path filters, except for the snapshot-only routing below.
|
||||
|
||||
Contributor snapshots refresh weekly, at 02:00 Monday in Asia/Shanghai, and can
|
||||
still be refreshed manually. An unchanged snapshot keeps its existing timestamp
|
||||
and creates no commit. A diff containing only `docs/data/contributors.json` skips
|
||||
desktop frontend validation, but the docs deployment still rebuilds the site.
|
||||
Mixed diffs and unavailable diffs keep the existing validation routing.
|
||||
|
||||
When `fast-checks` is selected, it owns the CI planner and gate tests before
|
||||
expensive dependent jobs start. `github-scripts` omits that subset in these runs,
|
||||
but runs every GitHub script test when the fast-check lane is not selected.
|
||||
|
||||
Issue command and spam jobs reject irrelevant comments before allocating a
|
||||
runner. Their existing command parser and malicious-link matcher remain the
|
||||
authoritative checks. PR notification triggers are commented out; release
|
||||
notifications in the publishing workflow are unchanged. Plugin development
|
||||
host lint and formatting run once on Linux before the Linux, macOS and Windows
|
||||
runtime, Rust and package checks; cross-platform coverage is unchanged.
|
||||
|
||||
Changes to the planner itself select the broad checks. Missing/invalid planning
|
||||
data fails the aggregate gates rather than producing a successful skip.
|
||||
|
||||
@@ -128,6 +128,7 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
|
||||
const java = allAgents || javaDriverChanges;
|
||||
const agents = agentsChanged || sharedAgents || nativeChanges.size > 0 || javaDriverChanges;
|
||||
return {
|
||||
contributor_snapshot_only: files.length > 0 && files.every((file) => file === "docs/data/contributors.json"),
|
||||
rust, rust_full: full, rust_matrix: { include: rustMatrix }, rust_groups_known: !unknownMember,
|
||||
affected_packages: [...affected].sort(),
|
||||
agents, agent_java: java, agent_go: { include: goMatrix }, agent_rust: { include: nativeRustMatrix },
|
||||
|
||||
@@ -58,6 +58,18 @@ const routedJobs = { frontend: "frontend", packages: "packages", "github-scripts
|
||||
"windows-standard-check": "windows_win7_bundle", "windows-win7-bundle": "windows_win7_bundle",
|
||||
"duckdb-windows-driver": "duckdb_windows", jdbc: "jdbc", "offline-jdbc-release": "offline_jdbc", "nix-packaging": "nix" };
|
||||
|
||||
test("only an exact contributor snapshot diff skips desktop frontend validation", () => {
|
||||
for (const eventName of ["pull_request", "push"]) {
|
||||
assert.equal(plan(["docs/data/contributors.json"], { eventName }).contributor_snapshot_only, true);
|
||||
for (const files of [null, [], ["docs/lib/contributorActivity.test.ts"],
|
||||
["docs/data/contributors.json", "apps/desktop/src/main.ts"],
|
||||
["docs/data/contributors.json", "docs/lib/contributorActivity.ts"],
|
||||
["docs/data/contributors.json", "Cargo.lock"]]) {
|
||||
assert.equal(plan(files, { eventName }).contributor_snapshot_only, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("foundation changes select transitive consumers and standalone DuckDB", () => {
|
||||
const result = plan(["crates/dbx-types/src/lib.rs"]);
|
||||
assert.deepEqual(groups(result), ["foundation", "drivers", "application"]);
|
||||
|
||||
@@ -23,6 +23,25 @@ test("fast checks run format and contracts before graph resolution or compilatio
|
||||
assert.ok(fast.includes("needs.changes.outputs.rust_groups_known == 'true'"));
|
||||
});
|
||||
|
||||
test("fast checks retain ownership of the planner subset before expensive jobs", () => {
|
||||
assert.match(job("github-scripts"), /FAST_CHECKS: \$\{\{ needs\.changes\.outputs\.fast \}\}/);
|
||||
assert.match(job("github-scripts"), /for file in \.github\/scripts\/\*\.test\.mjs/);
|
||||
assert.match(job("fast-checks"), /name: CI planner and gate tests\s+run: node --test \.github\/scripts\/ci-\*\.test\.mjs/);
|
||||
});
|
||||
|
||||
test("contributor snapshot routing preserves docs deployment and mixed frontend validation", () => {
|
||||
assert.ok(job("changes").includes("frontend: ${{ steps.filter.outputs.frontend == 'true' && steps.plan.outputs.contributor_snapshot_only != 'true' }}"));
|
||||
assert.match(job("changes"), /frontend:\s+- 'apps\/desktop\/\*\*'\s+- 'docs\/\*\*'/);
|
||||
const docsWorkflow = readFileSync(new URL("../workflows/docs.yml", import.meta.url), "utf8");
|
||||
assert.match(docsWorkflow, /paths: \["docs\/\*\*"\]/);
|
||||
for (const file of ["contributors", "issue-claim", "notify", "plugin-dev-host", "spam-comment-guard"]) {
|
||||
assert.ok(job("changes").includes(`- '.github/workflows/${file}.yml'`));
|
||||
}
|
||||
for (const file of ["contributor-snapshot", "update-contributors"]) {
|
||||
assert.ok(job("changes").includes(`- 'docs/scripts/${file}.mjs'`));
|
||||
}
|
||||
});
|
||||
|
||||
test("Agent and Rust matrices are bounded and do not cancel sibling failures", () => {
|
||||
for (const [name, output, parallel] of [
|
||||
["rust-test", "rust_matrix", 3],
|
||||
|
||||
@@ -159,7 +159,17 @@ jobs:
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: GitHub script tests
|
||||
run: node --test .github/scripts/*.test.mjs
|
||||
env:
|
||||
FAST_CHECKS: ${{ needs.changes.outputs.fast }}
|
||||
run: |
|
||||
tests=()
|
||||
for file in .github/scripts/*.test.mjs; do
|
||||
if [[ "$FAST_CHECKS" == true && "${file##*/}" == ci-* ]]; then
|
||||
continue
|
||||
fi
|
||||
tests+=("$file")
|
||||
done
|
||||
node --test "${tests[@]}"
|
||||
|
||||
- name: Docker mirror config tests
|
||||
run: python3 .github/scripts/configure-docker-mirror.test.py
|
||||
@@ -1093,7 +1103,7 @@ jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend == 'true' && steps.plan.outputs.contributor_snapshot_only != 'true' }}
|
||||
packages: ${{ steps.filter.outputs.packages }}
|
||||
rust: ${{ steps.plan.outputs.rust }}
|
||||
rust_full: ${{ steps.plan.outputs.rust_full }}
|
||||
@@ -1252,7 +1262,14 @@ jobs:
|
||||
- 'vendor/wry/**'
|
||||
github_scripts:
|
||||
- '.github/scripts/**'
|
||||
- 'docs/scripts/contributor-snapshot.mjs'
|
||||
- 'docs/scripts/update-contributors.mjs'
|
||||
- 'docs/public/install-mcp*'
|
||||
- '.github/workflows/contributors.yml'
|
||||
- '.github/workflows/issue-claim.yml'
|
||||
- '.github/workflows/notify.yml'
|
||||
- '.github/workflows/plugin-dev-host.yml'
|
||||
- '.github/workflows/spam-comment-guard.yml'
|
||||
- '.github/workflows/mcp-release.yml'
|
||||
- '.github/workflows/publish-packages.yml'
|
||||
- '.github/workflows/plugin-release-reusable.yml'
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Update Contributors
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 18 * * *"
|
||||
- cron: "0 18 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -16,7 +16,11 @@ permissions:
|
||||
jobs:
|
||||
claim:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
if: >-
|
||||
${{ !github.event.issue.pull_request && github.event.comment.user.type != 'Bot' &&
|
||||
(contains(github.event.comment.body, '/claim') ||
|
||||
contains(github.event.comment.body, '/unclaim') ||
|
||||
contains(github.event.comment.body, '/close')) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
|
||||
@@ -3,13 +3,15 @@ name: Notifications
|
||||
on:
|
||||
# issues:
|
||||
# types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened, closed]
|
||||
# pull_request_target:
|
||||
# types: [closed]
|
||||
workflow_dispatch:
|
||||
# Release 不在此通知:桌面正式版由 publish-packages.yml 在转正后发飞书卡片;
|
||||
# 组件包(agents-v*/packages-v*)发布不进群。
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build message
|
||||
|
||||
@@ -11,8 +11,22 @@ on:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- run: npm ci --prefix plugins/sdk/dev-host
|
||||
- run: npm run lint --prefix plugins/sdk/dev-host
|
||||
- run: npm run fmt:check --prefix plugins/sdk/dev-host
|
||||
test:
|
||||
needs: checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -29,8 +43,6 @@ jobs:
|
||||
go-version: '1.22'
|
||||
cache: false
|
||||
- run: npm ci --prefix plugins/sdk/dev-host
|
||||
- run: npm run lint --prefix plugins/sdk/dev-host
|
||||
- run: npm run fmt:check --prefix plugins/sdk/dev-host
|
||||
- run: npm test --prefix plugins/sdk/dev-host
|
||||
timeout-minutes: 5
|
||||
- uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
|
||||
|
||||
@@ -12,7 +12,12 @@ jobs:
|
||||
delete-malicious-link:
|
||||
runs-on: ubuntu-latest
|
||||
# 跳过机器人与仓库 owner,避免误删维护者的正当评论
|
||||
if: ${{ github.event.comment.user.type != 'Bot' && github.event.comment.user.login != github.repository_owner }}
|
||||
if: >-
|
||||
${{ github.event.comment.user.type != 'Bot' && github.event.comment.user.login != github.repository_owner &&
|
||||
(contains(github.event.comment.body, '.zip') ||
|
||||
contains(github.event.comment.body, '.7z') ||
|
||||
contains(github.event.comment.body, '.exe') ||
|
||||
contains(github.event.comment.body, '.scr')) }}
|
||||
env:
|
||||
# owner PAT:GITHUB_TOKEN 非 admin 删不掉别人的评论,必须用 owner 令牌
|
||||
GH_TOKEN: ${{ secrets.PROJECT_AUTOMATION_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
export async function writeContributorSnapshot(outputPath, snapshot) {
|
||||
try {
|
||||
const previous = JSON.parse(await readFile(outputPath, "utf8"));
|
||||
if (JSON.stringify({ ...previous, generatedAt: snapshot.generatedAt }) === JSON.stringify(snapshot)) return false;
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") throw error;
|
||||
}
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`);
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { writeContributorSnapshot } from "./contributor-snapshot.mjs";
|
||||
|
||||
const repository = process.env.DBX_GITHUB_REPOSITORY || "t8y2/dbx";
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
@@ -108,6 +108,8 @@ const output = {
|
||||
contributors: rankedContributors,
|
||||
};
|
||||
|
||||
await mkdir(resolve(outputPath, ".."), { recursive: true });
|
||||
await writeFile(outputPath, `${JSON.stringify(output, null, 2)}\n`);
|
||||
console.log(`Wrote ${rankedContributors.length} contributors to ${outputPath}`);
|
||||
if (await writeContributorSnapshot(outputPath, output)) {
|
||||
console.log(`Wrote ${rankedContributors.length} contributors to ${outputPath}`);
|
||||
} else {
|
||||
console.log("Contributor activity is unchanged");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user