diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0086358db..3ba13e0ce 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1 @@ -blank_issues_enabled: true +blank_issues_enabled: false diff --git a/.github/workflows/label-policy.yml b/.github/workflows/label-policy.yml deleted file mode 100644 index 0b6d45618..000000000 --- a/.github/workflows/label-policy.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: label-policy - -on: - issues: - types: [opened, edited, labeled, unlabeled, reopened] - pull_request_target: - types: [opened, edited, labeled, unlabeled, synchronize, reopened] - branches: [dev] - -permissions: - contents: read - issues: read - pull-requests: read - -jobs: - label-policy: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6 - if: github.event_name == 'pull_request_target' - with: - persist-credentials: false - ref: ${{ github.event.pull_request.base.sha }} - - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6 - if: github.event_name == 'issues' - with: - persist-credentials: false - - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # actions/github-script@v9.0.0 - with: - script: | - const path = require("node:path") - const { pathToFileURL } = require("node:url") - const policy = await import( - pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, ".github/scripts/label-policy-check.js")).href, - ) - - const item = - context.eventName === "issues" ? context.payload.issue : context.payload.pull_request - const itemType = context.eventName === "issues" ? "issue" : "pull_request" - const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - per_page: 100, - }) - const labels = currentLabels.map((label) => label.name) - const result = policy.validateLabelPolicy({ itemType, labels }) - - if (!result.ok) { - const labelText = labels.length > 0 ? labels.join(", ") : "(none)" - const errorText = result.errors.map((error) => `- ${error.message}`).join("\n") - core.setFailed(`Label policy failed for ${itemType} #${item.number}.\n\nLabels: ${labelText}\n\n${errorText}`) - return - } - - core.info(`Label policy passed for ${itemType} #${item.number}: ${labels.join(", ")}`) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index 8f731d4a5..000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: labeler - -on: - pull_request_target: - types: [opened, synchronize, reopened] - branches: [dev] - -permissions: - contents: read - pull-requests: write - -jobs: - labeler: - runs-on: ubuntu-latest - steps: - - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # actions/labeler@v6.1.0 - with: - sync-labels: true diff --git a/.github/workflows/pr-priority-triage.yml b/.github/workflows/pr-triage.yml similarity index 63% rename from .github/workflows/pr-priority-triage.yml rename to .github/workflows/pr-triage.yml index c69afd270..d4e0eae1f 100644 --- a/.github/workflows/pr-priority-triage.yml +++ b/.github/workflows/pr-triage.yml @@ -1,4 +1,4 @@ -name: pr-priority-triage +name: pr-triage on: pull_request_target: @@ -6,7 +6,7 @@ on: branches: [dev] concurrency: - group: pr-priority-triage-${{ github.event.pull_request.number }} + group: pr-triage-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: @@ -15,10 +15,14 @@ permissions: pull-requests: write jobs: - pr-priority-triage: + pr-triage: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # actions/labeler@v6.1.0 + with: + sync-labels: true + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # actions/checkout@v6 with: persist-credentials: false @@ -34,6 +38,9 @@ jobs: const triage = await import( pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, ".github/scripts/pr-priority-triage.js")).href, ) + const policy = await import( + pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, ".github/scripts/label-policy-check.js")).href, + ) const { buildPriorityReview, planPriorityLabels, TRIAGE_MARKER } = triage const pull_number = Number(process.env.PR_NUMBER) const owner = context.repo.owner @@ -85,18 +92,34 @@ jobs: per_page: 100, }) - if (reviews.some((review) => typeof review.body === "string" && review.body.includes(TRIAGE_MARKER))) { + if (!reviews.some((review) => typeof review.body === "string" && review.body.includes(TRIAGE_MARKER))) { + const review = buildPriorityReview(paths) + await github.rest.pulls.createReview({ + owner, + repo, + pull_number, + event: "COMMENT", + body: review.body, + }) + core.info(`Posted priority recommendation: ${review.priority}`) + } else { core.info("Priority recommendation comment already exists; skipping.") - return } - const review = buildPriorityReview(paths) - await github.rest.pulls.createReview({ + const finalLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, { owner, repo, - pull_number, - event: "COMMENT", - body: review.body, + issue_number: pull_number, + per_page: 100, }) + const labels = finalLabels.map((label) => label.name) + const result = policy.validateLabelPolicy({ itemType: "pull_request", labels }) + + if (!result.ok) { + const labelText = labels.length > 0 ? labels.join(", ") : "(none)" + const errorText = result.errors.map((error) => `- ${error.message}`).join("\n") + core.setFailed(`Label policy failed for pull_request #${pull_number}.\n\nLabels: ${labelText}\n\n${errorText}`) + return + } - core.info(`Posted priority recommendation: ${review.priority}`) + core.info(`Label policy passed for pull_request #${pull_number}: ${labels.join(", ")}`) diff --git a/packages/opencode/test/github/label-policy-workflow.test.ts b/packages/opencode/test/github/label-policy-workflow.test.ts deleted file mode 100644 index 55e8e0fd3..000000000 --- a/packages/opencode/test/github/label-policy-workflow.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, test } from "bun:test" -import path from "node:path" -import { parseWorkflow, readWorkflow } from "./workflow-parser" - -const repoRoot = path.join(import.meta.dir, "../../../..") -const workflowPath = path.join(repoRoot, ".github", "workflows", "label-policy.yml") - -describe("label policy workflow", () => { - test("validates current labels instead of labeled event snapshots", () => { - const workflow = readWorkflow(workflowPath) - const parsed = parseWorkflow(workflowPath) - const job = parsed.jobs?.["label-policy"] - const steps = job?.steps ?? [] - const scriptStep = steps.find((step) => step.uses?.startsWith("actions/github-script@")) - - expect(parsed.name).toBe("label-policy") - expect(parsed.permissions).toEqual({ - contents: "read", - issues: "read", - "pull-requests": "read", - }) - expect(scriptStep?.uses).toBe("actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3") - expect(workflow).toContain("github.rest.issues.listLabelsOnIssue") - expect(workflow).toContain("issue_number: item.number") - expect(workflow).toContain("const labels = currentLabels.map((label) => label.name)") - expect(workflow).not.toContain("const labels = item.labels.map((label) => label.name)") - }) -}) diff --git a/packages/opencode/test/github/pr-routing-triage.test.ts b/packages/opencode/test/github/pr-triage-workflow.test.ts similarity index 84% rename from packages/opencode/test/github/pr-routing-triage.test.ts rename to packages/opencode/test/github/pr-triage-workflow.test.ts index afb1a0b97..5e338c741 100644 --- a/packages/opencode/test/github/pr-routing-triage.test.ts +++ b/packages/opencode/test/github/pr-triage-workflow.test.ts @@ -11,8 +11,7 @@ import { parseWorkflow, readWorkflow } from "./workflow-parser" const repoRoot = path.join(import.meta.dir, "../../../..") const labelerConfigPath = path.join(repoRoot, ".github", "labeler.yml") -const labelerWorkflowPath = path.join(repoRoot, ".github", "workflows", "labeler.yml") -const triageWorkflowPath = path.join(repoRoot, ".github", "workflows", "pr-priority-triage.yml") +const triageWorkflowPath = path.join(repoRoot, ".github", "workflows", "pr-triage.yml") type LabelerRule = { "changed-files"?: Array<{ @@ -35,7 +34,7 @@ function labelerLabelsForGlob(glob: string) { .map(([label]) => label) } -describe("pr routing workflows", () => { +describe("pr triage workflow", () => { test("defines labeler routing for repo areas and workflow policy labels", () => { const config = readWorkflow(labelerConfigPath) expect(config).toContain("ci:") @@ -61,32 +60,15 @@ describe("pr routing workflows", () => { expect(validateLabelPolicy({ itemType: "pull_request", labels: [...labels, "P3"] }).ok).toBe(true) }) - test("pins labeler and triage workflow contracts", () => { - const labelerWorkflow = readWorkflow(labelerWorkflowPath) - const labelerParsed = parseWorkflow(labelerWorkflowPath) - const labelerSteps = labelerParsed.jobs?.labeler?.steps ?? [] - const labelerStep = labelerSteps[0] - - expect(labelerParsed.name).toBe("labeler") - expect(labelerParsed.on?.pull_request_target).toEqual({ - types: ["opened", "synchronize", "reopened"], - branches: ["dev"], - }) - expect(labelerParsed.permissions).toEqual({ - contents: "read", - "pull-requests": "write", - }) - expect(labelerStep?.uses).toBe("actions/labeler@f27b608878404679385c85cfa523b85ccb86e213") - expect(labelerStep?.with).toEqual({ "sync-labels": true }) - expect(labelerWorkflow).not.toContain("persist-credentials: true") - + test("pins pr-triage workflow contract: labeler, priority, and policy run serially", () => { const triageWorkflow = readWorkflow(triageWorkflowPath) const triageParsed = parseWorkflow(triageWorkflowPath) - const triageSteps = triageParsed.jobs?.["pr-priority-triage"]?.steps ?? [] + const triageSteps = triageParsed.jobs?.["pr-triage"]?.steps ?? [] + const labelerStep = triageSteps.find((step) => step.uses?.startsWith("actions/labeler@")) const checkout = triageSteps.find((step) => step.uses?.startsWith("actions/checkout@")) const script = triageSteps.find((step) => step.uses?.startsWith("actions/github-script@")) - expect(triageParsed.name).toBe("pr-priority-triage") + expect(triageParsed.name).toBe("pr-triage") expect(triageParsed.on?.pull_request_target).toEqual({ types: ["opened", "synchronize", "reopened"], branches: ["dev"], @@ -96,14 +78,19 @@ describe("pr routing workflows", () => { issues: "write", "pull-requests": "write", }) - expect(triageParsed.concurrency?.group).toBe("pr-priority-triage-${{ github.event.pull_request.number }}") + expect(triageParsed.concurrency?.group).toBe("pr-triage-${{ github.event.pull_request.number }}") expect(triageParsed.concurrency?.["cancel-in-progress"]).toBe(true) - expect(checkout?.uses).toBe("actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd") + + const stepUses = triageSteps.map((step) => step.uses).filter((value): value is string => Boolean(value)) + expect(stepUses[0]).toBe("actions/labeler@f27b608878404679385c85cfa523b85ccb86e213") + expect(stepUses[1]).toBe("actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd") + expect(stepUses[2]).toBe("actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3") + + expect(labelerStep?.with).toEqual({ "sync-labels": true }) expect(checkout?.with).toEqual({ "persist-credentials": false, ref: "${{ github.event.pull_request.base.sha }}", }) - expect(script?.uses).toBe("actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3") expect(script?.env).toEqual({ PR_NUMBER: "${{ github.event.pull_request.number }}" }) expect(script?.run).toBeUndefined() expect(triageWorkflow).toContain('event: "COMMENT"') @@ -114,11 +101,13 @@ describe("pr routing workflows", () => { expect(triageWorkflow).toContain("github.rest.issues.removeLabel") expect(triageWorkflow).not.toContain("github.rest.issues.setLabels") expect(triageWorkflow).toContain("planPriorityLabels") + expect(triageWorkflow).toContain("validateLabelPolicy") expect(triageWorkflow).toContain("github.rest.pulls.listReviews") expect(triageWorkflow).toContain("pathToFileURL") expect(triageWorkflow).toContain("process.env.GITHUB_WORKSPACE") expect(triageWorkflow).toContain("await import(") expect(triageWorkflow).not.toContain('require("./.github/scripts/pr-priority-triage.js")') + expect(triageWorkflow).not.toContain("persist-credentials: true") }) })