diff --git a/.gitea/actions/quickstack-oci/deploy.mjs b/.gitea/actions/quickstack-oci/deploy.mjs index 1748658..b166fb9 100644 --- a/.gitea/actions/quickstack-oci/deploy.mjs +++ b/.gitea/actions/quickstack-oci/deploy.mjs @@ -101,7 +101,10 @@ export function createQuickStackClient({ baseUrl, token, fetchImpl = fetch }) { }); const parsed = await readBody(response); if (!response.ok) { - throw new Error(`${method} ${pathname} failed with HTTP ${response.status}: ${parsed?.detail ?? response.statusText}`); + throw Object.assign( + new Error(`${method} ${pathname} failed with HTTP ${response.status}: ${parsed?.detail ?? response.statusText}`), + { statusCode: response.status }, + ); } return parsed; } @@ -118,7 +121,17 @@ export function createQuickStackClient({ baseUrl, token, fetchImpl = fetch }) { async function waitForDeployment(client, appId, deploymentId, sleep) { for (let attempt = 1; attempt <= 180; attempt += 1) { - const deployment = await client.getDeployment(appId, deploymentId); + let deployment; + try { + deployment = await client.getDeployment(appId, deploymentId); + } catch (error) { + if (error?.statusCode === 404) { + console.log(`QuickStack deployment ${deploymentId}: NOT_VISIBLE_YET (${attempt}/180)`); + await sleep(5_000); + continue; + } + throw error; + } const status = String(deployment?.status ?? "UNKNOWN").toUpperCase(); console.log(`QuickStack deployment ${deploymentId}: ${status} (${attempt}/180)`); if (SUCCESS.has(status)) return deployment; diff --git a/.gitea/actions/quickstack-oci/deploy.test.mjs b/.gitea/actions/quickstack-oci/deploy.test.mjs index bb17127..245cabf 100644 --- a/.gitea/actions/quickstack-oci/deploy.test.mjs +++ b/.gitea/actions/quickstack-oci/deploy.test.mjs @@ -147,3 +147,35 @@ test("transient QuickStack deployment failures retry the desired immutable image assert.equal(current.sourceType, "CONTAINER"); assert.equal(current.containerImageSource, image); }); + +test("a newly created deployment may be briefly unavailable without triggering rollback", async () => { + let current = app(); + let deployments = 0; + let lookups = 0; + const image = `gitea.example/owner/app@sha256:${"c".repeat(64)}`; + const client = { + getApp: async () => current, + saveApp: async (payload) => { current = { ...current, ...payload }; }, + deployApp: async () => ({ deploymentId: `deployment-${++deployments}` }), + getDeployment: async (_appId, deploymentId) => { + lookups += 1; + if (lookups === 1) throw Object.assign(new Error("not visible"), { statusCode: 404 }); + return { deploymentId, status: "DEPLOYED" }; + }, + }; + + const result = await deployExactImage({ + client, + appId: "app-one", + image, + registryUsername: "deploy", + registryToken: "token", + sha: "3".repeat(40), + sleep: async () => {}, + }); + + assert.equal(deployments, 1); + assert.equal(lookups, 2); + assert.equal(result.image, image); + assert.equal(current.sourceType, "CONTAINER"); +});