fix: make batch imports fault tolerant #62
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" | |
| SINGBOX_LINUX_AMD64_SHA256: "7c103cb2f9a7dc54cb82962043596718ed27989a478d6405f0939a9b775f889f" | |
| 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: Install pinned sing-box test kernel | |
| run: | | |
| archive="sing-box-${SINGBOX_VERSION}-linux-amd64.tar.gz" | |
| curl --fail --location --silent --show-error \ | |
| --output "${RUNNER_TEMP}/${archive}" \ | |
| "https://github.com/SagerNet/sing-box/releases/download/v${SINGBOX_VERSION}/${archive}" | |
| echo "${SINGBOX_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/${archive}" | sha256sum --check --strict | |
| tar --extract --gzip --file "${RUNNER_TEMP}/${archive}" --directory "${RUNNER_TEMP}" | |
| binary="${RUNNER_TEMP}/sing-box-${SINGBOX_VERSION}-linux-amd64/sing-box" | |
| chmod +x "${binary}" | |
| "${binary}" version | |
| echo "SINGBOX_TEST_BINARY=${binary}" >> "${GITHUB_ENV}" | |
| - name: Build backend | |
| run: CGO_ENABLED=0 go build -o singbox-proxy-manager ./backend | |
| - name: Run backend tests | |
| run: go test -race -count=1 ./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: Verify global version consistency | |
| run: npm run test:version-sync | |
| working-directory: frontend | |
| - name: Build frontend | |
| run: npm run build | |
| working-directory: frontend | |
| test-frontend-e2e: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| script: | |
| - test:e2e:dnd | |
| - test:e2e:dnd-virtual | |
| - test:e2e:preserve-ports | |
| - test:e2e:batch-import | |
| - test:e2e:batch-check-ip-live | |
| - test:e2e:outbound-dedup | |
| - test:e2e:remark | |
| - test:e2e:filter | |
| - test:e2e:tcp-reuse-ui | |
| - test:e2e:nodes-viewport | |
| - test:e2e:version-refresh | |
| - test:e2e:wireguard-ui | |
| - test:e2e:version-update | |
| - test:e2e:nodeform-config-preservation | |
| 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 frontend regression | |
| run: npm run ${{ matrix.script }} | |
| working-directory: frontend | |
| test-system-local-deploy: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - lint-backend | |
| - lint-frontend | |
| - test-backend | |
| - test-frontend-build | |
| - test-frontend-e2e | |
| 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 _ 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 + import + live sing-box chain | |
| run: | | |
| mkdir -p /tmp/sbpm-live-target | |
| printf 'sbpm-live-proxy-ok\n' >/tmp/sbpm-live-target/index.html | |
| python3 -m http.server 39090 --bind 127.0.0.1 \ | |
| --directory /tmp/sbpm-live-target >/tmp/sbpm-live-target.log 2>&1 & | |
| echo "$!" >/tmp/sbpm-live-target.pid | |
| cat >/tmp/sbpm-ci-ss-upstream.json <<'JSON' | |
| { | |
| "log": {"level": "warn"}, | |
| "inbounds": [ | |
| { | |
| "type": "shadowsocks", | |
| "tag": "ss-in", | |
| "listen": "127.0.0.1", | |
| "listen_port": 39091, | |
| "method": "aes-128-gcm", | |
| "password": "ci-ss-password" | |
| } | |
| ], | |
| "outbounds": [{"type": "direct", "tag": "direct"}], | |
| "route": {"final": "direct"} | |
| } | |
| JSON | |
| docker run --detach --name sbpm-ci-ss-upstream --network host \ | |
| --volume /tmp/sbpm-ci-ss-upstream.json:/tmp/config.json:ro \ | |
| --entrypoint sing-box singbox-proxy-manager:local \ | |
| run -c /tmp/config.json | |
| sleep 1 | |
| docker ps --filter name=sbpm-ci-ss-upstream --filter status=running --format '{{.Names}}' \ | |
| | grep -Fx sbpm-ci-ss-upstream | |
| 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 | |
| IMPORT_RESULT=$(curl -fsS -X POST http://127.0.0.1:38080/api/nodes/batch-import \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"content":"socks5://127.0.0.1:9#ci-system-socks","enabled":true}') | |
| echo "$IMPORT_RESULT" | tee /tmp/batch-import.json | |
| echo "$IMPORT_RESULT" | jq -e '.success == 1 and .failed == 0' | |
| SS_CREDENTIALS=$(printf 'aes-128-gcm:ci-ss-password' \ | |
| | base64 --wrap=0 | tr '+/' '-_' | tr -d '=') | |
| SS_LINK="ss://${SS_CREDENTIALS}@127.0.0.1:39091#ci-system-ss" | |
| SS_IMPORT=$(jq -n --arg link "$SS_LINK" '{links: [$link], enabled: true}' \ | |
| | curl -fsS -X POST http://127.0.0.1:38080/api/nodes/batch-import \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H 'Content-Type: application/json' \ | |
| --data-binary @-) | |
| echo "$SS_IMPORT" | tee /tmp/ss-import.json | |
| echo "$SS_IMPORT" | jq -e '.success == 1 and .failed == 0' | |
| SS_PORT=$(echo "$SS_IMPORT" | jq -er '.results[0].port | select(. > 0)') | |
| SS_USER=$(echo "$SS_IMPORT" | jq -er '.results[0].username | select(length > 0)') | |
| SS_PASS=$(echo "$SS_IMPORT" | jq -er '.results[0].password | select(length > 0)') | |
| SS_RESULT=$(curl -fsS --noproxy '' \ | |
| --proxy "http://127.0.0.1:${SS_PORT}" \ | |
| --proxy-user "${SS_USER}:${SS_PASS}" \ | |
| http://127.0.0.1:39090/) | |
| test "$SS_RESULT" = "sbpm-live-proxy-ok" | |
| curl -fsS -H "Authorization: Bearer $TOKEN" \ | |
| http://127.0.0.1:38080/api/nodes | tee /tmp/nodes.json | |
| jq -e '[.[] | select(.name == "ci-system-socks" and .type == "socks5" and .enabled == true)] | length == 1' \ | |
| /tmp/nodes.json | |
| DIRECT_NODE=$(curl -fsS -X POST http://127.0.0.1:38080/api/nodes \ | |
| -H "Authorization: Bearer $TOKEN" \ | |
| -H 'Content-Type: application/json' \ | |
| -d '{"name":"ci-system-direct","type":"direct","config":"{}","username":"ci-live-user","password":"ci-live-pass","tcp_reuse_enabled":true,"enabled":true}') | |
| echo "$DIRECT_NODE" | tee /tmp/direct-node.json | |
| DIRECT_PORT=$(echo "$DIRECT_NODE" | jq -er '.inbound_port | select(. > 0)') | |
| HTTP_RESULT=$(curl -fsS --noproxy '' \ | |
| --proxy "http://127.0.0.1:${DIRECT_PORT}" \ | |
| --proxy-user 'ci-live-user:ci-live-pass' \ | |
| http://127.0.0.1:39090/) | |
| test "$HTTP_RESULT" = "sbpm-live-proxy-ok" | |
| SOCKS_RESULT=$(curl -fsS --noproxy '' \ | |
| --socks5-hostname "127.0.0.1:${DIRECT_PORT}" \ | |
| --proxy-user 'ci-live-user:ci-live-pass' \ | |
| http://127.0.0.1:39090/) | |
| test "$SOCKS_RESULT" = "sbpm-live-proxy-ok" | |
| docker compose --profile local exec -T sb-proxy-local \ | |
| sing-box check -c /app/config/config.json | |
| docker compose --profile local top sb-proxy-local | grep -E '[s]ing-box( |$)' | |
| - name: Stop stack | |
| if: always() | |
| run: | | |
| if test -f /tmp/sbpm-live-target.pid; then | |
| kill "$(cat /tmp/sbpm-live-target.pid)" 2>/dev/null || true | |
| fi | |
| docker rm --force sbpm-ci-ss-upstream 2>/dev/null || true | |
| 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 arch in amd64 arm64; do | |
| GOOS=linux GOARCH="${arch}" CGO_ENABLED=0 \ | |
| go build -tags frontend_dist -trimpath -ldflags="-s -w" \ | |
| -o "release-binaries/singbox-proxy-manager-linux-${arch}" ./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 |