From 67696beab8e0b4bba7233ae17ba131451c1d6273 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 10 Apr 2026 16:17:32 +0800 Subject: [PATCH] Add GitHub Actions pipeline for bridge deploy --- .github/scripts/normalize-private-key.py | 47 +++++++ .github/workflows/pipeline.yml | 163 ++++++++++++++++++++++ README.md | 31 ++++ internal/acp/bootstrap.go | 4 +- internal/acp/execution.go | 18 ++- internal/acp/providers_sync_test.go | 12 +- internal/acp/routing.go | 4 - internal/acp/routing_test.go | 4 +- internal/acp/server.go | 4 +- internal/gatewayruntime/runtime_test.go | 4 +- internal/geminiadapter/server.go | 8 +- internal/geminiadapter/server_test.go | 4 +- internal/handler/auth_handler.go | 3 +- internal/mounts/config.go | 22 ++- internal/shared/tools.go | 16 ++- internal/shared/vault.go | 4 +- main_tools.go | 4 - scripts/github-actions/build-artifact.sh | 7 + scripts/github-actions/deploy.sh | 21 +++ scripts/github-actions/prep.sh | 6 + scripts/github-actions/prepare-ssh.sh | 23 +++ scripts/github-actions/validate-deploy.sh | 31 ++++ 22 files changed, 394 insertions(+), 46 deletions(-) create mode 100644 .github/scripts/normalize-private-key.py create mode 100644 .github/workflows/pipeline.yml create mode 100644 scripts/github-actions/build-artifact.sh create mode 100644 scripts/github-actions/deploy.sh create mode 100644 scripts/github-actions/prep.sh create mode 100644 scripts/github-actions/prepare-ssh.sh create mode 100644 scripts/github-actions/validate-deploy.sh diff --git a/.github/scripts/normalize-private-key.py b/.github/scripts/normalize-private-key.py new file mode 100644 index 0000000..baea94f --- /dev/null +++ b/.github/scripts/normalize-private-key.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import base64 +import os +import sys + + +def strip_outer_quotes(value: str) -> str: + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + return value[1:-1].strip() + return value + + +def raw_payload() -> str: + return strip_outer_quotes(os.environ["SINGLE_NODE_VPS_SSH_PRIVATE_KEY"].replace("\r", "").strip()) + + +def normalize() -> str: + raw = raw_payload() + candidates = [raw] + + if "\\n" in raw: + candidates.append(strip_outer_quotes(raw.replace("\\n", "\n").strip())) + + try: + decoded = base64.b64decode(raw, validate=True).decode("utf-8").replace("\r", "").strip() + except Exception: + decoded = "" + + if decoded: + candidates.append(strip_outer_quotes(decoded)) + + for candidate in candidates: + if "BEGIN " in candidate and "PRIVATE KEY" in candidate: + return candidate.rstrip("\n") + "\n" + + return raw.rstrip("\n") + "\n" + + +def main() -> None: + if len(sys.argv) != 2 or sys.argv[1] != "normalize": + raise SystemExit("usage: normalize-private-key.py normalize") + + sys.stdout.write(normalize()) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..1644e23 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,163 @@ +name: Pipeline + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + inputs: + target_host: + description: "Ansible inventory host or alias" + required: false + default: "jp-xhttp-contabo.svc.plus" + type: string + run_apply: + description: "Apply deployment (false = dry-run)" + required: true + default: true + type: boolean + internal_service_token: + description: "Optional ACP auth token for deploy" + required: false + default: "" + type: string + +permissions: + contents: read + +concurrency: + group: pipeline-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + DEFAULT_TARGET_HOST: jp-xhttp-contabo.svc.plus + +jobs: + prep: + name: Prep + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Set up golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.64.8 + + - name: Run Go static checks + run: bash ./scripts/github-actions/prep.sh + + build: + name: Build + needs: prep + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build x86 artifact + run: bash ./scripts/github-actions/build-artifact.sh dist + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: xworkmate-bridge-linux-amd64 + path: dist/xworkmate-bridge + + deploy: + name: Deploy + needs: build + if: ${{ github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + outputs: + run_apply: ${{ steps.deploy_meta.outputs.run_apply }} + env: + INTERNAL_SERVICE_TOKEN: ${{ github.event_name == 'workflow_dispatch' && inputs.internal_service_token || secrets.INTERNAL_SERVICE_TOKEN }} + steps: + - name: Checkout service repository + uses: actions/checkout@v4 + with: + path: xworkmate-bridge + + - name: Checkout playbooks repository + uses: actions/checkout@v4 + with: + repository: x-evor/playbooks + token: ${{ secrets.WORKSPACE_REPO_TOKEN || github.token }} + path: playbooks + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: xworkmate-bridge/go.mod + cache: true + cache-dependency-path: xworkmate-bridge/go.sum + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Ansible runtime + run: | + python -m pip install --upgrade pip + python -m pip install "ansible-core==2.18.3" + + - name: Resolve deployment settings + id: deploy_meta + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + target_host="${{ inputs.target_host }}" + run_apply="${{ inputs.run_apply }}" + else + target_host="${DEFAULT_TARGET_HOST}" + run_apply="true" + fi + + if [[ -z "${target_host}" ]]; then + target_host="${DEFAULT_TARGET_HOST}" + fi + + echo "target_host=${target_host}" >> "$GITHUB_OUTPUT" + echo "run_apply=${run_apply}" >> "$GITHUB_OUTPUT" + + - name: Prepare runner SSH access + working-directory: xworkmate-bridge + env: + SINGLE_NODE_VPS_SSH_PRIVATE_KEY: ${{ secrets.SINGLE_NODE_VPS_SSH_PRIVATE_KEY }} + SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }} + run: bash ./scripts/github-actions/prepare-ssh.sh "${{ steps.deploy_meta.outputs.target_host }}" "${SSH_KNOWN_HOSTS}" + + - name: Run Ansible deploy playbook + working-directory: xworkmate-bridge + run: bash ./scripts/github-actions/deploy.sh "${{ steps.deploy_meta.outputs.target_host }}" "${{ steps.deploy_meta.outputs.run_apply }}" ../playbooks + + validate: + name: Validate + needs: deploy + if: ${{ needs.deploy.result == 'success' && needs.deploy.outputs.run_apply == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate deployed endpoints + run: bash ./scripts/github-actions/validate-deploy.sh diff --git a/README.md b/README.md index 498e877..b64207a 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,37 @@ make build ./build/bin/xworkmate-go-core serve --listen 127.0.0.1:8787 ``` +## GitHub Actions + +This repository includes one GitHub Actions pipeline with four stages: + +- `prep`: Go static checks +- `build`: build the `linux/amd64` artifact for the x86 target host and upload it +- `deploy`: run Ansible CD with `x-evor/playbooks` +- `validate`: verify the public endpoints after deployment + +### Deploy stage + +The deploy stage checks out: + +- this service repository into `xworkmate-bridge/` +- the `x-evor/playbooks` repository into `playbooks/` + +Then it runs `playbooks/deploy_xworkmate_bridge_vhosts.yml`, which builds the service for `linux/amd64` and deploys it to the target host with Ansible. + +Required GitHub secrets: + +- `SINGLE_NODE_VPS_SSH_PRIVATE_KEY`: private key used by the Actions runner to SSH into the target host +- `WORKSPACE_REPO_TOKEN`: token with access to checkout `x-evor/playbooks` + +Optional GitHub secrets: + +- `SSH_KNOWN_HOSTS`: pre-seeded known_hosts content for stricter host verification + +Optional workflow input: + +- `internal_service_token`: manual dispatch input that is forwarded to Ansible as `INTERNAL_SERVICE_TOKEN` + ## Environment - `ACP_LISTEN_ADDR`: listen address for `serve` mode, default `127.0.0.1:8787` diff --git a/internal/acp/bootstrap.go b/internal/acp/bootstrap.go index e48be41..e0037aa 100644 --- a/internal/acp/bootstrap.go +++ b/internal/acp/bootstrap.go @@ -124,7 +124,9 @@ func consumeBootstrapFromAccounts(req bridgeBootstrapConsumeRequest) (accountsBr if err != nil { return accountsBridgeBootstrapConsumeResponse{}, http.StatusBadGateway, fmt.Errorf("failed to contact accounts service") } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return accountsBridgeBootstrapConsumeResponse{}, resp.StatusCode, fmt.Errorf("accounts bootstrap consume failed") } diff --git a/internal/acp/execution.go b/internal/acp/execution.go index dba4c86..3580c72 100644 --- a/internal/acp/execution.go +++ b/internal/acp/execution.go @@ -301,7 +301,9 @@ func requestExternalACPHTTP( if err != nil { return nil, err } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() var decoded map[string]any if err := json.NewDecoder(response.Body).Decode(&decoded); err != nil { return nil, err @@ -594,7 +596,9 @@ func requestExternalACPWebSocket( if err != nil { return nil, err } - defer conn.Close() + defer func() { + _ = conn.Close() + }() requestID := fmt.Sprintf("req-%d", time.Now().UnixNano()) if err := conn.WriteJSON(map[string]any{ @@ -679,9 +683,10 @@ func (u *urlSpec) basePath() string { func (u *urlSpec) httpRPCEndpoint() string { scheme := u.Scheme - if scheme == "ws" { + switch scheme { + case "ws": scheme = "http" - } else if scheme == "wss" { + case "wss": scheme = "https" } basePath := u.basePath() @@ -695,9 +700,10 @@ func (u *urlSpec) httpRPCEndpoint() string { func (u *urlSpec) webSocketEndpoint() string { scheme := u.Scheme - if scheme == "http" { + switch scheme { + case "http": scheme = "ws" - } else if scheme == "https" { + case "https": scheme = "wss" } basePath := u.basePath() diff --git a/internal/acp/providers_sync_test.go b/internal/acp/providers_sync_test.go index 7fa9849..a3b7f61 100644 --- a/internal/acp/providers_sync_test.go +++ b/internal/acp/providers_sync_test.go @@ -96,7 +96,9 @@ func TestExecuteSessionTaskUsesSyncedExternalProvider(t *testing.T) { http.NotFound(w, r) return } - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() var request map[string]any if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) @@ -188,7 +190,9 @@ func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteM http.NotFound(w, r) return } - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() var request map[string]any if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) @@ -274,7 +278,9 @@ func TestRunSingleAgentUsesFrozenExternalProviderParams(t *testing.T) { http.NotFound(w, r) return } - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() var request map[string]any if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) diff --git a/internal/acp/routing.go b/internal/acp/routing.go index 862957c..0943507 100644 --- a/internal/acp/routing.go +++ b/internal/acp/routing.go @@ -15,10 +15,6 @@ func handleRoutingResolve(params map[string]any) map[string]any { return mergeRoutingResponse(map[string]any{"ok": true}, result) } -func resolveRoutingMetadata(params map[string]any) (router.Result, bool) { - return resolveRoutingMetadataWithProviders(params, nil) -} - func resolveRoutingMetadataWithProviders( params map[string]any, availableProviders []string, diff --git a/internal/acp/routing_test.go b/internal/acp/routing_test.go index 46a29d1..25d73a1 100644 --- a/internal/acp/routing_test.go +++ b/internal/acp/routing_test.go @@ -23,7 +23,9 @@ func newExternalSingleAgentProvider( http.NotFound(w, r) return } - defer r.Body.Close() + defer func() { + _ = r.Body.Close() + }() var request map[string]any if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatalf("decode request: %v", err) diff --git a/internal/acp/server.go b/internal/acp/server.go index b8ab5a1..215b4b2 100644 --- a/internal/acp/server.go +++ b/internal/acp/server.go @@ -142,7 +142,9 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) { if err != nil { return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() var writeMu sync.Mutex notify := func(message map[string]any) { diff --git a/internal/gatewayruntime/runtime_test.go b/internal/gatewayruntime/runtime_test.go index 95845cb..e826c65 100644 --- a/internal/gatewayruntime/runtime_test.go +++ b/internal/gatewayruntime/runtime_test.go @@ -198,7 +198,9 @@ func newFakeGatewayServer(t *testing.T) *fakeGatewayServer { if err != nil { return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() _ = conn.WriteJSON(map[string]any{ "type": "event", "event": "connect.challenge", diff --git a/internal/geminiadapter/server.go b/internal/geminiadapter/server.go index b4b1373..9b33a66 100644 --- a/internal/geminiadapter/server.go +++ b/internal/geminiadapter/server.go @@ -77,7 +77,9 @@ func Serve(args []string) error { nil, shared.IntArg(shared.EnvOrDefault("GEMINI_ADAPTER_PROTOCOL_VERSION", "1"), 1), ) - defer client.Close() + defer func() { + _ = client.Close() + }() server := NewServer(client) httpServer := &http.Server{ @@ -140,7 +142,9 @@ func (s *Server) HandleWebSocket(w http.ResponseWriter, r *http.Request) { if err != nil { return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() var writeMu sync.Mutex notify := func(message map[string]any) { diff --git a/internal/geminiadapter/server_test.go b/internal/geminiadapter/server_test.go index 2ddc54f..aac54fe 100644 --- a/internal/geminiadapter/server_test.go +++ b/internal/geminiadapter/server_test.go @@ -227,7 +227,9 @@ func TestHandleWebSocketCapabilities(t *testing.T) { if err != nil { t.Fatalf("dial websocket: %v", err) } - defer conn.Close() + defer func() { + _ = conn.Close() + }() if err := conn.WriteJSON(shared.RPCRequest{ JSONRPC: "2.0", diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index c6269a7..9a49722 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,6 +1,7 @@ package handler import ( + "context" "encoding/json" "net/http" @@ -45,5 +46,5 @@ type authServiceAdapter struct { } func (a authServiceAdapter) Authenticate(username, password string) error { - return a.service.Authenticate(nil, username, password) + return a.service.Authenticate(context.TODO(), username, password) } diff --git a/internal/mounts/config.go b/internal/mounts/config.go index ff7d34d..084cfac 100644 --- a/internal/mounts/config.go +++ b/internal/mounts/config.go @@ -85,14 +85,12 @@ func buildCodexManagedMCPBlock(servers []ManagedMCPServer) string { var buffer strings.Builder buffer.WriteString(codexManagedMCPBlockStart) buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n") - buffer.WriteString( - fmt.Sprintf("# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)), - ) + _, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)) for _, server := range servers { - buffer.WriteString(fmt.Sprintf("[mcp_servers.%s]\n", server.ID)) - buffer.WriteString(fmt.Sprintf("command = %q\n", server.Command)) + _, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID) + _, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command) if len(server.Args) > 0 { - buffer.WriteString(fmt.Sprintf("args = %s\n", formatTOMLArray(server.Args))) + _, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args)) } buffer.WriteString("\n") } @@ -104,18 +102,16 @@ func buildOpencodeManagedMCPBlock(servers []ManagedMCPServer) string { var buffer strings.Builder buffer.WriteString(opencodeManagedMCPBlockStart) buffer.WriteString("\n# Generated by XWorkmate - Managed MCP Server Configuration\n") - buffer.WriteString( - fmt.Sprintf("# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)), - ) + _, _ = fmt.Fprintf(&buffer, "# Last updated: %s\n\n", time.Now().Format(time.RFC3339Nano)) for _, server := range servers { - buffer.WriteString(fmt.Sprintf("[mcp_servers.%s]\n", server.ID)) + _, _ = fmt.Fprintf(&buffer, "[mcp_servers.%s]\n", server.ID) if strings.TrimSpace(server.URL) != "" { - buffer.WriteString(fmt.Sprintf("url = %q\n", strings.TrimSpace(server.URL))) + _, _ = fmt.Fprintf(&buffer, "url = %q\n", strings.TrimSpace(server.URL)) } else { buffer.WriteString("type = \"stdio\"\n") - buffer.WriteString(fmt.Sprintf("command = %q\n", server.Command)) + _, _ = fmt.Fprintf(&buffer, "command = %q\n", server.Command) if len(server.Args) > 0 { - buffer.WriteString(fmt.Sprintf("args = %s\n", formatTOMLArray(server.Args))) + _, _ = fmt.Fprintf(&buffer, "args = %s\n", formatTOMLArray(server.Args)) } } buffer.WriteString("\n") diff --git a/internal/shared/tools.go b/internal/shared/tools.go index 5596ebb..052dfed 100644 --- a/internal/shared/tools.go +++ b/internal/shared/tools.go @@ -210,7 +210,7 @@ func ComposeHistoryPrompt(history []string) string { } var builder strings.Builder for index, turn := range history { - builder.WriteString(fmt.Sprintf("## User Turn %d\n", index+1)) + _, _ = fmt.Fprintf(&builder, "## User Turn %d\n", index+1) builder.WriteString(turn) builder.WriteString("\n\n") } @@ -248,7 +248,9 @@ func CallOpenAICompatibleCtx( if err != nil { return "", err } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() responseBody, err := io.ReadAll(response.Body) if err != nil { return "", err @@ -356,7 +358,7 @@ func RunClaudeReview( claudeBin := strings.TrimSpace(EnvOrDefault("CLAUDE_BIN", "claude")) resolved, err := exec.LookPath(claudeBin) if err != nil { - return "", fmt.Errorf("Claude CLI not found: %s", claudeBin) + return "", fmt.Errorf("claude CLI not found: %s", claudeBin) } args := []string{ @@ -389,13 +391,13 @@ func RunClaudeReview( if err := cmd.Run(); err != nil { if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return "", fmt.Errorf("Claude review timed out after %s", timeout) + return "", fmt.Errorf("claude review timed out after %s", timeout) } message := strings.TrimSpace(stderr.String()) if message == "" { message = err.Error() } - return "", fmt.Errorf("Claude review failed: %s", message) + return "", fmt.Errorf("claude review failed: %s", message) } payload, err := ParseClaudeJSON(stdout.String()) @@ -411,7 +413,7 @@ func RunClaudeReview( } response := strings.TrimSpace(fmt.Sprint(payload["result"])) if response == "" || response == "" { - return "", errors.New("Claude review returned empty output") + return "", errors.New("claude review returned empty output") } return response, nil } @@ -428,5 +430,5 @@ func ParseClaudeJSON(raw string) (map[string]any, error) { return payload, nil } } - return nil, errors.New("Claude CLI did not return JSON output") + return nil, errors.New("claude CLI did not return JSON output") } diff --git a/internal/shared/vault.go b/internal/shared/vault.go index d1482f8..6fbd3fd 100644 --- a/internal/shared/vault.go +++ b/internal/shared/vault.go @@ -208,7 +208,9 @@ func doVaultRequest( if err != nil { return nil, err } - defer response.Body.Close() + defer func() { + _ = response.Body.Close() + }() bodyBytes, err := io.ReadAll(response.Body) if err != nil { return nil, err diff --git a/main_tools.go b/main_tools.go index 5e76468..8936f52 100644 --- a/main_tools.go +++ b/main_tools.go @@ -23,10 +23,6 @@ func handleChatTool(arguments map[string]any) (string, error) { return shared.HandleChatTool(arguments) } -func handleVaultKVTool(arguments map[string]any) (string, error) { - return shared.HandleVaultKVTool(arguments) -} - func runClaudeReview( prompt, model, diff --git a/scripts/github-actions/build-artifact.sh b/scripts/github-actions/build-artifact.sh new file mode 100644 index 0000000..37389fa --- /dev/null +++ b/scripts/github-actions/build-artifact.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARTIFACT_DIR="${1:-dist}" +mkdir -p "${ARTIFACT_DIR}" + +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o "${ARTIFACT_DIR}/xworkmate-bridge" . diff --git a/scripts/github-actions/deploy.sh b/scripts/github-actions/deploy.sh new file mode 100644 index 0000000..6261ad0 --- /dev/null +++ b/scripts/github-actions/deploy.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET_HOST="${1:?target host is required}" +RUN_APPLY="${2:?run_apply flag is required}" +PLAYBOOK_DIR="${3:-playbooks}" + +cd "${PLAYBOOK_DIR}" + +args=( + ansible-playbook + -i inventory.ini + deploy_xworkmate_bridge_vhosts.yml + -l "${TARGET_HOST}" +) + +if [[ "${RUN_APPLY}" != "true" ]]; then + args+=(-C) +fi + +"${args[@]}" diff --git a/scripts/github-actions/prep.sh b/scripts/github-actions/prep.sh new file mode 100644 index 0000000..1264dc0 --- /dev/null +++ b/scripts/github-actions/prep.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +go mod download +go mod verify +golangci-lint run ./... diff --git a/scripts/github-actions/prepare-ssh.sh b/scripts/github-actions/prepare-ssh.sh new file mode 100644 index 0000000..d48c9ec --- /dev/null +++ b/scripts/github-actions/prepare-ssh.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +TARGET_HOST="${1:?target host is required}" +SSH_KNOWN_HOSTS_PAYLOAD="${2:-}" + +test -n "${SINGLE_NODE_VPS_SSH_PRIVATE_KEY:-}" + +mkdir -p "${HOME}/.ssh" +chmod 700 "${HOME}/.ssh" + +python3 .github/scripts/normalize-private-key.py normalize > "${HOME}/.ssh/id_rsa" +chmod 600 "${HOME}/.ssh/id_rsa" +ssh-keygen -y -f "${HOME}/.ssh/id_rsa" >/dev/null + +touch "${HOME}/.ssh/known_hosts" +chmod 600 "${HOME}/.ssh/known_hosts" + +if [[ -n "${SSH_KNOWN_HOSTS_PAYLOAD}" ]]; then + printf '%s\n' "${SSH_KNOWN_HOSTS_PAYLOAD}" >> "${HOME}/.ssh/known_hosts" +fi + +ssh-keyscan -H "${TARGET_HOST}" >> "${HOME}/.ssh/known_hosts" 2>/dev/null || true diff --git a/scripts/github-actions/validate-deploy.sh b/scripts/github-actions/validate-deploy.sh new file mode 100644 index 0000000..d4c4319 --- /dev/null +++ b/scripts/github-actions/validate-deploy.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${1:-https://xworkmate-bridge.svc.plus}" +INGRESS_URL="${2:-https://acp-server.svc.plus}" + +bridge_root="$(curl -fsS "${BASE_URL}/")" +test "${bridge_root}" = "xworkmate-bridge is running" + +codex_root="$(curl -fsS "${INGRESS_URL}/codex")" +test "${codex_root}" = "xworkmate-bridge is running" + +codex_rpc="$( + curl -sS "${INGRESS_URL}/codex/acp/rpc" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":"cap-1","method":"acp.capabilities"}' +)" +opencode_rpc="$( + curl -sS "${INGRESS_URL}/opencode/acp/rpc" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":"cap-1","method":"acp.capabilities"}' +)" +gemini_rpc="$( + curl -sS "${INGRESS_URL}/gemini/acp/rpc" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":"cap-1","method":"acp.capabilities"}' +)" + +grep -q '"missing bearer authorization"' <<<"${codex_rpc}" +grep -q '"missing bearer authorization"' <<<"${opencode_rpc}" +grep -q '"providers"' <<<"${gemini_rpc}"