From 197b23757fd87bb0cc0443087b964652a5a0a191 Mon Sep 17 00:00:00 2001 From: Hampus Kraft Date: Fri, 2 Jan 2026 12:13:01 +0000 Subject: [PATCH] [skip ci] chore: cleanup workflows --- .github/workflows/build-desktop-canary.yaml | 613 ------------------ .github/workflows/build-desktop.yaml | 105 ++- .github/workflows/channel-vars.yaml | 94 +++ .github/workflows/deploy-admin-canary.yaml | 141 ---- .github/workflows/deploy-admin.yaml | 163 +++-- .github/workflows/deploy-api-canary.yaml | 201 ------ .github/workflows/deploy-api-worker.yaml | 187 ------ .github/workflows/deploy-api.yaml | 379 +++++++---- .github/workflows/deploy-app-canary.yaml | 318 --------- .github/workflows/deploy-app.yaml | 253 +++++--- .github/workflows/deploy-docs-canary.yaml | 129 ---- .github/workflows/deploy-docs.yaml | 135 +++- .github/workflows/deploy-gateway.yaml | 7 +- .github/workflows/deploy-geoip.yaml | 33 +- .../workflows/deploy-marketing-canary.yaml | 148 ----- .github/workflows/deploy-marketing.yaml | 184 ++++-- .github/workflows/deploy-media-proxy.yaml | 32 +- .github/workflows/deploy-metrics.yaml | 32 +- .github/workflows/deploy-static-proxy.yaml | 32 +- .github/workflows/restart-gateway.yaml | 30 +- .github/workflows/update-word-lists.yaml | 4 +- 21 files changed, 1016 insertions(+), 2204 deletions(-) delete mode 100644 .github/workflows/build-desktop-canary.yaml create mode 100644 .github/workflows/channel-vars.yaml delete mode 100644 .github/workflows/deploy-admin-canary.yaml delete mode 100644 .github/workflows/deploy-api-canary.yaml delete mode 100644 .github/workflows/deploy-api-worker.yaml delete mode 100644 .github/workflows/deploy-app-canary.yaml delete mode 100644 .github/workflows/deploy-docs-canary.yaml delete mode 100644 .github/workflows/deploy-marketing-canary.yaml diff --git a/.github/workflows/build-desktop-canary.yaml b/.github/workflows/build-desktop-canary.yaml deleted file mode 100644 index fcddfeae..00000000 --- a/.github/workflows/build-desktop-canary.yaml +++ /dev/null @@ -1,613 +0,0 @@ -name: build desktop (canary) - -on: - workflow_dispatch: - inputs: - ref: - description: Git ref to build (branch, tag, or commit SHA) - required: false - default: canary - type: string - skip_windows: - description: Skip Windows builds - required: false - default: false - type: boolean - skip_macos: - description: Skip macOS builds - required: false - default: false - type: boolean - skip_linux: - description: Skip Linux builds - required: false - default: false - type: boolean - skip_windows_x64: - description: Skip Windows x64 builds - required: false - default: false - type: boolean - skip_windows_arm64: - description: Skip Windows ARM64 builds - required: false - default: false - type: boolean - skip_macos_x64: - description: Skip macOS x64 builds - required: false - default: false - type: boolean - skip_macos_arm64: - description: Skip macOS ARM64 builds - required: false - default: false - type: boolean - skip_linux_x64: - description: Skip Linux x64 builds - required: false - default: false - type: boolean - skip_linux_arm64: - description: Skip Linux ARM64 builds - required: false - default: false - type: boolean - -permissions: - contents: write - -concurrency: - group: desktop-canary-${{ inputs.ref || 'canary' }} - cancel-in-progress: true - -env: - CHANNEL: canary - SOURCE_REF: ${{ inputs.ref || 'canary' }} - -jobs: - matrix: - name: Resolve build matrix - runs-on: blacksmith-2vcpu-ubuntu-2404 - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Build platform matrix - id: set-matrix - run: | - PLATFORMS='[ - {"platform":"windows","arch":"x64","os":"windows-latest","electron_arch":"x64"}, - {"platform":"windows","arch":"arm64","os":"windows-11-arm","electron_arch":"arm64"}, - {"platform":"macos","arch":"x64","os":"macos-15-intel","electron_arch":"x64"}, - {"platform":"macos","arch":"arm64","os":"macos-15","electron_arch":"arm64"}, - {"platform":"linux","arch":"x64","os":"ubuntu-24.04","electron_arch":"x64"}, - {"platform":"linux","arch":"arm64","os":"ubuntu-24.04-arm","electron_arch":"arm64"} - ]' - - FILTERED="$(echo "$PLATFORMS" | jq -c \ - --argjson skipWin '${{ inputs.skip_windows }}' \ - --argjson skipWinX64 '${{ inputs.skip_windows_x64 }}' \ - --argjson skipWinArm '${{ inputs.skip_windows_arm64 }}' \ - --argjson skipMac '${{ inputs.skip_macos }}' \ - --argjson skipMacX64 '${{ inputs.skip_macos_x64 }}' \ - --argjson skipMacArm '${{ inputs.skip_macos_arm64 }}' \ - --argjson skipLinux '${{ inputs.skip_linux }}' \ - --argjson skipLinuxX64 '${{ inputs.skip_linux_x64 }}' \ - --argjson skipLinuxArm '${{ inputs.skip_linux_arm64 }}' ' - [.[] | select( - ( - ((.platform == "windows") and ( - $skipWin or - ((.arch == "x64") and $skipWinX64) or - ((.arch == "arm64") and $skipWinArm) - )) or - ((.platform == "macos") and ( - $skipMac or - ((.arch == "x64") and $skipMacX64) or - ((.arch == "arm64") and $skipMacArm) - )) or - ((.platform == "linux") and ( - $skipLinux or - ((.arch == "x64") and $skipLinuxX64) or - ((.arch == "arm64") and $skipLinuxArm) - )) - ) | not - )] - ')" - - echo "matrix={\"include\":$FILTERED}" >> "$GITHUB_OUTPUT" - - version: - name: Bump canary version - runs-on: blacksmith-2vcpu-ubuntu-2404 - outputs: - version: ${{ steps.bump.outputs.version }} - pub_date: ${{ steps.bump.outputs.pub_date }} - steps: - - name: Calculate next version - id: bump - run: | - VERSION="0.0.${GITHUB_RUN_NUMBER}" - PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "pub_date=$PUB_DATE" >> "$GITHUB_OUTPUT" - - build: - name: Build ${{ matrix.platform }} (${{ matrix.arch }}) - needs: [version, matrix] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} - env: - APP_WORKDIR: fluxer_app - steps: - - name: Checkout source - uses: actions/checkout@v6 - with: - ref: ${{ env.SOURCE_REF }} - - - name: Shorten Windows paths (workspace + temp for Squirrel) and pin pnpm store - if: runner.os == 'Windows' - shell: pwsh - run: | - subst W: "$env:GITHUB_WORKSPACE" - "APP_WORKDIR=W:\fluxer_app" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - New-Item -ItemType Directory -Force "C:\t" | Out-Null - New-Item -ItemType Directory -Force "C:\sq" | Out-Null - New-Item -ItemType Directory -Force "C:\ebcache" | Out-Null - "TEMP=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "TMP=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "SQUIRREL_TEMP=C:\sq" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "ELECTRON_BUILDER_CACHE=C:\ebcache" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - New-Item -ItemType Directory -Force "C:\pnpm-store" | Out-Null - "NPM_CONFIG_STORE_DIR=C:\pnpm-store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - "npm_config_store_dir=C:\pnpm-store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - "store-dir=C:\pnpm-store" | Set-Content -Path "W:\.npmrc" -Encoding ascii - git config --global core.longpaths true - - - 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: 20 - - - name: Resolve pnpm store path (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - $store = pnpm store path --silent - "PNPM_STORE_PATH=$store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - New-Item -ItemType Directory -Force $store | Out-Null - - - name: Resolve pnpm store path (Unix) - if: runner.os != 'Windows' - shell: bash - run: | - store="$(pnpm store path --silent)" - echo "PNPM_STORE_PATH=$store" >> "$GITHUB_ENV" - mkdir -p "$store" - - - name: Cache pnpm store - uses: actions/cache@v4 - with: - path: ${{ env.PNPM_STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Python setuptools (Windows ARM64) - if: matrix.platform == 'windows' && matrix.arch == 'arm64' - shell: pwsh - run: | - python -m pip install --upgrade pip - python -m pip install "setuptools>=69" wheel - - - name: Install Python setuptools (macOS) - if: matrix.platform == 'macos' - run: brew install python-setuptools - - - name: Install Linux dependencies - if: matrix.platform == 'linux' - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get update - sudo apt-get install -y \ - libx11-dev libxtst-dev libxt-dev libxinerama-dev libxkbcommon-dev libxrandr-dev \ - ruby ruby-dev build-essential rpm \ - libpixman-1-dev libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev - sudo gem install --no-document fpm - - - name: Install dependencies - working-directory: ${{ env.APP_WORKDIR }} - run: pnpm install --frozen-lockfile - - - name: Update version - working-directory: ${{ env.APP_WORKDIR }} - run: pnpm version ${{ needs.version.outputs.version }} --no-git-tag-version --allow-same-version - - - name: Build Electron main process - working-directory: ${{ env.APP_WORKDIR }} - env: - BUILD_CHANNEL: canary - run: pnpm electron:compile - - - name: Build Electron app (macOS) - if: matrix.platform == 'macos' - working-directory: ${{ env.APP_WORKDIR }} - env: - BUILD_CHANNEL: canary - CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: pnpm exec electron-builder --config electron-builder.canary.yaml --mac --${{ matrix.electron_arch }} - - - name: Build Electron app (Windows) - if: matrix.platform == 'windows' - working-directory: ${{ env.APP_WORKDIR }} - env: - BUILD_CHANNEL: canary - TEMP: C:\t - TMP: C:\t - SQUIRREL_TEMP: C:\sq - ELECTRON_BUILDER_CACHE: C:\ebcache - run: pnpm exec electron-builder --config electron-builder.canary.yaml --win --${{ matrix.electron_arch }} - - - name: Analyze Squirrel nupkg for long paths - if: matrix.platform == 'windows' - working-directory: ${{ env.APP_WORKDIR }} - shell: pwsh - env: - BUILD_VERSION: ${{ needs.version.outputs.version }} - MAX_WINDOWS_PATH_LEN: 260 - PATH_HEADROOM: 10 - run: | - $primaryDir = if ("${{ matrix.arch }}" -eq "arm64") { "dist-electron/squirrel-windows-arm64" } else { "dist-electron/squirrel-windows" } - $fallbackDir = if ("${{ matrix.arch }}" -eq "arm64") { "dist-electron/squirrel-windows" } else { "dist-electron/squirrel-windows-arm64" } - $dirs = @($primaryDir, $fallbackDir) - - $nupkg = $null - foreach ($d in $dirs) { - if (Test-Path $d) { - $nupkg = Get-ChildItem -Path "$d/*.nupkg" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($nupkg) { break } - } - } - - if (-not $nupkg) { - throw "No Squirrel nupkg found in: $($dirs -join ', ')" - } - - Write-Host "Analyzing Windows installer $($nupkg.FullName)" - $env:NUPKG_PATH = $nupkg.FullName - - $lines = @( - 'import os' - 'import zipfile' - '' - 'path = os.environ["NUPKG_PATH"]' - 'build_ver = os.environ["BUILD_VERSION"]' - 'prefix = os.path.join(os.environ["LOCALAPPDATA"], "fluxer_app", f"app-{build_ver}", "resources", "app.asar.unpacked")' - 'max_len = int(os.environ.get("MAX_WINDOWS_PATH_LEN", "260"))' - 'headroom = int(os.environ.get("PATH_HEADROOM", "10"))' - 'limit = max_len - headroom' - '' - 'with zipfile.ZipFile(path) as archive:' - ' entries = []' - ' for info in archive.infolist():' - ' normalized = info.filename.lstrip("/\\\\")' - ' total_len = len(os.path.join(prefix, normalized)) if normalized else len(prefix)' - ' entries.append((total_len, info.filename))' - '' - 'if not entries:' - ' raise SystemExit("nupkg archive contains no entries")' - '' - 'entries.sort(reverse=True)' - 'print(f"Assumed install prefix: {prefix} ({len(prefix)} chars). Maximum allowed path length: {limit} (total reserve {max_len}, headroom {headroom}).")' - 'print("Top 20 longest archived paths (length includes prefix):")' - 'for length, name in entries[:20]:' - ' print(f"{length:4d} {name}")' - '' - 'longest_len, longest_name = entries[0]' - 'if longest_len > limit:' - ' raise SystemExit(f"Longest path {longest_len} for {longest_name} exceeds limit {limit}")' - 'print(f"Longest archived path {longest_len} is within the limit of {limit}.")' - ) - - $scriptPath = Join-Path $env:TEMP "nupkg-long-path-check.py" - Set-Content -Path $scriptPath -Value $lines -Encoding utf8 - python $scriptPath - - - name: Build Electron app (Linux) - if: matrix.platform == 'linux' - working-directory: ${{ env.APP_WORKDIR }} - env: - BUILD_CHANNEL: canary - USE_SYSTEM_FPM: true - run: pnpm exec electron-builder --config electron-builder.canary.yaml --linux --${{ matrix.electron_arch }} - - - name: Prepare artifacts (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - New-Item -ItemType Directory -Force upload_staging | Out-Null - - $dist = Join-Path $env:APP_WORKDIR "dist-electron" - $sqDirName = if ("${{ matrix.arch }}" -eq "arm64") { "squirrel-windows-arm64" } else { "squirrel-windows" } - $sqFallbackName = if ($sqDirName -eq "squirrel-windows") { "squirrel-windows-arm64" } else { "squirrel-windows" } - - $sq = Join-Path $dist $sqDirName - $sqFallback = Join-Path $dist $sqFallbackName - - $picked = $null - if (Test-Path $sq) { $picked = $sq } - elseif (Test-Path $sqFallback) { $picked = $sqFallback } - - if ($picked) { - Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.exe" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.exe.blockmap" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$picked\RELEASES*" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.nupkg" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.nupkg.blockmap" "upload_staging\" - } - - if (Test-Path $dist) { - Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.yml" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.zip" "upload_staging\" - Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.zip.blockmap" "upload_staging\" - } - - if (-not (Get-ChildItem upload_staging -Filter *.exe -ErrorAction SilentlyContinue)) { - throw "No installer .exe staged. Squirrel outputs were not copied." - } - - Get-ChildItem -Force upload_staging | Format-Table -AutoSize - - - name: Prepare artifacts (Unix) - if: runner.os != 'Windows' - shell: bash - run: | - mkdir -p upload_staging - DIST="${{ env.APP_WORKDIR }}/dist-electron" - - cp -f "$DIST"/*.dmg upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.zip upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.zip.blockmap upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.yml upload_staging/ 2>/dev/null || true - - cp -f "$DIST"/*.AppImage upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.deb upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.rpm upload_staging/ 2>/dev/null || true - cp -f "$DIST"/*.tar.gz upload_staging/ 2>/dev/null || true - - ls -la upload_staging/ - - - name: Normalize updater YAML (arm64) - if: matrix.arch == 'arm64' - shell: bash - run: | - cd upload_staging - [[ "${{ matrix.platform }}" == "macos" && -f latest-mac.yml && ! -f latest-mac-arm64.yml ]] && mv latest-mac.yml latest-mac-arm64.yml || true - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: fluxer-desktop-canary-${{ matrix.platform }}-${{ matrix.arch }} - path: | - upload_staging/*.exe - upload_staging/*.exe.blockmap - upload_staging/*.dmg - upload_staging/*.zip - upload_staging/*.zip.blockmap - upload_staging/*.AppImage - upload_staging/*.deb - upload_staging/*.rpm - upload_staging/*.tar.gz - upload_staging/*.yml - upload_staging/*.nupkg - upload_staging/*.nupkg.blockmap - upload_staging/RELEASES* - retention-days: 30 - - upload: - name: Upload to S3 (rclone) - needs: [version, build] - runs-on: blacksmith-2vcpu-ubuntu-2404 - env: - CHANNEL: canary - S3_ENDPOINT: https://s3.us-east-va.io.cloud.ovh.us - S3_BUCKET: fluxer-downloads - PUBLIC_DL_BASE: https://api.fluxer.app/dl - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - pattern: fluxer-desktop-canary-* - - - 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: Configure rclone (OVH S3) - 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 = private - RCLONEEOF - - - name: Build S3 payload layout (+ manifest.json) - env: - VERSION: ${{ needs.version.outputs.version }} - PUB_DATE: ${{ needs.version.outputs.pub_date }} - run: | - set -euo pipefail - - mkdir -p s3_payload - - shopt -s nullglob - for dir in artifacts/fluxer-desktop-canary-*; do - [ -d "$dir" ] || continue - - base="$(basename "$dir")" - if [[ "$base" =~ ^fluxer-desktop-canary-([a-z]+)-([a-z0-9]+)$ ]]; then - platform="${BASH_REMATCH[1]}" - arch="${BASH_REMATCH[2]}" - else - echo "Skipping unrecognized artifact dir: $base" - continue - fi - - case "$platform" in - windows) plat="win32" ;; - macos) plat="darwin" ;; - linux) plat="linux" ;; - *) - echo "Unknown platform: $platform" - continue - ;; - esac - - dest="s3_payload/desktop/${CHANNEL}/${plat}/${arch}" - mkdir -p "$dest" - cp -av "$dir"/* "$dest/" || true - - if [[ "$plat" == "darwin" ]]; then - zip_file="" - for z in "$dest"/*.zip; do - zip_file="$z" - break - done - - if [[ -z "$zip_file" ]]; then - echo "No .zip found for macOS $arch in $dest (auto-update requires zip artifacts)." - else - zip_name="$(basename "$zip_file")" - url="${PUBLIC_DL_BASE}/desktop/${CHANNEL}/${plat}/${arch}/${zip_name}" - - cat > "$dest/RELEASES.json" </dev/null | grep -i 'setup' | head -n1 || true)" - if [[ -z "$setup_file" ]]; then - setup_file="$(ls -1 "$dest"/*.exe 2>/dev/null | head -n1 || true)" - fi - fi - - if [[ "$plat" == "darwin" ]]; then - dmg_file="$(ls -1 "$dest"/*.dmg 2>/dev/null | head -n1 || true)" - zip_file2="$(ls -1 "$dest"/*.zip 2>/dev/null | head -n1 || true)" - fi - - if [[ "$plat" == "linux" ]]; then - appimage_file="$(ls -1 "$dest"/*.AppImage 2>/dev/null | head -n1 || true)" - deb_file="$(ls -1 "$dest"/*.deb 2>/dev/null | head -n1 || true)" - rpm_file="$(ls -1 "$dest"/*.rpm 2>/dev/null | head -n1 || true)" - targz_file="$(ls -1 "$dest"/*.tar.gz 2>/dev/null | head -n1 || true)" - fi - - jq -n \ - --arg channel "${CHANNEL}" \ - --arg platform "${plat}" \ - --arg arch "${arch}" \ - --arg version "${VERSION}" \ - --arg pub_date "${PUB_DATE}" \ - --arg setup "$(basename "${setup_file:-}")" \ - --arg dmg "$(basename "${dmg_file:-}")" \ - --arg zip "$(basename "${zip_file2:-}")" \ - --arg appimage "$(basename "${appimage_file:-}")" \ - --arg deb "$(basename "${deb_file:-}")" \ - --arg rpm "$(basename "${rpm_file:-}")" \ - --arg tar_gz "$(basename "${targz_file:-}")" \ - '{ - channel: $channel, - platform: $platform, - arch: $arch, - version: $version, - pub_date: $pub_date, - files: { - setup: $setup, - dmg: $dmg, - zip: $zip, - appimage: $appimage, - deb: $deb, - rpm: $rpm, - tar_gz: $tar_gz - } - }' > "$dest/manifest.json" - done - - echo "Payload tree:" - find s3_payload -maxdepth 6 -type f | sort - - - name: Upload payload to S3 - run: | - set -euo pipefail - rclone copy s3_payload/desktop "ovh:${S3_BUCKET}/desktop" \ - --transfers 32 \ - --checkers 16 \ - --fast-list \ - --s3-upload-concurrency 8 \ - --s3-chunk-size 16M \ - -v - - - name: Build summary - run: | - { - echo "## Desktop Canary Upload Complete" - echo "" - echo "**Version:** ${{ needs.version.outputs.version }}" - echo "" - echo "**S3 prefix:** desktop/${CHANNEL}/" - echo "" - echo "**Redirect endpoint shape:** /dl/desktop/${CHANNEL}/{plat}/{arch}/{format}" - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/build-desktop.yaml b/.github/workflows/build-desktop.yaml index a9d0ad47..23a4faed 100644 --- a/.github/workflows/build-desktop.yaml +++ b/.github/workflows/build-desktop.yaml @@ -3,10 +3,18 @@ name: build desktop on: workflow_dispatch: inputs: + channel: + description: Channel to build (stable or canary) + required: false + type: choice + options: + - stable + - canary + default: stable ref: description: Git ref to build (branch, tag, or commit SHA) required: false - default: stable + default: '' type: string skip_windows: description: Skip Windows builds @@ -58,14 +66,38 @@ permissions: contents: write concurrency: - group: desktop-stable-${{ inputs.ref || 'stable' }} + group: desktop-${{ inputs.channel }} cancel-in-progress: true env: - CHANNEL: stable - SOURCE_REF: ${{ inputs.ref || 'stable' }} + CHANNEL: ${{ inputs.channel }} + BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }} + SOURCE_REF: ${{ inputs.ref && inputs.ref || (inputs.channel == 'canary' && 'canary' || 'main') }} jobs: + meta: + name: Resolve build metadata + runs-on: blacksmith-2vcpu-ubuntu-2404 + outputs: + version: ${{ steps.meta.outputs.version }} + pub_date: ${{ steps.meta.outputs.pub_date }} + channel: ${{ steps.meta.outputs.channel }} + build_channel: ${{ steps.meta.outputs.build_channel }} + source_ref: ${{ steps.meta.outputs.source_ref }} + steps: + - name: Set metadata + id: meta + shell: bash + run: | + set -euo pipefail + VERSION="0.0.${GITHUB_RUN_NUMBER}" + PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "pub_date=${PUB_DATE}" >> "$GITHUB_OUTPUT" + echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT" + echo "build_channel=${{ inputs.channel == 'canary' && 'canary' || 'stable' }}" >> "$GITHUB_OUTPUT" + echo "source_ref=${{ (inputs.ref && inputs.ref) || (inputs.channel == 'canary' && 'canary' || 'main') }}" >> "$GITHUB_OUTPUT" + matrix: name: Resolve build matrix runs-on: blacksmith-2vcpu-ubuntu-2404 @@ -74,7 +106,10 @@ jobs: steps: - name: Build platform matrix id: set-matrix + shell: bash run: | + set -euo pipefail + PLATFORMS='[ {"platform":"windows","arch":"x64","os":"windows-latest","electron_arch":"x64"}, {"platform":"windows","arch":"arm64","os":"windows-11-arm","electron_arch":"arm64"}, @@ -119,16 +154,20 @@ jobs: build: name: Build ${{ matrix.platform }} (${{ matrix.arch }}) - needs: matrix - outputs: - version: ${{ steps.metadata.outputs.version }} - pub_date: ${{ steps.metadata.outputs.pub_date }} + needs: + - meta + - matrix runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} env: APP_WORKDIR: fluxer_app + CHANNEL: ${{ needs.meta.outputs.channel }} + BUILD_CHANNEL: ${{ needs.meta.outputs.build_channel }} + SOURCE_REF: ${{ needs.meta.outputs.source_ref }} + VERSION: ${{ needs.meta.outputs.version }} + PUB_DATE: ${{ needs.meta.outputs.pub_date }} steps: - name: Checkout source uses: actions/checkout@v6 @@ -167,18 +206,6 @@ jobs: with: node-version: 20 - - name: Set build metadata - id: metadata - shell: bash - run: | - set -euo pipefail - VERSION="0.0.${GITHUB_RUN_NUMBER}" - PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" - echo "PUB_DATE=${PUB_DATE}" >> "$GITHUB_ENV" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "pub_date=${PUB_DATE}" >> "$GITHUB_OUTPUT" - - name: Resolve pnpm store path (Windows) if: runner.os == 'Windows' shell: pwsh @@ -191,6 +218,7 @@ jobs: if: runner.os != 'Windows' shell: bash run: | + set -euo pipefail store="$(pnpm store path --silent)" echo "PNPM_STORE_PATH=$store" >> "$GITHUB_ENV" mkdir -p "$store" @@ -232,19 +260,19 @@ jobs: - name: Update version working-directory: ${{ env.APP_WORKDIR }} - run: pnpm version "$VERSION" --no-git-tag-version --allow-same-version + run: pnpm version "${{ env.VERSION }}" --no-git-tag-version --allow-same-version - name: Build Electron main process working-directory: ${{ env.APP_WORKDIR }} env: - BUILD_CHANNEL: stable + BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }} run: pnpm electron:compile - name: Build Electron app (macOS) if: matrix.platform == 'macos' working-directory: ${{ env.APP_WORKDIR }} env: - BUILD_CHANNEL: stable + BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} @@ -256,7 +284,7 @@ jobs: if: matrix.platform == 'windows' working-directory: ${{ env.APP_WORKDIR }} env: - BUILD_CHANNEL: stable + BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }} TEMP: C:\t TMP: C:\t SQUIRREL_TEMP: C:\sq @@ -268,7 +296,7 @@ jobs: working-directory: ${{ env.APP_WORKDIR }} shell: pwsh env: - BUILD_VERSION: ${{ steps.metadata.outputs.version }} + BUILD_VERSION: ${{ env.VERSION }} MAX_WINDOWS_PATH_LEN: 260 PATH_HEADROOM: 10 run: | @@ -332,7 +360,7 @@ jobs: if: matrix.platform == 'linux' working-directory: ${{ env.APP_WORKDIR }} env: - BUILD_CHANNEL: stable + BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }} USE_SYSTEM_FPM: true run: pnpm exec electron-builder --config electron-builder.yaml --linux --${{ matrix.electron_arch }} @@ -377,6 +405,7 @@ jobs: if: runner.os != 'Windows' shell: bash run: | + set -euo pipefail mkdir -p upload_staging DIST="${{ env.APP_WORKDIR }}/dist-electron" @@ -396,13 +425,14 @@ jobs: if: matrix.arch == 'arm64' shell: bash run: | + set -euo pipefail cd upload_staging [[ "${{ matrix.platform }}" == "macos" && -f latest-mac.yml && ! -f latest-mac-arm64.yml ]] && mv latest-mac.yml latest-mac-arm64.yml || true - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: fluxer-desktop-stable-${{ matrix.platform }}-${{ matrix.arch }} + name: fluxer-desktop-${{ env.BUILD_CHANNEL }}-${{ matrix.platform }}-${{ matrix.arch }} path: | upload_staging/*.exe upload_staging/*.exe.blockmap @@ -421,10 +451,13 @@ jobs: upload: name: Upload to S3 (rclone) - needs: build + needs: + - meta + - build runs-on: blacksmith-2vcpu-ubuntu-2404 env: - CHANNEL: stable + CHANNEL: ${{ needs.meta.outputs.build_channel }} + DISPLAY_CHANNEL: ${{ needs.meta.outputs.channel }} S3_ENDPOINT: https://s3.us-east-va.io.cloud.ovh.us S3_BUCKET: fluxer-downloads PUBLIC_DL_BASE: https://api.fluxer.app/dl @@ -435,7 +468,7 @@ jobs: uses: actions/download-artifact@v4 with: path: artifacts - pattern: fluxer-desktop-stable-* + pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-* - name: Install rclone run: | @@ -459,19 +492,19 @@ jobs: - name: Build S3 payload layout (+ manifest.json) env: - VERSION: ${{ needs.build.outputs.version }} - PUB_DATE: ${{ needs.build.outputs.pub_date }} + VERSION: ${{ needs.meta.outputs.version }} + PUB_DATE: ${{ needs.meta.outputs.pub_date }} run: | set -euo pipefail mkdir -p s3_payload shopt -s nullglob - for dir in artifacts/fluxer-desktop-stable-*; do + for dir in artifacts/fluxer-desktop-${CHANNEL}-*; do [ -d "$dir" ] || continue base="$(basename "$dir")" - if [[ "$base" =~ ^fluxer-desktop-stable-([a-z]+)-([a-z0-9]+)$ ]]; then + if [[ "$base" =~ ^fluxer-desktop-[a-z]+-([a-z]+)-([a-z0-9]+)$ ]]; then platform="${BASH_REMATCH[1]}" arch="${BASH_REMATCH[2]}" else @@ -602,9 +635,9 @@ jobs: - name: Build summary run: | { - echo "## Desktop Stable Upload Complete" + echo "## Desktop ${DISPLAY_CHANNEL^} Upload Complete" echo "" - echo "**Version:** ${{ steps.metadata.outputs.version }}" + echo "**Version:** ${{ needs.meta.outputs.version }}" echo "" echo "**S3 prefix:** desktop/${CHANNEL}/" echo "" diff --git a/.github/workflows/channel-vars.yaml b/.github/workflows/channel-vars.yaml new file mode 100644 index 00000000..4913cfdd --- /dev/null +++ b/.github/workflows/channel-vars.yaml @@ -0,0 +1,94 @@ +name: channel vars + +on: + workflow_call: + inputs: + github_event_name: + type: string + github_ref_name: + type: string + required: false + github_ref: + type: string + required: false + workflow_dispatch_channel: + type: string + required: false + workflow_dispatch_ref: + type: string + required: false + + outputs: + channel: + description: 'Computed release channel (stable|canary)' + value: ${{ jobs.emit.outputs.channel }} + is_canary: + description: 'Whether this is a canary deploy (true|false)' + value: ${{ jobs.emit.outputs.is_canary }} + source_ref: + description: 'Git ref to check out for the deploy' + value: ${{ jobs.emit.outputs.source_ref }} + stack_suffix: + description: "Suffix for stack/image names ('' or '-canary')" + value: ${{ jobs.emit.outputs.stack_suffix }} + +jobs: + emit: + runs-on: ubuntu-latest + outputs: + channel: ${{ steps.compute.outputs.channel }} + is_canary: ${{ steps.compute.outputs.is_canary }} + source_ref: ${{ steps.compute.outputs.source_ref }} + stack_suffix: ${{ steps.compute.outputs.stack_suffix }} + steps: + - name: Determine channel + id: compute + shell: bash + run: | + set -euo pipefail + + event_name="${{ inputs.github_event_name }}" + ref_name="${{ inputs.github_ref_name || '' }}" + ref="${{ inputs.github_ref || '' }}" + dispatch_channel="${{ inputs.workflow_dispatch_channel || '' }}" + dispatch_ref="${{ inputs.workflow_dispatch_ref || '' }}" + + channel="stable" + if [[ "${event_name}" == "push" ]]; then + if [[ "${ref_name}" == "canary" ]]; then + channel="canary" + fi + else + if [[ "${dispatch_channel}" == "canary" ]]; then + channel="canary" + fi + fi + + if [[ "${event_name}" == "push" ]]; then + source_ref="${ref:-refs/heads/${ref_name:-main}}" + else + if [[ -n "${dispatch_ref}" ]]; then + source_ref="${dispatch_ref}" + else + if [[ "${channel}" == "canary" ]]; then + source_ref="refs/heads/canary" + else + source_ref="refs/heads/main" + fi + fi + fi + + stack_suffix="" + if [[ "${channel}" == "canary" ]]; then + stack_suffix="-canary" + fi + + is_canary="false" + if [[ "${channel}" == "canary" ]]; then + is_canary="true" + fi + + printf 'channel=%s\n' "${channel}" >> "$GITHUB_OUTPUT" + printf 'is_canary=%s\n' "${is_canary}" >> "$GITHUB_OUTPUT" + printf 'source_ref=%s\n' "${source_ref}" >> "$GITHUB_OUTPUT" + printf 'stack_suffix=%s\n' "${stack_suffix}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/deploy-admin-canary.yaml b/.github/workflows/deploy-admin-canary.yaml deleted file mode 100644 index 4be01efb..00000000 --- a/.github/workflows/deploy-admin-canary.yaml +++ /dev/null @@ -1,141 +0,0 @@ -name: deploy admin (canary) - -on: - push: - branches: - - canary - paths: - - fluxer_admin/**/* - - .github/workflows/deploy-admin-canary.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-admin-canary - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy app (canary) - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - 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: fluxer_admin - file: fluxer_admin/Dockerfile - tags: fluxer-admin-canary:${{ github.sha }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-admin-canary - cache-to: type=gha,mode=max,scope=deploy-fluxer-admin-canary - build-args: | - BUILD_TIMESTAMP=${{ github.run_id }} - 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: fluxer-admin-canary:${{ github.sha }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-admin-canary - sudo chown -R ${USER}:${USER} /opt/fluxer-admin-canary - cd /opt/fluxer-admin-canary - - cat > compose.yaml << COMPOSEEOF - services: - app: - image: ${IMAGE_TAG} - env_file: - - /etc/fluxer/fluxer.env - environment: - - FLUXER_API_PUBLIC_ENDPOINT=https://api.canary.fluxer.app - - FLUXER_APP_ENDPOINT=https://web.canary.fluxer.app - - FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_ADMIN_ENDPOINT=https://admin.canary.fluxer.app - - FLUXER_PATH_ADMIN=/ - - APP_MODE=admin - - FLUXER_ADMIN_PORT=8080 - - ADMIN_OAUTH2_REDIRECT_URI=https://admin.canary.fluxer.app/oauth2_callback - - ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - deploy: - replicas: 1 - 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 - labels: - - 'caddy=admin.canary.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - - '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' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml fluxer-admin-canary - docker service update --image ${IMAGE_TAG} fluxer-admin-canary_app - EOF diff --git a/.github/workflows/deploy-admin.yaml b/.github/workflows/deploy-admin.yaml index 68f7155b..a6fa039a 100644 --- a/.github/workflows/deploy-admin.yaml +++ b/.github/workflows/deploy-admin.yaml @@ -4,25 +4,71 @@ on: push: branches: - main + - canary paths: - - fluxer_admin/**/* + - fluxer_admin/** - .github/workflows/deploy-admin.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-admin + group: deploy-fluxer-admin-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} cancel-in-progress: true permissions: contents: read - 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 + name: Deploy admin + 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 }} + STACK: ${{ format('fluxer-admin{0}', needs.channel-vars.outputs.stack_suffix) }} + CACHE_SCOPE: ${{ format('deploy-fluxer-admin{0}', needs.channel-vars.outputs.stack_suffix) }} + CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'admin.canary.fluxer.app' || 'admin.fluxer.app' }} + APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }} + API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }} + ADMIN_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app' || 'https://admin.fluxer.app' }} + ADMIN_REDIRECT_URI: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app/oauth2_callback' || 'https://admin.fluxer.app/oauth2_callback' }} + REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }} steps: - uses: actions/checkout@v6 + with: + ref: ${{ env.SOURCE_REF }} + fetch-depth: 0 + + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -38,13 +84,11 @@ jobs: with: context: fluxer_admin file: fluxer_admin/Dockerfile - tags: fluxer-admin:${{ github.sha }} + tags: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-admin - cache-to: type=gha,mode=max,scope=deploy-fluxer-admin - build-args: | - BUILD_TIMESTAMP=${{ github.run_id }} + 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 @@ -70,51 +114,70 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: fluxer-admin:${{ github.sha }} + IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} + STACK: ${{ env.STACK }} + APP_ENDPOINT: ${{ env.APP_ENDPOINT }} + API_PUBLIC_ENDPOINT: ${{ env.API_PUBLIC_ENDPOINT }} + ADMIN_ENDPOINT: ${{ env.ADMIN_ENDPOINT }} + ADMIN_REDIRECT_URI: ${{ env.ADMIN_REDIRECT_URI }} + CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }} + REPLICAS: ${{ env.REPLICAS }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-admin - sudo chown -R ${USER}:${USER} /opt/fluxer-admin - cd /opt/fluxer-admin + ssh "${SERVER}" \ + "IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} APP_ENDPOINT=${APP_ENDPOINT} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} ADMIN_ENDPOINT=${ADMIN_ENDPOINT} ADMIN_REDIRECT_URI=${ADMIN_REDIRECT_URI} CADDY_DOMAIN=${CADDY_DOMAIN} REPLICAS=${REPLICAS} bash" << 'EOF' + set -euo pipefail + sudo mkdir -p "/opt/${STACK}" + sudo chown -R "${USER}:${USER}" "/opt/${STACK}" + cd "/opt/${STACK}" + + 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-healthcheck: &healthcheck + test: ['CMD', 'curl', '-f', 'http://localhost:8080/'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s - cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} env_file: - /etc/fluxer/fluxer.env environment: - - FLUXER_API_PUBLIC_ENDPOINT=https://api.fluxer.app - - FLUXER_APP_ENDPOINT=https://web.fluxer.app - - FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_ADMIN_ENDPOINT=https://admin.fluxer.app - - FLUXER_PATH_ADMIN=/ - - APP_MODE=admin - - FLUXER_ADMIN_PORT=8080 - - ADMIN_OAUTH2_REDIRECT_URI=https://admin.fluxer.app/oauth2_callback - - ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 + FLUXER_API_PUBLIC_ENDPOINT: ${API_PUBLIC_ENDPOINT} + FLUXER_APP_ENDPOINT: ${APP_ENDPOINT} + FLUXER_MEDIA_ENDPOINT: https://fluxerusercontent.com + FLUXER_CDN_ENDPOINT: https://fluxerstatic.com + FLUXER_ADMIN_ENDPOINT: ${ADMIN_ENDPOINT} + FLUXER_PATH_ADMIN: / + APP_MODE: admin + FLUXER_ADMIN_PORT: 8080 + ADMIN_OAUTH2_REDIRECT_URI: ${ADMIN_REDIRECT_URI} + ADMIN_OAUTH2_CLIENT_ID: 1440355698178071552 + ADMIN_OAUTH2_AUTO_CREATE: "false" + FLUXER_METRICS_HOST: fluxer-metrics_app:8080 deploy: - replicas: 2 - 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 + <<: *deploy_base + replicas: ${REPLICAS} labels: - - 'caddy=admin.fluxer.app' + - "caddy=${CADDY_DOMAIN}" - 'caddy.reverse_proxy={{upstreams 8080}}' - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - 'caddy.header.Strict-Transport-Security="max-age=31536000; includeSubDomains; preload"' @@ -122,20 +185,18 @@ jobs: - 'caddy.header.X-Content-Type-Options=nosniff' - 'caddy.header.Referrer-Policy=strict-origin-when-cross-origin' - 'caddy.header.X-Frame-Options=DENY' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + networks: [fluxer-shared] + healthcheck: *healthcheck networks: fluxer-shared: external: true COMPOSEEOF - docker stack deploy -c compose.yaml fluxer-admin - docker service update --image ${IMAGE_TAG} fluxer-admin_app + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${STACK}" EOF diff --git a/.github/workflows/deploy-api-canary.yaml b/.github/workflows/deploy-api-canary.yaml deleted file mode 100644 index 76bd2d62..00000000 --- a/.github/workflows/deploy-api-canary.yaml +++ /dev/null @@ -1,201 +0,0 @@ -name: deploy api (canary) - -on: - push: - branches: - - canary - paths: - - fluxer_api/**/* - - .github/workflows/deploy-api-canary.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-api-canary - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy app (canary) - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - 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: fluxer_api - file: fluxer_api/Dockerfile - tags: fluxer-api-canary:${{ github.sha }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-api-canary - cache-to: type=gha,mode=max,scope=deploy-fluxer-api-canary - 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: fluxer-api-canary:${{ github.sha }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-api-canary - sudo chown -R ${USER}:${USER} /opt/fluxer-api-canary - cd /opt/fluxer-api-canary - - cat > compose.yaml << COMPOSEEOF - services: - app: - image: ${IMAGE_TAG} - command: ['npm', 'run', 'start'] - env_file: - - /etc/fluxer/fluxer.env - environment: - - NODE_ENV=production - - FLUXER_API_PORT=8080 - - SENTRY_DSN=https://bb16e8b823b82d788db49a666b3b4b90@o4510149383094272.ingest.us.sentry.io/4510205804019712 - - CASSANDRA_HOSTS=cassandra - - CASSANDRA_KEYSPACE=fluxer - - CASSANDRA_LOCAL_DC=dc1 - - FLUXER_GATEWAY_RPC_HOST=fluxer-gateway_app - - FLUXER_GATEWAY_RPC_PORT=8081 - - FLUXER_MEDIA_PROXY_HOST=fluxer-media-proxy_app - - FLUXER_MEDIA_PROXY_PORT=8080 - - FLUXER_API_CLIENT_ENDPOINT=https://web.canary.fluxer.app/api - - FLUXER_APP_ENDPOINT=https://web.canary.fluxer.app - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com - - FLUXER_INVITE_ENDPOINT=https://fluxer.gg - - FLUXER_GIFT_ENDPOINT=https://fluxer.gift - - AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us - - AWS_S3_BUCKET_CDN=fluxer - - AWS_S3_BUCKET_UPLOADS=fluxer-uploads - - AWS_S3_BUCKET_REPORTS=fluxer-reports - - AWS_S3_BUCKET_HARVESTS=fluxer-harvests - - AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads - - SENDGRID_FROM_EMAIL=noreply@fluxer.app - - SENDGRID_FROM_NAME=Fluxer - - SENDGRID_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoeqQS37o9s8ZcLBJUtT4hghAmI5RqsvcQ0OvsUn3XPfl7GkjxljufyxuL8+m1mCHP2IA1jdYT3kJQoQYXP6ZpQ== - - FLUXER_API_PUBLIC_ENDPOINT=https://api.canary.fluxer.app - - FLUXER_GATEWAY_ENDPOINT=wss://gateway.fluxer.app - - FLUXER_MARKETING_ENDPOINT=https://canary.fluxer.app - - FLUXER_PATH_MARKETING=/ - - FLUXER_ADMIN_ENDPOINT=https://admin.canary.fluxer.app - - FLUXER_PATH_ADMIN=/ - - ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 - - ADMIN_OAUTH2_REDIRECT_URI=https://admin.canary.fluxer.app/oauth2_callback - - ADMIN_OAUTH2_AUTO_CREATE=false - - PASSKEYS_ENABLED=true - - PASSKEY_RP_NAME=Fluxer - - PASSKEY_RP_ID=fluxer.app - - PASSKEY_ALLOWED_ORIGINS=https://web.fluxer.app,https://web.canary.fluxer.app - - CAPTCHA_ENABLED=true - - CAPTCHA_PRIMARY_PROVIDER=turnstile - - HCAPTCHA_SITE_KEY=9cbad400-df84-4e0c-bda6-e65000be78aa - - TURNSTILE_SITE_KEY=0x4AAAAAAB_lAoDdTWznNHMq - - EMAIL_ENABLED=true - - SMS_ENABLED=true - - VOICE_ENABLED=true - - SEARCH_ENABLED=true - - MEILISEARCH_URL=http://meilisearch:7700 - - STRIPE_ENABLED=true - - STRIPE_PRICE_ID_MONTHLY_USD=price_1SJHZzFPC94Os7FdzBgvz0go - - STRIPE_PRICE_ID_YEARLY_USD=price_1SJHabFPC94Os7FdhSOWVfcr - - STRIPE_PRICE_ID_VISIONARY_USD=price_1SJHGTFPC94Os7FdWTyqvJZ8 - - STRIPE_PRICE_ID_MONTHLY_EUR=price_1SJHaFFPC94Os7FdmcrGicXa - - STRIPE_PRICE_ID_YEARLY_EUR=price_1SJHarFPC94Os7Fddbyzr5I8 - - STRIPE_PRICE_ID_VISIONARY_EUR=price_1SJHGnFPC94Os7FdZn23KkYB - - STRIPE_PRICE_ID_GIFT_VISIONARY_USD=price_1SKhWqFPC94Os7FdxRmQrg3k - - STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=price_1SKhXrFPC94Os7FdcepLrJqr - - STRIPE_PRICE_ID_GIFT_1_MONTH_USD=price_1SJHHKFPC94Os7FdGwUs1EQg - - STRIPE_PRICE_ID_GIFT_1_YEAR_USD=price_1SJHHrFPC94Os7FdWrQN5tKl - - STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=price_1SJHHaFPC94Os7FdwwpwhliW - - STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=price_1SJHI5FPC94Os7Fd3DpLxb0D - - FLUXER_VISIONARIES_GUILD_ID=1428504839258075143 - - FLUXER_OPERATORS_GUILD_ID=1434192442151473226 - - CLOUDFLARE_PURGE_ENABLED=true - - CLAMAV_ENABLED=true - - CLAMAV_HOST=clamav - - CLAMAV_PORT=3310 - - GEOIP_HOST=fluxer-geoip_app:8080 - - GEOIP_PROVIDER=maxmind - - MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb - - VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY - volumes: - - /opt/geoip/GeoLite2-City.mmdb:/data/GeoLite2-City.mmdb:ro - deploy: - replicas: 2 - 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 - labels: - - 'caddy=api.canary.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - '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=\"https://o4510149383094272.ingest.us.sentry.io/api/4510205804019712/security/?sentry_key=bb16e8b823b82d788db49a666b3b4b90\""' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml fluxer-api-canary - docker service update --image ${IMAGE_TAG} fluxer-api-canary_app - EOF diff --git a/.github/workflows/deploy-api-worker.yaml b/.github/workflows/deploy-api-worker.yaml deleted file mode 100644 index 3443ddfc..00000000 --- a/.github/workflows/deploy-api-worker.yaml +++ /dev/null @@ -1,187 +0,0 @@ -name: deploy api-worker - -on: - push: - branches: - - main - paths: - - fluxer_api/**/* - - .github/workflows/deploy-api-worker.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-api-worker - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy worker - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - 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: fluxer_api - file: fluxer_api/Dockerfile - tags: fluxer-api-worker:${{ github.sha }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-api-worker - cache-to: type=gha,mode=max,scope=deploy-fluxer-api-worker - 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: fluxer-api-worker:${{ github.sha }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-api-worker - sudo chown -R ${USER}:${USER} /opt/fluxer-api-worker - cd /opt/fluxer-api-worker - - cat > compose.yaml << COMPOSEEOF - services: - worker: - image: ${IMAGE_TAG} - command: ['npm', 'run', 'start:worker'] - env_file: - - /etc/fluxer/fluxer.env - environment: - - NODE_ENV=production - - FLUXER_API_PORT=8080 - - SENTRY_DSN=https://bb16e8b823b82d788db49a666b3b4b90@o4510149383094272.ingest.us.sentry.io/4510205804019712 - - CASSANDRA_HOSTS=cassandra - - CASSANDRA_KEYSPACE=fluxer - - CASSANDRA_LOCAL_DC=dc1 - - FLUXER_GATEWAY_RPC_HOST=fluxer-gateway_app - - FLUXER_GATEWAY_RPC_PORT=8081 - - FLUXER_MEDIA_PROXY_HOST=fluxer-media-proxy_app - - FLUXER_MEDIA_PROXY_PORT=8080 - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - FLUXER_API_CLIENT_ENDPOINT=https://web.fluxer.app/api - - FLUXER_APP_ENDPOINT=https://web.fluxer.app - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com - - FLUXER_INVITE_ENDPOINT=https://fluxer.gg - - FLUXER_GIFT_ENDPOINT=https://fluxer.gift - - AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us - - AWS_S3_BUCKET_CDN=fluxer - - AWS_S3_BUCKET_UPLOADS=fluxer-uploads - - AWS_S3_BUCKET_REPORTS=fluxer-reports - - AWS_S3_BUCKET_HARVESTS=fluxer-harvests - - AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads - - SENDGRID_FROM_EMAIL=noreply@fluxer.app - - SENDGRID_FROM_NAME=Fluxer - - SENDGRID_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoeqQS37o9s8ZcLBJUtT4hghAmI5RqsvcQ0OvsUn3XPfl7GkjxljufyxuL8+m1mCHP2IA1jdYT3kJQoQYXP6ZpQ== - - FLUXER_API_PUBLIC_ENDPOINT=https://api.fluxer.app - - FLUXER_GATEWAY_ENDPOINT=wss://gateway.fluxer.app - - FLUXER_MARKETING_ENDPOINT=https://fluxer.app - - FLUXER_PATH_MARKETING=/ - - FLUXER_ADMIN_ENDPOINT=https://admin.fluxer.app - - FLUXER_PATH_ADMIN=/ - - ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 - - ADMIN_OAUTH2_REDIRECT_URI=https://admin.fluxer.app/oauth2_callback - - ADMIN_OAUTH2_AUTO_CREATE=false - - PASSKEYS_ENABLED=true - - PASSKEY_RP_NAME=Fluxer - - PASSKEY_RP_ID=fluxer.app - - PASSKEY_ALLOWED_ORIGINS=https://web.fluxer.app,https://web.canary.fluxer.app - - CAPTCHA_ENABLED=true - - CAPTCHA_PRIMARY_PROVIDER=turnstile - - HCAPTCHA_SITE_KEY=9cbad400-df84-4e0c-bda6-e65000be78aa - - TURNSTILE_SITE_KEY=0x4AAAAAAB_lAoDdTWznNHMq - - EMAIL_ENABLED=true - - SMS_ENABLED=true - - VOICE_ENABLED=true - - SEARCH_ENABLED=true - - MEILISEARCH_URL=http://meilisearch:7700 - - STRIPE_ENABLED=true - - STRIPE_PRICE_ID_MONTHLY_USD=price_1SJHZzFPC94Os7FdzBgvz0go - - STRIPE_PRICE_ID_YEARLY_USD=price_1SJHabFPC94Os7FdhSOWVfcr - - STRIPE_PRICE_ID_VISIONARY_USD=price_1SJHGTFPC94Os7FdWTyqvJZ8 - - STRIPE_PRICE_ID_MONTHLY_EUR=price_1SJHaFFPC94Os7FdmcrGicXa - - STRIPE_PRICE_ID_YEARLY_EUR=price_1SJHarFPC94Os7Fddbyzr5I8 - - STRIPE_PRICE_ID_VISIONARY_EUR=price_1SJHGnFPC94Os7FdZn23KkYB - - STRIPE_PRICE_ID_GIFT_VISIONARY_USD=price_1SKhWqFPC94Os7FdxRmQrg3k - - STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=price_1SKhXrFPC94Os7FdcepLrJqr - - STRIPE_PRICE_ID_GIFT_1_MONTH_USD=price_1SJHHKFPC94Os7FdGwUs1EQg - - STRIPE_PRICE_ID_GIFT_1_YEAR_USD=price_1SJHHrFPC94Os7FdWrQN5tKl - - STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=price_1SJHHaFPC94Os7FdwwpwhliW - - STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=price_1SJHI5FPC94Os7Fd3DpLxb0D - - FLUXER_VISIONARIES_GUILD_ID=1428504839258075143 - - FLUXER_OPERATORS_GUILD_ID=1434192442151473226 - - CLOUDFLARE_PURGE_ENABLED=true - - CLAMAV_ENABLED=true - - CLAMAV_HOST=clamav - - CLAMAV_PORT=3310 - - GEOIP_HOST=fluxer-geoip_app:8080 - - GEOIP_PROVIDER=maxmind - - MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb - - VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY - volumes: - - /opt/geoip/GeoLite2-City.mmdb:/data/GeoLite2-City.mmdb:ro - deploy: - replicas: 1 - 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 - networks: - - fluxer-shared - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml fluxer-api-worker - docker service update --image ${IMAGE_TAG} fluxer-api-worker_worker - EOF diff --git a/.github/workflows/deploy-api.yaml b/.github/workflows/deploy-api.yaml index 2274ce92..fa361f0b 100644 --- a/.github/workflows/deploy-api.yaml +++ b/.github/workflows/deploy-api.yaml @@ -4,25 +4,80 @@ on: push: branches: - main + - canary paths: - - fluxer_api/**/* + - fluxer_api/** - .github/workflows/deploy-api.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-api + group: deploy-fluxer-api-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} cancel-in-progress: true permissions: contents: read 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 + name: Deploy api + 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 }} + + STACK: ${{ format('fluxer-api{0}', needs.channel-vars.outputs.stack_suffix) }} + WORKER_STACK: ${{ format('fluxer-api-worker{0}', needs.channel-vars.outputs.stack_suffix) }} + CACHE_SCOPE: ${{ format('deploy-fluxer-api{0}', needs.channel-vars.outputs.stack_suffix) }} + + API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }} + API_CLIENT_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app/api' || 'https://web.fluxer.app/api' }} + APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }} + + MARKETING_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://canary.fluxer.app' || 'https://fluxer.app' }} + + ADMIN_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app' || 'https://admin.fluxer.app' }} + ADMIN_REDIRECT_URI: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app/oauth2_callback' || 'https://admin.fluxer.app/oauth2_callback' }} + + CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'api.canary.fluxer.app' || 'api.fluxer.app' }} + steps: - uses: actions/checkout@v6 + with: + ref: ${{ env.SOURCE_REF }} + fetch-depth: 0 + + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -33,16 +88,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Build image + - name: Build image(s) uses: docker/build-push-action@v6 with: context: fluxer_api file: fluxer_api/Dockerfile - tags: fluxer-api:${{ github.sha }} + tags: | + ${{ env.STACK }}:${{ env.DEPLOY_SHA }} + ${{ env.WORKER_STACK }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-api - cache-to: type=gha,mode=max,scope=deploy-fluxer-api + 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 @@ -66,137 +123,237 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts - - name: Push image and deploy + - name: Push image(s) and deploy env: - IMAGE_TAG: fluxer-api:${{ github.sha }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} + IMAGE_TAG_APP: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} + IMAGE_TAG_WORKER: ${{ env.WORKER_STACK }}:${{ env.DEPLOY_SHA }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-api - sudo chown -R ${USER}:${USER} /opt/fluxer-api - cd /opt/fluxer-api + docker pussh "${IMAGE_TAG_APP}" "${SERVER}" + + if [[ "${IS_CANARY}" == "true" ]]; then + docker pussh "${IMAGE_TAG_WORKER}" "${SERVER}" + fi + + ssh "${SERVER}" \ + "IMAGE_TAG_APP=${IMAGE_TAG_APP} IMAGE_TAG_WORKER=${IMAGE_TAG_WORKER} STACK=${STACK} WORKER_STACK=${WORKER_STACK} IS_CANARY=${IS_CANARY} APP_ENDPOINT=${APP_ENDPOINT} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} API_CLIENT_ENDPOINT=${API_CLIENT_ENDPOINT} MARKETING_ENDPOINT=${MARKETING_ENDPOINT} ADMIN_ENDPOINT=${ADMIN_ENDPOINT} ADMIN_REDIRECT_URI=${ADMIN_REDIRECT_URI} CADDY_DOMAIN=${CADDY_DOMAIN} bash" << 'EOF' + set -euo pipefail + + write_runtime_env() { + local dir="$1" + cat > "${dir}/runtime.env" << ENVEOF + NODE_ENV=production + FLUXER_API_PORT=8080 + SENTRY_DSN=https://bb16e8b823b82d788db49a666b3b4b90@o4510149383094272.ingest.us.sentry.io/4510205804019712 + + CASSANDRA_HOSTS=cassandra + CASSANDRA_KEYSPACE=fluxer + CASSANDRA_LOCAL_DC=dc1 + + FLUXER_GATEWAY_RPC_HOST=fluxer-gateway_app + FLUXER_GATEWAY_RPC_PORT=8081 + + FLUXER_MEDIA_PROXY_HOST=fluxer-media-proxy_app + FLUXER_MEDIA_PROXY_PORT=8080 + + FLUXER_METRICS_HOST=fluxer-metrics_app:8080 + + FLUXER_API_CLIENT_ENDPOINT=${API_CLIENT_ENDPOINT} + FLUXER_APP_ENDPOINT=${APP_ENDPOINT} + FLUXER_CDN_ENDPOINT=https://fluxerstatic.com + FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com + FLUXER_INVITE_ENDPOINT=https://fluxer.gg + FLUXER_GIFT_ENDPOINT=https://fluxer.gift + + AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us + AWS_S3_BUCKET_CDN=fluxer + AWS_S3_BUCKET_UPLOADS=fluxer-uploads + AWS_S3_BUCKET_REPORTS=fluxer-reports + AWS_S3_BUCKET_HARVESTS=fluxer-harvests + AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads + + SENDGRID_FROM_EMAIL=noreply@fluxer.app + SENDGRID_FROM_NAME=Fluxer + SENDGRID_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoeqQS37o9s8ZcLBJUtT4hghAmI5RqsvcQ0OvsUn3XPfl7GkjxljufyxuL8+m1mCHP2IA1jdYT3kJQoQYXP6ZpQ== + + FLUXER_API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} + FLUXER_GATEWAY_ENDPOINT=wss://gateway.fluxer.app + + FLUXER_MARKETING_ENDPOINT=${MARKETING_ENDPOINT} + FLUXER_PATH_MARKETING=/ + + FLUXER_ADMIN_ENDPOINT=${ADMIN_ENDPOINT} + FLUXER_PATH_ADMIN=/ + + ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 + ADMIN_OAUTH2_REDIRECT_URI=${ADMIN_REDIRECT_URI} + ADMIN_OAUTH2_AUTO_CREATE=false + + PASSKEYS_ENABLED=true + PASSKEY_RP_NAME=Fluxer + PASSKEY_RP_ID=fluxer.app + PASSKEY_ALLOWED_ORIGINS=https://web.fluxer.app,https://web.canary.fluxer.app + + CAPTCHA_ENABLED=true + CAPTCHA_PRIMARY_PROVIDER=turnstile + HCAPTCHA_SITE_KEY=9cbad400-df84-4e0c-bda6-e65000be78aa + TURNSTILE_SITE_KEY=0x4AAAAAAB_lAoDdTWznNHMq + + EMAIL_ENABLED=true + SMS_ENABLED=true + VOICE_ENABLED=true + + SEARCH_ENABLED=true + MEILISEARCH_URL=http://meilisearch:7700 + + STRIPE_ENABLED=true + STRIPE_PRICE_ID_MONTHLY_USD=price_1SJHZzFPC94Os7FdzBgvz0go + STRIPE_PRICE_ID_YEARLY_USD=price_1SJHabFPC94Os7FdhSOWVfcr + STRIPE_PRICE_ID_VISIONARY_USD=price_1SJHGTFPC94Os7FdWTyqvJZ8 + STRIPE_PRICE_ID_MONTHLY_EUR=price_1SJHaFFPC94Os7FdmcrGicXa + STRIPE_PRICE_ID_YEARLY_EUR=price_1SJHarFPC94Os7Fddbyzr5I8 + STRIPE_PRICE_ID_VISIONARY_EUR=price_1SJHGnFPC94Os7FdZn23KkYB + STRIPE_PRICE_ID_GIFT_VISIONARY_USD=price_1SKhWqFPC94Os7FdxRmQrg3k + STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=price_1SKhXrFPC94Os7FdcepLrJqr + STRIPE_PRICE_ID_GIFT_1_MONTH_USD=price_1SJHHKFPC94Os7FdGwUs1EQg + STRIPE_PRICE_ID_GIFT_1_YEAR_USD=price_1SJHHrFPC94Os7FdWrQN5tKl + STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=price_1SJHHaFPC94Os7FdwwpwhliW + STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=price_1SJHI5FPC94Os7Fd3DpLxb0D + + FLUXER_VISIONARIES_GUILD_ID=1428504839258075143 + FLUXER_OPERATORS_GUILD_ID=1434192442151473226 + + CLOUDFLARE_PURGE_ENABLED=true + + CLAMAV_ENABLED=true + CLAMAV_HOST=clamav + CLAMAV_PORT=3310 + + GEOIP_HOST=fluxer-geoip_app:8080 + GEOIP_PROVIDER=maxmind + MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb + + VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY + ENVEOF + } + + deploy_api_stack() { + sudo mkdir -p "/opt/${STACK}" + sudo chown -R "${USER}:${USER}" "/opt/${STACK}" + cd "/opt/${STACK}" + + write_runtime_env "$(pwd)" 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-healthcheck: &healthcheck + test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + services: app: - image: ${IMAGE_TAG} + image: ${IMAGE_TAG_APP} command: ['npm', 'run', 'start'] env_file: - /etc/fluxer/fluxer.env - environment: - - NODE_ENV=production - - FLUXER_API_PORT=8080 - - SENTRY_DSN=https://bb16e8b823b82d788db49a666b3b4b90@o4510149383094272.ingest.us.sentry.io/4510205804019712 - - CASSANDRA_HOSTS=cassandra - - CASSANDRA_KEYSPACE=fluxer - - CASSANDRA_LOCAL_DC=dc1 - - FLUXER_GATEWAY_RPC_HOST=fluxer-gateway_app - - FLUXER_GATEWAY_RPC_PORT=8081 - - FLUXER_MEDIA_PROXY_HOST=fluxer-media-proxy_app - - FLUXER_MEDIA_PROXY_PORT=8080 - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - FLUXER_API_CLIENT_ENDPOINT=https://web.fluxer.app/api - - FLUXER_APP_ENDPOINT=https://web.fluxer.app - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com - - FLUXER_INVITE_ENDPOINT=https://fluxer.gg - - FLUXER_GIFT_ENDPOINT=https://fluxer.gift - - AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us - - AWS_S3_BUCKET_CDN=fluxer - - AWS_S3_BUCKET_UPLOADS=fluxer-uploads - - AWS_S3_BUCKET_REPORTS=fluxer-reports - - AWS_S3_BUCKET_HARVESTS=fluxer-harvests - - AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads - - SENDGRID_FROM_EMAIL=noreply@fluxer.app - - SENDGRID_FROM_NAME=Fluxer - - SENDGRID_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoeqQS37o9s8ZcLBJUtT4hghAmI5RqsvcQ0OvsUn3XPfl7GkjxljufyxuL8+m1mCHP2IA1jdYT3kJQoQYXP6ZpQ== - - FLUXER_API_PUBLIC_ENDPOINT=https://api.fluxer.app - - FLUXER_GATEWAY_ENDPOINT=wss://gateway.fluxer.app - - FLUXER_MARKETING_ENDPOINT=https://fluxer.app - - FLUXER_PATH_MARKETING=/ - - FLUXER_ADMIN_ENDPOINT=https://admin.fluxer.app - - FLUXER_PATH_ADMIN=/ - - ADMIN_OAUTH2_CLIENT_ID=1440355698178071552 - - ADMIN_OAUTH2_REDIRECT_URI=https://admin.fluxer.app/oauth2_callback - - ADMIN_OAUTH2_AUTO_CREATE=false - - PASSKEYS_ENABLED=true - - PASSKEY_RP_NAME=Fluxer - - PASSKEY_RP_ID=fluxer.app - - PASSKEY_ALLOWED_ORIGINS=https://web.fluxer.app,https://web.canary.fluxer.app - - CAPTCHA_ENABLED=true - - CAPTCHA_PRIMARY_PROVIDER=turnstile - - HCAPTCHA_SITE_KEY=9cbad400-df84-4e0c-bda6-e65000be78aa - - TURNSTILE_SITE_KEY=0x4AAAAAAB_lAoDdTWznNHMq - - EMAIL_ENABLED=true - - SMS_ENABLED=true - - VOICE_ENABLED=true - - SEARCH_ENABLED=true - - MEILISEARCH_URL=http://meilisearch:7700 - - STRIPE_ENABLED=true - - STRIPE_PRICE_ID_MONTHLY_USD=price_1SJHZzFPC94Os7FdzBgvz0go - - STRIPE_PRICE_ID_YEARLY_USD=price_1SJHabFPC94Os7FdhSOWVfcr - - STRIPE_PRICE_ID_VISIONARY_USD=price_1SJHGTFPC94Os7FdWTyqvJZ8 - - STRIPE_PRICE_ID_MONTHLY_EUR=price_1SJHaFFPC94Os7FdmcrGicXa - - STRIPE_PRICE_ID_YEARLY_EUR=price_1SJHarFPC94Os7Fddbyzr5I8 - - STRIPE_PRICE_ID_VISIONARY_EUR=price_1SJHGnFPC94Os7FdZn23KkYB - - STRIPE_PRICE_ID_GIFT_VISIONARY_USD=price_1SKhWqFPC94Os7FdxRmQrg3k - - STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=price_1SKhXrFPC94Os7FdcepLrJqr - - STRIPE_PRICE_ID_GIFT_1_MONTH_USD=price_1SJHHKFPC94Os7FdGwUs1EQg - - STRIPE_PRICE_ID_GIFT_1_YEAR_USD=price_1SJHHrFPC94Os7FdWrQN5tKl - - STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=price_1SJHHaFPC94Os7FdwwpwhliW - - STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=price_1SJHI5FPC94Os7Fd3DpLxb0D - - FLUXER_VISIONARIES_GUILD_ID=1428504839258075143 - - FLUXER_OPERATORS_GUILD_ID=1434192442151473226 - - CLOUDFLARE_PURGE_ENABLED=true - - CLAMAV_ENABLED=true - - CLAMAV_HOST=clamav - - CLAMAV_PORT=3310 - - GEOIP_HOST=fluxer-geoip_app:8080 - - GEOIP_PROVIDER=maxmind - - MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb - - VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY + - ./runtime.env volumes: - /opt/geoip/GeoLite2-City.mmdb:/data/GeoLite2-City.mmdb:ro deploy: + <<: *deploy_base replicas: 2 - 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 labels: - - 'caddy=api.fluxer.app' + - "caddy=${CADDY_DOMAIN}" - 'caddy.reverse_proxy={{upstreams 8080}}' - '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=\"https://o4510149383094272.ingest.us.sentry.io/api/4510205804019712/security/?sentry_key=bb16e8b823b82d788db49a666b3b4b90\""' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + - 'caddy.header.Expect-Ct="max-age=86400, report-uri=\\"https://o4510149383094272.ingest.us.sentry.io/api/4510205804019712/security/?sentry_key=bb16e8b823b82d788db49a666b3b4b90\\""' + networks: [fluxer-shared] + healthcheck: *healthcheck networks: fluxer-shared: external: true COMPOSEEOF - docker stack deploy -c compose.yaml fluxer-api - docker service update --image ${IMAGE_TAG} fluxer-api_app + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${STACK}" + } + + deploy_worker_stack_canary_only() { + if [[ "${IS_CANARY}" != "true" ]]; then + return 0 + fi + + sudo mkdir -p "/opt/${WORKER_STACK}" + sudo chown -R "${USER}:${USER}" "/opt/${WORKER_STACK}" + cd "/opt/${WORKER_STACK}" + + write_runtime_env "$(pwd)" + + 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 + + services: + worker: + image: ${IMAGE_TAG_WORKER} + command: ['npm', 'run', 'start:worker'] + env_file: + - /etc/fluxer/fluxer.env + - ./runtime.env + deploy: + <<: *deploy_base + replicas: 1 + networks: [fluxer-shared] + + networks: + fluxer-shared: + external: true + COMPOSEEOF + + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${WORKER_STACK}" + } + + deploy_api_stack + deploy_worker_stack_canary_only EOF diff --git a/.github/workflows/deploy-app-canary.yaml b/.github/workflows/deploy-app-canary.yaml deleted file mode 100644 index 708ec546..00000000 --- a/.github/workflows/deploy-app-canary.yaml +++ /dev/null @@ -1,318 +0,0 @@ -name: deploy app (canary) - -on: - push: - branches: - - canary - paths: - - fluxer_app/**/* - - .github/workflows/deploy-app-canary.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-app-canary - cancel-in-progress: true - -permissions: - contents: write - -env: - SERVICE_NAME: fluxer-app-canary - IMAGE_NAME: fluxer-app-canary - COMPOSE_STACK: fluxer-app-canary - DOCKERFILE: fluxer_app/proxy/Dockerfile - SENTRY_PROXY_PATH: /error-reporting-proxy - DEPLOY_BRANCH: ${{ github.event_name == 'workflow_dispatch' && 'canary' || github.ref_name }} - -jobs: - deploy: - name: Deploy app (canary) - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/canary' || github.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: https://web.canary.fluxer.app/api - PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://api.canary.fluxer.app - PUBLIC_API_VERSION: 1 - PUBLIC_PROJECT_ENV: canary - PUBLIC_SENTRY_PROJECT_ID: 4510205815291904 - PUBLIC_SENTRY_PUBLIC_KEY: 59ced0e2666ab83dd1ddb056cdd22d1b - PUBLIC_SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.canary.fluxer.app/4510205815291904 - 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": "canary" - } - 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.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=${{ env.SERVICE_NAME }} - cache-to: type=gha,mode=max,scope=${{ env.SERVICE_NAME }} - 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.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@o4510149383094272.ingest.us.sentry.io/4510205815291904 - SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }} - SENTRY_REPORT_HOST: https://sentry.web.canary.fluxer.app - 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}" 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 - services: - app: - image: ${IMAGE_TAG} - deploy: - replicas: 1 - 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 - labels: - - 'caddy=web.canary.fluxer.app' - - 'caddy.handle_path_0=/api*' - - 'caddy.handle_path_0.reverse_proxy=http://fluxer-api-canary_app:8080' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - - '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' - environment: - - PORT=8080 - - RELEASE_CHANNEL=canary - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - SENTRY_DSN=${SENTRY_DSN} - - SENTRY_PROXY_PATH=${SENTRY_PROXY_PATH} - - SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - sentry: - image: ${IMAGE_TAG} - deploy: - replicas: 1 - 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 - labels: - - 'caddy=sentry.web.canary.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - - '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' - environment: - - PORT=8080 - - RELEASE_CHANNEL=canary - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - SENTRY_DSN=${SENTRY_DSN} - - SENTRY_PROXY_PATH=/ - - SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml ${COMPOSE_STACK} - docker service update --image ${IMAGE_TAG} fluxer-app-canary_app - docker service update --image ${IMAGE_TAG} fluxer-app-canary_sentry - EOF diff --git a/.github/workflows/deploy-app.yaml b/.github/workflows/deploy-app.yaml index 8f69db2d..787a1cc5 100644 --- a/.github/workflows/deploy-app.yaml +++ b/.github/workflows/deploy-app.yaml @@ -4,35 +4,76 @@ on: push: branches: - main + - canary paths: - - fluxer_app/**/* + - 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-stable + group: deploy-fluxer-app-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} cancel-in-progress: true permissions: contents: write -env: - SERVICE_NAME: fluxer-app-stable - IMAGE_NAME: fluxer-app-stable - COMPOSE_STACK: fluxer-app-stable - DOCKERFILE: fluxer_app/proxy/Dockerfile - SENTRY_PROXY_PATH: /error-reporting-proxy - DEPLOY_BRANCH: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.ref_name }} - 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 (stable) + 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: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/main' || github.ref }} + ref: ${{ env.SOURCE_REF }} fetch-depth: 0 - name: Set up pnpm @@ -100,13 +141,13 @@ jobs: working-directory: fluxer_app env: NODE_ENV: production - PUBLIC_BOOTSTRAP_API_ENDPOINT: https://web.fluxer.app/api - PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://api.fluxer.app + 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: stable + PUBLIC_PROJECT_ENV: ${{ env.PUBLIC_PROJECT_ENV }} PUBLIC_SENTRY_PROJECT_ID: 4510205815291904 PUBLIC_SENTRY_PUBLIC_KEY: 59ced0e2666ab83dd1ddb056cdd22d1b - PUBLIC_SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.fluxer.app/4510205815291904 + PUBLIC_SENTRY_DSN: ${{ env.PUBLIC_SENTRY_DSN }} PUBLIC_SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }} PUBLIC_BUILD_NUMBER: ${{ github.run_number }} run: | @@ -119,7 +160,7 @@ jobs: "sha": "$PUBLIC_BUILD_SHA", "buildNumber": $PUBLIC_BUILD_NUMBER, "timestamp": $PUBLIC_BUILD_TIMESTAMP, - "env": "stable" + "env": "$PUBLIC_PROJECT_ENV" } EOF @@ -169,11 +210,11 @@ jobs: with: context: . file: ${{ env.DOCKERFILE }} - tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} + tags: ${{ env.SERVICE_NAME }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 - cache-from: type=gha,scope=${{ env.SERVICE_NAME }} - cache-to: type=gha,mode=max,scope=${{ env.SERVICE_NAME }} + 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 @@ -199,118 +240,114 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} + 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: https://sentry.web.fluxer.app + 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} + 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}" bash << 'EOF' - set -euo pipefail - sudo mkdir -p /opt/${SERVICE_NAME} - sudo chown -R ${USER}:${USER} /opt/${SERVICE_NAME} - cd /opt/${SERVICE_NAME} + 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 - cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} deploy: - replicas: 2 - 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 + <<: *deploy_base + replicas: ${APP_REPLICAS} labels: - - 'caddy=web.fluxer.app' - - 'caddy.handle_path_0=/api*' - - 'caddy.handle_path_0.reverse_proxy=http://fluxer-api_app:8080' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - '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' + <<: *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: - - PORT=8080 - - RELEASE_CHANNEL=stable - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - SENTRY_DSN=${SENTRY_DSN} - - SENTRY_PROXY_PATH=${SENTRY_PROXY_PATH} - - SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + <<: *env_base + SENTRY_PROXY_PATH: ${SENTRY_PROXY_PATH} + networks: [fluxer-shared] + healthcheck: *healthcheck sentry: image: ${IMAGE_TAG} deploy: + <<: *deploy_base replicas: 1 - 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 labels: - - 'caddy=sentry.web.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - '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' + <<: *common_caddy_headers + caddy: ${SENTRY_CADDY_DOMAIN} + caddy.reverse_proxy: "{{upstreams 8080}}" environment: - - PORT=8080 - - RELEASE_CHANNEL=stable - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - SENTRY_DSN=${SENTRY_DSN} - - SENTRY_PROXY_PATH=/ - - SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + <<: *env_base + SENTRY_PROXY_PATH: / + networks: [fluxer-shared] + healthcheck: *healthcheck networks: fluxer-shared: external: true COMPOSEEOF - docker stack deploy -c compose.yaml ${COMPOSE_STACK} - docker service update --image ${IMAGE_TAG} fluxer-app-stable_app - docker service update --image ${IMAGE_TAG} fluxer-app-stable_sentry + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${COMPOSE_STACK}" EOF diff --git a/.github/workflows/deploy-docs-canary.yaml b/.github/workflows/deploy-docs-canary.yaml deleted file mode 100644 index 44928280..00000000 --- a/.github/workflows/deploy-docs-canary.yaml +++ /dev/null @@ -1,129 +0,0 @@ -name: deploy docs (canary) - -on: - push: - branches: - - canary - paths: - - fluxer_docs/**/* - - .github/workflows/deploy-docs-canary.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-docs-canary - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy app (canary) - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - 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: fluxer_docs - file: fluxer_docs/Dockerfile - tags: fluxer-docs-canary:${{ github.sha }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-docs-canary - cache-to: type=gha,mode=max,scope=deploy-fluxer-docs-canary - 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: fluxer-docs-canary:${{ github.sha }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-docs-canary - sudo chown -R ${USER}:${USER} /opt/fluxer-docs-canary - cd /opt/fluxer-docs-canary - - cat > compose.yaml << COMPOSEEOF - services: - app: - image: ${IMAGE_TAG} - env_file: - - /etc/fluxer/fluxer.env - environment: - - NODE_ENV=production - deploy: - replicas: 2 - 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 - labels: - - 'caddy=docs.canary.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 3000}}' - - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - - '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' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:3000'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml fluxer-docs-canary - docker service update --image ${IMAGE_TAG} fluxer-docs-canary_app - EOF diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index ca33900a..1ff9a6ab 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -4,25 +4,70 @@ on: push: branches: - main + - canary paths: - - fluxer_docs/**/* + - fluxer_docs/** - .github/workflows/deploy-docs.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-docs + group: deploy-fluxer-docs-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} cancel-in-progress: true permissions: contents: read 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 + name: Deploy docs + 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 }} + + STACK: ${{ format('fluxer-docs{0}', needs.channel-vars.outputs.stack_suffix) }} + CACHE_SCOPE: ${{ format('deploy-fluxer-docs{0}', needs.channel-vars.outputs.stack_suffix) }} + + CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'docs.canary.fluxer.app' || 'docs.fluxer.app' }} + steps: - uses: actions/checkout@v6 + with: + ref: ${{ env.SOURCE_REF }} + fetch-depth: 0 + + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -38,11 +83,11 @@ jobs: with: context: fluxer_docs file: fluxer_docs/Dockerfile - tags: fluxer-docs:${{ github.sha }} + tags: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-docs - cache-to: type=gha,mode=max,scope=deploy-fluxer-docs + 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 @@ -68,19 +113,36 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: fluxer-docs:${{ github.sha }} + IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} + STACK: ${{ env.STACK }} + CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }} + IS_CANARY: ${{ env.IS_CANARY }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-docs - sudo chown -R ${USER}:${USER} /opt/fluxer-docs - cd /opt/fluxer-docs + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} CADDY_DOMAIN=${CADDY_DOMAIN} IS_CANARY=${IS_CANARY} bash" << 'EOF' + set -euo pipefail + + sudo mkdir -p "/opt/${STACK}" + sudo chown -R "${USER}:${USER}" "/opt/${STACK}" + cd "/opt/${STACK}" + + 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 - cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} @@ -89,26 +151,25 @@ jobs: environment: - NODE_ENV=production deploy: + <<: *deploy_base replicas: 2 - 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 - labels: - - 'caddy=docs.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 3000}}' - - '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' + labels: + caddy: "${CADDY_DOMAIN}" + caddy.reverse_proxy: "{{upstreams 3000}}" + COMPOSEEOF + + if [[ "${IS_CANARY}" == "true" ]]; then + cat >> compose.yaml << 'COMPOSEEOF' + caddy.header.X-Robots-Tag: "noindex, nofollow, nosnippet, noimageindex" + COMPOSEEOF + fi + + cat >> compose.yaml << 'COMPOSEEOF' + 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" networks: - fluxer-shared healthcheck: @@ -123,6 +184,10 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml fluxer-docs - docker service update --image ${IMAGE_TAG} fluxer-docs_app + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${STACK}" EOF diff --git a/.github/workflows/deploy-gateway.yaml b/.github/workflows/deploy-gateway.yaml index 9e91e526..1a82aa8b 100644 --- a/.github/workflows/deploy-gateway.yaml +++ b/.github/workflows/deploy-gateway.yaml @@ -5,7 +5,6 @@ on: push: branches: - canary - - main paths: - 'fluxer_gateway/**' @@ -23,15 +22,15 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: sparse-checkout: fluxer_gateway - name: Set up Erlang uses: erlef/setup-beam@v1 with: - otp-version: "28" - rebar3-version: "3.24.0" + otp-version: '28' + rebar3-version: '3.24.0' - name: Compile working-directory: fluxer_gateway diff --git a/.github/workflows/deploy-geoip.yaml b/.github/workflows/deploy-geoip.yaml index ec964536..46d89bac 100644 --- a/.github/workflows/deploy-geoip.yaml +++ b/.github/workflows/deploy-geoip.yaml @@ -5,9 +5,9 @@ on: branches: - main paths: - - fluxer_geoip/**/* + - fluxer_geoip/** - .github/workflows/deploy-geoip.yaml - workflow_dispatch: + workflow_dispatch: {} concurrency: group: deploy-fluxer-geoip @@ -24,6 +24,13 @@ jobs: steps: - uses: actions/checkout@v6 + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -38,7 +45,7 @@ jobs: with: context: fluxer_geoip file: fluxer_geoip/Dockerfile - tags: fluxer-geoip:${{ github.sha }} + tags: fluxer-geoip:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 cache-from: type=gha,scope=deploy-fluxer-geoip @@ -68,19 +75,20 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: fluxer-geoip:${{ github.sha }} + IMAGE_TAG: fluxer-geoip:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} bash << EOF - set -e - sudo mkdir -p /opt/fluxer-geoip - sudo chown -R \${USER}:\${USER} /opt/fluxer-geoip - cd /opt/fluxer-geoip + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} bash" << 'EOF' + set -euo pipefail - cat > compose.yaml << 'COMPOSEEOF' + sudo mkdir -p /opt/fluxer-geoip + sudo chown -R "${USER}:${USER}" /opt/fluxer-geoip + cd /opt/fluxer-geoip + + cat > compose.yaml << 'COMPOSEEOF' services: app: image: ${IMAGE_TAG} @@ -112,6 +120,5 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml fluxer-geoip - docker service update --image ${IMAGE_TAG} fluxer-geoip_app + docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml fluxer-geoip EOF diff --git a/.github/workflows/deploy-marketing-canary.yaml b/.github/workflows/deploy-marketing-canary.yaml deleted file mode 100644 index 36adc9fd..00000000 --- a/.github/workflows/deploy-marketing-canary.yaml +++ /dev/null @@ -1,148 +0,0 @@ -name: deploy marketing (canary) - -on: - push: - branches: - - canary - paths: - - fluxer_marketing/**/* - - .github/workflows/deploy-marketing-canary.yaml - workflow_dispatch: - -concurrency: - group: deploy-fluxer-marketing-canary - cancel-in-progress: true - -jobs: - deploy: - name: Deploy app (canary) - runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/canary' || github.ref }} - - - 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 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: fluxer_marketing - file: fluxer_marketing/Dockerfile - tags: fluxer-marketing-canary:${{ env.DEPLOY_SHA }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-marketing-canary - cache-to: type=gha,mode=max,scope=deploy-fluxer-marketing-canary - build-args: | - BUILD_TIMESTAMP=${{ github.run_id }} - 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: fluxer-marketing-canary:${{ env.DEPLOY_SHA }} - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | - set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} - - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-marketing-canary - sudo chown -R ${USER}:${USER} /opt/fluxer-marketing-canary - cd /opt/fluxer-marketing-canary - - cat > compose.yaml << COMPOSEEOF - services: - app: - image: ${IMAGE_TAG} - env_file: - - /etc/fluxer/fluxer.env - environment: - - FLUXER_API_PUBLIC_ENDPOINT=https://api.canary.fluxer.app - - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 - - FLUXER_API_HOST=fluxer-api-canary_app:8080 - - FLUXER_APP_ENDPOINT=https://web.canary.fluxer.app - - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_MARKETING_ENDPOINT=https://canary.fluxer.app - - FLUXER_MARKETING_PORT=8080 - - FLUXER_PATH_MARKETING=/ - - GEOIP_HOST=fluxer-geoip_app:8080 - - RELEASE_CHANNEL=canary - deploy: - replicas: 1 - 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 - labels: - - 'caddy=canary.fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"' - - '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.@channels.path=/channels /channels/*' - - 'caddy.redir=@channels https://web.canary.fluxer.app{uri}' - networks: - - fluxer-shared - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:8080/'] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - networks: - fluxer-shared: - external: true - COMPOSEEOF - - docker stack deploy -c compose.yaml fluxer-marketing-canary - docker service update --image ${IMAGE_TAG} fluxer-marketing-canary_app - EOF diff --git a/.github/workflows/deploy-marketing.yaml b/.github/workflows/deploy-marketing.yaml index 5b97d0f5..e600531e 100644 --- a/.github/workflows/deploy-marketing.yaml +++ b/.github/workflows/deploy-marketing.yaml @@ -4,27 +4,71 @@ on: push: branches: - main + - canary paths: - - fluxer_marketing/**/* + - fluxer_marketing/** - .github/workflows/deploy-marketing.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-marketing + group: deploy-fluxer-marketing-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }} cancel-in-progress: true +permissions: + contents: read + 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 + name: Deploy marketing + 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 }} + STACK: ${{ format('fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }} + IMAGE_NAME: ${{ format('fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }} + CACHE_SCOPE: ${{ format('deploy-fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }} + APP_REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }} + API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }} + API_HOST: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'fluxer-api-canary_app:8080' || 'fluxer-api_app:8080' }} + APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }} + MARKETING_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://canary.fluxer.app' || 'https://fluxer.app' }} + CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'canary.fluxer.app' || 'fluxer.app' }} + RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }} + steps: - uses: actions/checkout@v6 with: - ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/main' || github.ref }} + ref: ${{ env.SOURCE_REF }} + fetch-depth: 0 - name: Record deploy commit - run: | + run: |- set -euo pipefail sha=$(git rev-parse HEAD) echo "Deploying commit ${sha}" @@ -44,19 +88,18 @@ jobs: with: context: fluxer_marketing file: fluxer_marketing/Dockerfile - tags: fluxer-marketing:${{ env.DEPLOY_SHA }} + tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 - cache-from: type=gha,scope=deploy-fluxer-marketing - cache-to: type=gha,mode=max,scope=deploy-fluxer-marketing - build-args: | - BUILD_TIMESTAMP=${{ github.run_id }} + cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} + cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} + build-args: BUILD_TIMESTAMP=${{ github.run_id }} env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false - name: Install docker-pussh - run: | + run: |- set -euo pipefail mkdir -p ~/.docker/cli-plugins curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/v0.3.1/docker-pussh \ @@ -69,44 +112,55 @@ jobs: ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} - name: Add server to known hosts - run: | + run: |- set -euo pipefail mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts - name: Push image and deploy env: - IMAGE_TAG: fluxer-marketing:${{ env.DEPLOY_SHA }} + IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - run: | + STACK: ${{ env.STACK }} + IS_CANARY: ${{ env.IS_CANARY }} + API_PUBLIC_ENDPOINT: ${{ env.API_PUBLIC_ENDPOINT }} + API_HOST: ${{ env.API_HOST }} + APP_ENDPOINT: ${{ env.APP_ENDPOINT }} + MARKETING_ENDPOINT: ${{ env.MARKETING_ENDPOINT }} + CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }} + RELEASE_CHANNEL: ${{ env.RELEASE_CHANNEL }} + APP_REPLICAS: ${{ env.APP_REPLICAS }} + run: |- set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG}" bash << 'EOF' - set -e - sudo mkdir -p /opt/fluxer-marketing - sudo chown -R ${USER}:${USER} /opt/fluxer-marketing - cd /opt/fluxer-marketing + ssh "${SERVER}" \ + "IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} IS_CANARY=${IS_CANARY} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} API_HOST=${API_HOST} APP_ENDPOINT=${APP_ENDPOINT} MARKETING_ENDPOINT=${MARKETING_ENDPOINT} CADDY_DOMAIN=${CADDY_DOMAIN} RELEASE_CHANNEL=${RELEASE_CHANNEL} APP_REPLICAS=${APP_REPLICAS} bash" << 'EOF' + set -euo pipefail - cat > compose.yaml << COMPOSEEOF + sudo mkdir -p "/opt/${STACK}" + sudo chown -R "${USER}:${USER}" "/opt/${STACK}" + cd "/opt/${STACK}" + + cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} env_file: - /etc/fluxer/fluxer.env environment: - - FLUXER_API_PUBLIC_ENDPOINT=https://api.fluxer.app - - FLUXER_API_HOST=fluxer-api_app:8080 - - FLUXER_APP_ENDPOINT=https://web.fluxer.app + - FLUXER_API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} + - FLUXER_API_HOST=${API_HOST} + - FLUXER_APP_ENDPOINT=${APP_ENDPOINT} - FLUXER_CDN_ENDPOINT=https://fluxerstatic.com - - FLUXER_MARKETING_ENDPOINT=https://fluxer.app + - FLUXER_MARKETING_ENDPOINT=${MARKETING_ENDPOINT} - FLUXER_MARKETING_PORT=8080 - FLUXER_PATH_MARKETING=/ - GEOIP_HOST=fluxer-geoip_app:8080 - - RELEASE_CHANNEL=stable + - RELEASE_CHANNEL=${RELEASE_CHANNEL} - FLUXER_METRICS_HOST=fluxer-metrics_app:8080 deploy: - replicas: 2 + replicas: ${APP_REPLICAS} restart_policy: condition: on-failure delay: 5s @@ -119,33 +173,47 @@ jobs: parallelism: 1 delay: 10s labels: - - 'caddy=fluxer.app' - - 'caddy.reverse_proxy={{upstreams 8080}}' - - '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.redir_0=/channels/* https://web.fluxer.app{uri}' - - 'caddy.redir_1=/channels https://web.fluxer.app{uri}' - - 'caddy.redir_2=/delete-my-account https://fluxer.app/help/articles/1445724566704881664 302' - - 'caddy.redir_3=/delete-my-data https://fluxer.app/help/articles/1445730947679911936 302' - - 'caddy.redir_4=/export-my-data https://fluxer.app/help/articles/1445731738851475456 302' - - 'caddy.redir_5=/bugs https://fluxer.app/help/articles/1447264362996695040 302' - - 'caddy_1=www.fluxer.app' - - 'caddy_1.redir=https://fluxer.app{uri}' - - 'caddy_3=fluxer.gg' - - 'caddy_3.redir=https://web.fluxer.app/invite{uri}' - - 'caddy_4=fluxer.gift' - - 'caddy_4.redir=https://web.fluxer.app/gift{uri}' - - 'caddy_5=fluxerapp.com' - - 'caddy_5.redir=https://fluxer.app{uri}' - - 'caddy_6=www.fluxerapp.com' - - 'caddy_6.redir=https://fluxer.app{uri}' - - 'caddy_7=fluxer.dev' - - 'caddy_7.redir=https://docs.fluxer.app{uri}' - - 'caddy_8=www.fluxer.dev' - - 'caddy_8.redir=https://docs.fluxer.app{uri}' + caddy: "${CADDY_DOMAIN}" + caddy.reverse_proxy: "{{upstreams 8080}}" + 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" + COMPOSEEOF + + if [[ "${IS_CANARY}" == "true" ]]; then + cat >> compose.yaml << 'COMPOSEEOF' + caddy.header.X-Robots-Tag: "noindex, nofollow, nosnippet, noimageindex" + caddy.@channels.path: "/channels /channels/*" + caddy.redir: "@channels https://web.canary.fluxer.app{uri}" + COMPOSEEOF + else + cat >> compose.yaml << 'COMPOSEEOF' + caddy.redir_0: "/channels/* https://web.fluxer.app{uri}" + caddy.redir_1: "/channels https://web.fluxer.app{uri}" + caddy.redir_2: "/delete-my-account https://fluxer.app/help/articles/1445724566704881664 302" + caddy.redir_3: "/delete-my-data https://fluxer.app/help/articles/1445730947679911936 302" + caddy.redir_4: "/export-my-data https://fluxer.app/help/articles/1445731738851475456 302" + caddy.redir_5: "/bugs https://fluxer.app/help/articles/1447264362996695040 302" + caddy_1: "www.fluxer.app" + caddy_1.redir: "https://fluxer.app{uri}" + caddy_3: "fluxer.gg" + caddy_3.redir: "https://web.fluxer.app/invite{uri}" + caddy_4: "fluxer.gift" + caddy_4.redir: "https://web.fluxer.app/gift{uri}" + caddy_5: "fluxerapp.com" + caddy_5.redir: "https://fluxer.app{uri}" + caddy_6: "www.fluxerapp.com" + caddy_6.redir: "https://fluxer.app{uri}" + caddy_7: "fluxer.dev" + caddy_7.redir: "https://docs.fluxer.app{uri}" + caddy_8: "www.fluxer.dev" + caddy_8.redir: "https://docs.fluxer.app{uri}" + COMPOSEEOF + fi + + cat >> compose.yaml << 'COMPOSEEOF' networks: - fluxer-shared healthcheck: @@ -160,6 +228,10 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml fluxer-marketing - docker service update --image ${IMAGE_TAG} fluxer-marketing_app + docker stack deploy \ + --with-registry-auth \ + --detach=false \ + --resolve-image never \ + -c compose.yaml \ + "${STACK}" EOF diff --git a/.github/workflows/deploy-media-proxy.yaml b/.github/workflows/deploy-media-proxy.yaml index 37a9ee05..f080eee9 100644 --- a/.github/workflows/deploy-media-proxy.yaml +++ b/.github/workflows/deploy-media-proxy.yaml @@ -5,9 +5,9 @@ on: branches: - main paths: - - fluxer_media_proxy/**/* + - fluxer_media_proxy/** - .github/workflows/deploy-media-proxy.yaml - workflow_dispatch: + workflow_dispatch: {} concurrency: group: deploy-fluxer-media-proxy @@ -30,6 +30,13 @@ jobs: steps: - uses: actions/checkout@v6 + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -44,7 +51,7 @@ jobs: with: context: ${{ env.CONTEXT_DIR }} file: ${{ env.CONTEXT_DIR }}/Dockerfile - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 cache-from: type=gha,scope=${{ env.SERVICE_NAME }} @@ -74,19 +81,19 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ github.sha }} + IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK}" bash << 'EOF' - set -euo pipefail - sudo mkdir -p /opt/${SERVICE_NAME} - sudo chown -R ${USER}:${USER} /opt/${SERVICE_NAME} - cd /opt/${SERVICE_NAME} + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK} 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 + cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} @@ -139,6 +146,5 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml ${COMPOSE_STACK} - docker service update --image ${IMAGE_TAG} fluxer-media-proxy_app + docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}" EOF diff --git a/.github/workflows/deploy-metrics.yaml b/.github/workflows/deploy-metrics.yaml index 524cae6d..4890951b 100644 --- a/.github/workflows/deploy-metrics.yaml +++ b/.github/workflows/deploy-metrics.yaml @@ -5,9 +5,9 @@ on: branches: - main paths: - - fluxer_metrics/**/* + - fluxer_metrics/** - .github/workflows/deploy-metrics.yaml - workflow_dispatch: + workflow_dispatch: {} concurrency: group: deploy-fluxer-metrics @@ -24,6 +24,13 @@ jobs: steps: - uses: actions/checkout@v6 + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -38,7 +45,7 @@ jobs: with: context: fluxer_metrics file: fluxer_metrics/Dockerfile - tags: fluxer-metrics:${{ github.sha }} + tags: fluxer-metrics:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 cache-from: type=gha,scope=deploy-fluxer-metrics @@ -68,19 +75,19 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: fluxer-metrics:${{ github.sha }} + IMAGE_TAG: fluxer-metrics:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} bash << EOF - set -e - sudo mkdir -p /opt/fluxer-metrics - sudo chown -R \${USER}:\${USER} /opt/fluxer-metrics - cd /opt/fluxer-metrics + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} bash" << 'EOF' + set -euo pipefail + sudo mkdir -p /opt/fluxer-metrics + sudo chown -R "${USER}:${USER}" /opt/fluxer-metrics + cd /opt/fluxer-metrics - cat > compose.yaml << 'COMPOSEEOF' + cat > compose.yaml << 'COMPOSEEOF' services: app: image: ${IMAGE_TAG} @@ -120,6 +127,5 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml --with-registry-auth fluxer-metrics - docker service update --image ${IMAGE_TAG} fluxer-metrics_app + docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml fluxer-metrics EOF diff --git a/.github/workflows/deploy-static-proxy.yaml b/.github/workflows/deploy-static-proxy.yaml index 953f08e5..c157d443 100644 --- a/.github/workflows/deploy-static-proxy.yaml +++ b/.github/workflows/deploy-static-proxy.yaml @@ -5,9 +5,9 @@ on: branches: - main paths: - - fluxer_media_proxy/**/* + - fluxer_media_proxy/** - .github/workflows/deploy-static-proxy.yaml - workflow_dispatch: + workflow_dispatch: {} concurrency: group: deploy-fluxer-static-proxy @@ -30,6 +30,13 @@ jobs: steps: - uses: actions/checkout@v6 + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -44,7 +51,7 @@ jobs: with: context: ${{ env.CONTEXT_DIR }} file: ${{ env.CONTEXT_DIR }}/Dockerfile - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 cache-from: type=gha,scope=${{ env.SERVICE_NAME }} @@ -74,19 +81,19 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ github.sha }} + IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK}" bash << 'EOF' - set -euo pipefail - sudo mkdir -p /opt/${SERVICE_NAME} - sudo chown -R ${USER}:${USER} /opt/${SERVICE_NAME} - cd /opt/${SERVICE_NAME} + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK} 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 + cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} @@ -139,6 +146,5 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml ${COMPOSE_STACK} - docker service update --image ${IMAGE_TAG} fluxer-static-proxy_app + docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}" EOF diff --git a/.github/workflows/restart-gateway.yaml b/.github/workflows/restart-gateway.yaml index 939ce4fc..55910c70 100644 --- a/.github/workflows/restart-gateway.yaml +++ b/.github/workflows/restart-gateway.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: confirmation: - description: 'this will cause service interruption for all users. type RESTART to confirm.' + description: this will cause service interruption for all users. type RESTART to confirm. required: true type: string @@ -36,6 +36,13 @@ jobs: - uses: actions/checkout@v6 + - 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 Docker Buildx uses: docker/setup-buildx-action@v3 @@ -50,7 +57,7 @@ jobs: with: context: ${{ env.CONTEXT_DIR }} file: ${{ env.CONTEXT_DIR }}/Dockerfile - tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} load: true platforms: linux/amd64 cache-from: type=gha,scope=${{ env.SERVICE_NAME }} @@ -80,19 +87,19 @@ jobs: - name: Push image and deploy env: - IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ github.sha }} + IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }} SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} run: | set -euo pipefail - docker pussh ${IMAGE_TAG} ${SERVER} + docker pussh "${IMAGE_TAG}" "${SERVER}" - ssh ${SERVER} "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK}" bash << 'EOF' - set -euo pipefail - sudo mkdir -p /opt/${SERVICE_NAME} - sudo chown -R ${USER}:${USER} /opt/${SERVICE_NAME} - cd /opt/${SERVICE_NAME} + ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK} 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 + cat > compose.yaml << COMPOSEEOF services: app: image: ${IMAGE_TAG} @@ -138,6 +145,5 @@ jobs: external: true COMPOSEEOF - docker stack deploy -c compose.yaml ${COMPOSE_STACK} - docker service update --image ${IMAGE_TAG} fluxer-gateway_app + docker stack deploy --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}" EOF diff --git a/.github/workflows/update-word-lists.yaml b/.github/workflows/update-word-lists.yaml index 2c4f47a9..be2704c6 100644 --- a/.github/workflows/update-word-lists.yaml +++ b/.github/workflows/update-word-lists.yaml @@ -53,7 +53,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} branch: word-lists-update-${{ github.run_id }} base: canary - title: "chore: update word lists from Tailscale upstream" + title: 'chore: update word lists from Tailscale upstream' body: | Automated update of scales.txt and tails.txt from the Tailscale repository. @@ -62,7 +62,7 @@ jobs: Source: - https://github.com/tailscale/tailscale/blob/main/words/scales.txt - https://github.com/tailscale/tailscale/blob/main/words/tails.txt - commit-message: "chore: update word lists from Tailscale upstream" + commit-message: 'chore: update word lists from Tailscale upstream' files: | fluxer_api/src/words/scales.txt fluxer_api/src/words/tails.txt