From bf5e69a7bda2b737f72946cd5d9cd724b3b0f7e0 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 15 Oct 2026 23:07:52 +0200 Subject: [PATCH 1/2] Preserve deployment PR identity and GitHub merge receipts --- build/cli/index.js | 6 +++--- build/github_action/index.js | 6 +++--- .../push_single_action_contexts.test.ts | 8 +++++++- .../deployment_orchestration_use_case.test.ts | 7 ++++++- .../usecases/push_single_action_contexts.ts | 2 +- .../github_deployment_adapters.test.ts | 17 ++++++++++++++++- .../github_deployment_git_repository.ts | 2 +- .../github_managed_pull_request_repository.ts | 2 +- .../ports/github_deployment_provider_port.ts | 3 ++- 9 files changed, 40 insertions(+), 13 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 96fd3a725..d4100e34d 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -58012,7 +58012,7 @@ function projectDeploymentOrchestrationContext(source) { isPublishedDeploymentAction: source.singleAction.isPublishedDeploymentAction, isFailedDeploymentAction: source.singleAction.isFailedDeploymentAction, }), - pullRequest: Object.freeze({ ...source.pullRequest }), + pullRequest: Object.freeze({ number: source.pullRequest.number }), currentConfiguration: { ...source.currentConfiguration, ...(source.currentConfiguration.deploymentOrchestration @@ -79265,7 +79265,7 @@ class GithubDeploymentGitRepository { head: sourceSha, commit_message: `chore(release): reconcile ${sourceSha.slice(0, 7)} into ${branch}`, }); - if (!data.merged || !data.sha) + if (!data.sha) throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); return data.sha; } @@ -79405,7 +79405,7 @@ function mapPullRequest(value, owner, repository) { headSha: value.head.sha, baseBranch: value.base.ref, state: value.state === "closed" ? "closed" : "open", - merged: value.merged === true, + merged: value.merged === true || (typeof value.merged_at === "string" && value.merged_at.length > 0), autoMergeEnabled: value.auto_merge !== null && value.auto_merge !== undefined, mergeCommitSha: value.merge_commit_sha ?? undefined, repositoryFullName: value.base.repo?.full_name ?? value.head.repo?.full_name ?? `${owner}/${repository}`, diff --git a/build/github_action/index.js b/build/github_action/index.js index 5b69e4b95..edb4a6c4a 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -56596,7 +56596,7 @@ function projectDeploymentOrchestrationContext(source) { isPublishedDeploymentAction: source.singleAction.isPublishedDeploymentAction, isFailedDeploymentAction: source.singleAction.isFailedDeploymentAction, }), - pullRequest: Object.freeze({ ...source.pullRequest }), + pullRequest: Object.freeze({ number: source.pullRequest.number }), currentConfiguration: { ...source.currentConfiguration, ...(source.currentConfiguration.deploymentOrchestration @@ -71533,7 +71533,7 @@ class GithubDeploymentGitRepository { head: sourceSha, commit_message: `chore(release): reconcile ${sourceSha.slice(0, 7)} into ${branch}`, }); - if (!data.merged || !data.sha) + if (!data.sha) throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); return data.sha; } @@ -71673,7 +71673,7 @@ function mapPullRequest(value, owner, repository) { headSha: value.head.sha, baseBranch: value.base.ref, state: value.state === "closed" ? "closed" : "open", - merged: value.merged === true, + merged: value.merged === true || (typeof value.merged_at === "string" && value.merged_at.length > 0), autoMergeEnabled: value.auto_merge !== null && value.auto_merge !== undefined, mergeCommitSha: value.merge_commit_sha ?? undefined, repositoryFullName: value.base.repo?.full_name ?? value.head.repo?.full_name ?? `${owner}/${repository}`, 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 6955ca207..2b4cb7e66 100644 --- a/src/application/usecases/__tests__/push_single_action_contexts.test.ts +++ b/src/application/usecases/__tests__/push_single_action_contexts.test.ts @@ -3,6 +3,7 @@ import { DEFAULT_DEPLOYMENT_CONFIGURATION } from '../../../domain/deployment_con import type { DeploymentOrchestrationContext } from '../../ports/deployment_orchestration_ports'; import { Labels } from '../../../data/model/labels'; import { SingleAction } from '../../../data/model/single_action'; +import { PullRequest } from '../../../data/model/pull_request'; import { ACTIONS } from '../../../data/model/action_types'; import { projectAgentActivityContext, @@ -134,8 +135,9 @@ describe('push and single-action context projection', () => { ); labels.currentIssueLabels = [kind, 'deploy']; const singleAction = new SingleAction(action, '42', '3.4.0', 'Release', 'Changes', '', '', '', 'operation-12345678'); + const pullRequest = new PullRequest(1, 1, { pull_request: { number: 99 } } as PullRequest['inputs']); const input = { - ...source(), labels, singleAction, + ...source(), labels, singleAction, pullRequest, deployment: { ...DEFAULT_DEPLOYMENT_CONFIGURATION }, workflows: { release: 'release.yml', hotfix: 'hotfix.yml' }, } as unknown as DeploymentOrchestrationContext; @@ -151,10 +153,14 @@ describe('push and single-action context projection', () => { isRelease: kind === 'release', isHotfix: kind === 'hotfix', deploy: 'deploy', deployed: 'deployed', lifecycle: DEFAULT_COPILOT_LIFECYCLE_LABELS, }); + expect(projected.pullRequest).toEqual({ number: 99 }); + pullRequest.inputs = { pull_request: { number: 100 } } as PullRequest['inputs']; singleAction.currentSingleAction = ''; labels.currentIssueLabels = []; expect(projected.singleAction[selectedFlag]).toBe(true); expect(projected.labels.isRelease).toBe(kind === 'release'); + expect(projected.pullRequest.number).toBe(99); + expect(Object.isFrozen(projected.pullRequest)).toBe(true); expect(Object.isFrozen(projected.singleAction)).toBe(true); }); 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 88e6449c4..dcce836fd 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 @@ -10,6 +10,7 @@ import { } from "../../../../domain/deployment_state_fence"; import { ResolveMessageCatalogUseCase } from "../../../usecases/localization/resolve_message_catalog_use_case"; import { SingleAction } from "../../../../data/model/single_action"; +import { PullRequest } from "../../../../data/model/pull_request"; import { ACTIONS } from "../../../../data/model/action_types"; import { projectDeploymentOrchestrationContext } from "../../push_single_action_contexts"; @@ -542,8 +543,12 @@ describe("DeploymentOrchestrationUseCase", () => { it("verifies and dispatches publication for a merged promotion event", async () => { const value = harness(); value.pullRequests.getPullRequest.mockResolvedValue(pr({ state: "closed", merged: true, mergeCommitSha: productionSha })); - const input = execution("continue", operation("promotion_pr_pending")); + const source = execution("continue", operation("promotion_pr_pending")); + source.singleAction = new SingleAction(ACTIONS.CONTINUE_DEPLOYMENT, '355', '3.4.0', 'Release', 'Changes', '', '', '', 'operation-12345678'); + source.pullRequest = new PullRequest(1, 1, { pull_request: { number: 40 } } as PullRequest['inputs']); + const input = projectDeploymentOrchestrationContext(source); await value.useCase.invoke(input); + expect(value.pullRequests.getPullRequest).toHaveBeenCalledWith(40); expect(input.currentConfiguration.deploymentOrchestration).toEqual(expect.objectContaining({ phase: "publishing", productionSha })); expect(value.continuation.dispatch).toHaveBeenCalledTimes(1); }); diff --git a/src/application/usecases/push_single_action_contexts.ts b/src/application/usecases/push_single_action_contexts.ts index 5ffc2b1b3..887a0a567 100644 --- a/src/application/usecases/push_single_action_contexts.ts +++ b/src/application/usecases/push_single_action_contexts.ts @@ -253,7 +253,7 @@ export function projectDeploymentOrchestrationContext( isPublishedDeploymentAction: source.singleAction.isPublishedDeploymentAction, isFailedDeploymentAction: source.singleAction.isFailedDeploymentAction, }), - pullRequest: Object.freeze({ ...source.pullRequest }), + pullRequest: Object.freeze({ number: source.pullRequest.number }), currentConfiguration: { ...source.currentConfiguration, ...(source.currentConfiguration.deploymentOrchestration diff --git a/src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts b/src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts index b21f52415..10b0e7c0e 100644 --- a/src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts +++ b/src/data/repository/deployment/__tests__/github_deployment_adapters.test.ts @@ -128,6 +128,13 @@ describe("GitHub deployment repository", () => { .resolves.toEqual(expect.objectContaining({ state: "closed", merged: true, mergeCommitSha: "c".repeat(40) })); }); + it("recognizes a merged PR list response without the detail-only merged flag", async () => { + const value = harness(); + value.paginate.mockResolvedValue([pullRequest({ state: "closed", merged: undefined, merged_at: "2026-10-15T20:53:40Z", merge_commit_sha: "c".repeat(40) })]); + await expect(value.pullRequests.findManagedPullRequests(query)) + .resolves.toEqual([expect.objectContaining({ state: "closed", merged: true, mergeCommitSha: "c".repeat(40) })]); + }); + it("maps the provider's auto-merge receipt", async () => { const value = harness(); value.pulls.get.mockResolvedValue({ data: pullRequest({ auto_merge: {} }) }); @@ -825,11 +832,19 @@ describe("GitHub deployment repository", () => { it("merges the trusted source SHA into an ephemeral branch", async () => { const value = harness(); value.repos.compareCommits.mockResolvedValue({ data: { merge_base_commit: { sha: "f".repeat(40) } } }); - value.repos.merge.mockResolvedValue({ data: { merged: true, sha: "c".repeat(40) } }); + value.repos.merge.mockResolvedValue({ data: { sha: "c".repeat(40) } }); await expect(value.gitRepository.mergeCommitIntoBranch("owner", "repo", "sync/release", "a".repeat(40), "token")) .resolves.toBe("c".repeat(40)); }); + it("rejects a branch merge response without a commit receipt", async () => { + const value = harness(); + value.repos.compareCommits.mockResolvedValue({ data: { merge_base_commit: { sha: "f".repeat(40) } } }); + value.repos.merge.mockResolvedValue({ data: {} }); + await expect(value.gitRepository.mergeCommitIntoBranch("owner", "repo", "sync/release", "a".repeat(40), "token")) + .rejects.toThrow("Could not reconcile"); + }); + it("reuses an advanced sync branch that still contains its target base", async () => { const value = harness(); value.git.getRef.mockResolvedValue({ data: { object: { sha: "e".repeat(40) } } }); diff --git a/src/data/repository/deployment/github_deployment_git_repository.ts b/src/data/repository/deployment/github_deployment_git_repository.ts index f8eb0a1a5..5511aa2d1 100644 --- a/src/data/repository/deployment/github_deployment_git_repository.ts +++ b/src/data/repository/deployment/github_deployment_git_repository.ts @@ -82,7 +82,7 @@ export class GithubDeploymentGitRepository implements DeploymentGitPort { head: sourceSha, commit_message: `chore(release): reconcile ${sourceSha.slice(0, 7)} into ${branch}`, }); - if (!data.merged || !data.sha) throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); + if (!data.sha) throw new Error(data.message ?? `Could not reconcile ${sourceSha} into ${branch}.`); return data.sha; } diff --git a/src/data/repository/deployment/github_managed_pull_request_repository.ts b/src/data/repository/deployment/github_managed_pull_request_repository.ts index e9a0801c0..3d923db6a 100644 --- a/src/data/repository/deployment/github_managed_pull_request_repository.ts +++ b/src/data/repository/deployment/github_managed_pull_request_repository.ts @@ -136,7 +136,7 @@ function mapPullRequest( headSha: value.head.sha, baseBranch: value.base.ref, state: value.state === "closed" ? "closed" : "open", - merged: value.merged === true, + merged: value.merged === true || (typeof value.merged_at === "string" && value.merged_at.length > 0), autoMergeEnabled: value.auto_merge !== null && value.auto_merge !== undefined, mergeCommitSha: value.merge_commit_sha ?? undefined, repositoryFullName: value.base.repo?.full_name ?? value.head.repo?.full_name ?? `${owner}/${repository}`, diff --git a/src/infrastructure/github/ports/github_deployment_provider_port.ts b/src/infrastructure/github/ports/github_deployment_provider_port.ts index 411ade607..0b06fcabc 100644 --- a/src/infrastructure/github/ports/github_deployment_provider_port.ts +++ b/src/infrastructure/github/ports/github_deployment_provider_port.ts @@ -4,6 +4,7 @@ export interface GithubDeploymentPullRequest { body?: string | null; state?: string; merged?: boolean; + merged_at?: string | null; auto_merge?: object | null; mergeable?: boolean | null; mergeable_state?: string; @@ -38,7 +39,7 @@ export interface GithubDeploymentClient { getContent(parameters: Record): Promise<{ data: unknown }>; compareCommits(parameters: Record): Promise<{ data: { status?: string; merge_base_commit?: { sha?: string } } }>; listBranches(parameters: Record): Promise<{ data: Array<{ name: string }> }>; - merge(parameters: Record): Promise<{ data: { merged: boolean; sha?: string; message?: string } }>; + merge(parameters: Record): Promise<{ data: { sha?: string; message?: string } }>; }; git: { getRef(parameters: Record): Promise<{ data: { object: { sha: string } } }>; From e31c73d09307cddbc051cc42b62374492e323dfc Mon Sep 17 00:00:00 2001 From: vypbot Date: Thu, 15 Oct 2026 23:12:35 +0200 Subject: [PATCH 2/2] release-3.5.0: prepare release 3.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46aca6eae..fd09c3a22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vypdev/copilot", - "version": "3.4.0", + "version": "3.5.0", "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": {