* docs: harden collaboration policy and review automation * ci(docs): remove unsupported lychee --exclude-mail flag * docs(governance): reduce automation side-effects and tighten risk controls * docs(governance): add backlog pruning and supersede protocol * docs(agents): codify engineering principles and risk-tier workflow * docs(readme): add centered star history section at bottom * docs(agents): enforce privacy-safe and neutral test wording * docs(governance): enforce privacy-safe and neutral collaboration checks * fix(ci): satisfy rustfmt and discord schema test fields * docs(governance): require ZeroClaw-native identity wording * docs(agents): add ZeroClaw identity-safe naming palette * docs(governance): codify code naming and architecture contracts * docs(contributing): add naming and architecture good/bad examples * docs(pr): reduce checkbox TODOs and shift to label-first metadata * docs(pr): remove duplicate collaboration track field * ci(labeler): auto-derive module labels and expand provider hints * ci(labeler): auto-apply trusted contributor on PRs and issues * fix(ci): apply rustfmt updates from latest main * ci(labels): flatten namespaces and add contributor tiers * chore: drop stale rustfmt-only drift * ci: scope Rust and docs checks by change set * ci: exclude non-markdown docs from docs-quality targets * ci: satisfy actionlint shellcheck output style * ci(labels): auto-correct manual contributor tier edits * ci(labeler): auto-correct risk label edits * ci(labeler): auto-correct size label edits --------- Co-authored-by: Chummy <183474434+chumyin@users.noreply.github.com>
251 lines
9.2 KiB
YAML
251 lines
9.2 KiB
YAML
name: Auto Response
|
|
|
|
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: ubuntu-latest
|
|
permissions:
|
|
issues: write
|
|
steps:
|
|
- name: Apply contributor tier label for issue author
|
|
uses: actions/github-script@v7
|
|
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 legacyTrustedContributorLabel = "trusted contributor";
|
|
const contributorTierRules = [
|
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
|
{ label: "principal contributor", minMergedPRs: 20 },
|
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
|
];
|
|
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
|
const contributorTierColor = "39FF14";
|
|
const managedContributorLabels = new Set([
|
|
legacyTrustedContributorLabel,
|
|
...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;
|
|
|
|
async function ensureContributorTierLabels() {
|
|
for (const label of contributorTierLabels) {
|
|
try {
|
|
const { data: existing } = await github.rest.issues.getLabel({ owner, repo, name: label });
|
|
const currentColor = (existing.color || "").toUpperCase();
|
|
if (currentColor !== contributorTierColor) {
|
|
await github.rest.issues.updateLabel({
|
|
owner,
|
|
repo,
|
|
name: label,
|
|
new_name: label,
|
|
color: contributorTierColor,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
await github.rest.issues.createLabel({
|
|
owner,
|
|
repo,
|
|
name: label,
|
|
color: contributorTierColor,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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) => label !== legacyTrustedContributorLabel && !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: ubuntu-latest
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Greet first-time contributors
|
|
uses: actions/first-interaction@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: ubuntu-latest
|
|
permissions:
|
|
issues: write
|
|
pull-requests: write
|
|
steps:
|
|
- name: Handle label-driven responses
|
|
uses: actions/github-script@v7
|
|
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",
|
|
});
|
|
}
|