diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml new file mode 100644 index 0000000..7e07f85 --- /dev/null +++ b/.github/actions/deploy/action.yml @@ -0,0 +1,56 @@ +name: 'Deploy' +description: 'Build and deploy to Cloudflare Workers' + +inputs: + environment: + description: 'Cloudflare environment (e.g., presto, moderato)' + required: true + command: + description: 'Wrangler command (e.g., "versions upload" or "deploy")' + required: false + default: 'versions upload' + cloudflare-api-token: + description: 'Cloudflare API token' + required: true + cloudflare-account-id: + description: 'Cloudflare account ID' + required: true + +outputs: + status: + description: 'Deployment status (success or failed)' + value: ${{ steps.result.outputs.status }} + url: + description: 'Deployment URL' + value: ${{ steps.result.outputs.url }} + +runs: + using: 'composite' + steps: + - name: Build + shell: bash + env: + NODE_ENV: production + CLOUDFLARE_ENV: ${{ inputs.environment }} + run: pnpm build + + - name: Deploy + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ inputs.cloudflare-api-token }} + accountId: ${{ inputs.cloudflare-account-id }} + environment: ${{ inputs.environment }} + command: ${{ inputs.command }} + + - name: Set outputs + id: result + shell: bash + run: | + if [[ "${{ steps.deploy.outcome }}" == "success" ]]; then + echo "status=success" >> $GITHUB_OUTPUT + echo "url=${{ steps.deploy.outputs.deployment-url }}" >> $GITHUB_OUTPUT + else + echo "status=failed" >> $GITHUB_OUTPUT + echo "url=" >> $GITHUB_OUTPUT + fi diff --git a/.github/actions/post-deployment-table/action.yml b/.github/actions/post-deployment-table/action.yml deleted file mode 100644 index b5bf4d2..0000000 --- a/.github/actions/post-deployment-table/action.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: 'Cloudflare Post Deployment' -description: 'Posts a consolidated table of all Cloudflare deployments to a PR' - -inputs: - github-token: - description: 'GitHub token for posting comments' - required: true - deployments-path: - description: 'Path to directory containing deployment JSON files' - required: true - -runs: - using: 'composite' - steps: - - name: Generate and Post Deployment Table - uses: actions/github-script@v8 - env: - DEPLOYMENTS_PATH: ${{ inputs.deployments-path }} - with: - github-token: ${{ inputs.github-token }} - script: | - const fs = require('node:fs') - const path = require('node:path') - - const deploymentsPath = process.env.DEPLOYMENTS_PATH - const deployments = [] - - try { - const files = fs.readdirSync(deploymentsPath) - for (const file of files) { - if (file.endsWith('.json')) { - try { - const content = fs.readFileSync(path.join(deploymentsPath, file), 'utf8') - const deployment = JSON.parse(content) - deployments.push(deployment) - } catch (parseError) { - console.log(`Failed to parse ${file}:`, parseError.message) - } - } - } - } catch (error) { - console.log('No deployment files found or error reading them:', error.message) - } - - deployments.sort((a, b) => { - const appA = a.app || '' - const appB = b.app || '' - if (appA !== appB) return appA.localeCompare(appB) - const envA = a.env || '' - const envB = b.env || '' - return envA.localeCompare(envB) - }) - - const statusEmoji = { - 'success': '[OK]', - 'skipped': '[>>]', - 'failed': '[X]' - } - - const statusText = { - 'success': 'Deployed', - 'skipped': 'Skipped', - 'failed': 'Failed' - } - - let tableRows = [] - for (const deployment of deployments) { - const app = deployment.app - const env = deployment.env || '-' - const status = `${statusEmoji[deployment.status] || '[?]'} ${statusText[deployment.status] || 'Unknown'}` - - let preview = '-' - if (deployment.status === 'success') { - const url = deployment.deployment_alias_url || deployment.deployment_url - if (url) { - preview = `[View Preview](${url})` - } - } else if (deployment.status === 'skipped') { - preview = 'No changes' - } - - tableRows.push(`| ${app} | ${env} | ${status} | ${preview} |`) - } - - let commentBody = '\n' - commentBody += '## Cloudflare Deployments\n\n' - commentBody += '| App | Environment | Status | Preview |\n' - commentBody += '|-----|-------------|--------|---------|\n' - commentBody += tableRows.join('\n') + '\n\n' - commentBody += deployments.length === 0 ? '_No deployments found._' : '' - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }) - - const existingComment = comments.find(comment => - comment.body.includes('') - ) - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body: commentBody - }) - console.log('Updated existing deployment comment') - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: commentBody - }) - console.log('Created new deployment comment') - } diff --git a/.github/workflows/deploy-moderato.yml b/.github/workflows/deploy-moderato.yml deleted file mode 100644 index 7338935..0000000 --- a/.github/workflows/deploy-moderato.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Deploy (Moderato) -on: - workflow_dispatch: - push: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - NODE_ENV: production - WRANGLER_SEND_METRICS: false - -jobs: - verify: - name: Verify - uses: ./.github/workflows/verify.yml - secrets: inherit - - deploy: - name: Deploy (Moderato) - runs-on: ubuntu-latest - needs: verify - timeout-minutes: 60 - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Install dependencies - uses: ./.github/actions/install-dependencies - - - name: Build - env: - NODE_ENV: production - CLOUDFLARE_ENV: moderato - run: pnpm build - - - name: Deploy Worker - uses: cloudflare/wrangler-action@v3 - env: - NODE_ENV: production - CLOUDFLARE_ENV: moderato - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - packageManager: pnpm - command: deploy --env moderato diff --git a/.github/workflows/deploy-presto.yml b/.github/workflows/deploy-presto.yml deleted file mode 100644 index e549748..0000000 --- a/.github/workflows/deploy-presto.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Deploy (Presto) -on: - workflow_dispatch: - push: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - NODE_ENV: production - WRANGLER_SEND_METRICS: false - -jobs: - verify: - name: Verify - uses: ./.github/workflows/verify.yml - secrets: inherit - - deploy: - name: Deploy (Presto) - runs-on: ubuntu-latest - needs: verify - timeout-minutes: 60 - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Install dependencies - uses: ./.github/actions/install-dependencies - - - name: Build - env: - NODE_ENV: production - CLOUDFLARE_ENV: presto - run: pnpm build - - - name: Deploy Worker - uses: cloudflare/wrangler-action@v3 - env: - NODE_ENV: production - CLOUDFLARE_ENV: presto - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - packageManager: pnpm - command: deploy --env presto diff --git a/.github/workflows/deploy-preview-moderato.yml b/.github/workflows/deploy-preview-moderato.yml deleted file mode 100644 index 83e6ee0..0000000 --- a/.github/workflows/deploy-preview-moderato.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Deploy Preview (Moderato) -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - NODE_ENV: production - WRANGLER_SEND_METRICS: false - -jobs: - verify: - name: Verify - uses: ./.github/workflows/verify.yml - secrets: inherit - - deploy-preview: - name: Deploy Preview (Moderato) - runs-on: ubuntu-latest - needs: verify - permissions: - issues: write - contents: read - pull-requests: write - env: - NODE_ENV: production - CLOUDFLARE_ENV: moderato - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Install dependencies - uses: ./.github/actions/install-dependencies - - - name: Build - run: pnpm build - - - name: Upload Worker Version - id: deploy - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - environment: moderato - command: versions upload - - - name: Save Deployment Info - if: always() - shell: bash - run: | - mkdir -p deployment-info - - if [[ "${{ steps.deploy.outcome }}" == "success" ]]; then - STATUS="success" - DEPLOYMENT_URL="${{ steps.deploy.outputs.deployment-url }}" - ALIAS_URL="${{ steps.deploy.outputs.pages-deployment-alias-url }}" - else - STATUS="failed" - DEPLOYMENT_URL="" - ALIAS_URL="" - fi - - cat > "deployment-info/app-moderato.json" << EOF - { - "app": "app", - "env": "moderato", - "status": "$STATUS", - "deployment_url": "$DEPLOYMENT_URL", - "deployment_alias_url": "$ALIAS_URL" - } - EOF - - cat "deployment-info/app-moderato.json" - - - name: Upload Deployment Info - if: always() - uses: actions/upload-artifact@v6 - with: - name: deployment-app-moderato - path: deployment-info/app-moderato.json - retention-days: 1 - - comment-deployments: - name: Post Deployment Table - runs-on: ubuntu-latest - needs: deploy-preview - if: always() - permissions: - pull-requests: write - contents: read - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download All Deployment Artifacts - uses: actions/download-artifact@v7 - with: - pattern: deployment-* - path: deployments - merge-multiple: true - - - name: Post Deployment Table - uses: ./.github/actions/post-deployment-table - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - deployments-path: deployments diff --git a/.github/workflows/deploy-preview-presto.yml b/.github/workflows/deploy-preview-presto.yml deleted file mode 100644 index b59a55e..0000000 --- a/.github/workflows/deploy-preview-presto.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Deploy Preview (Presto) -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - NODE_ENV: production - WRANGLER_SEND_METRICS: false - -jobs: - verify: - name: Verify - uses: ./.github/workflows/verify.yml - secrets: inherit - - deploy-preview: - name: Deploy Preview (Presto) - runs-on: ubuntu-latest - needs: verify - permissions: - issues: write - contents: read - pull-requests: write - env: - NODE_ENV: production - CLOUDFLARE_ENV: presto - steps: - - name: Clone repository - uses: actions/checkout@v6 - - - name: Install dependencies - uses: ./.github/actions/install-dependencies - - - name: Build - run: pnpm build - - - name: Upload Worker Version - id: deploy - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - environment: presto - command: versions upload - - - name: Save Deployment Info - if: always() - shell: bash - run: | - mkdir -p deployment-info - - if [[ "${{ steps.deploy.outcome }}" == "success" ]]; then - STATUS="success" - DEPLOYMENT_URL="${{ steps.deploy.outputs.deployment-url }}" - ALIAS_URL="${{ steps.deploy.outputs.pages-deployment-alias-url }}" - else - STATUS="failed" - DEPLOYMENT_URL="" - ALIAS_URL="" - fi - - cat > "deployment-info/app-presto.json" << EOF - { - "app": "app", - "env": "presto", - "status": "$STATUS", - "deployment_url": "$DEPLOYMENT_URL", - "deployment_alias_url": "$ALIAS_URL" - } - EOF - - cat "deployment-info/app-presto.json" - - - name: Upload Deployment Info - if: always() - uses: actions/upload-artifact@v6 - with: - name: deployment-app-presto - path: deployment-info/app-presto.json - retention-days: 1 - - comment-deployments: - name: Post Deployment Table - runs-on: ubuntu-latest - needs: deploy-preview - if: always() - permissions: - pull-requests: write - contents: read - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Download All Deployment Artifacts - uses: actions/download-artifact@v7 - with: - pattern: deployment-* - path: deployments - merge-multiple: true - - - name: Post Deployment Table - uses: ./.github/actions/post-deployment-table - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - deployments-path: deployments diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml new file mode 100644 index 0000000..26f6c21 --- /dev/null +++ b/.github/workflows/deploy-preview.yml @@ -0,0 +1,117 @@ +name: Deploy Preview +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + workflow_dispatch: + +concurrency: + group: deploy-preview-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + NODE_ENV: production + WRANGLER_SEND_METRICS: false + +jobs: + verify: + name: Checks + uses: ./.github/workflows/verify.yml + secrets: inherit + + deploy: + name: Deploy ${{ matrix.env }} + runs-on: ubuntu-latest + needs: verify + permissions: + contents: read + strategy: + matrix: + env: [moderato, presto] + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/install-dependencies + - name: Deploy + id: deploy + uses: ./.github/actions/deploy + with: + environment: ${{ matrix.env }} + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + - name: Save result + if: always() + run: | + mkdir -p results + echo '{"env":"${{ matrix.env }}","status":"${{ steps.deploy.outputs.status }}","url":"${{ steps.deploy.outputs.url }}"}' > results/${{ matrix.env }}.json + - uses: actions/upload-artifact@v4 + if: always() + with: + name: deploy-${{ matrix.env }} + path: results/ + + post-table: + name: Post Table + runs-on: ubuntu-latest + needs: deploy + if: always() && needs.verify.result == 'success' + permissions: + pull-requests: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: deploy-* + merge-multiple: true + - name: Post table + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs') + const sha = '${{ github.event.pull_request.head.sha }}'.slice(0, 7) + const time = new Date().toISOString() + const formatted = new Date(time).toLocaleString('en-US', { + month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZone: 'UTC' + }) + ' UTC' + + const deployments = fs.readdirSync('.').filter(f => f.endsWith('.json')).map(f => JSON.parse(fs.readFileSync(f))) + deployments.sort((a, b) => a.env.localeCompare(b.env)) + + const rows = deployments.map(d => { + const status = d.status === 'success' ? '✓ Ready' : '✗ Failed' + const preview = d.status === 'success' && d.url ? '[Open](' + d.url + ')' : '-' + return `| ${d.env} | ${status} | ${preview} |` + }) + + const commitUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${{ github.event.pull_request.head.sha }}` + const body = [ + '', + '### Preview', + '', + '[' + sha + '](' + commitUrl + ') · ' + formatted, + '', + '| Environment | Status | Preview |', + '|-------------|--------|---------|', + rows.join('\n'), + ].join('\n') + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + + const existing = comments.find(c => c.body.includes('')) + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }) + } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..852c127 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,38 @@ +name: Deploy +on: + workflow_dispatch: + push: + branches: [main] + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_ENV: production + WRANGLER_SEND_METRICS: false + +jobs: + verify: + name: Checks + uses: ./.github/workflows/verify.yml + secrets: inherit + + deploy: + name: Deploy ${{ matrix.env }} + runs-on: ubuntu-latest + needs: verify + timeout-minutes: 60 + strategy: + matrix: + env: [moderato, presto] + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/install-dependencies + - name: Deploy + uses: ./.github/actions/deploy + with: + environment: ${{ matrix.env }} + command: deploy + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 262527b..0572b92 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,6 +1,5 @@ name: Verify on: - pull_request: workflow_call: workflow_dispatch: @@ -28,6 +27,3 @@ jobs: - name: Check types run: pnpm gen:types && pnpm check:types - - - name: Build - run: pnpm build