Skip to content

Commit 039dfe7

Browse files
feat(ci): add release pipeline
1 parent 8cb6f66 commit 039dfe7

9 files changed

Lines changed: 239 additions & 35 deletions

File tree

.github/workflows/release.yaml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
workflow_dispatch: {}
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: false
12+
13+
permissions:
14+
contents: write
15+
16+
env:
17+
BINARY_NAME: mcp-runtime
18+
19+
jobs:
20+
build:
21+
name: Build ${{ matrix.os }}-${{ matrix.arch }}
22+
runs-on: ubuntu-24.04
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
include:
27+
- os: linux
28+
arch: amd64
29+
- os: linux
30+
arch: arm64
31+
- os: darwin
32+
arch: amd64
33+
- os: darwin
34+
arch: arm64
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
38+
39+
- name: Set up Go
40+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
41+
with:
42+
go-version-file: go.mod
43+
cache: true
44+
45+
- name: Set release metadata
46+
shell: bash
47+
run: |
48+
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
49+
version="${GITHUB_REF_NAME}"
50+
else
51+
version="dev"
52+
fi
53+
54+
{
55+
echo "VERSION=${version}"
56+
echo "COMMIT=${GITHUB_SHA}"
57+
echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
58+
} >> "${GITHUB_ENV}"
59+
60+
- name: Build release archive
61+
shell: bash
62+
env:
63+
CGO_ENABLED: 0
64+
GOOS: ${{ matrix.os }}
65+
GOARCH: ${{ matrix.arch }}
66+
run: |
67+
set -euo pipefail
68+
69+
package_dir="dist/${BINARY_NAME}-${GOOS}-${GOARCH}"
70+
archive="dist/${BINARY_NAME}-${GOOS}-${GOARCH}.tar.gz"
71+
72+
mkdir -p "${package_dir}"
73+
74+
go build \
75+
-trimpath \
76+
-buildvcs=false \
77+
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_DATE}" \
78+
-o "${package_dir}/${BINARY_NAME}" \
79+
./cmd/mcp-runtime
80+
81+
cp README.md LICENSE "${package_dir}/"
82+
tar -C "${package_dir}" -czf "${archive}" .
83+
sha256sum "${archive}" > "${archive}.sha256"
84+
85+
- name: Upload build artifact
86+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
87+
with:
88+
name: mcp-runtime-${{ matrix.os }}-${{ matrix.arch }}
89+
path: |
90+
dist/*.tar.gz
91+
dist/*.tar.gz.sha256
92+
if-no-files-found: error
93+
94+
publish:
95+
name: Publish GitHub Release
96+
runs-on: ubuntu-24.04
97+
needs: [build]
98+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
99+
steps:
100+
- name: Download build artifacts
101+
uses: actions/download-artifact@v4
102+
with:
103+
path: dist
104+
merge-multiple: true
105+
106+
- name: List release assets
107+
shell: bash
108+
run: |
109+
find dist -maxdepth 1 -type f | sort
110+
111+
- name: Publish release
112+
uses: softprops/action-gh-release@v2
113+
with:
114+
tag_name: ${{ github.ref_name }}
115+
name: MCP Runtime ${{ github.ref_name }}
116+
generate_release_notes: true
117+
prerelease: ${{ contains(github.ref_name, '-') }}
118+
files: |
119+
dist/*.tar.gz
120+
dist/*.tar.gz.sha256

hack/multitenancytest.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ MCP_URL="${MCP_URL%/}"
2222
MCP_HOST="${MCP_URL#https://}"
2323
MCP_HOST="${MCP_HOST#http://}"
2424
MCP_HOST="${MCP_HOST%%/*}"
25-
KUBECONFIG="${KUBECONFIG:-/private/tmp/mcpruntime-k3s.yaml}"
26-
CREDS="${CREDS:-$HOME/Library/Application Support/mcp-runtime/credentials.json}"
27-
WORK_DIR="${WORK_DIR:-/private/tmp/mcp-runtime-multitenancy}"
25+
MCP_RUNTIME_CONFIG_DIR="${MCP_RUNTIME_CONFIG_DIR:-$HOME/.mcpruntime}"
26+
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config}"
27+
CREDS="${MCP_RUNTIME_CONFIG_DIR}/config.json"
28+
TMP_ROOT="${TMPDIR:-/tmp}"
29+
TMP_ROOT="${TMP_ROOT%/}"
30+
WORK_DIR="${WORK_DIR:-$TMP_ROOT/mcp-runtime-multitenancy}"
2831
TAG="${TAG:-v0.1.0}"
2932
ADAPTER_LISTEN="${ADAPTER_LISTEN:-127.0.0.1:8299}"
3033

@@ -51,6 +54,7 @@ GLOBEX_SERVER="${GLOBEX_SERVER:-globex-tools}"
5154
AGENT_ID="${AGENT_ID:-cursor}"
5255

5356
export KUBECONFIG
57+
export MCP_RUNTIME_CONFIG_DIR
5458

5559
need() {
5660
command -v "$1" >/dev/null 2>&1 || {
@@ -396,7 +400,7 @@ if [[ ! -x "$BIN" ]]; then
396400
exit 1
397401
fi
398402

399-
mkdir -p "$WORK_DIR"
403+
mkdir -p "$MCP_RUNTIME_CONFIG_DIR" "$WORK_DIR"
400404

401405
if [[ "${SKIP_SETUP:-0}" != "1" ]]; then
402406
setup_demo

internal/cli/auth/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Optional environment:
6666
` + authfile.EnvAPIURL + ` default API base for login, e.g. https://platform.example.com
6767
` + authfile.EnvAPIToken + ` use this token for API calls; overrides a saved file
6868
` + authfile.EnvAPIProfile + ` select a saved credentials profile
69-
MCP_RUNTIME_CONFIG_DIR override the config directory (mainly for tests)`,
69+
MCP_RUNTIME_CONFIG_DIR override the config directory (default ~/.mcpruntime)`,
7070
}
7171

7272
cmd.AddCommand(m.NewLoginCmd())

internal/cli/auth/auth_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestAuthLoginSavesAndVerifies(t *testing.T) {
7575
if err := cmd.Execute(); err != nil {
7676
t.Fatalf("execute: %v stderr=%s", err, errb.String())
7777
}
78-
b, rerr := os.ReadFile(filepath.Join(d, "credentials.json"))
78+
b, rerr := os.ReadFile(filepath.Join(d, "config.json"))
7979
if rerr != nil {
8080
t.Fatal(rerr)
8181
}
@@ -114,7 +114,7 @@ func TestAuthLoginNormalizesTrailingAPIPath(t *testing.T) {
114114
if err := cmd.Execute(); err != nil {
115115
t.Fatalf("execute: %v", err)
116116
}
117-
b, err := os.ReadFile(filepath.Join(d, "credentials.json"))
117+
b, err := os.ReadFile(filepath.Join(d, "config.json"))
118118
if err != nil {
119119
t.Fatal(err)
120120
}
@@ -150,7 +150,7 @@ func TestAuthLoginStoresMultipleProfilesAndUseSwitchesCurrent(t *testing.T) {
150150
t.Fatalf("acme login: %v", err)
151151
}
152152

153-
creds, err := authfile.Load(filepath.Join(d, "credentials.json"))
153+
creds, err := authfile.Load(filepath.Join(d, "config.json"))
154154
if err != nil {
155155
t.Fatal(err)
156156
}
@@ -166,7 +166,7 @@ func TestAuthLoginStoresMultipleProfilesAndUseSwitchesCurrent(t *testing.T) {
166166
if err := cmd.Execute(); err != nil {
167167
t.Fatalf("use admin: %v", err)
168168
}
169-
creds, err = authfile.Load(filepath.Join(d, "credentials.json"))
169+
creds, err = authfile.Load(filepath.Join(d, "config.json"))
170170
if err != nil {
171171
t.Fatal(err)
172172
}

pkg/authfile/authfile.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,24 @@ import (
1212
"sort"
1313
"strings"
1414
"time"
15-
)
1615

17-
const subDir = "mcp-runtime"
16+
"mcp-runtime/pkg/runtimeconfig"
17+
)
1818

1919
// ErrNotFound is returned when no credentials file exists or it is empty.
2020
var ErrNotFound = errors.New("not logged in: no saved credentials")
2121

2222
// ErrInvalid is returned when a credentials file exists but is malformed.
2323
var ErrInvalid = errors.New("saved credentials are invalid")
2424

25-
// ConfigDir is the per-user mcp-runtime configuration directory. If the environment
26-
// variable MCP_RUNTIME_CONFIG_DIR is set, that path is used (useful in tests).
25+
// ConfigDir is the per-user MCP Runtime configuration directory.
2726
func ConfigDir() (string, error) {
28-
if d := os.Getenv("MCP_RUNTIME_CONFIG_DIR"); d != "" {
29-
return d, nil
30-
}
31-
base, err := os.UserConfigDir()
32-
if err != nil {
33-
return "", err
34-
}
35-
return filepath.Join(base, subDir), nil
27+
return runtimeconfig.Dir()
3628
}
3729

38-
// FilePath returns the default path to credentials.json.
30+
// FilePath returns the default path to the MCP Runtime config file.
3931
func FilePath() (string, error) {
40-
dir, err := ConfigDir()
41-
if err != nil {
42-
return "", err
43-
}
44-
return filepath.Join(dir, "credentials.json"), nil
32+
return runtimeconfig.DefaultFile()
4533
}
4634

4735
// Credentials holds platform API identities saved after `mcp-runtime auth login`.
@@ -321,7 +309,7 @@ const EnvAPIToken = "MCP_PLATFORM_API_TOKEN"
321309
// EnvAPIURL is the default platform API base URL (e.g. https://platform.example.com).
322310
const EnvAPIURL = "MCP_PLATFORM_API_URL"
323311

324-
// EnvAPIProfile selects a saved platform API profile from credentials.json.
312+
// EnvAPIProfile selects a saved platform API profile from the MCP Runtime config file.
325313
const EnvAPIProfile = "MCP_PLATFORM_API_PROFILE"
326314

327315
// ResolveToken returns a token and API base URL: first from the environment, then the default credentials file.

pkg/authfile/authfile_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func TestResolveToken_PrefersEnv(t *testing.T) {
1313
d := t.TempDir()
14-
p := filepath.Join(d, "credentials.json")
14+
p := filepath.Join(d, "config.json")
1515
_ = os.MkdirAll(d, 0o700)
1616
_ = os.WriteFile(p, []byte(`{"api_url":"https://file.example","token":"filetok"}`), 0o600)
1717
t.Setenv("MCP_RUNTIME_CONFIG_DIR", d)
@@ -42,7 +42,7 @@ func TestConfigDir_RespectsEnv(t *testing.T) {
4242
func TestSaveLoadRoundTrip(t *testing.T) {
4343
t.Parallel()
4444
d := t.TempDir()
45-
p := filepath.Join(d, "credentials.json")
45+
p := filepath.Join(d, "config.json")
4646
orig := &Credentials{
4747
APIBaseURL: "https://platform.example.com",
4848
Token: "secret-token-value",
@@ -89,7 +89,7 @@ func TestSaveLoadRoundTrip(t *testing.T) {
8989

9090
func TestSaveProfilePreservesMultipleAccounts(t *testing.T) {
9191
d := t.TempDir()
92-
p := filepath.Join(d, "credentials.json")
92+
p := filepath.Join(d, "config.json")
9393
if err := SaveProfile(p, "admin", CredentialAccount{APIBaseURL: "https://platform.example.com", Token: "admin-token", Role: "admin"}); err != nil {
9494
t.Fatal(err)
9595
}
@@ -122,7 +122,7 @@ func TestResolveToken_UsesSelectedProfile(t *testing.T) {
122122
d := t.TempDir()
123123
t.Setenv("MCP_RUNTIME_CONFIG_DIR", d)
124124
t.Setenv(EnvAPIProfile, "admin")
125-
p := filepath.Join(d, "credentials.json")
125+
p := filepath.Join(d, "config.json")
126126
if err := SaveProfile(p, "admin", CredentialAccount{APIBaseURL: "https://platform.example.com", Token: "admin-token"}); err != nil {
127127
t.Fatal(err)
128128
}
@@ -150,7 +150,7 @@ func TestLoad_Missing(t *testing.T) {
150150
func TestLoad_InvalidJSON(t *testing.T) {
151151
t.Parallel()
152152
d := t.TempDir()
153-
p := filepath.Join(d, "credentials.json")
153+
p := filepath.Join(d, "config.json")
154154
if err := os.WriteFile(p, []byte(`{"api_url"`), 0o600); err != nil {
155155
t.Fatal(err)
156156
}
@@ -167,7 +167,7 @@ func TestLoad_InvalidJSON(t *testing.T) {
167167
func TestLoad_IncompleteCredentials(t *testing.T) {
168168
t.Parallel()
169169
d := t.TempDir()
170-
p := filepath.Join(d, "credentials.json")
170+
p := filepath.Join(d, "config.json")
171171
if err := os.WriteFile(p, []byte(`{"api_url":"https://platform.example.com"}`), 0o600); err != nil {
172172
t.Fatal(err)
173173
}

pkg/runtimeconfig/runtimeconfig.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Package runtimeconfig centralizes per-user MCP Runtime configuration paths.
2+
package runtimeconfig
3+
4+
import (
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
// EnvDir overrides the per-user MCP Runtime config directory.
11+
const EnvDir = "MCP_RUNTIME_CONFIG_DIR"
12+
13+
// DirName is the default config directory name under the user's home directory.
14+
const DirName = ".mcpruntime"
15+
16+
// DefaultFileName is the default MCP Runtime config file name.
17+
const DefaultFileName = "config.json"
18+
19+
// Dir returns the per-user MCP Runtime config directory.
20+
func Dir() (string, error) {
21+
if d := strings.TrimSpace(os.Getenv(EnvDir)); d != "" {
22+
return filepath.Clean(d), nil
23+
}
24+
home, err := os.UserHomeDir()
25+
if err != nil {
26+
return "", err
27+
}
28+
return filepath.Join(home, DirName), nil
29+
}
30+
31+
// Path joins path elements under the MCP Runtime config directory.
32+
func Path(elem ...string) (string, error) {
33+
dir, err := Dir()
34+
if err != nil {
35+
return "", err
36+
}
37+
parts := append([]string{dir}, elem...)
38+
return filepath.Join(parts...), nil
39+
}
40+
41+
// DefaultFile returns the default MCP Runtime config file path.
42+
func DefaultFile() (string, error) {
43+
return Path(DefaultFileName)
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package runtimeconfig
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
)
7+
8+
func TestDirDefaultsToHomeDotMCPRuntime(t *testing.T) {
9+
home := t.TempDir()
10+
t.Setenv("HOME", home)
11+
t.Setenv(EnvDir, "")
12+
13+
got, err := Dir()
14+
if err != nil {
15+
t.Fatal(err)
16+
}
17+
want := filepath.Join(home, DirName)
18+
if got != want {
19+
t.Fatalf("Dir() = %q, want %q", got, want)
20+
}
21+
}
22+
23+
func TestDirRespectsEnv(t *testing.T) {
24+
dir := filepath.Join(t.TempDir(), "custom")
25+
t.Setenv(EnvDir, dir)
26+
27+
got, err := Dir()
28+
if err != nil {
29+
t.Fatal(err)
30+
}
31+
if got != dir {
32+
t.Fatalf("Dir() = %q, want %q", got, dir)
33+
}
34+
}
35+
36+
func TestDefaultFile(t *testing.T) {
37+
dir := t.TempDir()
38+
t.Setenv(EnvDir, dir)
39+
40+
got, err := DefaultFile()
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
want := filepath.Join(dir, DefaultFileName)
45+
if got != want {
46+
t.Fatalf("DefaultFile() = %q, want %q", got, want)
47+
}
48+
}

0 commit comments

Comments
 (0)