| author | |
| committer | |
| log | ff0f702c7cb5006acf72b63edf5c0df7686f60c4 |
| tree | d43e48ec67793bfbb76c514d0054dc303ad3fe0f |
| parent | 1cbe8a7a90cb7b942ede116e744a71d45da8fdb0 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>3 files changed, 306 insertions(+), 54 deletions(-)
apps/web/src/state/sourceControlActions.ts+45-11| ... | ... | @@ -126,12 +126,6 @@ function resolveScope(scope: SourceControlActionScope) { |
| 126 | 126 | }; |
| 127 | 127 | } |
| 128 | 128 | |
| 129 | function unavailableResult(message: string) { | |
| 130 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 131 | Cause.fail(new VcsActionUnavailableError({ message })), | |
| 132 | ); | |
| 133 | } | |
| 134 | ||
| 135 | 129 | export function useSourceControlActionRunning( |
| 136 | 130 | scope: SourceControlActionScope, |
| 137 | 131 | kinds: ReadonlyArray<SourceControlActionKind>, |
| ... | ... | @@ -149,7 +143,15 @@ export function useVcsInitAction(scope: SourceControlActionScope) { |
| 149 | 143 | const action = useCallback(async () => { |
| 150 | 144 | const target = resolveScope(scope); |
| 151 | 145 | if (target === null) { |
| 152 | return unavailableResult("Git init is unavailable."); | |
| 146 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 147 | Cause.fail( | |
| 148 | new VcsActionUnavailableError({ | |
| 149 | operation: "init", | |
| 150 | environmentId: scope.environmentId, | |
| 151 | cwd: scope.cwd, | |
| 152 | }), | |
| 153 | ), | |
| 154 | ); | |
| 153 | 155 | } |
| 154 | 156 | return init({ |
| 155 | 157 | environmentId: target.environmentId, |
| ... | ... | @@ -172,7 +174,15 @@ export function useVcsPullAction(scope: SourceControlActionScope) { |
| 172 | 174 | const action = useCallback(async () => { |
| 173 | 175 | const target = resolveScope(scope); |
| 174 | 176 | if (target === null) { |
| 175 | return unavailableResult("Git pull is unavailable."); | |
| 177 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 178 | Cause.fail( | |
| 179 | new VcsActionUnavailableError({ | |
| 180 | operation: "pull", | |
| 181 | environmentId: scope.environmentId, | |
| 182 | cwd: scope.cwd, | |
| 183 | }), | |
| 184 | ), | |
| 185 | ); | |
| 176 | 186 | } |
| 177 | 187 | return pull({ |
| 178 | 188 | environmentId: target.environmentId, |
| ... | ... | @@ -211,7 +221,15 @@ export function useGitStackedAction(scope: SourceControlActionScope) { |
| 211 | 221 | onProgress?: (event: GitActionProgressEvent) => void; |
| 212 | 222 | }) => { |
| 213 | 223 | if (resolveScope(scope) === null) { |
| 214 | return unavailableResult("Git action is unavailable."); | |
| 224 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 225 | Cause.fail( | |
| 226 | new VcsActionUnavailableError({ | |
| 227 | operation: "run_change_request", | |
| 228 | environmentId: scope.environmentId, | |
| 229 | cwd: scope.cwd, | |
| 230 | }), | |
| 231 | ), | |
| 232 | ); | |
| 215 | 233 | } |
| 216 | 234 | return runStackedAction({ |
| 217 | 235 | actionId: input.actionId, |
| ... | ... | @@ -257,7 +275,15 @@ export function useSourceControlPublishRepositoryAction(scope: SourceControlActi |
| 257 | 275 | }) => { |
| 258 | 276 | const target = resolveScope(scope); |
| 259 | 277 | if (target === null) { |
| 260 | return unavailableResult("Repository publishing is unavailable."); | |
| 278 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 279 | Cause.fail( | |
| 280 | new VcsActionUnavailableError({ | |
| 281 | operation: "publish_repository", | |
| 282 | environmentId: scope.environmentId, | |
| 283 | cwd: scope.cwd, | |
| 284 | }), | |
| 285 | ), | |
| 286 | ); | |
| 261 | 287 | } |
| 262 | 288 | return publishRepository({ |
| 263 | 289 | environmentId: target.environmentId, |
| ... | ... | @@ -286,7 +312,15 @@ export function usePreparePullRequestThreadAction(scope: SourceControlActionScop |
| 286 | 312 | async (input: { reference: string; mode: "local" | "worktree"; threadId?: ThreadId }) => { |
| 287 | 313 | const target = resolveScope(scope); |
| 288 | 314 | if (target === null) { |
| 289 | return unavailableResult("Pull request thread preparation is unavailable."); | |
| 315 | return AsyncResult.failure<never, VcsActionUnavailableError>( | |
| 316 | Cause.fail( | |
| 317 | new VcsActionUnavailableError({ | |
| 318 | operation: "prepare_pull_request_thread", | |
| 319 | environmentId: scope.environmentId, | |
| 320 | cwd: scope.cwd, | |
| 321 | }), | |
| 322 | ), | |
| 323 | ); | |
| 290 | 324 | } |
| 291 | 325 | return preparePullRequestThread({ |
| 292 | 326 | environmentId: target.environmentId, |
packages/client-runtime/src/state/vcsAction.test.ts+141| ... | ... | @@ -7,6 +7,7 @@ import { describe, expect, it } from "@effect/vitest"; |
| 7 | 7 | import * as Cause from "effect/Cause"; |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | import * as Layer from "effect/Layer"; |
| 10 | import * as Schema from "effect/Schema"; | |
| 10 | 11 | import * as Stream from "effect/Stream"; |
| 11 | 12 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; |
| 12 | 13 | |
| ... | ... | @@ -21,12 +22,18 @@ import { |
| 21 | 22 | EMPTY_VCS_ACTION_STATE, |
| 22 | 23 | getVcsActionTargetKey, |
| 23 | 24 | normalizeVcsActionProgressEvent, |
| 25 | parseVcsActionTargetKey, | |
| 26 | VcsActionMissingTerminalEventError, | |
| 27 | VcsActionRemoteFailureError, | |
| 28 | VcsActionTargetKeyParseError, | |
| 29 | VcsActionUnavailableError, | |
| 24 | 30 | } from "./vcsAction.ts"; |
| 25 | 31 | |
| 26 | 32 | const actionId = "action-123"; |
| 27 | 33 | const action = "commit_push" as const; |
| 28 | 34 | const cwd = "/repo"; |
| 29 | 35 | const environmentId = EnvironmentId.make("environment-1"); |
| 36 | const isVcsActionUnavailableError = Schema.is(VcsActionUnavailableError); | |
| 30 | 37 | const result: GitRunStackedActionResult = { |
| 31 | 38 | action, |
| 32 | 39 | branch: { |
| ... | ... | @@ -57,6 +64,28 @@ function progress<T extends GitActionProgressEvent>(event: T): T { |
| 57 | 64 | } |
| 58 | 65 | |
| 59 | 66 | describe("vcsActionState", () => { |
| 67 | it("preserves malformed target key diagnostics and the native cause without copying the key", () => { | |
| 68 | const key = "not-json-with-credential=do-not-log"; | |
| 69 | let error: unknown; | |
| 70 | ||
| 71 | try { | |
| 72 | parseVcsActionTargetKey(key); | |
| 73 | } catch (cause) { | |
| 74 | error = cause; | |
| 75 | } | |
| 76 | ||
| 77 | expect(error).toBeInstanceOf(VcsActionTargetKeyParseError); | |
| 78 | expect(error).toMatchObject({ keyLength: key.length, cause: expect.any(SyntaxError) }); | |
| 79 | expect(error).not.toHaveProperty("key"); | |
| 80 | expect((error as Error).message).not.toContain(key); | |
| 81 | }); | |
| 82 | ||
| 83 | it("rejects invalid target key shapes", () => { | |
| 84 | const key = JSON.stringify([environmentId]); | |
| 85 | ||
| 86 | expect(() => parseVcsActionTargetKey(key)).toThrowError(VcsActionTargetKeyParseError); | |
| 87 | }); | |
| 88 | ||
| 60 | 89 | it("projects phase and hook progress without owning the async operation", () => { |
| 61 | 90 | const initial = beginVcsActionState({ |
| 62 | 91 | operation: "run_change_request", |
| ... | ... | @@ -254,6 +283,7 @@ describe("vcsActionState", () => { |
| 254 | 283 | target, |
| 255 | 284 | transportActionId, |
| 256 | 285 | actionId, |
| 286 | action, | |
| 257 | 287 | onProgress: (event) => |
| 258 | 288 | Effect.sync(() => { |
| 259 | 289 | observed.push(event); |
| ... | ... | @@ -266,6 +296,85 @@ describe("vcsActionState", () => { |
| 266 | 296 | }), |
| 267 | 297 | ); |
| 268 | 298 | |
| 299 | it.effect("retains structural remote failure context without copying the remote payload", () => | |
| 300 | Effect.gen(function* () { | |
| 301 | const target = { environmentId, cwd }; | |
| 302 | const transportActionId = createVcsActionTransportId(target, actionId); | |
| 303 | const remoteMessage = "The remote rejected the push with credential=do-not-log."; | |
| 304 | const error = yield* consumeVcsActionProgress( | |
| 305 | Stream.fromIterable<GitActionProgressEvent>([ | |
| 306 | { | |
| 307 | actionId: transportActionId, | |
| 308 | action, | |
| 309 | cwd, | |
| 310 | kind: "action_failed", | |
| 311 | phase: "push", | |
| 312 | message: remoteMessage, | |
| 313 | }, | |
| 314 | ]), | |
| 315 | { | |
| 316 | target, | |
| 317 | transportActionId, | |
| 318 | actionId, | |
| 319 | action, | |
| 320 | onProgress: () => Effect.void, | |
| 321 | }, | |
| 322 | ).pipe(Effect.flip); | |
| 323 | ||
| 324 | expect(error).toBeInstanceOf(VcsActionRemoteFailureError); | |
| 325 | expect(error).toMatchObject({ | |
| 326 | actionId, | |
| 327 | transportActionId, | |
| 328 | action, | |
| 329 | environmentId, | |
| 330 | cwd, | |
| 331 | phase: "push", | |
| 332 | remoteMessageLength: remoteMessage.length, | |
| 333 | }); | |
| 334 | expect(error).not.toHaveProperty("detail"); | |
| 335 | expect(error.message).toBe("Source control action 'commit_push' failed during push."); | |
| 336 | expect(error.message).not.toContain(remoteMessage); | |
| 337 | }), | |
| 338 | ); | |
| 339 | ||
| 340 | it.effect("reports a missing terminal event as a protocol failure", () => | |
| 341 | Effect.gen(function* () { | |
| 342 | const target = { environmentId, cwd }; | |
| 343 | const transportActionId = createVcsActionTransportId(target, actionId); | |
| 344 | const error = yield* consumeVcsActionProgress( | |
| 345 | Stream.fromIterable<GitActionProgressEvent>([ | |
| 346 | { | |
| 347 | actionId: transportActionId, | |
| 348 | action, | |
| 349 | cwd, | |
| 350 | kind: "phase_started", | |
| 351 | phase: "commit", | |
| 352 | label: "Committing...", | |
| 353 | }, | |
| 354 | ]), | |
| 355 | { | |
| 356 | target, | |
| 357 | transportActionId, | |
| 358 | actionId, | |
| 359 | action, | |
| 360 | onProgress: () => Effect.void, | |
| 361 | }, | |
| 362 | ).pipe(Effect.flip); | |
| 363 | ||
| 364 | expect(error).toBeInstanceOf(VcsActionMissingTerminalEventError); | |
| 365 | expect(error).toMatchObject({ | |
| 366 | actionId, | |
| 367 | transportActionId, | |
| 368 | action, | |
| 369 | environmentId, | |
| 370 | cwd, | |
| 371 | }); | |
| 372 | expect(error.message).toBe( | |
| 373 | "Source control action 'commit_push' ended without a terminal result.", | |
| 374 | ); | |
| 375 | }), | |
| 376 | ); | |
| 377 | ||
| 269 | 378 | it("keys mutation ownership by environment and cwd", () => { |
| 270 | 379 | const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime< |
| 271 | 380 | EnvironmentRegistry, |
| ... | ... | @@ -286,6 +395,38 @@ describe("vcsActionState", () => { |
| 286 | 395 | registry.dispose(); |
| 287 | 396 | }); |
| 288 | 397 | |
| 398 | it("retains the incomplete target and operation when tracking is unavailable", async () => { | |
| 399 | const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime< | |
| 400 | EnvironmentRegistry, | |
| 401 | never | |
| 402 | >; | |
| 403 | const manager = createVcsActionManager(runtime); | |
| 404 | const registry = AtomRegistry.make(); | |
| 405 | const result = await manager.track( | |
| 406 | registry, | |
| 407 | { environmentId, cwd: null }, | |
| 408 | { operation: "pull", label: "Pulling latest changes" }, | |
| 409 | async () => AsyncResult.success(undefined), | |
| 410 | ); | |
| 411 | ||
| 412 | expect(AsyncResult.isFailure(result)).toBe(true); | |
| 413 | if (AsyncResult.isFailure(result)) { | |
| 414 | const error = Cause.squash(result.cause); | |
| 415 | expect(error).toBeInstanceOf(VcsActionUnavailableError); | |
| 416 | if (!isVcsActionUnavailableError(error)) { | |
| 417 | throw error; | |
| 418 | } | |
| 419 | expect(error).toMatchObject({ | |
| 420 | operation: "pull", | |
| 421 | environmentId, | |
| 422 | cwd: null, | |
| 423 | }); | |
| 424 | expect(error.message).toBe("Source control operation 'pull' is unavailable."); | |
| 425 | } | |
| 426 | ||
| 427 | registry.dispose(); | |
| 428 | }); | |
| 429 | ||
| 289 | 430 | it("tracks finite mutations without letting an older completion clear newer state", async () => { |
| 290 | 431 | const runtime = Atom.runtime(Layer.empty) as unknown as Atom.AtomRuntime< |
| 291 | 432 | EnvironmentRegistry, |
packages/client-runtime/src/state/vcsAction.ts+120-43| ... | ... | @@ -1,10 +1,11 @@ |
| 1 | 1 | import { |
| 2 | 2 | EnvironmentId, |
| 3 | 3 | type EnvironmentId as EnvironmentIdType, |
| 4 | GitActionProgressPhase, | |
| 4 | 5 | type GitActionProgressEvent, |
| 5 | 6 | type GitRunStackedActionInput, |
| 6 | 7 | type GitRunStackedActionResult, |
| 7 | type GitStackedAction, | |
| 8 | GitStackedAction, | |
| 8 | 9 | WS_METHODS, |
| 9 | 10 | } from "@t3tools/contracts"; |
| 10 | 11 | import * as Cause from "effect/Cause"; |
| ... | ... | @@ -24,16 +25,18 @@ import { |
| 24 | 25 | } from "./runtime.ts"; |
| 25 | 26 | import { vcsCommandScheduler } from "./vcsCommandScheduler.ts"; |
| 26 | 27 | |
| 27 | export type VcsActionOperation = | |
| 28 | | "refresh_status" | |
| 29 | | "run_change_request" | |
| 30 | | "pull" | |
| 31 | | "switch_ref" | |
| 32 | | "create_ref" | |
| 33 | | "create_worktree" | |
| 34 | | "init" | |
| 35 | | "publish_repository" | |
| 36 | | "prepare_pull_request_thread"; | |
| 28 | export const VcsActionOperation = Schema.Literals([ | |
| 29 | "refresh_status", | |
| 30 | "run_change_request", | |
| 31 | "pull", | |
| 32 | "switch_ref", | |
| 33 | "create_ref", | |
| 34 | "create_worktree", | |
| 35 | "init", | |
| 36 | "publish_repository", | |
| 37 | "prepare_pull_request_thread", | |
| 38 | ]); | |
| 39 | export type VcsActionOperation = typeof VcsActionOperation.Type; | |
| 37 | 40 | |
| 38 | 41 | export interface VcsActionState { |
| 39 | 42 | readonly isRunning: boolean; |
| ... | ... | @@ -77,16 +80,66 @@ export interface RunVcsStackedActionInput { |
| 77 | 80 | export class VcsActionUnavailableError extends Schema.TaggedErrorClass<VcsActionUnavailableError>()( |
| 78 | 81 | "VcsActionUnavailableError", |
| 79 | 82 | { |
| 80 | message: Schema.String, | |
| 83 | operation: VcsActionOperation, | |
| 84 | environmentId: Schema.NullOr(EnvironmentId), | |
| 85 | cwd: Schema.NullOr(Schema.String), | |
| 81 | 86 | }, |
| 82 | ) {} | |
| 87 | ) { | |
| 88 | override get message(): string { | |
| 89 | return `Source control operation '${this.operation.replaceAll("_", " ")}' is unavailable.`; | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | export class VcsActionRemoteFailureError extends Schema.TaggedErrorClass<VcsActionRemoteFailureError>()( | |
| 94 | "VcsActionRemoteFailureError", | |
| 95 | { | |
| 96 | actionId: Schema.String, | |
| 97 | transportActionId: Schema.String, | |
| 98 | action: GitStackedAction, | |
| 99 | environmentId: EnvironmentId, | |
| 100 | cwd: Schema.String, | |
| 101 | phase: Schema.NullOr(GitActionProgressPhase), | |
| 102 | remoteMessageLength: Schema.Number, | |
| 103 | }, | |
| 104 | ) { | |
| 105 | override get message(): string { | |
| 106 | const phase = this.phase === null ? "execution" : this.phase; | |
| 107 | return `Source control action '${this.action}' failed during ${phase}.`; | |
| 108 | } | |
| 109 | } | |
| 83 | 110 | |
| 84 | export class VcsActionExecutionError extends Schema.TaggedErrorClass<VcsActionExecutionError>()( | |
| 85 | "VcsActionExecutionError", | |
| 111 | export class VcsActionMissingTerminalEventError extends Schema.TaggedErrorClass<VcsActionMissingTerminalEventError>()( | |
| 112 | "VcsActionMissingTerminalEventError", | |
| 86 | 113 | { |
| 87 | message: Schema.String, | |
| 114 | actionId: Schema.String, | |
| 115 | transportActionId: Schema.String, | |
| 116 | action: GitStackedAction, | |
| 117 | environmentId: EnvironmentId, | |
| 118 | cwd: Schema.String, | |
| 88 | 119 | }, |
| 89 | ) {} | |
| 120 | ) { | |
| 121 | override get message(): string { | |
| 122 | return `Source control action '${this.action}' ended without a terminal result.`; | |
| 123 | } | |
| 124 | } | |
| 125 | ||
| 126 | export class VcsActionTargetKeyParseError extends Schema.TaggedErrorClass<VcsActionTargetKeyParseError>()( | |
| 127 | "VcsActionTargetKeyParseError", | |
| 128 | { | |
| 129 | keyLength: Schema.Number, | |
| 130 | cause: Schema.Defect(), | |
| 131 | }, | |
| 132 | ) { | |
| 133 | override get message(): string { | |
| 134 | return `Invalid source control action target key (${this.keyLength} characters).`; | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | export const VcsActionExecutionError = Schema.Union([ | |
| 139 | VcsActionRemoteFailureError, | |
| 140 | VcsActionMissingTerminalEventError, | |
| 141 | ]); | |
| 142 | export type VcsActionExecutionError = typeof VcsActionExecutionError.Type; | |
| 90 | 143 | |
| 91 | 144 | export const EMPTY_VCS_ACTION_STATE = Object.freeze<VcsActionState>({ |
| 92 | 145 | isRunning: false, |
| ... | ... | @@ -104,6 +157,9 @@ export const EMPTY_VCS_ACTION_STATE = Object.freeze<VcsActionState>({ |
| 104 | 157 | |
| 105 | 158 | const nowMs = (): number => DateTime.toEpochMillis(DateTime.nowUnsafe()); |
| 106 | 159 | let nextLocalActionId = 0; |
| 160 | const decodeVcsActionTargetKey = Schema.decodeUnknownSync( | |
| 161 | Schema.Tuple([EnvironmentId, Schema.String]), | |
| 162 | ); | |
| 107 | 163 | |
| 108 | 164 | export const vcsActionStateAtom = Atom.family((key: string) => { |
| 109 | 165 | return Atom.make(EMPTY_VCS_ACTION_STATE).pipe( |
| ... | ... | @@ -124,12 +180,13 @@ export function getVcsActionTargetKey(target: VcsActionTarget): string | null { |
| 124 | 180 | return JSON.stringify([target.environmentId, target.cwd]); |
| 125 | 181 | } |
| 126 | 182 | |
| 127 | function parseVcsActionTargetKey(key: string): ResolvedVcsActionTarget { | |
| 128 | const [environmentId, cwd] = JSON.parse(key) as [string, string]; | |
| 129 | return { | |
| 130 | environmentId: EnvironmentId.make(environmentId), | |
| 131 | cwd, | |
| 132 | }; | |
| 183 | export function parseVcsActionTargetKey(key: string): ResolvedVcsActionTarget { | |
| 184 | try { | |
| 185 | const [environmentId, cwd] = decodeVcsActionTargetKey(JSON.parse(key)); | |
| 186 | return { environmentId, cwd }; | |
| 187 | } catch (cause) { | |
| 188 | throw new VcsActionTargetKeyParseError({ keyLength: key.length, cause }); | |
| 189 | } | |
| 133 | 190 | } |
| 134 | 191 | |
| 135 | 192 | export function getVcsActionStateAtom(target: VcsActionTarget) { |
| ... | ... | @@ -200,6 +257,7 @@ export function consumeVcsActionProgress<E, R>( |
| 200 | 257 | readonly target: ResolvedVcsActionTarget; |
| 201 | 258 | readonly transportActionId: string; |
| 202 | 259 | readonly actionId: string; |
| 260 | readonly action: GitStackedAction; | |
| 203 | 261 | readonly onProgress: (event: GitActionProgressEvent) => Effect.Effect<void>; |
| 204 | 262 | }, |
| 205 | 263 | ): Effect.Effect<GitRunStackedActionResult, E | VcsActionExecutionError, R> { |
| ... | ... | @@ -226,15 +284,25 @@ export function consumeVcsActionProgress<E, R>( |
| 226 | 284 | return Effect.succeed(terminalEvent.result); |
| 227 | 285 | } |
| 228 | 286 | if (terminalEvent?.kind === "action_failed") { |
| 229 | return Effect.fail( | |
| 230 | new VcsActionExecutionError({ | |
| 231 | message: terminalEvent.message, | |
| 287 | return Effect.fail<VcsActionExecutionError>( | |
| 288 | new VcsActionRemoteFailureError({ | |
| 289 | actionId: input.actionId, | |
| 290 | transportActionId: input.transportActionId, | |
| 291 | action: terminalEvent.action, | |
| 292 | environmentId: input.target.environmentId, | |
| 293 | cwd: input.target.cwd, | |
| 294 | phase: terminalEvent.phase, | |
| 295 | remoteMessageLength: terminalEvent.message.length, | |
| 232 | 296 | }), |
| 233 | 297 | ); |
| 234 | 298 | } |
| 235 | return Effect.fail( | |
| 236 | new VcsActionExecutionError({ | |
| 237 | message: "Source control action ended without a result.", | |
| 299 | return Effect.fail<VcsActionExecutionError>( | |
| 300 | new VcsActionMissingTerminalEventError({ | |
| 301 | actionId: input.actionId, | |
| 302 | transportActionId: input.transportActionId, | |
| 303 | action: input.action, | |
| 304 | environmentId: input.target.environmentId, | |
| 305 | cwd: input.target.cwd, | |
| 238 | 306 | }), |
| 239 | 307 | ); |
| 240 | 308 | }), |
| ... | ... | @@ -339,18 +407,25 @@ export function applyVcsActionProgressEvent( |
| 339 | 407 | export function createVcsActionManager<R, E>( |
| 340 | 408 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>, |
| 341 | 409 | ) { |
| 342 | const unavailableTargetKey = "vcs-action-target:unavailable"; | |
| 343 | 410 | const runStackedActionCommands = new Map< |
| 344 | 411 | string, |
| 345 | 412 | AtomCommand<RunVcsStackedActionInput, GitRunStackedActionResult, unknown> |
| 346 | 413 | >(); |
| 347 | const getRunStackedActionCommand = (key: string) => { | |
| 348 | const existing = runStackedActionCommands.get(key); | |
| 414 | const getRunStackedActionCommand = (requestedTarget: VcsActionTarget) => { | |
| 415 | const targetKey = getVcsActionTargetKey(requestedTarget); | |
| 416 | const commandKey = | |
| 417 | targetKey ?? | |
| 418 | JSON.stringify([ | |
| 419 | "vcs-action-target:unavailable", | |
| 420 | requestedTarget.environmentId, | |
| 421 | requestedTarget.cwd, | |
| 422 | ]); | |
| 423 | const existing = runStackedActionCommands.get(commandKey); | |
| 349 | 424 | if (existing !== undefined) { |
| 350 | 425 | return existing; |
| 351 | 426 | } |
| 352 | const target = key === unavailableTargetKey ? null : parseVcsActionTargetKey(key); | |
| 353 | const stateAtom = target === null ? EMPTY_VCS_ACTION_ATOM : vcsActionStateAtom(key); | |
| 427 | const target = targetKey === null ? null : parseVcsActionTargetKey(targetKey); | |
| 428 | const stateAtom = targetKey === null ? EMPTY_VCS_ACTION_ATOM : vcsActionStateAtom(targetKey); | |
| 354 | 429 | const command = createRuntimeCommand< |
| 355 | 430 | EnvironmentRegistry | R, |
| 356 | 431 | E, |
| ... | ... | @@ -358,14 +433,16 @@ export function createVcsActionManager<R, E>( |
| 358 | 433 | GitRunStackedActionResult, |
| 359 | 434 | unknown |
| 360 | 435 | >(runtime, { |
| 361 | label: `vcs-action:run-stacked:${key}`, | |
| 436 | label: `vcs-action:run-stacked:${commandKey}`, | |
| 362 | 437 | scheduler: vcsCommandScheduler, |
| 363 | concurrency: { mode: "serial", key: () => key }, | |
| 438 | concurrency: { mode: "serial", key: () => commandKey }, | |
| 364 | 439 | execute: (input: RunVcsStackedActionInput, registry) => { |
| 365 | 440 | if (target === null) { |
| 366 | 441 | return Effect.fail( |
| 367 | 442 | new VcsActionUnavailableError({ |
| 368 | message: "Source control action is unavailable.", | |
| 443 | operation: "run_change_request", | |
| 444 | environmentId: requestedTarget.environmentId, | |
| 445 | cwd: requestedTarget.cwd, | |
| 369 | 446 | }), |
| 370 | 447 | ); |
| 371 | 448 | } |
| ... | ... | @@ -396,6 +473,7 @@ export function createVcsActionManager<R, E>( |
| 396 | 473 | target, |
| 397 | 474 | transportActionId, |
| 398 | 475 | actionId: input.actionId, |
| 476 | action: input.action, | |
| 399 | 477 | onProgress: (event) => |
| 400 | 478 | Effect.sync(() => { |
| 401 | 479 | const current = registry.get(stateAtom); |
| ... | ... | @@ -427,7 +505,7 @@ export function createVcsActionManager<R, E>( |
| 427 | 505 | ); |
| 428 | 506 | }, |
| 429 | 507 | }); |
| 430 | runStackedActionCommands.set(key, command); | |
| 508 | runStackedActionCommands.set(commandKey, command); | |
| 431 | 509 | return command; |
| 432 | 510 | }; |
| 433 | 511 | |
| ... | ... | @@ -446,10 +524,7 @@ export function createVcsActionManager<R, E>( |
| 446 | 524 | |
| 447 | 525 | return { |
| 448 | 526 | stateAtom: getVcsActionStateAtom, |
| 449 | runStackedAction: (target: VcsActionTarget) => { | |
| 450 | const key = getVcsActionTargetKey(target); | |
| 451 | return getRunStackedActionCommand(key ?? unavailableTargetKey); | |
| 452 | }, | |
| 527 | runStackedAction: (target: VcsActionTarget) => getRunStackedActionCommand(target), | |
| 453 | 528 | track: async <A, E>( |
| 454 | 529 | registry: AtomRegistry.AtomRegistry, |
| 455 | 530 | target: VcsActionTarget, |
| ... | ... | @@ -461,7 +536,9 @@ export function createVcsActionManager<R, E>( |
| 461 | 536 | return AsyncResult.failure<never, VcsActionUnavailableError>( |
| 462 | 537 | Cause.fail( |
| 463 | 538 | new VcsActionUnavailableError({ |
| 464 | message: "Source control action is unavailable.", | |
| 539 | operation: input.operation, | |
| 540 | environmentId: target.environmentId, | |
| 541 | cwd: target.cwd, | |
| 465 | 542 | }), |
| 466 | 543 | ), |
| 467 | 544 | ); |