Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,3 @@ harness:
- changed-files:
- any-glob-to-any-file:
- "packages/opencode/**"

documentation:
- changed-files:
- any-glob-to-any-file:
- "docs/**"
- "**/*.md"
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Required for visible UI changes.

- [ ] Human review status is stated above as pending, approved, or not required
- [ ] I linked the related issue, or stated why there is no issue
- [ ] This PR has type, primary area, and priority labels, or I requested maintainer labeling
- [ ] This PR has exactly one type label (`bug`, `enhancement`, `task`, or `documentation`), at least one primary routing label (`app`, `ui`, `platform`, `harness`, or `ci`), and exactly one priority label (`P0` to `P3`), or I requested maintainer labeling
- [ ] I described the review focus and any meaningful risks
- [ ] I listed the relevant verification steps and the key result for each
- [ ] I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
Expand Down
56 changes: 56 additions & 0 deletions .github/scripts/label-policy-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export const POLICY = {
Comment thread
Astro-Han marked this conversation as resolved.
priorities: ["P0", "P1", "P2", "P3"],
types: ["bug", "enhancement", "task", "documentation"],
routing: ["app", "ui", "platform", "harness", "ci"],
issueForbiddenLabels: ["dependencies", "github_actions", "javascript"],
}

function intersection(labels, allowed) {
return allowed.filter((label) => labels.has(label))
}

function error(message, labels) {
return { message, labels }
}

function labelList(labels) {
if (labels.length <= 1) return labels.join("")
return `${labels.slice(0, -1).join(", ")}, or ${labels[labels.length - 1]}`
}

export function validateLabelPolicy({ itemType, labels = [] }) {
const labelSet = new Set(labels)
const errors = []

const priorities = intersection(labelSet, POLICY.priorities)
if (priorities.length !== 1) {
errors.push(error(`${itemType} must have exactly one priority label: ${labelList(POLICY.priorities)}`, priorities))
}

const types = intersection(labelSet, POLICY.types)
if (types.length !== 1) {
errors.push(error(`${itemType} must have exactly one type label: ${labelList(POLICY.types)}`, types))
}

const routing = intersection(labelSet, POLICY.routing)
if (routing.length < 1) {
errors.push(error(`${itemType} must have at least one primary routing label: ${labelList(POLICY.routing)}`, routing))
}

if (labelSet.has("tech-debt") && !labelSet.has("task")) {
errors.push(error("tech-debt is only allowed with the task type label", ["tech-debt"]))
}

const forbiddenIssueLabels =
itemType === "issue" ? intersection(labelSet, POLICY.issueForbiddenLabels) : []
if (forbiddenIssueLabels.length > 0) {
errors.push(
error(`issue must not use PR automation labels: ${labelList(POLICY.issueForbiddenLabels)}`, forbiddenIssueLabels),
)
}

return {
ok: errors.length === 0,
errors,
}
}
128 changes: 128 additions & 0 deletions .github/scripts/label-policy-check.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import test from "node:test"
import assert from "node:assert/strict"

import { validateLabelPolicy } from "./label-policy-check.js"

function messages(result) {
return result.errors.map((error) => error.message)
}

test("accepts a valid issue label set", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["task", "P2", "app", "tech-debt"],
})

assert.deepEqual(result.errors, [])
})

test("accepts a valid pull request label set", () => {
const result = validateLabelPolicy({
itemType: "pull_request",
labels: ["enhancement", "P2", "app", "ui"],
})

assert.deepEqual(result.errors, [])
})

test("accepts ci as a primary routing label", () => {
const result = validateLabelPolicy({
itemType: "pull_request",
labels: ["task", "P2", "ci"],
})

assert.deepEqual(result.errors, [])
})

test("rejects missing priority labels", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["bug", "app"],
})

assert.deepEqual(messages(result), ["issue must have exactly one priority label: P0, P1, P2, or P3"])
})

test("treats missing labels input as an empty label set", () => {
const result = validateLabelPolicy({
itemType: "issue",
})

assert.deepEqual(messages(result), [
"issue must have exactly one priority label: P0, P1, P2, or P3",
"issue must have exactly one type label: bug, enhancement, task, or documentation",
"issue must have at least one primary routing label: app, ui, platform, harness, or ci",
])
})

test("rejects multiple priority labels", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["bug", "P1", "P2", "app"],
})

assert.deepEqual(messages(result), ["issue must have exactly one priority label: P0, P1, P2, or P3"])
})

test("rejects missing type labels", () => {
const result = validateLabelPolicy({
itemType: "pull_request",
labels: ["P2", "app"],
})

assert.deepEqual(messages(result), [
"pull_request must have exactly one type label: bug, enhancement, task, or documentation",
])
})

test("rejects multiple type labels", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["bug", "task", "P2", "app"],
})

assert.deepEqual(messages(result), ["issue must have exactly one type label: bug, enhancement, task, or documentation"])
})

test("rejects missing primary routing labels", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["task", "P2"],
})

assert.deepEqual(messages(result), ["issue must have at least one primary routing label: app, ui, platform, harness, or ci"])
})

test("rejects tech-debt outside task issues", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["bug", "P2", "app", "tech-debt"],
})

assert.deepEqual(messages(result), ["tech-debt is only allowed with the task type label"])
})

test("rejects dependency automation labels on issues", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["task", "P3", "app", "dependencies"],
})

assert.deepEqual(messages(result), [
"issue must not use PR automation labels: dependencies, github_actions, or javascript",
])
})

test("reports all independent label policy failures", () => {
const result = validateLabelPolicy({
itemType: "issue",
labels: ["bug", "enhancement", "P1", "P2", "dependencies"],
})

assert.deepEqual(messages(result), [
"issue must have exactly one priority label: P0, P1, P2, or P3",
"issue must have exactly one type label: bug, enhancement, task, or documentation",
"issue must have at least one primary routing label: app, ui, platform, harness, or ci",
"issue must not use PR automation labels: dependencies, github_actions, or javascript",
])
})
53 changes: 53 additions & 0 deletions .github/workflows/label-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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@f28e40c7f34bde8b3046d885e986cb6290c5673b # actions/github-script@v7
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 labels = item.labels.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(", ")}`)
6 changes: 3 additions & 3 deletions packages/opencode/test/github/pr-routing-triage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ const labelerWorkflowPath = path.join(repoRoot, ".github", "workflows", "labeler
const triageWorkflowPath = path.join(repoRoot, ".github", "workflows", "pr-priority-triage.yml")

describe("pr routing workflows", () => {
test("defines labeler routing on current repo labels", () => {
test("defines labeler routing without automatic type labels", () => {
const config = readWorkflow(labelerConfigPath)
expect(config).toContain("ci:")
expect(config).toContain("platform:")
expect(config).toContain("app:")
expect(config).toContain("ui:")
expect(config).toContain("harness:")
expect(config).toContain("documentation:")
expect(config).not.toContain("documentation:")
expect(config).toContain(".github/workflows/**")
expect(config).toContain("packages/desktop-electron/**")
expect(config).toContain("packages/app/**")
expect(config).toContain("packages/opencode/**")
expect(config).toContain("**/*.tsx")
expect(config).toContain("**/*.md")
expect(config).not.toContain("**/*.md")
})

test("pins labeler and triage workflow contracts", () => {
Expand Down
Loading