mirror of
https://github.com/hwchase17/langchain.git
synced 2026-05-16 20:26:54 +00:00
Bumps [actions/github-script](https://github.com/actions/github-script) from 8.0.0 to 9.0.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/actions/github-script/releases">actions/github-script's releases</a>.</em></p> <blockquote> <h2>v9.0.0</h2> <p><strong>New features:</strong></p> <ul> <li><strong><code>getOctokit</code> factory function</strong> — Available directly in the script context. Create additional authenticated Octokit clients with different tokens for multi-token workflows, GitHub App tokens, and cross-org access. See <a href="https://github.com/actions/github-script#creating-additional-clients-with-getoctokit">Creating additional clients with <code>getOctokit</code></a> for details and examples.</li> <li><strong>Orchestration ID in user-agent</strong> — The <code>ACTIONS_ORCHESTRATION_ID</code> environment variable is automatically appended to the user-agent string for request tracing.</li> </ul> <p><strong>Breaking changes:</strong></p> <ul> <li><strong><code>require('@actions/github')</code> no longer works in scripts.</strong> The upgrade to <code>@actions/github</code> v9 (ESM-only) means <code>require('@actions/github')</code> will fail at runtime. If you previously used patterns like <code>const { getOctokit } = require('@actions/github')</code> to create secondary clients, use the new injected <code>getOctokit</code> function instead — it's available directly in the script context with no imports needed.</li> <li><code>getOctokit</code> is now an injected function parameter. Scripts that declare <code>const getOctokit = ...</code> or <code>let getOctokit = ...</code> will get a <code>SyntaxError</code> because JavaScript does not allow <code>const</code>/<code>let</code> redeclaration of function parameters. Use the injected <code>getOctokit</code> directly, or use <code>var getOctokit = ...</code> if you need to redeclare it.</li> <li>If your script accesses other <code>@actions/github</code> internals beyond the standard <code>github</code>/<code>octokit</code> client, you may need to update those references for v9 compatibility.</li> </ul> <h2>What's Changed</h2> <ul> <li>Add ACTIONS_ORCHESTRATION_ID to user-agent string by <a href="https://github.com/Copilot"><code>@Copilot</code></a> in <a href="https://redirect.github.com/actions/github-script/pull/695">actions/github-script#695</a></li> <li>ci: use deployment: false for integration test environments by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/github-script/pull/712">actions/github-script#712</a></li> <li>feat!: add getOctokit to script context, upgrade <code>@actions/github</code> v9, <code>@octokit/core</code> v7, and related packages by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/github-script/pull/700">actions/github-script#700</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/Copilot"><code>@Copilot</code></a> made their first contribution in <a href="https://redirect.github.com/actions/github-script/pull/695">actions/github-script#695</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/github-script/compare/v8.0.0...v9.0.0">https://github.com/actions/github-script/compare/v8.0.0...v9.0.0</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="3a2844b7e9"><code>3a2844b</code></a> Merge pull request <a href="https://redirect.github.com/actions/github-script/issues/700">#700</a> from actions/salmanmkc/expose-getoctokit + prepare re...</li> <li><a href="ca10bbdd1a"><code>ca10bbd</code></a> fix: use <code>@octokit/core/</code>types import for v7 compatibility</li> <li><a href="86e48e20ac"><code>86e48e2</code></a> merge: incorporate main branch changes</li> <li><a href="c1084728b5"><code>c108472</code></a> chore: rebuild dist for v9 upgrade and getOctokit factory</li> <li><a href="afff112e4f"><code>afff112</code></a> Merge pull request <a href="https://redirect.github.com/actions/github-script/issues/712">#712</a> from actions/salmanmkc/deployment-false + fix user-ag...</li> <li><a href="ff8117e5b7"><code>ff8117e</code></a> ci: fix user-agent test to handle orchestration ID</li> <li><a href="81c6b78760"><code>81c6b78</code></a> ci: use deployment: false to suppress deployment noise from integration tests</li> <li><a href="3953caf885"><code>3953caf</code></a> docs: update README examples from <a href="https://github.com/v8"><code>@v8</code></a> to <a href="https://github.com/v9"><code>@v9</code></a>, add getOctokit docs and v9 brea...</li> <li><a href="c17d55b90d"><code>c17d55b</code></a> ci: add getOctokit integration test job</li> <li><a href="a047196d9a"><code>a047196</code></a> test: add getOctokit integration tests via callAsyncFunction</li> <li>Additional commits viewable in <a href="ed597411d8...3a2844b7e9">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
468 lines
22 KiB
YAML
468 lines
22 KiB
YAML
# Require external PRs to reference an approved issue (e.g. Fixes #NNN) and
|
|
# the PR author to be assigned to that issue. On failure the PR is
|
|
# labeled "missing-issue-link", commented on, and closed.
|
|
#
|
|
# Maintainer override: an org member can reopen the PR or remove
|
|
# "missing-issue-link" — both add "bypass-issue-check" and reopen.
|
|
#
|
|
# Dependency: pr_labeler.yml must apply the "external" label first. This
|
|
# workflow does NOT trigger on "opened" (new PRs have no labels yet, so the
|
|
# gate would always skip).
|
|
|
|
name: Require Issue Link
|
|
|
|
on:
|
|
pull_request_target:
|
|
# NEVER CHECK OUT UNTRUSTED CODE FROM A PR's HEAD IN A pull_request_target JOB.
|
|
# Doing so would allow attackers to execute arbitrary code in the context of your repository.
|
|
types: [edited, reopened, labeled, unlabeled]
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# Enforcement gate: set to 'true' to activate the issue link requirement.
|
|
# When 'false', the workflow still runs the check logic (useful for dry-run
|
|
# visibility) but will NOT label, comment, close, or fail PRs.
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
env:
|
|
ENFORCE_ISSUE_LINK: "true"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
check-issue-link:
|
|
# Run when the "external" label is added, on edit/reopen if already labeled,
|
|
# or when "missing-issue-link" is removed (triggers maintainer override check).
|
|
# Skip entirely when the PR already carries "trusted-contributor" or
|
|
# "bypass-issue-check".
|
|
if: >-
|
|
!contains(github.event.pull_request.labels.*.name, 'trusted-contributor') &&
|
|
!contains(github.event.pull_request.labels.*.name, 'bypass-issue-check') &&
|
|
(
|
|
(github.event.action == 'labeled' && github.event.label.name == 'external') ||
|
|
(github.event.action == 'unlabeled' && github.event.label.name == 'missing-issue-link' && contains(github.event.pull_request.labels.*.name, 'external')) ||
|
|
(github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'external'))
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: write
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Check for issue link and assignee
|
|
id: check-link
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
const action = context.payload.action;
|
|
|
|
// ── Helper: ensure a label exists, then add it to the PR ────────
|
|
async function ensureAndAddLabel(labelName, color) {
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name: labelName });
|
|
} catch (e) {
|
|
if (e.status !== 404) throw e;
|
|
try {
|
|
await github.rest.issues.createLabel({ owner, repo, name: labelName, color });
|
|
} catch (createErr) {
|
|
// 422 = label was created by a concurrent run between our
|
|
// GET and POST — safe to ignore.
|
|
if (createErr.status !== 422) throw createErr;
|
|
}
|
|
}
|
|
await github.rest.issues.addLabels({
|
|
owner, repo, issue_number: prNumber, labels: [labelName],
|
|
});
|
|
}
|
|
|
|
// ── Helper: check if the user who triggered this event (reopened
|
|
// the PR / removed the label) has write+ access on the repo ───
|
|
// Uses the repo collaborator permission endpoint instead of the
|
|
// org membership endpoint. The org endpoint requires the caller
|
|
// to be an org member, which GITHUB_TOKEN (an app installation
|
|
// token) never is — so it always returns 403.
|
|
async function senderIsOrgMember() {
|
|
const sender = context.payload.sender?.login;
|
|
if (!sender) {
|
|
throw new Error('Event has no sender — cannot check permissions');
|
|
}
|
|
try {
|
|
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
owner, repo, username: sender,
|
|
});
|
|
const perm = data.permission;
|
|
if (['admin', 'maintain', 'write'].includes(perm)) {
|
|
console.log(`${sender} has ${perm} permission — treating as maintainer`);
|
|
return { isMember: true, login: sender };
|
|
}
|
|
console.log(`${sender} has ${perm} permission — not a maintainer`);
|
|
return { isMember: false, login: sender };
|
|
} catch (e) {
|
|
if (e.status === 404) {
|
|
console.log(`Cannot check permissions for ${sender} — treating as non-maintainer`);
|
|
return { isMember: false, login: sender };
|
|
}
|
|
const status = e.status ?? 'unknown';
|
|
throw new Error(
|
|
`Permission check failed for ${sender} (HTTP ${status}): ${e.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Helper: apply maintainer bypass (shared by both override paths) ──
|
|
async function applyMaintainerBypass(reason) {
|
|
console.log(reason);
|
|
|
|
// Remove missing-issue-link if present
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: prNumber, name: 'missing-issue-link',
|
|
});
|
|
} catch (e) {
|
|
if (e.status !== 404) throw e;
|
|
}
|
|
|
|
// Reopen before adding bypass label — a failed reopen is more
|
|
// actionable than a closed PR with a bypass label stuck on it.
|
|
if (context.payload.pull_request.state === 'closed') {
|
|
try {
|
|
await github.rest.pulls.update({
|
|
owner, repo, pull_number: prNumber, state: 'open',
|
|
});
|
|
console.log(`Reopened PR #${prNumber}`);
|
|
} catch (e) {
|
|
// 422 if head branch deleted; 403 if permissions insufficient.
|
|
// Bypass labels still apply — maintainer can reopen manually.
|
|
core.warning(
|
|
`Could not reopen PR #${prNumber} (HTTP ${e.status ?? 'unknown'}): ${e.message}. ` +
|
|
`Bypass labels were applied — a maintainer may need to reopen manually.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Add bypass-issue-check so future triggers skip enforcement
|
|
await ensureAndAddLabel('bypass-issue-check', '0e8a16');
|
|
|
|
// Minimize stale enforcement comment (best-effort; must not
|
|
// abort bypass — sync w/ reopen_on_assignment.yml & step below)
|
|
try {
|
|
const marker = '<!-- require-issue-link -->';
|
|
const comments = await github.paginate(
|
|
github.rest.issues.listComments,
|
|
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
|
);
|
|
const stale = comments.find(c => c.body && c.body.includes(marker));
|
|
if (stale) {
|
|
await github.graphql(`
|
|
mutation($id: ID!) {
|
|
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
|
|
minimizedComment { isMinimized }
|
|
}
|
|
}
|
|
`, { id: stale.node_id });
|
|
console.log(`Minimized stale enforcement comment ${stale.id} as outdated`);
|
|
}
|
|
} catch (e) {
|
|
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
|
|
}
|
|
|
|
core.setOutput('has-link', 'true');
|
|
core.setOutput('is-assigned', 'true');
|
|
}
|
|
|
|
// ── Maintainer override: removed "missing-issue-link" label ─────
|
|
if (action === 'unlabeled') {
|
|
const { isMember, login } = await senderIsOrgMember();
|
|
if (isMember) {
|
|
await applyMaintainerBypass(
|
|
`Maintainer ${login} removed missing-issue-link from PR #${prNumber} — bypassing enforcement`,
|
|
);
|
|
return;
|
|
}
|
|
// Non-member removed the label — re-add it defensively and
|
|
// set failure outputs so downstream steps (comment, close) fire.
|
|
// NOTE: addLabels fires a "labeled" event, but the job-level gate
|
|
// only matches labeled events for "external", so no re-trigger.
|
|
console.log(`Non-member ${login} removed missing-issue-link — re-adding`);
|
|
try {
|
|
await ensureAndAddLabel('missing-issue-link', 'b76e79');
|
|
} catch (e) {
|
|
core.warning(
|
|
`Failed to re-add missing-issue-link (HTTP ${e.status ?? 'unknown'}): ${e.message}. ` +
|
|
`Downstream step will retry.`,
|
|
);
|
|
}
|
|
core.setOutput('has-link', 'false');
|
|
core.setOutput('is-assigned', 'false');
|
|
return;
|
|
}
|
|
|
|
// ── Maintainer override: reopened PR with "missing-issue-link" ──
|
|
const prLabels = context.payload.pull_request.labels.map(l => l.name);
|
|
if (action === 'reopened' && prLabels.includes('missing-issue-link')) {
|
|
const { isMember, login } = await senderIsOrgMember();
|
|
if (isMember) {
|
|
await applyMaintainerBypass(
|
|
`Maintainer ${login} reopened PR #${prNumber} — bypassing enforcement`,
|
|
);
|
|
return;
|
|
}
|
|
console.log(`Non-member ${login} reopened PR — proceeding with check`);
|
|
}
|
|
|
|
// ── Fetch live labels (race guard) ──────────────────────────────
|
|
const { data: liveLabels } = await github.rest.issues.listLabelsOnIssue({
|
|
owner, repo, issue_number: prNumber,
|
|
});
|
|
const liveNames = liveLabels.map(l => l.name);
|
|
if (liveNames.includes('trusted-contributor') || liveNames.includes('bypass-issue-check')) {
|
|
console.log('PR has trusted-contributor or bypass-issue-check label — bypassing');
|
|
core.setOutput('has-link', 'true');
|
|
core.setOutput('is-assigned', 'true');
|
|
return;
|
|
}
|
|
|
|
const body = context.payload.pull_request.body || '';
|
|
const pattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*#(\d+)/gi;
|
|
const matches = [...body.matchAll(pattern)];
|
|
|
|
if (matches.length === 0) {
|
|
console.log('No issue link found in PR body');
|
|
core.setOutput('has-link', 'false');
|
|
core.setOutput('is-assigned', 'false');
|
|
return;
|
|
}
|
|
|
|
const issues = matches.map(m => `#${m[1]}`).join(', ');
|
|
console.log(`Found issue link(s): ${issues}`);
|
|
core.setOutput('has-link', 'true');
|
|
|
|
// Check whether the PR author is assigned to at least one linked issue
|
|
const prAuthor = context.payload.pull_request.user.login;
|
|
const MAX_ISSUES = 5;
|
|
const allIssueNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
|
const issueNumbers = allIssueNumbers.slice(0, MAX_ISSUES);
|
|
if (allIssueNumbers.length > MAX_ISSUES) {
|
|
core.warning(
|
|
`PR references ${allIssueNumbers.length} issues — only checking the first ${MAX_ISSUES}`,
|
|
);
|
|
}
|
|
|
|
let assignedToAny = false;
|
|
for (const num of issueNumbers) {
|
|
try {
|
|
const { data: issue } = await github.rest.issues.get({
|
|
owner, repo, issue_number: num,
|
|
});
|
|
const assignees = issue.assignees.map(a => a.login.toLowerCase());
|
|
if (assignees.includes(prAuthor.toLowerCase())) {
|
|
console.log(`PR author "${prAuthor}" is assigned to #${num}`);
|
|
assignedToAny = true;
|
|
break;
|
|
} else {
|
|
console.log(`PR author "${prAuthor}" is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`);
|
|
}
|
|
} catch (error) {
|
|
if (error.status === 404) {
|
|
console.log(`Issue #${num} not found — skipping`);
|
|
} else {
|
|
// Non-404 errors (rate limit, server error) must not be
|
|
// silently skipped — they could cause false enforcement
|
|
// (closing a legitimate PR whose assignment can't be verified).
|
|
throw new Error(
|
|
`Cannot verify assignee for issue #${num} (${error.status}): ${error.message}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
core.setOutput('is-assigned', assignedToAny ? 'true' : 'false');
|
|
|
|
- name: Add missing-issue-link label
|
|
if: >-
|
|
env.ENFORCE_ISSUE_LINK == 'true' &&
|
|
(steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true')
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
const labelName = 'missing-issue-link';
|
|
|
|
// Ensure the label exists (no checkout/shared helper available)
|
|
try {
|
|
await github.rest.issues.getLabel({ owner, repo, name: labelName });
|
|
} catch (e) {
|
|
if (e.status !== 404) throw e;
|
|
try {
|
|
await github.rest.issues.createLabel({
|
|
owner, repo, name: labelName, color: 'b76e79',
|
|
});
|
|
} catch (createErr) {
|
|
if (createErr.status !== 422) throw createErr;
|
|
}
|
|
}
|
|
|
|
await github.rest.issues.addLabels({
|
|
owner, repo, issue_number: prNumber, labels: [labelName],
|
|
});
|
|
|
|
- name: Remove missing-issue-link label and reopen PR
|
|
if: >-
|
|
env.ENFORCE_ISSUE_LINK == 'true' &&
|
|
steps.check-link.outputs.has-link == 'true' && steps.check-link.outputs.is-assigned == 'true'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner, repo, issue_number: prNumber, name: 'missing-issue-link',
|
|
});
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
|
|
// Reopen if this workflow previously closed the PR. We check the
|
|
// event payload labels (not live labels) because we already removed
|
|
// missing-issue-link above; the payload still reflects pre-step state.
|
|
const labels = context.payload.pull_request.labels.map(l => l.name);
|
|
if (context.payload.pull_request.state === 'closed' && labels.includes('missing-issue-link')) {
|
|
await github.rest.pulls.update({
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
state: 'open',
|
|
});
|
|
console.log(`Reopened PR #${prNumber}`);
|
|
}
|
|
|
|
// Minimize stale enforcement comment (best-effort;
|
|
// sync w/ applyMaintainerBypass above & reopen_on_assignment.yml)
|
|
try {
|
|
const marker = '<!-- require-issue-link -->';
|
|
const comments = await github.paginate(
|
|
github.rest.issues.listComments,
|
|
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
|
);
|
|
const stale = comments.find(c => c.body && c.body.includes(marker));
|
|
if (stale) {
|
|
await github.graphql(`
|
|
mutation($id: ID!) {
|
|
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
|
|
minimizedComment { isMinimized }
|
|
}
|
|
}
|
|
`, { id: stale.node_id });
|
|
console.log(`Minimized stale enforcement comment ${stale.id} as outdated`);
|
|
}
|
|
} catch (e) {
|
|
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
|
|
}
|
|
|
|
- name: Post comment, close PR, and fail
|
|
if: >-
|
|
env.ENFORCE_ISSUE_LINK == 'true' &&
|
|
(steps.check-link.outputs.has-link != 'true' || steps.check-link.outputs.is-assigned != 'true')
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const prNumber = context.payload.pull_request.number;
|
|
const hasLink = '${{ steps.check-link.outputs.has-link }}' === 'true';
|
|
const isAssigned = '${{ steps.check-link.outputs.is-assigned }}' === 'true';
|
|
const marker = '<!-- require-issue-link -->';
|
|
|
|
let lines;
|
|
if (!hasLink) {
|
|
lines = [
|
|
marker,
|
|
'**This PR has been automatically closed** because it does not link to an approved issue.',
|
|
'',
|
|
'All external contributions must reference an approved issue or discussion. Please:',
|
|
'1. Find or [open an issue](https://github.com/' + owner + '/' + repo + '/issues/new/choose) describing the change',
|
|
'2. Wait for a maintainer to approve and assign you',
|
|
'3. Add `Fixes #<issue_number>`, `Closes #<issue_number>`, or `Resolves #<issue_number>` to your PR description and the PR will be reopened automatically',
|
|
'',
|
|
'*Maintainers: reopen this PR or remove the `missing-issue-link` label to bypass this check.*',
|
|
];
|
|
} else {
|
|
lines = [
|
|
marker,
|
|
'**This PR has been automatically closed** because you are not assigned to the linked issue.',
|
|
'',
|
|
'External contributors must be assigned to an issue before opening a PR for it. Please:',
|
|
'1. Comment on the linked issue to request assignment from a maintainer',
|
|
'2. Once assigned, your PR will be reopened automatically',
|
|
'',
|
|
'*Maintainers: reopen this PR or remove the `missing-issue-link` label to bypass this check.*',
|
|
];
|
|
}
|
|
|
|
const body = lines.join('\n');
|
|
|
|
// Deduplicate: check for existing comment with the marker
|
|
const comments = await github.paginate(
|
|
github.rest.issues.listComments,
|
|
{ owner, repo, issue_number: prNumber, per_page: 100 },
|
|
);
|
|
const existing = comments.find(c => c.body && c.body.includes(marker));
|
|
|
|
if (!existing) {
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
body,
|
|
});
|
|
console.log('Posted requirement comment');
|
|
} else if (existing.body !== body) {
|
|
await github.rest.issues.updateComment({
|
|
owner,
|
|
repo,
|
|
comment_id: existing.id,
|
|
body,
|
|
});
|
|
console.log('Updated existing comment with new message');
|
|
} else {
|
|
console.log('Comment already exists — skipping');
|
|
}
|
|
|
|
// Close the PR
|
|
if (context.payload.pull_request.state === 'open') {
|
|
await github.rest.pulls.update({
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
state: 'closed',
|
|
});
|
|
console.log(`Closed PR #${prNumber}`);
|
|
}
|
|
|
|
// Cancel all other in-progress and queued workflow runs for this PR
|
|
const headSha = context.payload.pull_request.head.sha;
|
|
for (const status of ['in_progress', 'queued']) {
|
|
const runs = await github.paginate(
|
|
github.rest.actions.listWorkflowRunsForRepo,
|
|
{ owner, repo, head_sha: headSha, status, per_page: 100 },
|
|
);
|
|
for (const run of runs) {
|
|
if (run.id === context.runId) continue;
|
|
try {
|
|
await github.rest.actions.cancelWorkflowRun({
|
|
owner, repo, run_id: run.id,
|
|
});
|
|
console.log(`Cancelled ${status} run ${run.id} (${run.name})`);
|
|
} catch (err) {
|
|
console.log(`Could not cancel run ${run.id}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const reason = !hasLink
|
|
? 'PR must reference an issue using auto-close keywords (e.g., "Fixes #123").'
|
|
: 'PR author must be assigned to the linked issue.';
|
|
core.setFailed(reason);
|