ENG-755 - [build] A work item's remote_* fields are its ticket: rename them - #129
Conversation
There was a problem hiding this comment.
Verdict: approve
All five acceptance criteria are satisfied by the diff at fe31ca8. The rename lands atomically across the migration, models, schemas, subscribers, workflow input, tests, and frontend, with no compatibility aliases and no scope creep.
Per-criterion
- AC1 — pass.
backend/migrations/versions/f2c8b81a9d4e_rename_work_item_ticket_fields.pychains offd3a5c71f8e40, dropswork_items_remote_unique, renames both columns viaalter_column(..., new_column_name=...)withexisting_nullablepreserved (ticket_keyNOT NULL,ticket_urlnullable), and createswork_items_ticket_uniqueon(source, ticket_key). The downgrade is a faithful mirror.backend/tests/test_work_item_ticket_migration.pyseeds a row atd3a5c71f8e40, upgrades, asserts columns/data/index, downgrades, and asserts restoration; it is registered in_OWN_DATABASE_MODULES. CI shows it ran and passed rather than skipping. - AC2 — pass.
WorkItemexposes onlyticket_key,ticket_url,get_for_ticket_key(), andset_ticket_status().get_label(),ship(),close_external(),Ticket.ref(...), the log line, the query predicate,create(), andupdate()all moved. Both subscribers and both PR-webhook paths callset_ticket_status();test_ticketing.pycovers the push, the non-tracker no-op, and the failure-close path. - AC3 — pass.
Build.run_multistepnow takesticket_title/ticket_urlalongside the retainedticket_ref, andBuild.dispatchpasses all three.test_dispatch_pulls_cancelled_item_back_onto_the_boardcaptures the fullstart()kwarg dict, so the stored-input shape is pinned by an assertion rather than by reading. - AC4 — pass.
WorkItemSummarydeclaresticket_key/ticket_url; the detail test assertsticketKey,ticketUrl,links.ticket, and — usefully — the exact key set of the summary object, which is what actually provesremoteKey/remoteUrlare no longer emitted. The frontend interface and all five consumers read the new names, including the renamedworkItemTicketKeyparameter inslug.ts.DashboardItem.ticket_refandLinks.ticketare untouched, as specified. - AC5 — pass.
git grepfor the legacy identifiers overbackend/druks,backend/tests/ship, andfrontend/srcreturns nothing. The only remaining hits are the baseline revision,d3a5c71f8e40, the new reversible migration, and the migration compatibility test — exactly the set the plan's migration-history reconciliation allows.
Verification
This sandbox has neither a Postgres server nor npm, so the profile commands could not run here beyond uv run ruff check backend (passed). Both GitHub checks on fe31ca8 are green and cover the same ground: On Pull Request Backend ran ruff and uv run pytest backend/ (986 passed, including the new migration test), and On Pull Request Frontend ran lint, test, and build. The drain requirement is recorded in the PR body under Risk.
Follow-up recommendations
Neither of these blocks the merge; file them as separate tickets if they're worth the churn.
- Test-DB configuration convention.
backend/tests/test_work_item_ticket_migration.pyintroduces its ownDRUKS_TEST_PGenvironment variable, while the rest of the suite readsDRUKS_TEST_DATABASE_URLthroughdruks.testing.TEST_DATABASE_URL. Combined with theskipif(not _postgres_is_reachable())guard, a future environment that points the standard variable somewhere non-default would silently skip this test rather than fail. The constraint is that the migration test should follow the suite's single source of truth for the test database; deriving the maintenance URL fromTEST_DATABASE_URLand keeping the isolated-database creation is one way, and dropping the reachability skip so an unreachable server errors loudly is another — either is reasonable, and you know the CI matrix better than I do. Whichever you pick,test_work_item_ticket_fields_upgrade_and_downgrade_preserve_schema_and_datais the test to keep green. upgrade(..., "head")in the migration test. Targeting"head"means the test's assertions travel with whatever revision lands next, so an unrelated future migration could change what this test is actually exercising. Pinning tof2c8b81a9d4ewould keep it a focused test of this revision. Same test as above.
Push back on either of these if you disagree — the reachability skip in particular may be a deliberate choice for local runs without a database, in which case say so and I'll drop it.
|
Code review: Clean, fully-consistent rename of WorkItem's remote_key/remote_url to ticket_key/ticket_url across models, schemas, workflows, migration, and both backend/frontend call sites, with a solid reversible migration test following the repo's existing scratch-database pattern. |
czpython
left a comment
There was a problem hiding this comment.
-
backend/tests/test_work_item_ticket_migration.py — delete it. 129 lines proving ALTER TABLE … RENAME COLUMN works. Worse, it invents its own test infrastructure to do it: a new DRUKS_TEST_PG env var with hardcoded creds, _recreate_database()/_drop_database() via raw psycopg, and a skipif(not _postgres_is_reachable()) that silently passes when PG is absent. The house pattern is right there in test_proof_extension_migration.py:5 — TEST_DATABASE_URL from druks.testing, 54 lines, and it tests migration machinery, not one migration. None of your 8 existing migrations has a per-migration test. This one adds a permanent _OWN_DATABASE_MODULES entry and a novel precedent to cover a rename Alembic can't get wrong.
-
assert set(summary) == {...} in test_api_work_items.py. Pins the full wire key set inside a test whose job is "composes summary status and timeline". Pydantic already guarantees the shape; this just breaks an unrelated test the next time a field lands. Same shape of over-pinning as the full-kwargs assert started == {...} in the dispatch test.
-
The ticket now has two canonical names. Post-rename, WorkItemSummary.ticketKey and DashboardItem.ticketRef carry the identical string — schemas.py:157 literally does ticket_ref=item.ticket_key. Before the rename that was a translation between domains; now it's two names for one thing across two wire contracts of the same board. The PR explicitly rules out fixing it. That's the half of "a work item's fields are its ticket" that didn't land. ticket_ref on the workflow input is defensible (it's the prompt-rendered reference); DashboardItem.ticket_ref isn't.
|
@czpython — acknowledged. Three changes will be made: 1. Delete 2. Remove the over-pinning assertions — drop 3. Rename
|
Superseded by commit 91bb6cb implementing the requested revision.
There was a problem hiding this comment.
Round 2 — pass
All five acceptance criteria hold at 91bb6cb. The revision-2 delta (93904ff..91bb6cb) applies exactly the three changes czpython requested, and nothing else.
Per-criterion
- AC1 — pass.
backend/migrations/versions/f2c8b81a9d4e_rename_work_item_ticket_fields.pyis untouched by this revision:down_revision = "d3a5c71f8e40", dropswork_items_remote_unique, renames both columns withexisting_nullable=False(key) /True(url), createswork_items_ticket_uniqueon(source, ticket_key), with a mirrored downgrade.backend/tests/test_work_item_ticket_migration.pyis deleted (129 lines gone), and"test_work_item_ticket_migration"is absent from_OWN_DATABASE_MODULESinbackend/tests/conftest.py. - AC2 — pass.
models.pyexposes onlyticket_key,ticket_url,get_for_ticket_key(),set_ticket_status(); bothsubscribers.pycall sites useset_ticket_status(). Unchanged from round 1 and still clean. - AC3 — pass.
Build.run_multistepkeepsticket_refand takesticket_title/ticket_url;Build.dispatchpasses all three.test_build_dispatch.py:39-41now asserts the three keys individually — noassert started == {...}remains. - AC4 — pass.
DashboardItemdeclaresticket_key: strandfrom_work_itempassesticket_key=item.ticket_key(schemas.py:140,157). The frontend follows:api.tsinterface field,slug.ts:85dashboardItemPath, and bothHistoryPage.tsxreads (search filter line 48, rendered cell line 128).WorkItemSummaryand its consumers are unchanged.TicketCell.tsxandWorkItemsPage.tsx's<TicketCell ticketRef={wi.ticketKey} …>are untouched, as scoped. The over-pinnedassert set(summary) == {...}block is gone; the individualsummary["ticketKey"]/summary["ticketUrl"]/links.ticketassertions survive. - AC5 — pass.
grepforremote_key|remote_url|remote_title|set_remote_status|remoteKey|remoteUrlacrossbackend/druks,backend/tests, andfrontend/srcreturns hits only in1ba6e314b314_baseline_schema.py,d3a5c71f8e40_…, and the new reversible migration — exactly the permitted set. The survivingticket_ref/ticketRefoccurrences are all the carved-out lineage:workflows.py(workflow input),prompt_context.py, the two build templates,test_build_prompts.py,test_build_dispatch.py's kwarg assertion,slug.ts's genericworkItemPathparameter, andTicketCell's prop.
Verification
This sandbox has no Postgres server (connection to 127.0.0.1:5432 refused) and no npm/node, so most profile commands could not run locally. uv run ruff check backend passed locally ("All checks passed!"). Both GitHub checks on 91bb6cb are green and cover the same ground: On Pull Request Backend ran ruff plus uv run pytest backend/ (985 passed — one fewer than round 1's 986, consistent with the deleted migration test), and On Pull Request Frontend ran lint, test (all vitest files green), and build (tsc -b && vite build, built in 346ms). The drain note is recorded in the PR body under Risk.
Note on the ticket-vs-review conflict
The Linear ticket text says "DashboardItem.ticket_ref already reads off remote_key and needs no change." The reviewer's point 3 knowingly overrides that, and the revised plan documents the override with its reasoning. Flagging it here only so the record shows the divergence was deliberate, not accidental — no action needed.
Follow-up recommendations
- No test pins
DashboardItem's newticketKeywire key. The history endpoint tests (test_api_work_items.py:159,181,194) exercise the serializer but only assert titles and counts, so a future regression on this field name would ship silently. Two reasonable options: extendtest_history_returns_only_done_work_itemswith a singleitems[0]["ticketKey"] == "…"assertion, or add one totest_pr_closed_without_merge_is_cancelled_in_history. Either stays well clear of the whole-shape pinning you asked to remove — a single field assertion is the same shape as thesummary["ticketKey"]line that survived in the detail test. Not blocking; the frontendtscbuild plus the green suite give reasonable cover today.
If you disagree with the follow-up above — including "the type-level coverage is enough, skip it" — say so and I'll drop it.
|
Code review: The DashboardItem.ticket_ref → ticket_key rename goes beyond the ticket's stated scope (ENG-755 explicitly says that field needs no change) and this revision also deleted the only migration round-trip test plus loosened two unrelated test assertions, so I've filed a follow-up ticket for those. |
DashboardItem.ticket_key is a required column-backed value, so the frontend mirror declared it optional and carried fallbacks that could never fire.
Linear ticket: ENG-755
A work item is born from a ticket and carries it for life, so the fields that hold it are named for what they are.
WorkItem.remote_key/remote_urlbecometicket_key/ticket_url, along withget_for_remote_key()→get_for_ticket_key(),set_remote_status()→set_ticket_status(), and thework_items_remote_uniqueindex →work_items_ticket_unique. A migration renames the columns and recreates the index; the column contract is unchanged (ticket_keyrequired,ticket_urlnullable).The rename carries the whole contract in one move.
Build's stored input dropsremote_title/remote_urlforticket_title/ticket_url.WorkItemSummaryemitsticketKey/ticketUrl, andDashboardItememitsticketKeywhere it used to sayticketRef— the same value had two names across the board's two responses. The frontend mirror follows, and now declaresticketKeyrequired, matching the column and dropping fallbacks that could never fire.ticket_refsurvives onBuild's input and in the build prompts: that one is the reference rendered into an agent's instructions, carried on the input so the workflow needn't fetch the item.Ticket.status_name,Ticket.status_kind, andLinks.ticketalready read as ticket-domain names and are untouched.Deploying
In-flight build runs must drain first. There is no dual-name path, so replaying a run whose stored input still holds
remote_title/remote_urlwill fail signature validation.