From da405b13985ca1ea4cad651d8436990774234d3c Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Thu, 15 Oct 2026 22:32:48 +0200 Subject: [PATCH 1/2] release-3.4.0: prepare release 3.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": { From 47388e5db5d1af7693432b9662417780ae661fb2 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 15 Oct 2026 22:47:33 +0200 Subject: [PATCH 2/2] Preserve deployment model selectors across context projection Project SingleAction and Labels getter values explicitly so live prepare, continuation, publication and failure commands select their handlers. Reject an unselected phase instead of returning a false successful result. Cover real class instances and the promotion route. Refs #499. --- build/cli/index.js | 20 +++++++-- build/github_action/index.js | 20 +++++++-- .../push_single_action_contexts.test.ts | 41 +++++++++++++++++++ .../deployment_orchestration_use_case.test.ts | 20 ++++++++- .../deployment_orchestration_use_case.ts | 4 +- .../usecases/push_single_action_contexts.ts | 18 +++++++- 6 files changed, 112 insertions(+), 11 deletions(-) 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/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