@tscircuit/schematic-trace-solver 0.0.76 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.github/ISSUE_TEMPLATE/json-bug-report.yml +24 -0
  2. package/.github/scripts/import-json-bug-report.ts +483 -0
  3. package/.github/workflows/json-bug-report.yml +154 -0
  4. package/dist/index.d.ts +32 -10
  5. package/dist/index.js +226 -74
  6. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +4 -0
  7. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -0
  8. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +6 -0
  9. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +21 -1
  10. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver_visualize.ts +9 -3
  11. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +27 -2
  12. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver.ts +1 -0
  13. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +121 -30
  14. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +10 -10
  15. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +5 -8
  16. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +31 -6
  17. package/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts +11 -0
  18. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +3 -0
  19. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +1 -0
  20. package/lib/solvers/VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver.ts +3 -0
  21. package/lib/solvers/VccNetLabelCornerPlacementSolver/types.ts +1 -0
  22. package/lib/utils/textBoxBounds.ts +40 -0
  23. package/package.json +1 -1
  24. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +88 -0
  25. package/tests/repros/manufacturePartNumber-text-box.test.ts +57 -0
@@ -0,0 +1,24 @@
1
+ name: JSON Bug Report
2
+ description: Attach a schematic trace solver input JSON file for a generated snapshot regression test.
3
+ title: "JSON Bug Report: "
4
+ labels: ["json-bug-report"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ Attach a `.json` file containing a `SchematicTracePipelineSolver` input problem. The automation will validate the file, generate a snapshot-only regression test, and open a pull request.
10
+ - type: textarea
11
+ id: json-file
12
+ attributes:
13
+ label: Solver input JSON attachment
14
+ description: Drag and drop exactly one `.json` file into this field.
15
+ placeholder: Attach the JSON file here.
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: notes
20
+ attributes:
21
+ label: Notes
22
+ description: Add any context that helps explain the solver behavior.
23
+ validations:
24
+ required: false
@@ -0,0 +1,483 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ type IssueEvent = {
5
+ issue: {
6
+ number: number
7
+ created_at: string
8
+ body?: string | null
9
+ labels?: Array<string | { name?: string }>
10
+ }
11
+ }
12
+
13
+ type JsonLinkCandidate = {
14
+ label?: string
15
+ url: string
16
+ }
17
+
18
+ const validDirections = new Set(["x+", "x-", "y+", "y-"])
19
+
20
+ const repoRoot = process.cwd()
21
+ const eventPath = process.env["GITHUB_EVENT_PATH"]
22
+ const githubOutputPath = process.env["GITHUB_OUTPUT"]
23
+ const githubToken =
24
+ process.env["TSCIRCUIT_BOT_GITHUB_TOKEN"] || process.env["GITHUB_TOKEN"]
25
+ const maxAttachmentBytes = 10 * 1024 * 1024
26
+
27
+ async function main() {
28
+ if (!eventPath) {
29
+ throw new Error("GITHUB_EVENT_PATH is not set")
30
+ }
31
+
32
+ const event = JSON.parse(fs.readFileSync(eventPath, "utf-8")) as IssueEvent
33
+ const issue = event.issue
34
+
35
+ if (!issue) {
36
+ throw new Error("This workflow must be run from an issue event")
37
+ }
38
+
39
+ const body = issue.body ?? ""
40
+ if (!isJsonBugReportIssue(issue, body)) {
41
+ writeOutput("skipped", "true")
42
+ writeOutput("skip_reason", "Issue is not from the JSON Bug Report form")
43
+ console.log("Skipping issue because it is not a JSON Bug Report")
44
+ return
45
+ }
46
+
47
+ const timestamp = getTimestampFromCreatedAt(issue.created_at)
48
+ const reportId = `bug-report-${timestamp}`
49
+ const branchName = `json-bug-report/${reportId}`
50
+ const reportDir = path.join("tests", "bug-reports", reportId)
51
+ const reportDirAbs = path.join(repoRoot, reportDir)
52
+ const jsonPath = path.join(reportDir, `${reportId}.json`)
53
+ const testPath = path.join(reportDir, `${reportId}.test.ts`)
54
+
55
+ writeOutput("report_id", reportId)
56
+ writeOutput("branch_name", branchName)
57
+ writeOutput("report_dir", reportDir)
58
+ writeOutput("json_path", jsonPath)
59
+ writeOutput("test_path", testPath)
60
+
61
+ if (fs.existsSync(reportDirAbs)) {
62
+ writeOutput("created", "false")
63
+ writeOutput("skipped", "true")
64
+ writeOutput("skip_reason", `${reportDir} already exists`)
65
+ console.log(`${reportDir} already exists; no files generated`)
66
+ return
67
+ }
68
+
69
+ const jsonAttachment = await getJsonAttachment(body)
70
+ assertInputProblem(jsonAttachment.value)
71
+
72
+ fs.mkdirSync(reportDirAbs, { recursive: true })
73
+ fs.writeFileSync(
74
+ path.join(repoRoot, jsonPath),
75
+ `${JSON.stringify(jsonAttachment.value, null, 2)}\n`,
76
+ )
77
+ fs.writeFileSync(path.join(repoRoot, testPath), getTestFile(reportId))
78
+
79
+ writeOutput("created", "true")
80
+ writeOutput("attachment_url", jsonAttachment.url)
81
+ console.log(`Generated ${jsonPath} and ${testPath}`)
82
+ }
83
+
84
+ function isJsonBugReportIssue(issue: IssueEvent["issue"], body: string) {
85
+ const labelNames = issue.labels?.map((label) => {
86
+ if (typeof label === "string") return label
87
+ return label.name ?? ""
88
+ })
89
+
90
+ return (
91
+ labelNames?.includes("json-bug-report") ||
92
+ body.includes("### Solver input JSON attachment")
93
+ )
94
+ }
95
+
96
+ function getTimestampFromCreatedAt(createdAt: string) {
97
+ const date = new Date(createdAt)
98
+
99
+ if (Number.isNaN(date.getTime())) {
100
+ throw new Error(`Could not parse issue created_at timestamp: ${createdAt}`)
101
+ }
102
+
103
+ return date
104
+ .toISOString()
105
+ .replace(/\.\d{3}Z$/, "Z")
106
+ .replaceAll("-", "")
107
+ .replaceAll(":", "")
108
+ }
109
+
110
+ async function getJsonAttachment(body: string) {
111
+ const candidates = getJsonLinkCandidates(body)
112
+
113
+ if (candidates.length === 0) {
114
+ throw new Error(
115
+ "No .json GitHub user attachment was found in the issue body. Attach a JSON file to the 'Solver input JSON attachment' field and edit the issue to retry.",
116
+ )
117
+ }
118
+
119
+ const errors: string[] = []
120
+
121
+ for (const candidate of candidates) {
122
+ try {
123
+ const content = await downloadGitHubUserAttachment(candidate.url)
124
+ return {
125
+ url: candidate.url,
126
+ value: JSON.parse(content),
127
+ }
128
+ } catch (error) {
129
+ errors.push(
130
+ `${candidate.label ?? candidate.url}: ${getErrorMessage(error)}`,
131
+ )
132
+ }
133
+ }
134
+
135
+ throw new Error(
136
+ `Could not download and parse a JSON attachment.\n\n${errors.join("\n")}`,
137
+ )
138
+ }
139
+
140
+ function getJsonLinkCandidates(body: string): JsonLinkCandidate[] {
141
+ const candidates: JsonLinkCandidate[] = []
142
+ const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g
143
+ let markdownMatch = markdownLinkRegex.exec(body)
144
+
145
+ while (markdownMatch) {
146
+ candidates.push({
147
+ label: markdownMatch[1],
148
+ url: cleanupUrl(markdownMatch[2]!),
149
+ })
150
+ markdownMatch = markdownLinkRegex.exec(body)
151
+ }
152
+
153
+ const bareUrlRegex = /https?:\/\/[^\s<>)"']+/g
154
+ let bareUrlMatch = bareUrlRegex.exec(body)
155
+
156
+ while (bareUrlMatch) {
157
+ candidates.push({ url: cleanupUrl(bareUrlMatch[0]) })
158
+ bareUrlMatch = bareUrlRegex.exec(body)
159
+ }
160
+
161
+ const seen = new Set<string>()
162
+ const filtered: JsonLinkCandidate[] = []
163
+
164
+ for (const candidate of candidates) {
165
+ if (!isAllowedGitHubUserAttachment(candidate.url)) continue
166
+ if (!isJsonAttachmentCandidate(candidate)) continue
167
+ if (seen.has(candidate.url)) continue
168
+ seen.add(candidate.url)
169
+ filtered.push(candidate)
170
+ }
171
+
172
+ return filtered
173
+ }
174
+
175
+ function cleanupUrl(url: string) {
176
+ return url.replace(/[.,;]+$/, "")
177
+ }
178
+
179
+ function isAllowedGitHubUserAttachment(url: string) {
180
+ try {
181
+ const parsed = new URL(url)
182
+ return (
183
+ parsed.protocol === "https:" &&
184
+ parsed.hostname === "github.com" &&
185
+ parsed.pathname.startsWith("/user-attachments/")
186
+ )
187
+ } catch {
188
+ return false
189
+ }
190
+ }
191
+
192
+ function isJsonAttachmentCandidate(candidate: JsonLinkCandidate) {
193
+ return [candidate.label, candidate.url].some((value) =>
194
+ /\.json(?:[?#].*)?$/i.test(value ?? ""),
195
+ )
196
+ }
197
+
198
+ async function downloadGitHubUserAttachment(url: string, redirectCount = 0) {
199
+ if (redirectCount > 5) {
200
+ throw new Error("Too many redirects while downloading attachment")
201
+ }
202
+
203
+ const parsed = new URL(url)
204
+ const headers: Record<string, string> = {
205
+ Accept: "application/octet-stream",
206
+ "User-Agent": "schematic-trace-solver-json-bug-report-importer",
207
+ }
208
+
209
+ if (githubToken && parsed.hostname === "github.com") {
210
+ headers["Authorization"] = `Bearer ${githubToken}`
211
+ }
212
+
213
+ const response = await fetch(url, {
214
+ headers,
215
+ redirect: "manual",
216
+ })
217
+
218
+ if (response.status >= 300 && response.status < 400) {
219
+ const location = response.headers.get("location")
220
+
221
+ if (!location) {
222
+ throw new Error(
223
+ `Attachment download redirected without a Location header`,
224
+ )
225
+ }
226
+
227
+ return downloadGitHubUserAttachment(
228
+ new URL(location, url).toString(),
229
+ redirectCount + 1,
230
+ )
231
+ }
232
+
233
+ if (!response.ok) {
234
+ throw new Error(
235
+ `Attachment download failed with HTTP ${response.status} ${response.statusText}`,
236
+ )
237
+ }
238
+
239
+ const contentLength = Number(response.headers.get("content-length"))
240
+
241
+ if (Number.isFinite(contentLength) && contentLength > maxAttachmentBytes) {
242
+ throw new Error(
243
+ `Attachment is larger than the ${maxAttachmentBytes} byte limit`,
244
+ )
245
+ }
246
+
247
+ const content = await response.text()
248
+
249
+ if (content.length > maxAttachmentBytes) {
250
+ throw new Error(
251
+ `Attachment is larger than the ${maxAttachmentBytes} byte limit`,
252
+ )
253
+ }
254
+
255
+ return content
256
+ }
257
+
258
+ function assertInputProblem(value: unknown) {
259
+ assertRecord(value, "input problem")
260
+ assertArray(value["chips"], "chips")
261
+ assertArray(value["directConnections"], "directConnections")
262
+ assertArray(value["netConnections"], "netConnections")
263
+ assertRecord(
264
+ value["availableNetLabelOrientations"],
265
+ "availableNetLabelOrientations",
266
+ )
267
+
268
+ for (let index = 0; index < value["chips"].length; index++) {
269
+ assertChip(value["chips"][index], `chips[${index}]`)
270
+ }
271
+
272
+ for (let index = 0; index < value["directConnections"].length; index++) {
273
+ assertDirectConnection(
274
+ value["directConnections"][index],
275
+ `directConnections[${index}]`,
276
+ )
277
+ }
278
+
279
+ for (let index = 0; index < value["netConnections"].length; index++) {
280
+ assertNetConnection(
281
+ value["netConnections"][index],
282
+ `netConnections[${index}]`,
283
+ )
284
+ }
285
+
286
+ for (const [netId, orientations] of Object.entries(
287
+ value["availableNetLabelOrientations"],
288
+ )) {
289
+ const pathName = `availableNetLabelOrientations.${netId}`
290
+ assertArray(orientations, pathName)
291
+
292
+ for (let index = 0; index < orientations.length; index++) {
293
+ const orientation = orientations[index]
294
+
295
+ if (
296
+ typeof orientation !== "string" ||
297
+ !validDirections.has(orientation)
298
+ ) {
299
+ throw new Error(
300
+ `${pathName}[${index}] must be one of x+, x-, y+, or y-`,
301
+ )
302
+ }
303
+ }
304
+ }
305
+
306
+ if ("textBoxes" in value && value["textBoxes"] !== undefined) {
307
+ assertArray(value["textBoxes"], "textBoxes")
308
+
309
+ for (let index = 0; index < value["textBoxes"].length; index++) {
310
+ assertTextBox(value["textBoxes"][index], `textBoxes[${index}]`)
311
+ }
312
+ }
313
+
314
+ if (
315
+ "maxMspPairDistance" in value &&
316
+ value["maxMspPairDistance"] !== undefined
317
+ ) {
318
+ assertFiniteNumber(value["maxMspPairDistance"], "maxMspPairDistance")
319
+ }
320
+ }
321
+
322
+ function assertChip(value: unknown, pathName: string) {
323
+ assertRecord(value, pathName)
324
+ assertString(value["chipId"], `${pathName}.chipId`)
325
+ assertPoint(value["center"], `${pathName}.center`)
326
+ assertFiniteNumber(value["width"], `${pathName}.width`)
327
+ assertFiniteNumber(value["height"], `${pathName}.height`)
328
+ assertArray(value["pins"], `${pathName}.pins`)
329
+
330
+ for (let index = 0; index < value["pins"].length; index++) {
331
+ assertPin(value["pins"][index], `${pathName}.pins[${index}]`)
332
+ }
333
+
334
+ if ("sectionId" in value && value["sectionId"] !== undefined) {
335
+ assertString(value["sectionId"], `${pathName}.sectionId`)
336
+ }
337
+ }
338
+
339
+ function assertPin(value: unknown, pathName: string) {
340
+ assertRecord(value, pathName)
341
+ assertString(value["pinId"], `${pathName}.pinId`)
342
+ assertFiniteNumber(value["x"], `${pathName}.x`)
343
+ assertFiniteNumber(value["y"], `${pathName}.y`)
344
+
345
+ if ("_facingDirection" in value && value["_facingDirection"] !== undefined) {
346
+ const direction = value["_facingDirection"]
347
+
348
+ if (typeof direction !== "string" || !validDirections.has(direction)) {
349
+ throw new Error(
350
+ `${pathName}._facingDirection must be one of x+, x-, y+, or y-`,
351
+ )
352
+ }
353
+ }
354
+ }
355
+
356
+ function assertDirectConnection(value: unknown, pathName: string) {
357
+ assertRecord(value, pathName)
358
+ assertArray(value["pinIds"], `${pathName}.pinIds`)
359
+
360
+ if (value["pinIds"].length !== 2) {
361
+ throw new Error(`${pathName}.pinIds must contain exactly 2 pin ids`)
362
+ }
363
+
364
+ assertString(value["pinIds"][0], `${pathName}.pinIds[0]`)
365
+ assertString(value["pinIds"][1], `${pathName}.pinIds[1]`)
366
+
367
+ if ("netId" in value && value["netId"] !== undefined) {
368
+ assertString(value["netId"], `${pathName}.netId`)
369
+ }
370
+
371
+ if ("netLabelWidth" in value && value["netLabelWidth"] !== undefined) {
372
+ assertFiniteNumber(value["netLabelWidth"], `${pathName}.netLabelWidth`)
373
+ }
374
+ }
375
+
376
+ function assertNetConnection(value: unknown, pathName: string) {
377
+ assertRecord(value, pathName)
378
+ assertString(value["netId"], `${pathName}.netId`)
379
+ assertArray(value["pinIds"], `${pathName}.pinIds`)
380
+
381
+ for (let index = 0; index < value["pinIds"].length; index++) {
382
+ assertString(value["pinIds"][index], `${pathName}.pinIds[${index}]`)
383
+ }
384
+
385
+ if ("netLabelWidth" in value && value["netLabelWidth"] !== undefined) {
386
+ assertFiniteNumber(value["netLabelWidth"], `${pathName}.netLabelWidth`)
387
+ }
388
+
389
+ if ("netLabelHeight" in value && value["netLabelHeight"] !== undefined) {
390
+ assertFiniteNumber(value["netLabelHeight"], `${pathName}.netLabelHeight`)
391
+ }
392
+ }
393
+
394
+ function assertTextBox(value: unknown, pathName: string) {
395
+ assertRecord(value, pathName)
396
+ assertPoint(value["center"], `${pathName}.center`)
397
+ assertFiniteNumber(value["width"], `${pathName}.width`)
398
+ assertFiniteNumber(value["height"], `${pathName}.height`)
399
+
400
+ if ("chipId" in value && value["chipId"] !== undefined) {
401
+ assertString(value["chipId"], `${pathName}.chipId`)
402
+ }
403
+
404
+ if ("text" in value && value["text"] !== undefined) {
405
+ assertString(value["text"], `${pathName}.text`)
406
+ }
407
+ }
408
+
409
+ function assertPoint(value: unknown, pathName: string) {
410
+ assertRecord(value, pathName)
411
+ assertFiniteNumber(value["x"], `${pathName}.x`)
412
+ assertFiniteNumber(value["y"], `${pathName}.y`)
413
+ }
414
+
415
+ function assertRecord(
416
+ value: unknown,
417
+ pathName: string,
418
+ ): asserts value is Record<string, unknown> {
419
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
420
+ throw new Error(`${pathName} must be an object`)
421
+ }
422
+ }
423
+
424
+ function assertArray(
425
+ value: unknown,
426
+ pathName: string,
427
+ ): asserts value is unknown[] {
428
+ if (!Array.isArray(value)) {
429
+ throw new Error(`${pathName} must be an array`)
430
+ }
431
+ }
432
+
433
+ function assertString(value: unknown, pathName: string) {
434
+ if (typeof value !== "string") {
435
+ throw new Error(`${pathName} must be a string`)
436
+ }
437
+ }
438
+
439
+ function assertFiniteNumber(value: unknown, pathName: string) {
440
+ if (typeof value !== "number" || !Number.isFinite(value)) {
441
+ throw new Error(`${pathName} must be a finite number`)
442
+ }
443
+ }
444
+
445
+ function getTestFile(reportId: string) {
446
+ return `import { expect, test } from "bun:test"
447
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
448
+ import inputProblem from "./${reportId}.json"
449
+ import "tests/fixtures/matcher"
450
+
451
+ test("${reportId}", () => {
452
+ const solver = new SchematicTracePipelineSolver(inputProblem as any)
453
+
454
+ solver.solve()
455
+
456
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
457
+ })
458
+ `
459
+ }
460
+
461
+ function writeOutput(key: string, value: string) {
462
+ if (!githubOutputPath) return
463
+ fs.appendFileSync(githubOutputPath, `${key}=${value}\n`)
464
+ }
465
+
466
+ function writeFailureSummary(error: unknown) {
467
+ const message = getErrorMessage(error)
468
+ fs.writeFileSync(
469
+ path.join(repoRoot, "bug-report-error.md"),
470
+ `The JSON bug report import failed before a pull request could be opened.\n\n${message}\n`,
471
+ )
472
+ }
473
+
474
+ function getErrorMessage(error: unknown) {
475
+ if (error instanceof Error) return error.message
476
+ return String(error)
477
+ }
478
+
479
+ main().catch((error) => {
480
+ writeFailureSummary(error)
481
+ console.error(error)
482
+ process.exit(1)
483
+ })
@@ -0,0 +1,154 @@
1
+ name: JSON Bug Report
2
+
3
+ on:
4
+ issues:
5
+ types: [opened, edited]
6
+
7
+ permissions:
8
+ contents: write
9
+ issues: write
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: json-bug-report-${{ github.event.issue.number }}
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ import-json-bug-report:
18
+ if: >-
19
+ ${{
20
+ contains(join(github.event.issue.labels.*.name, ','), 'json-bug-report') ||
21
+ contains(github.event.issue.body || '', '### Solver input JSON attachment')
22
+ }}
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 10
25
+
26
+ steps:
27
+ - name: Checkout code
28
+ uses: actions/checkout@v4
29
+ with:
30
+ token: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN || github.token }}
31
+ persist-credentials: false
32
+
33
+ - name: Setup bun
34
+ uses: oven-sh/setup-bun@v2
35
+ with:
36
+ bun-version: 1.3.1
37
+
38
+ - name: Install dependencies
39
+ run: bun install
40
+
41
+ - name: Import JSON bug report
42
+ id: import
43
+ env:
44
+ GITHUB_TOKEN: ${{ github.token }}
45
+ TSCIRCUIT_BOT_GITHUB_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN }}
46
+ run: bun .github/scripts/import-json-bug-report.ts
47
+
48
+ - name: Run generated snapshot test
49
+ if: steps.import.outputs.created == 'true'
50
+ run: |
51
+ set +e
52
+ timeout 7m bun test "${{ steps.import.outputs.test_path }}" -u 2>&1 | tee bug-report-test.log
53
+ status=${PIPESTATUS[0]}
54
+ if [ "$status" -ne 0 ]; then
55
+ {
56
+ echo "The attached JSON was downloaded and validated, but the generated solver snapshot test failed."
57
+ echo
58
+ echo '```'
59
+ tail -n 80 bug-report-test.log
60
+ echo '```'
61
+ } > bug-report-error.md
62
+ exit "$status"
63
+ fi
64
+
65
+ - name: Format generated test
66
+ if: steps.import.outputs.created == 'true'
67
+ run: ./node_modules/.bin/biome format --write "${{ steps.import.outputs.test_path }}"
68
+
69
+ - name: Open pull request
70
+ id: pr
71
+ if: steps.import.outputs.created == 'true'
72
+ env:
73
+ GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN || github.token }}
74
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
75
+ REPORT_ID: ${{ steps.import.outputs.report_id }}
76
+ BRANCH_NAME: ${{ steps.import.outputs.branch_name }}
77
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
78
+ run: |
79
+ set -euo pipefail
80
+
81
+ git config user.name "tscircuit-bot"
82
+ git config user.email "tscircuit-bot@users.noreply.github.com"
83
+ git checkout -B "$BRANCH_NAME"
84
+ git add "${{ steps.import.outputs.report_dir }}"
85
+
86
+ if git diff --cached --quiet; then
87
+ echo "No changes to commit."
88
+ exit 0
89
+ fi
90
+
91
+ git commit -m "Add JSON bug report $REPORT_ID"
92
+ git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
93
+ git push --force-with-lease origin "HEAD:$BRANCH_NAME"
94
+
95
+ pr_body_file="$(mktemp)"
96
+ {
97
+ echo "Generated from #${ISSUE_NUMBER}."
98
+ echo
99
+ echo "Adds a snapshot-only regression test for the attached JSON solver input."
100
+ echo
101
+ echo "Workflow run: ${RUN_URL}"
102
+ } > "$pr_body_file"
103
+
104
+ pr_url="$(gh pr view "$BRANCH_NAME" --json url --jq .url 2>/dev/null || true)"
105
+ if [ -z "$pr_url" ]; then
106
+ pr_url="$(gh pr create \
107
+ --base main \
108
+ --head "$BRANCH_NAME" \
109
+ --title "Add JSON bug report $REPORT_ID" \
110
+ --body-file "$pr_body_file")"
111
+ else
112
+ gh pr edit "$BRANCH_NAME" \
113
+ --title "Add JSON bug report $REPORT_ID" \
114
+ --body-file "$pr_body_file"
115
+ fi
116
+
117
+ echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT"
118
+
119
+ - name: Tag original issue
120
+ if: steps.import.outputs.created == 'true' && steps.pr.outputs.pr_url != ''
121
+ env:
122
+ GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN || github.token }}
123
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
124
+ REPORT_ID: ${{ steps.import.outputs.report_id }}
125
+ PR_URL: ${{ steps.pr.outputs.pr_url }}
126
+ run: |
127
+ message_file="$(mktemp)"
128
+ {
129
+ echo "Created or updated ${PR_URL} for this JSON bug report."
130
+ echo
131
+ echo "Report id: \`${REPORT_ID}\`"
132
+ } > "$message_file"
133
+
134
+ gh issue comment "$ISSUE_NUMBER" --body-file "$message_file"
135
+
136
+ - name: Comment on issue when import fails
137
+ if: failure()
138
+ env:
139
+ GH_TOKEN: ${{ secrets.TSCIRCUIT_BOT_GITHUB_TOKEN || github.token }}
140
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
141
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
142
+ run: |
143
+ message_file="$(mktemp)"
144
+ {
145
+ echo "I couldn't import this JSON bug report automatically."
146
+ echo
147
+ if [ -f bug-report-error.md ]; then
148
+ cat bug-report-error.md
149
+ echo
150
+ fi
151
+ echo "Workflow run: ${RUN_URL}"
152
+ } > "$message_file"
153
+
154
+ gh issue comment "$ISSUE_NUMBER" --body-file "$message_file"