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: 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 = ``; 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", }); }