feat: preserve inbound ports on reorder #32
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: CI/CD | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| env: | |
| GO_VERSION: "1.24.x" | |
| NODE_VERSION: "18" | |
| SINGBOX_VERSION: "1.12.12" | |
| jobs: | |
| lint-backend: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@v8 | |
| with: | |
| version: v2.8.0 | |
| working-directory: ./ | |
| args: ./backend/... | |
| lint-frontend: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Run ESLint | |
| run: npm run lint | |
| working-directory: frontend | |
| test-backend: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Download dependencies | |
| run: go mod download | |
| - name: Build backend | |
| run: CGO_ENABLED=0 go build -o singbox-proxy-manager ./backend | |
| - name: Run backend tests | |
| run: go test ./backend/... -v | |
| test-frontend-build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Build frontend | |
| run: npm run build | |
| working-directory: frontend | |
| test-frontend-e2e-dnd: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Resolve browser path | |
| run: | | |
| if command -v google-chrome >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome)" >> "$GITHUB_ENV" | |
| elif command -v google-chrome-stable >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome-stable)" >> "$GITHUB_ENV" | |
| elif command -v chromium-browser >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium-browser)" >> "$GITHUB_ENV" | |
| elif command -v chromium >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium)" >> "$GITHUB_ENV" | |
| else | |
| echo "::error::No Chrome/Chromium executable found on runner." | |
| exit 1 | |
| fi | |
| - name: Run drag reorder regression e2e | |
| run: npm run test:e2e:dnd | |
| working-directory: frontend | |
| test-frontend-e2e-version-refresh: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Resolve browser path | |
| run: | | |
| if command -v google-chrome >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome)" >> "$GITHUB_ENV" | |
| elif command -v google-chrome-stable >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome-stable)" >> "$GITHUB_ENV" | |
| elif command -v chromium-browser >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium-browser)" >> "$GITHUB_ENV" | |
| elif command -v chromium >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium)" >> "$GITHUB_ENV" | |
| else | |
| echo "::error::No Chrome/Chromium executable found on runner." | |
| exit 1 | |
| fi | |
| - name: Run version refresh regression e2e | |
| run: npm run test:e2e:version-refresh | |
| working-directory: frontend | |
| test-frontend-e2e-remark: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Resolve browser path | |
| run: | | |
| if command -v google-chrome >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome)" >> "$GITHUB_ENV" | |
| elif command -v google-chrome-stable >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v google-chrome-stable)" >> "$GITHUB_ENV" | |
| elif command -v chromium-browser >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium-browser)" >> "$GITHUB_ENV" | |
| elif command -v chromium >/dev/null 2>&1; then | |
| echo "PUPPETEER_EXECUTABLE_PATH=$(command -v chromium)" >> "$GITHUB_ENV" | |
| else | |
| echo "::error::No Chrome/Chromium executable found on runner." | |
| exit 1 | |
| fi | |
| - name: Run remark regression e2e | |
| run: npm run test:e2e:remark | |
| working-directory: frontend | |
| test-system-local-deploy: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - lint-backend | |
| - lint-frontend | |
| - test-backend | |
| - test-frontend-build | |
| - test-frontend-e2e-dnd | |
| - test-frontend-e2e-version-refresh | |
| - test-frontend-e2e-remark | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Prepare .env for system test | |
| run: | | |
| cp .env.example .env | |
| cat <<'EOT' >> .env | |
| COMPOSE_PROFILES=local | |
| PORT=38080 | |
| ADMIN_PASSWORD=system-test-password | |
| CONFIG_VOLUME_HOST=./config-ci | |
| TZ=UTC+8 | |
| EOT | |
| mkdir -p config-ci | |
| - name: Start stack with local build | |
| run: docker compose --profile local up -d --build | |
| - name: Wait for API readiness | |
| run: | | |
| for i in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:38080/api/version >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "Service is not ready in time" >&2 | |
| docker compose logs | |
| exit 1 | |
| - name: Validate login + protected API | |
| run: | | |
| curl -fsS http://127.0.0.1:38080/api/auth/status | tee /tmp/auth-status.json | |
| TOKEN=$(curl -fsS -X POST http://127.0.0.1:38080/api/login \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"password":"system-test-password"}' | jq -r '.token') | |
| test -n "$TOKEN" && test "$TOKEN" != "null" | |
| curl -fsS -H "Authorization: Bearer $TOKEN" http://127.0.0.1:38080/api/settings >/tmp/settings.json | |
| - name: Stop stack | |
| if: always() | |
| run: docker compose --profile local down -v | |
| test-release-image-build: | |
| runs-on: ubuntu-latest | |
| needs: [test-system-local-deploy] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Build frontend dist for embedded binary | |
| run: npm run build | |
| working-directory: frontend | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Build linux release binaries for image test | |
| run: | | |
| mkdir -p release-binaries | |
| for GOARCH in amd64 arm64; do | |
| GOOS=linux GOARCH="${GOARCH}" CGO_ENABLED=0 \ | |
| go build -tags frontend_dist -trimpath -ldflags="-s -w" \ | |
| -o "release-binaries/singbox-proxy-manager-linux-${GOARCH}" ./backend | |
| done | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Validate Dockerfile.release multi-arch build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: Dockerfile.release | |
| push: false | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: | | |
| SINGBOX_VERSION=${{ env.SINGBOX_VERSION }} | |
| build-frontend-dist: | |
| runs-on: ubuntu-latest | |
| needs: [test-release-image-build] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install frontend dependencies | |
| run: npm ci | |
| working-directory: frontend | |
| - name: Build frontend dist | |
| run: npm run build | |
| working-directory: frontend | |
| - name: Upload frontend dist artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-dist | |
| path: frontend/dist | |
| retention-days: 1 | |
| build-binaries: | |
| runs-on: ubuntu-latest | |
| needs: [build-frontend-dist] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| - goos: linux | |
| goarch: arm64 | |
| - goos: freebsd | |
| goarch: amd64 | |
| - goos: freebsd | |
| goarch: arm64 | |
| - goos: darwin | |
| goarch: amd64 | |
| - goos: darwin | |
| goarch: arm64 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Download frontend dist artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: frontend-dist | |
| path: frontend/dist | |
| - name: Download dependencies | |
| run: go mod download | |
| - name: Build binary | |
| run: | | |
| mkdir -p release-binaries | |
| GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 \ | |
| go build -tags frontend_dist -trimpath -ldflags="-s -w" \ | |
| -o "release-binaries/singbox-proxy-manager-${{ matrix.goos }}-${{ matrix.goarch }}" ./backend | |
| - name: Upload binary artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: singbox-proxy-manager-${{ matrix.goos }}-${{ matrix.goarch }} | |
| path: release-binaries/singbox-proxy-manager-${{ matrix.goos }}-${{ matrix.goarch }} | |
| retention-days: 30 | |
| create-release: | |
| runs-on: ubuntu-latest | |
| needs: [build-binaries] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: write | |
| outputs: | |
| app_version: ${{ steps.app_version.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Read app version | |
| id: app_version | |
| run: echo "version=$(cat internal/version/version.txt)" >> "$GITHUB_OUTPUT" | |
| - name: Download all binary artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: singbox-proxy-manager-* | |
| path: release-binaries | |
| merge-multiple: true | |
| - name: Prepare checksums | |
| run: | | |
| chmod +x release-binaries/singbox-proxy-manager-* || true | |
| (cd release-binaries && sha256sum singbox-proxy-manager-* > checksums.txt) | |
| - name: Create or update release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.app_version.outputs.version }} | |
| name: v${{ steps.app_version.outputs.version }} | |
| body: | | |
| ## 自动构建产物 | |
| ### 二进制 | |
| - Linux: amd64 / arm64 | |
| - FreeBSD: amd64 / arm64 | |
| - macOS: amd64 / arm64 | |
| ### 镜像 | |
| GHCR 多架构镜像在后续作业自动发布(linux/amd64 + linux/arm64),并复用本次 Release 的 Linux 二进制。 | |
| files: | | |
| release-binaries/singbox-proxy-manager-* | |
| release-binaries/checksums.txt | |
| fail_on_unmatched_files: true | |
| draft: false | |
| prerelease: false | |
| build-ghcr: | |
| runs-on: ubuntu-latest | |
| needs: [create-release] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download linux binaries from release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| APP_VERSION: ${{ needs.create-release.outputs.app_version }} | |
| run: | | |
| mkdir -p release-binaries | |
| gh release download "v${APP_VERSION}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --pattern "singbox-proxy-manager-linux-*" \ | |
| --dir release-binaries | |
| chmod +x release-binaries/singbox-proxy-manager-linux-* | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push multi-arch image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: Dockerfile.release | |
| push: true | |
| platforms: linux/amd64,linux/arm64 | |
| build-args: | | |
| SINGBOX_VERSION=${{ env.SINGBOX_VERSION }} | |
| tags: | | |
| ghcr.io/${{ github.repository }}:${{ github.sha }} | |
| ghcr.io/${{ github.repository }}:v${{ needs.create-release.outputs.app_version }} | |
| ghcr.io/${{ github.repository }}:v${{ needs.create-release.outputs.app_version }}-linux-amd64-arm64 | |
| ghcr.io/${{ github.repository }}:latest | |
| labels: | | |
| org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} | |
| org.opencontainers.image.version=v${{ needs.create-release.outputs.app_version }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.description=SingBox Proxy Manager multi-arch image (linux/amd64, linux/arm64) | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max |