Repository navigation
Merge remote-tracking branch 'origin/dev' into dev #6266
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | |
| - ".github/actions/**" | |
| - ".github/scripts/**" | |
| - "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" | |
| - ".github/actions/**" | |
| - ".github/scripts/**" | |
| - "resources/docker/**" | |
| - "resources/development/*" | |
| - "resources/demo/*" | |
| release: | |
| types: | |
| - published | |
| permissions: | |
| contents: read | |
| jobs: | |
| build_app: | |
| runs-on: macos-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go | |
| if: github.event_name == 'release' | |
| uses: actions/setup-go@v7 | |
| with: | |
| go-version: ^1.27.1 | |
| cache: false | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.4.2 | |
| - 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: ./.github/actions/upload-artifact-retry | |
| 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@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 | |
| if: github.event_name == 'release' | |
| with: | |
| files: | | |
| app-dist.tar.gz | |
| app-dist.tar.gz.minisig | |
| build: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| needs: build_app | |
| strategy: | |
| max-parallel: 3 | |
| 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 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go | |
| uses: actions/setup-go@v7 | |
| with: | |
| go-version: ^1.27.1 | |
| 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 | |
| if: github.event_name != 'release' # ISSUE-705: release builds must not restore a shared, poisonable 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 | |
| if: github.event_name != 'release' # ISSUE-705: release builds must not restore a shared, poisonable 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 Go modules | |
| run: bash .github/scripts/retry.sh -- go mod download | |
| - name: Download app artifacts | |
| uses: ./.github/actions/download-artifact-retry | |
| 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 | |
| # The toolchain comes from a GitHub release asset that intermittently | |
| # answers 5xx; retry the whole action twice with a growing pause. | |
| - name: Install musl cross compiler | |
| if: env.GOOS == 'linux' | |
| uses: nginxui/musl-cross-compilers@57f4de0b40cd0b53b1dc02fb8b317d87f78f3483 # v1 | |
| id: musl | |
| continue-on-error: true | |
| with: | |
| target: ${{ env.ARCH_NAME }}-linux-musl${{ env.ABI }} | |
| variant: ${{ env.GOARCH == 'loong64' && 'userdocs/qbt-musl-cross-make' || 'richfelker/musl-cross-make' }} | |
| - name: Wait before retrying musl cross compiler | |
| if: env.GOOS == 'linux' && steps.musl.outcome == 'failure' | |
| run: sleep 30 | |
| - name: Install musl cross compiler (retry 1) | |
| if: env.GOOS == 'linux' && steps.musl.outcome == 'failure' | |
| uses: nginxui/musl-cross-compilers@57f4de0b40cd0b53b1dc02fb8b317d87f78f3483 # v1 | |
| id: musl_retry1 | |
| continue-on-error: true | |
| with: | |
| target: ${{ env.ARCH_NAME }}-linux-musl${{ env.ABI }} | |
| variant: ${{ env.GOARCH == 'loong64' && 'userdocs/qbt-musl-cross-make' || 'richfelker/musl-cross-make' }} | |
| - name: Wait before retrying musl cross compiler again | |
| if: env.GOOS == 'linux' && steps.musl_retry1.outcome == 'failure' | |
| run: sleep 90 | |
| - name: Install musl cross compiler (retry 2) | |
| if: env.GOOS == 'linux' && steps.musl_retry1.outcome == 'failure' | |
| uses: nginxui/musl-cross-compilers@57f4de0b40cd0b53b1dc02fb8b317d87f78f3483 # v1 | |
| id: musl_retry2 | |
| 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' | |
| env: | |
| MUSL_PATH: ${{ steps.musl.outputs.path || steps.musl_retry1.outputs.path || steps.musl_retry2.outputs.path }} | |
| run: | | |
| test -n "$MUSL_PATH" | |
| echo "PATH=$MUSL_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: ./.github/actions/upload-artifact-retry | |
| 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@efb35369e0ad2afab669f228072c1b0d510eae64 # 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 | |
| permissions: | |
| contents: write | |
| needs: [build_app, build] | |
| env: | |
| CGO_ENABLED: 1 | |
| GOOS: android | |
| GOARCH: arm64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go | |
| uses: actions/setup-go@v7 | |
| with: | |
| go-version: ^1.27.1 | |
| 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 | |
| if: github.event_name != 'release' # ISSUE-705: release builds must not restore a shared, poisonable 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 | |
| if: github.event_name != 'release' # ISSUE-705: release builds must not restore a shared, poisonable 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 Go modules | |
| run: bash .github/scripts/retry.sh -- go mod download | |
| - name: Download app artifacts | |
| uses: ./.github/actions/download-artifact-retry | |
| 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@ed92fe6cadad69be94a966a7ee3271275e62f779 # 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: ./.github/actions/upload-artifact-retry | |
| 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@efb35369e0ad2afab669f228072c1b0d510eae64 # 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 | |
| permissions: | |
| contents: write | |
| needs: [build_app, build] | |
| strategy: | |
| matrix: | |
| goarch: [amd64, arm64] | |
| env: | |
| CGO_ENABLED: 1 | |
| GOOS: darwin | |
| GOARCH: ${{ matrix.goarch }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Go | |
| uses: actions/setup-go@v7 | |
| with: | |
| go-version: ^1.27.1 | |
| 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 | |
| if: github.event_name != 'release' # ISSUE-705: release builds must not restore a shared, poisonable 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 Go modules | |
| run: bash .github/scripts/retry.sh -- go mod download | |
| - name: Download app artifacts | |
| uses: ./.github/actions/download-artifact-retry | |
| 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: ./.github/actions/upload-artifact-retry | |
| 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@efb35369e0ad2afab669f228072c1b0d510eae64 # 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 | |
| with: | |
| persist-credentials: false | |
| - name: Docker meta | |
| id: meta | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # 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: ./.github/actions/download-artifact-retry | |
| with: | |
| path: ./dist | |
| - name: Prepare Artifacts | |
| run: chmod +x ./dist/nginx-ui-*/nginx-ui* | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4 | |
| with: | |
| platforms: arm,arm64,riscv64 | |
| - name: Set up Docker Buildx | |
| id: buildx | |
| uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 | |
| - name: Login to DockerHub | |
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # 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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # 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@c3c9e263c25d99ce0380d002d59b67737d91b0dc # 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 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # 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 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Helm | |
| uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 | |
| with: | |
| version: v4.3.0 | |
| - name: Create kind cluster | |
| uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 # v1.15.0 | |
| with: | |
| version: v0.33.0 | |
| kubectl_version: v1.37.1 | |
| 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 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Helm | |
| uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 | |
| with: | |
| version: v4.3.0 | |
| - 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 | |
| with: | |
| persist-credentials: false | |
| - 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: ./.github/actions/download-artifact-retry | |
| 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: ./.github/actions/upload-artifact-retry | |
| 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: Checkout retry actions | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| sparse-checkout: .github/actions | |
| - name: Download repository artifacts | |
| uses: ./.github/actions/download-artifact-retry | |
| 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 | |
| with: | |
| persist-credentials: false | |
| - 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: Checkout homebrew-tools repository | |
| uses: actions/checkout@v7 | |
| with: | |
| repository: 0xJacky/homebrew-tools | |
| path: homebrew-tools | |
| token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | |
| persist-credentials: false | |
| - name: Update Formula file | |
| env: | |
| REPOSITORY: ${{ github.repository }} | |
| TAG_NAME: ${{ steps.release.outputs.tag_name }} | |
| MACOS_INTEL_SHA256: ${{ steps.checksums.outputs.macos_intel_sha256 }} | |
| MACOS_ARM_SHA256: ${{ steps.checksums.outputs.macos_arm_sha256 }} | |
| LINUX_INTEL_SHA256: ${{ steps.checksums.outputs.linux_intel_sha256 }} | |
| LINUX_ARM_SHA256: ${{ steps.checksums.outputs.linux_arm_sha256 }} | |
| run: | | |
| # Only the download URLs and checksums are rewritten here. The formula | |
| # body is owned by the tap, so fixes landed there (for example when | |
| # Homebrew deprecates a formula DSL method) survive the next release. | |
| python3 - << 'PY' | |
| import os | |
| import pathlib | |
| import re | |
| import sys | |
| formula = pathlib.Path("homebrew-tools/Formula/nginx-ui.rb") | |
| if not formula.exists(): | |
| sys.exit(f"{formula} is missing: add the formula to the tap first") | |
| repository = os.environ["REPOSITORY"] | |
| tag_name = os.environ["TAG_NAME"] | |
| assets = { | |
| "nginx-ui-macos-64.tar.gz": os.environ["MACOS_INTEL_SHA256"], | |
| "nginx-ui-macos-arm64-v8a.tar.gz": os.environ["MACOS_ARM_SHA256"], | |
| "nginx-ui-linux-64.tar.gz": os.environ["LINUX_INTEL_SHA256"], | |
| "nginx-ui-linux-arm64-v8a.tar.gz": os.environ["LINUX_ARM_SHA256"], | |
| } | |
| content = formula.read_text() | |
| for asset, sha256 in assets.items(): | |
| pattern = re.compile( | |
| r'(url\s+"https://github\.com/' | |
| + re.escape(repository) | |
| + r'/releases/download/)[^/"]+(/' | |
| + re.escape(asset) | |
| + r'"\s*\n\s*sha256\s+")[0-9a-f]{64}(")' | |
| ) | |
| content, replaced = pattern.subn( | |
| lambda match: match[1] + tag_name + match[2] + sha256 + match[3], | |
| content, | |
| ) | |
| if replaced != 1: | |
| sys.exit(f"expected 1 url/sha256 pair for {asset}, found {replaced}") | |
| formula.write_text(content) | |
| PY | |
| echo "Updated Homebrew Formula:" | |
| cat homebrew-tools/Formula/nginx-ui.rb | |
| # Leave `token` unset: setup-homebrew writes it as a global git | |
| # extraheader for the rest of the job, and create-pull-request then adds | |
| # its own, so the tap push fails with a duplicate Authorization header. | |
| # `brew style` only needs `brew-gh-api-token`, which defaults to | |
| # github.token. | |
| - name: Set up Homebrew | |
| uses: Homebrew/actions/setup-homebrew@1e34f2e2acaa766b7efacb8c26352e6abbb023f9 # 2026.09.13.1 | |
| - name: Verify Formula | |
| run: | | |
| ruby -c homebrew-tools/Formula/nginx-ui.rb | |
| # The tap runs `brew style` on the pull request, so run it here too: | |
| # a deprecated formula DSL fails the release job instead of the tap. | |
| brew style homebrew-tools/Formula/nginx-ui.rb | |
| - name: Create Pull Request to homebrew-tools | |
| id: homebrew-pr | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # 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 | |
| - name: Close superseded Homebrew pull requests | |
| if: ${{ steps.homebrew-pr.outputs.pull-request-number }} | |
| env: | |
| GH_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| mapfile -t nginx_ui_pull_requests < <( | |
| gh pr list \ | |
| --repo 0xJacky/homebrew-tools \ | |
| --state open \ | |
| --limit 100 \ | |
| --json number,headRefName,baseRefName \ | |
| --jq '.[] | |
| | select(.baseRefName == "main") | |
| | select(.headRefName | startswith("update-nginx-ui-")) | |
| | [(.headRefName | sub("^update-nginx-ui-"; "")), (.number | tostring)] | |
| | @tsv' | |
| ) | |
| if (( ${#nginx_ui_pull_requests[@]} <= 1 )); then | |
| exit 0 | |
| fi | |
| keeper="$(printf '%s\n' "${nginx_ui_pull_requests[@]}" | sort -t $'\t' -k1,1V -k2,2n | tail -n 1)" | |
| keeper_number="${keeper##*$'\t'}" | |
| for pull_request in "${nginx_ui_pull_requests[@]}"; do | |
| pull_request_number="${pull_request##*$'\t'}" | |
| if [[ "$pull_request_number" == "$keeper_number" ]]; then | |
| continue | |
| fi | |
| gh pr close "$pull_request_number" \ | |
| --repo 0xJacky/homebrew-tools \ | |
| --comment "Superseded by #${keeper_number}." | |
| done | |
| publish-winget: | |
| runs-on: windows-latest | |
| needs: [build, build_macos_native] | |
| if: github.event_name == 'release' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Publish to WinGet | |
| uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 | |
| with: | |
| identifier: 0xJacky.nginx-ui | |
| max-versions-to-keep: 0 | |
| token: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} | |
| installers-regex: 'nginx-ui-windows.*\.zip$' |