[skip ci] chore: cleanup workflows

This commit is contained in:
Hampus Kraft 2026-01-02 12:13:01 +00:00
parent 2f557eda8c
commit 197b23757f
No known key found for this signature in database
GPG Key ID: 6090864C465A454D
21 changed files with 1016 additions and 2204 deletions

View File

@ -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" <<EOF
{
"currentRelease": "${VERSION}",
"releases": [
{
"version": "${VERSION}",
"updateTo": {
"version": "${VERSION}",
"pub_date": "${PUB_DATE}",
"notes": "",
"name": "${VERSION}",
"url": "${url}"
}
}
]
}
EOF
cp -f "$dest/RELEASES.json" "$dest/releases.json"
fi
fi
setup_file=""
dmg_file=""
zip_file2=""
appimage_file=""
deb_file=""
rpm_file=""
targz_file=""
if [[ "$plat" == "win32" ]]; then
setup_file="$(ls -1 "$dest"/*.exe 2>/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"

View File

@ -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 ""

94
.github/workflows/channel-vars.yaml vendored Normal file
View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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