From 186b1bdaa916edd778f2d2f263ac1d603798ce10 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 13 May 2026 01:34:45 +0200 Subject: [PATCH 1/3] test(deploy): e2e smoke for weekly-digest --mode cloud --- .github/workflows/deploy-e2e.yml | 70 +++ .../test/e2e/weekly-digest.smoke.test.ts | 455 ++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 .github/workflows/deploy-e2e.yml create mode 100644 packages/deploy/test/e2e/weekly-digest.smoke.test.ts diff --git a/.github/workflows/deploy-e2e.yml b/.github/workflows/deploy-e2e.yml new file mode 100644 index 0000000..7ecbebb --- /dev/null +++ b/.github/workflows/deploy-e2e.yml @@ -0,0 +1,70 @@ +name: Deploy E2E Smoke + +on: + workflow_dispatch: + schedule: + - cron: '17 8 * * *' + +permissions: + contents: read + issues: write + +concurrency: + group: deploy-e2e-${{ github.ref }} + cancel-in-progress: false + +jobs: + weekly-digest: + name: weekly-digest cloud deploy + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + WORKFORCE_E2E_STAGING_TOKEN: ${{ secrets.WORKFORCE_E2E_STAGING_TOKEN }} + WORKFORCE_E2E_STAGING_URL: ${{ vars.WORKFORCE_E2E_STAGING_URL }} + WORKFORCE_E2E_STAGING_WORKSPACE_ID: ${{ vars.WORKFORCE_E2E_STAGING_WORKSPACE_ID }} + WORKFORCE_E2E_GITHUB_TOKEN: ${{ secrets.WORKFORCE_E2E_GITHUB_TOKEN || github.token }} + WORKFORCE_E2E_FIXTURE_REPO: AgentWorkforce/deploy-e2e-fixtures + SLACK_WEBHOOK_URL: ${{ secrets.WORKFORCE_ALERTS_SLACK_WEBHOOK_URL }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + cache: 'pnpm' + + - name: Install deps + run: pnpm install --frozen-lockfile + + - name: Build workspace + run: pnpm -r run build + + - name: Run weekly-digest smoke + id: smoke + continue-on-error: true + run: node --test packages/deploy/test/e2e/weekly-digest.smoke.test.ts + + - name: Report smoke result + if: always() + run: | + if [ "${{ steps.smoke.outcome }}" = "success" ]; then + echo "SMOKE_TEST: PASS" + else + echo "SMOKE_TEST: FAIL — see logs" + fi + + - name: Notify #workforce-alerts + if: steps.smoke.outcome == 'failure' + run: | + if [ -z "${SLACK_WEBHOOK_URL}" ]; then + echo "No Slack webhook configured; skipping #workforce-alerts notification." + exit 0 + fi + payload=$(node -e "console.log(JSON.stringify({text: 'SMOKE_TEST: FAIL — weekly-digest deploy E2E failed. See ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'}))") + curl -sS -X POST -H 'content-type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL" diff --git a/packages/deploy/test/e2e/weekly-digest.smoke.test.ts b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts new file mode 100644 index 0000000..7338dd5 --- /dev/null +++ b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts @@ -0,0 +1,455 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { mkdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import test from 'node:test'; +import { pathToFileURL } from 'node:url'; + +const DEFAULT_STAGING_URL = 'https://staging.agentrelay.com'; +const FIXTURE_REPO = envValue('WORKFORCE_E2E_FIXTURE_REPO') ?? 'AgentWorkforce/deploy-e2e-fixtures'; +const ISSUE_TITLE_RE = /^Weekly digest\s+—\s+/u; +const POLL_TIMEOUT_MS = 90_000; +const POLL_INTERVAL_MS = 5_000; + +test( + 'weekly-digest deploys to staging cloud and fires from a cron tick', + { timeout: 240_000 }, + async (t) => { + const stagingToken = process.env.WORKFORCE_E2E_STAGING_TOKEN?.trim(); + if (!stagingToken) { + console.log('SMOKE_TEST: SKIP — WORKFORCE_E2E_STAGING_TOKEN is unset'); + t.skip('WORKFORCE_E2E_STAGING_TOKEN is unset'); + return; + } + + const workspaceId = ( + process.env.WORKFORCE_E2E_STAGING_WORKSPACE_ID ?? + process.env.WORKFORCE_WORKSPACE_ID ?? + '' + ).trim(); + if (!workspaceId) { + throw new Error('set WORKFORCE_E2E_STAGING_WORKSPACE_ID or WORKFORCE_WORKSPACE_ID'); + } + + const stagingUrl = normalizeBaseUrl( + envValue('WORKFORCE_E2E_STAGING_URL') ?? envValue('WORKFORCE_CLOUD_URL') ?? DEFAULT_STAGING_URL + ); + const startedAt = new Date(Date.now() - 120_000); + + try { + const repoRoot = process.cwd(); + const personaPath = path.resolve(repoRoot, 'examples/weekly-digest/persona.json'); + const outDir = path.resolve(repoRoot, '.workforce/build/smoke-weekly-digest'); + + const { persona } = await buildBundleLocally({ personaPath, outDir }); + assert.equal(persona.id, 'weekly-digest'); + + let deployedAgentId; + let deployedDeploymentId; + try { + const deploy = await deployViaCloudCli({ + repoRoot, + personaPath, + stagingUrl, + stagingToken, + workspaceId + }); + + const agentId = await resolveAgentId({ + stagingUrl, + stagingToken, + workspaceId, + deployOutput: deploy.combinedOutput, + personaId: persona.id + }); + assert.ok(agentId, `expected deployed agent id in CLI output or cloud agent lookup`); + deployedAgentId = agentId; + + const deploymentId = await resolveDeploymentId({ + stagingUrl, + stagingToken, + workspaceId, + deployOutput: deploy.combinedOutput, + agentId, + personaId: persona.id + }); + assert.ok(deploymentId, `expected deployment id in CLI output or cloud deployment lookup`); + deployedDeploymentId = deploymentId; + + const tick = await forceCronTick({ stagingUrl, stagingToken, workspaceId, agentId }); + if (tick.skipped) { + const status = await readDeploymentStatus({ + stagingUrl, + stagingToken, + workspaceId, + deploymentId + }); + assert.equal(status, 'active', `expected active deployment when test tick hook is unavailable`); + console.log( + `SMOKE_TEST: PASS — deployed ${deploymentId}; tick hook unavailable, active deployment verified` + ); + return; + } + + const issue = await waitForWeeklyDigestIssue({ since: startedAt }); + assert.match(issue.title, ISSUE_TITLE_RE); + await closeIssue(issue).catch((err) => { + console.warn(`SMOKE_TEST: cleanup warning — failed to close issue #${issue.number}: ${messageOf(err)}`); + }); + + console.log(`SMOKE_TEST: PASS — deployed ${deploymentId}; issue #${issue.number} observed`); + } finally { + if (deployedAgentId || deployedDeploymentId) { + await destroyDeployment({ + stagingUrl, + stagingToken, + workspaceId, + agentId: deployedAgentId, + deploymentId: deployedDeploymentId + }).catch((err) => { + console.warn(`SMOKE_TEST: cleanup warning — failed to destroy deployment: ${messageOf(err)}`); + }); + } + } + } catch (err) { + console.error(`SMOKE_TEST: FAIL — ${messageOf(err)}`); + throw err; + } + } +); + +async function buildBundleLocally({ personaPath, outDir }) { + const raw = JSON.parse(await readFile(personaPath, 'utf8')); + const { parsePersonaSpec } = await importDist('packages/persona-kit/dist/index.js'); + const deployModule = await importDist('packages/deploy/dist/index.js'); + const persona = parsePersonaSpec(raw, raw.intent ?? 'documentation'); + + await mkdir(outDir, { recursive: true }); + const stageBundle = + deployModule.stageBundle ?? + ((input) => { + const stager = deployModule.bundleStager; + if (!stager?.stage) { + throw new Error('deploy package does not export stageBundle or bundleStager.stage'); + } + return stager.stage(input); + }); + + const bundle = await stageBundle({ personaPath, persona, outDir }); + for (const key of ['runnerPath', 'bundlePath', 'packageJsonPath']) { + assert.ok(bundle[key], `bundle missing ${key}`); + } + return { persona, bundle }; +} + +async function deployViaCloudCli({ repoRoot, personaPath, stagingUrl, stagingToken, workspaceId }) { + const args = [ + path.resolve(repoRoot, 'packages/cli/dist/cli.js'), + 'deploy', + personaPath, + '--mode', + 'cloud', + '--cloud-url', + stagingUrl, + '--workspace', + workspaceId, + '--no-connect', + '--no-prompt', + '--on-exists', + 'update', + '--input', + `WEEKLY_DIGEST_REPO=${FIXTURE_REPO}`, + '--input', + 'WEEKLY_DIGEST_TOPICS=agentworkforce,relayfile,proactive-agents', + '--detach' + ]; + const env = { + ...process.env, + WORKFORCE_CLOUD_URL: stagingUrl, + WORKFORCE_E2E_STAGING_URL: stagingUrl, + WORKFORCE_WORKSPACE_ID: workspaceId, + WORKFORCE_WORKSPACE_TOKEN: stagingToken + }; + + const result = await runNode(args, { cwd: repoRoot, env, timeoutMs: 120_000 }); + if (result.code !== 0) { + throw new Error( + `cloud deploy CLI exited ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}` + ); + } + return { ...result, combinedOutput: `${result.stdout}\n${result.stderr}` }; +} + +async function resolveAgentId({ stagingUrl, stagingToken, workspaceId, deployOutput, personaId }) { + const parsed = parseOutputId(deployOutput, ['agentId'], [ + /\bagentId["' ]*[:=]["' ]*([A-Za-z0-9_-]+)/, + /\bagent\s+([0-9a-f-]{20,})\b/i + ]); + if (parsed) return parsed; + + const queryUrls = [ + `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/agents?persona_slug=${encodeURIComponent(personaId)}`, + `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/agents?persona_id=${encodeURIComponent(personaId)}` + ]; + for (const url of queryUrls) { + const res = await fetch(url, { headers: authHeaders(stagingToken) }); + if (!res.ok) continue; + const payload = await res.json(); + const candidate = firstAgent(payload); + if (candidate?.id) return String(candidate.id); + } + return undefined; +} + +async function resolveDeploymentId({ stagingUrl, stagingToken, workspaceId, deployOutput, agentId, personaId }) { + const parsed = parseOutputId(deployOutput, ['deploymentId', 'id'], [ + /\bdeploymentId["' ]*[:=]["' ]*([A-Za-z0-9_-]+)/, + /\bdeployment\s+([A-Za-z0-9_-]{8,})\b/i, + /\bok:\s*([A-Za-z0-9_-]{8,})\b/ + ]); + if (parsed && parsed !== personaId) return parsed; + + const queryUrls = [ + `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/deployments?agent_id=${encodeURIComponent(agentId)}`, + `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/deployments?persona_slug=${encodeURIComponent(personaId)}`, + `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/deployments?persona_id=${encodeURIComponent(personaId)}` + ]; + for (const url of queryUrls) { + const res = await fetch(url, { headers: authHeaders(stagingToken) }); + if (!res.ok) continue; + const payload = await res.json(); + const candidate = firstDeployment(payload); + if (candidate?.id) return String(candidate.id); + } + return undefined; +} + +function parseOutputId(output, jsonKeys, regexes) { + for (const line of output.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + if (trimmed.startsWith('{')) { + try { + const json = JSON.parse(trimmed); + const id = + findJsonId(json, jsonKeys) ?? + findJsonId(json.agent, jsonKeys) ?? + findJsonId(json.deployment, jsonKeys) ?? + findJsonId(json.runHandle, jsonKeys); + if (id) return String(id); + } catch { + // Continue with regex parsing below. + } + } + for (const regex of regexes) { + const match = trimmed.match(regex); + if (match?.[1]) return match[1]; + } + } + return undefined; +} + +function findJsonId(value, keys) { + if (typeof value !== 'object' || value === null) return undefined; + for (const key of keys) { + if (value[key]) return value[key]; + } + return undefined; +} + +async function forceCronTick({ stagingUrl, stagingToken, workspaceId, agentId }) { + const url = `${stagingUrl}/api/v1/workspaces/${encodeURIComponent( + workspaceId + )}/agents/${encodeURIComponent(agentId)}/_test/tick`; + const res = await fetch(url, { + method: 'POST', + headers: { ...authHeaders(stagingToken), 'content-type': 'application/json' }, + body: JSON.stringify({ + scheduleName: 'weekly', + name: 'weekly', + occurredAt: new Date().toISOString() + }) + }); + if (res.status === 404 || res.status === 405 || res.status === 501) { + return { skipped: true }; + } + if (!res.ok) { + throw new Error(`cron test tick failed: ${res.status} ${await res.text()}`); + } + return { skipped: false }; +} + +async function readDeploymentStatus({ stagingUrl, stagingToken, workspaceId, deploymentId }) { + const url = `${stagingUrl}/api/v1/workspaces/${encodeURIComponent( + workspaceId + )}/deployments/${encodeURIComponent(deploymentId)}`; + const res = await fetch(url, { headers: authHeaders(stagingToken) }); + if (!res.ok) { + throw new Error(`deployment status lookup failed: ${res.status} ${await res.text()}`); + } + const payload = await res.json(); + return ( + payload.status ?? + payload.deployment?.status ?? + payload.data?.status ?? + payload.data?.deployment?.status + ); +} + +async function destroyDeployment({ stagingUrl, stagingToken, workspaceId, agentId, deploymentId }) { + const candidates = [ + deploymentId + ? `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/deployments/${encodeURIComponent( + deploymentId + )}` + : undefined, + agentId + ? `${stagingUrl}/api/v1/workspaces/${encodeURIComponent(workspaceId)}/agents/${encodeURIComponent(agentId)}` + : undefined + ].filter(Boolean); + + let lastError; + for (const url of candidates) { + const res = await fetch(url, { method: 'DELETE', headers: authHeaders(stagingToken) }); + if (res.ok || res.status === 404 || res.status === 405 || res.status === 501) return; + lastError = new Error(`${res.status} ${await res.text()}`); + } + if (lastError) throw lastError; +} + +async function waitForWeeklyDigestIssue({ since }) { + const deadline = Date.now() + POLL_TIMEOUT_MS; + let lastError; + while (Date.now() < deadline) { + try { + const issue = await findWeeklyDigestIssue({ since }); + if (issue) return issue; + } catch (err) { + lastError = err; + } + await sleep(POLL_INTERVAL_MS); + } + if (lastError) { + throw new Error(`weekly digest issue was not observed within 90s; last error: ${messageOf(lastError)}`); + } + throw new Error(`weekly digest issue was not observed within 90s`); +} + +async function findWeeklyDigestIssue({ since }) { + const [owner, repo] = splitRepo(FIXTURE_REPO); + const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`); + url.searchParams.set('state', 'open'); + url.searchParams.set('per_page', '30'); + url.searchParams.set('since', since.toISOString()); + + const res = await fetch(url, { headers: githubHeaders() }); + if (!res.ok) { + throw new Error(`GitHub issue lookup failed: ${res.status} ${await res.text()}`); + } + const issues = await res.json(); + return issues.find((issue) => { + if (issue.pull_request) return false; + if (!ISSUE_TITLE_RE.test(issue.title ?? '')) return false; + return new Date(issue.updated_at).getTime() >= since.getTime(); + }); +} + +async function closeIssue(issue) { + const token = githubToken(); + if (!token) return; + const [owner, repo] = splitRepo(FIXTURE_REPO); + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${issue.number}`, { + method: 'PATCH', + headers: { ...githubHeaders(), 'content-type': 'application/json' }, + body: JSON.stringify({ state: 'closed', state_reason: 'completed' }) + }); + if (!res.ok) { + throw new Error(`${res.status} ${await res.text()}`); + } +} + +function firstAgent(payload) { + if (Array.isArray(payload)) return payload[0]; + if (Array.isArray(payload?.agents)) return payload.agents[0]; + if (Array.isArray(payload?.data)) return payload.data[0]; + if (Array.isArray(payload?.data?.agents)) return payload.data.agents[0]; + return payload?.agent ?? payload?.data?.agent; +} + +function firstDeployment(payload) { + if (Array.isArray(payload)) return payload[0]; + if (Array.isArray(payload?.deployments)) return payload.deployments[0]; + if (Array.isArray(payload?.data)) return payload.data[0]; + if (Array.isArray(payload?.data?.deployments)) return payload.data.deployments[0]; + return payload?.deployment ?? payload?.data?.deployment; +} + +function authHeaders(token) { + return { authorization: `Bearer ${token}` }; +} + +function githubHeaders() { + const token = githubToken(); + return { + accept: 'application/vnd.github+json', + 'x-github-api-version': '2022-11-28', + ...(token ? { authorization: `Bearer ${token}` } : {}) + }; +} + +function githubToken() { + return (process.env.WORKFORCE_E2E_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN ?? '').trim(); +} + +function splitRepo(repo) { + const parts = repo.split('/'); + if (parts.length !== 2 || !parts[0] || !parts[1]) { + throw new Error(`WORKFORCE_E2E_FIXTURE_REPO must be owner/repo; got "${repo}"`); + } + return parts; +} + +function normalizeBaseUrl(value) { + return value.replace(/\/+$/, ''); +} + +function envValue(name) { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function importDist(relativePath) { + return import(pathToFileURL(path.resolve(process.cwd(), relativePath)).href); +} + +function runNode(args, { cwd, env, timeoutMs }) { + return new Promise((resolve) => { + const child = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + const timeout = setTimeout(() => { + child.kill('SIGTERM'); + stderr += `\nprocess timed out after ${timeoutMs}ms`; + }, timeoutMs); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk) => { + stderr += chunk; + }); + child.on('close', (code) => { + clearTimeout(timeout); + resolve({ code, stdout, stderr }); + }); + }); +} + +function messageOf(err) { + return err instanceof Error ? err.message : String(err); +} From ea4db5d8c8875d9350b454df9b1afc4ce1f6cc47 Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 13 May 2026 02:27:42 +0200 Subject: [PATCH 2/3] test(deploy): e2e smoke for weekly-digest --mode cloud MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track K: Track K — End-to-end smoke test See workforce/docs/plans/deploy-v1-schema-cascade-spec.md --- .github/workflows/deploy-e2e.yml | 3 +-- .../test/e2e/weekly-digest.smoke.test.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-e2e.yml b/.github/workflows/deploy-e2e.yml index 7ecbebb..a8ee57d 100644 --- a/.github/workflows/deploy-e2e.yml +++ b/.github/workflows/deploy-e2e.yml @@ -47,7 +47,6 @@ jobs: - name: Run weekly-digest smoke id: smoke - continue-on-error: true run: node --test packages/deploy/test/e2e/weekly-digest.smoke.test.ts - name: Report smoke result @@ -60,7 +59,7 @@ jobs: fi - name: Notify #workforce-alerts - if: steps.smoke.outcome == 'failure' + if: failure() && steps.smoke.outcome == 'failure' run: | if [ -z "${SLACK_WEBHOOK_URL}" ]; then echo "No Slack webhook configured; skipping #workforce-alerts notification." diff --git a/packages/deploy/test/e2e/weekly-digest.smoke.test.ts b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts index 7338dd5..94d414d 100644 --- a/packages/deploy/test/e2e/weekly-digest.smoke.test.ts +++ b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts @@ -10,6 +10,7 @@ const FIXTURE_REPO = envValue('WORKFORCE_E2E_FIXTURE_REPO') ?? 'AgentWorkforce/d const ISSUE_TITLE_RE = /^Weekly digest\s+—\s+/u; const POLL_TIMEOUT_MS = 90_000; const POLL_INTERVAL_MS = 5_000; +const READY_DEPLOYMENT_STATUSES = new Set(['active', 'ready', 'live', 'running']); test( 'weekly-digest deploys to staging cloud and fires from a cron tick', @@ -28,7 +29,9 @@ test( '' ).trim(); if (!workspaceId) { - throw new Error('set WORKFORCE_E2E_STAGING_WORKSPACE_ID or WORKFORCE_WORKSPACE_ID'); + console.log('SMOKE_TEST: SKIP — WORKFORCE_E2E_STAGING_WORKSPACE_ID is unset'); + t.skip('WORKFORCE_E2E_STAGING_WORKSPACE_ID is unset'); + return; } const stagingUrl = normalizeBaseUrl( @@ -84,9 +87,12 @@ test( workspaceId, deploymentId }); - assert.equal(status, 'active', `expected active deployment when test tick hook is unavailable`); + assert.ok( + READY_DEPLOYMENT_STATUSES.has(status), + `expected ready deployment when test tick hook is unavailable; got ${String(status)}` + ); console.log( - `SMOKE_TEST: PASS — deployed ${deploymentId}; tick hook unavailable, active deployment verified` + `SMOKE_TEST: PASS — deployed ${deploymentId}; tick hook unavailable, deployment status ${status} verified` ); return; } @@ -154,9 +160,6 @@ async function deployViaCloudCli({ repoRoot, personaPath, stagingUrl, stagingTok '--workspace', workspaceId, '--no-connect', - '--no-prompt', - '--on-exists', - 'update', '--input', `WEEKLY_DIGEST_REPO=${FIXTURE_REPO}`, '--input', @@ -168,7 +171,8 @@ async function deployViaCloudCli({ repoRoot, personaPath, stagingUrl, stagingTok WORKFORCE_CLOUD_URL: stagingUrl, WORKFORCE_E2E_STAGING_URL: stagingUrl, WORKFORCE_WORKSPACE_ID: workspaceId, - WORKFORCE_WORKSPACE_TOKEN: stagingToken + WORKFORCE_WORKSPACE_TOKEN: stagingToken, + WORKFORCE_INTEGRATION_GITHUB_TOKEN: githubToken() }; const result = await runNode(args, { cwd: repoRoot, env, timeoutMs: 120_000 }); From 40e3ce7d015783567af2b540738ab7abbf869f8d Mon Sep 17 00:00:00 2001 From: Ricky Schema Cascade Date: Wed, 13 May 2026 08:29:40 +0200 Subject: [PATCH 3/3] test(deploy): address PR#99 review feedback - destroyDeployment: continue to next candidate on acceptable DELETE response so both deployment and agent cleanups are always attempted (was returning early, leaking agent resources) - deploy-e2e workflow: use curl -fsS --retry for Slack webhook so non-2xx responses fail the alert step instead of being silently swallowed Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/deploy-e2e.yml | 2 +- packages/deploy/test/e2e/weekly-digest.smoke.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-e2e.yml b/.github/workflows/deploy-e2e.yml index a8ee57d..414fbf7 100644 --- a/.github/workflows/deploy-e2e.yml +++ b/.github/workflows/deploy-e2e.yml @@ -66,4 +66,4 @@ jobs: exit 0 fi payload=$(node -e "console.log(JSON.stringify({text: 'SMOKE_TEST: FAIL — weekly-digest deploy E2E failed. See ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'}))") - curl -sS -X POST -H 'content-type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL" + curl -fsS --retry 3 --retry-delay 2 -X POST -H 'content-type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL" diff --git a/packages/deploy/test/e2e/weekly-digest.smoke.test.ts b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts index 94d414d..ef9dc3d 100644 --- a/packages/deploy/test/e2e/weekly-digest.smoke.test.ts +++ b/packages/deploy/test/e2e/weekly-digest.smoke.test.ts @@ -315,7 +315,7 @@ async function destroyDeployment({ stagingUrl, stagingToken, workspaceId, agentI let lastError; for (const url of candidates) { const res = await fetch(url, { method: 'DELETE', headers: authHeaders(stagingToken) }); - if (res.ok || res.status === 404 || res.status === 405 || res.status === 501) return; + if (res.ok || res.status === 404 || res.status === 405 || res.status === 501) continue; lastError = new Error(`${res.status} ${await res.text()}`); } if (lastError) throw lastError;