diff --git a/.github/actions/auto-tag/action.yml b/.github/actions/auto-tag/action.yml index b64ff3f..86fc212 100644 --- a/.github/actions/auto-tag/action.yml +++ b/.github/actions/auto-tag/action.yml @@ -26,6 +26,9 @@ runs: # main → latest type=raw,enable=${{ github.ref == 'refs/heads/main' }},value=latest + # commit → short sha + type=sha,format=short,prefix= + # release tag(v1.2.3) type=ref,event=tag type=semver,pattern={{version}} diff --git a/.github/workflows/build-base-images.yml b/.github/workflows/build-base-images.yml index 95c2463..6759f94 100644 --- a/.github/workflows/build-base-images.yml +++ b/.github/workflows/build-base-images.yml @@ -145,10 +145,17 @@ jobs: # ------------------------------------------------------------- - uses: actions/checkout@v4 + - name: Resolve short sha tag + id: vars + shell: bash + run: | + set -euo pipefail + echo "sha_short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + - uses: anchore/sbom-action@v0 with: - image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }} + image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}:${{ steps.vars.outputs.sha_short }} output-file: sbom.spdx.json - uses: actions/upload-artifact@v4 @@ -161,7 +168,7 @@ jobs: # ------------------------------------------------------------- - uses: aquasecurity/trivy-action@0.28.0 with: - image-ref: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }} + image-ref: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}:${{ steps.vars.outputs.sha_short }} severity: HIGH,CRITICAL exit-code: '1' @@ -173,5 +180,5 @@ jobs: env: COSIGN_EXPERIMENTAL: "true" run: | - COSIGN_IMAGE=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }} + COSIGN_IMAGE=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}:${{ steps.vars.outputs.sha_short }} cosign sign --yes "$COSIGN_IMAGE" diff --git a/.github/workflows/build-service-images.yml b/.github/workflows/build-service-images.yml index 1593e67..b7cf685 100644 --- a/.github/workflows/build-service-images.yml +++ b/.github/workflows/build-service-images.yml @@ -83,10 +83,10 @@ jobs: strategy: matrix: service: - - { name: account, workdir: account, dockerfile: account/Dockerfile } - - { name: dashboard, workdir: dashboard, dockerfile: dashboard/Dockerfile } - - { name: rag-server, workdir: rag-server, dockerfile: rag-server/Dockerfile } - - { name: xcontrol-init, workdir: ., dockerfile: xcontrol-init/Dockerfile } + - { name: account, image: accounts, workdir: account, dockerfile: account/Dockerfile } + - { name: dashboard, image: dashboard, workdir: dashboard, dockerfile: dashboard/Dockerfile } + - { name: rag-server, image: rag-server, workdir: rag-server, dockerfile: rag-server/Dockerfile } + - { name: xcontrol-init, image: xcontrol-init, workdir: ., dockerfile: xcontrol-init/Dockerfile } steps: # ------------------------------------------------------------- @@ -110,7 +110,7 @@ jobs: id: meta uses: ./.github/actions/auto-tag with: - image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }} + image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.image }} # ------------------------------------------------------------- # Docker Buildx setup @@ -157,9 +157,9 @@ jobs: run: | set -euo pipefail - SERVICE="${{ matrix.service.name }}" - ORIGIN_IMG="${{ env.REGISTRY }}/${{ env.ORG }}/${SERVICE}@${{ steps.build.outputs.digest }}" - TARGET_REPO="docker.io/${TARGET_NS}/${SERVICE}" + IMAGE_NAME="${{ matrix.service.image }}" + ORIGIN_IMG="${{ env.REGISTRY }}/${{ env.ORG }}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}" + TARGET_REPO="docker.io/${TARGET_NS}/${IMAGE_NAME}" TAG="latest" docker pull "$ORIGIN_IMG" @@ -173,10 +173,10 @@ jobs: strategy: matrix: service: - - { name: dashboard, workdir: dashboard, dockerfile: dashboard/Dockerfile } - - { name: account, workdir: account, dockerfile: account/Dockerfile } - - { name: rag-server, workdir: rag-server, dockerfile: rag-server/Dockerfile } - - { name: xcontrol-init, workdir: ., dockerfile: xcontrol-init/Dockerfile } + - { name: dashboard, image: dashboard, workdir: dashboard, dockerfile: dashboard/Dockerfile } + - { name: account, image: accounts, workdir: account, dockerfile: account/Dockerfile } + - { name: rag-server, image: rag-server, workdir: rag-server, dockerfile: rag-server/Dockerfile } + - { name: xcontrol-init, image: xcontrol-init, workdir: ., dockerfile: xcontrol-init/Dockerfile } steps: # ------------------------------------------------------------- @@ -184,17 +184,24 @@ jobs: # ------------------------------------------------------------- - uses: actions/checkout@v4 + - name: Resolve short sha tag + id: vars + shell: bash + run: | + set -euo pipefail + echo "sha_short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + # ------------------------------------------------------------- # SBOM Generation # ------------------------------------------------------------- - uses: anchore/sbom-action@v0 with: - image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }}@${{ steps.build.outputs.digest }} + image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.image }}:${{ steps.vars.outputs.sha_short }} output-file: sbom.spdx.json - uses: actions/upload-artifact@v4 with: - name: sbom-${{ matrix.service.name }} + name: sbom-${{ matrix.service.image }} path: sbom.spdx.json # ------------------------------------------------------------- @@ -202,7 +209,7 @@ jobs: # ------------------------------------------------------------- - uses: aquasecurity/trivy-action@0.28.0 with: - image-ref: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }}@${{ steps.build.outputs.digest }} + image-ref: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.image }}:${{ steps.vars.outputs.sha_short }} severity: HIGH,CRITICAL exit-code: '1' @@ -217,5 +224,5 @@ jobs: env: COSIGN_EXPERIMENTAL: "true" run: | - IMG=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }}@${{ steps.build.outputs.digest }} + IMG=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.image }}:${{ steps.vars.outputs.sha_short }} cosign sign --yes "$IMG" diff --git a/.github/workflows/check-xcontrol-image.yaml b/.github/workflows/check-xcontrol-image.yaml index cac921e..10ef213 100644 --- a/.github/workflows/check-xcontrol-image.yaml +++ b/.github/workflows/check-xcontrol-image.yaml @@ -30,13 +30,13 @@ jobs: IMAGES=( "ghcr.io/cloud-neutral-toolkit/openresty-geoip" "ghcr.io/cloud-neutral-toolkit/postgres-runtime" - "ghcr.io/cloud-neutral-toolkit/account" + "ghcr.io/cloud-neutral-toolkit/accounts" "ghcr.io/cloud-neutral-toolkit/dashboard" "ghcr.io/cloud-neutral-toolkit/rag-server" "ghcr.io/cloud-neutral-toolkit/xcontrol-init" "docker.io/cloudneutral/openresty-geoip" "docker.io/cloudneutral/postgres-runtime" - "docker.io/cloudneutral/account" + "docker.io/cloudneutral/accounts" "docker.io/cloudneutral/dashboard" "docker.io/cloudneutral/rag-server" "docker.io/cloudneutral/xcontrol-init" diff --git a/api/api_test.go b/api/api_test.go index f07bd2d..5dde2b8 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -112,6 +112,14 @@ type testEmailSender struct { messages []capturedEmail } +type stubAgentStatusReader struct { + statuses []agentserver.StatusSnapshot +} + +func (s stubAgentStatusReader) Statuses() []agentserver.StatusSnapshot { + return append([]agentserver.StatusSnapshot(nil), s.statuses...) +} + func (s *testEmailSender) Send(ctx context.Context, msg EmailMessage) error { _ = ctx s.mu.Lock() @@ -643,6 +651,60 @@ func TestSyncConfigSnapshotFallsBackWhenRenderFails(t *testing.T) { } } +func TestSyncConfigSnapshotIncludesNodeDisplayMetadata(t *testing.T) { + gin.SetMode(gin.TestMode) + + router, _, token := newAuthenticatedSyncHarness(t, WithAgentStatusReader(stubAgentStatusReader{ + statuses: []agentserver.StatusSnapshot{ + {Agent: agentserver.Identity{ID: "jp-xhttp.svc.plus", Name: "Japan Node"}}, + {Agent: agentserver.Identity{ID: "us-xhttp.svc.plus", Name: "US Node"}}, + }, + })) + + req := httptest.NewRequest(http.MethodGet, "/api/auth/sync/config?since_version=0", nil) + req.Header.Set("Authorization", "Bearer "+token) + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected sync config success, got %d: %s", rr.Code, rr.Body.String()) + } + + resp := decodeSyncConfigResponse(t, rr) + if len(resp.Nodes) != 2 { + t.Fatalf("expected 2 nodes, got %d", len(resp.Nodes)) + } + + byHost := make(map[string]map[string]interface{}, len(resp.Nodes)) + for _, node := range resp.Nodes { + host, _ := node["host"].(string) + if strings.TrimSpace(host) == "" { + t.Fatalf("expected node host to be populated: %#v", node) + } + byHost[host] = node + } + + jp := byHost["jp-xhttp.svc.plus"] + if jp == nil { + t.Fatalf("expected jp-xhttp.svc.plus node in response: %#v", byHost) + } + if got, _ := jp["id"].(string); got != "jp-xhttp.svc.plus" { + t.Fatalf("expected node id to match host, got %q", got) + } + if got, _ := jp["name"].(string); got != "Japan Node" { + t.Fatalf("expected node name to preserve display name, got %q", got) + } + if got, _ := jp["display_name"].(string); got != "Japan Node" { + t.Fatalf("expected display_name to preserve display name, got %q", got) + } + if got, _ := jp["server_name"].(string); got != "jp-xhttp.svc.plus" { + t.Fatalf("expected server_name to match host, got %q", got) + } + if got, _ := jp["uri_scheme_xhttp"].(string); strings.TrimSpace(got) == "" { + t.Fatalf("expected uri_scheme_xhttp to be populated") + } +} + func TestResendVerificationEndpoint(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/api/config_sync.go b/api/config_sync.go index f8a392f..9bc2e40 100644 --- a/api/config_sync.go +++ b/api/config_sync.go @@ -92,6 +92,7 @@ func (h *handler) respondSyncConfigSnapshot(c *gin.Context) { for _, host := range hosts { nodeName := resolveNodeName(host, registeredNames) countryCode := countryCodeForHost(host) + nodeID := host vlessURI := renderVLESSURIScheme(xhttpScheme, map[string]string{ "UUID": proxyUUID, "DOMAIN": host, @@ -104,8 +105,10 @@ func (h *handler) respondSyncConfigSnapshot(c *gin.Context) { }) profiles = append(profiles, gin.H{ - "id": strings.TrimSpace(user.ID), + "id": nodeID, "remark": nodeName, + "display_name": nodeName, + "host": host, "address": host, "port": 443, "uuid": proxyUUID, @@ -117,19 +120,24 @@ func (h *handler) respondSyncConfigSnapshot(c *gin.Context) { "vless_uri": vlessURI, }) nodes = append(nodes, gin.H{ - "id": strings.TrimSpace(user.ID), - "name": nodeName, - "protocol": "vless", - "transport": "xhttp", - "security": "tls", - "address": host, - "port": 443, - "uuid": proxyUUID, - "flow": "", - "source": "server", - "country_code": countryCode, - "updated_at": updatedAt, - "vless_uri": vlessURI, + "id": nodeID, + "name": nodeName, + "display_name": nodeName, + "remark": nodeName, + "host": host, + "protocol": "vless", + "transport": "xhttp", + "security": "tls", + "address": host, + "port": 443, + "server_name": host, + "uuid": proxyUUID, + "flow": "", + "source": "server", + "country_code": countryCode, + "updated_at": updatedAt, + "vless_uri": vlessURI, + "uri_scheme_xhttp": vlessURI, }) } }