From 4b3158408e87904b6ac0099cffea477b61431885 Mon Sep 17 00:00:00 2001 From: Michal Date: Sun, 8 Mar 2026 22:02:07 +0000 Subject: [PATCH] ci: full CI/CD pipeline via Gitea Actions Replaces the minimal CI workflow with a complete build/release pipeline: - lint, typecheck, test (parallel, every push/PR) - build: TS + completions + bun binaries + RPM packaging - docker: build & push all 4 images (mcpd, node-runner, python-runner, docmost-mcp) - publish-rpm: upload RPM to Gitea packages - deploy: update Portainer stack Also adds scripts/link-package.sh shared helper to auto-link packages to the repository (Gitea 1.24+ API with graceful fallback), called from all build/publish scripts. Co-Authored-By: Claude Opus 4.6 --- .gitea/workflows/ci.yml | 194 +++++++++++++++++++++++++++------ scripts/build-docmost-mcp.sh | 4 + scripts/build-mcpd.sh | 4 + scripts/build-python-runner.sh | 4 + scripts/link-package.sh | 64 +++++++++++ scripts/publish-rpm.sh | 17 +-- 6 files changed, 241 insertions(+), 46 deletions(-) create mode 100644 scripts/link-package.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9b1d6de..5c1326a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI/CD on: push: @@ -6,7 +6,20 @@ on: pull_request: branches: [main] +env: + GITEA_REGISTRY: 10.0.0.194:3012 + GITEA_OWNER: michal + +# ============================================================ +# Required Gitea secrets: +# PACKAGES_TOKEN — Gitea API token (packages + registry) +# PORTAINER_PASSWORD — Portainer login for stack deploy +# POSTGRES_PASSWORD — Database password for production stack +# ============================================================ + jobs: + # ── CI checks (run in parallel on every push/PR) ────────── + lint: runs-on: ubuntu-latest steps: @@ -70,6 +83,8 @@ jobs: - name: Run tests run: pnpm test:run + # ── Build & package RPM ─────────────────────────────────── + build: runs-on: ubuntu-latest needs: [lint, typecheck, test] @@ -93,50 +108,165 @@ jobs: - name: Build all packages run: pnpm build - package: - runs-on: ubuntu-latest - needs: [build] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - steps: - - uses: actions/checkout@v4 + - name: Generate shell completions + run: pnpm completions:generate - - uses: pnpm/action-setup@v4 - with: - version: 9 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: pnpm - - - run: pnpm install --frozen-lockfile - - - name: Generate Prisma client - run: pnpm --filter @mcpctl/db exec prisma generate - - - name: Build TypeScript - run: pnpm build - - - name: Install bun - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@v2 - name: Install nfpm run: | 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 binary - run: bun build src/cli/src/index.ts --compile --outfile dist/mcpctl + - name: Bundle standalone binaries + run: | + mkdir -p dist + # Stub for optional dep that bun tries to resolve + if [ ! -e node_modules/react-devtools-core ]; then + ln -s ../src/cli/stubs/react-devtools-core node_modules/react-devtools-core + fi + bun build src/cli/src/index.ts --compile --outfile dist/mcpctl + bun build src/mcplocal/src/main.ts --compile --outfile dist/mcpctl-local - - name: Build RPM + - name: Package RPM run: nfpm pkg --packager rpm --target dist/ - - name: Publish to Gitea packages + - name: Upload RPM artifact + uses: actions/upload-artifact@v4 + with: + name: rpm-package + path: dist/mcpctl-*.rpm + retention-days: 7 + + # ── Release pipeline (main branch push only) ────────────── + + docker: + runs-on: ubuntu-latest + needs: [build] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Configure insecure registry + run: | + sudo mkdir -p /etc/docker + echo '{"insecure-registries":["${{ env.GITEA_REGISTRY }}"]}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker + + - name: Login to Gitea container registry + run: | + echo "${{ secrets.PACKAGES_TOKEN }}" | docker login \ + --username ${{ env.GITEA_OWNER }} --password-stdin \ + ${{ env.GITEA_REGISTRY }} + + - name: Build & push mcpd + run: | + docker build -t ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpd:latest \ + -f deploy/Dockerfile.mcpd . + docker push ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpd:latest + + - name: Build & push node-runner + run: | + docker build -t ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpctl-node-runner:latest \ + -f deploy/Dockerfile.node-runner . + docker push ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpctl-node-runner:latest + + - name: Build & push python-runner + run: | + docker build -t ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpctl-python-runner:latest \ + -f deploy/Dockerfile.python-runner . + docker push ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/mcpctl-python-runner:latest + + - name: Build & push docmost-mcp + run: | + docker build -t ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/docmost-mcp:latest \ + -f deploy/Dockerfile.docmost-mcp . + docker push ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_OWNER }}/docmost-mcp:latest + + - name: Link packages to repository env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }} + GITEA_URL: http://${{ env.GITEA_REGISTRY }} + GITEA_OWNER: ${{ env.GITEA_OWNER }} + GITEA_REPO: mcpctl + run: | + source scripts/link-package.sh + link_package "container" "mcpd" + link_package "container" "mcpctl-node-runner" + link_package "container" "mcpctl-python-runner" + link_package "container" "docmost-mcp" + + publish-rpm: + runs-on: ubuntu-latest + needs: [build] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Download RPM artifact + uses: actions/download-artifact@v4 + with: + name: rpm-package + path: dist/ + + - name: Install rpm tools + run: sudo apt-get update && sudo apt-get install -y rpm + + - name: Publish RPM 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" \ - "${{ github.server_url }}/api/packages/${{ github.repository_owner }}/rpm/upload" + "${GITEA_URL}/api/packages/${GITEA_OWNER}/rpm/upload" + + echo "Published successfully!" + + # Link package to repo + source scripts/link-package.sh + link_package "rpm" "mcpctl" + + deploy: + runs-on: ubuntu-latest + needs: [docker, publish-rpm] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Create stack env file + env: + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + run: | + printf '%s\n' \ + "POSTGRES_USER=mcpctl" \ + "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \ + "POSTGRES_DB=mcpctl" \ + "MCPD_PORT=3100" \ + "MCPD_LOG_LEVEL=info" \ + > stack/.env + + - name: Deploy to Portainer + env: + PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }} + run: bash deploy.sh diff --git a/scripts/build-docmost-mcp.sh b/scripts/build-docmost-mcp.sh index 1697b17..5c4b25f 100644 --- a/scripts/build-docmost-mcp.sh +++ b/scripts/build-docmost-mcp.sh @@ -28,5 +28,9 @@ podman login --tls-verify=false -u michal -p "$GITEA_TOKEN" "$REGISTRY" echo "==> Pushing to $REGISTRY/michal/$IMAGE:$TAG..." podman push --tls-verify=false "$REGISTRY/michal/$IMAGE:$TAG" +# Ensure package is linked to the repository +source "$SCRIPT_DIR/link-package.sh" +link_package "container" "$IMAGE" + echo "==> Done!" echo " Image: $REGISTRY/michal/$IMAGE:$TAG" diff --git a/scripts/build-mcpd.sh b/scripts/build-mcpd.sh index 39596df..ae1b313 100755 --- a/scripts/build-mcpd.sh +++ b/scripts/build-mcpd.sh @@ -28,5 +28,9 @@ podman login --tls-verify=false -u michal -p "$GITEA_TOKEN" "$REGISTRY" echo "==> Pushing to $REGISTRY/michal/$IMAGE:$TAG..." podman push --tls-verify=false "$REGISTRY/michal/$IMAGE:$TAG" +# Ensure package is linked to the repository +source "$SCRIPT_DIR/link-package.sh" +link_package "container" "$IMAGE" + echo "==> Done!" echo " Image: $REGISTRY/michal/$IMAGE:$TAG" diff --git a/scripts/build-python-runner.sh b/scripts/build-python-runner.sh index 74393b4..c74a565 100755 --- a/scripts/build-python-runner.sh +++ b/scripts/build-python-runner.sh @@ -28,5 +28,9 @@ podman login --tls-verify=false -u michal -p "$GITEA_TOKEN" "$REGISTRY" echo "==> Pushing to $REGISTRY/michal/$IMAGE:$TAG..." podman push --tls-verify=false "$REGISTRY/michal/$IMAGE:$TAG" +# Ensure package is linked to the repository +source "$SCRIPT_DIR/link-package.sh" +link_package "container" "$IMAGE" + echo "==> Done!" echo " Image: $REGISTRY/michal/$IMAGE:$TAG" diff --git a/scripts/link-package.sh b/scripts/link-package.sh new file mode 100644 index 0000000..51a5944 --- /dev/null +++ b/scripts/link-package.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Link a Gitea package to a repository. +# Works automatically on Gitea 1.24+ (uses API), warns on older versions. +# +# Usage: source scripts/link-package.sh +# link_package +# +# Requires: GITEA_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO + +link_package() { + local PKG_TYPE="$1" # e.g. "rpm", "container" + local PKG_NAME="$2" # e.g. "mcpctl", "mcpd" + + if [ -z "$PKG_TYPE" ] || [ -z "$PKG_NAME" ]; then + echo "Usage: link_package " + return 1 + fi + + local GITEA_URL="${GITEA_URL:-http://10.0.0.194:3012}" + local GITEA_OWNER="${GITEA_OWNER:-michal}" + local GITEA_REPO="${GITEA_REPO:-mcpctl}" + + if [ -z "$GITEA_TOKEN" ]; then + echo "WARNING: GITEA_TOKEN not set, skipping package-repo linking." + return 0 + fi + + # Check if already linked (search all packages, filter by type+name client-side) + local REPO_LINK + REPO_LINK=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_URL}/api/v1/packages/${GITEA_OWNER}" \ + | python3 -c " +import json,sys +for p in json.load(sys.stdin): + if p['type']=='$PKG_TYPE' and p['name']=='$PKG_NAME': + r=p.get('repository') + if r: print(r['full_name']) + break +" 2>/dev/null) + + if [ -n "$REPO_LINK" ]; then + echo "==> Package ${PKG_TYPE}/${PKG_NAME} already linked to ${REPO_LINK}" + return 0 + fi + + # Try Gitea 1.24+ link API + local HTTP_CODE + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/${PKG_TYPE}/${PKG_NAME}/-/link/${GITEA_REPO}") + + if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then + echo "==> Linked ${PKG_TYPE}/${PKG_NAME} to ${GITEA_OWNER}/${GITEA_REPO}" + return 0 + fi + + # API not available (Gitea < 1.24) — warn with manual instructions + echo "" + echo "WARNING: Could not auto-link ${PKG_TYPE}/${PKG_NAME} to repository (Gitea < 1.24)." + echo "Link it manually in the Gitea UI:" + echo " ${GITEA_URL}/${GITEA_OWNER}/-/packages/${PKG_TYPE}/${PKG_NAME}/settings" + echo " -> Link to repository: ${GITEA_OWNER}/${GITEA_REPO}" + return 0 +} diff --git a/scripts/publish-rpm.sh b/scripts/publish-rpm.sh index ba6ca9f..abcbc9c 100755 --- a/scripts/publish-rpm.sh +++ b/scripts/publish-rpm.sh @@ -51,20 +51,9 @@ curl --fail -s -X PUT \ echo "" echo "==> Published successfully!" -# Verify the package is linked to the repo (Gitea stores the link at package level) -REPO_LINK=$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ - "${GITEA_URL}/api/v1/packages/${GITEA_OWNER}/rpm/mcpctl/${RPM_VERSION}" \ - | python3 -c "import json,sys; d=json.load(sys.stdin); r=d.get('repository'); print(r.get('full_name') if r else '')" 2>/dev/null) - -if [ -n "$REPO_LINK" ]; then - echo "==> Linked to repo: ${REPO_LINK}" -else - echo "" - echo "WARNING: Package is not linked to a repository." - echo "Link it manually in the Gitea UI:" - echo " ${GITEA_URL}/${GITEA_OWNER}/-/packages/rpm/mcpctl/${RPM_VERSION}/settings" - echo " → Link to repository: ${GITEA_OWNER}/${GITEA_REPO}" -fi +# Ensure package is linked to the repository +source "$SCRIPT_DIR/link-package.sh" +link_package "rpm" "mcpctl" echo "" echo "Install with:"