* fix(workflows): standardize runner configuration for security jobs * ci(actionlint): add Blacksmith runner label to config Add blacksmith-2vcpu-ubuntu-2404 to actionlint self-hosted-runner labels config to suppress "unknown label" warnings during workflow linting. This label is used across all workflows after the Blacksmith migration. * fix(actionlint): adjust indentation for self-hosted runner labels * feat(security): enhance security workflow with CodeQL analysis steps * fix(security): update CodeQL action to version 4 for improved analysis * fix(security): remove duplicate permissions in security workflow * fix(security): revert CodeQL action to v3 for stability The v4 version was causing workflow file validation failures. Reverting to proven v3 version that is working on main branch. * fix(security): remove duplicate permissions causing workflow validation failure The permissions block had duplicate security-events and actions keys, which caused YAML validation errors and prevented workflow execution. Fixes: workflow file validation failures on main branch * fix(security): remove pull_request trigger to reduce costs * fix(security): restore PR trigger but skip codeql on PRs * fix(security): resolve YAML syntax error in security workflow * refactor(security): split CodeQL into dedicated scheduled workflow * fix(security): update workflow name to Rust Package Security Audit * fix(codeql): remove push trigger, keep schedule and on-demand only * feat(codeql): add CodeQL configuration file to ignore specific paths * Potential fix for code scanning alert no. 39: Hard-coded cryptographic value Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(ci): resolve auto-response workflow merge markers * fix(build): restore ChannelMessage reply_target usage * ci(workflows): run workflow sanity on workflow pushes for all branches * ci(workflows): rename auto-response workflow to PR Auto Responder * ci(workflows): require owner approval for workflow file changes --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
258 lines
9.8 KiB
YAML
258 lines
9.8 KiB
YAML
name: PR Auto Responder
|
|
|
|
on:
|
|
issues:
|
|
types: [opened, reopened, labeled, unlabeled]
|
|
pull_request_target:
|
|
types: [opened, labeled, unlabeled]
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
contributor-tier-issues:
|
|
if: >-
|
|
(github.event_name == 'issues' &&
|
|
(github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')) ||
|
|
(github.event_name == 'pull_request_target' &&
|
|
(github.event.action == 'labeled' || github.event.action == 'unlabeled'))
|
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Apply contributor tier label for issue author
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const issue = context.payload.issue;
|
|
const pullRequest = context.payload.pull_request;
|
|
const target = issue ?? pullRequest;
|
|
const contributorTierRules = [
|
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
|
{ label: "principal contributor", minMergedPRs: 20 },
|
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
|
{ label: "trusted contributor", minMergedPRs: 5 },
|
|
];
|
|
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
|
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml
|
|
const managedContributorLabels = new Set(contributorTierLabels);
|
|
const action = context.payload.action;
|
|
const changedLabel = context.payload.label?.name;
|
|
|
|
if (!target) return;
|
|
if ((action === "labeled" || action === "unlabeled") && !managedContributorLabels.has(changedLabel)) {
|
|
return;
|
|
}
|
|
|
|
const author = target.user;
|
|
if (!author || author.type === "Bot") return;
|
|
|
|
function contributorTierDescription(rule) {
|
|
return `Contributor with ${rule.minMergedPRs}+ merged PRs.`;
|
|
}
|
|
|
|
async function ensureContributorTierLabels() {
|
|
for (const rule of contributorTierRules) {
|
|
const label = rule.label;
|
|
const expectedDescription = contributorTierDescription(rule);
|
|
try {
|
|
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label });
|
|
const currentColor = (existing.color || "").toUpperCase();
|
|
const currentDescription = (existing.description || "").trim();
|
|
if (currentColor !== contributorTierColor || currentDescription !== expectedDescription) {
|
|
await github.rest.issues.updateLabel({
|
|
owner,
|
|
repo,
|
|
name: label,
|
|
new_name: label,
|
|
color: contributorTierColor,
|
|
description: expectedDescription,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
await github.rest.issues.createLabel({
|
|
owner,
|
|
repo,
|
|
name: label,
|
|
color: contributorTierColor,
|
|
description: expectedDescription,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function selectContributorTier(mergedCount) {
|
|
const matchedTier = contributorTierRules.find((rule) => mergedCount >= rule.minMergedPRs);
|
|
return matchedTier ? matchedTier.label : null;
|
|
}
|
|
|
|
let contributorTierLabel = null;
|
|
try {
|
|
const { data: mergedSearch } = await github.rest.search.issuesAndPullRequests({
|
|
q: `repo:${owner}/${repo} is:pr is:merged author:${author.login}`,
|
|
per_page: 1,
|
|
});
|
|
const mergedCount = mergedSearch.total_count || 0;
|
|
contributorTierLabel = selectContributorTier(mergedCount);
|
|
} catch (error) {
|
|
core.warning(`failed to evaluate contributor tier status: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
await ensureContributorTierLabels();
|
|
|
|
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
|
owner,
|
|
repo,
|
|
issue_number: target.number,
|
|
});
|
|
const keepLabels = currentLabels
|
|
.map((label) => label.name)
|
|
.filter((label) => !contributorTierLabels.includes(label));
|
|
|
|
if (contributorTierLabel) {
|
|
keepLabels.push(contributorTierLabel);
|
|
}
|
|
|
|
await github.rest.issues.setLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: target.number,
|
|
labels: [...new Set(keepLabels)],
|
|
});
|
|
|
|
first-interaction:
|
|
if: github.event.action == 'opened'
|
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Greet first-time contributors
|
|
uses: actions/first-interaction@2ec0f0fd78838633cd1c1342e4536d49ef72be54 # v1
|
|
with:
|
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
issue-message: |
|
|
Thanks for opening this issue.
|
|
|
|
Before maintainers triage it, please confirm:
|
|
- Repro steps are complete and run on latest `main`
|
|
- Environment details are included (OS, Rust version, ZeroClaw version)
|
|
- Sensitive values are redacted
|
|
|
|
This helps us keep issue throughput high and response latency low.
|
|
pr-message: |
|
|
Thanks for contributing to ZeroClaw.
|
|
|
|
For faster review, please ensure:
|
|
- PR template sections are fully completed
|
|
- `cargo fmt --all -- --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` are included
|
|
- If automation/agents were used heavily, add brief workflow notes
|
|
- Scope is focused (prefer one concern per PR)
|
|
|
|
See `CONTRIBUTING.md` and `docs/pr-workflow.md` for full collaboration rules.
|
|
|
|
labeled-routes:
|
|
if: github.event.action == 'labeled'
|
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Handle label-driven responses
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
with:
|
|
script: |
|
|
const label = context.payload.label?.name;
|
|
if (!label) return;
|
|
|
|
const issue = context.payload.issue;
|
|
const pullRequest = context.payload.pull_request;
|
|
const target = issue ?? pullRequest;
|
|
if (!target) return;
|
|
|
|
const isIssue = Boolean(issue);
|
|
const issueNumber = target.number;
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
|
|
const rules = [
|
|
{
|
|
label: "r:support",
|
|
close: true,
|
|
closeIssuesOnly: true,
|
|
closeReason: "not_planned",
|
|
message:
|
|
"This looks like a usage/support request. Please use README + docs first, then open a focused bug with repro details if behavior is incorrect.",
|
|
},
|
|
{
|
|
label: "r:needs-repro",
|
|
close: false,
|
|
message:
|
|
"Thanks for the report. Please add deterministic repro steps, exact environment, and redacted logs so maintainers can triage quickly.",
|
|
},
|
|
{
|
|
label: "invalid",
|
|
close: true,
|
|
closeIssuesOnly: true,
|
|
closeReason: "not_planned",
|
|
message:
|
|
"Closing as invalid based on current information. If this is still relevant, open a new issue with updated evidence and reproducible steps.",
|
|
},
|
|
{
|
|
label: "duplicate",
|
|
close: true,
|
|
closeIssuesOnly: true,
|
|
closeReason: "not_planned",
|
|
message:
|
|
"Closing as duplicate. Please continue discussion in the canonical linked issue/PR.",
|
|
},
|
|
];
|
|
|
|
const rule = rules.find((entry) => entry.label === label);
|
|
if (!rule) return;
|
|
|
|
const marker = `<!-- auto-response:${rule.label} -->`;
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: issueNumber,
|
|
per_page: 100,
|
|
});
|
|
|
|
const alreadyCommented = comments.some((comment) =>
|
|
(comment.body || "").includes(marker)
|
|
);
|
|
|
|
if (!alreadyCommented) {
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: issueNumber,
|
|
body: `${rule.message}\n\n${marker}`,
|
|
});
|
|
}
|
|
|
|
if (!rule.close) return;
|
|
if (rule.closeIssuesOnly && !isIssue) return;
|
|
if (target.state === "closed") return;
|
|
|
|
if (isIssue) {
|
|
await github.rest.issues.update({
|
|
owner,
|
|
repo,
|
|
issue_number: issueNumber,
|
|
state: "closed",
|
|
state_reason: rule.closeReason || "not_planned",
|
|
});
|
|
} else {
|
|
await github.rest.issues.update({
|
|
owner,
|
|
repo,
|
|
issue_number: issueNumber,
|
|
state: "closed",
|
|
});
|
|
}
|