diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e46d89622..37c4bca5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1077,7 +1077,11 @@ jobs: fi - name: Promote verified Website artifact unchanged if: steps.freshness.outputs.stale != 'true' && steps.affected.outputs.website == 'true' && steps.website_promotion_freshness.outputs.fresh == 'true' - run: npx vercel promote "${{ steps.deploy_website.outputs.deployment_url }}" --yes --token=${{ secrets.VERCEL_TOKEN }} + # `promote` takes a bare deployment URL, so unlike build/deploy/pull it + # cannot read the team from .vercel/project.json and falls back to the + # token's default team — a personal one here, which fails with + # "Deployment doesn't belong to current team". Scope it explicitly. + run: npx vercel promote "${{ steps.deploy_website.outputs.deployment_url }}" --scope=${{ secrets.VERCEL_ORG_ID }} --yes --token=${{ secrets.VERCEL_TOKEN }} - name: Verify deployed website if: steps.freshness.outputs.stale != 'true' && ((steps.affected.outputs.website == 'true' && steps.website_promotion_freshness.outputs.fresh == 'true') || steps.affected.outputs.cockpit == 'true') run: npx nx e2e website --skip-nx-cache @@ -1125,7 +1129,11 @@ jobs: fi - name: Promote verified cockpit artifact unchanged if: steps.freshness.outputs.stale != 'true' && steps.affected.outputs.cockpit == 'true' && steps.cockpit_promotion_freshness.outputs.fresh == 'true' - run: npx vercel promote "${{ steps.deploy_cockpit.outputs.deployment_url }}" --yes --token=${{ secrets.VERCEL_TOKEN }} + # `promote` takes a bare deployment URL, so unlike build/deploy/pull it + # cannot read the team from .vercel/project.json and falls back to the + # token's default team — a personal one here, which fails with + # "Deployment doesn't belong to current team". Scope it explicitly. + run: npx vercel promote "${{ steps.deploy_cockpit.outputs.deployment_url }}" --scope=${{ secrets.VERCEL_ORG_ID }} --yes --token=${{ secrets.VERCEL_TOKEN }} - name: Verify production cockpit redirects if: steps.freshness.outputs.stale != 'true' && steps.affected.outputs.cockpit == 'true' && steps.cockpit_promotion_freshness.outputs.fresh == 'true' run: npx tsx apps/cockpit/scripts/deploy-smoke.ts --url https://cockpit.threadplane.ai --mode production --retries 20 --retry-delay-ms 5000 diff --git a/scripts/ci-workflow.spec.mjs b/scripts/ci-workflow.spec.mjs index d4794125e..ae0270174 100644 --- a/scripts/ci-workflow.spec.mjs +++ b/scripts/ci-workflow.spec.mjs @@ -77,6 +77,21 @@ function readNamedStep(job, name) { } describe('CI workflow', () => { + it('scopes every vercel promote to the team that owns the deployment', async () => { + const workflow = await readFile('.github/workflows/ci.yml', 'utf8'); + const promotes = workflow + .split('\n') + .filter((line) => line.includes('vercel promote')); + + assert.ok(promotes.length >= 2, 'expected Website and cockpit promotions'); + for (const line of promotes) { + // `promote` takes a bare URL and cannot read .vercel/project.json, so + // without --scope it uses the token's default team and fails. + assert.match(line, /--scope=/, `unscoped vercel promote: ${line.trim()}`); + } + }); + + it('runs in a merge queue and reports the required context there', async () => { const workflow = await readFile('.github/workflows/ci.yml', 'utf8'); @@ -368,7 +383,7 @@ describe('CI workflow', () => { assert.match(freshnessStep, /fresh=true.*GITHUB_OUTPUT/); assert.match( promoteStep, - /vercel promote "\$\{\{ steps\.deploy_website\.outputs\.deployment_url \}\}" --yes/ + /vercel promote "\$\{\{ steps\.deploy_website\.outputs\.deployment_url \}\}" --scope=\$\{\{ secrets\.VERCEL_ORG_ID \}\} --yes/ ); assert.match( promoteStep, @@ -519,7 +534,7 @@ describe('CI workflow', () => { assert.match(promotionFreshnessStep, /fresh=true.*GITHUB_OUTPUT/); assert.match( promoteStep, - /vercel promote "\$\{\{ steps\.deploy_cockpit\.outputs\.deployment_url \}\}" --yes/ + /vercel promote "\$\{\{ steps\.deploy_cockpit\.outputs\.deployment_url \}\}" --scope=\$\{\{ secrets\.VERCEL_ORG_ID \}\} --yes/ ); assert.match( promoteStep,