Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,25 @@ jobs:
with: { node-version: '22.19', cache: pnpm }
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm cf:build
- uses: cloudflare/wrangler-action@v3
- name: Upload Worker version
id: upload
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Code releases preserve existing routes; route changes use a separately authorized credential.
command: |
versions upload --env="" --tag ${{ github.sha }}
versions deploy --env="" --version-tag ${{ github.sha }}@100% --yes
command: versions upload --env="" --tag ${{ github.sha }}
- name: Resolve this upload's version ID
id: uploaded-version
run: node scripts/uploaded-worker-version.mjs
env:
WORKER_UPLOAD_OUTPUT: ${{ steps.upload.outputs.command-output }}
- name: Deploy the uploaded version
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions deploy --env="" ${{ steps.uploaded-version.outputs.version-id }}@100% --yes
- name: Smoke check production
run: pnpm smoke:prod

Expand Down
7 changes: 6 additions & 1 deletion __tests__/deploy-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ describe('deploy workflow', () => {
expect(deployWorkflow).toContain('workflow_dispatch:');
expect(deployWorkflow).toContain("if: github.event_name == 'workflow_dispatch'");
expect(deployWorkflow).toContain('pnpm cf:build');
expect(deployWorkflow).toMatch(/versions upload --env="" --tag \$\{\{ github\.sha \}\}/);
expect(deployWorkflow).toMatch(
/versions upload --env="" --tag \$\{\{ github\.sha \}\}\n\s+versions deploy --env="" --version-tag \$\{\{ github\.sha \}\}@100% --yes/
/WORKER_UPLOAD_OUTPUT: \$\{\{ steps\.upload\.outputs\.command-output \}\}/
);
expect(deployWorkflow).toMatch(
/versions deploy --env="" \$\{\{ steps\.uploaded-version\.outputs\.version-id \}\}@100% --yes/
);
expect(deployWorkflow).not.toContain('--version-tag');
expect(deployWorkflow).not.toMatch(
/^[ \t]+(?:command: )?(?:wrangler )?(?:deploy|triggers deploy)(?:[ \t]|$)/m
);
Expand Down
29 changes: 29 additions & 0 deletions __tests__/uploaded-worker-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// A repeat deployment produced two versions with the same SHA tag. Resolve the
// UUID from this upload's actual Wrangler output, never from a shared tag.
import { describe, expect, it } from 'vitest';
import { uploadedWorkerVersionId } from '../scripts/uploaded-worker-version.mjs';

const version = '11111111-2222-4333-8444-555555555555';

describe('uploaded Worker identity', () => {
it('selects the uploaded version despite other IDs and a reused SHA tag', () => {
const output = `Bucket ID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\nUploaded resume-tailor (6.17 sec)\nWorker Version ID: ${version}\nTag: ${'a'.repeat(40)}\n`;
expect(uploadedWorkerVersionId(output)).toBe(version);
});

it('accepts Wrangler ANSI formatting without changing the UUID', () => {
expect(uploadedWorkerVersionId(`\u001b[32mWorker Version ID: ${version}\u001b[0m`)).toBe(
version
);
});

it.each([
'',
`Version ID: ${version}`,
'Worker Version ID: not-a-version',
`Worker Version ID: ${version} trailing-command`,
`Worker Version ID: ${version}\nWorker Version ID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`,
])('refuses missing or ambiguous upload identity (%s)', (output) => {
expect(() => uploadedWorkerVersionId(output)).toThrow();
});
});
21 changes: 21 additions & 0 deletions scripts/uploaded-worker-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { appendFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

export function uploadedWorkerVersionId(output) {
const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
const plain = String(output).replace(ansi, '');
const matches = [
...plain.matchAll(
/^\s*Worker Version ID:\s*([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\s*$/gim
),
];
if (matches.length !== 1) throw new Error('Expected exactly one uploaded Worker Version ID.');
return matches[0][1].toLowerCase();
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const versionId = uploadedWorkerVersionId(process.env.WORKER_UPLOAD_OUTPUT ?? '');
if (!process.env.GITHUB_OUTPUT) throw new Error('GITHUB_OUTPUT is required.');
appendFileSync(process.env.GITHUB_OUTPUT, `version-id=${versionId}\n`);
}
8 changes: 7 additions & 1 deletion src/__tests__/resume-oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,13 @@ it('qualifies registration, consent, PKCE, replay resistance, owner identity, re
expect(valid.status).toBe(200);
expect(await valid.json()).toEqual({ user: 'oauth-owner' });
expect((await invoke(tokens.access_token, '/api/resume')).status).toBe(401);
expect((await invoke(`${tokens.access_token.slice(0, -2)}XX`)).status).toBe(401);
// Base64url's unused trailing bits can make an XX suffix decode to the same
// signature. Change the first signature character so the bytes always differ.
const parts = tokens.access_token.split('.');
const signature = parts[2];
parts[2] = `${signature[0] === 'A' ? 'B' : 'A'}${signature.slice(1)}`;
expect(Buffer.from(parts[2], 'base64url')).not.toEqual(Buffer.from(signature, 'base64url'));
expect((await invoke(parts.join('.'))).status).toBe(401);
expect((await token({ ...grant, code: freshCode })).status).toBeGreaterThanOrEqual(400);
const refreshed = await token({
grant_type: 'refresh_token',
Expand Down
Loading