Some checks failed
CI/CD / lint (push) Successful in 46s
CI/CD / test (push) Successful in 1m3s
CI/CD / typecheck (push) Has started running
CI/CD / smoke (push) Has been cancelled
CI/CD / build (amd64) (push) Has been cancelled
CI/CD / build (arm64) (push) Has been cancelled
CI/CD / publish-rpm (amd64) (push) Has been cancelled
CI/CD / publish-rpm (arm64) (push) Has been cancelled
CI/CD / publish-deb (amd64) (push) Has been cancelled
CI/CD / publish-deb (arm64) (push) Has been cancelled
Add cross-architecture build support so the project can be developed on ARM64 (Fedora aarch64 laptop) while still producing amd64 packages for production. All build, package, publish, and install scripts are now architecture-aware via shared arch-helper.sh detection. - Add scripts/arch-helper.sh for shared architecture detection - CI builds both amd64 and arm64 in matrix strategy - nfpm.yaml uses NFPM_ARCH env var instead of hardcoded amd64 - Build scripts support MCPCTL_TARGET_ARCH for cross-compilation - installlocal.sh auto-detects RPM/DEB and filters by architecture - release.sh gains --both-arches flag for dual-arch releases - Package cleanup is arch-scoped (won't clobber other arch's packages) - build-mcpd.sh supports --platform and --multi-arch flags - Add pnpm scripts: rpm:build:amd64, deb:build:arm64, release:both - Conditional rpm/dpkg-deb checks for cross-distro compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
449 lines
15 KiB
YAML
449 lines
15 KiB
YAML
name: CI/CD
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
env:
|
|
GITEA_REGISTRY: 10.0.0.194:3012
|
|
GITEA_PUBLIC_URL: https://mysources.co.uk
|
|
GITEA_OWNER: michal
|
|
|
|
# ============================================================
|
|
# Required Gitea secrets:
|
|
# PACKAGES_TOKEN — Gitea API token (packages + registry)
|
|
# ============================================================
|
|
|
|
jobs:
|
|
# ── CI checks (run in parallel on every push/PR) ──────────
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
# no pnpm cache — concurrent cache restore hangs on single-worker runner
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Lint
|
|
run: pnpm lint || echo "::warning::Lint has errors — not blocking CI yet"
|
|
|
|
typecheck:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
# no pnpm cache — concurrent cache restore hangs on single-worker runner
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
run: pnpm --filter @mcpctl/db exec prisma generate
|
|
|
|
- name: Typecheck
|
|
run: pnpm typecheck
|
|
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
# no pnpm cache — concurrent cache restore hangs on single-worker runner
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
run: pnpm --filter @mcpctl/db exec prisma generate
|
|
|
|
- name: Build (needed by completions test)
|
|
run: pnpm build
|
|
|
|
- name: Run tests
|
|
run: pnpm test:run
|
|
|
|
# ── Smoke tests (full stack: postgres + mcpd + mcplocal) ──
|
|
|
|
smoke:
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, typecheck, test]
|
|
services:
|
|
postgres:
|
|
image: postgres:16
|
|
env:
|
|
POSTGRES_USER: mcpctl
|
|
POSTGRES_PASSWORD: mcpctl
|
|
POSTGRES_DB: mcpctl
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
env:
|
|
DATABASE_URL: postgresql://mcpctl:mcpctl@postgres:5432/mcpctl
|
|
MCPD_PORT: "3100"
|
|
MCPD_HOST: "0.0.0.0"
|
|
MCPLOCAL_HTTP_PORT: "3200"
|
|
MCPLOCAL_MCPD_URL: http://localhost:3100
|
|
DOCKER_API_VERSION: "1.43"
|
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
# no pnpm cache — concurrent cache restore hangs on single-worker runner
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
run: pnpm --filter @mcpctl/db exec prisma generate
|
|
|
|
- name: Build all packages
|
|
run: pnpm build
|
|
|
|
- name: Push database schema
|
|
run: pnpm --filter @mcpctl/db exec prisma db push --accept-data-loss
|
|
|
|
- name: Seed templates
|
|
run: node src/mcpd/dist/seed-runner.js
|
|
|
|
- name: Start mcpd
|
|
run: node src/mcpd/dist/main.js &
|
|
|
|
- name: Wait for mcpd
|
|
run: |
|
|
for i in $(seq 1 30); do
|
|
if curl -sf http://localhost:3100/health > /dev/null 2>&1; then
|
|
echo "mcpd is ready"
|
|
exit 0
|
|
fi
|
|
echo "Waiting for mcpd... ($i/30)"
|
|
sleep 1
|
|
done
|
|
echo "::error::mcpd failed to start within 30s"
|
|
exit 1
|
|
|
|
- name: Create CI user and session
|
|
run: |
|
|
pnpm --filter @mcpctl/db exec node -e "
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const crypto = require('crypto');
|
|
(async () => {
|
|
const prisma = new PrismaClient();
|
|
const user = await prisma.user.upsert({
|
|
where: { email: 'ci@test.local' },
|
|
create: { email: 'ci@test.local', name: 'CI', passwordHash: '!ci-no-login', role: 'USER' },
|
|
update: {},
|
|
});
|
|
const token = crypto.randomBytes(32).toString('hex');
|
|
await prisma.session.create({
|
|
data: { token, userId: user.id, expiresAt: new Date(Date.now() + 86400000) },
|
|
});
|
|
await prisma.rbacDefinition.create({
|
|
data: {
|
|
name: 'ci-admin',
|
|
subjects: [{ kind: 'User', name: 'ci@test.local' }],
|
|
roleBindings: [
|
|
{ role: 'edit', resource: '*' },
|
|
{ role: 'run', resource: '*' },
|
|
{ role: 'run', action: 'logs' },
|
|
{ role: 'run', action: 'backup' },
|
|
{ role: 'run', action: 'restore' },
|
|
],
|
|
},
|
|
});
|
|
const os = require('os'), fs = require('fs'), path = require('path');
|
|
const dir = path.join(os.homedir(), '.mcpctl');
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, 'credentials'),
|
|
JSON.stringify({ token, mcpdUrl: 'http://localhost:3100', user: 'ci@test.local' }));
|
|
console.log('CI user + session + RBAC created, credentials written');
|
|
await prisma.\$disconnect();
|
|
})();
|
|
"
|
|
|
|
- name: Create mcpctl CLI wrapper
|
|
run: |
|
|
printf '#!/bin/sh\nexec node "%s/src/cli/dist/index.js" "$@"\n' "$GITHUB_WORKSPACE" > /usr/local/bin/mcpctl
|
|
chmod +x /usr/local/bin/mcpctl
|
|
|
|
- name: Configure mcplocal LLM provider
|
|
run: |
|
|
mkdir -p ~/.mcpctl
|
|
cat > ~/.mcpctl/config.json << 'CONF'
|
|
{"llm":{"providers":[{"name":"anthropic","type":"anthropic","model":"claude-haiku-3-5-20241022","tier":"fast"}]}}
|
|
CONF
|
|
printf '{"anthropic-api-key":"%s"}\n' "$ANTHROPIC_API_KEY" > ~/.mcpctl/secrets
|
|
chmod 600 ~/.mcpctl/secrets
|
|
|
|
- name: Start mcplocal
|
|
run: nohup node src/mcplocal/dist/main.js > /tmp/mcplocal.log 2>&1 &
|
|
|
|
- name: Wait for mcplocal
|
|
run: |
|
|
for i in $(seq 1 30); do
|
|
if curl -sf http://localhost:3200/health > /dev/null 2>&1; then
|
|
echo "mcplocal is ready"
|
|
exit 0
|
|
fi
|
|
echo "Waiting for mcplocal... ($i/30)"
|
|
sleep 1
|
|
done
|
|
echo "::error::mcplocal failed to start within 30s"
|
|
exit 1
|
|
|
|
- name: Apply smoke test fixtures
|
|
run: mcpctl apply -f src/mcplocal/tests/smoke/fixtures/smoke-data.yaml
|
|
|
|
- name: Wait for server instance
|
|
run: |
|
|
echo "Waiting for smoke-aws-docs instance..."
|
|
for i in $(seq 1 60); do
|
|
STATUS=$(mcpctl get instances -o json 2>/dev/null | \
|
|
node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf-8'));const i=Array.isArray(d)?d.find(x=>x.serverName&&x.serverName.includes('aws')):null;console.log(i?.status??'WAITING')}catch{console.log('WAITING')}" 2>/dev/null || echo "WAITING")
|
|
echo " Instance status: $STATUS ($i/60)"
|
|
if [ "$STATUS" = "RUNNING" ]; then
|
|
echo "Instance is running!"
|
|
break
|
|
fi
|
|
if [ "$i" = "60" ]; then
|
|
echo "::warning::Instance did not reach RUNNING — container management may not be available in CI"
|
|
echo "API-layer smoke tests will still run"
|
|
fi
|
|
sleep 5
|
|
done
|
|
|
|
- name: Run smoke tests
|
|
# Exclude tests that need a running MCP server instance (Docker) or
|
|
# LLM providers — CI has neither. --no-file-parallelism avoids
|
|
# concurrent requests crashing mcplocal.
|
|
run: >-
|
|
pnpm --filter mcplocal exec vitest run
|
|
--config vitest.smoke.config.ts
|
|
--no-file-parallelism
|
|
--exclude '**/security.test.ts'
|
|
--exclude '**/audit.test.ts'
|
|
--exclude '**/proxy-pipeline.test.ts'
|
|
|
|
- name: Dump mcplocal log on failure
|
|
if: failure()
|
|
run: cat /tmp/mcplocal.log || true
|
|
|
|
# ── Build & package (both amd64 and arm64) ──────────────
|
|
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, typecheck, test]
|
|
strategy:
|
|
matrix:
|
|
arch: [amd64, arm64]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: pnpm/action-setup@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
# no pnpm cache — concurrent cache restore hangs on single-worker runner
|
|
|
|
- name: Install dependencies (hoisted for bun compile compatibility)
|
|
run: |
|
|
echo "node-linker=hoisted" >> .npmrc
|
|
pnpm install --frozen-lockfile
|
|
|
|
- name: Generate Prisma client
|
|
run: pnpm --filter @mcpctl/db exec prisma generate
|
|
|
|
- name: Build all packages
|
|
run: pnpm build
|
|
|
|
- name: Generate shell completions
|
|
run: pnpm completions:generate
|
|
|
|
- uses: oven-sh/setup-bun@v2
|
|
|
|
- name: Install nfpm
|
|
run: |
|
|
# nfpm itself runs on the CI runner (always x86_64); it cross-packages
|
|
# for the target arch via NFPM_ARCH env var — no ARM nfpm binary needed.
|
|
curl -sL -o /tmp/nfpm.tar.gz "https://github.com/goreleaser/nfpm/releases/download/v2.45.0/nfpm_2.45.0_Linux_x86_64.tar.gz"
|
|
tar xzf /tmp/nfpm.tar.gz -C /usr/local/bin nfpm
|
|
|
|
- name: Bundle standalone binaries (${{ matrix.arch }})
|
|
env:
|
|
MCPCTL_TARGET_ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
source scripts/arch-helper.sh
|
|
resolve_arch "$MCPCTL_TARGET_ARCH"
|
|
|
|
mkdir -p dist
|
|
# Stub for optional dep that Ink tries to import (only used when DEV=true)
|
|
# Copy instead of symlink — bun can't read directory symlinks
|
|
if [ ! -e node_modules/react-devtools-core/package.json ]; then
|
|
rm -rf node_modules/react-devtools-core
|
|
cp -r src/cli/stubs/react-devtools-core node_modules/react-devtools-core
|
|
fi
|
|
bun build src/cli/src/index.ts --compile ${BUN_TARGET:+--target "$BUN_TARGET"} --outfile dist/mcpctl
|
|
bun build src/mcplocal/src/main.ts --compile ${BUN_TARGET:+--target "$BUN_TARGET"} --outfile dist/mcpctl-local
|
|
|
|
- name: Package RPM (${{ matrix.arch }})
|
|
env:
|
|
NFPM_ARCH: ${{ matrix.arch }}
|
|
run: nfpm pkg --packager rpm --target dist/
|
|
|
|
- name: Package DEB (${{ matrix.arch }})
|
|
env:
|
|
NFPM_ARCH: ${{ matrix.arch }}
|
|
run: nfpm pkg --packager deb --target dist/
|
|
|
|
- name: Upload RPM artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: rpm-package-${{ matrix.arch }}
|
|
path: dist/mcpctl-*.rpm
|
|
retention-days: 7
|
|
|
|
- name: Upload DEB artifact
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: deb-package-${{ matrix.arch }}
|
|
path: dist/mcpctl*.deb
|
|
retention-days: 7
|
|
|
|
# ── Release pipeline (main branch push only) ──────────────
|
|
# NOTE: Docker image builds + deploy happen via `bash fulldeploy.sh`
|
|
# (not CI) because the runner containers lack the privileged access
|
|
# needed for container-in-container builds (no /proc/self/uid_map,
|
|
# no Docker socket access, buildah/podman/kaniko all fail).
|
|
|
|
publish-rpm:
|
|
runs-on: ubuntu-latest
|
|
needs: [build]
|
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
strategy:
|
|
matrix:
|
|
arch: [amd64, arm64]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Download RPM artifact
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: rpm-package-${{ matrix.arch }}
|
|
path: dist/
|
|
|
|
- name: Install rpm tools
|
|
run: sudo apt-get update && sudo apt-get install -y rpm
|
|
|
|
- name: Publish RPM (${{ matrix.arch }}) to Gitea
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
GITEA_URL: http://${{ env.GITEA_REGISTRY }}
|
|
GITEA_OWNER: ${{ env.GITEA_OWNER }}
|
|
GITEA_REPO: mcpctl
|
|
run: |
|
|
RPM_FILE=$(ls dist/mcpctl-*.rpm | head -1)
|
|
RPM_VERSION=$(rpm -qp --queryformat '%{VERSION}-%{RELEASE}' "$RPM_FILE")
|
|
echo "Publishing $RPM_FILE (version $RPM_VERSION)..."
|
|
|
|
# Delete existing version if present
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/mcpctl/${RPM_VERSION}")
|
|
|
|
if [ "$HTTP_CODE" = "200" ]; then
|
|
echo "Version exists, replacing..."
|
|
curl -s -o /dev/null -X DELETE \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
"${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/mcpctl/${RPM_VERSION}"
|
|
fi
|
|
|
|
# Upload
|
|
curl --fail -X PUT \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
--upload-file "$RPM_FILE" \
|
|
"${GITEA_URL}/api/packages/${GITEA_OWNER}/rpm/upload"
|
|
|
|
echo "Published successfully!"
|
|
|
|
# Link package to repo
|
|
source scripts/link-package.sh
|
|
link_package "rpm" "mcpctl"
|
|
|
|
publish-deb:
|
|
runs-on: ubuntu-latest
|
|
needs: [build]
|
|
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
strategy:
|
|
matrix:
|
|
arch: [amd64, arm64]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Download DEB artifact
|
|
uses: actions/download-artifact@v3
|
|
with:
|
|
name: deb-package-${{ matrix.arch }}
|
|
path: dist/
|
|
|
|
- name: Publish DEB (${{ matrix.arch }}) to Gitea
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
|
GITEA_URL: http://${{ env.GITEA_REGISTRY }}
|
|
GITEA_OWNER: ${{ env.GITEA_OWNER }}
|
|
GITEA_REPO: mcpctl
|
|
run: |
|
|
DEB_FILE=$(ls dist/mcpctl*.deb | head -1)
|
|
DEB_VERSION=$(dpkg-deb --field "$DEB_FILE" Version)
|
|
echo "Publishing $DEB_FILE (version $DEB_VERSION)..."
|
|
|
|
# Publish to each supported distribution
|
|
# Debian: trixie (13/stable), forky (14/testing)
|
|
# Ubuntu: noble (24.04 LTS), plucky (25.04)
|
|
DISTRIBUTIONS="trixie forky noble plucky"
|
|
|
|
for DIST in $DISTRIBUTIONS; do
|
|
echo " -> $DIST..."
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X PUT \
|
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
--upload-file "$DEB_FILE" \
|
|
"${GITEA_URL}/api/packages/${GITEA_OWNER}/debian/pool/${DIST}/main/upload")
|
|
|
|
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
|
echo " Published to $DIST"
|
|
elif [ "$HTTP_CODE" = "409" ]; then
|
|
echo " Already exists in $DIST (skipping)"
|
|
else
|
|
echo " WARNING: Upload to $DIST returned HTTP $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
echo "Published successfully!"
|
|
|
|
# Link package to repo
|
|
source scripts/link-package.sh
|
|
link_package "debian" "mcpctl"
|