Support multiple merge requests per ticket

This commit is contained in:
2026-09-29 01:08:46 +09:00
parent ac7a6027bd
commit 590abf3142
26 changed files with 1539 additions and 723 deletions
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
// Input contract: 0.1.0 (sha256:fbaf40391d4ab81b9ffb558ff4c055efb8c58e26b6a88d3eefbbbeefc6ea0822)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a)
// Input contract: 0.1.0 (sha256:fbaf40391d4ab81b9ffb558ff4c055efb8c58e26b6a88d3eefbbbeefc6ea0822)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a)
// Input contract: 0.1.0 (sha256:fbaf40391d4ab81b9ffb558ff4c055efb8c58e26b6a88d3eefbbbeefc6ea0822)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:80be292e32441730f7de890e64836f4ca9655576d655e60e6d44daf5227a1b1c)
// Input contract: 0.1.0 (sha256:fbaf40391d4ab81b9ffb558ff4c055efb8c58e26b6a88d3eefbbbeefc6ea0822)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -49,7 +49,7 @@ export type MergeRequestRefResponse = { diagnostic?: MergeRequestRefDiagnostic |
export type MergeRequestState = "open" | "merged" | "closed";
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_item_revision: string; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_item_revision: string; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
export type MergeRequestWorkerIdentity = { runtime_id: string; worker_id: string; };
@@ -89,7 +89,7 @@ export type TicketAssignmentRole = "orchestrator" | "coder" | "owner" | "contrib
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; created_at?: string | null; current_coder?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; item_revision: string; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; created_at?: string | null; current_coder?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; item_revision: string; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetailDerivedRelation = { at: string; author: string; forward_kind: string; inverse_kind: string; note?: string | null; source_resource_key?: string | null; source_ticket: string; };
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:a92b7346166ec2316878ef38b0426090cb4bdd0d5b96cae3014bef35c60df02a)
// Input contract: 0.1.0 (sha256:fbaf40391d4ab81b9ffb558ff4c055efb8c58e26b6a88d3eefbbbeefc6ea0822)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -712,6 +712,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
"item_revision",
"linked_objectives",
"merge_request",
"merge_requests",
"priority",
"queued_at",
"queued_by",
@@ -769,6 +770,11 @@ export function parseTicketDetail(value: unknown): TicketDetail {
`${label}.linked_objectives`,
parseObjectiveLink,
),
merge_requests: array(
item.merge_requests,
`${label}.merge_requests`,
parseTicketMergeRequestSummary,
),
merge_request: item.merge_request === undefined
? undefined
: item.merge_request === null
@@ -1082,7 +1088,17 @@ function parseMergeRequestThreadEvent(
sequence: integer(item.sequence, `${label}.sequence`),
};
if (kind === "review_requested") {
exact(item, [...base, "requested_by", "reviewer", "subject_ref"], label);
exact(
item,
[
...base,
"requested_by",
"reviewer",
"subject_ref",
"ticket_item_revision",
],
label,
);
return {
...eventBase,
kind,
@@ -1092,6 +1108,10 @@ function parseMergeRequestThreadEvent(
),
reviewer: parseMergeRequestWorker(item.reviewer, `${label}.reviewer`),
subject_ref: string(item.subject_ref, `${label}.subject_ref`),
ticket_item_revision: string(
item.ticket_item_revision,
`${label}.ticket_item_revision`,
),
};
}
if (kind === "review") {
@@ -1103,6 +1123,7 @@ function parseMergeRequestThreadEvent(
"request_event_id",
"reviewer",
"subject_ref",
"ticket_item_revision",
], label);
const decision = string(item.decision, `${label}.decision`);
if (!["approve", "request_changes"].includes(decision)) {
@@ -1120,6 +1141,10 @@ function parseMergeRequestThreadEvent(
),
reviewer: parseMergeRequestWorker(item.reviewer, `${label}.reviewer`),
subject_ref: string(item.subject_ref, `${label}.subject_ref`),
ticket_item_revision: string(
item.ticket_item_revision,
`${label}.ticket_item_revision`,
),
};
}
if (kind === "review_revoked") {
@@ -71,7 +71,7 @@
const loadedRepositories = $derived(data.repositories.data);
let ticket = $state<TicketDetail>(loadedTicket);
const mergeRequest = $derived(ticket.merge_request);
const mergeRequests = $derived(ticket.merge_requests);
let editing = $state(false);
let editTitle = $state(loadedTicket.title);
let editBody = $state(loadedTicket.body);
@@ -682,28 +682,32 @@
</details>
<section class="ticket-control-card">
<header><h2>Merge Request</h2></header>
{#if mergeRequest}
<p><strong>{mergeRequest.state}</strong></p>
<p>
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
to <code>{mergeRequest.selector_to}</code>
</p>
{#if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref === mergeRequest.current_subject_ref}
<p><strong>Source review:</strong> {mergeRequest.review_status} for exact ref <code>{mergeRequest.current_subject_ref}</code></p>
{:else if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref}
<p><strong>Fresh source review required:</strong> selector_from moved from <code>{mergeRequest.review_subject_ref}</code> to <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else if mergeRequest.current_subject_ref}
<p><strong>Fresh source review required:</strong> no effective verdict exists for <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else}
<p><strong>Source review unavailable:</strong> selector_from is unresolved.</p>
{/if}
<p><strong>Target integration:</strong> {mergeRequest.state === "merged" ? "recorded" : `awaiting Orchestrator integration into ${mergeRequest.selector_to}`}.</p>
<p class="workspace-empty-copy">Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.</p>
<a
class="workspace-secondary-button"
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}
>Open Merge Request</a>
<header><h2>Merge Requests</h2></header>
{#if mergeRequests.length > 0}
{#each mergeRequests as mergeRequest (mergeRequest.merge_request_id)}
<article class="ticket-control-card">
<p><strong>{mergeRequest.repository_key}: {mergeRequest.state}</strong></p>
<p>
From <code>{mergeRequest.selector_from ?? "requires repair"}</code>
to <code>{mergeRequest.selector_to}</code>
</p>
{#if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref === mergeRequest.current_subject_ref}
<p><strong>Source review:</strong> {mergeRequest.review_status} for exact ref <code>{mergeRequest.current_subject_ref}</code></p>
{:else if mergeRequest.current_subject_ref && mergeRequest.review_subject_ref}
<p><strong>Fresh source review required:</strong> selector_from moved from <code>{mergeRequest.review_subject_ref}</code> to <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else if mergeRequest.current_subject_ref}
<p><strong>Fresh source review required:</strong> no effective verdict exists for <code>{mergeRequest.current_subject_ref}</code>.</p>
{:else}
<p><strong>Source review unavailable:</strong> selector_from is unresolved.</p>
{/if}
<p><strong>Target integration:</strong> {mergeRequest.state === "merged" ? "recorded" : `awaiting Orchestrator integration into ${mergeRequest.selector_to}`}.</p>
<p class="workspace-empty-copy">Target-only movement refreshes integration evidence; it does not invalidate approval for an unchanged source.</p>
<a
class="workspace-secondary-button"
href={mergeRequestPagePath(data.workspaceId, mergeRequest.merge_request_id)}
>Open Merge Request</a>
</article>
{/each}
{:else}
<p class="workspace-empty-copy">The assigned Coder has not opened a Merge Request.</p>
{/if}