Files
nginx-ui/.github/workflows/build.yml
2026-08-17 22:54:40 +08:00

1273 lines
46 KiB
YAML

name: Build
on:
push:
branches-ignore:
- 'weblate'
paths:
- "app/**/*.js"
- "app/**/*.ts"
- "app/**/*.vue"
- "app/src/language/**/*.po"
- "app/i18n.json"
- "app/package.json"
- "app/.env*"
- "**/*.go"
- "go.mod"
- "go.sum"
- ".github/workflows/build*.yml"
- "resources/docker/**"
- "resources/development/*"
- "resources/demo/*"
- "Dockerfile"
- "demo.Dockerfile"
# Without this a Worker-only change never reaches the deploy-demo job, so
# the demo keeps serving the previous Worker until some unrelated commit
# happens to trigger a build.
- "cloudflare/**"
pull_request:
types: [ opened, synchronize, reopened ]
paths:
- "**/*.js"
- "**/*.vue"
- "app/package.json"
- "app/.env*"
- "**/*.go"
- "go.mod"
- "go.sum"
- ".github/workflows/*.yml"
- "resources/docker/**"
- "resources/development/*"
- "resources/demo/*"
release:
types:
- published
jobs:
build_app:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
if: github.event_name == 'release'
uses: actions/setup-go@v7
with:
go-version: ^1.26.5
cache: false
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Install dependencies
run: bun ci
- name: Check frontend code style
run: bun run lint
- name: Check frontend types
run: bun run typecheck
- name: Build
run: |
bunx update-browserslist-db@latest
bun run build
- name: Archive app artifacts
uses: actions/upload-artifact@v7
with:
name: app-dist
path: |
app/dist
- name: Prepare publish
if: github.event_name == 'release'
run: |
set -euo pipefail
cp README*.md app/dist
tar -C app/dist -czf app-dist.tar.gz .
test -s app-dist.tar.gz
tar -tzf app-dist.tar.gz | grep -qx './index.html'
tar -tzf app-dist.tar.gz | grep -q '^./assets/'
tar -tzf app-dist.tar.gz | grep -qx './README.md'
- name: Sign publish archive
if: github.event_name == 'release'
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: env CGO_ENABLED=0 go run ./cmd/sign-release app-dist.tar.gz
- name: Publish
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
app-dist.tar.gz
app-dist.tar.gz.minisig
build:
runs-on: ubuntu-latest
needs: build_app
strategy:
max-parallel: 4
matrix:
goos: [ linux, windows ]
goarch: [ amd64, 386, arm64 ]
# exclude:
# Exclude i386 on windows (if needed)
# - goarch: 386
# goos: windows
include:
# BEGIN Linux ARM 5 6 7
- goos: linux
goarch: arm
goarm: 7
- goos: linux
goarch: arm
goarm: 6
- goos: linux
goarch: arm
goarm: 5
# END Linux ARM 5 6 7
- goos: linux
goarch: riscv64
- goos: linux
goarch: loong64
# BEGIN MIPS
- goos: linux
goarch: mips64
- goos: linux
goarch: mips64le
- goos: linux
goarch: mipsle
- goos: linux
goarch: mips
# END MIPS
env:
CGO_ENABLED: 1
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: ^1.26.5
cache: false
- name: Setup environment
id: info
run: |
export _NAME=$(jq ".$GOOS[\"$GOARCH$GOARM\"].name" -r < .github/build/build_info.json)
export _ARCH=$(jq ".$GOOS[\"$GOARCH$GOARM\"].arch" -r < .github/build/build_info.json)
export _ABI=$(jq ".$GOOS[\"$GOARCH$GOARM\"].abi // \"\"" -r < .github/build/build_info.json)
export _ARTIFACT=nginx-ui-$GOOS-$GOARCH$(if [[ "$GOARM" ]]; then echo "v$GOARM"; fi)
export _BINARY=nginx-ui$(if [[ "$GOOS" == "windows" ]]; then echo ".exe"; fi)
echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, ABI: $_ABI, RELEASE_NAME: $_NAME, ARTIFACT_NAME: $_ARTIFACT, BINARY_NAME: $_BINARY"
echo "CACHE_NAME=$_NAME" >> $GITHUB_ENV
echo "ARCH_NAME=$_ARCH" >> $GITHUB_ENV
echo "ABI=$_ABI" >> $GITHUB_ENV
echo "DIST=nginx-ui-$_NAME" >> $GITHUB_ENV
echo "ARTIFACT=$_ARTIFACT" >> $GITHUB_ENV
echo "BINARY_NAME=$_BINARY" >> $GITHUB_ENV
- name: Setup Go modules cache
uses: actions/cache@v6
with:
path: |
~/go/pkg/mod
key: go-${{ runner.os }}-${{ runner.arch }}-mod-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-${{ runner.arch }}-mod-
- name: Setup Go build cache
uses: actions/cache@v6
with:
path: |
~/.cache/go-build
key: go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-
- name: Download app artifacts
uses: actions/download-artifact@v8
with:
name: app-dist
path: frontend-dist
- name: Prepare frontend assets
run: |
rm -rf app/dist
mkdir -p app
if [[ -d frontend-dist/app/dist ]]; then
mv frontend-dist/app/dist app/dist
elif [[ -d frontend-dist/dist ]]; then
mv frontend-dist/dist app/dist
else
mv frontend-dist app/dist
fi
- name: Generate files
env:
GOOS: linux
GOARCH: amd64
run: go generate cmd/version/generate.go
- name: Install musl cross compiler
if: env.GOOS == 'linux'
uses: nginxui/musl-cross-compilers@v1
id: musl
with:
target: ${{ env.ARCH_NAME }}-linux-musl${{ env.ABI }}
variant: ${{ env.GOARCH == 'loong64' && 'userdocs/qbt-musl-cross-make' || 'richfelker/musl-cross-make' }}
- name: Post install musl cross compiler
if: env.GOOS == 'linux'
run: |
echo "PATH=${{ steps.musl.outputs.path }}:$PATH" >> $GITHUB_ENV
echo "CC=${{ env.ARCH_NAME }}-linux-musl${{ env.ABI }}-gcc" >> $GITHUB_ENV
echo "CXX=${{ env.ARCH_NAME }}-linux-musl${{ env.ABI }}-g++" >> $GITHUB_ENV
echo "LD_FLAGS=-w --extldflags '-static'" >> $GITHUB_ENV
- name: Setup for Windows
if: env.GOOS == 'windows'
run: |
echo "LD_FLAGS=-w" >> $GITHUB_ENV
echo "CGO_ENABLED=1" >> $GITHUB_ENV
# Install cross compilers based on architecture
sudo apt-get update
sudo apt-get install -y zip
if [[ "$GOARCH" == "amd64" ]]; then
echo "Installing x86_64 Windows cross compiler"
sudo apt-get install -y gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64
echo "CC=x86_64-w64-mingw32-gcc" >> $GITHUB_ENV
echo "CXX=x86_64-w64-mingw32-g++" >> $GITHUB_ENV
elif [[ "$GOARCH" == "386" ]]; then
echo "Installing i686 Windows cross compiler"
sudo apt-get install -y gcc-mingw-w64-i686 g++-mingw-w64-i686
echo "CC=i686-w64-mingw32-gcc" >> $GITHUB_ENV
echo "CXX=i686-w64-mingw32-g++" >> $GITHUB_ENV
elif [[ "$GOARCH" == "arm64" ]]; then
echo "Installing ARM64 Windows cross compiler"
# Ubuntu's apt repositories don't have mingw for ARM64
# Use llvm-mingw project instead
mkdir -p $HOME/llvm-mingw
wget -q https://github.com/mstorsjo/llvm-mingw/releases/download/20231128/llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz
tar xf llvm-mingw-20231128-ucrt-ubuntu-20.04-x86_64.tar.xz -C $HOME/llvm-mingw --strip-components=1
echo "PATH=$HOME/llvm-mingw/bin:$PATH" >> $GITHUB_ENV
echo "CC=aarch64-w64-mingw32-clang" >> $GITHUB_ENV
echo "CXX=aarch64-w64-mingw32-clang++" >> $GITHUB_ENV
else
echo "Unsupported Windows architecture: $GOARCH"
exit 1
fi
- name: Build
run: |
mkdir -p dist
go build -trimpath -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'" -o dist/$BINARY_NAME -v main.go
- name: Archive backend artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ env.ARTIFACT }}
path: dist/${{ env.BINARY_NAME }}
- name: Prepare publish
run: |
cp README*.md ./dist
find dist -printf '%P\n' | tar -C dist --no-recursion -zcvf ${{ env.DIST }}.tar.gz -T -
openssl dgst -sha512 ${{ env.DIST }}.tar.gz | sed 's/([^)]*)//g' | awk '{print $NF}' >> ${{ env.DIST }}.tar.gz.digest
# Create zip for Windows builds (for winget compatibility)
if [[ "$GOOS" == "windows" ]]; then
cd dist
zip -r ../${{ env.DIST }}.zip .
cd ..
openssl dgst -sha512 ${{ env.DIST }}.zip | sed 's/([^)]*)//g' | awk '{print $NF}' >> ${{ env.DIST }}.zip.digest
fi
- name: Sign publish archives
if: github.event_name == 'release' || (github.event_name != 'pull_request' && github.ref == 'refs/heads/dev')
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: |
archives=("${{ env.DIST }}.tar.gz")
if [[ "$GOOS" == "windows" ]]; then
archives+=("${{ env.DIST }}.zip")
fi
env GOOS="$(go env GOHOSTOS)" GOARCH="$(go env GOHOSTARCH)" CGO_ENABLED=0 go run ./cmd/sign-release "${archives[@]}"
- name: Publish
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
${{ env.DIST }}.tar.gz
${{ env.DIST }}.tar.gz.digest
${{ env.DIST }}.tar.gz.minisig
${{ env.GOOS == 'windows' && format('{0}.zip', env.DIST) || '' }}
${{ env.GOOS == 'windows' && format('{0}.zip.digest', env.DIST) || '' }}
${{ env.GOOS == 'windows' && format('{0}.zip.minisig', env.DIST) || '' }}
- name: Upload to R2 using S3 API
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
run: |
echo "Uploading ${{ env.DIST }}.tar.gz to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.digest to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.digest s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.digest --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.minisig to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.minisig s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.minisig --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Upload completed successfully"
build_termux:
runs-on: ubuntu-latest
needs: build_app
env:
CGO_ENABLED: 1
GOOS: android
GOARCH: arm64
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: ^1.26.5
cache: false
- name: Setup environment
id: info_termux
run: |
export _NAME=$(jq ".termux[\"$GOARCH\"].name" -r < .github/build/build_info.json)
export _ARTIFACT=nginx-ui-$_NAME
export _BINARY=nginx-ui
echo "GOOS: $GOOS, GOARCH: $GOARCH, RELEASE_NAME: $_NAME, ARTIFACT_NAME: $_ARTIFACT, BINARY_NAME: $_BINARY"
echo "CACHE_NAME=$_NAME" >> $GITHUB_ENV
echo "DIST=nginx-ui-$_NAME" >> $GITHUB_ENV
echo "ARTIFACT=$_ARTIFACT" >> $GITHUB_ENV
echo "BINARY_NAME=$_BINARY" >> $GITHUB_ENV
- name: Setup Go modules cache
uses: actions/cache@v6
with:
path: |
~/go/pkg/mod
key: go-${{ runner.os }}-${{ runner.arch }}-mod-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-${{ runner.arch }}-mod-
- name: Setup Go build cache
uses: actions/cache@v6
with:
path: |
~/.cache/go-build
key: go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-
- name: Download app artifacts
uses: actions/download-artifact@v8
with:
name: app-dist
path: frontend-dist
- name: Prepare frontend assets
run: |
rm -rf app/dist
mkdir -p app
if [[ -d frontend-dist/app/dist ]]; then
mv frontend-dist/app/dist app/dist
elif [[ -d frontend-dist/dist ]]; then
mv frontend-dist/dist app/dist
else
mv frontend-dist app/dist
fi
- name: Generate files
env:
GOOS: linux
GOARCH: amd64
run: go generate cmd/version/generate.go
- name: Set up Android NDK
id: ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: r27c
- name: Configure Android toolchain
run: |
API_LEVEL=24
TOOLCHAIN="${{ steps.ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "PATH=$TOOLCHAIN:$PATH" >> $GITHUB_ENV
echo "ANDROID_API_LEVEL=$API_LEVEL" >> $GITHUB_ENV
echo "CC=aarch64-linux-android${API_LEVEL}-clang" >> $GITHUB_ENV
echo "CXX=aarch64-linux-android${API_LEVEL}-clang++" >> $GITHUB_ENV
echo "LD_FLAGS=-w" >> $GITHUB_ENV
echo "GOOS=android" >> $GITHUB_ENV
echo "GOARCH=arm64" >> $GITHUB_ENV
echo "CGO_ENABLED=1" >> $GITHUB_ENV
- name: Build
env:
GOOS: android
GOARCH: arm64
CGO_ENABLED: 1
run: |
mkdir -p dist
go build -trimpath -tags=jsoniter -ldflags "$LD_FLAGS -X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'" -o dist/$BINARY_NAME -v main.go
- name: Archive backend artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ env.ARTIFACT }}
path: dist/${{ env.BINARY_NAME }}
- name: Prepare publish
run: |
cp README*.md ./dist
find dist -printf '%P\n' | tar -C dist --no-recursion -zcvf ${{ env.DIST }}.tar.gz -T -
openssl dgst -sha512 ${{ env.DIST }}.tar.gz | sed 's/([^)]*)//g' | awk '{print $NF}' >> ${{ env.DIST }}.tar.gz.digest
- name: Sign publish archive
if: github.event_name == 'release' || (github.event_name != 'pull_request' && github.ref == 'refs/heads/dev')
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: env GOOS="$(go env GOHOSTOS)" GOARCH="$(go env GOHOSTARCH)" CGO_ENABLED=0 go run ./cmd/sign-release "${{ env.DIST }}.tar.gz"
- name: Publish
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
${{ env.DIST }}.tar.gz
${{ env.DIST }}.tar.gz.digest
${{ env.DIST }}.tar.gz.minisig
- name: Upload to R2 using S3 API
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
run: |
echo "Uploading ${{ env.DIST }}.tar.gz to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.digest to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.digest s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.digest --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.minisig to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.minisig s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.minisig --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Upload completed successfully"
build_macos_native:
runs-on: macos-latest
needs: build_app
strategy:
matrix:
goarch: [amd64, arm64]
env:
CGO_ENABLED: 1
GOOS: darwin
GOARCH: ${{ matrix.goarch }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: ^1.26.5
cache: false
- name: Setup environment
id: info
run: |
export _NAME=$(jq ".darwin[\"$GOARCH\"].name" -r < .github/build/build_info.json)
export _ARTIFACT=nginx-ui-darwin-$GOARCH
export _BINARY=nginx-ui
echo "GOOS: darwin, GOARCH: $GOARCH, RELEASE_NAME: $_NAME, ARTIFACT_NAME: $_ARTIFACT, BINARY_NAME: $_BINARY"
echo "CACHE_NAME=$_NAME" >> $GITHUB_ENV
echo "DIST=nginx-ui-$_NAME" >> $GITHUB_ENV
echo "ARTIFACT=$_ARTIFACT" >> $GITHUB_ENV
echo "BINARY_NAME=$_BINARY" >> $GITHUB_ENV
- name: Setup Go build cache
uses: actions/cache@v6
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-${{ hashFiles('go.sum') }}
restore-keys: |
go-${{ runner.os }}-${{ runner.arch }}-${{ env.CACHE_NAME }}-
- name: Download app artifacts
uses: actions/download-artifact@v8
with:
name: app-dist
path: frontend-dist
- name: Prepare frontend assets
run: |
rm -rf app/dist
mkdir -p app
if [[ -d frontend-dist/app/dist ]]; then
mv frontend-dist/app/dist app/dist
elif [[ -d frontend-dist/dist ]]; then
mv frontend-dist/dist app/dist
else
mv frontend-dist app/dist
fi
- name: Generate files
run: go generate cmd/version/generate.go
- name: Build with native CGO
run: |
mkdir -p dist
go build -trimpath -tags=jsoniter -ldflags "-w -X 'github.com/0xJacky/Nginx-UI/settings.buildTime=$(date +%s)'" -o dist/$BINARY_NAME -v main.go
- name: Archive backend artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ env.ARTIFACT }}
path: dist/${{ env.BINARY_NAME }}
- name: Prepare publish
run: |
cp README*.md ./dist
cd dist && tar -zcvf ../${{ env.DIST }}.tar.gz .
cd ..
openssl dgst -sha512 ${{ env.DIST }}.tar.gz | sed 's/([^)]*)//g' | awk '{print $NF}' >> ${{ env.DIST }}.tar.gz.digest
- name: Sign publish archive
if: github.event_name == 'release' || (github.event_name != 'pull_request' && github.ref == 'refs/heads/dev')
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
run: env GOOS="$(go env GOHOSTOS)" GOARCH="$(go env GOHOSTARCH)" CGO_ENABLED=0 go run ./cmd/sign-release "${{ env.DIST }}.tar.gz"
- name: Publish
uses: softprops/action-gh-release@v3
if: github.event_name == 'release'
with:
files: |
${{ env.DIST }}.tar.gz
${{ env.DIST }}.tar.gz.digest
${{ env.DIST }}.tar.gz.minisig
- name: Upload to R2 using S3 API
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
run: |
echo "Uploading ${{ env.DIST }}.tar.gz to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.digest to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.digest s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.digest --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Uploading ${{ env.DIST }}.tar.gz.minisig to R2..."
aws s3 cp ./${{ env.DIST }}.tar.gz.minisig s3://nginx-ui-dev-build/${{ env.DIST }}.tar.gz.minisig --endpoint-url=${{ secrets.R2_S3_API_ENDPOINT }}
echo "Upload completed successfully"
docker-build:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: [build, build_macos_native]
env:
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6,linux/arm/v5,linux/riscv64
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
uozi/nginx-ui
tags: |
type=schedule
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{raw}}
type=sha
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
- name: Download artifacts
uses: actions/download-artifact@v8
with:
path: ./dist
- name: Prepare Artifacts
run: chmod +x ./dist/nginx-ui-*/nginx-ui*
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm,arm64,riscv64
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Prepare Dockerfile
run: |
cp ./Dockerfile ./dist
cp -rp ./resources ./dist
- name: Build and push
uses: docker/build-push-action@v7
with:
context: ./dist
file: ./dist/Dockerfile
platforms: ${{ env.PLATFORMS }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Prepare Demo Dockerfile
if: github.ref == 'refs/heads/dev'
run: |
cp ./demo.Dockerfile ./dist
cp -rp ./resources ./dist
- name: Build and push demo
uses: docker/build-push-action@v7
if: github.ref == 'refs/heads/dev'
with:
context: ./dist
file: ./dist/demo.Dockerfile
# linux/amd64 only. The demo image exists to run on Cloudflare
# Containers, which is amd64; the other five platforms were built
# under QEMU and thrown away.
platforms: linux/amd64
push: 'true'
# The immutable tag is what the Worker deploys against — Cloudflare
# pins a container application to a specific image, so redeploying
# with an unchanged :latest would not roll instances onto new bytes.
tags: |
uozi/nginx-ui-demo:latest
uozi/nginx-ui-demo:sha-${{ github.sha }}
deploy-demo:
name: Deploy demo to Cloudflare
if: github.ref == 'refs/heads/dev' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
needs: docker-build
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Worker dependencies
working-directory: cloudflare
run: bun install --frozen-lockfile
# Deploys against the image docker-build just published rather than
# rebuilding it, so this step is a Worker update and takes seconds.
- name: Deploy
working-directory: cloudflare
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: sh ./deploy-published.sh "docker.io/uozi/nginx-ui-demo:sha-${{ github.sha }}"
helm-smoke:
name: Helm install and upgrade smoke test
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: docker-build
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Helm
uses: azure/setup-helm@v5
with:
version: v4.2.3
- name: Create kind cluster
uses: helm/kind-action@v1.14.0
with:
version: v0.31.0
kubectl_version: v1.35.0
cluster_name: nginx-ui-helm
wait: 120s
- name: Install, persist, and upgrade
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
kubectl apply -f charts/nginx-ui/ci/smoke-pvs.yaml
helm install nginx-ui charts/nginx-ui \
--namespace nginx-ui \
--create-namespace \
--values charts/nginx-ui/ci/smoke-values.yaml \
--set-string image.tag="$version" \
--wait \
--timeout 10m
old_pod_uid="$(kubectl get pod -n nginx-ui -l app.kubernetes.io/instance=nginx-ui -o jsonpath='{.items[0].metadata.uid}')"
test -n "$old_pod_uid"
kubectl exec -n nginx-ui deploy/nginx-ui -- \
sh -c 'printf "%s\n" persisted > /etc/nginx-ui/.helm-smoke-marker'
helm upgrade nginx-ui charts/nginx-ui \
--namespace nginx-ui \
--values charts/nginx-ui/ci/smoke-values.yaml \
--set-string image.tag="$version" \
--set-string podAnnotations.nginx-ui-smoke-revision=2 \
--wait \
--timeout 10m
new_pod_uid="$(kubectl get pod -n nginx-ui -l app.kubernetes.io/instance=nginx-ui -o jsonpath='{.items[0].metadata.uid}')"
test -n "$new_pod_uid"
test "$new_pod_uid" != "$old_pod_uid"
kubectl exec -n nginx-ui deploy/nginx-ui -- \
test -f /etc/nginx-ui/.helm-smoke-marker
kubectl port-forward -n nginx-ui service/nginx-ui 18080:80 >/tmp/nginx-ui-port-forward.log 2>&1 &
port_forward_pid=$!
trap 'kill "$port_forward_pid" 2>/dev/null || true' EXIT
for attempt in {1..30}; do
if curl -fsS http://127.0.0.1:18080/healthz; then
exit 0
fi
sleep 2
done
cat /tmp/nginx-ui-port-forward.log
exit 1
- name: Collect failure diagnostics
if: failure()
run: |
kubectl get all,pvc,pv -A
kubectl describe pod -n nginx-ui -l app.kubernetes.io/instance=nginx-ui
kubectl logs -n nginx-ui deploy/nginx-ui --all-containers=true --tail=200 || true
publish-helm:
name: Publish Helm repository
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: helm-smoke
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Helm
uses: azure/setup-helm@v5
with:
version: v4.2.3
- name: Package chart
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
mkdir -p helm-repository
helm package charts/nginx-ui \
--version "$version" \
--app-version "$version" \
--destination helm-repository
- name: Build repository index
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
AWS_EC2_METADATA_DISABLED: "true"
R2_ENDPOINT: ${{ secrets.R2_S3_API_ENDPOINT }}
run: |
set -euo pipefail
if aws s3api head-object \
--bucket nginx-ui-dev-build \
--key helm/index.yaml \
--endpoint-url="$R2_ENDPOINT" \
>/dev/null 2>helm-repository/head-object.error; then
aws s3 cp s3://nginx-ui-dev-build/helm/index.yaml helm-repository/previous-index.yaml \
--endpoint-url="$R2_ENDPOINT"
helm repo index helm-repository \
--url https://cloud.nginxui.com/helm \
--merge helm-repository/previous-index.yaml
rm helm-repository/previous-index.yaml
elif grep -Eq '(404|Not Found|NoSuchKey)' helm-repository/head-object.error; then
helm repo index helm-repository --url https://cloud.nginxui.com/helm
else
cat helm-repository/head-object.error >&2
exit 1
fi
rm helm-repository/head-object.error
- name: Upload chart before index
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
AWS_EC2_METADATA_DISABLED: "true"
R2_ENDPOINT: ${{ secrets.R2_S3_API_ENDPOINT }}
run: |
set -euo pipefail
chart_file="$(find helm-repository -maxdepth 1 -name 'nginx-ui-*.tgz' -print -quit)"
test -n "$chart_file"
aws s3 cp "$chart_file" "s3://nginx-ui-dev-build/helm/$(basename "$chart_file")" \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/gzip \
--cache-control 'public, max-age=31536000, immutable'
aws s3 cp helm-repository/index.yaml s3://nginx-ui-dev-build/helm/index.yaml \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/yaml \
--cache-control 'public, max-age=300'
build-openwrt:
name: OpenWrt ${{ matrix.feed_arch }}
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
include:
- target: x86
subtarget: "64"
feed_arch: x86_64
binary_artifact: nginx-ui-linux-amd64
- target: armsr
subtarget: armv8
feed_arch: aarch64_generic
binary_artifact: nginx-ui-linux-arm64
- target: mediatek
subtarget: filogic
feed_arch: aarch64_cortex-a53
binary_artifact: nginx-ui-linux-arm64
- target: armsr
subtarget: armv7
feed_arch: arm_cortex-a15_neon-vfpv4
binary_artifact: nginx-ui-linux-armv7
- target: ipq40xx
subtarget: generic
feed_arch: arm_cortex-a7_neon-vfpv4
binary_artifact: nginx-ui-linux-armv7
- target: sifiveu
subtarget: generic
feed_arch: riscv64_generic
binary_artifact: nginx-ui-linux-riscv64
env:
OPENWRT_VERSION: 25.12.5
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Install SDK dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential clang flex bison g++ gawk gcc-multilib gettext git \
libncurses-dev libssl-dev python3-setuptools rsync swig unzip zlib1g-dev \
file wget zstd
- name: Download release binary
uses: actions/download-artifact@v8
with:
name: ${{ matrix.binary_artifact }}
path: release-binary
- name: Download and verify OpenWrt SDK
env:
TARGET: ${{ matrix.target }}
SUBTARGET: ${{ matrix.subtarget }}
run: |
set -euo pipefail
mirror_base_url="https://mirror-03.infra.openwrt.org/releases/${OPENWRT_VERSION}/targets/${TARGET}/${SUBTARGET}"
downloads_base_url="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/${TARGET}/${SUBTARGET}"
archive_base_url="https://archive.openwrt.org/releases/${OPENWRT_VERSION}/targets/${TARGET}/${SUBTARGET}"
download_openwrt_file() {
local output="$1"
local filename="$2"
local base_url
for base_url in "$mirror_base_url" "$downloads_base_url" "$archive_base_url"; do
if curl --retry 3 --retry-all-errors --retry-delay 2 \
-fsSLo "$output" "${base_url}/${filename}"; then
return 0
fi
done
return 1
}
download_openwrt_file sha256sums sha256sums
sdk_filename="$(awk '$2 ~ /^\*?openwrt-sdk-.*\.Linux-x86_64\.tar\.zst$/ {sub(/^\*/, "", $2); print $2; exit}' sha256sums)"
sdk_sha256="$(awk -v file="$sdk_filename" '$2 == file || $2 == "*" file {print $1; exit}' sha256sums)"
test -n "$sdk_filename"
test -n "$sdk_sha256"
download_openwrt_file "$sdk_filename" "$sdk_filename"
echo "${sdk_sha256} ${sdk_filename}" | sha256sum --check
sdk_dir="$(tar --zstd -tf "$sdk_filename" | sed -n '1s#/.*##p')"
test -n "$sdk_dir"
tar --zstd -xf "$sdk_filename"
echo "SDK_DIR=$sdk_dir" >> "$GITHUB_ENV"
- name: Prepare package and signing key
env:
OPENWRT_APK_PRIVATE_KEY: ${{ secrets.OPENWRT_APK_PRIVATE_KEY }}
run: |
set -euo pipefail
test -n "$OPENWRT_APK_PRIVATE_KEY"
package_dir="$SDK_DIR/package/nginx-ui"
cp -R packaging/openwrt "$package_dir"
install -m 0755 release-binary/nginx-ui "$package_dir/files/nginx-ui"
install -m 0755 resources/services/nginx-ui.openwrt "$package_dir/files/nginx-ui.init"
install -m 0644 resources/keep.openwrt "$package_dir/files/nginx-ui.keep"
install -m 0644 LICENSE "$package_dir/files/LICENSE"
printf '%s\n' "$OPENWRT_APK_PRIVATE_KEY" > "$SDK_DIR/private-key.pem"
if ! grep -q '^untrusted comment:' "$SDK_DIR/private-key.pem"; then
sed -i '1i untrusted comment: Nginx UI repository key' "$SDK_DIR/private-key.pem"
fi
openssl ec -in "$SDK_DIR/private-key.pem" -pubout > "$SDK_DIR/public-key.pem.tmp"
{
echo 'untrusted comment: Nginx UI repository key'
cat "$SDK_DIR/public-key.pem.tmp"
} > "$SDK_DIR/public-key.pem"
rm "$SDK_DIR/public-key.pem.tmp"
chmod 0600 "$SDK_DIR/private-key.pem"
- name: Build signed APK repository
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
EXPECTED_FEED_ARCH: ${{ matrix.feed_arch }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
cd "$SDK_DIR"
{
echo 'CONFIG_PACKAGE_nginx-ui=m'
echo 'CONFIG_SIGNED_PACKAGES=y'
echo 'CONFIG_USE_APK=y'
} >> .config
make defconfig
make package/nginx-ui/compile V=s NGINX_UI_VERSION="$version"
make package/index V=s
package_files=(bin/packages/*/base/nginx-ui-*.apk)
test "${#package_files[@]}" -eq 1
package_file="${package_files[0]}"
package_repo="$(dirname "$package_file")"
feed_arch="$(basename "$(dirname "$package_repo")")"
test "$feed_arch" = "$EXPECTED_FEED_ARCH"
test -s "$package_repo/packages.adb"
test -s "$package_repo/index.json"
output_dir="$GITHUB_WORKSPACE/openwrt-artifact/$feed_arch"
mkdir -p "$output_dir"
cp "$package_file" "$package_repo/packages.adb" "$package_repo/index.json" "$output_dir/"
cp public-key.pem "$output_dir/public-key.pem"
- name: Upload repository artifact
uses: actions/upload-artifact@v7
with:
name: openwrt-${{ matrix.feed_arch }}
path: openwrt-artifact
if-no-files-found: error
publish-openwrt:
name: Publish OpenWrt repository
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: build-openwrt
steps:
- name: Download repository artifacts
uses: actions/download-artifact@v8
with:
pattern: openwrt-*
path: openwrt-repository
merge-multiple: true
- name: Verify repository artifacts
run: |
set -euo pipefail
reference_key=""
for feed_dir in openwrt-repository/*; do
test -d "$feed_dir"
test -s "$feed_dir/packages.adb"
test -s "$feed_dir/index.json"
package_files=("$feed_dir"/nginx-ui-*.apk)
test "${#package_files[@]}" -eq 1
if [[ -z "$reference_key" ]]; then
reference_key="$feed_dir/public-key.pem"
else
cmp "$reference_key" "$feed_dir/public-key.pem"
fi
done
test -n "$reference_key"
cp "$reference_key" openwrt-repository/public-key.pem
- name: Upload packages before indexes
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
AWS_EC2_METADATA_DISABLED: "true"
R2_ENDPOINT: ${{ secrets.R2_S3_API_ENDPOINT }}
run: |
set -euo pipefail
aws s3 cp openwrt-repository/public-key.pem s3://nginx-ui-dev-build/openwrt/public-key.pem \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/x-pem-file \
--cache-control 'public, max-age=3600'
for feed_dir in openwrt-repository/*; do
[[ -d "$feed_dir" ]] || continue
feed_arch="$(basename "$feed_dir")"
package_files=("$feed_dir"/nginx-ui-*.apk)
aws s3 cp "${package_files[0]}" "s3://nginx-ui-dev-build/openwrt/25.12/${feed_arch}/$(basename "${package_files[0]}")" \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/vnd.alpine.apk \
--cache-control 'public, max-age=31536000, immutable'
done
for feed_dir in openwrt-repository/*; do
[[ -d "$feed_dir" ]] || continue
feed_arch="$(basename "$feed_dir")"
aws s3 cp "$feed_dir/index.json" "s3://nginx-ui-dev-build/openwrt/25.12/${feed_arch}/index.json" \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/json \
--cache-control 'public, max-age=300'
aws s3 cp "$feed_dir/packages.adb" "s3://nginx-ui-dev-build/openwrt/25.12/${feed_arch}/packages.adb" \
--endpoint-url="$R2_ENDPOINT" \
--content-type application/octet-stream \
--cache-control 'public, max-age=300'
done
update-homebrew:
runs-on: ubuntu-latest
needs: [build, build_macos_native]
if: github.event_name == 'release'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Get release info
id: release
run: |
echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Download release assets and calculate SHA256 checksums
id: checksums
run: |
VERSION="${{ steps.release.outputs.version }}"
TAG_NAME="${{ steps.release.outputs.tag_name }}"
# Download binary files from releases and calculate SHA256
mkdir -p downloads
# macOS Intel
wget -O downloads/nginx-ui-macos-64.tar.gz "https://github.com/${{ github.repository }}/releases/download/$TAG_NAME/nginx-ui-macos-64.tar.gz"
MACOS_INTEL_SHA256=$(sha256sum downloads/nginx-ui-macos-64.tar.gz | cut -d' ' -f1)
# macOS ARM
wget -O downloads/nginx-ui-macos-arm64-v8a.tar.gz "https://github.com/${{ github.repository }}/releases/download/$TAG_NAME/nginx-ui-macos-arm64-v8a.tar.gz"
MACOS_ARM_SHA256=$(sha256sum downloads/nginx-ui-macos-arm64-v8a.tar.gz | cut -d' ' -f1)
# Linux Intel
wget -O downloads/nginx-ui-linux-64.tar.gz "https://github.com/${{ github.repository }}/releases/download/$TAG_NAME/nginx-ui-linux-64.tar.gz"
LINUX_INTEL_SHA256=$(sha256sum downloads/nginx-ui-linux-64.tar.gz | cut -d' ' -f1)
# Linux ARM
wget -O downloads/nginx-ui-linux-arm64-v8a.tar.gz "https://github.com/${{ github.repository }}/releases/download/$TAG_NAME/nginx-ui-linux-arm64-v8a.tar.gz"
LINUX_ARM_SHA256=$(sha256sum downloads/nginx-ui-linux-arm64-v8a.tar.gz | cut -d' ' -f1)
echo "macos_intel_sha256=$MACOS_INTEL_SHA256" >> $GITHUB_OUTPUT
echo "macos_arm_sha256=$MACOS_ARM_SHA256" >> $GITHUB_OUTPUT
echo "linux_intel_sha256=$LINUX_INTEL_SHA256" >> $GITHUB_OUTPUT
echo "linux_arm_sha256=$LINUX_ARM_SHA256" >> $GITHUB_OUTPUT
- name: Generate Homebrew Formula
id: formula
run: |
cat > nginx-ui.rb << 'EOF'
class NginxUi < Formula
desc "Yet another Nginx Web UI"
homepage "https://github.com/0xJacky/nginx-ui"
license "AGPL-3.0"
on_macos do
on_intel do
url "https://github.com/0xJacky/nginx-ui/releases/download/${{ steps.release.outputs.tag_name }}/nginx-ui-macos-64.tar.gz"
sha256 "${{ steps.checksums.outputs.macos_intel_sha256 }}"
end
on_arm do
url "https://github.com/0xJacky/nginx-ui/releases/download/${{ steps.release.outputs.tag_name }}/nginx-ui-macos-arm64-v8a.tar.gz"
sha256 "${{ steps.checksums.outputs.macos_arm_sha256 }}"
end
end
on_linux do
on_intel do
url "https://github.com/0xJacky/nginx-ui/releases/download/${{ steps.release.outputs.tag_name }}/nginx-ui-linux-64.tar.gz"
sha256 "${{ steps.checksums.outputs.linux_intel_sha256 }}"
end
on_arm do
url "https://github.com/0xJacky/nginx-ui/releases/download/${{ steps.release.outputs.tag_name }}/nginx-ui-linux-arm64-v8a.tar.gz"
sha256 "${{ steps.checksums.outputs.linux_arm_sha256 }}"
end
end
def install
bin.install "nginx-ui"
# Create configuration directory
(etc/"nginx-ui").mkpath
# Create default configuration file if it doesn't exist
config_file = etc/"nginx-ui/app.ini"
unless config_file.exist?
config_file.write <<~EOS
[app]
PageSize = 10
[server]
Host = 0.0.0.0
Port = 9000
RunMode = release
[cert]
HTTPChallengePort = 9180
[terminal]
StartCmd = login
EOS
end
# Create data directory
(var/"nginx-ui").mkpath
end
def post_install
# Ensure correct permissions
(var/"nginx-ui").chmod 0755
end
service do
run [opt_bin/"nginx-ui", "serve", "--config", etc/"nginx-ui/app.ini"]
keep_alive true
working_dir var/"nginx-ui"
log_path var/"log/nginx-ui.log"
error_log_path var/"log/nginx-ui.err.log"
end
test do
assert_match version.to_s, shell_output("#{bin}/nginx-ui --version")
end
end
EOF
echo "Generated Homebrew Formula:"
cat nginx-ui.rb
- name: Checkout homebrew-tools repository
uses: actions/checkout@v7
with:
repository: 0xJacky/homebrew-tools
path: homebrew-tools
token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
- name: Update Formula file
run: |
# Copy the generated formula to the correct location
mkdir -p homebrew-tools/Formula/
cp nginx-ui.rb homebrew-tools/Formula/nginx-ui.rb
- name: Verify Formula
run: |
cd homebrew-tools
# Basic syntax check
ruby -c Formula/nginx-ui.rb
echo "Formula syntax is valid"
- name: Create Pull Request to homebrew-tools
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
path: homebrew-tools
branch: update-nginx-ui-${{ steps.release.outputs.version }}
delete-branch: true
title: 'nginx-ui ${{ steps.release.outputs.version }}'
body: |
Update nginx-ui to version ${{ steps.release.outputs.version }}
**Release Notes:**
- Version: ${{ steps.release.outputs.version }}
- Release URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag_name }}
**Checksums (SHA256):**
- macOS Intel: ${{ steps.checksums.outputs.macos_intel_sha256 }}
- macOS ARM: ${{ steps.checksums.outputs.macos_arm_sha256 }}
- Linux Intel: ${{ steps.checksums.outputs.linux_intel_sha256 }}
- Linux ARM: ${{ steps.checksums.outputs.linux_arm_sha256 }}
---
This PR was automatically generated by GitHub Actions.
commit-message: 'nginx-ui ${{ steps.release.outputs.version }}'
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
add-paths: |
Formula/nginx-ui.rb
publish-winget:
runs-on: windows-latest
needs: [build, build_macos_native]
if: github.event_name == 'release'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Publish to WinGet
uses: vedantmgoyal9/winget-releaser@v2
with:
identifier: 0xJacky.nginx-ui
max-versions-to-keep: 2
token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
installers-regex: 'nginx-ui-windows.*\.zip$'