Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 2ec0f0fd78838633cd1c1342e4536d49ef72be54 to a1db7729b356323c7988c20ed6f0d33fe31297be.
- [Release notes](https://github.com/actions/first-interaction/releases)
- [Commits](2ec0f0fd78...a1db7729b3)
---
updated-dependencies:
- dependency-name: actions/first-interaction
dependency-version: a1db7729b356323c7988c20ed6f0d33fe31297be
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
285 lines
11 KiB
YAML
285 lines
11 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;
|
|
async function loadContributorTierPolicy() {
|
|
const fallback = {
|
|
contributorTierColor: "2ED9FF",
|
|
contributorTierRules: [
|
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
|
{ label: "principal contributor", minMergedPRs: 20 },
|
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
|
{ label: "trusted contributor", minMergedPRs: 5 },
|
|
],
|
|
};
|
|
try {
|
|
const { data } = await github.rest.repos.getContent({
|
|
owner,
|
|
repo,
|
|
path: ".github/label-policy.json",
|
|
ref: context.payload.repository?.default_branch || "main",
|
|
});
|
|
const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
|
|
const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
|
|
label: String(entry.label || "").trim(),
|
|
minMergedPRs: Number(entry.min_merged_prs || 0),
|
|
}));
|
|
const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
|
|
if (!contributorTierColor || contributorTierRules.length === 0) {
|
|
return fallback;
|
|
}
|
|
return { contributorTierColor, contributorTierRules };
|
|
} catch (error) {
|
|
core.warning(`failed to load .github/label-policy.json, using fallback policy: ${error.message}`);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
|
|
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
|
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@a1db7729b356323c7988c20ed6f0d33fe31297be # 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",
|
|
});
|
|
}
|