fix(embed): react duplicate key issue (#64) #64
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 app | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - canary | |
| paths: | |
| - fluxer_app/** | |
| - .github/workflows/deploy-app.yaml | |
| workflow_dispatch: | |
| inputs: | |
| channel: | |
| type: choice | |
| options: | |
| - stable | |
| - canary | |
| default: stable | |
| description: Channel to deploy | |
| ref: | |
| type: string | |
| required: false | |
| default: '' | |
| description: Optional git ref to deploy (defaults to main/canary based on channel) | |
| concurrency: | |
| group: deploy-fluxer-app-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| jobs: | |
| channel-vars: | |
| uses: ./.github/workflows/channel-vars.yaml | |
| with: | |
| github_event_name: ${{ github.event_name }} | |
| github_ref_name: ${{ github.ref_name }} | |
| github_ref: ${{ github.ref }} | |
| workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }} | |
| workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} | |
| deploy: | |
| name: Deploy app | |
| needs: channel-vars | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| env: | |
| CHANNEL: ${{ needs.channel-vars.outputs.channel }} | |
| IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }} | |
| SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }} | |
| STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }} | |
| SERVICE_NAME: ${{ format('fluxer-app{0}', needs.channel-vars.outputs.stack_suffix) }} | |
| DOCKERFILE: fluxer_app/proxy/Dockerfile | |
| SENTRY_PROXY_PATH: /error-reporting-proxy | |
| CACHE_SCOPE: ${{ format('fluxer-app{0}', needs.channel-vars.outputs.stack_suffix) }} | |
| PUBLIC_BOOTSTRAP_API_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app/api' || 'https://web.fluxer.app/api' }} | |
| PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }} | |
| PUBLIC_PROJECT_ENV: ${{ needs.channel-vars.outputs.channel }} | |
| PUBLIC_SENTRY_DSN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.canary.fluxer.app/4510205815291904' || 'https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.fluxer.app/4510205815291904' }} | |
| SENTRY_REPORT_HOST: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://sentry.web.canary.fluxer.app' || 'https://sentry.web.fluxer.app' }} | |
| API_TARGET: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'fluxer-api-canary_app' || 'fluxer-api_app' }} | |
| CADDY_APP_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'web.canary.fluxer.app' || 'web.fluxer.app' }} | |
| SENTRY_CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'sentry.web.canary.fluxer.app' || 'sentry.web.fluxer.app' }} | |
| RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }} | |
| APP_REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ env.SOURCE_REF }} | |
| fetch-depth: 0 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.26.0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: fluxer_app/pnpm-lock.yaml | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.25.5' | |
| - name: Install dependencies | |
| working-directory: fluxer_app | |
| run: pnpm install --frozen-lockfile | |
| - name: Run Lingui i18n tasks | |
| working-directory: fluxer_app | |
| run: pnpm lingui:extract && pnpm lingui:compile --strict | |
| - name: Record deploy commit | |
| run: | | |
| set -euo pipefail | |
| sha=$(git rev-parse HEAD) | |
| echo "Deploying commit ${sha}" | |
| printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV" | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Cache Rust dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/bin/ | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| fluxer_app/crates/gif_wasm/target/ | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('fluxer_app/crates/gif_wasm/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo- | |
| - name: Install wasm-pack | |
| run: | | |
| set -euo pipefail | |
| if ! command -v wasm-pack >/dev/null 2>&1; then | |
| cargo install wasm-pack --version 0.13.1 | |
| fi | |
| - name: Generate wasm artifacts | |
| working-directory: fluxer_app | |
| run: pnpm wasm:codegen | |
| - name: Build application | |
| working-directory: fluxer_app | |
| env: | |
| NODE_ENV: production | |
| PUBLIC_BOOTSTRAP_API_ENDPOINT: ${{ env.PUBLIC_BOOTSTRAP_API_ENDPOINT }} | |
| PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${{ env.PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT }} | |
| PUBLIC_API_VERSION: 1 | |
| PUBLIC_PROJECT_ENV: ${{ env.PUBLIC_PROJECT_ENV }} | |
| PUBLIC_SENTRY_PROJECT_ID: 4510205815291904 | |
| PUBLIC_SENTRY_PUBLIC_KEY: 59ced0e2666ab83dd1ddb056cdd22d1b | |
| PUBLIC_SENTRY_DSN: ${{ env.PUBLIC_SENTRY_DSN }} | |
| PUBLIC_SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }} | |
| PUBLIC_BUILD_NUMBER: ${{ github.run_number }} | |
| run: | | |
| set -euo pipefail | |
| export PUBLIC_BUILD_SHA=$(git rev-parse --short HEAD) | |
| export PUBLIC_BUILD_TIMESTAMP=$(date +%s) | |
| pnpm build | |
| cat > dist/version.json << EOF | |
| { | |
| "sha": "$PUBLIC_BUILD_SHA", | |
| "buildNumber": $PUBLIC_BUILD_NUMBER, | |
| "timestamp": $PUBLIC_BUILD_TIMESTAMP, | |
| "env": "$PUBLIC_PROJECT_ENV" | |
| } | |
| EOF | |
| - name: Install rclone | |
| run: | | |
| set -euo pipefail | |
| if ! command -v rclone >/dev/null 2>&1; then | |
| curl -fsSL https://rclone.org/install.sh | sudo bash | |
| fi | |
| - name: Upload assets to S3 static bucket | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ~/.config/rclone | |
| cat > ~/.config/rclone/rclone.conf << RCLONEEOF | |
| [ovh] | |
| type = s3 | |
| provider = Other | |
| env_auth = true | |
| endpoint = https://s3.us-east-va.io.cloud.ovh.us | |
| acl = public-read | |
| RCLONEEOF | |
| rclone copy fluxer_app/dist/assets ovh:fluxer-static/assets \ | |
| --transfers 32 \ | |
| --checkers 16 \ | |
| --size-only \ | |
| --fast-list \ | |
| --s3-upload-concurrency 8 \ | |
| --s3-chunk-size 16M \ | |
| -v | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_PASSWORD }} | |
| - name: Build image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ env.DOCKERFILE }} | |
| tags: ${{ env.SERVICE_NAME }}:${{ env.DEPLOY_SHA }} | |
| load: true | |
| platforms: linux/amd64 | |
| cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} | |
| cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} | |
| env: | |
| DOCKER_BUILD_SUMMARY: false | |
| DOCKER_BUILD_RECORD_UPLOAD: false | |
| - name: Install docker-pussh | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ~/.docker/cli-plugins | |
| curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/v0.3.1/docker-pussh \ | |
| -o ~/.docker/cli-plugins/docker-pussh | |
| chmod +x ~/.docker/cli-plugins/docker-pussh | |
| - name: Set up SSH agent | |
| uses: webfactory/ssh-agent@v0.9.1 | |
| with: | |
| ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} | |
| - name: Add server to known hosts | |
| run: | | |
| set -euo pipefail | |
| mkdir -p ~/.ssh | |
| ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts | |
| - name: Push image and deploy | |
| env: | |
| IMAGE_TAG: ${{ env.SERVICE_NAME }}:${{ env.DEPLOY_SHA }} | |
| SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} | |
| SERVICE_NAME: ${{ env.SERVICE_NAME }} | |
| COMPOSE_STACK: ${{ env.SERVICE_NAME }} | |
| SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@o4510149383094272.ingest.us.sentry.io/4510205815291904 | |
| SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }} | |
| SENTRY_REPORT_HOST: ${{ env.SENTRY_REPORT_HOST }} | |
| CADDY_APP_DOMAIN: ${{ env.CADDY_APP_DOMAIN }} | |
| SENTRY_CADDY_DOMAIN: ${{ env.SENTRY_CADDY_DOMAIN }} | |
| API_TARGET: ${{ env.API_TARGET }} | |
| RELEASE_CHANNEL: ${{ env.RELEASE_CHANNEL }} | |
| APP_REPLICAS: ${{ env.APP_REPLICAS }} | |
| run: | | |
| set -euo pipefail | |
| docker pussh "${IMAGE_TAG}" "${SERVER}" | |
| ssh "${SERVER}" \ | |
| "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK} SENTRY_DSN=${SENTRY_DSN} SENTRY_PROXY_PATH=${SENTRY_PROXY_PATH} SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} CADDY_APP_DOMAIN=${CADDY_APP_DOMAIN} SENTRY_CADDY_DOMAIN=${SENTRY_CADDY_DOMAIN} API_TARGET=${API_TARGET} RELEASE_CHANNEL=${RELEASE_CHANNEL} APP_REPLICAS=${APP_REPLICAS} bash" << 'EOF' | |
| set -euo pipefail | |
| sudo mkdir -p "/opt/${SERVICE_NAME}" | |
| sudo chown -R "${USER}:${USER}" "/opt/${SERVICE_NAME}" | |
| cd "/opt/${SERVICE_NAME}" | |
| cat > compose.yaml << COMPOSEEOF | |
| x-deploy-base: &deploy_base | |
| restart_policy: | |
| condition: on-failure | |
| delay: 5s | |
| max_attempts: 3 | |
| update_config: | |
| parallelism: 1 | |
| delay: 10s | |
| order: start-first | |
| rollback_config: | |
| parallelism: 1 | |
| delay: 10s | |
| x-common-caddy-headers: &common_caddy_headers | |
| caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload" | |
| caddy.header.X-Xss-Protection: "1; mode=block" | |
| caddy.header.X-Content-Type-Options: "nosniff" | |
| caddy.header.Referrer-Policy: "strict-origin-when-cross-origin" | |
| caddy.header.X-Frame-Options: "DENY" | |
| caddy.header.Expect-Ct: "max-age=86400, report-uri=\\"${SENTRY_REPORT_HOST}/api/4510205815291904/security/?sentry_key=59ced0e2666ab83dd1ddb056cdd22d1b\\"" | |
| caddy.header.Cache-Control: "no-store, no-cache, must-revalidate" | |
| caddy.header.Pragma: "no-cache" | |
| caddy.header.Expires: "0" | |
| x-env-base: &env_base | |
| PORT: 8080 | |
| RELEASE_CHANNEL: ${RELEASE_CHANNEL} | |
| FLUXER_METRICS_HOST: fluxer-metrics_app:8080 | |
| SENTRY_DSN: ${SENTRY_DSN} | |
| SENTRY_REPORT_HOST: ${SENTRY_REPORT_HOST} | |
| x-healthcheck: &healthcheck | |
| test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] | |
| interval: 30s | |
| timeout: 10s | |
| retries: 3 | |
| start_period: 40s | |
| services: | |
| app: | |
| image: ${IMAGE_TAG} | |
| deploy: | |
| <<: *deploy_base | |
| replicas: ${APP_REPLICAS} | |
| labels: | |
| <<: *common_caddy_headers | |
| caddy: ${CADDY_APP_DOMAIN} | |
| caddy.handle_path_0: /api* | |
| caddy.handle_path_0.reverse_proxy: "http://${API_TARGET}:8080" | |
| caddy.reverse_proxy: "{{upstreams 8080}}" | |
| environment: | |
| <<: *env_base | |
| SENTRY_PROXY_PATH: ${SENTRY_PROXY_PATH} | |
| networks: [fluxer-shared] | |
| healthcheck: *healthcheck | |
| sentry: | |
| image: ${IMAGE_TAG} | |
| deploy: | |
| <<: *deploy_base | |
| replicas: 1 | |
| labels: | |
| <<: *common_caddy_headers | |
| caddy: ${SENTRY_CADDY_DOMAIN} | |
| caddy.reverse_proxy: "{{upstreams 8080}}" | |
| environment: | |
| <<: *env_base | |
| SENTRY_PROXY_PATH: / | |
| networks: [fluxer-shared] | |
| healthcheck: *healthcheck | |
| networks: | |
| fluxer-shared: | |
| external: true | |
| COMPOSEEOF | |
| docker stack deploy \ | |
| --with-registry-auth \ | |
| --detach=false \ | |
| --resolve-image never \ | |
| -c compose.yaml \ | |
| "${COMPOSE_STACK}" | |
| EOF |