initial commit
This commit is contained in:
613
.github/workflows/build-desktop-canary.yaml
vendored
Normal file
613
.github/workflows/build-desktop-canary.yaml
vendored
Normal file
@@ -0,0 +1,613 @@
|
||||
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"
|
||||
612
.github/workflows/build-desktop.yaml
vendored
Normal file
612
.github/workflows/build-desktop.yaml
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
name: build desktop
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Git ref to build (branch, tag, or commit SHA)
|
||||
required: false
|
||||
default: stable
|
||||
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-stable-${{ inputs.ref || 'stable' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CHANNEL: stable
|
||||
SOURCE_REF: ${{ inputs.ref || 'stable' }}
|
||||
|
||||
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"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }})
|
||||
needs: matrix
|
||||
outputs:
|
||||
version: ${{ steps.metadata.outputs.version }}
|
||||
pub_date: ${{ steps.metadata.outputs.pub_date }}
|
||||
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: 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
|
||||
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 "$VERSION" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Build Electron main process
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
env:
|
||||
BUILD_CHANNEL: stable
|
||||
run: pnpm electron:compile
|
||||
|
||||
- name: Build Electron app (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
env:
|
||||
BUILD_CHANNEL: stable
|
||||
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.yaml --mac --${{ matrix.electron_arch }}
|
||||
|
||||
- name: Build Electron app (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
env:
|
||||
BUILD_CHANNEL: stable
|
||||
TEMP: C:\t
|
||||
TMP: C:\t
|
||||
SQUIRREL_TEMP: C:\sq
|
||||
ELECTRON_BUILDER_CACHE: C:\ebcache
|
||||
run: pnpm exec electron-builder --config electron-builder.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: ${{ steps.metadata.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: stable
|
||||
USE_SYSTEM_FPM: true
|
||||
run: pnpm exec electron-builder --config electron-builder.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-stable-${{ 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: build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
env:
|
||||
CHANNEL: stable
|
||||
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-stable-*
|
||||
|
||||
- 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.build.outputs.version }}
|
||||
PUB_DATE: ${{ needs.build.outputs.pub_date }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p s3_payload
|
||||
|
||||
shopt -s nullglob
|
||||
for dir in artifacts/fluxer-desktop-stable-*; do
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
base="$(basename "$dir")"
|
||||
if [[ "$base" =~ ^fluxer-desktop-stable-([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 Stable Upload Complete"
|
||||
echo ""
|
||||
echo "**Version:** ${{ steps.metadata.outputs.version }}"
|
||||
echo ""
|
||||
echo "**S3 prefix:** desktop/${CHANNEL}/"
|
||||
echo ""
|
||||
echo "**Redirect endpoint shape:** /dl/desktop/${CHANNEL}/{plat}/{arch}/{format}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
141
.github/workflows/deploy-admin-canary.yaml
vendored
Normal file
141
.github/workflows/deploy-admin-canary.yaml
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
141
.github/workflows/deploy-admin.yaml
vendored
Normal file
141
.github/workflows/deploy-admin.yaml
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
name: deploy admin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_admin/**/*
|
||||
- .github/workflows/deploy-admin.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-admin
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
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:${{ github.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 }}
|
||||
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:${{ 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
|
||||
sudo chown -R ${USER}:${USER} /opt/fluxer-admin
|
||||
cd /opt/fluxer-admin
|
||||
|
||||
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
|
||||
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=admin.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
|
||||
docker service update --image ${IMAGE_TAG} fluxer-admin_app
|
||||
EOF
|
||||
201
.github/workflows/deploy-api-canary.yaml
vendored
Normal file
201
.github/workflows/deploy-api-canary.yaml
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
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
|
||||
187
.github/workflows/deploy-api-worker.yaml
vendored
Normal file
187
.github/workflows/deploy-api-worker.yaml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
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
|
||||
202
.github/workflows/deploy-api.yaml
vendored
Normal file
202
.github/workflows/deploy-api.yaml
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
name: deploy api
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_api/**/*
|
||||
- .github/workflows/deploy-api.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-api
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
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:${{ github.sha }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=deploy-fluxer-api
|
||||
cache-to: type=gha,mode=max,scope=deploy-fluxer-api
|
||||
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:${{ 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
|
||||
sudo chown -R ${USER}:${USER} /opt/fluxer-api
|
||||
cd /opt/fluxer-api
|
||||
|
||||
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_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: 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.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
|
||||
docker service update --image ${IMAGE_TAG} fluxer-api_app
|
||||
EOF
|
||||
318
.github/workflows/deploy-app-canary.yaml
vendored
Normal file
318
.github/workflows/deploy-app-canary.yaml
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
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
|
||||
316
.github/workflows/deploy-app.yaml
vendored
Normal file
316
.github/workflows/deploy-app.yaml
vendored
Normal file
@@ -0,0 +1,316 @@
|
||||
name: deploy app
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_app/**/*
|
||||
- .github/workflows/deploy-app.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-app-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:
|
||||
deploy:
|
||||
name: Deploy app (stable)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/main' || 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.fluxer.app/api
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: https://api.fluxer.app
|
||||
PUBLIC_API_VERSION: 1
|
||||
PUBLIC_PROJECT_ENV: stable
|
||||
PUBLIC_SENTRY_PROJECT_ID: 4510205815291904
|
||||
PUBLIC_SENTRY_PUBLIC_KEY: 59ced0e2666ab83dd1ddb056cdd22d1b
|
||||
PUBLIC_SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.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": "stable"
|
||||
}
|
||||
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.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: 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=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'
|
||||
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
|
||||
|
||||
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.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'
|
||||
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
|
||||
|
||||
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
|
||||
EOF
|
||||
129
.github/workflows/deploy-docs-canary.yaml
vendored
Normal file
129
.github/workflows/deploy-docs-canary.yaml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
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
|
||||
128
.github/workflows/deploy-docs.yaml
vendored
Normal file
128
.github/workflows/deploy-docs.yaml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: deploy docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_docs/**/*
|
||||
- .github/workflows/deploy-docs.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-docs
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
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:${{ github.sha }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=deploy-fluxer-docs
|
||||
cache-to: type=gha,mode=max,scope=deploy-fluxer-docs
|
||||
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:${{ 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
|
||||
sudo chown -R ${USER}:${USER} /opt/fluxer-docs
|
||||
cd /opt/fluxer-docs
|
||||
|
||||
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.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'
|
||||
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
|
||||
docker service update --image ${IMAGE_TAG} fluxer-docs_app
|
||||
EOF
|
||||
278
.github/workflows/deploy-gateway.yaml
vendored
Normal file
278
.github/workflows/deploy-gateway.yaml
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
name: deploy gateway
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
- main
|
||||
paths:
|
||||
- 'fluxer_gateway/**'
|
||||
|
||||
concurrency:
|
||||
group: deploy-gateway
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy (hot patch)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: fluxer_gateway
|
||||
|
||||
- name: Set up Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: "28"
|
||||
rebar3-version: "3.24.0"
|
||||
|
||||
- name: Compile
|
||||
working-directory: fluxer_gateway
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rebar3 as prod compile
|
||||
|
||||
- name: Set up SSH
|
||||
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: Deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
GATEWAY_ADMIN_SECRET: ${{ secrets.GATEWAY_ADMIN_SECRET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_ID="$(ssh "${SERVER}" "docker ps -q --filter label=com.docker.swarm.service.name=fluxer-gateway_app | head -1")"
|
||||
if [ -z "${CONTAINER_ID}" ]; then
|
||||
echo "::error::No running container found for service fluxer-gateway_app"
|
||||
ssh "${SERVER}" "docker ps --filter 'name=fluxer-gateway_app' --format '{{.ID}} {{.Names}} {{.Status}}'" || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Container: ${CONTAINER_ID}"
|
||||
|
||||
LOCAL_MD5_LINES="$(
|
||||
erl -noshell -eval '
|
||||
Files = filelib:wildcard("fluxer_gateway/_build/prod/lib/fluxer_gateway/ebin/*.beam"),
|
||||
lists:foreach(
|
||||
fun(F) ->
|
||||
{ok, {M, Md5}} = beam_lib:md5(F),
|
||||
Hex = binary:encode_hex(Md5, lowercase),
|
||||
io:format("~s ~s ~s~n", [atom_to_list(M), binary_to_list(Hex), F])
|
||||
end,
|
||||
Files
|
||||
),
|
||||
halt().'
|
||||
)"
|
||||
|
||||
REMOTE_MD5_LINES="$(
|
||||
ssh "${SERVER}" "docker exec ${CONTAINER_ID} /opt/fluxer_gateway/bin/fluxer_gateway eval '
|
||||
Mods = hot_reload:get_loaded_modules(),
|
||||
lists:foreach(
|
||||
fun(M) ->
|
||||
case hot_reload:get_module_info(M) of
|
||||
{ok, Info} ->
|
||||
V = maps:get(loaded_md5, Info),
|
||||
S = case V of
|
||||
null -> \"null\";
|
||||
B when is_binary(B) -> binary_to_list(B)
|
||||
end,
|
||||
io:format(\"~s ~s~n\", [atom_to_list(M), S]);
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end,
|
||||
Mods
|
||||
),
|
||||
ok.
|
||||
' " | tr -d '\r'
|
||||
)"
|
||||
|
||||
LOCAL_MD5_FILE="$(mktemp)"
|
||||
REMOTE_MD5_FILE="$(mktemp)"
|
||||
CHANGED_FILE_LIST="$(mktemp)"
|
||||
CHANGED_MAIN_LIST="$(mktemp)"
|
||||
CHANGED_SELF_LIST="$(mktemp)"
|
||||
RELOAD_RESULT_MAIN="$(mktemp)"
|
||||
RELOAD_RESULT_SELF="$(mktemp)"
|
||||
trap 'rm -f "${LOCAL_MD5_FILE}" "${REMOTE_MD5_FILE}" "${CHANGED_FILE_LIST}" "${CHANGED_MAIN_LIST}" "${CHANGED_SELF_LIST}" "${RELOAD_RESULT_MAIN}" "${RELOAD_RESULT_SELF}"' EXIT
|
||||
|
||||
printf '%s' "${LOCAL_MD5_LINES}" > "${LOCAL_MD5_FILE}"
|
||||
printf '%s' "${REMOTE_MD5_LINES}" > "${REMOTE_MD5_FILE}"
|
||||
|
||||
python3 - <<'PY' "${LOCAL_MD5_FILE}" "${REMOTE_MD5_FILE}" "${CHANGED_FILE_LIST}"
|
||||
import sys
|
||||
|
||||
local_path, remote_path, out_path = sys.argv[1:4]
|
||||
|
||||
remote = {}
|
||||
with open(remote_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(None, 1)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
mod, md5 = parts
|
||||
remote[mod] = md5.strip()
|
||||
|
||||
changed_paths = []
|
||||
with open(local_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split(" ", 2)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
mod, md5, path = parts
|
||||
r = remote.get(mod)
|
||||
if r is None or r == "null" or r != md5:
|
||||
changed_paths.append(path)
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
for p in changed_paths:
|
||||
f.write(p + "\n")
|
||||
PY
|
||||
|
||||
mapfile -t CHANGED_FILES < "${CHANGED_FILE_LIST}"
|
||||
|
||||
if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then
|
||||
echo "No BEAM changes detected, nothing to hot-reload."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changed modules count: ${#CHANGED_FILES[@]}"
|
||||
|
||||
while IFS= read -r p; do
|
||||
[ -n "${p}" ] || continue
|
||||
m="$(basename "${p}")"
|
||||
m="${m%.beam}"
|
||||
if [ "${m}" = "hot_reload" ] || [ "${m}" = "hot_reload_handler" ]; then
|
||||
printf '%s\n' "${p}" >> "${CHANGED_SELF_LIST}"
|
||||
else
|
||||
printf '%s\n' "${p}" >> "${CHANGED_MAIN_LIST}"
|
||||
fi
|
||||
done < "${CHANGED_FILE_LIST}"
|
||||
|
||||
build_json() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys, json, base64, os
|
||||
list_path = sys.argv[1]
|
||||
beams = []
|
||||
with open(list_path, "r", encoding="utf-8") as f:
|
||||
for path in f:
|
||||
path = path.strip()
|
||||
if not path:
|
||||
continue
|
||||
mod = os.path.basename(path)
|
||||
if not mod.endswith(".beam"):
|
||||
continue
|
||||
mod = mod[:-5]
|
||||
with open(path, "rb") as bf:
|
||||
b = bf.read()
|
||||
beams.append({"module": mod, "beam_b64": base64.b64encode(b).decode("ascii")})
|
||||
print(json.dumps({"beams": beams, "purge": "soft"}, separators=(",", ":")))
|
||||
PY
|
||||
}
|
||||
|
||||
strict_verify() {
|
||||
python3 -c '
|
||||
import json, sys
|
||||
raw = sys.stdin.read()
|
||||
if not raw.strip():
|
||||
print("::error::Empty reload response")
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception as e:
|
||||
print(f"::error::Invalid JSON reload response: {e}")
|
||||
raise SystemExit(1)
|
||||
results = data.get("results", [])
|
||||
if not isinstance(results, list):
|
||||
print("::error::Reload response missing results array")
|
||||
raise SystemExit(1)
|
||||
bad = [
|
||||
r for r in results
|
||||
if r.get("status") != "ok"
|
||||
or r.get("verified") is not True
|
||||
or r.get("purged_old_code") is not True
|
||||
or (r.get("lingering_count") or 0) != 0
|
||||
]
|
||||
if bad:
|
||||
print("::error::Hot reload verification failed")
|
||||
print(json.dumps(bad, indent=2))
|
||||
raise SystemExit(1)
|
||||
print(f"Verified {len(results)} modules")
|
||||
'
|
||||
}
|
||||
|
||||
self_verify() {
|
||||
python3 -c '
|
||||
import json, sys
|
||||
raw = sys.stdin.read()
|
||||
if not raw.strip():
|
||||
print("::error::Empty reload response")
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except Exception as e:
|
||||
print(f"::error::Invalid JSON reload response: {e}")
|
||||
raise SystemExit(1)
|
||||
results = data.get("results", [])
|
||||
if not isinstance(results, list):
|
||||
print("::error::Reload response missing results array")
|
||||
raise SystemExit(1)
|
||||
bad = [
|
||||
r for r in results
|
||||
if r.get("status") != "ok"
|
||||
or r.get("verified") is not True
|
||||
]
|
||||
if bad:
|
||||
print("::error::Hot reload verification failed")
|
||||
print(json.dumps(bad, indent=2))
|
||||
raise SystemExit(1)
|
||||
warns = [
|
||||
r for r in results
|
||||
if r.get("purged_old_code") is not True
|
||||
or (r.get("lingering_count") or 0) != 0
|
||||
]
|
||||
if warns:
|
||||
print("::warning::Self-reload modules may linger until request completes")
|
||||
print(json.dumps(warns, indent=2))
|
||||
print(f"Verified {len(results)} self modules")
|
||||
'
|
||||
}
|
||||
|
||||
if [ -s "${CHANGED_MAIN_LIST}" ]; then
|
||||
if ! build_json "${CHANGED_MAIN_LIST}" | ssh "${SERVER}" "docker exec -i ${CONTAINER_ID} curl -fsS -X POST -H 'Authorization: Bearer ${GATEWAY_ADMIN_SECRET}' -H 'Content-Type: application/json' --data @- http://localhost:8081/_admin/reload" | tee "${RELOAD_RESULT_MAIN}" | strict_verify; then
|
||||
echo "::group::Hot reload response (main)"
|
||||
cat "${RELOAD_RESULT_MAIN}" || true
|
||||
echo "::endgroup::"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -s "${CHANGED_SELF_LIST}" ]; then
|
||||
if ! build_json "${CHANGED_SELF_LIST}" | ssh "${SERVER}" "docker exec -i ${CONTAINER_ID} curl -fsS -X POST -H 'Authorization: Bearer ${GATEWAY_ADMIN_SECRET}' -H 'Content-Type: application/json' --data @- http://localhost:8081/_admin/reload" | tee "${RELOAD_RESULT_SELF}" | self_verify; then
|
||||
echo "::group::Hot reload response (self)"
|
||||
cat "${RELOAD_RESULT_SELF}" || true
|
||||
echo "::endgroup::"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
117
.github/workflows/deploy-geoip.yaml
vendored
Normal file
117
.github/workflows/deploy-geoip.yaml
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
name: deploy geoip
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_geoip/**/*
|
||||
- .github/workflows/deploy-geoip.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-geoip
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy geoip
|
||||
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_geoip
|
||||
file: fluxer_geoip/Dockerfile
|
||||
tags: fluxer-geoip:${{ github.sha }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=deploy-fluxer-geoip
|
||||
cache-to: type=gha,mode=max,scope=deploy-fluxer-geoip
|
||||
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-geoip:${{ github.sha }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
|
||||
cat > compose.yaml << 'COMPOSEEOF'
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
volumes:
|
||||
- /etc/fluxer/ipinfo_lite.mmdb:/data/ipinfo_lite.mmdb:ro
|
||||
deploy:
|
||||
replicas: 3
|
||||
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
|
||||
environment:
|
||||
- FLUXER_GEOIP_PORT=8080
|
||||
- GEOIP_DB_PATH=/data/ipinfo_lite.mmdb
|
||||
- GEOIP_CACHE_TTL=10m
|
||||
- GEOIP_CACHE_SIZE=20000
|
||||
networks:
|
||||
- fluxer-shared
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy -c compose.yaml fluxer-geoip
|
||||
docker service update --image ${IMAGE_TAG} fluxer-geoip_app
|
||||
EOF
|
||||
148
.github/workflows/deploy-marketing-canary.yaml
vendored
Normal file
148
.github/workflows/deploy-marketing-canary.yaml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
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
|
||||
165
.github/workflows/deploy-marketing.yaml
vendored
Normal file
165
.github/workflows/deploy-marketing.yaml
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
name: deploy marketing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_marketing/**/*
|
||||
- .github/workflows/deploy-marketing.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-marketing
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy app
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/main' || 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:${{ 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 }}
|
||||
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:${{ 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
|
||||
sudo chown -R ${USER}:${USER} /opt/fluxer-marketing
|
||||
cd /opt/fluxer-marketing
|
||||
|
||||
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_CDN_ENDPOINT=https://fluxerstatic.com
|
||||
- FLUXER_MARKETING_ENDPOINT=https://fluxer.app
|
||||
- FLUXER_MARKETING_PORT=8080
|
||||
- FLUXER_PATH_MARKETING=/
|
||||
- GEOIP_HOST=fluxer-geoip_app:8080
|
||||
- RELEASE_CHANNEL=stable
|
||||
- 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
|
||||
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}'
|
||||
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
|
||||
docker service update --image ${IMAGE_TAG} fluxer-marketing_app
|
||||
EOF
|
||||
144
.github/workflows/deploy-media-proxy.yaml
vendored
Normal file
144
.github/workflows/deploy-media-proxy.yaml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: deploy media-proxy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_media_proxy/**/*
|
||||
- .github/workflows/deploy-media-proxy.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-media-proxy
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SERVICE_NAME: fluxer-media-proxy
|
||||
IMAGE_NAME: fluxer-media-proxy
|
||||
CONTEXT_DIR: fluxer_media_proxy
|
||||
COMPOSE_STACK: fluxer-media-proxy
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy media proxy
|
||||
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: ${{ env.CONTEXT_DIR }}
|
||||
file: ${{ env.CONTEXT_DIR }}/Dockerfile
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ github.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 }}:${{ github.sha }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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}
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
command: ['pnpm', 'start']
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- FLUXER_MEDIA_PROXY_PORT=8080
|
||||
- FLUXER_MEDIA_PROXY_REQUIRE_CLOUDFLARE=true
|
||||
- SENTRY_DSN=https://2670068cd12b6a62f3a30a7f0055f0f1@o4510149383094272.ingest.us.sentry.io/4510205811556352
|
||||
- AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us
|
||||
- AWS_S3_BUCKET_CDN=fluxer
|
||||
- AWS_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
- 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
|
||||
labels:
|
||||
- 'caddy=http://fluxerusercontent.com'
|
||||
- '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=\"https://o4510149383094272.ingest.us.sentry.io/api/4510205811556352/security/?sentry_key=2670068cd12b6a62f3a30a7f0055f0f1\""'
|
||||
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-media-proxy_app
|
||||
EOF
|
||||
125
.github/workflows/deploy-metrics.yaml
vendored
Normal file
125
.github/workflows/deploy-metrics.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: deploy metrics
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_metrics/**/*
|
||||
- .github/workflows/deploy-metrics.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-metrics
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy metrics
|
||||
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_metrics
|
||||
file: fluxer_metrics/Dockerfile
|
||||
tags: fluxer-metrics:${{ github.sha }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=deploy-fluxer-metrics
|
||||
cache-to: type=gha,mode=max,scope=deploy-fluxer-metrics
|
||||
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-metrics:${{ github.sha }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
|
||||
cat > compose.yaml << 'COMPOSEEOF'
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- METRICS_PORT=8080
|
||||
- CLICKHOUSE_URL=http://clickhouse:8123
|
||||
- CLICKHOUSE_DATABASE=fluxer_metrics
|
||||
- CLICKHOUSE_USER=fluxer
|
||||
- FLUXER_ADMIN_ENDPOINT=https://admin.fluxer.app
|
||||
- ANOMALY_DETECTION_ENABLED=true
|
||||
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
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy -c compose.yaml --with-registry-auth fluxer-metrics
|
||||
docker service update --image ${IMAGE_TAG} fluxer-metrics_app
|
||||
EOF
|
||||
144
.github/workflows/deploy-static-proxy.yaml
vendored
Normal file
144
.github/workflows/deploy-static-proxy.yaml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: deploy static-proxy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- fluxer_media_proxy/**/*
|
||||
- .github/workflows/deploy-static-proxy.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-static-proxy
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SERVICE_NAME: fluxer-static-proxy
|
||||
IMAGE_NAME: fluxer-static-proxy
|
||||
CONTEXT_DIR: fluxer_media_proxy
|
||||
COMPOSE_STACK: fluxer-static-proxy
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy static proxy
|
||||
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: ${{ env.CONTEXT_DIR }}
|
||||
file: ${{ env.CONTEXT_DIR }}/Dockerfile
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ github.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 }}:${{ github.sha }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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}
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
command: ['pnpm', 'start']
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- FLUXER_MEDIA_PROXY_PORT=8080
|
||||
- FLUXER_MEDIA_PROXY_STATIC_MODE=true
|
||||
- FLUXER_MEDIA_PROXY_REQUIRE_CLOUDFLARE=true
|
||||
- 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_STATIC=fluxer-static
|
||||
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=http://fluxerstatic.com'
|
||||
- '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=\"https://o4510149383094272.ingest.us.sentry.io/api/4510205811556352/security/?sentry_key=2670068cd12b6a62f3a30a7f0055f0f1\""'
|
||||
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-static-proxy_app
|
||||
EOF
|
||||
125
.github/workflows/migrate-cassandra.yaml
vendored
Normal file
125
.github/workflows/migrate-cassandra.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
name: migrate cassandra
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_devops/cassandra/migrations/**/*.cql
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: migrate-cassandra-prod
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
migrate:
|
||||
name: Run database migrations
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
scripts/cassandra-migrate/target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('scripts/cassandra-migrate/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Build migration tool
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd scripts/cassandra-migrate
|
||||
cargo build --release
|
||||
|
||||
- name: Validate migrations
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/cassandra-migrate/target/release/cassandra-migrate check
|
||||
|
||||
- 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: Set up SSH tunnel for Cassandra
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nohup ssh -N -o ConnectTimeout=30 -o ServerAliveInterval=10 -o ServerAliveCountMax=30 -o ExitOnForwardFailure=yes -L 9042:localhost:9042 ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} > /tmp/ssh-tunnel.log 2>&1 &
|
||||
SSH_TUNNEL_PID=$!
|
||||
printf 'SSH_TUNNEL_PID=%s\n' "$SSH_TUNNEL_PID" >> "$GITHUB_ENV"
|
||||
|
||||
for i in {1..30}; do
|
||||
if timeout 1 bash -c "echo > /dev/tcp/localhost/9042" 2>/dev/null; then
|
||||
echo "SSH tunnel established"
|
||||
break
|
||||
elif command -v ss >/dev/null 2>&1 && ss -tln | grep -q ":9042 "; then
|
||||
echo "SSH tunnel established"
|
||||
break
|
||||
elif command -v netstat >/dev/null 2>&1 && netstat -tln | grep -q ":9042 "; then
|
||||
echo "SSH tunnel established"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
cat /tmp/ssh-tunnel.log || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
ps -p $SSH_TUNNEL_PID > /dev/null || exit 1
|
||||
|
||||
- name: Test Cassandra connection
|
||||
env:
|
||||
CASSANDRA_USERNAME: ${{ secrets.CASSANDRA_USERNAME }}
|
||||
CASSANDRA_PASSWORD: ${{ secrets.CASSANDRA_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/cassandra-migrate/target/release/cassandra-migrate \
|
||||
--host localhost \
|
||||
--port 9042 \
|
||||
--username "${CASSANDRA_USERNAME}" \
|
||||
--password "${CASSANDRA_PASSWORD}" \
|
||||
test
|
||||
|
||||
- name: Run migrations
|
||||
env:
|
||||
CASSANDRA_USERNAME: ${{ secrets.CASSANDRA_USERNAME }}
|
||||
CASSANDRA_PASSWORD: ${{ secrets.CASSANDRA_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/cassandra-migrate/target/release/cassandra-migrate \
|
||||
--host localhost \
|
||||
--port 9042 \
|
||||
--username "${CASSANDRA_USERNAME}" \
|
||||
--password "${CASSANDRA_PASSWORD}" \
|
||||
up
|
||||
|
||||
- name: Close SSH tunnel
|
||||
if: always()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${SSH_TUNNEL_PID:-}" ]; then
|
||||
kill "$SSH_TUNNEL_PID" 2>/dev/null || true
|
||||
fi
|
||||
pkill -f "ssh.*9042:localhost:9042" || true
|
||||
rm -f /tmp/ssh-tunnel.log || true
|
||||
143
.github/workflows/restart-gateway.yaml
vendored
Normal file
143
.github/workflows/restart-gateway.yaml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: restart gateway
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirmation:
|
||||
description: 'this will cause service interruption for all users. type RESTART to confirm.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: restart-gateway
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SERVICE_NAME: fluxer-gateway
|
||||
IMAGE_NAME: fluxer-gateway
|
||||
CONTEXT_DIR: fluxer_gateway
|
||||
COMPOSE_STACK: fluxer-gateway
|
||||
|
||||
jobs:
|
||||
restart:
|
||||
name: Restart gateway
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Validate confirmation
|
||||
if: ${{ github.event.inputs.confirmation != 'RESTART' }}
|
||||
run: |
|
||||
echo "::error::Confirmation failed. You must type 'RESTART' to proceed with a full restart."
|
||||
echo "::error::For regular updates, use deploy-gateway.yaml instead."
|
||||
exit 1
|
||||
|
||||
- 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: ${{ env.CONTEXT_DIR }}
|
||||
file: ${{ env.CONTEXT_DIR }}/Dockerfile
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ github.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 }}:${{ github.sha }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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}
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
hostname: "{{.Node.Hostname}}-{{.Task.Slot}}"
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- API_HOST=fluxer-api_app:8080
|
||||
- API_CANARY_HOST=fluxer-api-canary_app:8080
|
||||
- RELEASE_NODE=fluxer_gateway@{{.Node.Hostname}}-{{.Task.Slot}}
|
||||
- LOGGER_LEVEL=info
|
||||
- VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY
|
||||
- FLUXER_METRICS_HOST=fluxer-metrics_app:8080
|
||||
- MEDIA_PROXY_ENDPOINT=https://fluxerusercontent.com
|
||||
deploy:
|
||||
replicas: 1
|
||||
endpoint_mode: dnsrr
|
||||
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_gw=gateway.fluxer.app'
|
||||
- 'caddy_gw.reverse_proxy={{upstreams 8080}}'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy -c compose.yaml ${COMPOSE_STACK}
|
||||
docker service update --image ${IMAGE_TAG} fluxer-gateway_app
|
||||
EOF
|
||||
59
.github/workflows/sync-static.yaml
vendored
Normal file
59
.github/workflows/sync-static.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: sync static-bucket
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'fluxer_static/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: sync-fluxer-static
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
RCLONE_REMOTE: ovh
|
||||
RCLONE_BUCKET: fluxer-static
|
||||
RCLONE_SOURCE: fluxer_static
|
||||
RCLONE_ENDPOINT: https://s3.us-east-va.io.cloud.ovh.us
|
||||
RCLONE_REGION: us-east-1
|
||||
RCLONE_SOURCE_DIR: fluxer_static
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: true
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: Push repo contents to 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 = $RCLONE_ENDPOINT
|
||||
acl = private
|
||||
RCLONEEOF
|
||||
|
||||
mkdir -p "$RCLONE_SOURCE_DIR"
|
||||
rclone sync "$RCLONE_SOURCE" "$RCLONE_REMOTE:$RCLONE_BUCKET" --create-empty-src-dirs --exclude "assets/**"
|
||||
306
.github/workflows/test-cassandra-backup.yaml
vendored
Normal file
306
.github/workflows/test-cassandra-backup.yaml
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
name: test cassandra-backup
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *'
|
||||
|
||||
concurrency:
|
||||
group: test-cassandra-backup
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-backup:
|
||||
name: Test latest Cassandra backup
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
|
||||
env:
|
||||
CASSANDRA_IMAGE: cassandra:5.0.6
|
||||
CASS_CONTAINER: cass-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
UTIL_CONTAINER: cass-util-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
CASS_VOLUME: cassandra-data-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
BACKUP_VOLUME: cassandra-backup-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
MAX_HEAP_SIZE: 2G
|
||||
HEAP_NEWSIZE: 512M
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set temp paths
|
||||
run: |
|
||||
set -euo pipefail
|
||||
: "${RUNNER_TEMP:?RUNNER_TEMP is not set}"
|
||||
echo "WORKDIR=$RUNNER_TEMP/cassandra-restore-test" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Pre-clean
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker rm -f "${CASS_CONTAINER}" "${UTIL_CONTAINER}" 2>/dev/null || true
|
||||
docker volume rm "${CASS_VOLUME}" 2>/dev/null || true
|
||||
docker volume rm "${BACKUP_VOLUME}" 2>/dev/null || true
|
||||
rm -rf "${WORKDIR}" 2>/dev/null || true
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y --no-install-recommends rclone age ca-certificates
|
||||
|
||||
- name: Find latest backup, validate freshness, download, decrypt, extract into Docker volume
|
||||
env:
|
||||
B2_KEY_ID: ${{ secrets.B2_KEY_ID }}
|
||||
B2_APPLICATION_KEY: ${{ secrets.B2_APPLICATION_KEY }}
|
||||
AGE_PRIVATE_KEY: ${{ secrets.CASSANDRA_AGE_PRIVATE_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
rm -rf "$WORKDIR"
|
||||
mkdir -p "$WORKDIR"
|
||||
|
||||
export RCLONE_CONFIG_B2S3_TYPE=s3
|
||||
export RCLONE_CONFIG_B2S3_PROVIDER=Other
|
||||
export RCLONE_CONFIG_B2S3_ACCESS_KEY_ID="${B2_KEY_ID}"
|
||||
export RCLONE_CONFIG_B2S3_SECRET_ACCESS_KEY="${B2_APPLICATION_KEY}"
|
||||
export RCLONE_CONFIG_B2S3_ENDPOINT="https://s3.eu-central-003.backblazeb2.com"
|
||||
export RCLONE_CONFIG_B2S3_REGION="eu-central-003"
|
||||
export RCLONE_CONFIG_B2S3_FORCE_PATH_STYLE=true
|
||||
|
||||
LATEST_BACKUP="$(
|
||||
rclone lsf "B2S3:fluxer" --recursive --files-only --fast-list \
|
||||
| grep -E '(^|/)cassandra-backup-[0-9]{8}-[0-9]{6}\.tar\.age$' \
|
||||
| sort -r \
|
||||
| head -n 1
|
||||
)"
|
||||
|
||||
if [ -z "${LATEST_BACKUP}" ]; then
|
||||
echo "Error: No backup found in bucket"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "LATEST_BACKUP=${LATEST_BACKUP}" >> "$GITHUB_ENV"
|
||||
|
||||
base="$(basename "${LATEST_BACKUP}")"
|
||||
ts="${base#cassandra-backup-}"
|
||||
ts="${ts%.tar.age}"
|
||||
|
||||
if ! [[ "$ts" =~ ^[0-9]{8}-[0-9]{6}$ ]]; then
|
||||
echo "Error: Could not extract timestamp from backup filename: ${base}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_EPOCH="$(date -u -d "${ts:0:8} ${ts:9:2}:${ts:11:2}:${ts:13:2}" +%s)"
|
||||
CURRENT_EPOCH="$(date -u +%s)"
|
||||
AGE_HOURS=$(( (CURRENT_EPOCH - BACKUP_EPOCH) / 3600 ))
|
||||
|
||||
echo "Backup age: ${AGE_HOURS} hours"
|
||||
if [ "${AGE_HOURS}" -ge 3 ]; then
|
||||
echo "Error: Latest backup is ${AGE_HOURS} hours old (threshold: 3 hours)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rclone copyto "B2S3:fluxer/${LATEST_BACKUP}" "${WORKDIR}/backup.tar.age" --fast-list
|
||||
|
||||
umask 077
|
||||
printf '%s' "${AGE_PRIVATE_KEY}" > "${WORKDIR}/age.key"
|
||||
|
||||
docker volume create "${BACKUP_VOLUME}"
|
||||
|
||||
age -d -i "${WORKDIR}/age.key" "${WORKDIR}/backup.tar.age" \
|
||||
| docker run --rm -i \
|
||||
-v "${BACKUP_VOLUME}:/backup" \
|
||||
--entrypoint bash \
|
||||
"${CASSANDRA_IMAGE}" -lc '
|
||||
set -euo pipefail
|
||||
rm -rf /backup/*
|
||||
mkdir -p /backup/_tmp
|
||||
tar -C /backup/_tmp -xf -
|
||||
|
||||
top="$(find /backup/_tmp -maxdepth 1 -mindepth 1 -type d -name "cassandra-backup-*" | head -n 1 || true)"
|
||||
|
||||
if [ -n "$top" ] && [ -f "$top/schema.cql" ]; then
|
||||
cp -a "$top"/. /backup/
|
||||
elif [ -f /backup/_tmp/schema.cql ]; then
|
||||
cp -a /backup/_tmp/. /backup/
|
||||
else
|
||||
echo "Error: schema.cql not found after extraction"
|
||||
find /backup/_tmp -maxdepth 3 -type f -print | sed -n "1,80p" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf /backup/_tmp
|
||||
'
|
||||
|
||||
docker run --rm \
|
||||
-v "${BACKUP_VOLUME}:/backup:ro" \
|
||||
--entrypoint bash \
|
||||
"${CASSANDRA_IMAGE}" -lc '
|
||||
set -euo pipefail
|
||||
test -f /backup/schema.cql
|
||||
echo "Extracted backup layout (top 3 levels):"
|
||||
find /backup -maxdepth 3 -type d -print | sed -n "1,200p" || true
|
||||
echo "Sample SSTables (*Data.db):"
|
||||
find /backup -type f -name "*Data.db" | sed -n "1,30p" || true
|
||||
'
|
||||
|
||||
- name: Create data volume
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker volume create "${CASS_VOLUME}"
|
||||
|
||||
- name: Restore keyspaces into volume and promote snapshot SSTables
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
docker run --rm \
|
||||
--name "${UTIL_CONTAINER}" \
|
||||
-v "${CASS_VOLUME}:/var/lib/cassandra" \
|
||||
-v "${BACKUP_VOLUME}:/backup:ro" \
|
||||
--entrypoint bash \
|
||||
"${CASSANDRA_IMAGE}" -lc '
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
BASE=/var/lib/cassandra
|
||||
DATA_DIR="$BASE/data"
|
||||
mkdir -p "$DATA_DIR" "$BASE/commitlog" "$BASE/hints" "$BASE/saved_caches"
|
||||
|
||||
ROOT=/backup
|
||||
if [ -d "$ROOT/cassandra_data" ]; then ROOT="$ROOT/cassandra_data"; fi
|
||||
if [ -d "$ROOT/data" ]; then ROOT="$ROOT/data"; fi
|
||||
|
||||
echo "Using backup ROOT=$ROOT"
|
||||
echo "Restoring into DATA_DIR=$DATA_DIR"
|
||||
|
||||
restored=0
|
||||
for keyspace_dir in "$ROOT"/*/; do
|
||||
[ -d "$keyspace_dir" ] || continue
|
||||
ks="$(basename "$keyspace_dir")"
|
||||
|
||||
if [ "$ks" = "system_schema" ] || ! [[ "$ks" =~ ^system ]]; then
|
||||
echo "Restoring keyspace: $ks"
|
||||
rm -rf "$DATA_DIR/$ks"
|
||||
cp -a "$keyspace_dir" "$DATA_DIR/"
|
||||
restored=$((restored + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$restored" -le 0 ]; then
|
||||
echo "Error: No keyspaces restored from backup root: $ROOT"
|
||||
echo "Debug: listing $ROOT:"
|
||||
ls -la "$ROOT" || true
|
||||
find "$ROOT" -maxdepth 2 -type d -print | sed -n "1,100p" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
promoted=0
|
||||
for ks_dir in "$DATA_DIR"/*/; do
|
||||
[ -d "$ks_dir" ] || continue
|
||||
ks="$(basename "$ks_dir")"
|
||||
|
||||
if [ "$ks" != "system_schema" ] && [[ "$ks" =~ ^system ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for table_dir in "$ks_dir"*/; do
|
||||
[ -d "$table_dir" ] || continue
|
||||
|
||||
snap_root="$table_dir/snapshots"
|
||||
[ -d "$snap_root" ] || continue
|
||||
|
||||
latest_snap="$(ls -1d "$snap_root"/*/ 2>/dev/null | sort -r | head -n 1 || true)"
|
||||
[ -n "$latest_snap" ] || continue
|
||||
|
||||
files=( "$latest_snap"* )
|
||||
if [ "${#files[@]}" -gt 0 ]; then
|
||||
cp -av "${files[@]}" "$table_dir"
|
||||
promoted=$((promoted + $(ls -1 "$latest_snap"/*Data.db 2>/dev/null | wc -l || true)))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
chown -R cassandra:cassandra "$BASE"
|
||||
|
||||
echo "Promoted Data.db files: $promoted"
|
||||
if [ "$promoted" -le 0 ]; then
|
||||
echo "Error: No *Data.db files were promoted out of snapshots"
|
||||
echo "Debug: first snapshot dirs found:"
|
||||
find "$DATA_DIR" -type d -path "*/snapshots/*" | sed -n "1,50p" || true
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
- name: Start Cassandra
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
docker run -d \
|
||||
--name "${CASS_CONTAINER}" \
|
||||
-v "${CASS_VOLUME}:/var/lib/cassandra" \
|
||||
-e MAX_HEAP_SIZE="${MAX_HEAP_SIZE}" \
|
||||
-e HEAP_NEWSIZE="${HEAP_NEWSIZE}" \
|
||||
-e JVM_OPTS="-Dcassandra.disable_mlock=true" \
|
||||
"${CASSANDRA_IMAGE}"
|
||||
|
||||
for i in $(seq 1 150); do
|
||||
status="$(docker inspect -f '{{.State.Status}}' "${CASS_CONTAINER}" 2>/dev/null || true)"
|
||||
if [ "${status}" != "running" ]; then
|
||||
docker inspect "${CASS_CONTAINER}" --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}' || true
|
||||
docker logs --tail 300 "${CASS_CONTAINER}" || true
|
||||
exit 1
|
||||
fi
|
||||
if docker exec "${CASS_CONTAINER}" cqlsh -e "SELECT now() FROM system.local;" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
docker exec "${CASS_CONTAINER}" cqlsh -e "SELECT now() FROM system.local;" >/dev/null 2>&1
|
||||
|
||||
- name: Verify data
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
USER_COUNT=""
|
||||
for i in $(seq 1 20); do
|
||||
USER_COUNT="$(
|
||||
docker exec "${CASS_CONTAINER}" cqlsh -e "SELECT COUNT(*) FROM fluxer.users;" 2>/dev/null \
|
||||
| awk "/^[[:space:]]*[0-9]+[[:space:]]*$/ {print \$1; exit}" || true
|
||||
)"
|
||||
if [ -n "${USER_COUNT}" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -n "${USER_COUNT}" ] && [ "${USER_COUNT}" -gt 0 ] 2>/dev/null; then
|
||||
echo "Backup restore verification passed"
|
||||
else
|
||||
echo "Backup restore verification failed"
|
||||
docker logs --tail 300 "${CASS_CONTAINER}" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker rm -f "${CASS_CONTAINER}" 2>/dev/null || true
|
||||
docker volume rm "${CASS_VOLUME}" 2>/dev/null || true
|
||||
docker volume rm "${BACKUP_VOLUME}" 2>/dev/null || true
|
||||
rm -rf "${WORKDIR}" 2>/dev/null || true
|
||||
|
||||
- name: Report status
|
||||
if: always()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
LATEST_BACKUP_NAME="${LATEST_BACKUP:-unknown}"
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
echo "Backup ${LATEST_BACKUP_NAME} is valid and restorable"
|
||||
else
|
||||
echo "Backup ${LATEST_BACKUP_NAME} test failed"
|
||||
fi
|
||||
81
.github/workflows/update-geoip-db.yaml
vendored
Normal file
81
.github/workflows/update-geoip-db.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: update geoip-db
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: update-geoip-db
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
refresh-db:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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: Refresh MMDB on server & roll restart
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
ssh "${SERVER}" bash << EOSSH
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y curl
|
||||
fi
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y golang-go
|
||||
fi
|
||||
|
||||
export PATH="\$PATH:\$(go env GOPATH)/bin"
|
||||
if ! command -v mmdbverify >/dev/null 2>&1; then
|
||||
GOBIN="\$(go env GOPATH)/bin" go install github.com/maxmind/mmdbverify@latest
|
||||
fi
|
||||
|
||||
TMPDIR="\$(mktemp -d)"
|
||||
trap 'rm -rf "\$TMPDIR"' EXIT
|
||||
|
||||
DEST_DIR="/etc/fluxer"
|
||||
DEST_DB="\${DEST_DIR}/ipinfo_lite.mmdb"
|
||||
|
||||
mkdir -p "\$DEST_DIR"
|
||||
|
||||
curl -fsSL -o "\$TMPDIR/ipinfo_lite.mmdb" \
|
||||
"https://ipinfo.io/data/ipinfo_lite.mmdb?token=${IPINFO_TOKEN}"
|
||||
|
||||
[ -s "\$TMPDIR/ipinfo_lite.mmdb" ]
|
||||
|
||||
mmdbverify -file "\$TMPDIR/ipinfo_lite.mmdb"
|
||||
|
||||
install -m 0644 "\$TMPDIR/ipinfo_lite.mmdb" "\$DEST_DB.tmp"
|
||||
mv -f "\$DEST_DB.tmp" "\$DEST_DB"
|
||||
|
||||
docker service update --force fluxer-geoip_app
|
||||
EOSSH
|
||||
73
.github/workflows/update-word-lists.yaml
vendored
Normal file
73
.github/workflows/update-word-lists.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: update word-lists
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 1 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-word-lists:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: canary
|
||||
|
||||
- name: Download latest word lists
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL https://raw.githubusercontent.com/tailscale/tailscale/refs/heads/main/words/scales.txt -o /tmp/scales.txt
|
||||
curl -fsSL https://raw.githubusercontent.com/tailscale/tailscale/refs/heads/main/words/tails.txt -o /tmp/tails.txt
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Compare the downloaded files with the existing ones
|
||||
if ! diff -q /tmp/scales.txt fluxer_api/src/words/scales.txt > /dev/null 2>&1 || \
|
||||
! diff -q /tmp/tails.txt fluxer_api/src/words/tails.txt > /dev/null 2>&1; then
|
||||
printf 'changes_detected=true\n' >> "$GITHUB_OUTPUT"
|
||||
echo "Changes detected in word lists"
|
||||
else
|
||||
printf 'changes_detected=false\n' >> "$GITHUB_OUTPUT"
|
||||
echo "No changes detected in word lists"
|
||||
fi
|
||||
|
||||
- name: Update word lists
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp /tmp/scales.txt fluxer_api/src/words/scales.txt
|
||||
cp /tmp/tails.txt fluxer_api/src/words/tails.txt
|
||||
|
||||
- name: Create pull request for updated word lists
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: word-lists-update-${{ github.run_id }}
|
||||
base: canary
|
||||
title: "chore: update word lists from Tailscale upstream"
|
||||
body: |
|
||||
Automated update of scales.txt and tails.txt from the Tailscale repository.
|
||||
|
||||
These files are used to generate connection IDs for voice connections.
|
||||
|
||||
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"
|
||||
files: |
|
||||
fluxer_api/src/words/scales.txt
|
||||
fluxer_api/src/words/tails.txt
|
||||
labels: automation
|
||||
|
||||
- name: No changes detected
|
||||
if: steps.check_changes.outputs.changes_detected == 'false'
|
||||
run: echo "Word lists are already up to date."
|
||||
Reference in New Issue
Block a user