ci: ship bridge via image ref artifact
This commit is contained in:
parent
6314df67d5
commit
0fcaa845e1
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.worktrees
|
||||||
84
.github/workflows/pipeline.yml
vendored
84
.github/workflows/pipeline.yml
vendored
@ -25,6 +25,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pipeline-${{ github.ref }}
|
group: pipeline-${{ github.ref }}
|
||||||
@ -64,33 +65,67 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
needs: prep
|
needs: prep
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SERVICE_REGISTRY: ghcr.io
|
||||||
|
SERVICE_IMAGE_REPO_OWNER: ${{ vars.IMAGE_REPO_OWNER || github.repository_owner }}
|
||||||
|
SERVICE_IMAGE_NAME: xworkmate-bridge
|
||||||
outputs:
|
outputs:
|
||||||
artifact_name: ${{ steps.artifact_meta.outputs.artifact_name }}
|
artifact_name: ${{ steps.artifact_meta.outputs.artifact_name }}
|
||||||
|
service_image_ref: ${{ steps.service_ref.outputs.image_ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up QEMU
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
registry: ghcr.io
|
||||||
cache: true
|
username: ${{ vars.GHCR_USERNAME || github.repository_owner }}
|
||||||
|
password: ${{ secrets.GHCR_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Build x86 artifact
|
- name: Resolve service image ref
|
||||||
run: bash ./scripts/github-actions/build-artifact.sh dist
|
id: service_ref
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
image_repo="${SERVICE_REGISTRY}/${SERVICE_IMAGE_REPO_OWNER}/${SERVICE_IMAGE_NAME}"
|
||||||
|
image_tag="${GITHUB_SHA}"
|
||||||
|
image_ref="${image_repo}:${image_tag}"
|
||||||
|
|
||||||
- name: Smoke test built artifact
|
echo "image_repo=${image_repo}" >> "$GITHUB_OUTPUT"
|
||||||
run: ./dist/xworkmate-bridge --help >/dev/null
|
echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build and optionally push service image
|
||||||
|
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.service_ref.outputs.image_ref }}
|
||||||
|
|
||||||
|
- name: Write image ref artifact
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p dist
|
||||||
|
printf '%s\n' "${{ steps.service_ref.outputs.image_ref }}" > dist/service-image-ref.txt
|
||||||
|
|
||||||
- name: Record artifact metadata
|
- name: Record artifact metadata
|
||||||
id: artifact_meta
|
id: artifact_meta
|
||||||
run: echo "artifact_name=xworkmate-bridge-linux-amd64" >> "$GITHUB_OUTPUT"
|
run: echo "artifact_name=xworkmate-bridge-service-image-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload image ref artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.artifact_meta.outputs.artifact_name }}
|
name: ${{ steps.artifact_meta.outputs.artifact_name }}
|
||||||
path: dist/xworkmate-bridge
|
path: dist/service-image-ref.txt
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy
|
name: Deploy
|
||||||
@ -101,6 +136,8 @@ jobs:
|
|||||||
run_apply: ${{ steps.deploy_meta.outputs.run_apply }}
|
run_apply: ${{ steps.deploy_meta.outputs.run_apply }}
|
||||||
env:
|
env:
|
||||||
INTERNAL_SERVICE_TOKEN: ${{ github.event_name == 'workflow_dispatch' && inputs.internal_service_token || secrets.INTERNAL_SERVICE_TOKEN }}
|
INTERNAL_SERVICE_TOKEN: ${{ github.event_name == 'workflow_dispatch' && inputs.internal_service_token || secrets.INTERNAL_SERVICE_TOKEN }}
|
||||||
|
GHCR_USERNAME: ${{ vars.GHCR_USERNAME || github.repository_owner }}
|
||||||
|
GHCR_PASSWORD: ${{ secrets.GHCR_TOKEN || github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout service repository
|
- name: Checkout service repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@ -114,18 +151,11 @@ jobs:
|
|||||||
token: ${{ secrets.WORKSPACE_REPO_TOKEN || github.token }}
|
token: ${{ secrets.WORKSPACE_REPO_TOKEN || github.token }}
|
||||||
path: playbooks
|
path: playbooks
|
||||||
|
|
||||||
- name: Download build artifact
|
- name: Download build image ref artifact
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.build.outputs.artifact_name }}
|
name: ${{ needs.build.outputs.artifact_name }}
|
||||||
path: xworkmate-bridge/dist
|
path: xworkmate-bridge/dist/image-artifact
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
||||||
with:
|
|
||||||
go-version-file: xworkmate-bridge/go.mod
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: xworkmate-bridge/go.sum
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
@ -166,7 +196,9 @@ jobs:
|
|||||||
working-directory: xworkmate-bridge
|
working-directory: xworkmate-bridge
|
||||||
env:
|
env:
|
||||||
INTERNAL_SERVICE_TOKEN: ${{ env.INTERNAL_SERVICE_TOKEN }}
|
INTERNAL_SERVICE_TOKEN: ${{ env.INTERNAL_SERVICE_TOKEN }}
|
||||||
XWORKMATE_BRIDGE_ARTIFACT_PATH: ${{ github.workspace }}/xworkmate-bridge/dist/xworkmate-bridge
|
GHCR_USERNAME: ${{ env.GHCR_USERNAME }}
|
||||||
|
GHCR_PASSWORD: ${{ env.GHCR_PASSWORD }}
|
||||||
|
XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH: ${{ github.workspace }}/xworkmate-bridge/dist/image-artifact/service-image-ref.txt
|
||||||
run: bash ./scripts/github-actions/deploy.sh "${{ steps.deploy_meta.outputs.target_host }}" "${{ steps.deploy_meta.outputs.run_apply }}" ../playbooks
|
run: bash ./scripts/github-actions/deploy.sh "${{ steps.deploy_meta.outputs.target_host }}" "${{ steps.deploy_meta.outputs.run_apply }}" ../playbooks
|
||||||
|
|
||||||
publish_release:
|
publish_release:
|
||||||
@ -177,7 +209,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Download build artifact
|
- name: Download image ref artifact
|
||||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4
|
||||||
with:
|
with:
|
||||||
name: ${{ needs.build.outputs.artifact_name }}
|
name: ${{ needs.build.outputs.artifact_name }}
|
||||||
@ -198,7 +230,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
gh release create "${{ steps.release_meta.outputs.tag }}" \
|
gh release create "${{ steps.release_meta.outputs.tag }}" \
|
||||||
dist/xworkmate-bridge#xworkmate-bridge-linux-amd64 \
|
dist/service-image-ref.txt#xworkmate-bridge-service-image-ref.txt \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--target "${{ github.sha }}" \
|
--target "${{ github.sha }}" \
|
||||||
--title "${{ steps.release_meta.outputs.title }}" \
|
--title "${{ steps.release_meta.outputs.title }}" \
|
||||||
@ -206,7 +238,9 @@ jobs:
|
|||||||
|
|
||||||
validate:
|
validate:
|
||||||
name: Validate
|
name: Validate
|
||||||
needs: deploy
|
needs:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
if: ${{ needs.deploy.result == 'success' && needs.deploy.outputs.run_apply == 'true' }}
|
if: ${{ needs.deploy.result == 'success' && needs.deploy.outputs.run_apply == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
@ -221,4 +255,4 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Validate deployed endpoints
|
- name: Validate deployed endpoints
|
||||||
run: bash ./scripts/github-actions/validate-deploy.sh
|
run: bash ./scripts/github-actions/validate-deploy.sh "${{ needs.build.outputs.service_image_ref }}" "${BRIDGE_SERVER_URL}" "${OPENCLAW_URL}" "${CODEX_RPC_URL}" "${OPENCODE_RPC_URL}" "${GEMINI_RPC_URL}" "${INTERNAL_SERVICE_TOKEN}"
|
||||||
|
|||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Stage 1 - build the bridge binary
|
||||||
|
FROM golang:1.25.1 AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/xworkmate-bridge .
|
||||||
|
|
||||||
|
# Stage 2 - minimal runtime image
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /out/xworkmate-bridge /usr/local/bin/xworkmate-bridge
|
||||||
|
|
||||||
|
EXPOSE 8787
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/xworkmate-bridge", "serve", "--listen", "0.0.0.0:8787"]
|
||||||
54
internal/acp/runtime_version.go
Normal file
54
internal/acp/runtime_version.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package acp
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type imageVersionInfo struct {
|
||||||
|
ImageRef string `json:"image_ref"`
|
||||||
|
Tag string `json:"tag,omitempty"`
|
||||||
|
Commit string `json:"commit,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseImageVersionInfo(imageRef string) imageVersionInfo {
|
||||||
|
ref := strings.TrimSpace(imageRef)
|
||||||
|
info := imageVersionInfo{ImageRef: ref}
|
||||||
|
if ref == "" {
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx := strings.LastIndex(ref, "@"); idx >= 0 {
|
||||||
|
ref = ref[:idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := ref
|
||||||
|
if idx := strings.LastIndex(tag, ":"); idx >= 0 && idx > strings.LastIndex(tag, "/") {
|
||||||
|
tag = tag[idx+1:]
|
||||||
|
}
|
||||||
|
tag = strings.TrimSpace(tag)
|
||||||
|
info.Tag = tag
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isHexCommit(tag):
|
||||||
|
info.Commit = tag
|
||||||
|
info.Version = tag
|
||||||
|
default:
|
||||||
|
info.Version = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHexCommit(value string) bool {
|
||||||
|
if len(value) != 40 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range value {
|
||||||
|
switch {
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
case r >= 'a' && r <= 'f':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -74,23 +75,7 @@ func Serve(args []string) error {
|
|||||||
server := NewServer()
|
server := NewServer()
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: strings.TrimSpace(*listen),
|
Addr: strings.TrimSpace(*listen),
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
Handler: server.Handler(),
|
||||||
switch r.URL.Path {
|
|
||||||
case "/":
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
_, _ = w.Write([]byte("xworkmate-bridge is running"))
|
|
||||||
case "/bridge/bootstrap/health":
|
|
||||||
server.HandleBridgeBootstrapHealth(w, r)
|
|
||||||
case "/bridge/bootstrap/consume":
|
|
||||||
server.HandleBridgeBootstrapConsume(w, r)
|
|
||||||
case "/acp/rpc":
|
|
||||||
server.HandleRPC(w, r)
|
|
||||||
case "/acp":
|
|
||||||
server.HandleWebSocket(w, r)
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 5 * time.Minute,
|
WriteTimeout: 5 * time.Minute,
|
||||||
IdleTimeout: 2 * time.Minute,
|
IdleTimeout: 2 * time.Minute,
|
||||||
@ -115,6 +100,37 @@ func NewServer() *Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) Handler() http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/":
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, _ = w.Write([]byte("xworkmate-bridge is running"))
|
||||||
|
case "/api/ping":
|
||||||
|
info := parseImageVersionInfo(os.Getenv("IMAGE"))
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"status": "ok",
|
||||||
|
"image": info.ImageRef,
|
||||||
|
"tag": info.Tag,
|
||||||
|
"commit": info.Commit,
|
||||||
|
"version": info.Version,
|
||||||
|
})
|
||||||
|
case "/bridge/bootstrap/health":
|
||||||
|
s.HandleBridgeBootstrapHealth(w, r)
|
||||||
|
case "/bridge/bootstrap/consume":
|
||||||
|
s.HandleBridgeBootstrapConsume(w, r)
|
||||||
|
case "/acp/rpc":
|
||||||
|
s.HandleRPC(w, r)
|
||||||
|
case "/acp":
|
||||||
|
s.HandleWebSocket(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||||
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
||||||
if !s.originAllowed(origin) {
|
if !s.originAllowed(origin) {
|
||||||
|
|||||||
@ -8,6 +8,70 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestHTTPHandlerRootAndPingExposeRuntimeVersionInfo(t *testing.T) {
|
||||||
|
t.Setenv("IMAGE", "ghcr.io/x-evor/xworkmate-bridge:0123456789abcdef0123456789abcdef01234567")
|
||||||
|
|
||||||
|
server := NewServer()
|
||||||
|
handler := server.Handler()
|
||||||
|
|
||||||
|
rootRecorder := httptest.NewRecorder()
|
||||||
|
rootRequest := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
|
||||||
|
handler.ServeHTTP(rootRecorder, rootRequest)
|
||||||
|
|
||||||
|
if rootRecorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected root 200, got %d", rootRecorder.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rootRecorder.Body.String(), "xworkmate-bridge is running") {
|
||||||
|
t.Fatalf("expected root body to contain service banner, got %q", rootRecorder.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
pingRecorder := httptest.NewRecorder()
|
||||||
|
pingRequest := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/api/ping", nil)
|
||||||
|
handler.ServeHTTP(pingRecorder, pingRequest)
|
||||||
|
|
||||||
|
if pingRecorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected ping 200, got %d", pingRecorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(pingRecorder.Body.Bytes(), &payload); err != nil {
|
||||||
|
t.Fatalf("decode ping payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := payload["status"]; got != "ok" {
|
||||||
|
t.Fatalf("expected status ok, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := payload["image"]; got != "ghcr.io/x-evor/xworkmate-bridge:0123456789abcdef0123456789abcdef01234567" {
|
||||||
|
t.Fatalf("expected full image ref, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := payload["tag"]; got != "0123456789abcdef0123456789abcdef01234567" {
|
||||||
|
t.Fatalf("expected full image tag, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := payload["commit"]; got != "0123456789abcdef0123456789abcdef01234567" {
|
||||||
|
t.Fatalf("expected full image commit, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := payload["version"]; got != "0123456789abcdef0123456789abcdef01234567" {
|
||||||
|
t.Fatalf("expected full image version, got %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseImageVersionInfoHandlesTaggedImageRef(t *testing.T) {
|
||||||
|
info := parseImageVersionInfo("ghcr.io/x-evor/xworkmate-bridge:main-2026-04-12")
|
||||||
|
|
||||||
|
if info.ImageRef != "ghcr.io/x-evor/xworkmate-bridge:main-2026-04-12" {
|
||||||
|
t.Fatalf("expected full image ref, got %q", info.ImageRef)
|
||||||
|
}
|
||||||
|
if info.Tag != "main-2026-04-12" {
|
||||||
|
t.Fatalf("expected tag main-2026-04-12, got %q", info.Tag)
|
||||||
|
}
|
||||||
|
if info.Commit != "" {
|
||||||
|
t.Fatalf("expected empty commit for non-hex tag, got %q", info.Commit)
|
||||||
|
}
|
||||||
|
if info.Version != "main-2026-04-12" {
|
||||||
|
t.Fatalf("expected version main-2026-04-12, got %q", info.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleWebSocketRejectsUnknownOrigin(t *testing.T) {
|
func TestHandleWebSocketRejectsUnknownOrigin(t *testing.T) {
|
||||||
t.Setenv("ACP_ALLOWED_ORIGINS", "https://xworkmate.svc.plus")
|
t.Setenv("ACP_ALLOWED_ORIGINS", "https://xworkmate.svc.plus")
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,25 @@ set -euo pipefail
|
|||||||
TARGET_HOST="${1:?target host is required}"
|
TARGET_HOST="${1:?target host is required}"
|
||||||
RUN_APPLY="${2:?run_apply flag is required}"
|
RUN_APPLY="${2:?run_apply flag is required}"
|
||||||
PLAYBOOK_DIR="${3:-playbooks}"
|
PLAYBOOK_DIR="${3:-playbooks}"
|
||||||
XWORKMATE_BRIDGE_ARTIFACT_PATH="${XWORKMATE_BRIDGE_ARTIFACT_PATH:?artifact path is required}"
|
XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH="${XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH:?image artifact path is required}"
|
||||||
|
|
||||||
|
if [[ ! -f "${XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH}" ]]; then
|
||||||
|
echo "image artifact not found at ${XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVICE_COMPOSE_IMAGE="$(tr -d '\n' < "${XWORKMATE_BRIDGE_IMAGE_ARTIFACT_PATH}" | xargs)"
|
||||||
|
if [[ -z "${SERVICE_COMPOSE_IMAGE}" ]]; then
|
||||||
|
echo "service compose image is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
image_no_digest="${SERVICE_COMPOSE_IMAGE%@*}"
|
||||||
|
image_tag="${image_no_digest##*:}"
|
||||||
|
if [[ -z "${image_tag}" || "${image_no_digest}" == "${image_tag}" ]]; then
|
||||||
|
echo "invalid service image ref: ${SERVICE_COMPOSE_IMAGE}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cd "${PLAYBOOK_DIR}"
|
cd "${PLAYBOOK_DIR}"
|
||||||
|
|
||||||
@ -13,7 +31,6 @@ args=(
|
|||||||
-i inventory.ini
|
-i inventory.ini
|
||||||
deploy_xworkmate_bridge_vhosts.yml
|
deploy_xworkmate_bridge_vhosts.yml
|
||||||
-l "${TARGET_HOST}"
|
-l "${TARGET_HOST}"
|
||||||
-e "xworkmate_bridge_artifact_path=${XWORKMATE_BRIDGE_ARTIFACT_PATH}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "${RUN_APPLY}" != "true" ]]; then
|
if [[ "${RUN_APPLY}" != "true" ]]; then
|
||||||
@ -21,4 +38,7 @@ if [[ "${RUN_APPLY}" != "true" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
ANSIBLE_CONFIG="${PWD}/ansible.cfg" \
|
ANSIBLE_CONFIG="${PWD}/ansible.cfg" \
|
||||||
|
SERVICE_COMPOSE_IMAGE="${SERVICE_COMPOSE_IMAGE}" \
|
||||||
|
GHCR_USERNAME="${GHCR_USERNAME:-}" \
|
||||||
|
GHCR_PASSWORD="${GHCR_PASSWORD:-}" \
|
||||||
"${args[@]}"
|
"${args[@]}"
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE_REF="${1:?image_ref is required}"
|
||||||
|
|
||||||
normalize_url() {
|
normalize_url() {
|
||||||
local value="$1"
|
local value="$1"
|
||||||
if [[ "${value}" =~ ^https:([^/].*)$ ]]; then
|
if [[ "${value}" =~ ^https:([^/].*)$ ]]; then
|
||||||
@ -27,12 +29,31 @@ websocket_probe_url() {
|
|||||||
printf '%s\n' "${value}"
|
printf '%s\n' "${value}"
|
||||||
}
|
}
|
||||||
|
|
||||||
BASE_URL="$(normalize_url "${BRIDGE_SERVER_URL:-${1:-https://xworkmate-bridge.svc.plus}}")"
|
image_ref="$(printf '%s' "${IMAGE_REF}" | tr -d '\n' | xargs)"
|
||||||
OPENCLAW_HTTP_PROBE_URL="$(websocket_probe_url "${OPENCLAW_URL:-${2:-wss://openclaw.svc.plus}}")"
|
if [[ -z "${image_ref}" ]]; then
|
||||||
CODEX_RPC_URL="$(normalize_url "${CODEX_RPC_URL:-${3:-https://acp-server.svc.plus/codex/acp/rpc}}")"
|
echo "image_ref is required" >&2
|
||||||
OPENCODE_RPC_URL="$(normalize_url "${OPENCODE_RPC_URL:-${4:-https://acp-server.svc.plus/opencode/acp/rpc}}")"
|
exit 1
|
||||||
GEMINI_RPC_URL="$(normalize_url "${GEMINI_RPC_URL:-${5:-https://acp-server.svc.plus/gemini/acp/rpc}}")"
|
fi
|
||||||
AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-${INTERNAL_SERVICE_TOKEN:-${6:-}}}"
|
|
||||||
|
image_no_digest="${image_ref%@*}"
|
||||||
|
tag="${image_no_digest##*:}"
|
||||||
|
if [[ "${image_no_digest}" == "${tag}" ]]; then
|
||||||
|
tag=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
commit=""
|
||||||
|
version="${tag}"
|
||||||
|
|
||||||
|
if [[ "${tag}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
commit="${tag}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE_URL="$(normalize_url "${BRIDGE_SERVER_URL:-${2:-https://xworkmate-bridge.svc.plus}}")"
|
||||||
|
OPENCLAW_HTTP_PROBE_URL="$(websocket_probe_url "${OPENCLAW_URL:-${3:-wss://openclaw.svc.plus}}")"
|
||||||
|
CODEX_RPC_URL="$(normalize_url "${CODEX_RPC_URL:-${4:-https://acp-server.svc.plus/codex/acp/rpc}}")"
|
||||||
|
OPENCODE_RPC_URL="$(normalize_url "${OPENCODE_RPC_URL:-${5:-https://acp-server.svc.plus/opencode/acp/rpc}}")"
|
||||||
|
GEMINI_RPC_URL="$(normalize_url "${GEMINI_RPC_URL:-${6:-https://acp-server.svc.plus/gemini/acp/rpc}}")"
|
||||||
|
AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-${INTERNAL_SERVICE_TOKEN:-${7:-}}}"
|
||||||
|
|
||||||
curl_common=(
|
curl_common=(
|
||||||
--silent
|
--silent
|
||||||
@ -93,6 +114,36 @@ probe_safe_http_endpoint() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ping_json="$(
|
||||||
|
curl \
|
||||||
|
"${curl_common[@]}" \
|
||||||
|
"${BASE_URL}/api/ping"
|
||||||
|
)"
|
||||||
|
|
||||||
|
PING_JSON="${ping_json}" python3 - "${image_ref}" "${tag}" "${commit}" "${version}" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
image_ref, tag, commit, version = sys.argv[1:5]
|
||||||
|
payload = json.loads(os.environ["PING_JSON"])
|
||||||
|
|
||||||
|
if payload.get("status") != "ok":
|
||||||
|
raise SystemExit("ping status not ok")
|
||||||
|
|
||||||
|
if payload.get("image") != image_ref:
|
||||||
|
raise SystemExit(f"expected image {image_ref!r}, got {payload.get('image')!r}")
|
||||||
|
|
||||||
|
if tag and payload.get("tag") != tag:
|
||||||
|
raise SystemExit(f"expected tag {tag!r}, got {payload.get('tag')!r}")
|
||||||
|
|
||||||
|
if commit and payload.get("commit") != commit:
|
||||||
|
raise SystemExit(f"expected commit {commit!r}, got {payload.get('commit')!r}")
|
||||||
|
|
||||||
|
if version and payload.get("version") != version:
|
||||||
|
raise SystemExit(f"expected version {version!r}, got {payload.get('version')!r}")
|
||||||
|
PY
|
||||||
|
|
||||||
bridge_root="$(curl "${curl_common[@]}" "${auth_headers[@]}" "${BASE_URL}/")"
|
bridge_root="$(curl "${curl_common[@]}" "${auth_headers[@]}" "${BASE_URL}/")"
|
||||||
grep -qi 'xworkmate-bridge' <<<"${bridge_root}"
|
grep -qi 'xworkmate-bridge' <<<"${bridge_root}"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user