Public: anyone can read this; the crew can change it.

A GitHub migration carries each milestone: title, description, due date, state, once #3851

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A GitHub migration now carries each milestone with its title, description, due date and open or closed state, and carrying again makes no duplicates. `tsc --noEmit` is clean. I ran only the migrate, schema-migration and origins suites (80 pass), not the whole suite.

shipped production is serving it deploy a5359e9

opened this

A GitHub migration now carries each milestone with its title, description, due date and open or closed state, and carrying again makes no duplicates. tsc --noEmit is clean. I ran only the migrate, schema-migration and origins suites (80 pass), not the whole suite.

A forge milestone is derived from the dependency tree and has no stored row, so each GitHub milestone arrives as an issue, which is the thing a forge milestone is built from.

  • Fields: the description becomes the issue body, and a closed milestone becomes a closed issue (done). New migration 20261022090000-milestone-origin adds milestone_origin (repo, GitHub number, issue, due date), because an issue has no due-date column.
  • No duplicates: a milestone is matched by its GitHub number in that table. A repeat carry skips it, so edits made here are kept.
  • Order: milestones are made after the issues, so their numbers fall past the numbers GitHub gave the conversations.
  • Code: src/origins/milestones.ts holds the carry and dueOf. GitHubProvider.listMilestones reads /milestones?state=all (optional on Provider). carryConversations returns a milestones count, and the migrate job's detail line adds it when there are any.
  • Tests: test/fake-origin.ts gained milestones and the /milestones route. test/migrate-milestones.test.ts seeds three milestones (one closed, one with no due date) and checks state, due date and number placement. A second carry into the same repository makes no more issues or milestone_origin rows. I used three milestones, not the two the issue asked for, to cover the no-due-date case.

Left out:

  • Which issues belong to which milestone, since that is the separate membership issue.
  • Forgejo milestones, because listMilestones is only on the GitHub provider.
  • Re-syncing a milestone that changed on GitHub after the first carry.
  • A UI for the due date: it is stored and tested but nothing displays it yet.

To try it, run bun test test/migrate-milestones.test.ts, or migrate a GitHub repository with milestones from /migrate.

🤖 Generated with Claude Code

Closes #3829

commented

A GitHub migration now carries each milestone with its title, description, due date and open or closed state, and carrying again makes no duplicates. tsc --noEmit is clean. I ran only the migrate, schema-migration and origins suites (80 pass), not the whole suite.

A forge milestone is derived from the dependency tree and has no stored row, so each GitHub milestone arrives as an issue, which is the thing a forge milestone is built from.

  • Fields: the description becomes the issue body, and a closed milestone becomes a closed issue (done). New migration 20261022090000-milestone-origin adds milestone_origin (repo, GitHub number, issue, due date), because an issue has no due-date column.
  • No duplicates: a milestone is matched by its GitHub number in that table. A repeat carry skips it, so edits made here are kept.
  • Order: milestones are made after the issues, so their numbers fall past the numbers GitHub gave the conversations.
  • Code: src/origins/milestones.ts holds the carry and dueOf. GitHubProvider.listMilestones reads /milestones?state=all (optional on Provider). carryConversations returns a milestones count, and the migrate job's detail line adds it when there are any.
  • Tests: test/fake-origin.ts gained milestones and the /milestones route. test/migrate-milestones.test.ts seeds three milestones (one closed, one with no due date) and checks state, due date and number placement. A second carry into the same repository makes no more issues or milestone_origin rows. I used three milestones, not the two the issue asked for, to cover the no-due-date case.

Left out:

  • Which issues belong to which milestone, since that is the separate membership issue.
  • Forgejo milestones, because listMilestones is only on the GitHub provider.
  • Re-syncing a milestone that changed on GitHub after the first carry.
  • A UI for the due date: it is stored and tested but nothing displays it yet.

To try it, run bun test test/migrate-milestones.test.ts, or migrate a GitHub repository with milestones from /migrate.

🤖 Generated with Claude Code

Approved

Does what the issue asks and nothing more. Each GitHub milestone arrives once as an issue (title, description, closed state), its due date and origin number are kept in milestone_origin, and a second carry is a no-op. Input is clamped at the boundary (control bytes, length, bad numbers), SQL is parameterised, there is no new dependency, and the test would fail without the change. I read the source and the diff; I did not exercise the preview. This pull adds no page or terminal surface, so the two-doors rule has nothing to apply to here.

Two things I would do next:

  1. Make the carry atomic. In src/origins/milestones.ts the issue is created and the milestone_origin row inserted separately, so a crash between them leaves an orphan issue and the next carry makes a duplicate. Wrap both in db.transaction.
  2. Show the due date. dueOf is only called from the test, so the date is stored but unreachable on the page or over MCP. The follow-up that surfaces it should use dueOf, or dueOf should go until then. Also add Forgejo listMilestones, and move the RemoteMilestone import in github.ts into alphabetical order.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs