Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1b1de76
chore: memo leak problem of ticker. fix#2075
skie1997 Apr 21, 2026
b12fc52
Merge pull request #2076 from VisActor/feat/memo-leak
xile611 Sep 16, 2026
f8862df
Merge pull request #2130 from VisActor/sync/main-1.1.10
xile611 Sep 16, 2026
ceecfe1
fix(components): initialize brush mask with computed points
xile611 Sep 16, 2026
3c80bbd
Merge pull request #2131 from VisActor/fix/brush-shape
xile611 Sep 16, 2026
7f4f465
fix: unify glyph state resolution and preserve legacy ordering
xile611 Sep 15, 2026
0fc68ae
fix: synchronize glyph children across attribute and animation commits
xile611 Sep 15, 2026
0965416
fix: preserve paint-only invalidation for glyph-derived attributes
xile611 Sep 15, 2026
de63027
fix: invalidate glyph geometry and offset caches
xile611 Sep 16, 2026
de54e0a
test: separate source and build artifact checks
xile611 Sep 16, 2026
770a9fc
fix(geometry): ignore invalid points in bounds
Aug 20, 2026
1541754
fix(area): exclude undefined points from area geometry
xile611 Sep 16, 2026
5afef93
Merge pull request #2132 from VisActor/codex/source-only-tests
xile611 Sep 17, 2026
2fffc74
Merge pull request #2129 from VisActor/codex/fix-sankey-glyph-state
xile611 Sep 17, 2026
8f79cc1
Merge pull request #2133 from VisActor/codex/invalid-point-bounds-dev…
xile611 Sep 17, 2026
fc423f0
docs: plan manual Bug Server dispatch
xile611 Sep 17, 2026
95514c6
feat: add manual Bug Server validation for fork PRs
xile611 Sep 17, 2026
a1d3089
fix: restrict bug server workflow permissions and cache access
xile611 Sep 17, 2026
f8fe1a0
fix: verify cache mode in the JavaScript action environment
xile611 Sep 17, 2026
a3d5f2e
docs: record bug server security validation and remove probe
xile611 Sep 17, 2026
46ad9c3
docs: record verified automatic build permissions
xile611 Sep 17, 2026
ae7fc09
fix: build Bug Server PR artifacts outside the default branch context
xile611 Sep 17, 2026
dac694e
docs: record Bug Server artifact security verification
xile611 Sep 17, 2026
ad6eb37
Merge pull request #2134 from VisActor/codex/bugserver-workflow-dispatch
xile611 Sep 17, 2026
a204d2e
build: prelease version 1.1.11
github-actions[bot] Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/scripts/bug-server-dispatch.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
async function resolveBugServerTarget({ github, context, prNumber, headSha }) {
const defaultBranch = context.payload.repository.default_branch;
if (context.ref !== `refs/heads/${defaultBranch}`) {
throw new Error(`Run this workflow from the default branch (${defaultBranch}).`);
}
if (!/^[1-9][0-9]*$/.test(prNumber) || !Number.isSafeInteger(Number(prNumber))) {
throw new Error('PR number must be a positive integer.');
}
if (headSha.length !== 40 || !/^[0-9a-f]+$/i.test(headSha)) {
throw new Error('Head SHA must be a full 40-character hexadecimal commit SHA.');
}

const { data: pull } = await github.rest.pulls.get({
...context.repo,
pull_number: Number(prNumber)
});
const repository = `${context.repo.owner}/${context.repo.repo}`;
if (pull.base.repo.full_name !== repository) {
throw new Error(`PR base repository must be ${repository}.`);
}
const sha = headSha.toLowerCase();
if (pull.head.sha !== sha) {
throw new Error(
`PR #${prNumber} head changed: expected ${sha}, current ${pull.head.sha}. Review the current head before retrying.`
);
}

if (!pull.head.repo) {
throw new Error('The PR head repository no longer exists.');
}
const workflowPath = '.github/workflows/bug-server-pr-bundle.yml';
const { data: workflow } = await github.rest.actions.getWorkflow({
...context.repo,
workflow_id: 'bug-server-pr-bundle.yml'
});
const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
...context.repo,
workflow_id: workflow.id,
event: 'pull_request',
head_sha: sha,
status: 'success',
per_page: 100
});
const run = runs
.filter(
candidate =>
candidate.workflow_id === workflow.id &&
candidate.path === workflowPath &&
candidate.event === 'pull_request' &&
candidate.status === 'completed' &&
candidate.conclusion === 'success' &&
candidate.head_sha === sha &&
candidate.head_branch === pull.head.ref &&
candidate.repository?.id === pull.base.repo.id &&
candidate.head_repository?.id === pull.head.repo.id &&
// GitHub omits PR associations for fork runs. Repository, branch and SHA still bind the source.
(!candidate.pull_requests?.length || candidate.pull_requests.some(pr => pr.number === Number(prNumber)))
)
.sort((a, b) => b.id - a.id)[0];
if (!run) {
throw new Error(
`No successful PR bundle build for PR #${prNumber} at ${sha}. Wait for or re-run Bug Server PR Bundle.`
);
}
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
...context.repo,
run_id: run.id,
per_page: 100
});
const matches = artifacts.filter(artifact => artifact.name === `bug-server-pr-${prNumber}-${sha}`);
if (matches.length !== 1) {
throw new Error(`Expected exactly one PR bundle artifact in run ${run.id}. Re-run Bug Server PR Bundle.`);
}
const artifact = matches[0];
if (artifact.expired) {
throw new Error('The PR bundle artifact has expired. Re-run Bug Server PR Bundle.');
}
if (
artifact.workflow_run?.id !== run.id ||
artifact.workflow_run.head_sha !== sha ||
artifact.workflow_run.repository_id !== pull.base.repo.id ||
artifact.workflow_run.head_repository_id !== pull.head.repo.id
) {
throw new Error('Artifact provenance does not match the reviewed PR build.');
}

return {
prNumber: Number(prNumber),
sha,
headRef: pull.head.ref,
prUrl: pull.html_url,
runId: run.id,
runUrl: run.html_url,
artifactId: artifact.id
};
}

module.exports = { resolveBugServerTarget };
285 changes: 285 additions & 0 deletions .github/scripts/bug-server-dispatch.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const { resolveBugServerTarget } = require('./bug-server-dispatch.cjs');

const sha = '40be3619d1608aa1d5827f0a465704eeb036a7d3';

function fixture(overrides = {}) {
const calls = [];
const context = {
repo: { owner: 'VisActor', repo: 'VRender' },
ref: 'refs/heads/develop',
payload: { repository: { default_branch: 'develop' } }
};
const pull = {
base: { repo: { id: 1, full_name: 'VisActor/VRender' } },
head: { sha, ref: 'feat/line-render-contribution', repo: { id: 2, full_name: 'g1f9/VRender' } },
html_url: 'https://github.com/VisActor/VRender/pull/2128'
};
const run = {
id: 100,
workflow_id: 50,
path: '.github/workflows/bug-server-pr-bundle.yml',
event: 'pull_request',
status: 'completed',
conclusion: 'success',
head_sha: sha,
head_branch: pull.head.ref,
repository: { id: 1 },
head_repository: { id: 2 },
pull_requests: [],
html_url: 'https://github.com/VisActor/VRender/actions/runs/100'
};
const artifact = {
id: 200,
name: `bug-server-pr-2128-${sha}`,
expired: false,
workflow_run: { id: 100, repository_id: 1, head_repository_id: 2, head_sha: sha }
};
const runs = [run];
const artifacts = [artifact];
const github = {
paginate: async (method, params) => {
const { data } = await method(params);
return data.workflow_runs ?? data.artifacts;
},
rest: {
actions: {
getWorkflow: async params => {
assert.equal(params.workflow_id, 'bug-server-pr-bundle.yml');
return { data: { id: 50, path: '.github/workflows/bug-server-pr-bundle.yml' } };
},
listWorkflowRuns: async params => {
assert.equal(params.workflow_id, 50);
assert.equal(params.head_sha, sha);
assert.equal(params.event, 'pull_request');
assert.equal(params.status, 'success');
return { data: { workflow_runs: runs } };
},
listWorkflowRunArtifacts: async params => {
assert.equal(params.run_id, 100);
return { data: { artifacts } };
}
},
pulls: {
get: async params => {
calls.push(params);
return { data: pull };
}
}
}
};
return {
args: { github, context, prNumber: '2128', headSha: sha, ...overrides },
calls,
pull,
run,
artifact,
runs,
artifacts
};
}

test('resolves the reviewed fork head, including source metadata', async () => {
const { args, calls } = fixture({ headSha: sha.toUpperCase() });
assert.deepEqual(await resolveBugServerTarget(args), {
prNumber: 2128,
sha,
headRef: 'feat/line-render-contribution',
prUrl: 'https://github.com/VisActor/VRender/pull/2128',
runId: 100,
runUrl: 'https://github.com/VisActor/VRender/actions/runs/100',
artifactId: 200
});
assert.deepEqual(calls, [{ owner: 'VisActor', repo: 'VRender', pull_number: 2128 }]);
});

for (const prNumber of ['', '0', '-1', '1.5', '2128;echo injected', '9007199254740992']) {
test(`rejects invalid PR number ${JSON.stringify(prNumber)} before API access`, async () => {
const { args, calls } = fixture({ prNumber });
await assert.rejects(resolveBugServerTarget(args), /PR number/);
assert.equal(calls.length, 0);
});
}

for (const headSha of ['', '40be3619', 'g'.repeat(40), `${sha}\n`]) {
test(`rejects invalid SHA ${JSON.stringify(headSha)} before API access`, async () => {
const { args, calls } = fixture({ headSha });
await assert.rejects(resolveBugServerTarget(args), /40-character/);
assert.equal(calls.length, 0);
});
}

test('rejects stale approval when the PR has a different head', async () => {
const { args, pull } = fixture();
pull.head.sha = 'a'.repeat(40);
await assert.rejects(resolveBugServerTarget(args), /head changed/);
});

test('rejects a workflow launched from a non-default branch', async () => {
const { args, calls } = fixture();
args.context.ref = 'refs/heads/feature';
await assert.rejects(resolveBugServerTarget(args), /default branch/);
assert.equal(calls.length, 0);
});

test('rejects a PR belonging to another base repository', async () => {
const { args, pull } = fixture();
pull.base.repo.full_name = 'someone/VRender';
await assert.rejects(resolveBugServerTarget(args), /base repository/);
});

test('propagates API lookup failures without producing a build target', async () => {
const { args } = fixture();
args.github.rest.pulls.get = async () => {
throw new Error('Not Found');
};
await assert.rejects(resolveBugServerTarget(args), /Not Found/);
});

for (const [name, change] of [
[
'wrong workflow',
run => {
run.workflow_id = 51;
}
],
[
'wrong workflow path',
run => {
run.path = '.github/workflows/other.yml';
}
],
[
'wrong event',
run => {
run.event = 'workflow_dispatch';
}
],
[
'wrong run SHA',
run => {
run.head_sha = 'a'.repeat(40);
}
],
[
'wrong base repository',
run => {
run.repository.id = 3;
}
],
[
'wrong head repository',
run => {
run.head_repository.id = 3;
}
],
[
'wrong source branch',
run => {
run.head_branch = 'another-branch';
}
],
[
'wrong PR association',
run => {
run.pull_requests = [{ number: 2135 }];
}
],
[
'failed build',
run => {
run.conclusion = 'failure';
}
],
[
'unfinished build',
run => {
run.status = 'in_progress';
}
]
]) {
test(`rejects artifact source with ${name}`, async () => {
const { args, run } = fixture();
change(run);
await assert.rejects(resolveBugServerTarget(args), /No successful PR bundle build/);
});
}

test('accepts an explicit matching PR association', async () => {
const { args, run } = fixture();
run.pull_requests = [{ number: 2128 }];
assert.equal((await resolveBugServerTarget(args)).artifactId, 200);
});

test('rejects missing workflow runs', async () => {
const { args, runs } = fixture();
runs.length = 0;
await assert.rejects(resolveBugServerTarget(args), /No successful PR bundle build/);
});

test('does not select an older run instead of the latest matching run', async () => {
const { args, run, runs } = fixture();
runs.unshift({ ...run, id: 99 });
assert.equal((await resolveBugServerTarget(args)).runId, 100);
});

for (const [name, change] of [
[
'expired',
artifact => {
artifact.expired = true;
}
],
[
'wrong run',
artifact => {
artifact.workflow_run.id = 101;
}
],
[
'wrong head SHA',
artifact => {
artifact.workflow_run.head_sha = 'a'.repeat(40);
}
],
[
'wrong base repository',
artifact => {
artifact.workflow_run.repository_id = 3;
}
],
[
'wrong head repository',
artifact => {
artifact.workflow_run.head_repository_id = 3;
}
]
]) {
test(`rejects ${name} artifact`, async () => {
const { args, artifact } = fixture();
change(artifact);
await assert.rejects(resolveBugServerTarget(args), /Artifact provenance|expired/);
});
}

test('rejects an artifact from a different PR or SHA', async () => {
const { args, artifact } = fixture();
artifact.name = `bug-server-pr-2135-${sha}`;
await assert.rejects(resolveBugServerTarget(args), /exactly one/);
});

test('rejects missing or ambiguous artifacts', async () => {
for (const count of [0, 2]) {
const { args, artifacts, artifact } = fixture();
artifacts.splice(0, 1, ...Array(count).fill(artifact));
await assert.rejects(resolveBugServerTarget(args), /exactly one/);
}
});

test('does not silently fall back when the newest run artifact has expired', async () => {
const { args, run, runs, artifact } = fixture();
runs.push({ ...run, id: 99 });
artifact.expired = true;
await assert.rejects(resolveBugServerTarget(args), /expired/);
});
Loading
Loading