src/drivers/ios/index.ts contains a raw NUL byte, so grep and ripgrep treat it as binary #34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue state | |
| # The one place that enforces docs/internal/agent-rules/delivery.md | |
| # mechanically: | |
| # - one <kind>:<state> label per issue (adding the next label drops the old); | |
| # - task:draft becomes task:ready when approved, specified, and unblocked, and | |
| # task:ready goes back to task:draft when that stops being true; | |
| # - a reporter's reply moves bug:needs-info back to bug:triage, reopening the | |
| # issue if the stale job closed it (agents post under a maintainer's | |
| # account, so comments carrying the agent footer from rule 12 do not count); | |
| # - a feature whose last sub-issue closed gets a note to check completion | |
| # conditions and close; | |
| # - a feature closed as completed closes the request it came from; | |
| # - a PR closed without merge releases the claim on the issue it would close; | |
| # - a claim with no activity for three days is released; | |
| # - bug:needs-info with no activity for two weeks closes as not planned; | |
| # - a PR from a <kind>/<n> branch must close issue <n> and no other. | |
| # Actions taken with the workflow token do not trigger this workflow again, | |
| # so every job removes the label it replaces itself. | |
| on: | |
| issues: | |
| types: [labeled, edited, closed, reopened] | |
| issue_comment: | |
| types: [created] | |
| pull_request: | |
| types: [opened, edited, synchronize, closed] | |
| schedule: | |
| - cron: "17 6 * * *" | |
| workflow_dispatch: | |
| permissions: {} | |
| concurrency: | |
| group: issue-state-${{ github.event.issue.number || github.event.pull_request.number || 'schedule' }} | |
| cancel-in-progress: false | |
| env: | |
| STATE_LABEL: "^(request|bug|feature|task):" | |
| AGENT_FOOTER: "Written by an agent." | |
| jobs: | |
| exclusive-label: | |
| name: One state label per issue | |
| if: github.event_name == 'issues' && github.event.action == 'labeled' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const re = new RegExp(process.env.STATE_LABEL); | |
| const added = context.payload.label.name; | |
| if (!re.test(added)) return; | |
| const issue = context.payload.issue; | |
| for (const label of issue.labels.map((l) => l.name)) { | |
| if (label === added || !re.test(label)) continue; | |
| await github.rest.issues.removeLabel({ | |
| ...context.repo, issue_number: issue.number, name: label, | |
| }); | |
| core.info(`#${issue.number}: ${label} replaced by ${added}`); | |
| } | |
| task-ready: | |
| name: Promote or demote between task:draft and task:ready | |
| if: >- | |
| github.event_name == 'issues' && | |
| (github.event.action == 'closed' || github.event.action == 'reopened' || | |
| (github.event.action == 'edited' && github.event.changes.body != null)) | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const hasLabel = (issue, name) => issue.labels.some((l) => l.name === name); | |
| const section = (body, heading) => { | |
| const m = body.match(new RegExp(`^## ${heading}\\s*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "m")); | |
| return m ? m[1] : null; | |
| }; | |
| const dependencies = (body) => | |
| [...(section(body, "Depends on") ?? "").matchAll(/#(\d+)/g)].map((m) => Number(m[1])); | |
| const specified = (body) => { | |
| const spec = section(body, "Technical spec"); | |
| if (spec === null) return false; | |
| return spec | |
| .replace(/<!--[\s\S]*?-->/g, "") | |
| .split("\n") | |
| .some((line) => line.trim() !== "" && !line.startsWith("#") && line.trim() !== "- ..."); | |
| }; | |
| const approved = (body) => /^- \[x\] Approved for delivery/im.test(body); | |
| const candidates = []; | |
| if (context.payload.action === "edited") { | |
| const issue = context.payload.issue; | |
| if (hasLabel(issue, "task:draft") || hasLabel(issue, "task:ready")) candidates.push(issue); | |
| } else { | |
| const changed = context.payload.issue.number; | |
| for (const labels of ["task:draft", "task:ready"]) { | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| ...context.repo, state: "open", labels, per_page: 100, | |
| }); | |
| for (const issue of issues) { | |
| if (dependencies(issue.body ?? "").includes(changed)) candidates.push(issue); | |
| } | |
| } | |
| } | |
| for (const issue of candidates) { | |
| const body = issue.body ?? ""; | |
| const why = []; | |
| if (!approved(body)) why.push("approval box not ticked"); | |
| if (!specified(body)) why.push("Technical spec section empty"); | |
| const open = []; | |
| for (const n of dependencies(body)) { | |
| const dep = await github.rest.issues.get({ ...context.repo, issue_number: n }); | |
| if (dep.data.state !== "closed") open.push(`#${n}`); | |
| } | |
| if (open.length) why.push(`waiting on ${open.join(", ")}`); | |
| const ready = why.length === 0; | |
| const isReady = hasLabel(issue, "task:ready"); | |
| const issue_number = issue.number; | |
| if (ready && !isReady) { | |
| await github.rest.issues.addLabels({ ...context.repo, issue_number, labels: ["task:ready"] }); | |
| await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: "task:draft" }); | |
| const deps = dependencies(body).map((n) => `#${n}`); | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number, | |
| body: `Now \`task:ready\`: approved, technical spec present${deps.length ? `, ${deps.join(", ")} closed` : ""}.`, | |
| }); | |
| core.info(`#${issue_number}: task:draft -> task:ready`); | |
| } else if (!ready && isReady) { | |
| if (issue.assignees?.length) { | |
| core.info(`#${issue_number} no longer ready (${why.join("; ")}) but is claimed; leaving it`); | |
| continue; | |
| } | |
| await github.rest.issues.addLabels({ ...context.repo, issue_number, labels: ["task:draft"] }); | |
| await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: "task:ready" }); | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number, body: `Back to \`task:draft\`: ${why.join("; ")}.`, | |
| }); | |
| core.info(`#${issue_number}: task:ready -> task:draft`); | |
| } else { | |
| core.info(`#${issue_number} unchanged${why.length ? ` (${why.join("; ")})` : ""}`); | |
| } | |
| } | |
| needs-info-reply: | |
| name: Reporter replied on bug:needs-info | |
| if: >- | |
| github.event_name == 'issue_comment' && | |
| github.event.issue.pull_request == null && | |
| github.event.comment.user.login == github.event.issue.user.login && | |
| !contains(github.event.comment.body, 'Written by an agent.') && | |
| contains(github.event.issue.labels.*.name, 'bug:needs-info') | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const issue_number = context.payload.issue.number; | |
| if (context.payload.issue.state === "closed") { | |
| await github.rest.issues.update({ ...context.repo, issue_number, state: "open" }); | |
| core.info(`#${issue_number}: reopened`); | |
| } | |
| await github.rest.issues.addLabels({ ...context.repo, issue_number, labels: ["bug:triage"] }); | |
| await github.rest.issues.removeLabel({ ...context.repo, issue_number, name: "bug:needs-info" }); | |
| core.info(`#${issue_number}: bug:needs-info -> bug:triage`); | |
| feature-complete: | |
| name: Last sub-issue closed | |
| if: github.event_name == 'issues' && github.event.action == 'closed' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { repository } = await github.graphql(` | |
| query($owner:String!,$repo:String!,$number:Int!){ | |
| repository(owner:$owner,name:$repo){ issue(number:$number){ | |
| parent{ number state labels(first:10){nodes{name}} subIssuesSummary{ total completed } } | |
| }}}`, { ...context.repo, number: context.payload.issue.number }); | |
| const parent = repository.issue.parent; | |
| if (!parent || parent.state !== "OPEN") return; | |
| if (!parent.labels.nodes.some((l) => l.name === "feature:planned")) return; | |
| const { total, completed } = parent.subIssuesSummary; | |
| if (completed < total) return; | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number: parent.number, | |
| body: `All ${total} sub-issues are closed. If the completion conditions hold on \`main\`, close this feature and flip its ADRs to *Accepted*.`, | |
| }); | |
| core.info(`#${parent.number}: all sub-issues closed`); | |
| request-close: | |
| name: Feature completed, close its request | |
| if: >- | |
| github.event_name == 'issues' && github.event.action == 'closed' && | |
| github.event.issue.state_reason == 'completed' && | |
| (contains(github.event.issue.labels.*.name, 'feature:planned') || | |
| contains(github.event.issue.labels.*.name, 'feature:ready') || | |
| contains(github.event.issue.labels.*.name, 'feature:spec')) | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const feature = context.payload.issue; | |
| const m = (feature.body ?? "").match(/^Request:\s*#(\d+)/im); | |
| if (!m) return; | |
| const issue_number = Number(m[1]); | |
| const request = await github.rest.issues.get({ ...context.repo, issue_number }); | |
| if (request.data.state !== "open") return; | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number, body: `Shipped as #${feature.number}.`, | |
| }); | |
| await github.rest.issues.update({ | |
| ...context.repo, issue_number, state: "closed", state_reason: "completed", | |
| }); | |
| core.info(`#${issue_number}: closed, shipped as #${feature.number}`); | |
| pr-closed-unmerged: | |
| name: PR closed without merge releases the claim | |
| if: >- | |
| github.event_name == 'pull_request' && github.event.action == 'closed' && | |
| github.event.pull_request.merged == false | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const closes = [...(pr.body ?? "").matchAll(/\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi)] | |
| .map((x) => Number(x[1])); | |
| for (const issue_number of new Set(closes)) { | |
| const issue = await github.rest.issues.get({ ...context.repo, issue_number }); | |
| if (issue.data.state !== "open") continue; | |
| const assignees = issue.data.assignees.map((a) => a.login); | |
| if (assignees.length) { | |
| await github.rest.issues.removeAssignees({ ...context.repo, issue_number, assignees }); | |
| } | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number, | |
| body: `#${pr.number} was closed without merging. Claim released; the label is unchanged, so the next agent starts from the branch and the last handoff.`, | |
| }); | |
| core.info(`#${issue_number}: claim released after #${pr.number} closed unmerged`); | |
| } | |
| stale-claims: | |
| name: Release claims with no activity | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const days = 3; | |
| const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; | |
| const q = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open -no:assignee label:bug:ready,feature:ready,task:ready`; | |
| const found = await github.paginate(github.rest.search.issuesAndPullRequests, { q, per_page: 100 }); | |
| for (const issue of found) { | |
| let last = new Date(issue.updated_at).getTime(); | |
| const kind = issue.labels.map((l) => l.name).find((n) => /:ready$/.test(n))?.split(":")[0]; | |
| try { | |
| const branch = await github.rest.repos.getBranch({ ...context.repo, branch: `${kind}/${issue.number}` }); | |
| last = Math.max(last, new Date(branch.data.commit.commit.committer.date).getTime()); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| if (last > cutoff) continue; | |
| const assignees = issue.assignees.map((a) => a.login); | |
| await github.rest.issues.removeAssignees({ ...context.repo, issue_number: issue.number, assignees }); | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number: issue.number, | |
| body: `Claim released: no activity for ${days} days. The label is unchanged; the next agent starts from the branch and the last handoff.`, | |
| }); | |
| core.info(`#${issue.number}: stale claim released`); | |
| } | |
| stale-needs-info: | |
| name: Close silent bug:needs-info | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| issues: write | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const cutoff = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); | |
| const q = `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open label:bug:needs-info updated:<${cutoff}`; | |
| const found = await github.paginate(github.rest.search.issuesAndPullRequests, { q, per_page: 100 }); | |
| for (const issue of found) { | |
| await github.rest.issues.createComment({ | |
| ...context.repo, issue_number: issue.number, | |
| body: "Closing: no reply in two weeks. Comment with the missing information and it reopens for triage.", | |
| }); | |
| await github.rest.issues.update({ | |
| ...context.repo, issue_number: issue.number, state: "closed", state_reason: "not_planned", | |
| }); | |
| core.info(`#${issue.number}: closed as not planned (stale bug:needs-info)`); | |
| } | |
| branch-matches-issue: | |
| name: Branch name closes its issue | |
| if: github.event_name == 'pull_request' && github.event.action != 'closed' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const m = pr.head.ref.match(/^(bug|feature|task)\/(\d+)$/); | |
| if (!m) { | |
| core.info(`${pr.head.ref} is not a <kind>/<n> branch; nothing to check`); | |
| return; | |
| } | |
| const expected = Number(m[2]); | |
| const closes = [...(pr.body ?? "").matchAll(/\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi)] | |
| .map((x) => Number(x[1])); | |
| if (!closes.includes(expected)) { | |
| core.setFailed(`Branch ${pr.head.ref} must close #${expected}: add "Closes #${expected}" to the PR body.`); | |
| return; | |
| } | |
| const others = closes.filter((n) => n !== expected); | |
| if (others.length) { | |
| core.setFailed(`Branch ${pr.head.ref} closes #${expected} but also ${others.map((n) => `#${n}`).join(", ")}; one branch closes one issue.`); | |
| } |