Add GitHub Actions pipeline for bridge deploy
This commit is contained in:
parent
cb3be97ac6
commit
67696beab8
47
.github/scripts/normalize-private-key.py
vendored
Normal file
47
.github/scripts/normalize-private-key.py
vendored
Normal file
@ -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()
|
||||
163
.github/workflows/pipeline.yml
vendored
Normal file
163
.github/workflows/pipeline.yml
vendored
Normal file
@ -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
|
||||
31
README.md
31
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`
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 == "<nil>" {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
7
scripts/github-actions/build-artifact.sh
Normal file
7
scripts/github-actions/build-artifact.sh
Normal file
@ -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" .
|
||||
21
scripts/github-actions/deploy.sh
Normal file
21
scripts/github-actions/deploy.sh
Normal file
@ -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[@]}"
|
||||
6
scripts/github-actions/prep.sh
Normal file
6
scripts/github-actions/prep.sh
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
go mod download
|
||||
go mod verify
|
||||
golangci-lint run ./...
|
||||
23
scripts/github-actions/prepare-ssh.sh
Normal file
23
scripts/github-actions/prepare-ssh.sh
Normal file
@ -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
|
||||
31
scripts/github-actions/validate-deploy.sh
Normal file
31
scripts/github-actions/validate-deploy.sh
Normal file
@ -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}"
|
||||
Loading…
Reference in New Issue
Block a user