diff --git a/build/cli/index.js b/build/cli/index.js index b1de0dcd1..96fd3a725 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -53465,7 +53465,7 @@ class DeploymentOrchestrationUseCase { return await this.confirmPublication.invoke(context); if (context.singleAction.isFailedDeploymentAction) return await this.recordFailure.invoke(context); - return undefined; + throw new application_error_1.ApplicationError("workflow.invalid-event", "Deployment command does not select a supported phase."); } async handleFailure(context, error) { if (error instanceof deployment_state_boundary_1.SupersededDeploymentInvocationError) { @@ -57993,11 +57993,25 @@ function projectDeploymentOrchestrationContext(source) { ? { agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }) } : {}), labels: Object.freeze({ - ...source.labels, + isRelease: source.labels.isRelease, + isHotfix: source.labels.isHotfix, + deploy: source.labels.deploy, + deployed: source.labels.deployed, lifecycle: Object.freeze({ ...source.labels.lifecycle }), }), deployment: deepFreezeCopy(source.deployment), - singleAction: Object.freeze({ ...source.singleAction }), + singleAction: Object.freeze({ + issue: source.singleAction.issue, + version: source.singleAction.version, + title: source.singleAction.title, + changelog: source.singleAction.changelog, + operationId: source.singleAction.operationId, + message: source.singleAction.message, + isPrepareDeploymentAction: source.singleAction.isPrepareDeploymentAction, + isContinueDeploymentAction: source.singleAction.isContinueDeploymentAction, + isPublishedDeploymentAction: source.singleAction.isPublishedDeploymentAction, + isFailedDeploymentAction: source.singleAction.isFailedDeploymentAction, + }), pullRequest: Object.freeze({ ...source.pullRequest }), currentConfiguration: { ...source.currentConfiguration, diff --git a/build/github_action/index.js b/build/github_action/index.js index bd485524e..5b69e4b95 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -51779,7 +51779,7 @@ class DeploymentOrchestrationUseCase { return await this.confirmPublication.invoke(context); if (context.singleAction.isFailedDeploymentAction) return await this.recordFailure.invoke(context); - return undefined; + throw new application_error_1.ApplicationError("workflow.invalid-event", "Deployment command does not select a supported phase."); } async handleFailure(context, error) { if (error instanceof deployment_state_boundary_1.SupersededDeploymentInvocationError) { @@ -56577,11 +56577,25 @@ function projectDeploymentOrchestrationContext(source) { ? { agentConfiguration: Object.freeze({ ...source.ai.getAgentConfiguration('planner') }) } : {}), labels: Object.freeze({ - ...source.labels, + isRelease: source.labels.isRelease, + isHotfix: source.labels.isHotfix, + deploy: source.labels.deploy, + deployed: source.labels.deployed, lifecycle: Object.freeze({ ...source.labels.lifecycle }), }), deployment: deepFreezeCopy(source.deployment), - singleAction: Object.freeze({ ...source.singleAction }), + singleAction: Object.freeze({ + issue: source.singleAction.issue, + version: source.singleAction.version, + title: source.singleAction.title, + changelog: source.singleAction.changelog, + operationId: source.singleAction.operationId, + message: source.singleAction.message, + isPrepareDeploymentAction: source.singleAction.isPrepareDeploymentAction, + isContinueDeploymentAction: source.singleAction.isContinueDeploymentAction, + isPublishedDeploymentAction: source.singleAction.isPublishedDeploymentAction, + isFailedDeploymentAction: source.singleAction.isFailedDeploymentAction, + }), pullRequest: Object.freeze({ ...source.pullRequest }), currentConfiguration: { ...source.currentConfiguration, diff --git a/package.json b/package.json index 685f9dd48..46aca6eae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vypdev/copilot", - "version": "3.3.1", + "version": "3.4.0", "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": { diff --git a/src/application/usecases/__tests__/push_single_action_contexts.test.ts b/src/application/usecases/__tests__/push_single_action_contexts.test.ts index 6ce19e36b..6955ca207 100644 --- a/src/application/usecases/__tests__/push_single_action_contexts.test.ts +++ b/src/application/usecases/__tests__/push_single_action_contexts.test.ts @@ -1,6 +1,9 @@ import { DEFAULT_COPILOT_LIFECYCLE_LABELS } from '../../../domain/copilot_lifecycle'; import { DEFAULT_DEPLOYMENT_CONFIGURATION } from '../../../domain/deployment_configuration'; import type { DeploymentOrchestrationContext } from '../../ports/deployment_orchestration_ports'; +import { Labels } from '../../../data/model/labels'; +import { SingleAction } from '../../../data/model/single_action'; +import { ACTIONS } from '../../../data/model/action_types'; import { projectAgentActivityContext, projectBranchObservationContext, @@ -117,6 +120,44 @@ function source(): DeepMutable { } describe('push and single-action context projection', () => { + it.each(['release', 'hotfix'].flatMap(kind => [ + [kind, ACTIONS.PREPARE_DEPLOYMENT, 'isPrepareDeploymentAction'] as const, + [kind, ACTIONS.CONTINUE_DEPLOYMENT, 'isContinueDeploymentAction'] as const, + [kind, ACTIONS.PUBLISHED_DEPLOYMENT, 'isPublishedDeploymentAction'] as const, + [kind, ACTIONS.FAILED_DEPLOYMENT, 'isFailedDeploymentAction'] as const, + ]))('preserves %s model getters for %s', (kind, action, selectedFlag) => { + const labels = new Labels( + 'bug', 'bugfix', 'hotfix', 'enhancement', 'feature', 'release', 'question', 'help', + 'deploy', 'deployed', 'docs', 'documentation', 'chore', 'maintenance', + 'priority: high', 'priority: medium', 'priority: low', 'priority: none', + 'size: XXL', 'size: XL', 'size: L', 'size: M', 'size: S', 'size: XS', + ); + labels.currentIssueLabels = [kind, 'deploy']; + const singleAction = new SingleAction(action, '42', '3.4.0', 'Release', 'Changes', '', '', '', 'operation-12345678'); + const input = { + ...source(), labels, singleAction, + deployment: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, + workflows: { release: 'release.yml', hotfix: 'hotfix.yml' }, + } as unknown as DeploymentOrchestrationContext; + + const projected = projectDeploymentOrchestrationContext(input); + expect(projected.singleAction).toMatchObject({ + issue: 42, version: '3.4.0', title: 'Release', changelog: 'Changes', operationId: 'operation-12345678', + }); + for (const flag of ['isPrepareDeploymentAction', 'isContinueDeploymentAction', 'isPublishedDeploymentAction', 'isFailedDeploymentAction'] as const) { + expect(projected.singleAction[flag]).toBe(flag === selectedFlag); + } + expect(projected.labels).toEqual({ + isRelease: kind === 'release', isHotfix: kind === 'hotfix', + deploy: 'deploy', deployed: 'deployed', lifecycle: DEFAULT_COPILOT_LIFECYCLE_LABELS, + }); + singleAction.currentSingleAction = ''; + labels.currentIssueLabels = []; + expect(projected.singleAction[selectedFlag]).toBe(true); + expect(projected.labels.isRelease).toBe(kind === 'release'); + expect(Object.isFrozen(projected.singleAction)).toBe(true); + }); + it('copies and freezes deployment publication state', () => { const input = source(); const projected = projectDeploymentPublicationContext(input); diff --git a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts index 4c193aa6a..88e6449c4 100644 --- a/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/deployment_orchestration_use_case.test.ts @@ -9,6 +9,9 @@ import { readDeploymentOperationState, } from "../../../../domain/deployment_state_fence"; import { ResolveMessageCatalogUseCase } from "../../../usecases/localization/resolve_message_catalog_use_case"; +import { SingleAction } from "../../../../data/model/single_action"; +import { ACTIONS } from "../../../../data/model/action_types"; +import { projectDeploymentOrchestrationContext } from "../../push_single_action_contexts"; const sourceSha = "a".repeat(40); const originSha = "b".repeat(40); @@ -221,13 +224,28 @@ function harness() { describe("DeploymentOrchestrationUseCase", () => { it("prepares a durable operation and opens exactly one promotion PR", async () => { const value = harness(); - const input = execution("prepare"); + const source = execution("prepare"); + source.singleAction = new SingleAction(ACTIONS.PREPARE_DEPLOYMENT, '355', '3.4.0', 'Release', 'Changes'); + const input = projectDeploymentOrchestrationContext(source); const result = await value.useCase.invoke(input); expect(result[0].success).toBe(true); expect(value.pullRequests.createManagedPullRequest).toHaveBeenCalledTimes(1); expect(input.currentConfiguration.deploymentOrchestration).toEqual(expect.objectContaining({ phase: "promotion_pr_pending", promotionPullRequest: 40 })); }); + it("rejects a deployment command with no selected phase instead of reporting an empty success", async () => { + const value = harness(); + const input = execution("prepare"); + Object.assign(input.singleAction, { isPrepareDeploymentAction: false }); + const results = await value.useCase.invoke(input); + + expect(results).toHaveLength(1); + expect(results[0].success).toBe(false); + expect(value.state.save).not.toHaveBeenCalled(); + expect(value.pullRequests.createManagedPullRequest).not.toHaveBeenCalled(); + expect(value.continuation.dispatch).not.toHaveBeenCalled(); + }); + it("resolves one catalog per destination and snapshots the effective locale profile", async () => { const value = harness(); const input = execution("prepare"); diff --git a/src/application/usecases/actions/deployment_orchestration_use_case.ts b/src/application/usecases/actions/deployment_orchestration_use_case.ts index 03c5c04d8..7da180771 100644 --- a/src/application/usecases/actions/deployment_orchestration_use_case.ts +++ b/src/application/usecases/actions/deployment_orchestration_use_case.ts @@ -1,4 +1,4 @@ -import { toApplicationError } from "../../errors/application_error"; +import { ApplicationError, toApplicationError } from "../../errors/application_error"; import type { DeploymentOrchestrationContext } from "../../ports/deployment_orchestration_ports"; import type { DeploymentOrchestrationDependencies } from "../../services/deployment_orchestration_dependencies"; import { @@ -57,7 +57,7 @@ export class DeploymentOrchestrationUseCase implements ParamUseCase