From 7db7297654f551488f5847fe8f988ea20582e98a Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 15:33:51 +0530 Subject: [PATCH 1/5] ci: add PR preview, staging and production GitHub Pages deploy workflows --- .github/workflows/deploy-preview.yml | 97 ++++++++++++++++++++++++++++ .github/workflows/deploy-prod.yml | 43 ++++++++++++ .github/workflows/deploy-staging.yml | 51 +++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 .github/workflows/deploy-preview.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/deploy-staging.yml diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml new file mode 100644 index 0000000..e8c6b65 --- /dev/null +++ b/.github/workflows/deploy-preview.yml @@ -0,0 +1,97 @@ +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +permissions: + contents: write + pull-requests: write + +concurrency: + group: pr-preview-${{ github.event.number }} + cancel-in-progress: true + +jobs: + deploy: + if: github.event.action != 'closed' + name: Deploy preview + runs-on: ubuntu-latest + environment: + name: pr-preview-${{ github.event.number }} + url: https://getbms.github.io/bms/pr-${{ github.event.number }}/ + + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.2' + channel: stable + cache: true + + - run: flutter pub get + + - run: dart run build_runner build --delete-conflicting-outputs + + - run: flutter build web --release --base-href /bms/pr-${{ github.event.number }}/ + + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/web + destination_dir: pr-${{ github.event.number }} + keep_files: true + + - name: Post preview URL comment + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ github.event.number }}; + const sha = '${{ github.sha }}'.slice(0, 7); + const url = `https://getbms.github.io/bms/pr-${prNumber}/`; + const marker = ''; + const body = `${marker}\n### Preview deployed\n\n| | |\n|---|---|\n| **URL** | ${url} |\n| **Commit** | \`${sha}\` |\n| **Status** | Live |\n\nUpdates automatically on every push.`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existing = comments.find(c => c.body.includes(marker)); + 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: prNumber, + body, + }); + } + + cleanup: + if: github.event.action == 'closed' + name: Remove preview + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: Delete PR preview directory + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if [ -d "pr-${{ github.event.number }}" ]; then + git rm -rf pr-${{ github.event.number }} + git commit -m "cleanup: remove PR ${{ github.event.number }} preview" + git push + fi diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..93dc4a2 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,43 @@ +name: Deploy to Production + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: deploy-prod + cancel-in-progress: true + +jobs: + deploy: + name: Deploy production + runs-on: ubuntu-latest + environment: + name: production + url: https://getbms.github.io/bms/ + + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.2' + channel: stable + cache: true + + - run: flutter pub get + + - run: dart run build_runner build --delete-conflicting-outputs + + - run: flutter build web --release --base-href /bms/ + + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/web + keep_files: true + commit_message: 'deploy: production @ ${{ github.sha }}' diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..9d21f0b --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,51 @@ +name: Deploy to Staging + +on: + workflow_dispatch: + inputs: + ref: + description: 'Branch or tag to deploy (default: master)' + required: false + default: 'master' + +permissions: + contents: write + +concurrency: + group: deploy-staging + cancel-in-progress: true + +jobs: + deploy: + name: Deploy staging + runs-on: ubuntu-latest + # Configure this environment in GitHub Settings > Environments > staging + # to add required reviewers for manual approval before each deploy. + environment: + name: staging + url: https://getbms.github.io/bms/staging/ + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.2' + channel: stable + cache: true + + - run: flutter pub get + + - run: dart run build_runner build --delete-conflicting-outputs + + - run: flutter build web --release --base-href /bms/staging/ + + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/web + destination_dir: staging + keep_files: true + commit_message: 'deploy: staging from ${{ inputs.ref }} @ ${{ github.sha }}' From 706593cd35bad9942b56ee51c4fda5d59787ca9c Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 15:35:53 +0530 Subject: [PATCH 2/5] ci: remove auto production deploy - not needed yet --- .github/workflows/deploy-prod.yml | 43 ------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 .github/workflows/deploy-prod.yml diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml deleted file mode 100644 index 93dc4a2..0000000 --- a/.github/workflows/deploy-prod.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Deploy to Production - -on: - push: - branches: [master] - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: deploy-prod - cancel-in-progress: true - -jobs: - deploy: - name: Deploy production - runs-on: ubuntu-latest - environment: - name: production - url: https://getbms.github.io/bms/ - - steps: - - uses: actions/checkout@v4 - - - uses: subosito/flutter-action@v2 - with: - flutter-version: '3.44.2' - channel: stable - cache: true - - - run: flutter pub get - - - run: dart run build_runner build --delete-conflicting-outputs - - - run: flutter build web --release --base-href /bms/ - - - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build/web - keep_files: true - commit_message: 'deploy: production @ ${{ github.sha }}' From d003076fe9c3fb16f0b7384a1a565912619665c2 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 15:36:48 +0530 Subject: [PATCH 3/5] ci: restore production deploy workflow --- .github/workflows/deploy-prod.yml | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/deploy-prod.yml diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..93dc4a2 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,43 @@ +name: Deploy to Production + +on: + push: + branches: [master] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: deploy-prod + cancel-in-progress: true + +jobs: + deploy: + name: Deploy production + runs-on: ubuntu-latest + environment: + name: production + url: https://getbms.github.io/bms/ + + steps: + - uses: actions/checkout@v4 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.44.2' + channel: stable + cache: true + + - run: flutter pub get + + - run: dart run build_runner build --delete-conflicting-outputs + + - run: flutter build web --release --base-href /bms/ + + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/web + keep_files: true + commit_message: 'deploy: production @ ${{ github.sha }}' From ebd0d28d3d04d3ff53827132025c767feb1de4e9 Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 15:38:55 +0530 Subject: [PATCH 4/5] ci: pipeline - PR merge triggers staging, staging success triggers production --- .github/workflows/deploy-prod.yml | 15 +++++++++++++-- .github/workflows/deploy-staging.yml | 10 ++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 93dc4a2..b64faca 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,8 +1,12 @@ name: Deploy to Production on: - push: + # Auto-triggers only when staging completes successfully + workflow_run: + workflows: ["Deploy to Staging"] + types: [completed] branches: [master] + # Keep manual trigger as fallback workflow_dispatch: permissions: @@ -16,12 +20,19 @@ jobs: deploy: name: Deploy production runs-on: ubuntu-latest + # Skip if triggered by workflow_run and staging did not succeed + if: > + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' environment: name: production url: https://getbms.github.io/bms/ steps: - uses: actions/checkout@v4 + with: + # When triggered by workflow_run, deploy the exact commit staging used + ref: ${{ github.event.workflow_run.head_sha || github.sha }} - uses: subosito/flutter-action@v2 with: @@ -40,4 +51,4 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build/web keep_files: true - commit_message: 'deploy: production @ ${{ github.sha }}' + commit_message: 'deploy: production @ ${{ github.event.workflow_run.head_sha || github.sha }}' diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 9d21f0b..c2b8edf 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -1,6 +1,10 @@ name: Deploy to Staging on: + # Auto-triggers when a PR is merged to master + push: + branches: [master] + # Keep manual trigger as fallback for re-runs or hotfixes workflow_dispatch: inputs: ref: @@ -19,8 +23,6 @@ jobs: deploy: name: Deploy staging runs-on: ubuntu-latest - # Configure this environment in GitHub Settings > Environments > staging - # to add required reviewers for manual approval before each deploy. environment: name: staging url: https://getbms.github.io/bms/staging/ @@ -28,7 +30,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ inputs.ref }} + ref: ${{ inputs.ref || github.sha }} - uses: subosito/flutter-action@v2 with: @@ -48,4 +50,4 @@ jobs: publish_dir: ./build/web destination_dir: staging keep_files: true - commit_message: 'deploy: staging from ${{ inputs.ref }} @ ${{ github.sha }}' + commit_message: 'deploy: staging @ ${{ github.sha }}' From aef1ac23185b276d57375e256d8025ec908644ae Mon Sep 17 00:00:00 2001 From: iamvirul Date: Wed, 17 Jun 2026 15:58:01 +0530 Subject: [PATCH 5/5] fix: harden deploy workflows against supply chain and credential risks - Pin all action references to immutable commit SHAs - Add persist-credentials: false to all checkout steps - Fix staging commit_message to use actual HEAD SHA after checkout - Move deploy-preview permissions to job level (least privilege) --- .github/workflows/deploy-preview.yml | 21 ++++++++++++--------- .github/workflows/deploy-prod.yml | 7 ++++--- .github/workflows/deploy-staging.yml | 13 +++++++++---- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index e8c6b65..8846ffc 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -4,10 +4,6 @@ on: pull_request: types: [opened, synchronize, reopened, closed] -permissions: - contents: write - pull-requests: write - concurrency: group: pr-preview-${{ github.event.number }} cancel-in-progress: true @@ -17,14 +13,19 @@ jobs: if: github.event.action != 'closed' name: Deploy preview runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write environment: name: pr-preview-${{ github.event.number }} url: https://getbms.github.io/bms/pr-${{ github.event.number }}/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4.2.2 + with: + persist-credentials: false - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # subosito/flutter-action@v2 with: flutter-version: '3.44.2' channel: stable @@ -36,7 +37,7 @@ jobs: - run: flutter build web --release --base-href /bms/pr-${{ github.event.number }}/ - - uses: peaceiris/actions-gh-pages@v4 + - uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build/web @@ -44,7 +45,7 @@ jobs: keep_files: true - name: Post preview URL comment - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # actions/github-script@v7 with: script: | const prNumber = ${{ github.event.number }}; @@ -80,9 +81,11 @@ jobs: if: github.event.action == 'closed' name: Remove preview runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4.2.2 with: ref: gh-pages diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index b64faca..32a5648 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -29,12 +29,13 @@ jobs: url: https://getbms.github.io/bms/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4.2.2 with: # When triggered by workflow_run, deploy the exact commit staging used ref: ${{ github.event.workflow_run.head_sha || github.sha }} + persist-credentials: false - - uses: subosito/flutter-action@v2 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # subosito/flutter-action@v2 with: flutter-version: '3.44.2' channel: stable @@ -46,7 +47,7 @@ jobs: - run: flutter build web --release --base-href /bms/ - - uses: peaceiris/actions-gh-pages@v4 + - uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build/web diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index c2b8edf..9311b4e 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -28,11 +28,16 @@ jobs: url: https://getbms.github.io/bms/staging/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4.2.2 with: ref: ${{ inputs.ref || github.sha }} + persist-credentials: false - - uses: subosito/flutter-action@v2 + - name: Capture deployed SHA + id: head + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # subosito/flutter-action@v2 with: flutter-version: '3.44.2' channel: stable @@ -44,10 +49,10 @@ jobs: - run: flutter build web --release --base-href /bms/staging/ - - uses: peaceiris/actions-gh-pages@v4 + - uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./build/web destination_dir: staging keep_files: true - commit_message: 'deploy: staging @ ${{ github.sha }}' + commit_message: 'deploy: staging @ ${{ steps.head.outputs.sha }}'