From c7e537b54c78e9cb626806775f6ca571aa889108 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 24 Jul 2025 18:59:16 -0700 Subject: [PATCH] fix: stale version does not evict newer version --- e2e/tests/api/releases/release.spec.yaml | 4 + .../api/releases/version-release.spec.ts | 135 ++++++++++++++++++ .../src/manager/version-manager.ts | 21 ++- 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/e2e/tests/api/releases/release.spec.yaml b/e2e/tests/api/releases/release.spec.yaml index 555fcca4b..026643558 100644 --- a/e2e/tests/api/releases/release.spec.yaml +++ b/e2e/tests/api/releases/release.spec.yaml @@ -22,3 +22,7 @@ environments: type: identifier operator: contains value: "{{ prefix }}" + +agents: + - name: "{{ prefix }}-agent" + type: "{{ prefix }}-agent-type" diff --git a/e2e/tests/api/releases/version-release.spec.ts b/e2e/tests/api/releases/version-release.spec.ts index 3128d7d8a..aaec845fb 100644 --- a/e2e/tests/api/releases/version-release.spec.ts +++ b/e2e/tests/api/releases/version-release.spec.ts @@ -15,6 +15,7 @@ test.describe("Version Release Creation", () => { await builder.upsertSystemFixture(); await builder.upsertResourcesFixtures(); await builder.upsertEnvironmentFixtures(); + await builder.upsertAgentFixtures(); await new Promise((resolve) => setTimeout(resolve, 1_000)); }); @@ -102,4 +103,138 @@ test.describe("Version Release Creation", () => { const release = releases.find((rel) => rel.version.tag === versionTag); expect(release).toBeDefined(); }); + + test("should not create a release when a new version is created but is older than the current version deployed to a target", async ({ + api, + page, + workspace, + }) => { + const systemPrefix = builder.refs.system.slug.split("-")[0]!; + const { id: agentId } = builder.refs.oneAgent(); + const deploymentName = `${systemPrefix}-${faker.string.alphanumeric(10)}`; + const deploymentCreateResponse = await api.POST("/v1/deployments", { + body: { + name: deploymentName, + slug: deploymentName, + systemId: builder.refs.system.id, + jobAgentId: agentId, + }, + }); + expect(deploymentCreateResponse.response.status).toBe(201); + const deploymentId = deploymentCreateResponse.data?.id ?? ""; + + const newerVersionTag = faker.string.alphanumeric(10); + const olderVersionTag = faker.string.alphanumeric(10); + + const newerCreatedAtDate = new Date(Date.now() - 1000); + const olderCreatedAtDate = new Date(Date.now() - 2000); + + const versionResponse = await api.POST("/v1/deployment-versions", { + body: { + deploymentId, + tag: newerVersionTag, + metadata: { e2e: "true" }, + createdAt: newerCreatedAtDate.toISOString(), + }, + }); + expect(versionResponse.response.status).toBe(201); + + const importedResource = builder.refs.resources.at(0)!; + const resourceResponse = await api.GET( + "/v1/workspaces/{workspaceId}/resources/identifier/{identifier}", + { + params: { + path: { + workspaceId: workspace.id, + identifier: importedResource.identifier, + }, + }, + }, + ); + const resourceId = resourceResponse.data?.id ?? ""; + + const releaseTargetResponse = await api.GET( + "/v1/resources/{resourceId}/release-targets", + { + params: { + path: { + resourceId, + }, + }, + }, + ); + expect(releaseTargetResponse.response.status).toBe(200); + const releaseTargets = releaseTargetResponse.data ?? []; + const releaseTarget = releaseTargets.find( + (rt) => rt.deployment.id === deploymentId, + ); + expect(releaseTarget).toBeDefined(); + + await page.waitForTimeout(12_000); + + const releaseResponse = await api.GET( + "/v1/release-targets/{releaseTargetId}/releases", + { + params: { + path: { + releaseTargetId: releaseTarget?.id ?? "", + }, + }, + }, + ); + expect(releaseResponse.response.status).toBe(200); + const releases = releaseResponse.data ?? []; + const newerRelease = releases.find( + (rel) => rel.version.tag === newerVersionTag, + ); + expect(newerRelease).toBeDefined(); + + const nextJobResponse = await api.GET( + "/v1/job-agents/{agentId}/queue/next", + { + params: { + path: { agentId }, + }, + }, + ); + expect(nextJobResponse.response.status).toBe(200); + const nextJobId = nextJobResponse.data?.jobs?.[0]?.id; + expect(nextJobId).toBeDefined(); + + const successfulJobResponse = await api.PATCH("/v1/jobs/{jobId}", { + params: { path: { jobId: nextJobId ?? "" } }, + body: { status: "successful" }, + }); + + expect(successfulJobResponse.response.status).toBe(200); + + const olderVersionResponse = await api.POST("/v1/deployment-versions", { + body: { + deploymentId, + tag: olderVersionTag, + metadata: { e2e: "true" }, + createdAt: olderCreatedAtDate.toISOString(), + }, + }); + expect(olderVersionResponse.response.status).toBe(201); + + await page.waitForTimeout(12_000); + + const nextReleaseResponse = await api.GET( + "/v1/release-targets/{releaseTargetId}/releases", + { + params: { + path: { + releaseTargetId: releaseTarget?.id ?? "", + }, + }, + }, + ); + expect(nextReleaseResponse.response.status).toBe(200); + const nextReleases = nextReleaseResponse.data ?? []; + const nextRelease = nextReleases.find( + (rel) => rel.version.tag === olderVersionTag, + ); + expect(nextRelease).toBeUndefined(); + }); }); diff --git a/packages/rule-engine/src/manager/version-manager.ts b/packages/rule-engine/src/manager/version-manager.ts index 34c833766..7d68f35dc 100644 --- a/packages/rule-engine/src/manager/version-manager.ts +++ b/packages/rule-engine/src/manager/version-manager.ts @@ -6,6 +6,7 @@ import { desc, eq, inArray, + or, selector, takeFirst, takeFirstOrNull, @@ -102,7 +103,10 @@ export class VersionReleaseManager implements ReleaseManager { .innerJoin(schema.job, eq(schema.releaseJob.jobId, schema.job.id)) .where( and( - eq(schema.job.status, JobStatus.Successful), + or( + eq(schema.job.status, JobStatus.Successful), + eq(schema.job.status, JobStatus.InProgress), + ), eq(schema.versionRelease.releaseTargetId, this.releaseTarget.id), ), ) @@ -179,6 +183,15 @@ export class VersionReleaseManager implements ReleaseManager { if (desiredVersion != null) return versions.filter((version) => version.id === desiredVersion.id); + const latestDeployedVersion = await this.findLastestDeployedVersion(); + const versionsNewerThanLatest = versions.filter((version) => + isAfter( + version.createdAt, + latestDeployedVersion?.createdAt ?? new Date(0), + ), + ); + if (versionsNewerThanLatest.length === 0) return []; + const policy = await this.getPolicy(); const deploymentVersionSelector = policy?.deploymentVersionSelector?.deploymentVersionSelector; @@ -190,11 +203,11 @@ export class VersionReleaseManager implements ReleaseManager { .sql(); const isTargetedId = - versions.length === 1 - ? eq(schema.deploymentVersion.id, versions.at(0)!.id) + versionsNewerThanLatest.length === 1 + ? eq(schema.deploymentVersion.id, versionsNewerThanLatest.at(0)!.id) : inArray( schema.deploymentVersion.id, - versions.map((v) => v.id), + versionsNewerThanLatest.map((v) => v.id), ); const isReady = eq(