chore(ci): Implement /query-regression command handling and admission workflow (#8975)

* Implement `/query-regression` command handling and admission workflow

- Add `query-regression-slash.py` script for processing `/query-regression` commands in PR comments, validating case arguments, and checking permissions.
- Update `checks.yml` to include tests for the new slash command functionality.
- Modify `query-regression-comment.yml` to trigger on the new `Query Regression Command` workflow.
- Create `query-regression-slash.yml` to handle the dispatched command, validate allowlist and permissions, and initiate the regression workflow.
- Enhance `query-regression.yml` to support additional inputs for PR admission and SHA verification.
- Introduce `slash-command-dispatch.yml` to parse and dispatch commands from PR comments.
- Document the new command admission process in `AGENTS.md` and `README.md`.
- Add unit tests in `test_query_regression_slash.py` to cover command parsing and admission logic.

* refactor: enhance query-regression command handling with comment validation and identity checks

* feat: implement admission identity handling for query regression workflows

* refactor: update PR admission logic in query regression workflow

* refactor: update token usage in slash command dispatch and README for clarity

* test: add cases for handling re-run failed jobs and stale runner artifacts

* refactor: improve repository metadata handling in query regression scripts

* chore: enable overwrite for artifact uploads to handle re-run failed jobs

* chore: enable overwrite for query regression admission uploads

* feat: enhance query-regression admission with HMAC signing and verification

- Introduced HMAC signing for admission markers in query-regression workflows to ensure integrity and authenticity.
- Updated `query-regression-comment.test.cjs` to include tests for signing and verifying admission markers.
- Modified `query-regression-slash.py` to handle admission marker signing and verification, including checks for dispatch sender and head SHA consistency.
- Enhanced workflows to securely manage admission markers and HMAC secrets, ensuring they are not exposed to untrusted contexts.
- Improved documentation to clarify the admission process and the role of HMAC in securing the workflow.

* test: add case to find newly posted marker among newer comments

* test: add case to verify multiline output handling in write_outputs function
This commit is contained in:
localhost
2026-09-07 07:23:05 +00:00
committed by GitHub
parent f9df4def74
commit 4d65e8984a
12 changed files with 2515 additions and 193 deletions
+212 -72
View File
@@ -14,6 +14,19 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const MARKER_PREFIX = '<!-- query-regression-admission v1';
const MARKER_SUFFIX = '-->';
const ADMISSION_MAC_FIELDS = [
'run_id',
'pr_number',
'head_sha',
'head_repo',
'base_repo',
'candidate_sha',
'base_sha',
];
function skip(core, message) {
core.info(message);
@@ -300,95 +313,212 @@ function renderSummaryTable(rows) {
return details === '' ? table : `${table}\n\n${details}`;
}
function admissionMacMessage(identity) {
return ADMISSION_MAC_FIELDS.map(field => {
const value = String(identity[field] ?? '');
return field.endsWith('_sha') ? value.toLowerCase() : value;
}).join('|');
}
function admissionMac(secret, identity) {
return crypto.createHmac('sha256', secret).update(admissionMacMessage(identity)).digest('hex');
}
function verifyAdmissionMac(secret, identity, mac) {
if (!secret || !mac) {
return false;
}
const expected = admissionMac(secret, identity);
const actual = String(mac);
if (expected.length !== actual.length) {
return false;
}
try {
return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(actual, 'utf8'));
} catch {
return false;
}
}
function formatAdmissionMarker(identity) {
return `${MARKER_PREFIX}\n${JSON.stringify(identity, Object.keys(identity).sort())}\n${MARKER_SUFFIX}\n`;
}
function parseAdmissionMarker(body) {
const start = String(body || '').indexOf(MARKER_PREFIX);
if (start < 0) {
return null;
}
const rest = String(body).slice(start + MARKER_PREFIX.length);
const end = rest.indexOf(MARKER_SUFFIX);
if (end < 0) {
return null;
}
try {
const payload = JSON.parse(rest.slice(0, end).trim());
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : null;
} catch {
return null;
}
}
function isAdmissionIdentity(payload) {
return ADMISSION_MAC_FIELDS.every(field => {
const value = payload?.[field];
return value !== undefined && value !== null && String(value) !== '';
});
}
async function loadAdmissionMarker(github, { owner, repo, prNumber, runId, secret }) {
// Newest first: the marker is posted at admit time, so it is among the
// latest comments. Oldest-first with a page cap misses it on busy PRs.
for (let page = 1; ; page += 1) {
const { data } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
page,
sort: 'created',
direction: 'desc',
});
if (!Array.isArray(data) || data.length === 0) {
return null;
}
for (const comment of data) {
const payload = parseAdmissionMarker(comment.body);
if (!payload || !isAdmissionIdentity(payload)) {
continue;
}
if (Number(payload.run_id) !== Number(runId)) {
continue;
}
if (!verifyAdmissionMac(secret, payload, payload.mac)) {
continue;
}
return { id: Number(comment.id), payload };
}
if (data.length < 100) {
return null;
}
}
}
function identitiesMatch(admission, metadata) {
return ADMISSION_MAC_FIELDS.every(field => {
const left = String(admission[field] ?? '');
const right = String(metadata[field] ?? '');
if (field === 'pr_number' || field === 'run_id') {
return Number(left) === Number(right);
}
if (field.endsWith('_sha')) {
return left.toLowerCase() === right.toLowerCase();
}
return left === right;
});
}
module.exports = async function validateQueryRegressionComment({ github, context, core }) {
const artifactDir = 'query-regression-comment';
const admissionPath = path.join('query-regression-admission', 'query-regression-admission.json');
const metadataPath = path.join(artifactDir, 'query-regression-pr.json');
const summaryPath = path.join(artifactDir, 'query-regression-summary.md');
if (!fs.existsSync(admissionPath)) {
return skip(core, 'Missing trusted admission identity; skipping sticky comment.');
}
if (!fs.existsSync(metadataPath)) {
return skip(core, 'Missing query-regression-pr.json; skipping sticky comment.');
}
let admission;
let metadata;
try {
admission = JSON.parse(fs.readFileSync(admissionPath, 'utf8'));
metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
} catch (error) {
core.warning(`Invalid PR metadata JSON: ${error.message}`);
return skip(core, 'Invalid PR metadata JSON; skipping sticky comment.');
core.warning(`Invalid admission or PR metadata JSON: ${error.message}`);
return skip(core, 'Invalid admission or PR metadata JSON; skipping.');
}
// Artifact identity is a lookup hint only. Candidate code on ECS shares this
// run and can overwrite artifacts; the signed PR comment is the source of truth.
if (!identitiesMatch(admission, metadata)) {
return skip(core, 'Runner artifact identity does not match admission artifact; skipping.');
}
const expectedRunId = Number(process.env.WORKFLOW_RUN_ID);
const expectedRunAttempt = Number(process.env.WORKFLOW_RUN_ATTEMPT);
if (metadata.run_id !== expectedRunId || metadata.run_attempt !== expectedRunAttempt) {
return skip(core, 'Artifact metadata does not match this workflow_run; skipping.');
// parse is not rerun on "Re-run failed jobs", so admission keeps attempt 1.
// Bind it to the stable run id; the runner artifact must match this attempt.
if (Number(admission.run_id) !== expectedRunId) {
return skip(core, 'Trusted admission does not match this workflow_run; skipping.');
}
if (metadata.base_repo !== `${context.repo.owner}/${context.repo.repo}`) {
return skip(core, `PR targets ${metadata.base_repo}, not this repository; skipping.`);
}
const prNumber = Number(metadata.pr_number);
if (!Number.isInteger(prNumber) || prNumber <= 0) {
return skip(core, 'Invalid PR number in metadata; skipping.');
if (
Number(metadata.run_id) !== expectedRunId ||
Number(metadata.run_attempt) !== expectedRunAttempt
) {
return skip(core, 'Runner artifact does not match this workflow_run attempt; skipping.');
}
const run = context.payload.workflow_run;
if (run.event !== 'pull_request') {
return skip(core, `Workflow run event is ${run.event}, not pull_request; skipping.`);
}
if (run.head_sha !== metadata.head_sha) {
return skip(core, 'Workflow run head SHA differs from artifact metadata; skipping.');
}
const runHeadRepo = run.head_repository?.full_name;
if (!runHeadRepo) {
return skip(core, 'Workflow run head repository is missing; skipping.');
}
if (runHeadRepo !== metadata.head_repo) {
return skip(core, 'Workflow run head repository differs from artifact metadata; skipping.');
// Command-handler runs execute from repository_dispatch on the default
// branch, so workflow_run head SHA/repo are that commit, not the PR.
if (run.event !== 'repository_dispatch') {
return skip(core, `Workflow run event is ${run.event}, not repository_dispatch; skipping.`);
}
// GitHub leaves workflow_run.pull_requests empty for fork PRs. When present,
// use it as an extra guard; otherwise resolve the unique open PR from trusted
// workflow_run head repo/branch/SHA metadata before accepting the artifact PR.
const workflowPrNumbers = new Set(
(run.pull_requests || []).map(pr => Number(pr.number)).filter(Number.isInteger)
);
if (workflowPrNumbers.size > 0) {
if (!workflowPrNumbers.has(prNumber)) {
return skip(core, `PR #${prNumber} is not listed in workflow_run ${run.id}; skipping.`);
}
} else {
const runHeadOwner = run.head_repository?.owner?.login;
const runHeadBranch = run.head_branch;
if (!runHeadOwner || !runHeadBranch) {
return skip(core, 'Workflow run head owner or branch is missing; skipping.');
}
const hmacSecret = (process.env.QUERY_REGRESSION_ADMISSION_HMAC || '').trim();
if (!hmacSecret) {
return skip(core, 'QUERY_REGRESSION_ADMISSION_HMAC is unset; skipping sticky comment.');
}
let matchingPrs;
try {
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${runHeadOwner}:${runHeadBranch}`,
per_page: 100,
});
matchingPrs = pullRequests.filter(pr => (
pr.head.repo?.full_name === runHeadRepo &&
pr.head.sha === run.head_sha &&
pr.base.repo?.full_name === metadata.base_repo
));
} catch (error) {
core.warning(`Could not resolve PR from workflow_run metadata: ${error.message}`);
return skip(core, 'Could not resolve PR from workflow_run metadata; skipping.');
}
const hintedPrNumber = Number(admission.pr_number);
if (!Number.isInteger(hintedPrNumber) || hintedPrNumber <= 0) {
return skip(core, 'Invalid PR number in admission artifact; skipping.');
}
if (matchingPrs.length !== 1) {
return skip(core, `Workflow run matched ${matchingPrs.length} open PRs; skipping.`);
}
if (Number(matchingPrs[0].number) !== prNumber) {
return skip(core, `Artifact PR #${prNumber} does not match workflow_run PR #${matchingPrs[0].number}; skipping.`);
}
let marker;
try {
marker = await loadAdmissionMarker(github, {
owner: context.repo.owner,
repo: context.repo.repo,
prNumber: hintedPrNumber,
runId: expectedRunId,
secret: hmacSecret,
});
} catch (error) {
core.warning(`Could not list admission markers on PR #${hintedPrNumber}: ${error.message}`);
return skip(core, `Could not list admission markers on PR #${hintedPrNumber}; skipping.`);
}
if (!marker) {
return skip(
core,
'No signed admission marker for this run on the hinted PR; skipping.',
);
}
admission = {
pr_number: marker.payload.pr_number,
head_sha: marker.payload.head_sha,
head_repo: marker.payload.head_repo,
base_repo: marker.payload.base_repo,
candidate_sha: marker.payload.candidate_sha,
base_sha: marker.payload.base_sha,
run_id: marker.payload.run_id,
};
if (!identitiesMatch(admission, metadata)) {
return skip(core, 'Runner artifact identity does not match signed admission marker; skipping.');
}
if (admission.base_repo !== `${context.repo.owner}/${context.repo.repo}`) {
return skip(core, `PR targets ${admission.base_repo}, not this repository; skipping.`);
}
const prNumber = Number(admission.pr_number);
if (!Number.isInteger(prNumber) || prNumber <= 0) {
return skip(core, 'Invalid PR number in signed admission marker; skipping.');
}
let pull;
@@ -406,11 +536,14 @@ module.exports = async function validateQueryRegressionComment({ github, context
if (pull.state !== 'open') {
return skip(core, `PR #${prNumber} is ${pull.state}; skipping.`);
}
if (pull.base.repo.full_name !== metadata.base_repo || pull.head.repo.full_name !== metadata.head_repo) {
return skip(core, 'Current PR repository metadata does not match artifact; skipping.');
if (
pull.base?.repo?.full_name !== admission.base_repo ||
pull.head?.repo?.full_name !== admission.head_repo
) {
return skip(core, 'Current PR repository metadata does not match trusted admission; skipping.');
}
if (pull.head.sha !== metadata.head_sha) {
return skip(core, 'Current PR head SHA differs from artifact; skipping stale run.');
if (pull.head?.sha !== admission.head_sha) {
return skip(core, 'Current PR head SHA differs from trusted admission; skipping stale run.');
}
const reportPaths = findReports(artifactDir);
@@ -422,9 +555,9 @@ module.exports = async function validateQueryRegressionComment({ github, context
'',
`- **Workflow run:** ${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${expectedRunId}`,
`- **Built base SHA:** \`${text(metadata.built_base_sha)}\``,
`- **Event base SHA:** \`${text(metadata.event_base_sha)}\``,
`- **Head SHA:** \`${text(metadata.head_sha)}\``,
`- **Candidate merge SHA:** \`${text(metadata.candidate_sha)}\``,
`- **Event base SHA:** \`${text(admission.base_sha)}\``,
`- **Head SHA:** \`${text(admission.head_sha)}\``,
`- **Candidate merge SHA:** \`${text(admission.candidate_sha)}\``,
'',
].join('\n');
@@ -451,4 +584,11 @@ module.exports = async function validateQueryRegressionComment({ github, context
core.setOutput('summary_path', summaryPath);
};
module.exports._test = { collectReportRows, renderSummaryTable };
module.exports._test = {
collectReportRows,
renderSummaryTable,
admissionMac,
verifyAdmissionMac,
formatAdmissionMarker,
parseAdmissionMarker,
};
+790 -18
View File
@@ -5,7 +5,16 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const handler = require('./query-regression-comment.cjs');
const { collectReportRows, renderSummaryTable } = handler._test;
const {
collectReportRows,
renderSummaryTable,
admissionMac,
verifyAdmissionMac,
formatAdmissionMarker,
parseAdmissionMarker,
} = handler._test;
const HMAC_SECRET = 'test-admission-hmac';
function report(name, measurements, thresholds = []) {
return {
@@ -19,10 +28,41 @@ function report(name, measurements, thresholds = []) {
};
}
function markerComment(identity, { id = 1, secret = HMAC_SECRET } = {}) {
const payload = { ...identity, mac: admissionMac(secret, identity) };
return { id, body: formatAdmissionMarker(payload) };
}
function githubApi({ identity, pull, comments }) {
return {
rest: {
issues: {
listComments: async () => ({ data: comments ?? [markerComment(identity)] }),
},
pulls: {
get: async () => ({
data: pull ?? {
state: 'open',
base: { repo: { full_name: identity.base_repo } },
head: { repo: { full_name: identity.head_repo }, sha: identity.head_sha },
},
}),
},
},
};
}
test('keeps the default export callable and exposes only the test seam', () => {
assert.equal(typeof handler, 'function');
assert.equal(handler.constructor.name, 'AsyncFunction');
assert.deepEqual(Object.keys(handler._test).sort(), ['collectReportRows', 'renderSummaryTable']);
assert.deepEqual(Object.keys(handler._test).sort(), [
'admissionMac',
'collectReportRows',
'formatAdmissionMarker',
'parseAdmissionMarker',
'renderSummaryTable',
'verifyAdmissionMac',
]);
});
test('renders every case in one summary table without per-case separators', () => {
@@ -305,18 +345,87 @@ test('escapes Markdown table content, including bare carriage returns', () => {
assert.doesNotMatch(table, /hidden|comment|drop|\r/);
});
test('writes the explicit no-report summary without an empty table', async () => {
test('posts a comment-command report without treating workflow_run as the PR head', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
try {
fs.mkdirSync(artifactDir);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify({
run_id: 101,
const metadata = {
run_id: 202,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
built_base_sha: 'base-sha',
event_base_sha: 'event-base-sha',
candidate_sha: 'merge-sha',
base_sha: 'event-base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '202';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info() {},
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({ identity: metadata }),
});
assert.equal(outputs.get('should_post'), 'true');
assert.equal(outputs.get('pr_number'), '42');
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips reports produced by a pull_request workflow_run', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 303,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
@@ -325,14 +434,21 @@ test('writes the explicit no-report summary without an empty table', async () =>
built_base_sha: 'base-sha',
event_base_sha: 'event-base-sha',
candidate_sha: 'candidate-sha',
}));
base_sha: 'event-base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '101';
process.env.WORKFLOW_RUN_ID = '303';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
await handler({
core: {
info() {},
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
@@ -347,19 +463,73 @@ test('writes the explicit no-report summary without an empty table', async () =>
},
},
},
github: {
rest: {
pulls: {
get: async () => ({
data: {
state: 'open',
base: { repo: { full_name: 'owner/repo' } },
head: { repo: { full_name: 'fork/repo' }, sha: 'head-sha' },
},
}),
github: { rest: { pulls: { get: async () => { throw new Error('should not fetch PR'); } } } },
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /not repository_dispatch/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('writes the explicit no-report summary without an empty table', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 101,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'head-sha',
head_repo: 'fork/repo',
built_base_sha: 'base-sha',
event_base_sha: 'event-base-sha',
candidate_sha: 'candidate-sha',
base_sha: 'event-base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '101';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info() {},
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({ identity: metadata }),
});
const summary = fs.readFileSync(path.join(artifactDir, 'query-regression-summary.md'), 'utf8');
@@ -372,6 +542,608 @@ test('writes the explicit no-report summary without an empty table', async () =>
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips when the runner artifact forges a different PR number', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const admission = {
run_id: 404,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(admission),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify({
...admission,
pr_number: 99,
}));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '404';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: { rest: { pulls: { get: async () => { throw new Error('should not fetch PR'); } } } },
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /does not match admission artifact/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('posts after Re-run failed jobs when admission stays on attempt 1', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
try {
fs.mkdirSync(artifactDir);
const admission = {
run_id: 505,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
const metadata = {
...admission,
run_attempt: 2,
built_base_sha: 'base-sha',
event_base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(admission),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '505';
process.env.WORKFLOW_RUN_ATTEMPT = '2';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info() {},
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({ identity: admission }),
});
assert.equal(outputs.get('should_post'), 'true');
assert.equal(outputs.get('pr_number'), '42');
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips a stale runner artifact from a previous attempt', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const admission = {
run_id: 606,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(admission),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify({
...admission,
built_base_sha: 'base-sha',
event_base_sha: 'base-sha',
}));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '606';
process.env.WORKFLOW_RUN_ATTEMPT = '2';
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: { rest: { pulls: { get: async () => { throw new Error('should not fetch PR'); } } } },
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /does not match this workflow_run attempt/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips when the current PR head repository is missing', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 707,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
built_base_sha: 'base-sha',
event_base_sha: 'base-sha',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '707';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({
identity: metadata,
pull: {
state: 'open',
base: { repo: { full_name: 'owner/repo' } },
head: { repo: null, sha: 'pr-head-sha' },
},
}),
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /does not match trusted admission/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('signs and verifies admission markers', () => {
const identity = {
run_id: 808,
pr_number: 42,
head_sha: 'HEADSHA',
head_repo: 'fork/repo',
base_repo: 'owner/repo',
candidate_sha: 'MERGESHA',
base_sha: 'BASESHA',
};
const mac = admissionMac(HMAC_SECRET, identity);
assert.equal(verifyAdmissionMac(HMAC_SECRET, identity, mac), true);
assert.equal(verifyAdmissionMac('other', identity, mac), false);
assert.equal(verifyAdmissionMac(HMAC_SECRET, { ...identity, pr_number: 99 }, mac), false);
const parsed = parseAdmissionMarker(`noise\n${formatAdmissionMarker({ ...identity, mac })}\n`);
assert.equal(parsed.pr_number, 42);
assert.equal(verifyAdmissionMac(HMAC_SECRET, parsed, parsed.mac), true);
});
test('skips when the HMAC secret is unset', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 808,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '808';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({ identity: metadata }),
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /QUERY_REGRESSION_ADMISSION_HMAC is unset/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips when the hinted PR has no signed marker for this run', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 909,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 99,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '909';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({ identity: metadata, comments: [{ id: 1, body: 'unrelated' }] }),
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /No signed admission marker/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('skips a marker whose HMAC does not match', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const infos = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 910,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '910';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info(message) { infos.push(message); },
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: githubApi({
identity: metadata,
comments: [markerComment(metadata, { secret: 'forged-secret' })],
}),
});
assert.equal(outputs.get('should_post'), 'false');
assert.match(infos.join('\n'), /No signed admission marker/);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
test('finds a newly posted marker after a full page of newer comments', async () => {
const originalCwd = process.cwd();
const originalRunId = process.env.WORKFLOW_RUN_ID;
const originalRunAttempt = process.env.WORKFLOW_RUN_ATTEMPT;
const originalHmac = process.env.QUERY_REGRESSION_ADMISSION_HMAC;
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'query-regression-comment-'));
const artifactDir = path.join(temporaryDir, 'query-regression-comment');
const outputs = new Map();
const pages = [];
try {
fs.mkdirSync(artifactDir);
const metadata = {
run_id: 911,
run_attempt: 1,
base_repo: 'owner/repo',
pr_number: 42,
head_sha: 'pr-head-sha',
head_repo: 'fork/repo',
candidate_sha: 'merge-sha',
base_sha: 'base-sha',
};
fs.mkdirSync(path.join(temporaryDir, 'query-regression-admission'));
fs.writeFileSync(
path.join(temporaryDir, 'query-regression-admission', 'query-regression-admission.json'),
JSON.stringify(metadata),
);
fs.writeFileSync(path.join(artifactDir, 'query-regression-pr.json'), JSON.stringify(metadata));
process.chdir(temporaryDir);
process.env.WORKFLOW_RUN_ID = '911';
process.env.WORKFLOW_RUN_ATTEMPT = '1';
process.env.QUERY_REGRESSION_ADMISSION_HMAC = HMAC_SECRET;
await handler({
core: {
info() {},
warning() {},
setOutput(name, value) { outputs.set(name, value); },
},
context: {
repo: { owner: 'owner', repo: 'repo' },
payload: {
workflow_run: {
event: 'repository_dispatch',
head_sha: 'default-branch-sha',
head_repository: { full_name: 'owner/repo' },
pull_requests: [],
},
},
},
github: {
rest: {
issues: {
listComments: async ({ page, direction }) => {
pages.push({ page, direction });
if (page === 1) {
return {
data: Array.from({ length: 100 }, (_, index) => ({
id: 10_000 - index,
body: 'unrelated',
})),
};
}
return { data: [markerComment(metadata, { id: 50 })] };
},
},
pulls: {
get: async () => ({
data: {
state: 'open',
base: { repo: { full_name: 'owner/repo' } },
head: { repo: { full_name: 'fork/repo' }, sha: 'pr-head-sha' },
},
}),
},
},
},
});
assert.equal(outputs.get('should_post'), 'true');
assert.deepEqual(pages, [
{ page: 1, direction: 'desc' },
{ page: 2, direction: 'desc' },
]);
} finally {
process.chdir(originalCwd);
if (originalRunId === undefined) delete process.env.WORKFLOW_RUN_ID;
else process.env.WORKFLOW_RUN_ID = originalRunId;
if (originalRunAttempt === undefined) delete process.env.WORKFLOW_RUN_ATTEMPT;
else process.env.WORKFLOW_RUN_ATTEMPT = originalRunAttempt;
if (originalHmac === undefined) delete process.env.QUERY_REGRESSION_ADMISSION_HMAC;
else process.env.QUERY_REGRESSION_ADMISSION_HMAC = originalHmac;
fs.rmSync(temporaryDir, { recursive: true, force: true });
}
});
+696
View File
@@ -0,0 +1,696 @@
#!/usr/bin/env python3
# Copyright 2023 Greptime Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Admit a dispatched `/query-regression` command as a query-regression run.
slash-command-dispatch owns comment parsing and admin permission. This script
requires the dispatch sender to be github-actions[bot], re-fetches the
triggering comment by id (so a forged repository_dispatch payload cannot
spoof the actor or PR), requires the current PR head to match the
dispatcher-snapshotted head SHA, then validates case args, the
QUERY_REGRESSION_COMMENT_ALLOWLIST subset, and the PR's current merge commit.
On admit it posts a hidden HMAC-signed marker comment
(QUERY_REGRESSION_ADMISSION_HMAC) that the sticky-comment workflow verifies;
the ECS job cannot forge that marker.
"""
from __future__ import annotations
import argparse
import hashlib
import hmac
import json
import os
import re
import sys
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any
COMMAND = "/query-regression"
ALLOWED_PERMISSIONS = frozenset({"admin"})
ALLOWED_DISPATCH_SENDERS = frozenset({"github-actions[bot]"})
FULL_SHA = re.compile(r"^[0-9a-fA-F]{40}$")
CASE_TOKEN = re.compile(r"^(?:all|heavy|tests/perf/query_cases/[A-Za-z0-9_][A-Za-z0-9_./-]*)$")
COMMAND_LINE = re.compile(rf"^{re.escape(COMMAND)}(?:\s+(.*))?$")
ISSUE_URL = re.compile(r"/repos/([^/]+/[^/]+)/issues/(\d+)$")
@dataclass(frozen=True)
class CommandParse:
matched: bool
case: str = ""
error: str = ""
@dataclass(frozen=True)
class Decision:
skip: bool
reason: str
case: str = ""
pr_number: str = ""
base_sha: str = ""
candidate_sha: str = ""
head_sha: str = ""
head_repo: str = ""
base_repo: str = ""
reply: str = ""
@dataclass(frozen=True)
class CommentIdentity:
actor: str = ""
pr_number: int = 0
command: CommandParse = CommandParse(matched=False)
error: str = ""
def parse_case_args(raw_args: str) -> CommandParse:
tokens = [part for part in re.split(r"[\s,]+", (raw_args or "").strip()) if part]
if not tokens:
return CommandParse(matched=True, case="all")
if "all" in tokens and len(tokens) > 1:
return CommandParse(matched=True, error="'all' cannot be mixed with other case selectors")
for token in tokens:
if ".." in token or not CASE_TOKEN.fullmatch(token):
return CommandParse(
matched=True,
error=(
"case selector must be 'all', 'heavy', or tests/perf/query_cases/... "
f"paths without '..'; got {token!r}"
),
)
return CommandParse(matched=True, case=",".join(tokens))
def parse_command(body: str) -> CommandParse:
first = (body or "").replace("\r\n", "\n").replace("\r", "\n").split("\n", 1)[0].strip()
match = COMMAND_LINE.fullmatch(first)
if match is None:
return CommandParse(matched=False)
return parse_case_args(match.group(1) or "")
def parse_allowlist(raw: str) -> frozenset[str]:
names: set[str] = set()
for part in re.split(r"[\s,]+", raw or ""):
login = part.strip().lstrip("@")
if login:
names.add(login.lower())
return frozenset(names)
def is_full_sha(value: str) -> bool:
return bool(FULL_SHA.fullmatch(value or ""))
def nested_str(payload: Any, *keys: str) -> str:
current: Any = payload
for key in keys:
if not isinstance(current, dict):
return ""
current = current.get(key)
if current is None:
return ""
return str(current)
MARKER_PREFIX = "<!-- query-regression-admission v1"
MARKER_SUFFIX = "-->"
def admission_mac_message(identity: dict[str, Any]) -> str:
return "|".join(
[
str(identity["run_id"]),
str(identity["pr_number"]),
str(identity["head_sha"]).lower(),
str(identity["head_repo"]),
str(identity["base_repo"]),
str(identity["candidate_sha"]).lower(),
str(identity["base_sha"]).lower(),
]
)
def sign_admission(secret: str, identity: dict[str, Any]) -> str:
return hmac.new(
secret.encode("utf-8"),
admission_mac_message(identity).encode("utf-8"),
hashlib.sha256,
).hexdigest()
def verify_admission_mac(secret: str, identity: dict[str, Any], mac: str) -> bool:
if not secret or not mac:
return False
expected = sign_admission(secret, identity)
try:
return hmac.compare_digest(expected, mac)
except (TypeError, ValueError):
return False
def format_admission_marker(identity: dict[str, Any]) -> str:
body = json.dumps(identity, sort_keys=True)
return f"{MARKER_PREFIX}\n{body}\n{MARKER_SUFFIX}\n"
def parse_admission_marker(body: str) -> dict[str, Any] | None:
start = (body or "").find(MARKER_PREFIX)
if start < 0:
return None
rest = body[start + len(MARKER_PREFIX) :]
end = rest.find(MARKER_SUFFIX)
if end < 0:
return None
try:
payload = json.loads(rest[:end].strip())
except json.JSONDecodeError:
return None
return payload if isinstance(payload, dict) else None
def parse_github_id(value: str) -> int | None:
stripped = (value or "").strip()
if not stripped.isdigit():
return None
number = int(stripped)
return number if number > 0 else None
def github_request(
token: str,
api_url: str,
path: str,
*,
method: str = "GET",
payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
data = None if payload is None else json.dumps(payload).encode("utf-8")
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
}
if data is not None:
headers["Content-Type"] = "application/json"
request = urllib.request.Request(
f"{api_url.rstrip('/')}{path}",
method=method,
data=data,
headers=headers,
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read().decode("utf-8")
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", "replace")
raise SystemExit(
f"GitHub API {method} {path} failed: HTTP {error.code}: {body}"
) from error
def github_get(token: str, api_url: str, path: str) -> dict[str, Any]:
return github_request(token, api_url, path)
def fetch_comment(token: str, api_url: str, repo: str, comment_id: int) -> dict[str, Any]:
return github_get(token, api_url, f"/repos/{repo}/issues/comments/{comment_id}")
def identity_from_comment(comment: dict[str, Any], expected_repo: str) -> CommentIdentity:
login = str((comment.get("user") or {}).get("login") or "")
issue_url = str(comment.get("issue_url") or "")
match = ISSUE_URL.search(issue_url)
if match is None:
return CommentIdentity(error="comment issue_url is missing or invalid")
repo, pr_number = match.group(1), int(match.group(2))
if repo.lower() != expected_repo.lower():
return CommentIdentity(
actor=login,
pr_number=pr_number,
error=f"comment targets {repo}, not {expected_repo}",
)
command = parse_command(str(comment.get("body") or ""))
if not command.matched:
return CommentIdentity(
actor=login,
pr_number=pr_number,
command=command,
error="comment is not a query-regression command",
)
if command.error:
return CommentIdentity(
actor=login,
pr_number=pr_number,
command=command,
error=command.error,
)
return CommentIdentity(actor=login, pr_number=pr_number, command=command)
def payload_matches_comment(
identity: CommentIdentity,
*,
actor: str,
pr_number: str,
command_args: str,
) -> str:
if actor and actor.strip().lstrip("@").lower() != identity.actor.strip().lstrip("@").lower():
return "payload actor does not match comment author"
if pr_number.strip():
parsed = parse_github_id(pr_number)
if parsed is None:
return "payload PR number is not a valid integer"
if parsed != identity.pr_number:
return "payload PR number does not match comment"
payload_command = parse_case_args(command_args)
if payload_command.error:
return payload_command.error
if payload_command.case != identity.command.case:
return "payload command args do not match comment"
return ""
def dispatch_sender_ok(sender: str) -> str:
"""Empty if repository_dispatch came from Actions; otherwise a deny reason."""
login = (sender or "").strip().lstrip("@").lower()
if login not in ALLOWED_DISPATCH_SENDERS:
return "repository_dispatch sender is not github-actions[bot]"
return ""
def dispatch_head_matches(pull: dict[str, Any], snapshot_sha: str) -> str:
"""Empty if the current PR head is the dispatcher snapshot; otherwise a deny reason."""
snapshot = (snapshot_sha or "").strip().lower()
if not is_full_sha(snapshot):
return "dispatch payload is missing an immutable PR head SHA"
current = nested_str(pull, "head", "sha").lower()
if current != snapshot:
return "PR head changed since the slash command was dispatched"
return ""
def fetch_pull(
token: str,
api_url: str,
repo: str,
pr_number: int,
*,
attempts: int = 5,
) -> dict[str, Any]:
path = f"/repos/{repo}/pulls/{pr_number}"
payload: dict[str, Any] = {}
for attempt in range(1, attempts + 1):
payload = github_get(token, api_url, path)
if payload.get("mergeable") is not None or payload.get("draft"):
return payload
if attempt < attempts:
time.sleep(2 ** (attempt - 1))
return payload
def fetch_permission(token: str, api_url: str, repo: str, username: str) -> str:
encoded = urllib.parse.quote(username)
try:
payload = github_get(
token,
api_url,
f"/repos/{repo}/collaborators/{encoded}/permission",
)
except SystemExit as error:
message = str(error)
if "HTTP 404" in message:
return ""
raise
permission = str(payload.get("permission") or "")
return permission if permission in ALLOWED_PERMISSIONS else "denied"
def deny(reason: str, *, reply: str = "", pr_number: str = "") -> Decision:
return Decision(skip=True, reason=reason, reply=reply, pr_number=pr_number)
def admit_pull(
pull: dict[str, Any],
*,
actor: str,
allowlist: frozenset[str],
permission: str,
command: CommandParse,
expected_repo: str,
pr_number: str = "",
) -> Decision:
def reject(reason: str, *, reply: str = "") -> Decision:
return deny(reason, reply=reply, pr_number=pr_number)
if not command.matched:
return reject("comment is not a query-regression command")
if command.error:
return reject(
command.error,
reply=f"Query regression command ignored: {command.error}.",
)
actor_key = actor.strip().lstrip("@").lower()
if not allowlist:
return reject(
"QUERY_REGRESSION_COMMENT_ALLOWLIST is unset",
reply=(
"Query regression command ignored: repository variable "
"`QUERY_REGRESSION_COMMENT_ALLOWLIST` is empty."
),
)
if actor_key not in allowlist:
return reject(
"commenter is not on QUERY_REGRESSION_COMMENT_ALLOWLIST",
reply="Query regression command ignored: you are not on the allowlist.",
)
if permission not in ALLOWED_PERMISSIONS:
return reject(
"commenter is not a repository admin",
reply="Query regression command ignored: repository admin permission is required.",
)
if pull.get("draft"):
return reject(
"PR is a draft",
reply="Query regression command ignored: draft PRs are not admitted.",
)
if pull.get("state") != "open":
return reject(
f"PR is {pull.get('state')}",
reply="Query regression command ignored: the pull request is not open.",
)
if pull.get("merged"):
return reject(
"PR is already merged",
reply="Query regression command ignored: the pull request is already merged.",
)
if pull.get("mergeable") is False:
return reject(
"PR has merge conflicts",
reply=(
"Query regression command ignored: the pull request is not mergeable. "
"Resolve conflicts and comment `/query-regression` again."
),
)
if pull.get("mergeable") is None:
return reject(
"PR mergeability is not yet computed",
reply=(
"Query regression command ignored: GitHub has not computed mergeability yet. "
"Retry in a few seconds."
),
)
base_repo = nested_str(pull, "base", "repo", "full_name")
head_repo = nested_str(pull, "head", "repo", "full_name")
base_sha = nested_str(pull, "base", "sha")
head_sha = nested_str(pull, "head", "sha")
merge_sha = nested_str(pull, "merge_commit_sha")
pr_number = str(pull.get("number") or pr_number)
if not head_repo:
return reject(
"PR head repository is missing",
reply=(
"Query regression command ignored: the pull request head repository "
"is unavailable (the fork may have been deleted)."
),
)
if base_repo != expected_repo:
return reject(
f"PR targets {base_repo}, not {expected_repo}",
reply="Query regression command ignored: pull request is not against this repository.",
)
if not is_full_sha(base_sha) or not is_full_sha(head_sha) or not is_full_sha(merge_sha):
return reject(
"PR is missing an immutable merge, head, or base SHA",
reply=(
"Query regression command ignored: GitHub did not provide a full merge SHA. "
"Retry once the PR is mergeable."
),
)
return Decision(
skip=False,
reason="",
case=command.case,
pr_number=pr_number,
base_sha=base_sha.lower(),
candidate_sha=merge_sha.lower(),
head_sha=head_sha.lower(),
head_repo=head_repo,
base_repo=base_repo,
)
def write_outputs(decision: Decision) -> None:
values = {
"skip": "true" if decision.skip else "false",
"reason": decision.reason,
"case": decision.case,
"pr_number": decision.pr_number,
"base_sha": decision.base_sha,
"candidate_sha": decision.candidate_sha,
"head_sha": decision.head_sha,
"head_repo": decision.head_repo,
"base_repo": decision.base_repo,
"reply": decision.reply,
}
output_path = os.environ.get("GITHUB_OUTPUT")
if output_path:
with open(output_path, "a", encoding="utf-8") as handle:
for key, value in values.items():
delimiter = f"ghadelimiter_{uuid.uuid4().hex}"
handle.write(f"{key}<<{delimiter}\n{value}\n{delimiter}\n")
for key, value in values.items():
print(f"{key}={value}")
def build_admission_identity(decision: Decision) -> dict[str, Any] | None:
"""PR identity from the trusted admission job. MAC is added at persist time."""
if decision.skip:
return None
run_id = parse_github_id(os.environ.get("GITHUB_RUN_ID") or "")
pr_number = parse_github_id(decision.pr_number)
if run_id is None or pr_number is None:
return None
if not all(
[
decision.head_sha,
decision.head_repo,
decision.base_repo,
decision.candidate_sha,
decision.base_sha,
]
):
return None
return {
"pr_number": pr_number,
"head_sha": decision.head_sha,
"head_repo": decision.head_repo,
"base_repo": decision.base_repo,
"candidate_sha": decision.candidate_sha,
"base_sha": decision.base_sha,
"run_id": run_id,
"run_attempt": parse_github_id(os.environ.get("GITHUB_RUN_ATTEMPT") or "1") or 1,
}
def post_admission_marker(
token: str,
api_url: str,
repo: str,
identity: dict[str, Any],
) -> None:
"""Hide a signed identity on the admitted PR. ECS cannot forge this."""
github_request(
token,
api_url,
f"/repos/{repo}/issues/{identity['pr_number']}/comments",
method="POST",
payload={"body": format_admission_marker(identity)},
)
def persist_admission_identity(
decision: Decision,
*,
token: str,
api_url: str,
repo: str,
secret: str,
) -> str:
"""Write the lookup artifact and post the HMAC marker. Empty string on success."""
if decision.skip:
return ""
if not secret.strip():
return "QUERY_REGRESSION_ADMISSION_HMAC is unset"
identity = build_admission_identity(decision)
if identity is None:
return "could not persist admission identity"
payload = {**identity, "mac": sign_admission(secret, identity)}
with open("query-regression-admission.json", "w", encoding="utf-8") as handle:
json.dump(payload, handle, sort_keys=True)
handle.write("\n")
post_admission_marker(token, api_url, repo, payload)
return ""
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY") or "")
parser.add_argument(
"--token",
default=os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or "",
)
parser.add_argument("--api-url", default=os.environ.get("GITHUB_API_URL") or "https://api.github.com")
parser.add_argument("--actor", default=os.environ.get("COMMENT_ACTOR") or "")
parser.add_argument("--args", default=os.environ.get("COMMAND_ARGS") or "")
parser.add_argument("--pr-number", default=os.environ.get("PR_NUMBER") or "")
parser.add_argument("--comment-id", default=os.environ.get("COMMENT_ID") or "")
parser.add_argument(
"--allowlist",
default=os.environ.get("QUERY_REGRESSION_COMMENT_ALLOWLIST") or "",
)
parser.add_argument(
"--dispatch-sender",
default=os.environ.get("DISPATCH_SENDER") or "",
)
parser.add_argument(
"--dispatch-head-sha",
default=os.environ.get("DISPATCH_HEAD_SHA") or "",
)
return parser.parse_args(argv)
def admit_dispatched_command(args: argparse.Namespace, comment_id: int) -> int:
comment = fetch_comment(args.token, args.api_url, args.repo, comment_id)
identity = identity_from_comment(comment, args.repo)
pr_number = str(identity.pr_number or args.pr_number or "")
if identity.error:
write_outputs(
deny(
identity.error,
reply=f"Query regression command ignored: {identity.error}.",
pr_number=pr_number,
)
)
return 0
mismatch = payload_matches_comment(
identity,
actor=args.actor,
pr_number=args.pr_number,
command_args=args.args,
)
if mismatch:
write_outputs(
deny(
mismatch,
reply=f"Query regression command ignored: {mismatch}.",
pr_number=str(identity.pr_number),
)
)
return 0
allowlist = parse_allowlist(args.allowlist)
permission = fetch_permission(args.token, args.api_url, args.repo, identity.actor)
pull = fetch_pull(args.token, args.api_url, args.repo, identity.pr_number)
head_mismatch = dispatch_head_matches(pull, args.dispatch_head_sha)
if head_mismatch:
write_outputs(
deny(
head_mismatch,
reply=(
"Query regression command ignored: the PR head changed since the "
"command was dispatched. Review the current revision and comment "
"`/query-regression` again."
),
pr_number=str(identity.pr_number),
)
)
return 0
decision = admit_pull(
pull,
actor=identity.actor,
allowlist=allowlist,
permission=permission,
command=identity.command,
expected_repo=args.repo,
pr_number=str(identity.pr_number),
)
if not decision.skip:
persist_error = persist_admission_identity(
decision,
token=args.token,
api_url=args.api_url,
repo=args.repo,
secret=os.environ.get("QUERY_REGRESSION_ADMISSION_HMAC") or "",
)
if persist_error:
write_outputs(
deny(
persist_error,
reply=f"Query regression command ignored: {persist_error}.",
pr_number=decision.pr_number,
)
)
print(persist_error, file=sys.stderr)
return 1
write_outputs(decision)
return 0
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
comment_id = parse_github_id(str(args.comment_id))
if not args.repo or not args.token or comment_id is None:
print("--repo, --token, and a numeric --comment-id are required.", file=sys.stderr)
return 2
sender_error = dispatch_sender_ok(args.dispatch_sender)
if sender_error:
# Do not reply: the payload PR number is untrusted when the sender is not Actions.
write_outputs(deny(sender_error))
return 0
try:
return admit_dispatched_command(args, comment_id)
except SystemExit as error:
write_outputs(
deny(
f"admission failed: {error}",
reply=(
"Query regression command ignored: GitHub API error while "
"admitting; please retry."
),
pr_number=str(args.pr_number or ""),
)
)
print(error, file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())