refactor progress
This commit is contained in:
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
custom: ['https://fluxer.app/donate']
|
||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -2,9 +2,9 @@
|
||||
|
||||
<!-- A few bullets is perfect: what changed, why it changed, and anything reviewers should pay attention to. -->
|
||||
|
||||
- **What:**
|
||||
- **Why:**
|
||||
- **Notes for reviewers:**
|
||||
- **What:**
|
||||
- **Why:**
|
||||
- **Notes for reviewers:**
|
||||
|
||||
## How to verify
|
||||
|
||||
@@ -16,18 +16,16 @@
|
||||
|
||||
## Tests
|
||||
|
||||
<!-- List what you ran, or explain why tests weren’t added/changed. -->
|
||||
<!-- List what you ran, or explain why tests weren't added/changed. -->
|
||||
|
||||
- [ ] Added/updated tests (where it makes sense)
|
||||
- [ ] Unit tests:
|
||||
- [ ] Integration tests:
|
||||
- [ ] Added/updated unit tests (where it makes sense)
|
||||
- [ ] Manual verification:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] PR targets `canary`
|
||||
- [ ] PR title follows Conventional Commits (mostly lowercase)
|
||||
- [ ] CI is green (or I’m actively addressing failures)
|
||||
- [ ] CI is green (or I'm actively addressing failures)
|
||||
- [ ] CLA signed (the bot will guide you on first PR)
|
||||
|
||||
## Screenshots / recordings (UI changes)
|
||||
|
||||
570
.github/workflows/build-desktop.yaml
vendored
570
.github/workflows/build-desktop.yaml
vendored
@@ -72,85 +72,59 @@ concurrency:
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel }}
|
||||
BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
|
||||
SOURCE_REF: ${{ inputs.ref && inputs.ref || (inputs.channel == 'canary' && 'canary' || 'main') }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
name: Resolve build metadata
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
pub_date: ${{ steps.meta.outputs.pub_date }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
build_channel: ${{ steps.meta.outputs.build_channel }}
|
||||
source_ref: ${{ steps.meta.outputs.source_ref }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Set metadata
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="0.0.${GITHUB_RUN_NUMBER}"
|
||||
PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "pub_date=${PUB_DATE}" >> "$GITHUB_OUTPUT"
|
||||
echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT"
|
||||
echo "build_channel=${{ inputs.channel == 'canary' && 'canary' || 'stable' }}" >> "$GITHUB_OUTPUT"
|
||||
echo "source_ref=${{ (inputs.ref && inputs.ref) || (inputs.channel == 'canary' && 'canary' || 'main') }}" >> "$GITHUB_OUTPUT"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step set_metadata
|
||||
--channel "${{ inputs.channel }}"
|
||||
--ref "${{ inputs.ref }}"
|
||||
|
||||
matrix:
|
||||
name: Resolve build matrix
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Build platform matrix
|
||||
id: set-matrix
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
PLATFORMS='[
|
||||
{"platform":"windows","arch":"x64","os":"windows-latest","electron_arch":"x64"},
|
||||
{"platform":"windows","arch":"arm64","os":"windows-11-arm","electron_arch":"arm64"},
|
||||
{"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"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step set_matrix
|
||||
--skip-windows "${{ inputs.skip_windows }}"
|
||||
--skip-windows-x64 "${{ inputs.skip_windows_x64 }}"
|
||||
--skip-windows-arm64 "${{ inputs.skip_windows_arm64 }}"
|
||||
--skip-macos "${{ inputs.skip_macos }}"
|
||||
--skip-macos-x64 "${{ inputs.skip_macos_x64 }}"
|
||||
--skip-macos-arm64 "${{ inputs.skip_macos_arm64 }}"
|
||||
--skip-linux "${{ inputs.skip_linux }}"
|
||||
--skip-linux-x64 "${{ inputs.skip_linux_x64 }}"
|
||||
--skip-linux-arm64 "${{ inputs.skip_linux_arm64 }}"
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.platform }} (${{ matrix.arch }})
|
||||
@@ -158,70 +132,55 @@ jobs:
|
||||
- meta
|
||||
- matrix
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
|
||||
env:
|
||||
APP_WORKDIR: fluxer_app
|
||||
CHANNEL: ${{ needs.meta.outputs.channel }}
|
||||
BUILD_CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||
SOURCE_REF: ${{ needs.meta.outputs.source_ref }}
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
ARCH: ${{ matrix.arch }}
|
||||
ELECTRON_ARCH: ${{ matrix.electron_arch }}
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
ref: ${{ inputs.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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step windows_paths
|
||||
|
||||
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 workdir (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step set_workdir_unix
|
||||
|
||||
- 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
|
||||
node-version: 24
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step resolve_pnpm_store_windows
|
||||
|
||||
- name: Resolve pnpm store path (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
store="$(pnpm store path --silent)"
|
||||
echo "PNPM_STORE_PATH=$store" >> "$GITHUB_ENV"
|
||||
mkdir -p "$store"
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step resolve_pnpm_store_unix
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
@@ -233,44 +192,58 @@ jobs:
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step install_setuptools_windows_arm64
|
||||
|
||||
- name: Install Python setuptools (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
run: brew install python-setuptools
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step install_setuptools_macos
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step install_linux_deps
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step install_dependencies
|
||||
|
||||
- name: Update version
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
run: pnpm version "${{ env.VERSION }}" --no-git-tag-version --allow-same-version
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step update_version
|
||||
|
||||
- name: Build Electron main process
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
- name: Set build channel
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
run: pnpm electron:compile
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step set_build_channel
|
||||
|
||||
- name: Build Electron main process
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step build_electron_main
|
||||
|
||||
- name: Build Electron app (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -278,176 +251,82 @@ jobs:
|
||||
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.config.cjs --mac --${{ matrix.electron_arch }}
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step build_app_macos
|
||||
|
||||
- name: Verify macOS bundle ID (fail fast if wrong channel)
|
||||
if: matrix.platform == 'macos'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
shell: bash
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DIST="dist-electron"
|
||||
ZIP="$(ls -1 "$DIST"/*"${{ matrix.electron_arch }}"*.zip | head -n1)"
|
||||
tmp="$(mktemp -d)"
|
||||
ditto -xk "$ZIP" "$tmp"
|
||||
APP="$(find "$tmp" -maxdepth 2 -name "*.app" -print -quit)"
|
||||
BID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist")
|
||||
|
||||
expected="app.fluxer"
|
||||
if [[ "${BUILD_CHANNEL:-stable}" == "canary" ]]; then expected="app.fluxer.canary"; fi
|
||||
echo "Bundle id in zip: $BID (expected: $expected)"
|
||||
test "$BID" = "$expected"
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step verify_bundle_id
|
||||
|
||||
- name: Build Electron app (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
TEMP: C:\t
|
||||
TMP: C:\t
|
||||
SQUIRREL_TEMP: C:\sq
|
||||
ELECTRON_BUILDER_CACHE: C:\ebcache
|
||||
run: pnpm exec electron-builder --config electron-builder.config.cjs --win --${{ matrix.electron_arch }}
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step build_app_windows
|
||||
|
||||
- name: Analyze Squirrel nupkg for long paths
|
||||
if: matrix.platform == 'windows'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
shell: pwsh
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_VERSION: ${{ env.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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step analyse_squirrel_paths
|
||||
|
||||
- name: Build Electron app (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
working-directory: ${{ env.APP_WORKDIR }}
|
||||
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
|
||||
env:
|
||||
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
|
||||
USE_SYSTEM_FPM: true
|
||||
run: pnpm exec electron-builder --config electron-builder.config.cjs --linux --${{ matrix.electron_arch }}
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step build_app_linux
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step prepare_artifacts_windows
|
||||
|
||||
- name: Prepare artifacts (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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/
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step prepare_artifacts_unix
|
||||
|
||||
- name: Normalize updater YAML (arm64)
|
||||
if: matrix.arch == 'arm64'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd upload_staging
|
||||
[[ "${{ matrix.platform }}" == "macos" && -f latest-mac.yml && ! -f latest-mac-arm64.yml ]] && mv latest-mac.yml latest-mac-arm64.yml || true
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step normalise_updater_yaml
|
||||
|
||||
- name: Generate SHA256 checksums (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step generate_checksums_unix
|
||||
|
||||
- name: Generate SHA256 checksums (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/build_desktop.py
|
||||
--step generate_checksums_windows
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -456,16 +335,24 @@ jobs:
|
||||
path: |
|
||||
upload_staging/*.exe
|
||||
upload_staging/*.exe.blockmap
|
||||
upload_staging/*.exe.sha256
|
||||
upload_staging/*.dmg
|
||||
upload_staging/*.dmg.sha256
|
||||
upload_staging/*.zip
|
||||
upload_staging/*.zip.blockmap
|
||||
upload_staging/*.zip.sha256
|
||||
upload_staging/*.AppImage
|
||||
upload_staging/*.AppImage.sha256
|
||||
upload_staging/*.deb
|
||||
upload_staging/*.deb.sha256
|
||||
upload_staging/*.rpm
|
||||
upload_staging/*.rpm.sha256
|
||||
upload_staging/*.tar.gz
|
||||
upload_staging/*.tar.gz.sha256
|
||||
upload_staging/*.yml
|
||||
upload_staging/*.nupkg
|
||||
upload_staging/*.nupkg.blockmap
|
||||
upload_staging/*.nupkg.sha256
|
||||
upload_staging/RELEASES*
|
||||
retention-days: 30
|
||||
|
||||
@@ -474,16 +361,25 @@ jobs:
|
||||
needs:
|
||||
- meta
|
||||
- build
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CHANNEL: ${{ needs.meta.outputs.build_channel }}
|
||||
DISPLAY_CHANNEL: ${{ needs.meta.outputs.channel }}
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
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: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -491,175 +387,29 @@ jobs:
|
||||
pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-*
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step install_rclone
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step configure_rclone
|
||||
|
||||
- name: Build S3 payload layout (+ manifest.json)
|
||||
env:
|
||||
VERSION: ${{ needs.meta.outputs.version }}
|
||||
PUB_DATE: ${{ needs.meta.outputs.pub_date }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p s3_payload
|
||||
|
||||
shopt -s nullglob
|
||||
for dir in artifacts/fluxer-desktop-${CHANNEL}-*; do
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
base="$(basename "$dir")"
|
||||
if [[ "$base" =~ ^fluxer-desktop-[a-z]+-([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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step build_payload
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step upload_payload
|
||||
|
||||
- name: Build summary
|
||||
run: |
|
||||
{
|
||||
echo "## Desktop ${DISPLAY_CHANNEL^} Upload Complete"
|
||||
echo ""
|
||||
echo "**Version:** ${{ needs.meta.outputs.version }}"
|
||||
echo ""
|
||||
echo "**S3 prefix:** desktop/${CHANNEL}/"
|
||||
echo ""
|
||||
echo "**Redirect endpoint shape:** /dl/desktop/${CHANNEL}/{plat}/{arch}/{format}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/build_desktop.py
|
||||
--step build_summary
|
||||
|
||||
70
.github/workflows/channel-vars.yaml
vendored
70
.github/workflows/channel-vars.yaml
vendored
@@ -8,15 +8,9 @@ on:
|
||||
github_ref_name:
|
||||
type: string
|
||||
required: false
|
||||
github_ref:
|
||||
type: string
|
||||
required: false
|
||||
workflow_dispatch_channel:
|
||||
type: string
|
||||
required: false
|
||||
workflow_dispatch_ref:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
channel:
|
||||
@@ -25,9 +19,6 @@ on:
|
||||
is_canary:
|
||||
description: 'Whether this is a canary deploy (true|false)'
|
||||
value: ${{ jobs.emit.outputs.is_canary }}
|
||||
source_ref:
|
||||
description: 'Git ref to check out for the deploy'
|
||||
value: ${{ jobs.emit.outputs.source_ref }}
|
||||
stack_suffix:
|
||||
description: "Suffix for stack/image names ('' or '-canary')"
|
||||
value: ${{ jobs.emit.outputs.stack_suffix }}
|
||||
@@ -35,60 +26,23 @@ on:
|
||||
jobs:
|
||||
emit:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
outputs:
|
||||
channel: ${{ steps.compute.outputs.channel }}
|
||||
is_canary: ${{ steps.compute.outputs.is_canary }}
|
||||
source_ref: ${{ steps.compute.outputs.source_ref }}
|
||||
stack_suffix: ${{ steps.compute.outputs.stack_suffix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Determine channel
|
||||
id: compute
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
event_name="${{ inputs.github_event_name }}"
|
||||
ref_name="${{ inputs.github_ref_name || '' }}"
|
||||
ref="${{ inputs.github_ref || '' }}"
|
||||
dispatch_channel="${{ inputs.workflow_dispatch_channel || '' }}"
|
||||
dispatch_ref="${{ inputs.workflow_dispatch_ref || '' }}"
|
||||
|
||||
channel="stable"
|
||||
if [[ "${event_name}" == "push" ]]; then
|
||||
if [[ "${ref_name}" == "canary" ]]; then
|
||||
channel="canary"
|
||||
fi
|
||||
else
|
||||
if [[ "${dispatch_channel}" == "canary" ]]; then
|
||||
channel="canary"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${event_name}" == "push" ]]; then
|
||||
source_ref="${ref:-refs/heads/${ref_name:-main}}"
|
||||
else
|
||||
if [[ -n "${dispatch_ref}" ]]; then
|
||||
source_ref="${dispatch_ref}"
|
||||
else
|
||||
if [[ "${channel}" == "canary" ]]; then
|
||||
source_ref="refs/heads/canary"
|
||||
else
|
||||
source_ref="refs/heads/main"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
stack_suffix=""
|
||||
if [[ "${channel}" == "canary" ]]; then
|
||||
stack_suffix="-canary"
|
||||
fi
|
||||
|
||||
is_canary="false"
|
||||
if [[ "${channel}" == "canary" ]]; then
|
||||
is_canary="true"
|
||||
fi
|
||||
|
||||
printf 'channel=%s\n' "${channel}" >> "$GITHUB_OUTPUT"
|
||||
printf 'is_canary=%s\n' "${is_canary}" >> "$GITHUB_OUTPUT"
|
||||
printf 'source_ref=%s\n' "${source_ref}" >> "$GITHUB_OUTPUT"
|
||||
printf 'stack_suffix=%s\n' "${stack_suffix}" >> "$GITHUB_OUTPUT"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/channel_vars.py
|
||||
--event-name "${{ inputs.github_event_name }}"
|
||||
--ref-name "${{ inputs.github_ref_name || '' }}"
|
||||
--dispatch-channel "${{ inputs.workflow_dispatch_channel || '' }}"
|
||||
|
||||
137
.github/workflows/ci.yaml
vendored
Normal file
137
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python3 scripts/ci/workflows/ci.py --step install_dependencies
|
||||
|
||||
- name: Run typecheck
|
||||
run: python3 scripts/ci/workflows/ci.py --step typecheck
|
||||
env:
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
|
||||
test:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python3 scripts/ci/workflows/ci.py --step install_dependencies
|
||||
|
||||
- name: Run tests
|
||||
run: python3 scripts/ci/workflows/ci.py --step test
|
||||
env:
|
||||
FLUXER_CONFIG: config/config.test.json
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
|
||||
gateway:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: '28'
|
||||
rebar3-version: '3.24.0'
|
||||
|
||||
- name: Cache rebar3 dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
fluxer_gateway/_build
|
||||
~/.cache/rebar3
|
||||
key: rebar3-${{ runner.os }}-${{ hashFiles('fluxer_gateway/rebar.lock') }}
|
||||
restore-keys: |
|
||||
rebar3-${{ runner.os }}-
|
||||
|
||||
- name: Compile
|
||||
run: python3 scripts/ci/workflows/ci.py --step gateway_compile
|
||||
|
||||
- name: Run dialyzer
|
||||
run: python3 scripts/ci/workflows/ci.py --step gateway_dialyzer
|
||||
|
||||
- name: Run eunit tests
|
||||
run: python3 scripts/ci/workflows/ci.py --step gateway_eunit
|
||||
env:
|
||||
FLUXER_CONFIG: ../config/config.test.json
|
||||
|
||||
knip:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: python3 scripts/ci/workflows/ci.py --step install_dependencies
|
||||
|
||||
- name: Run knip
|
||||
run: python3 scripts/ci/workflows/ci.py --step knip
|
||||
env:
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
|
||||
ci-scripts:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Sync ci python dependencies
|
||||
run: python3 scripts/ci/workflows/ci_scripts.py --step sync
|
||||
|
||||
- name: Run ci python tests
|
||||
run: python3 scripts/ci/workflows/ci_scripts.py --step test
|
||||
125
.github/workflows/deploy-admin.yaml
vendored
125
.github/workflows/deploy-admin.yaml
vendored
@@ -16,12 +16,12 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
description: Channel to deploy
|
||||
description: Release channel to deploy
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref to deploy (defaults to main/canary based on channel)
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-admin-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }}
|
||||
@@ -35,43 +35,33 @@ jobs:
|
||||
with:
|
||||
github_event_name: ${{ github.event_name }}
|
||||
github_ref_name: ${{ github.ref_name }}
|
||||
github_ref: ${{ github.ref }}
|
||||
workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
|
||||
deploy:
|
||||
name: Deploy admin
|
||||
needs: channel-vars
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }}
|
||||
SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }}
|
||||
STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }}
|
||||
STACK: ${{ format('fluxer-admin{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
CACHE_SCOPE: ${{ format('deploy-fluxer-admin{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'admin.canary.fluxer.app' || 'admin.fluxer.app' }}
|
||||
APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }}
|
||||
API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }}
|
||||
ADMIN_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app' || 'https://admin.fluxer.app' }}
|
||||
ADMIN_REDIRECT_URI: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app/oauth2_callback' || 'https://admin.fluxer.app/oauth2_callback' }}
|
||||
REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }}
|
||||
RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha=$(git rev-parse HEAD)
|
||||
echo "Deploying commit ${sha}"
|
||||
printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/deploy_admin.py --step record_deploy_commit
|
||||
|
||||
- name: Set build timestamp
|
||||
run: echo "BUILD_TIMESTAMP=$(date -u +%s)" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/deploy_admin.py --step set_build_timestamp
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -85,7 +75,7 @@ jobs:
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: fluxer_admin
|
||||
context: .
|
||||
file: fluxer_admin/Dockerfile
|
||||
tags: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
@@ -93,18 +83,16 @@ jobs:
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_admin.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -112,96 +100,13 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_admin.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
STACK: ${{ env.STACK }}
|
||||
APP_ENDPOINT: ${{ env.APP_ENDPOINT }}
|
||||
API_PUBLIC_ENDPOINT: ${{ env.API_PUBLIC_ENDPOINT }}
|
||||
ADMIN_ENDPOINT: ${{ env.ADMIN_ENDPOINT }}
|
||||
ADMIN_REDIRECT_URI: ${{ env.ADMIN_REDIRECT_URI }}
|
||||
CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }}
|
||||
REPLICAS: ${{ env.REPLICAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker pussh "${IMAGE_TAG}" "${SERVER}"
|
||||
|
||||
ssh "${SERVER}" \
|
||||
"IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} APP_ENDPOINT=${APP_ENDPOINT} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} ADMIN_ENDPOINT=${ADMIN_ENDPOINT} ADMIN_REDIRECT_URI=${ADMIN_REDIRECT_URI} CADDY_DOMAIN=${CADDY_DOMAIN} REPLICAS=${REPLICAS} bash" << 'EOF'
|
||||
set -euo pipefail
|
||||
sudo mkdir -p "/opt/${STACK}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${STACK}"
|
||||
cd "/opt/${STACK}"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
x-deploy-base: &deploy_base
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
|
||||
x-healthcheck: &healthcheck
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8080/']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
FLUXER_API_PUBLIC_ENDPOINT: ${API_PUBLIC_ENDPOINT}
|
||||
FLUXER_APP_ENDPOINT: ${APP_ENDPOINT}
|
||||
FLUXER_MEDIA_ENDPOINT: https://fluxerusercontent.com
|
||||
FLUXER_CDN_ENDPOINT: https://fluxerstatic.com
|
||||
FLUXER_ADMIN_ENDPOINT: ${ADMIN_ENDPOINT}
|
||||
FLUXER_PATH_ADMIN: /
|
||||
APP_MODE: admin
|
||||
FLUXER_ADMIN_PORT: 8080
|
||||
ADMIN_OAUTH2_REDIRECT_URI: ${ADMIN_REDIRECT_URI}
|
||||
ADMIN_OAUTH2_CLIENT_ID: 1440355698178071552
|
||||
ADMIN_OAUTH2_AUTO_CREATE: "false"
|
||||
FLUXER_METRICS_HOST: fluxer-metrics_app:8080
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: ${REPLICAS}
|
||||
labels:
|
||||
- "caddy=${CADDY_DOMAIN}"
|
||||
- 'caddy.reverse_proxy={{upstreams 8080}}'
|
||||
- 'caddy.header.X-Robots-Tag="noindex, nofollow, nosnippet, noimageindex"'
|
||||
- 'caddy.header.Strict-Transport-Security="max-age=31536000; includeSubDomains; preload"'
|
||||
- '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: *healthcheck
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/deploy_admin.py --step push_and_deploy
|
||||
|
||||
286
.github/workflows/deploy-api.yaml
vendored
286
.github/workflows/deploy-api.yaml
vendored
@@ -16,12 +16,12 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
description: Channel to deploy
|
||||
description: Release channel to deploy
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref to deploy (defaults to main/canary based on channel)
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-api-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }}
|
||||
@@ -36,48 +36,33 @@ jobs:
|
||||
with:
|
||||
github_event_name: ${{ github.event_name }}
|
||||
github_ref_name: ${{ github.ref_name }}
|
||||
github_ref: ${{ github.ref }}
|
||||
workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
|
||||
deploy:
|
||||
name: Deploy api
|
||||
needs: channel-vars
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }}
|
||||
SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }}
|
||||
STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }}
|
||||
|
||||
STACK: ${{ format('fluxer-api{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
WORKER_STACK: ${{ format('fluxer-api-worker{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
WORKER_STACK: fluxer-api-worker
|
||||
CANARY_WORKER_REPLICAS: 3
|
||||
CACHE_SCOPE: ${{ format('deploy-fluxer-api{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
|
||||
API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }}
|
||||
API_CLIENT_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app/api' || 'https://web.fluxer.app/api' }}
|
||||
APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }}
|
||||
|
||||
MARKETING_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://canary.fluxer.app' || 'https://fluxer.app' }}
|
||||
|
||||
ADMIN_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app' || 'https://admin.fluxer.app' }}
|
||||
ADMIN_REDIRECT_URI: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://admin.canary.fluxer.app/oauth2_callback' || 'https://admin.fluxer.app/oauth2_callback' }}
|
||||
|
||||
CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'api.canary.fluxer.app' || 'api.fluxer.app' }}
|
||||
|
||||
RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha=$(git rev-parse HEAD)
|
||||
echo "Deploying commit ${sha}"
|
||||
printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/deploy_api.py --step record_deploy_commit
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -91,7 +76,7 @@ jobs:
|
||||
- name: Build image(s)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: fluxer_api
|
||||
context: .
|
||||
file: fluxer_api/Dockerfile
|
||||
tags: |
|
||||
${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
@@ -100,17 +85,17 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.SENTRY_BUILD_SHA }}
|
||||
BUILD_NUMBER=${{ env.SENTRY_BUILD_NUMBER }}
|
||||
BUILD_TIMESTAMP=${{ env.SENTRY_BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_api.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -118,240 +103,17 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_api.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image(s) and deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
IMAGE_TAG_APP: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
IMAGE_TAG_WORKER: ${{ env.WORKER_STACK }}:${{ env.DEPLOY_SHA }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
docker pussh "${IMAGE_TAG_APP}" "${SERVER}"
|
||||
|
||||
if [[ "${IS_CANARY}" == "true" ]]; then
|
||||
docker pussh "${IMAGE_TAG_WORKER}" "${SERVER}"
|
||||
fi
|
||||
|
||||
ssh "${SERVER}" \
|
||||
"IMAGE_TAG_APP=${IMAGE_TAG_APP} IMAGE_TAG_WORKER=${IMAGE_TAG_WORKER} STACK=${STACK} WORKER_STACK=${WORKER_STACK} IS_CANARY=${IS_CANARY} APP_ENDPOINT=${APP_ENDPOINT} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} API_CLIENT_ENDPOINT=${API_CLIENT_ENDPOINT} MARKETING_ENDPOINT=${MARKETING_ENDPOINT} ADMIN_ENDPOINT=${ADMIN_ENDPOINT} ADMIN_REDIRECT_URI=${ADMIN_REDIRECT_URI} CADDY_DOMAIN=${CADDY_DOMAIN} bash" << 'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
write_runtime_env() {
|
||||
local dir="$1"
|
||||
cat > "${dir}/runtime.env" << ENVEOF
|
||||
NODE_ENV=production
|
||||
FLUXER_API_PORT=8080
|
||||
SENTRY_DSN=https://bb16e8b823b82d788db49a666b3b4b90@o4510149383094272.ingest.us.sentry.io/4510205804019712
|
||||
|
||||
CASSANDRA_HOSTS=cassandra
|
||||
CASSANDRA_KEYSPACE=fluxer
|
||||
CASSANDRA_LOCAL_DC=dc1
|
||||
|
||||
FLUXER_GATEWAY_RPC_HOST=fluxer-gateway_app
|
||||
FLUXER_GATEWAY_RPC_PORT=8081
|
||||
|
||||
FLUXER_MEDIA_PROXY_HOST=fluxer-media-proxy_app
|
||||
FLUXER_MEDIA_PROXY_PORT=8080
|
||||
|
||||
FLUXER_METRICS_HOST=fluxer-metrics_app:8080
|
||||
|
||||
FLUXER_API_CLIENT_ENDPOINT=${API_CLIENT_ENDPOINT}
|
||||
FLUXER_APP_ENDPOINT=${APP_ENDPOINT}
|
||||
FLUXER_CDN_ENDPOINT=https://fluxerstatic.com
|
||||
FLUXER_MEDIA_ENDPOINT=https://fluxerusercontent.com
|
||||
FLUXER_INVITE_ENDPOINT=https://fluxer.gg
|
||||
FLUXER_GIFT_ENDPOINT=https://fluxer.gift
|
||||
|
||||
AWS_S3_ENDPOINT=https://s3.us-east-va.io.cloud.ovh.us
|
||||
AWS_S3_BUCKET_CDN=fluxer
|
||||
AWS_S3_BUCKET_UPLOADS=fluxer-uploads
|
||||
AWS_S3_BUCKET_REPORTS=fluxer-reports
|
||||
AWS_S3_BUCKET_HARVESTS=fluxer-harvests
|
||||
AWS_S3_BUCKET_DOWNLOADS=fluxer-downloads
|
||||
|
||||
SENDGRID_FROM_EMAIL=noreply@fluxer.app
|
||||
SENDGRID_FROM_NAME=Fluxer
|
||||
SENDGRID_WEBHOOK_PUBLIC_KEY=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoeqQS37o9s8ZcLBJUtT4hghAmI5RqsvcQ0OvsUn3XPfl7GkjxljufyxuL8+m1mCHP2IA1jdYT3kJQoQYXP6ZpQ==
|
||||
|
||||
FLUXER_API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT}
|
||||
FLUXER_GATEWAY_ENDPOINT=wss://gateway.fluxer.app
|
||||
|
||||
FLUXER_MARKETING_ENDPOINT=${MARKETING_ENDPOINT}
|
||||
FLUXER_PATH_MARKETING=/
|
||||
|
||||
FLUXER_ADMIN_ENDPOINT=${ADMIN_ENDPOINT}
|
||||
FLUXER_PATH_ADMIN=/
|
||||
|
||||
ADMIN_OAUTH2_CLIENT_ID=1440355698178071552
|
||||
ADMIN_OAUTH2_REDIRECT_URI=${ADMIN_REDIRECT_URI}
|
||||
ADMIN_OAUTH2_AUTO_CREATE=false
|
||||
|
||||
PASSKEYS_ENABLED=true
|
||||
PASSKEY_RP_NAME=Fluxer
|
||||
PASSKEY_RP_ID=fluxer.app
|
||||
PASSKEY_ALLOWED_ORIGINS=https://web.fluxer.app,https://web.canary.fluxer.app
|
||||
|
||||
CAPTCHA_ENABLED=true
|
||||
CAPTCHA_PRIMARY_PROVIDER=turnstile
|
||||
HCAPTCHA_SITE_KEY=9cbad400-df84-4e0c-bda6-e65000be78aa
|
||||
TURNSTILE_SITE_KEY=0x4AAAAAAB_lAoDdTWznNHMq
|
||||
|
||||
EMAIL_ENABLED=true
|
||||
SMS_ENABLED=true
|
||||
VOICE_ENABLED=true
|
||||
|
||||
SEARCH_ENABLED=true
|
||||
MEILISEARCH_URL=http://meilisearch:7700
|
||||
|
||||
STRIPE_ENABLED=true
|
||||
STRIPE_PRICE_ID_MONTHLY_USD=price_1SJHZzFPC94Os7FdzBgvz0go
|
||||
STRIPE_PRICE_ID_YEARLY_USD=price_1SJHabFPC94Os7FdhSOWVfcr
|
||||
STRIPE_PRICE_ID_VISIONARY_USD=price_1SJHGTFPC94Os7FdWTyqvJZ8
|
||||
STRIPE_PRICE_ID_MONTHLY_EUR=price_1SJHaFFPC94Os7FdmcrGicXa
|
||||
STRIPE_PRICE_ID_YEARLY_EUR=price_1SJHarFPC94Os7Fddbyzr5I8
|
||||
STRIPE_PRICE_ID_VISIONARY_EUR=price_1SJHGnFPC94Os7FdZn23KkYB
|
||||
STRIPE_PRICE_ID_GIFT_VISIONARY_USD=price_1SKhWqFPC94Os7FdxRmQrg3k
|
||||
STRIPE_PRICE_ID_GIFT_VISIONARY_EUR=price_1SKhXrFPC94Os7FdcepLrJqr
|
||||
STRIPE_PRICE_ID_GIFT_1_MONTH_USD=price_1SJHHKFPC94Os7FdGwUs1EQg
|
||||
STRIPE_PRICE_ID_GIFT_1_YEAR_USD=price_1SJHHrFPC94Os7FdWrQN5tKl
|
||||
STRIPE_PRICE_ID_GIFT_1_MONTH_EUR=price_1SJHHaFPC94Os7FdwwpwhliW
|
||||
STRIPE_PRICE_ID_GIFT_1_YEAR_EUR=price_1SJHI5FPC94Os7Fd3DpLxb0D
|
||||
|
||||
FLUXER_VISIONARIES_GUILD_ID=1428504839258075143
|
||||
FLUXER_OPERATORS_GUILD_ID=1434192442151473226
|
||||
|
||||
CLOUDFLARE_PURGE_ENABLED=true
|
||||
|
||||
CLAMAV_ENABLED=true
|
||||
CLAMAV_HOST=clamav
|
||||
CLAMAV_PORT=3310
|
||||
|
||||
MAXMIND_DB_PATH=/data/GeoLite2-City.mmdb
|
||||
|
||||
VAPID_PUBLIC_KEY=BEIwQxIwfj6m90tLYAR0AU_GJWU4kw8J_zJcHQG55NCUWSyRy-dzMOgvxk8yEDwdVyJZa6xUL4fmwngijq8T2pY
|
||||
ENVEOF
|
||||
}
|
||||
|
||||
deploy_api_stack() {
|
||||
sudo mkdir -p "/opt/${STACK}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${STACK}"
|
||||
cd "/opt/${STACK}"
|
||||
|
||||
write_runtime_env "$(pwd)"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
x-deploy-base: &deploy_base
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
|
||||
x-healthcheck: &healthcheck
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG_APP}
|
||||
command: ['npm', 'run', 'start']
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
- ./runtime.env
|
||||
volumes:
|
||||
- /opt/geoip/GeoLite2-City.mmdb:/data/GeoLite2-City.mmdb:ro
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: 2
|
||||
labels:
|
||||
- "caddy=${CADDY_DOMAIN}"
|
||||
- 'caddy.reverse_proxy={{upstreams 8080}}'
|
||||
- 'caddy.header.Strict-Transport-Security="max-age=31536000; includeSubDomains; preload"'
|
||||
- 'caddy.header.X-Xss-Protection="1; mode=block"'
|
||||
- 'caddy.header.X-Content-Type-Options=nosniff'
|
||||
- 'caddy.header.Referrer-Policy=strict-origin-when-cross-origin'
|
||||
- 'caddy.header.X-Frame-Options=DENY'
|
||||
- 'caddy.header.Expect-Ct="max-age=86400, report-uri=\\"https://o4510149383094272.ingest.us.sentry.io/api/4510205804019712/security/?sentry_key=bb16e8b823b82d788db49a666b3b4b90\\""'
|
||||
networks: [fluxer-shared]
|
||||
healthcheck: *healthcheck
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${STACK}"
|
||||
}
|
||||
|
||||
deploy_worker_stack_canary_only() {
|
||||
if [[ "${IS_CANARY}" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
sudo mkdir -p "/opt/${WORKER_STACK}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${WORKER_STACK}"
|
||||
cd "/opt/${WORKER_STACK}"
|
||||
|
||||
write_runtime_env "$(pwd)"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
x-deploy-base: &deploy_base
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
|
||||
services:
|
||||
worker:
|
||||
image: ${IMAGE_TAG_WORKER}
|
||||
command: ['npm', 'run', 'start:worker']
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
- ./runtime.env
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: 1
|
||||
networks: [fluxer-shared]
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${WORKER_STACK}"
|
||||
}
|
||||
|
||||
deploy_api_stack
|
||||
deploy_worker_stack_canary_only
|
||||
EOF
|
||||
CANARY_WORKER_REPLICAS: ${{ env.CANARY_WORKER_REPLICAS }}
|
||||
SENTRY_BUILD_SHA: ${{ env.SENTRY_BUILD_SHA }}
|
||||
SENTRY_BUILD_NUMBER: ${{ env.SENTRY_BUILD_NUMBER }}
|
||||
SENTRY_BUILD_TIMESTAMP: ${{ env.SENTRY_BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL: ${{ env.CHANNEL }}
|
||||
SENTRY_RELEASE: ${{ format('fluxer-api@{0}', env.SENTRY_BUILD_SHA) }}
|
||||
run: python3 scripts/ci/workflows/deploy_api.py --step push_and_deploy
|
||||
|
||||
264
.github/workflows/deploy-app.yaml
vendored
264
.github/workflows/deploy-app.yaml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_app/**
|
||||
- fluxer_app_proxy/**
|
||||
- .github/workflows/deploy-app.yaml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -16,12 +17,12 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
description: Channel to deploy
|
||||
description: Release channel to deploy
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref to deploy (defaults to main/canary based on channel)
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-app-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }}
|
||||
@@ -36,50 +37,33 @@ jobs:
|
||||
with:
|
||||
github_event_name: ${{ github.event_name }}
|
||||
github_ref_name: ${{ github.ref_name }}
|
||||
github_ref: ${{ github.ref }}
|
||||
workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
|
||||
deploy:
|
||||
name: Deploy app
|
||||
needs: channel-vars
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }}
|
||||
SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }}
|
||||
STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }}
|
||||
|
||||
SERVICE_NAME: ${{ format('fluxer-app{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
DOCKERFILE: fluxer_app/proxy/Dockerfile
|
||||
SENTRY_PROXY_PATH: /error-reporting-proxy
|
||||
DOCKERFILE: fluxer_app_proxy/Dockerfile
|
||||
CACHE_SCOPE: ${{ format('fluxer-app{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app/api' || 'https://web.fluxer.app/api' }}
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }}
|
||||
PUBLIC_PROJECT_ENV: ${{ needs.channel-vars.outputs.channel }}
|
||||
PUBLIC_SENTRY_DSN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.canary.fluxer.app/4510205815291904' || 'https://59ced0e2666ab83dd1ddb056cdd22d1b@sentry.web.fluxer.app/4510205815291904' }}
|
||||
|
||||
SENTRY_REPORT_HOST: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://sentry.web.canary.fluxer.app' || 'https://sentry.web.fluxer.app' }}
|
||||
API_TARGET: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'fluxer-api-canary_app' || 'fluxer-api_app' }}
|
||||
|
||||
CADDY_APP_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'web.canary.fluxer.app' || 'web.fluxer.app' }}
|
||||
SENTRY_CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'sentry.web.canary.fluxer.app' || 'sentry.web.fluxer.app' }}
|
||||
|
||||
RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
APP_REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
ref: ${{ inputs.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
|
||||
@@ -88,25 +72,18 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step install_dependencies
|
||||
|
||||
- name: Run Lingui i18n tasks
|
||||
working-directory: fluxer_app
|
||||
run: pnpm lingui:extract && pnpm lingui:compile --strict
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step run_lingui
|
||||
env:
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
|
||||
- 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"
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step record_deploy_commit
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -127,74 +104,45 @@ jobs:
|
||||
${{ 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
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step install_wasm_pack
|
||||
|
||||
- name: Generate wasm artifacts
|
||||
working-directory: fluxer_app
|
||||
run: pnpm wasm:codegen
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step generate_wasm
|
||||
env:
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
|
||||
- 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: python3 scripts/ci/workflows/deploy_app.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Fetch deployment config
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
RELEASE_CHANNEL: ${{ env.RELEASE_CHANNEL }}
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step fetch_deployment_config
|
||||
|
||||
- name: Build application
|
||||
working-directory: fluxer_app
|
||||
env:
|
||||
NODE_ENV: production
|
||||
PUBLIC_BOOTSTRAP_API_ENDPOINT: ${{ env.PUBLIC_BOOTSTRAP_API_ENDPOINT }}
|
||||
PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${{ env.PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT }}
|
||||
PUBLIC_API_VERSION: 1
|
||||
PUBLIC_PROJECT_ENV: ${{ env.PUBLIC_PROJECT_ENV }}
|
||||
PUBLIC_SENTRY_PROJECT_ID: 4510205815291904
|
||||
PUBLIC_SENTRY_PUBLIC_KEY: 59ced0e2666ab83dd1ddb056cdd22d1b
|
||||
PUBLIC_SENTRY_DSN: ${{ env.PUBLIC_SENTRY_DSN }}
|
||||
PUBLIC_SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }}
|
||||
PUBLIC_BUILD_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PUBLIC_BUILD_SHA=$(git rev-parse --short HEAD)
|
||||
export PUBLIC_BUILD_TIMESTAMP=$(date +%s)
|
||||
pnpm build
|
||||
cat > dist/version.json << EOF
|
||||
{
|
||||
"sha": "$PUBLIC_BUILD_SHA",
|
||||
"buildNumber": $PUBLIC_BUILD_NUMBER,
|
||||
"timestamp": $PUBLIC_BUILD_TIMESTAMP,
|
||||
"env": "$PUBLIC_PROJECT_ENV"
|
||||
}
|
||||
EOF
|
||||
FLUXER_CONFIG: ${{ github.workspace }}/fluxer_app/config.json
|
||||
TURBO_API: https://turborepo.fluxer.dev
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: team_fluxer
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step build_application
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step install_rclone
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step upload_assets
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -205,6 +153,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -215,28 +166,17 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step install_docker_pussh
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
@@ -246,108 +186,6 @@ jobs:
|
||||
SERVICE_NAME: ${{ env.SERVICE_NAME }}
|
||||
COMPOSE_STACK: ${{ env.SERVICE_NAME }}
|
||||
|
||||
SENTRY_DSN: https://59ced0e2666ab83dd1ddb056cdd22d1b@o4510149383094272.ingest.us.sentry.io/4510205815291904
|
||||
SENTRY_PROXY_PATH: ${{ env.SENTRY_PROXY_PATH }}
|
||||
SENTRY_REPORT_HOST: ${{ env.SENTRY_REPORT_HOST }}
|
||||
|
||||
CADDY_APP_DOMAIN: ${{ env.CADDY_APP_DOMAIN }}
|
||||
SENTRY_CADDY_DOMAIN: ${{ env.SENTRY_CADDY_DOMAIN }}
|
||||
API_TARGET: ${{ env.API_TARGET }}
|
||||
|
||||
RELEASE_CHANNEL: ${{ env.RELEASE_CHANNEL }}
|
||||
APP_REPLICAS: ${{ env.APP_REPLICAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker pussh "${IMAGE_TAG}" "${SERVER}"
|
||||
|
||||
ssh "${SERVER}" \
|
||||
"IMAGE_TAG=${IMAGE_TAG} SERVICE_NAME=${SERVICE_NAME} COMPOSE_STACK=${COMPOSE_STACK} SENTRY_DSN=${SENTRY_DSN} SENTRY_PROXY_PATH=${SENTRY_PROXY_PATH} SENTRY_REPORT_HOST=${SENTRY_REPORT_HOST} CADDY_APP_DOMAIN=${CADDY_APP_DOMAIN} SENTRY_CADDY_DOMAIN=${SENTRY_CADDY_DOMAIN} API_TARGET=${API_TARGET} RELEASE_CHANNEL=${RELEASE_CHANNEL} APP_REPLICAS=${APP_REPLICAS} bash" << 'EOF'
|
||||
set -euo pipefail
|
||||
sudo mkdir -p "/opt/${SERVICE_NAME}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${SERVICE_NAME}"
|
||||
cd "/opt/${SERVICE_NAME}"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
x-deploy-base: &deploy_base
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
|
||||
x-common-caddy-headers: &common_caddy_headers
|
||||
caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
|
||||
caddy.header.X-Xss-Protection: "1; mode=block"
|
||||
caddy.header.X-Content-Type-Options: "nosniff"
|
||||
caddy.header.Referrer-Policy: "strict-origin-when-cross-origin"
|
||||
caddy.header.X-Frame-Options: "DENY"
|
||||
caddy.header.Expect-Ct: "max-age=86400, report-uri=\\"${SENTRY_REPORT_HOST}/api/4510205815291904/security/?sentry_key=59ced0e2666ab83dd1ddb056cdd22d1b\\""
|
||||
caddy.header.Cache-Control: "no-store, no-cache, must-revalidate"
|
||||
caddy.header.Pragma: "no-cache"
|
||||
caddy.header.Expires: "0"
|
||||
|
||||
x-env-base: &env_base
|
||||
PORT: 8080
|
||||
RELEASE_CHANNEL: ${RELEASE_CHANNEL}
|
||||
FLUXER_METRICS_HOST: fluxer-metrics_app:8080
|
||||
SENTRY_DSN: ${SENTRY_DSN}
|
||||
SENTRY_REPORT_HOST: ${SENTRY_REPORT_HOST}
|
||||
|
||||
x-healthcheck: &healthcheck
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:8080/_health']
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: ${APP_REPLICAS}
|
||||
labels:
|
||||
<<: *common_caddy_headers
|
||||
caddy: ${CADDY_APP_DOMAIN}
|
||||
caddy.handle_path_0: /api*
|
||||
caddy.handle_path_0.reverse_proxy: "http://${API_TARGET}:8080"
|
||||
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||
environment:
|
||||
<<: *env_base
|
||||
SENTRY_PROXY_PATH: ${SENTRY_PROXY_PATH}
|
||||
networks: [fluxer-shared]
|
||||
healthcheck: *healthcheck
|
||||
|
||||
sentry:
|
||||
image: ${IMAGE_TAG}
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: 1
|
||||
labels:
|
||||
<<: *common_caddy_headers
|
||||
caddy: ${SENTRY_CADDY_DOMAIN}
|
||||
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||
environment:
|
||||
<<: *env_base
|
||||
SENTRY_PROXY_PATH: /
|
||||
networks: [fluxer-shared]
|
||||
healthcheck: *healthcheck
|
||||
|
||||
networks:
|
||||
fluxer-shared:
|
||||
external: true
|
||||
COMPOSEEOF
|
||||
|
||||
docker stack deploy \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${COMPOSE_STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/deploy_app.py --step push_and_deploy
|
||||
|
||||
193
.github/workflows/deploy-docs.yaml
vendored
193
.github/workflows/deploy-docs.yaml
vendored
@@ -1,193 +0,0 @@
|
||||
name: deploy docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_docs/**
|
||||
- .github/workflows/deploy-docs.yaml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
type: choice
|
||||
options:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
description: Channel to deploy
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref to deploy (defaults to main/canary based on channel)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-docs-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
channel-vars:
|
||||
uses: ./.github/workflows/channel-vars.yaml
|
||||
with:
|
||||
github_event_name: ${{ github.event_name }}
|
||||
github_ref_name: ${{ github.ref_name }}
|
||||
github_ref: ${{ github.ref }}
|
||||
workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
|
||||
deploy:
|
||||
name: Deploy docs
|
||||
needs: channel-vars
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }}
|
||||
SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }}
|
||||
STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }}
|
||||
|
||||
STACK: ${{ format('fluxer-docs{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
CACHE_SCOPE: ${{ format('deploy-fluxer-docs{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
|
||||
CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'docs.canary.fluxer.app' || 'docs.fluxer.app' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha=$(git rev-parse HEAD)
|
||||
echo "Deploying commit ${sha}"
|
||||
printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- 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: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
- name: Install docker-pussh
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
curl -fsSL https://raw.githubusercontent.com/psviderski/unregistry/v0.3.1/docker-pussh \
|
||||
-o ~/.docker/cli-plugins/docker-pussh
|
||||
chmod +x ~/.docker/cli-plugins/docker-pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }}
|
||||
|
||||
- name: Add server to known hosts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
STACK: ${{ env.STACK }}
|
||||
CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }}
|
||||
IS_CANARY: ${{ env.IS_CANARY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker pussh "${IMAGE_TAG}" "${SERVER}"
|
||||
|
||||
ssh "${SERVER}" "IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} CADDY_DOMAIN=${CADDY_DOMAIN} IS_CANARY=${IS_CANARY} bash" << 'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
sudo mkdir -p "/opt/${STACK}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${STACK}"
|
||||
cd "/opt/${STACK}"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
x-deploy-base: &deploy_base
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
deploy:
|
||||
<<: *deploy_base
|
||||
replicas: 2
|
||||
labels:
|
||||
caddy: "${CADDY_DOMAIN}"
|
||||
caddy.reverse_proxy: "{{upstreams 3000}}"
|
||||
COMPOSEEOF
|
||||
|
||||
if [[ "${IS_CANARY}" == "true" ]]; then
|
||||
cat >> compose.yaml << 'COMPOSEEOF'
|
||||
caddy.header.X-Robots-Tag: "noindex, nofollow, nosnippet, noimageindex"
|
||||
COMPOSEEOF
|
||||
fi
|
||||
|
||||
cat >> compose.yaml << 'COMPOSEEOF'
|
||||
caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
|
||||
caddy.header.X-Xss-Protection: "1; mode=block"
|
||||
caddy.header.X-Content-Type-Options: "nosniff"
|
||||
caddy.header.Referrer-Policy: "strict-origin-when-cross-origin"
|
||||
caddy.header.X-Frame-Options: "DENY"
|
||||
networks:
|
||||
- fluxer-shared
|
||||
healthcheck:
|
||||
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 \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${STACK}"
|
||||
EOF
|
||||
251
.github/workflows/deploy-gateway.yaml
vendored
251
.github/workflows/deploy-gateway.yaml
vendored
@@ -2,6 +2,12 @@ name: deploy gateway
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
@@ -18,13 +24,16 @@ permissions:
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy (hot patch)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: fluxer_gateway
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
sparse-checkout: |
|
||||
fluxer_gateway
|
||||
scripts/ci
|
||||
|
||||
- name: Set up Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
@@ -33,10 +42,7 @@ jobs:
|
||||
rebar3-version: '3.24.0'
|
||||
|
||||
- name: Compile
|
||||
working-directory: fluxer_gateway
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rebar3 as prod compile
|
||||
run: python3 scripts/ci/workflows/deploy_gateway.py --step compile
|
||||
|
||||
- name: Set up SSH
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -44,234 +50,13 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_gateway.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Record deploy commit
|
||||
run: python3 scripts/ci/workflows/deploy_gateway.py --step record_deploy_commit
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/deploy_gateway.py --step deploy
|
||||
|
||||
91
.github/workflows/deploy-kv.yaml
vendored
Normal file
91
.github/workflows/deploy-kv.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: deploy kv
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_kv/**
|
||||
- .github/workflows/deploy-kv.yaml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-kv
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy kv
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
STACK: fluxer-kv
|
||||
CACHE_SCOPE: deploy-fluxer-kv
|
||||
IS_CANARY: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: python3 scripts/ci/workflows/deploy_kv.py --step record_deploy_commit
|
||||
|
||||
- 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: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_kv.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_kv/Dockerfile
|
||||
tags: |
|
||||
${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=canary
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
- name: Install docker-pussh
|
||||
run: python3 scripts/ci/workflows/deploy_kv.py --step install_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: python3 scripts/ci/workflows/deploy_kv.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
run: python3 scripts/ci/workflows/deploy_kv.py --step push_and_deploy
|
||||
151
.github/workflows/deploy-marketing.yaml
vendored
151
.github/workflows/deploy-marketing.yaml
vendored
@@ -16,12 +16,12 @@ on:
|
||||
- stable
|
||||
- canary
|
||||
default: stable
|
||||
description: Channel to deploy
|
||||
description: Release channel to deploy
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref to deploy (defaults to main/canary based on channel)
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-marketing-${{ github.event_name == 'workflow_dispatch' && inputs.channel || (github.ref_name == 'canary' && 'canary') || 'stable' }}
|
||||
@@ -36,46 +36,35 @@ jobs:
|
||||
with:
|
||||
github_event_name: ${{ github.event_name }}
|
||||
github_ref_name: ${{ github.ref_name }}
|
||||
github_ref: ${{ github.ref }}
|
||||
workflow_dispatch_channel: ${{ github.event_name == 'workflow_dispatch' && inputs.channel || '' }}
|
||||
workflow_dispatch_ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
|
||||
|
||||
deploy:
|
||||
name: Deploy marketing
|
||||
needs: channel-vars
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
IS_CANARY: ${{ needs.channel-vars.outputs.is_canary }}
|
||||
SOURCE_REF: ${{ needs.channel-vars.outputs.source_ref }}
|
||||
STACK_SUFFIX: ${{ needs.channel-vars.outputs.stack_suffix }}
|
||||
STACK: ${{ format('fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
IMAGE_NAME: ${{ format('fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
CACHE_SCOPE: ${{ format('deploy-fluxer-marketing{0}', needs.channel-vars.outputs.stack_suffix) }}
|
||||
APP_REPLICAS: ${{ needs.channel-vars.outputs.is_canary == 'true' && 1 || 2 }}
|
||||
API_PUBLIC_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://api.canary.fluxer.app' || 'https://api.fluxer.app' }}
|
||||
API_HOST: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'fluxer-api-canary_app:8080' || 'fluxer-api_app:8080' }}
|
||||
APP_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://web.canary.fluxer.app' || 'https://web.fluxer.app' }}
|
||||
MARKETING_ENDPOINT: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'https://canary.fluxer.app' || 'https://fluxer.app' }}
|
||||
CADDY_DOMAIN: ${{ needs.channel-vars.outputs.is_canary == 'true' && 'canary.fluxer.app' || 'fluxer.app' }}
|
||||
RELEASE_CHANNEL: ${{ needs.channel-vars.outputs.channel }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
sha=$(git rev-parse HEAD)
|
||||
echo "Deploying commit ${sha}"
|
||||
printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/deploy_marketing.py --step record_deploy_commit
|
||||
|
||||
- name: Set build timestamp
|
||||
run: echo "BUILD_TIMESTAMP=$(date -u +%s)" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/deploy_marketing.py --step set_build_timestamp
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -89,7 +78,7 @@ jobs:
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: fluxer_marketing
|
||||
context: .
|
||||
file: fluxer_marketing/Dockerfile
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
@@ -97,18 +86,16 @@ jobs:
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_marketing.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -116,10 +103,7 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_marketing.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
@@ -127,114 +111,7 @@ jobs:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
STACK: ${{ env.STACK }}
|
||||
IS_CANARY: ${{ env.IS_CANARY }}
|
||||
API_PUBLIC_ENDPOINT: ${{ env.API_PUBLIC_ENDPOINT }}
|
||||
API_HOST: ${{ env.API_HOST }}
|
||||
APP_ENDPOINT: ${{ env.APP_ENDPOINT }}
|
||||
MARKETING_ENDPOINT: ${{ env.MARKETING_ENDPOINT }}
|
||||
CADDY_DOMAIN: ${{ env.CADDY_DOMAIN }}
|
||||
RELEASE_CHANNEL: ${{ env.RELEASE_CHANNEL }}
|
||||
APP_REPLICAS: ${{ env.APP_REPLICAS }}
|
||||
run: |-
|
||||
set -euo pipefail
|
||||
docker pussh "${IMAGE_TAG}" "${SERVER}"
|
||||
|
||||
ssh "${SERVER}" \
|
||||
"IMAGE_TAG=${IMAGE_TAG} STACK=${STACK} IS_CANARY=${IS_CANARY} API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT} API_HOST=${API_HOST} APP_ENDPOINT=${APP_ENDPOINT} MARKETING_ENDPOINT=${MARKETING_ENDPOINT} CADDY_DOMAIN=${CADDY_DOMAIN} RELEASE_CHANNEL=${RELEASE_CHANNEL} APP_REPLICAS=${APP_REPLICAS} bash" << 'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
sudo mkdir -p "/opt/${STACK}"
|
||||
sudo chown -R "${USER}:${USER}" "/opt/${STACK}"
|
||||
cd "/opt/${STACK}"
|
||||
|
||||
cat > compose.yaml << COMPOSEEOF
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_TAG}
|
||||
env_file:
|
||||
- /etc/fluxer/fluxer.env
|
||||
environment:
|
||||
- FLUXER_API_PUBLIC_ENDPOINT=${API_PUBLIC_ENDPOINT}
|
||||
- FLUXER_API_HOST=${API_HOST}
|
||||
- FLUXER_APP_ENDPOINT=${APP_ENDPOINT}
|
||||
- FLUXER_CDN_ENDPOINT=https://fluxerstatic.com
|
||||
- FLUXER_MARKETING_ENDPOINT=${MARKETING_ENDPOINT}
|
||||
- FLUXER_MARKETING_PORT=8080
|
||||
- FLUXER_PATH_MARKETING=/
|
||||
- RELEASE_CHANNEL=${RELEASE_CHANNEL}
|
||||
- FLUXER_METRICS_HOST=fluxer-metrics_app:8080
|
||||
deploy:
|
||||
replicas: ${APP_REPLICAS}
|
||||
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: "${CADDY_DOMAIN}"
|
||||
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||
caddy.header.Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
|
||||
caddy.header.X-Xss-Protection: "1; mode=block"
|
||||
caddy.header.X-Content-Type-Options: "nosniff"
|
||||
caddy.header.Referrer-Policy: "strict-origin-when-cross-origin"
|
||||
caddy.header.X-Frame-Options: "DENY"
|
||||
COMPOSEEOF
|
||||
|
||||
if [[ "${IS_CANARY}" == "true" ]]; then
|
||||
cat >> compose.yaml << 'COMPOSEEOF'
|
||||
caddy.header.X-Robots-Tag: "noindex, nofollow, nosnippet, noimageindex"
|
||||
caddy.@channels.path: "/channels /channels/*"
|
||||
caddy.redir: "@channels https://web.canary.fluxer.app{uri}"
|
||||
COMPOSEEOF
|
||||
else
|
||||
cat >> compose.yaml << 'COMPOSEEOF'
|
||||
caddy.redir_0: "/channels/* https://web.fluxer.app{uri}"
|
||||
caddy.redir_1: "/channels https://web.fluxer.app{uri}"
|
||||
caddy.redir_2: "/delete-my-account https://fluxer.app/help/articles/1445724566704881664 302"
|
||||
caddy.redir_3: "/delete-my-data https://fluxer.app/help/articles/1445730947679911936 302"
|
||||
caddy.redir_4: "/export-my-data https://fluxer.app/help/articles/1445731738851475456 302"
|
||||
caddy.redir_5: "/bugs https://fluxer.app/help/articles/1447264362996695040 302"
|
||||
caddy_1: "www.fluxer.app"
|
||||
caddy_1.redir: "https://fluxer.app{uri}"
|
||||
caddy_3: "fluxer.gg"
|
||||
caddy_3.redir: "https://web.fluxer.app/invite{uri}"
|
||||
caddy_4: "fluxer.gift"
|
||||
caddy_4.redir: "https://web.fluxer.app/gift{uri}"
|
||||
caddy_5: "fluxerapp.com"
|
||||
caddy_5.redir: "https://fluxer.app{uri}"
|
||||
caddy_6: "www.fluxerapp.com"
|
||||
caddy_6.redir: "https://fluxer.app{uri}"
|
||||
caddy_7: "fluxer.dev"
|
||||
caddy_7.redir: "https://docs.fluxer.app{uri}"
|
||||
caddy_8: "www.fluxer.dev"
|
||||
caddy_8.redir: "https://docs.fluxer.app{uri}"
|
||||
COMPOSEEOF
|
||||
fi
|
||||
|
||||
cat >> compose.yaml << 'COMPOSEEOF'
|
||||
networks:
|
||||
- fluxer-shared
|
||||
healthcheck:
|
||||
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 \
|
||||
--with-registry-auth \
|
||||
--detach=false \
|
||||
--resolve-image never \
|
||||
-c compose.yaml \
|
||||
"${STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/deploy_marketing.py --step push_and_deploy
|
||||
|
||||
110
.github/workflows/deploy-media-proxy.yaml
vendored
110
.github/workflows/deploy-media-proxy.yaml
vendored
@@ -7,7 +7,13 @@ on:
|
||||
paths:
|
||||
- fluxer_media_proxy/**
|
||||
- .github/workflows/deploy-media-proxy.yaml
|
||||
workflow_dispatch: {}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-media-proxy
|
||||
@@ -25,17 +31,17 @@ env:
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy media proxy
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
RELEASE_CHANNEL: stable
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.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"
|
||||
run: python3 scripts/ci/workflows/deploy_media_proxy.py --step record_deploy_commit
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -46,27 +52,30 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_media_proxy.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.CONTEXT_DIR }}
|
||||
context: .
|
||||
file: ${{ env.CONTEXT_DIR }}/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 }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_media_proxy.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -74,77 +83,10 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_media_proxy.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ 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} 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 --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/deploy_media_proxy.py --step push_and_deploy
|
||||
|
||||
131
.github/workflows/deploy-metrics.yaml
vendored
131
.github/workflows/deploy-metrics.yaml
vendored
@@ -1,131 +0,0 @@
|
||||
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: 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_metrics
|
||||
file: fluxer_metrics/Dockerfile
|
||||
tags: fluxer-metrics:${{ env.DEPLOY_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:${{ 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 -euo pipefail
|
||||
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 --with-registry-auth --detach=false --resolve-image never -c compose.yaml fluxer-metrics
|
||||
EOF
|
||||
92
.github/workflows/deploy-queue.yaml
vendored
Normal file
92
.github/workflows/deploy-queue.yaml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: deploy queue
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_queue/**
|
||||
- .github/workflows/deploy-queue.yaml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-queue
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy queue
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
IS_CANARY: true
|
||||
STACK: fluxer-queue
|
||||
CACHE_SCOPE: deploy-fluxer-queue
|
||||
RELEASE_CHANNEL: canary
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: python3 scripts/ci/workflows/deploy_queue.py --step record_deploy_commit
|
||||
|
||||
- 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: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_queue.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_queue/Dockerfile
|
||||
tags: |
|
||||
${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
- name: Install docker-pussh
|
||||
run: python3 scripts/ci/workflows/deploy_queue.py --step install_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: python3 scripts/ci/workflows/deploy_queue.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
run: python3 scripts/ci/workflows/deploy_queue.py --step push_and_deploy
|
||||
91
.github/workflows/deploy-relay-directory.yaml
vendored
Normal file
91
.github/workflows/deploy-relay-directory.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: deploy relay directory
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
paths:
|
||||
- fluxer_relay_directory/**
|
||||
- .github/workflows/deploy-relay-directory.yaml
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-relay-directory
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy relay directory
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
STACK: fluxer-relay-directory
|
||||
CACHE_SCOPE: deploy-fluxer-relay-directory
|
||||
IS_CANARY: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Record deploy commit
|
||||
run: python3 scripts/ci/workflows/deploy_relay_directory.py --step record_deploy_commit
|
||||
|
||||
- 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: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_relay_directory.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_relay_directory/Dockerfile
|
||||
tags: |
|
||||
${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
load: true
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=canary
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
- name: Install docker-pussh
|
||||
run: python3 scripts/ci/workflows/deploy_relay_directory.py --step install_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: python3 scripts/ci/workflows/deploy_relay_directory.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }}
|
||||
run: python3 scripts/ci/workflows/deploy_relay_directory.py --step push_and_deploy
|
||||
62
.github/workflows/deploy-relay.yaml
vendored
Normal file
62
.github/workflows/deploy-relay.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: deploy relay
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
push:
|
||||
branches:
|
||||
- canary
|
||||
paths:
|
||||
- 'fluxer_relay/**'
|
||||
|
||||
concurrency:
|
||||
group: deploy-relay
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy (hot patch)
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
sparse-checkout: |
|
||||
fluxer_relay
|
||||
scripts/ci
|
||||
|
||||
- name: Set up Erlang
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: '28'
|
||||
rebar3-version: '3.24.0'
|
||||
|
||||
- name: Compile
|
||||
run: python3 scripts/ci/workflows/deploy_relay.py --step 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: python3 scripts/ci/workflows/deploy_relay.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Record deploy commit
|
||||
run: python3 scripts/ci/workflows/deploy_relay.py --step record_deploy_commit
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}
|
||||
RELAY_ADMIN_SECRET: ${{ secrets.RELAY_ADMIN_SECRET }}
|
||||
run: python3 scripts/ci/workflows/deploy_relay.py --step deploy
|
||||
110
.github/workflows/deploy-static-proxy.yaml
vendored
110
.github/workflows/deploy-static-proxy.yaml
vendored
@@ -7,7 +7,13 @@ on:
|
||||
paths:
|
||||
- fluxer_media_proxy/**
|
||||
- .github/workflows/deploy-static-proxy.yaml
|
||||
workflow_dispatch: {}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: false
|
||||
default: ''
|
||||
description: Optional git ref (defaults to the triggering branch)
|
||||
|
||||
concurrency:
|
||||
group: deploy-fluxer-static-proxy
|
||||
@@ -25,17 +31,17 @@ env:
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy static proxy
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
RELEASE_CHANNEL: stable
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.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"
|
||||
run: python3 scripts/ci/workflows/deploy_static_proxy.py --step record_deploy_commit
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -46,27 +52,30 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set build timestamp
|
||||
run: python3 scripts/ci/workflows/deploy_static_proxy.py --step set_build_timestamp
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ env.CONTEXT_DIR }}
|
||||
context: .
|
||||
file: ${{ env.CONTEXT_DIR }}/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 }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ env.DEPLOY_SHA }}
|
||||
BUILD_NUMBER=${{ github.run_number }}
|
||||
BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }}
|
||||
RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }}
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_static_proxy.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -74,77 +83,10 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/deploy_static_proxy.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ 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} 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 --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/deploy_static_proxy.py --step push_and_deploy
|
||||
|
||||
92
.github/workflows/migrate-cassandra.yaml
vendored
92
.github/workflows/migrate-cassandra.yaml
vendored
@@ -18,37 +18,26 @@ permissions:
|
||||
jobs:
|
||||
migrate:
|
||||
name: Run database migrations
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Set up pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v5
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
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-
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Build migration tool
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd scripts/cassandra-migrate
|
||||
cargo build --release
|
||||
- name: Install dependencies
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step install_dependencies
|
||||
|
||||
- name: Validate migrations
|
||||
run: |
|
||||
set -euo pipefail
|
||||
./scripts/cassandra-migrate/target/release/cassandra-migrate check
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step validate_migrations
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -56,70 +45,23 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step setup_tunnel --server-user ${{ secrets.SERVER_USER }} --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step test_connection
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step run_migrations
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/migrate_cassandra.py --step close_tunnel
|
||||
|
||||
60
.github/workflows/promote-canary-to-main.yaml
vendored
60
.github/workflows/promote-canary-to-main.yaml
vendored
@@ -25,13 +25,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.PROMOTE_APP_ID }}
|
||||
private-key: ${{ secrets.PROMOTE_APP_PRIVATE_KEY }}
|
||||
@@ -45,49 +45,23 @@ jobs:
|
||||
|
||||
- name: Verify ff-only + summarize
|
||||
id: verify
|
||||
run: |
|
||||
set -euo pipefail
|
||||
src="${{ inputs.src }}"
|
||||
dst="${{ inputs.dst }}"
|
||||
|
||||
git fetch origin "${dst}" "${src}" --prune
|
||||
|
||||
# Ensure HEAD is exactly origin/src
|
||||
git reset --hard "origin/${src}"
|
||||
|
||||
# FF-only requirement: dst must be an ancestor of src
|
||||
if ! git merge-base --is-ancestor "origin/${dst}" "origin/${src}"; then
|
||||
echo "::error::Cannot fast-forward: origin/${dst} is not an ancestor of origin/${src} (branches diverged)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ahead="$(git rev-list --count "origin/${dst}..origin/${src}")"
|
||||
echo "ahead=$ahead" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "## Promote \`${src}\` → \`${dst}\` (ff-only)"
|
||||
echo ""
|
||||
echo "- \`${dst}\`: \`$(git rev-parse "origin/${dst}")\`"
|
||||
echo "- \`${src}\`: \`$(git rev-parse "origin/${src}")\`"
|
||||
echo "- Commits to promote: **${ahead}**"
|
||||
echo ""
|
||||
echo "### Commits"
|
||||
if [ "$ahead" -eq 0 ]; then
|
||||
echo "_Nothing to promote._"
|
||||
else
|
||||
git log --oneline --decorate "origin/${dst}..origin/${src}"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/promote_canary_to_main.py
|
||||
--step verify
|
||||
--src "${{ inputs.src }}"
|
||||
--dst "${{ inputs.dst }}"
|
||||
|
||||
- name: Push fast-forward
|
||||
if: ${{ steps.verify.outputs.ahead != '0' && inputs.dry_run != true }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dst="${{ inputs.dst }}"
|
||||
# Push src HEAD to dst (no merge commit, same SHAs)
|
||||
git push origin "HEAD:refs/heads/${dst}"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/promote_canary_to_main.py
|
||||
--step push
|
||||
--dst "${{ inputs.dst }}"
|
||||
|
||||
- name: Dry run / no-op
|
||||
if: ${{ steps.verify.outputs.ahead == '0' || inputs.dry_run == true }}
|
||||
run: |
|
||||
echo "No push performed (dry_run=${{ inputs.dry_run }}, ahead=${{ steps.verify.outputs.ahead }})."
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/promote_canary_to_main.py
|
||||
--step dry_run
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--ahead "${{ steps.verify.outputs.ahead }}"
|
||||
|
||||
151
.github/workflows/release-livekitctl.yaml
vendored
Normal file
151
.github/workflows/release-livekitctl.yaml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: release livekitctl
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'livekitctl-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to release (e.g., 1.0.0)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release-livekitctl
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.24'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache-dependency-path: fluxer_devops/livekitctl/go.sum
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_livekitctl.py
|
||||
--step determine_version
|
||||
--event-name "${{ github.event_name }}"
|
||||
--input-version "${{ inputs.version }}"
|
||||
--ref-name "${{ github.ref_name }}"
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: 0
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/release_livekitctl.py
|
||||
--step build_binary
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: livekitctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: fluxer_devops/livekitctl/livekitctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
name: Create release
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_livekitctl.py
|
||||
--step determine_version
|
||||
--event-name "${{ github.event_name }}"
|
||||
--input-version "${{ inputs.version }}"
|
||||
--ref-name "${{ github.ref_name }}"
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release assets
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/release_livekitctl.py
|
||||
--step prepare_release_assets
|
||||
|
||||
- name: Generate checksums
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/release_livekitctl.py
|
||||
--step generate_checksums
|
||||
--release-dir release
|
||||
|
||||
- name: Create tag (workflow_dispatch only)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: >-
|
||||
python3 ${{ github.workspace }}/scripts/ci/workflows/release_livekitctl.py
|
||||
--step create_tag
|
||||
--tag "${{ steps.version.outputs.tag }}"
|
||||
--version "${{ steps.version.outputs.version }}"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
name: livekitctl v${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
## livekitctl v${{ steps.version.outputs.version }}
|
||||
|
||||
Self-hosted LiveKit bootstrap and operations CLI.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://fluxer.app/get/livekitctl | sudo bash
|
||||
```
|
||||
|
||||
### Manual download
|
||||
|
||||
Download the appropriate binary for your system:
|
||||
- `livekitctl-linux-amd64` - Linux x86_64
|
||||
- `livekitctl-linux-arm64` - Linux ARM64
|
||||
|
||||
Then make it executable and move to your PATH:
|
||||
```bash
|
||||
chmod +x livekitctl-linux-*
|
||||
sudo mv livekitctl-linux-* /usr/local/bin/livekitctl
|
||||
```
|
||||
|
||||
### Checksums
|
||||
|
||||
See `checksums.txt` for SHA256 checksums.
|
||||
files: |
|
||||
release/livekitctl-linux-amd64
|
||||
release/livekitctl-linux-arm64
|
||||
release/checksums.txt
|
||||
draft: false
|
||||
prerelease: false
|
||||
259
.github/workflows/release-relay-directory.yaml
vendored
Normal file
259
.github/workflows/release-relay-directory.yaml
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
name: release relay directory
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [canary]
|
||||
paths:
|
||||
- fluxer_relay_directory/**
|
||||
- .github/workflows/release-relay-directory.yaml
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel
|
||||
type: choice
|
||||
options: [stable, nightly]
|
||||
default: nightly
|
||||
required: false
|
||||
|
||||
ref:
|
||||
description: Git ref (branch, tag, or commit SHA)
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
|
||||
version:
|
||||
description: Stable version (e.g. 1.0.0). Defaults to 0.0.<run_number>
|
||||
type: string
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
concurrency:
|
||||
group: release-relay-directory-${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/fluxer-relay-directory
|
||||
CHANNEL: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
SOURCE_REF: >-
|
||||
${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.ref)
|
||||
|| ((github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'stable') && 'main')
|
||||
|| 'canary' }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
name: resolve build metadata
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
source_ref: ${{ steps.meta.outputs.source_ref }}
|
||||
sha_short: ${{ steps.meta.outputs.sha_short }}
|
||||
timestamp: ${{ steps.meta.outputs.timestamp }}
|
||||
date: ${{ steps.meta.outputs.date }}
|
||||
build_number: ${{ steps.meta.outputs.build_number }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
|
||||
- name: metadata
|
||||
id: meta
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_relay_directory.py
|
||||
--step metadata
|
||||
--version-input "${{ github.event.inputs.version }}"
|
||||
--channel "${{ env.CHANNEL }}"
|
||||
--source-ref "${{ env.SOURCE_REF }}"
|
||||
|
||||
build:
|
||||
name: build fluxer relay directory
|
||||
needs: meta
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
image_tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: set up buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=nightly-${{ needs.meta.outputs.date }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=sha-${{ needs.meta.outputs.sha_short }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=stable,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=latest,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=v${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=semver,pattern={{version}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
|
||||
- name: build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_relay_directory/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.docker_meta.outputs.labels }}
|
||||
org.opencontainers.image.version=v${{ needs.meta.outputs.version }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ needs.meta.outputs.timestamp }}
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
dev.fluxer.build.channel=${{ needs.meta.outputs.channel }}
|
||||
dev.fluxer.build.number=${{ needs.meta.outputs.build_number }}
|
||||
dev.fluxer.build.sha=${{ github.sha }}
|
||||
dev.fluxer.build.short_sha=${{ needs.meta.outputs.sha_short }}
|
||||
dev.fluxer.build.date=${{ needs.meta.outputs.date }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ github.sha }}
|
||||
BUILD_NUMBER=${{ needs.meta.outputs.build_number }}
|
||||
BUILD_TIMESTAMP=${{ needs.meta.outputs.timestamp }}
|
||||
RELEASE_CHANNEL=${{ needs.meta.outputs.channel }}
|
||||
cache-from: type=gha,scope=relay-directory-${{ needs.meta.outputs.channel }}
|
||||
cache-to: type=gha,mode=max,scope=relay-directory-${{ needs.meta.outputs.channel }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
create-release:
|
||||
name: create release
|
||||
needs: [meta, build]
|
||||
if: |
|
||||
always() &&
|
||||
needs.meta.outputs.version != '' &&
|
||||
needs.build.result == 'success'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: stable release
|
||||
if: needs.meta.outputs.channel == 'stable'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: relay-directory-v${{ needs.meta.outputs.version }}
|
||||
name: Fluxer Relay Directory v${{ needs.meta.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Fluxer Relay Directory
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${{ needs.meta.outputs.version }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: stable
|
||||
|
||||
Docs: https://docs.fluxer.app/federation
|
||||
|
||||
- name: nightly release
|
||||
if: needs.meta.outputs.channel == 'nightly'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: relay-directory-nightly-${{ needs.meta.outputs.date }}-${{ needs.meta.outputs.sha_short }}
|
||||
name: Relay Directory nightly ${{ needs.meta.outputs.date }} (${{ needs.meta.outputs.sha_short }})
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Nightly Fluxer Relay Directory image from canary.
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${{ needs.meta.outputs.date }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.meta.outputs.sha_short }}
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: nightly
|
||||
- branch: canary
|
||||
|
||||
release-summary:
|
||||
name: release summary
|
||||
needs: [meta, build]
|
||||
if: always()
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: summary
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_relay_directory.py
|
||||
--step summary
|
||||
--build-result "${{ needs.build.result }}"
|
||||
--channel "${{ needs.meta.outputs.channel }}"
|
||||
--version "${{ needs.meta.outputs.version }}"
|
||||
--build-number "${{ needs.meta.outputs.build_number }}"
|
||||
--sha-short "${{ needs.meta.outputs.sha_short }}"
|
||||
--timestamp "${{ needs.meta.outputs.timestamp }}"
|
||||
--date-ymd "${{ needs.meta.outputs.date }}"
|
||||
--source-ref "${{ needs.meta.outputs.source_ref }}"
|
||||
--image-tags "${{ needs.build.outputs.image_tags }}"
|
||||
--image-digest "${{ needs.build.outputs.image_digest }}"
|
||||
--registry "${{ env.REGISTRY }}"
|
||||
--image-name "${{ env.IMAGE_NAME }}"
|
||||
259
.github/workflows/release-relay.yaml
vendored
Normal file
259
.github/workflows/release-relay.yaml
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
name: release relay
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [canary]
|
||||
paths:
|
||||
- fluxer_relay/**
|
||||
- .github/workflows/release-relay.yaml
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel
|
||||
type: choice
|
||||
options: [stable, nightly]
|
||||
default: nightly
|
||||
required: false
|
||||
|
||||
ref:
|
||||
description: Git ref (branch, tag, or commit SHA)
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
|
||||
version:
|
||||
description: Stable version (e.g. 1.0.0). Defaults to 0.0.<run_number>
|
||||
type: string
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
concurrency:
|
||||
group: release-relay-${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/fluxer-relay
|
||||
CHANNEL: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
SOURCE_REF: >-
|
||||
${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.ref)
|
||||
|| ((github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'stable') && 'main')
|
||||
|| 'canary' }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
name: resolve build metadata
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
source_ref: ${{ steps.meta.outputs.source_ref }}
|
||||
sha_short: ${{ steps.meta.outputs.sha_short }}
|
||||
timestamp: ${{ steps.meta.outputs.timestamp }}
|
||||
date: ${{ steps.meta.outputs.date }}
|
||||
build_number: ${{ steps.meta.outputs.build_number }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
|
||||
- name: metadata
|
||||
id: meta
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_relay.py
|
||||
--step metadata
|
||||
--version-input "${{ github.event.inputs.version }}"
|
||||
--channel "${{ env.CHANNEL }}"
|
||||
--source-ref "${{ env.SOURCE_REF }}"
|
||||
|
||||
build:
|
||||
name: build fluxer relay
|
||||
needs: meta
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
image_tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: set up buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=nightly-${{ needs.meta.outputs.date }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=sha-${{ needs.meta.outputs.sha_short }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=stable,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=latest,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=v${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=semver,pattern={{version}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
|
||||
- name: build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: fluxer_relay
|
||||
file: fluxer_relay/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.docker_meta.outputs.labels }}
|
||||
org.opencontainers.image.version=v${{ needs.meta.outputs.version }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ needs.meta.outputs.timestamp }}
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
dev.fluxer.build.channel=${{ needs.meta.outputs.channel }}
|
||||
dev.fluxer.build.number=${{ needs.meta.outputs.build_number }}
|
||||
dev.fluxer.build.sha=${{ github.sha }}
|
||||
dev.fluxer.build.short_sha=${{ needs.meta.outputs.sha_short }}
|
||||
dev.fluxer.build.date=${{ needs.meta.outputs.date }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ github.sha }}
|
||||
BUILD_NUMBER=${{ needs.meta.outputs.build_number }}
|
||||
BUILD_TIMESTAMP=${{ needs.meta.outputs.timestamp }}
|
||||
RELEASE_CHANNEL=${{ needs.meta.outputs.channel }}
|
||||
cache-from: type=gha,scope=relay-${{ needs.meta.outputs.channel }}
|
||||
cache-to: type=gha,mode=max,scope=relay-${{ needs.meta.outputs.channel }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
create-release:
|
||||
name: create release
|
||||
needs: [meta, build]
|
||||
if: |
|
||||
always() &&
|
||||
needs.meta.outputs.version != '' &&
|
||||
needs.build.result == 'success'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: stable release
|
||||
if: needs.meta.outputs.channel == 'stable'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: relay-v${{ needs.meta.outputs.version }}
|
||||
name: Fluxer Relay v${{ needs.meta.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Fluxer Relay
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${{ needs.meta.outputs.version }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: stable
|
||||
|
||||
Docs: https://docs.fluxer.app/federation
|
||||
|
||||
- name: nightly release
|
||||
if: needs.meta.outputs.channel == 'nightly'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: relay-nightly-${{ needs.meta.outputs.date }}-${{ needs.meta.outputs.sha_short }}
|
||||
name: Relay nightly ${{ needs.meta.outputs.date }} (${{ needs.meta.outputs.sha_short }})
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Nightly Fluxer Relay image from canary.
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${{ needs.meta.outputs.date }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ needs.meta.outputs.sha_short }}
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: nightly
|
||||
- branch: canary
|
||||
|
||||
release-summary:
|
||||
name: release summary
|
||||
needs: [meta, build]
|
||||
if: always()
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: summary
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_relay.py
|
||||
--step summary
|
||||
--build-result "${{ needs.build.result }}"
|
||||
--channel "${{ needs.meta.outputs.channel }}"
|
||||
--version "${{ needs.meta.outputs.version }}"
|
||||
--build-number "${{ needs.meta.outputs.build_number }}"
|
||||
--sha-short "${{ needs.meta.outputs.sha_short }}"
|
||||
--timestamp "${{ needs.meta.outputs.timestamp }}"
|
||||
--date-ymd "${{ needs.meta.outputs.date }}"
|
||||
--source-ref "${{ needs.meta.outputs.source_ref }}"
|
||||
--image-tags "${{ needs.build.outputs.image_tags }}"
|
||||
--image-digest "${{ needs.build.outputs.image_digest }}"
|
||||
--registry "${{ env.REGISTRY }}"
|
||||
--image-name "${{ env.IMAGE_NAME }}"
|
||||
278
.github/workflows/release-server.yaml
vendored
Normal file
278
.github/workflows/release-server.yaml
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
name: release server
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [canary]
|
||||
paths:
|
||||
- packages/**
|
||||
- fluxer_server/**
|
||||
- fluxer_gateway/**
|
||||
- pnpm-lock.yaml
|
||||
- .github/workflows/release-server.yaml
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: Release channel
|
||||
type: choice
|
||||
options: [stable, nightly]
|
||||
default: nightly
|
||||
required: false
|
||||
|
||||
ref:
|
||||
description: Git ref (branch, tag, or commit SHA)
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
|
||||
version:
|
||||
description: Stable version (e.g. 1.0.0). Defaults to 0.0.<run_number>
|
||||
type: string
|
||||
required: false
|
||||
|
||||
build_server:
|
||||
description: Build Fluxer Server
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
concurrency:
|
||||
group: release-server-${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME_SERVER: ${{ github.repository_owner }}/fluxer-server
|
||||
CHANNEL: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.channel) || 'nightly' }}
|
||||
SOURCE_REF: >-
|
||||
${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.ref)
|
||||
|| ((github.event_name == 'workflow_dispatch' && github.event.inputs.channel == 'stable') && 'main')
|
||||
|| 'canary' }}
|
||||
|
||||
jobs:
|
||||
meta:
|
||||
name: resolve build metadata
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
channel: ${{ steps.meta.outputs.channel }}
|
||||
source_ref: ${{ steps.meta.outputs.source_ref }}
|
||||
sha_short: ${{ steps.meta.outputs.sha_short }}
|
||||
timestamp: ${{ steps.meta.outputs.timestamp }}
|
||||
date: ${{ steps.meta.outputs.date }}
|
||||
build_number: ${{ steps.meta.outputs.build_number }}
|
||||
build_server: ${{ steps.should_build.outputs.server }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.SOURCE_REF }}
|
||||
|
||||
- name: metadata
|
||||
id: meta
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_server.py
|
||||
--step metadata
|
||||
--version-input "${{ github.event.inputs.version }}"
|
||||
--channel "${{ env.CHANNEL }}"
|
||||
--source-ref "${{ env.SOURCE_REF }}"
|
||||
|
||||
- name: determine build targets
|
||||
id: should_build
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_server.py
|
||||
--step determine_build_targets
|
||||
--event-name "${{ github.event_name }}"
|
||||
--build-server-input "${{ github.event.inputs.build_server }}"
|
||||
|
||||
build-server:
|
||||
name: build fluxer server
|
||||
needs: meta
|
||||
if: needs.meta.outputs.build_server == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
outputs:
|
||||
image_tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: set up buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=nightly-${{ needs.meta.outputs.date }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=sha-${{ needs.meta.outputs.sha_short }},enable=${{ needs.meta.outputs.channel == 'nightly' }}
|
||||
type=raw,value=stable,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=latest,enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=raw,value=v${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' }}
|
||||
type=semver,pattern={{version}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.meta.outputs.version }},enable=${{ needs.meta.outputs.channel == 'stable' && !startsWith(needs.meta.outputs.version, '0.0.') }}
|
||||
|
||||
- name: build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: fluxer_server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.docker_meta.outputs.labels }}
|
||||
org.opencontainers.image.version=v${{ needs.meta.outputs.version }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ needs.meta.outputs.timestamp }}
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
dev.fluxer.build.channel=${{ needs.meta.outputs.channel }}
|
||||
dev.fluxer.build.number=${{ needs.meta.outputs.build_number }}
|
||||
dev.fluxer.build.sha=${{ github.sha }}
|
||||
dev.fluxer.build.short_sha=${{ needs.meta.outputs.sha_short }}
|
||||
dev.fluxer.build.date=${{ needs.meta.outputs.date }}
|
||||
build-args: |
|
||||
BUILD_SHA=${{ github.sha }}
|
||||
BUILD_NUMBER=${{ needs.meta.outputs.build_number }}
|
||||
BUILD_TIMESTAMP=${{ needs.meta.outputs.timestamp }}
|
||||
RELEASE_CHANNEL=${{ needs.meta.outputs.channel }}
|
||||
cache-from: type=gha,scope=server-${{ needs.meta.outputs.channel }}
|
||||
cache-to: type=gha,mode=max,scope=server-${{ needs.meta.outputs.channel }}
|
||||
provenance: true
|
||||
sbom: true
|
||||
|
||||
- name: attest
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
create-release:
|
||||
name: create release
|
||||
needs: [meta, build-server]
|
||||
if: |
|
||||
always() &&
|
||||
needs.meta.outputs.version != '' &&
|
||||
(needs.build-server.result == 'success' || needs.build-server.result == 'skipped')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.source_ref }}
|
||||
|
||||
- name: stable release
|
||||
if: needs.meta.outputs.channel == 'stable'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ needs.meta.outputs.version }}
|
||||
name: Fluxer Server v${{ needs.meta.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Fluxer Server
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}:v${{ needs.meta.outputs.version }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}:latest
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: stable
|
||||
|
||||
Docs: https://docs.fluxer.app/self-hosting
|
||||
|
||||
- name: nightly release
|
||||
if: needs.meta.outputs.channel == 'nightly'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: nightly-${{ needs.meta.outputs.date }}-${{ needs.meta.outputs.sha_short }}
|
||||
name: Nightly build ${{ needs.meta.outputs.date }} (${{ needs.meta.outputs.sha_short }})
|
||||
draft: false
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
Nightly Fluxer Server image from canary.
|
||||
|
||||
Pull:
|
||||
```bash
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}:nightly
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}:nightly-${{ needs.meta.outputs.date }}
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_SERVER }}:sha-${{ needs.meta.outputs.sha_short }}
|
||||
```
|
||||
|
||||
Build:
|
||||
- version: v${{ needs.meta.outputs.version }}
|
||||
- build: ${{ needs.meta.outputs.build_number }}
|
||||
- sha: ${{ github.sha }}
|
||||
- time: ${{ needs.meta.outputs.timestamp }}
|
||||
- channel: nightly
|
||||
- branch: canary
|
||||
|
||||
release-summary:
|
||||
name: release summary
|
||||
needs: [meta, build-server]
|
||||
if: always()
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: summary
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/release_server.py
|
||||
--step summary
|
||||
--build-result "${{ needs.build-server.result }}"
|
||||
--channel "${{ needs.meta.outputs.channel }}"
|
||||
--version "${{ needs.meta.outputs.version }}"
|
||||
--build-number "${{ needs.meta.outputs.build_number }}"
|
||||
--sha-short "${{ needs.meta.outputs.sha_short }}"
|
||||
--timestamp "${{ needs.meta.outputs.timestamp }}"
|
||||
--date-ymd "${{ needs.meta.outputs.date }}"
|
||||
--source-ref "${{ needs.meta.outputs.source_ref }}"
|
||||
--image-tags "${{ needs.build-server.outputs.image_tags }}"
|
||||
--image-digest "${{ needs.build-server.outputs.image_digest }}"
|
||||
--registry "${{ env.REGISTRY }}"
|
||||
--image-name-server "${{ env.IMAGE_NAME_SERVER }}"
|
||||
87
.github/workflows/restart-gateway.yaml
vendored
87
.github/workflows/restart-gateway.yaml
vendored
@@ -20,28 +20,22 @@ env:
|
||||
IMAGE_NAME: fluxer-gateway
|
||||
CONTEXT_DIR: fluxer_gateway
|
||||
COMPOSE_STACK: fluxer-gateway
|
||||
RELEASE_CHANNEL: ${{ github.ref_name == 'canary' && 'staging' || 'production' }}
|
||||
|
||||
jobs:
|
||||
restart:
|
||||
name: Restart gateway
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
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
|
||||
run: python3 scripts/ci/workflows/restart_gateway.py --step validate_confirmation --confirmation "${{ github.event.inputs.confirmation }}"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Record deploy commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sha=$(git rev-parse HEAD)
|
||||
echo "Deploying commit ${sha}"
|
||||
printf 'DEPLOY_SHA=%s\n' "$sha" >> "$GITHUB_ENV"
|
||||
run: python3 scripts/ci/workflows/restart_gateway.py --step record_deploy_commit
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -67,12 +61,7 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/restart_gateway.py --step install_docker_pussh
|
||||
|
||||
- name: Set up SSH agent
|
||||
uses: webfactory/ssh-agent@v0.9.1
|
||||
@@ -80,70 +69,10 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/restart_gateway.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }}
|
||||
|
||||
- name: Push image and deploy
|
||||
env:
|
||||
IMAGE_TAG: ${{ env.IMAGE_NAME }}:${{ 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} 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 --with-registry-auth --detach=false --resolve-image never -c compose.yaml "${COMPOSE_STACK}"
|
||||
EOF
|
||||
run: python3 scripts/ci/workflows/restart_gateway.py --step push_and_deploy
|
||||
|
||||
102
.github/workflows/sync-desktop.yaml
vendored
Normal file
102
.github/workflows/sync-desktop.yaml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: sync desktop
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
paths:
|
||||
- 'fluxer_desktop/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Branch to sync (main or canary)
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: sync-desktop-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync to fluxerapp/fluxer_desktop
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
|
||||
steps:
|
||||
- name: Checkout CI scripts
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: scripts/ci
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Create GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.SYNC_APP_ID }}
|
||||
private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }}
|
||||
owner: fluxerapp
|
||||
repositories: fluxer_desktop
|
||||
|
||||
- name: Get GitHub App user ID
|
||||
id: get-user-id
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step get_user_id
|
||||
--app-slug "${{ steps.app-token.outputs.app-slug }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Checkout source repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: source
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Determine target branch
|
||||
id: branch
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step determine_branch
|
||||
--input-branch "${{ inputs.branch }}"
|
||||
--ref-name "${{ github.ref_name }}"
|
||||
|
||||
- name: Clone target repository
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step clone_target
|
||||
--token "${{ steps.app-token.outputs.token }}"
|
||||
|
||||
- name: Configure git
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step configure_git
|
||||
--app-slug "${{ steps.app-token.outputs.app-slug }}"
|
||||
--user-id "${{ steps.get-user-id.outputs.user-id }}"
|
||||
|
||||
- name: Checkout or create target branch
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step checkout_or_create_branch
|
||||
--branch-name "${{ steps.branch.outputs.name }}"
|
||||
|
||||
- name: Sync files
|
||||
run: python3 scripts/ci/workflows/sync_desktop.py --step sync_files
|
||||
|
||||
- name: Commit and push
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step commit_and_push
|
||||
--branch-name "${{ steps.branch.outputs.name }}"
|
||||
|
||||
- name: Summary
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/sync_desktop.py
|
||||
--step summary
|
||||
--branch-name "${{ steps.branch.outputs.name }}"
|
||||
23
.github/workflows/sync-static.yaml
vendored
23
.github/workflows/sync-static.yaml
vendored
@@ -15,7 +15,7 @@ concurrency:
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
@@ -33,27 +33,10 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/sync_static.py --step install_rclone
|
||||
|
||||
- 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/**"
|
||||
run: python3 scripts/ci/workflows/sync_static.py --step push
|
||||
|
||||
282
.github/workflows/test-cassandra-backup.yaml
vendored
282
.github/workflows/test-cassandra-backup.yaml
vendored
@@ -15,7 +15,7 @@ permissions:
|
||||
jobs:
|
||||
test-backup:
|
||||
name: Test latest Cassandra backup
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
|
||||
env:
|
||||
@@ -32,275 +32,59 @@ jobs:
|
||||
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"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step set_temp_paths
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step pre_clean
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y --no-install-recommends rclone age ca-certificates
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step install_tools
|
||||
|
||||
- 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
|
||||
'
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step fetch_backup
|
||||
|
||||
- name: Create data volume
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker volume create "${CASS_VOLUME}"
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step create_data_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
|
||||
'
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step restore_keyspaces
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step start_cassandra
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step verify_data
|
||||
|
||||
- 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
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step cleanup
|
||||
|
||||
- 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
|
||||
env:
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
run: >-
|
||||
python3 scripts/ci/workflows/test_cassandra_backup.py
|
||||
--step report_status
|
||||
|
||||
28
.github/workflows/update-word-lists.yaml
vendored
28
.github/workflows/update-word-lists.yaml
vendored
@@ -7,8 +7,8 @@ on:
|
||||
|
||||
jobs:
|
||||
update-word-lists:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 10
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -20,31 +20,15 @@ jobs:
|
||||
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
|
||||
run: python3 scripts/ci/workflows/update_word_lists.py --step download
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/update_word_lists.py --step check_changes
|
||||
|
||||
- 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
|
||||
run: python3 scripts/ci/workflows/update_word_lists.py --step update
|
||||
|
||||
- name: Create pull request for updated word lists
|
||||
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||
@@ -70,4 +54,4 @@ jobs:
|
||||
|
||||
- name: No changes detected
|
||||
if: steps.check_changes.outputs.changes_detected == 'false'
|
||||
run: echo "Word lists are already up to date."
|
||||
run: python3 scripts/ci/workflows/update_word_lists.py --step no_changes
|
||||
|
||||
Reference in New Issue
Block a user