Deploy #122
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy | |
| on: | |
| workflow_run: | |
| workflows: ["Build"] | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| inputs: | |
| artifact_url: | |
| description: 'Cloudflare build artifact URL (.tar.gz, e.g., build-v1.0.0-cloudflare.tar.gz)' | |
| required: true | |
| type: string | |
| environment: | |
| description: 'Deployment environment' | |
| required: true | |
| default: 'production' | |
| type: choice | |
| options: | |
| - production | |
| - preview | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| actions: read | |
| jobs: | |
| prepare: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} | |
| outputs: | |
| is_production: ${{ steps.meta.outputs.is_production }} | |
| pr_number: ${{ steps.meta.outputs.pr_number }} | |
| steps: | |
| - name: Download build artifacts from workflow | |
| if: ${{ github.event_name == 'workflow_run' }} | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ github.event.workflow_run.id }} | |
| path: ./artifacts | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download artifacts from URL | |
| if: ${{ github.event_name == 'workflow_dispatch' }} | |
| run: | | |
| URL="${{ github.event.inputs.artifact_url }}" | |
| echo "Downloading from: $URL" | |
| mkdir -p ./artifacts | |
| if [[ "$URL" == *.zip ]]; then | |
| curl -L "$URL" -o ./artifacts/build.zip | |
| unzip -q ./artifacts/build.zip -d ./artifacts | |
| rm ./artifacts/build.zip | |
| else | |
| curl -L "$URL" -o ./artifacts/build.tar.gz | |
| tar -xzf ./artifacts/build.tar.gz -C ./artifacts | |
| rm ./artifacts/build.tar.gz | |
| fi | |
| - name: Set deployment type | |
| id: meta | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # Manual deployment | |
| if [ "${{ github.event.inputs.environment }}" == "production" ]; then | |
| echo "is_production=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_production=false" >> $GITHUB_OUTPUT | |
| fi | |
| echo "pr_number=0" >> $GITHUB_OUTPUT | |
| else | |
| # Auto deployment from workflow_run - check ref | |
| REF=$(cat ./artifacts/build-meta/ref 2>/dev/null || echo "unknown") | |
| if [[ "$REF" == "refs/heads/main" || "$REF" == "refs/heads/master" ]]; then | |
| echo "is_production=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_production=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Read PR number from build-meta | |
| PR_NUMBER=$(cat ./artifacts/build-meta/pr_number 2>/dev/null || echo "0") | |
| echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Upload artifacts for deploy job | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: deploy-artifacts-${{ github.run_id }} | |
| path: ./artifacts | |
| retention-days: 1 | |
| deploy: | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: ${{ needs.prepare.outputs.is_production == 'true' && 'production' || 'preview' }} | |
| url: ${{ steps.deploy.outputs.url }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: deploy-artifacts-${{ github.run_id }} | |
| path: ./artifacts | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: 1.3.6 | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Restore build artifacts | |
| run: | | |
| cp -r ./artifacts/dist ./dist | |
| if [ -d ./artifacts/server/sql ]; then | |
| mkdir -p ./server | |
| rm -rf ./server/sql | |
| cp -r ./artifacts/server/sql ./server/sql | |
| fi | |
| echo "Checking build artifacts..." | |
| ls -la ./dist/client/ || echo "No client dist found" | |
| ls -la ./dist/server/ || echo "No server dist found" | |
| ls -la ./server/sql/ || echo "No server sql found" | |
| - name: Validate deploy script version | |
| run: | | |
| CURRENT_HASH=$(cat ./cli/src/tasks/deploy-cf.ts ./cli/src/lib/db-migration.ts | sha256sum | awk '{print $1}') | |
| ARTIFACT_HASH=$(cat ./artifacts/build-meta/deploy_script_hash 2>/dev/null || echo "") | |
| if [ -z "$ARTIFACT_HASH" ]; then | |
| echo "❌ Build artifact is missing deploy_script_hash metadata." | |
| echo "Please rebuild the artifact from a newer Rin release and sync your repository before deploying." | |
| exit 1 | |
| fi | |
| echo "Current deploy script hash: $CURRENT_HASH" | |
| echo "Artifact deploy script hash: $ARTIFACT_HASH" | |
| if [ "$CURRENT_HASH" != "$ARTIFACT_HASH" ]; then | |
| echo "❌ Deploy script hash mismatch." | |
| echo "This repository's deploy logic does not match the selected artifact." | |
| echo "Please sync your repository to the release that produced the artifact, then retry deployment." | |
| exit 1 | |
| fi | |
| - name: Determine deployment config | |
| id: config | |
| env: | |
| # Repository-level variables (available before environment is set) | |
| REPO_NAME: ${{ vars.NAME }} | |
| REPO_DESCRIPTION: ${{ vars.DESCRIPTION }} | |
| REPO_AVATAR: ${{ vars.AVATAR }} | |
| REPO_PAGE_SIZE: ${{ vars.PAGE_SIZE }} | |
| REPO_RSS_ENABLE: ${{ vars.RSS_ENABLE }} | |
| REPO_WORKER_NAME: ${{ vars.WORKER_NAME }} | |
| REPO_DB_NAME: ${{ vars.DB_NAME }} | |
| run: | | |
| PR_NUMBER="${{ needs.prepare.outputs.pr_number }}" | |
| if [[ "${{ needs.prepare.outputs.is_production }}" == "true" ]]; then | |
| echo "worker_name=${REPO_WORKER_NAME:-rin-server}" >> $GITHUB_OUTPUT | |
| echo "db_name=${REPO_DB_NAME:-rin}" >> $GITHUB_OUTPUT | |
| echo "name=${REPO_NAME:-Rin}" >> $GITHUB_OUTPUT | |
| elif [[ "$PR_NUMBER" != "0" ]]; then | |
| # PR Preview uses -pr-<number> suffix | |
| echo "worker_name=${REPO_WORKER_NAME:-rin-server}-pr-${PR_NUMBER}" >> $GITHUB_OUTPUT | |
| echo "db_name=${REPO_DB_NAME:-rin-preview}" >> $GITHUB_OUTPUT | |
| echo "name=${REPO_NAME:-Rin} (PR #${PR_NUMBER})" >> $GITHUB_OUTPUT | |
| else | |
| echo "worker_name=${REPO_WORKER_NAME:-rin-server}-preview" >> $GITHUB_OUTPUT | |
| echo "db_name=${REPO_DB_NAME:-rin-preview}" >> $GITHUB_OUTPUT | |
| echo "name=${REPO_NAME:-Rin} (Preview)" >> $GITHUB_OUTPUT | |
| fi | |
| # Output repository-level vars for use in deploy step | |
| echo "description=${REPO_DESCRIPTION}" >> $GITHUB_OUTPUT | |
| echo "avatar=${REPO_AVATAR}" >> $GITHUB_OUTPUT | |
| echo "page_size=${REPO_PAGE_SIZE}" >> $GITHUB_OUTPUT | |
| echo "rss_enable=${REPO_RSS_ENABLE}" >> $GITHUB_OUTPUT | |
| - name: Deploy to Cloudflare | |
| id: deploy | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| WORKER_NAME: ${{ steps.config.outputs.worker_name }} | |
| DB_NAME: ${{ steps.config.outputs.db_name }} | |
| NAME: ${{ steps.config.outputs.name }} | |
| DESCRIPTION: ${{ steps.config.outputs.description || 'A lightweight personal blogging system' }} | |
| AVATAR: ${{ steps.config.outputs.avatar || '' }} | |
| PAGE_SIZE: ${{ steps.config.outputs.page_size || '5' }} | |
| RSS_ENABLE: ${{ steps.config.outputs.rss_enable || 'false' }} | |
| S3_CACHE_FOLDER: ${{ vars.S3_CACHE_FOLDER || 'cache/' }} | |
| S3_FOLDER: ${{ vars.S3_FOLDER || 'images/' }} | |
| S3_REGION: ${{ vars.S3_REGION || 'auto' }} | |
| S3_FORCE_PATH_STYLE: ${{ vars.S3_FORCE_PATH_STYLE || 'false' }} | |
| R2_BUCKET_NAME: ${{ vars.R2_BUCKET_NAME || '' }} | |
| S3_ENDPOINT: ${{ vars.S3_ENDPOINT || '' }} | |
| S3_ACCESS_HOST: ${{ vars.S3_ACCESS_HOST || '' }} | |
| S3_BUCKET: ${{ vars.S3_BUCKET || '' }} | |
| S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID || '' }} | |
| S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY || '' }} | |
| RIN_GITHUB_CLIENT_ID: ${{ secrets.RIN_GITHUB_CLIENT_ID || '' }} | |
| RIN_GITHUB_CLIENT_SECRET: ${{ secrets.RIN_GITHUB_CLIENT_SECRET || '' }} | |
| ADMIN_USERNAME: ${{ secrets.ADMIN_USERNAME }} | |
| ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} | |
| JWT_SECRET: ${{ secrets.JWT_SECRET || '' }} | |
| WEBHOOK_URL: ${{ vars.WEBHOOK_URL || '' }} | |
| RSS_TITLE: ${{ vars.RSS_TITLE || '' }} | |
| RSS_DESCRIPTION: ${{ vars.RSS_DESCRIPTION || '' }} | |
| CACHE_STORAGE_MODE: ${{ vars.CACHE_STORAGE_MODE || 's3' }} | |
| run: | | |
| set -euo pipefail | |
| bun run deploy | tee deploy_output.log | |
| URL=$(grep -oP 'App URL: \Khttps://[^\s]+' deploy_output.log || echo "https://${WORKER_NAME}.${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.workers.dev") | |
| echo "url=${URL}" >> $GITHUB_OUTPUT | |
| - name: Comment on PR | |
| if: needs.prepare.outputs.pr_number != '0' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const url = '${{ steps.deploy.outputs.url }}'; | |
| const prNumber = parseInt('${{ needs.prepare.outputs.pr_number }}'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('🚀 **Preview deployed!**') | |
| ); | |
| const body = `🚀 **Preview deployed!**\n\n🔗 ${url}`; | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } |