mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* Improve bug report template for desktop, remote, and mobile Capture where users hit the bug (local desktop, SSH, remote server, WSL, mobile, web) without extra host jargon, and collect remote OS when relevant. Extend the OS labeler for mobile. * Reconcile OS and mobile labels from the bug form's current state Form: one "Where were you using Orca?" choice (Desktop only, + SSH host, + Orca server, + WSL, Mobile app, Web browser, Other), device OS with iOS and Android, optional remote host OS, shorter help copy and per-platform version instructions. Main's English request is kept. Labeler: read the issue as it is now instead of the webhook snapshot, recognize only a bug form at the very start of the body (optionally after the exact machine-translation note line), and stop at the free-text heading. Unknown, repeated, out-of-order or multi-line metadata leaves labels untouched. Only os:macos, os:Windows, os:linux, os:mobile and mobile are ever added or removed, one label at a time, so labels added concurrently by people or other workflows survive. Each pass is verified by a re-read and retried up to three times; runs are serialized per issue. Fixes stale mobile labels after an edit to desktop, stale OS labels after an edit to Other or blank, empty sections reading the next heading as a value, quoted form sections relabeling unrelated issues, and prototype names such as "constructor" being sent as labels. Retired Where option strings and the Client / Host operating system headings keep working for open issues. Tests run the workflow's inline script against a stateful mock of the issues API. * Trim OS labeler tests to behaviour that fails when the fix is reverted Review finding F11140-C1-1: several added tests pinned form copy, option inventories, schema keys, workflow constants or script source, and kept passing with the production change reverted. - Delete bug-report-issue-form.test.mjs (schema keys, copy, option lists, About-menu source regexes). The form/labeler contract is kept as one behavioural case: every option the shipped form offers is recognised. - Drop the workflow-constant and script-source regex tests, the log-copy assertion, the note-length check and the plain-string label case. - Replace the Where x OS matrix (with its test-side oracle) and the other Cartesian tables with one literal case per distinct behaviour. - Point negative fixtures at an OS the old script would label, so each kept case fails against the reverted workflow rather than passing because the old script skipped iOS. 220 -> 59 tests; all 59 fail with only the workflow reverted to main. Test-only change; no runtime path changed. * Keep the shadowed legacy value case in the heading-presence test It covers a distinct rule (an invalid legacy value is ignored when the current heading is present) and was dropped by mistake in the trim.
134 lines
4.4 KiB
JavaScript
134 lines
4.4 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import { runInNewContext } from 'node:vm'
|
|
import { parse } from 'yaml'
|
|
|
|
export const MANAGED_LABELS = ['os:macos', 'os:Windows', 'os:linux', 'os:mobile', 'mobile']
|
|
export const ISSUE_NUMBER = 4242
|
|
|
|
const labelerScript = parse(readFileSync('.github/workflows/issue-os-labeler.yaml', 'utf8')).jobs[
|
|
'apply-os-label'
|
|
].steps[0].with.script
|
|
|
|
export function readIssueForm(name) {
|
|
return parse(readFileSync(`.github/ISSUE_TEMPLATE/${name}.yml`, 'utf8'))
|
|
}
|
|
|
|
export const bugForm = readIssueForm('bug_report')
|
|
|
|
// GitHub renders each non-markdown form field as "### <label>", blank line, value.
|
|
export function renderSections(sections) {
|
|
return sections
|
|
.map(([heading, value]) => `### ${heading}\n\n${value === '' ? '_No response_' : value}`)
|
|
.join('\n\n')
|
|
}
|
|
|
|
function formHeadings(form) {
|
|
return form.body
|
|
.filter((field) => field.type !== 'markdown')
|
|
.map((field) => field.attributes.label)
|
|
}
|
|
|
|
// Sections follow the form's real labels and order; unspecified fields are unselected.
|
|
export function renderFormBody(form, valuesByHeading = {}) {
|
|
const headings = formHeadings(form)
|
|
const unknown = Object.keys(valuesByHeading).filter((heading) => !headings.includes(heading))
|
|
if (unknown.length > 0) {
|
|
throw new Error(`Form has no field labelled: ${unknown.join(', ')}`)
|
|
}
|
|
return renderSections(headings.map((heading) => [heading, valuesByHeading[heading] ?? '']))
|
|
}
|
|
|
|
export function renderBugBody(values = {}) {
|
|
return renderFormBody(bugForm, { Details: 'It broke.', ...values })
|
|
}
|
|
|
|
function notFound() {
|
|
return Object.assign(new Error('Not Found'), { status: 404 })
|
|
}
|
|
|
|
// In-memory issue behind the REST calls the labeler may make. `onCall` runs before each call
|
|
// takes effect, so a test can edit the issue mid-run or throw to inject an API failure.
|
|
export function createFakeIssue({ body, labels = [], number = ISSUE_NUMBER }) {
|
|
const issue = { number, body, labels: [...labels], deleted: false, calls: [], onCall: null }
|
|
|
|
const endpoint = (method, apply) => async (args) => {
|
|
const call = { method, args, index: issue.calls.length }
|
|
issue.calls.push(call)
|
|
await issue.onCall?.(call, issue)
|
|
if (issue.deleted) {
|
|
throw notFound()
|
|
}
|
|
return apply(args)
|
|
}
|
|
|
|
issue.rest = {
|
|
issues: {
|
|
get: endpoint('get', () => ({
|
|
data: {
|
|
number: issue.number,
|
|
body: issue.body,
|
|
labels: issue.labels.map((name) => ({ name }))
|
|
}
|
|
})),
|
|
addLabels: endpoint('addLabels', ({ labels: added }) => {
|
|
issue.labels = [...new Set([...issue.labels, ...added])]
|
|
return { data: issue.labels.map((name) => ({ name })) }
|
|
}),
|
|
removeLabel: endpoint('removeLabel', ({ name }) => {
|
|
if (!issue.labels.includes(name)) {
|
|
throw notFound()
|
|
}
|
|
issue.labels = issue.labels.filter((label) => label !== name)
|
|
return { data: issue.labels.map((label) => ({ name: label })) }
|
|
}),
|
|
setLabels: endpoint('setLabels', ({ labels: next }) => {
|
|
issue.labels = [...new Set(next)]
|
|
return { data: [] }
|
|
}),
|
|
removeAllLabels: endpoint('removeAllLabels', () => {
|
|
issue.labels = []
|
|
return { data: [] }
|
|
})
|
|
}
|
|
}
|
|
issue.writes = () => issue.calls.filter((call) => call.method !== 'get')
|
|
return issue
|
|
}
|
|
|
|
// Runs a workflow script exactly as actions/github-script does: as an async function body.
|
|
export function runLabelerScript(issue, { payloadIssue } = {}) {
|
|
const core = {
|
|
info: () => {},
|
|
warning: () => {},
|
|
setFailed: (message) => {
|
|
throw new Error(message)
|
|
}
|
|
}
|
|
const context = {
|
|
repo: { owner: 'owner', repo: 'repo' },
|
|
payload: {
|
|
// Webhook snapshot; defaults to the issue as it was when the run was queued.
|
|
issue: payloadIssue ?? {
|
|
number: issue.number,
|
|
body: issue.body,
|
|
labels: issue.labels.map((name) => ({ name }))
|
|
}
|
|
}
|
|
}
|
|
return runInNewContext(`(async () => { ${labelerScript}\n})()`, {
|
|
context,
|
|
github: { rest: issue.rest },
|
|
core
|
|
})
|
|
}
|
|
|
|
// Runs the labeler on a fresh issue and reports the complete outcome.
|
|
export async function labelIssue(body, labels = [], options = {}) {
|
|
const issue = createFakeIssue({ body, labels })
|
|
await runLabelerScript(issue, options)
|
|
return {
|
|
labels: issue.labels,
|
|
writes: issue.writes().map(({ method, args }) => [method, args.labels ?? args.name])
|
|
}
|
|
}
|