fix: replace unsupported lucide brand icons #75
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 Web | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths: | |
| - "apps/web/**" | |
| - "packages/**" | |
| - "bun.lock" | |
| - "package.json" | |
| - ".github/workflows/deploy-web.yaml" | |
| pull_request: | |
| types: [opened, reopened, synchronize, closed] | |
| paths: | |
| - "apps/web/**" | |
| - "packages/**" | |
| - "bun.lock" | |
| - "package.json" | |
| - ".github/workflows/deploy-web.yaml" | |
| workflow_dispatch: | |
| inputs: | |
| stage: | |
| description: "Stage to deploy (e.g. production, staging, pr-42)" | |
| required: true | |
| type: string | |
| concurrency: | |
| group: deploy-web-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| NEON_API_KEY: ${{ secrets.NEON_API_KEY }} | |
| jobs: | |
| stage: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| stage: ${{ steps.resolve.outputs.stage }} | |
| action: ${{ steps.resolve.outputs.action }} | |
| steps: | |
| - id: resolve | |
| run: | | |
| set -euo pipefail | |
| event="${{ github.event_name }}" | |
| if [[ "$event" == "workflow_dispatch" ]]; then | |
| stage="${{ inputs.stage }}" | |
| action="deploy" | |
| elif [[ "$event" == "push" ]]; then | |
| case "${{ github.ref_name }}" in | |
| main) stage="production" ;; | |
| develop) stage="staging" ;; | |
| *) echo "Unexpected ref ${{ github.ref_name }}" >&2; exit 1 ;; | |
| esac | |
| action="deploy" | |
| elif [[ "$event" == "pull_request" ]]; then | |
| stage="pr-${{ github.event.pull_request.number }}" | |
| if [[ "${{ github.event.action }}" == "closed" ]]; then | |
| action="destroy" | |
| else | |
| action="deploy" | |
| fi | |
| fi | |
| echo "stage=$stage" >> "$GITHUB_OUTPUT" | |
| echo "action=$action" >> "$GITHUB_OUTPUT" | |
| echo "Resolved: $action stage=$stage" | |
| deploy: | |
| needs: stage | |
| if: needs.stage.outputs.action == 'deploy' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: ${{ needs.stage.outputs.stage }} | |
| url: ${{ steps.deploy.outputs.url }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| # Persist alchemy's LocalState (.alchemy/state/<stack>/<stage>/) across | |
| # runners so the destroy job on PR close can find the resources to tear down. | |
| # alchemy currently ships only a filesystem state backend; without this | |
| # cache, every runner starts blind and orphans Workers + Neon projects. | |
| - name: Restore alchemy state | |
| uses: actions/cache@v4 | |
| with: | |
| path: apps/web/.alchemy | |
| key: alchemy-state-web-${{ needs.stage.outputs.stage }} | |
| # Invalidate Cloudflare.Vite's content-hash memo on every push. | |
| # | |
| # `Cloudflare.Vite` hashes the Worker's working directory (`apps/web/`) | |
| # to decide whether a rebuild + upload is needed. That scope does *not* | |
| # include workspace dependencies under `packages/**` (`@stackpanel/api`, | |
| # `@stackpanel/auth`, `@stackpanel/db`, …), so a push-to-`main` that | |
| # only touches those packages hashes identically to the last deploy and | |
| # alchemy short-circuits with "no change" — silently skipping the Vite | |
| # build and the Worker upload. The apex then stays frozen on whichever | |
| # commit last happened to touch `apps/web/`. | |
| # | |
| # Stamping a commit SHA (+ refs) file inside `apps/web/` forces a fresh | |
| # memo hash on every run, so production tracks `main` even for diffs | |
| # that live entirely in `packages/**`. The file must NOT be gitignored | |
| # (the default memo `exclude` is the merged gitignore rules, which | |
| # would otherwise filter it out). It's written only in CI — runners | |
| # are ephemeral, so there's nothing to clean up — and contains no | |
| # secrets. | |
| - name: Stamp build info for memo invalidation | |
| working-directory: apps/web | |
| run: | | |
| set -euo pipefail | |
| cat > .build-info <<EOF | |
| sha=${{ github.sha }} | |
| ref=${{ github.ref }} | |
| event=${{ github.event_name }} | |
| run_id=${{ github.run_id }} | |
| EOF | |
| echo "--- .build-info ---" | |
| cat .build-info | |
| - name: Deploy | |
| id: deploy | |
| working-directory: apps/web | |
| env: | |
| STAGE: ${{ needs.stage.outputs.stage }} | |
| # SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the | |
| # generated per-app payloads. `production` uses the prod key; | |
| # everything else (staging, pr-*, dev) uses the dev key. | |
| # All stages encrypt with the github_actions age recipient | |
| # (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split | |
| # was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod | |
| # payloads, so production deploys failed at decrypt time. | |
| SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} | |
| run: | | |
| set -euo pipefail | |
| bunx alchemy deploy --stage ${{ needs.stage.outputs.stage }} --yes | |
| - name: Comment preview URL on PR | |
| if: github.event_name == 'pull_request' | |
| uses: marocchino/sticky-pull-request-comment@v2 | |
| with: | |
| header: preview-web | |
| message: | | |
| Preview deployed to `${{ needs.stage.outputs.stage }}` — https://${{ needs.stage.outputs.stage }}.stackpanel.com | |
| destroy: | |
| needs: stage | |
| if: needs.stage.outputs.action == 'destroy' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write # gh cache delete | |
| contents: read | |
| pull-requests: write # sticky comment | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| # Restore-only — we never want the destroy job's post-step to write the | |
| # (now-empty) state back to the cache. | |
| - name: Restore alchemy state | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: apps/web/.alchemy | |
| key: alchemy-state-web-${{ needs.stage.outputs.stage }} | |
| - name: Destroy PR preview | |
| working-directory: apps/web | |
| env: | |
| STAGE: ${{ needs.stage.outputs.stage }} | |
| # PR previews always use the dev key. | |
| SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} | |
| run: | | |
| set -euo pipefail | |
| bunx alchemy destroy --stage ${{ needs.stage.outputs.stage }} --yes | |
| - name: Delete cached alchemy state | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh cache delete "alchemy-state-web-${{ needs.stage.outputs.stage }}" \ | |
| --repo "${{ github.repository }}" || true | |
| - name: Mark preview comment as torn down | |
| uses: marocchino/sticky-pull-request-comment@v2 | |
| with: | |
| header: preview-web | |
| message: | | |
| Preview `${{ needs.stage.outputs.stage }}` has been destroyed. |