diff --git a/README.md b/README.md index 91c7abd..cf0b83a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ First-party Paca plugin connecting **GitLab projects**, **merge requests**, and **branches** to Paca projects and tasks. -Port of the GitHub plugin (`com.paca.github`) to GitLab. The **source code and CI remain on GitHub**; the product communicates with GitLab.com (and, in the codebase, with Self-Managed instances via an optional URL). +The source code and releases are hosted on [GitHub](https://github.com/gorlix/paca-plugin-gitlab); the plugin communicates with **GitLab.com** (and Self-Managed GitLab instances via `instance_url`). + +| | | +|---|---| +| **Plugin ID** | `com.paca.gitlab` | +| **Release Version** | [`v0.5.1`](https://github.com/gorlix/paca-plugin-gitlab/releases/tag/v0.5.1) | +| **Minimum Paca Version** | `minCoreVersion` **v0.13.3** (declared in `plugin.json`) | +| **Minimum GitLab Version** | **19+** (Standard Webhooks with HMAC `signing_token`) | +| **UI Localization** | **en** + **it** | Reference GitLab documentation: [docs.gitlab.com](https://docs.gitlab.com/) · [REST API](https://docs.gitlab.com/ee/api/rest/) · [Project webhooks](https://docs.gitlab.com/user/project/integrations/webhooks/) · [Access token scopes](https://docs.gitlab.com/security/tokens/access_token_scopes/) · [Official MCP server](https://docs.gitlab.com/user/model_context_protocol/mcp_server/) @@ -10,79 +18,220 @@ Reference GitLab documentation: [docs.gitlab.com](https://docs.gitlab.com/) · [ ## Features -- Stores an encrypted GitLab access token per Paca project -- Lists and links GitLab projects accessible by the token -- Creates webhooks on the linked project (push, merge request, pipeline) -- On each Paca task: creates/links branches and merge requests, comments, reviews, and CI pipeline status -- Exposes automation triggers, conditions, and actions (`gitlab.mr_*`, `gitlab.branch_linked`, …) -- Bundles MCP tools + the `paca-gitlab-workflow` skill centered around *linking to Paca tasks* (does not replace the [official GitLab MCP](https://docs.gitlab.com/user/model_context_protocol/mcp_server/)) +- **Encrypted Token Storage**: Stores an AES-256-GCM encrypted GitLab access token per Paca project. +- **Repository Linking**: Lists and links GitLab projects accessible by the token. +- **Automated Webhooks**: Creates project webhooks for push, merge request, and pipeline events. +- **Task Integration**: Link/create branches and merge requests directly on Paca tasks; view CI pipeline status, add comments, and submit reviews (approvals). +- **Automations**: Exposes automation triggers, conditions, and actions (`gitlab.mr_linked`, `gitlab.mr_state`, `gitlab.merge_mr`, `gitlab.comment_mr`, `gitlab.branch_linked`). +- **AI Agent Skill & MCP**: Bundles MCP tools and the `paca-gitlab-workflow` skill centered on Paca task linking (does not replace the [official GitLab MCP](https://docs.gitlab.com/user/model_context_protocol/mcp_server/)). --- -## Differences Compared to the GitHub Plugin +## Production Installation -| | GitHub (`com.paca.github`) | GitLab (`com.paca.gitlab`) | -|--|---------------------------|----------------------------| -| Object | Pull request | Merge request (`iid`) | -| Auth | PAT | Personal **or** Project **or** Group Access Token | -| API Host | Fixed `api.github.com` | `instance_url` (defaults to `https://gitlab.com`) | -| Webhook | `X-Hub-Signature-256` | HMAC `signing_token` (GitLab **19+**, Standard Webhooks) | -| CI | Checks / commit status | MR Pipelines | -| Review | PR reviews | Note + Approvals API (`APPROVE` / `REQUEST_CHANGES` / `COMMENT`) | -| UI | English | i18n **en** + **it** | +### Method 1: Marketplace UI (Recommended once listed) + +Once published in the public Paca plugin marketplace catalog (see [Paca-AI/paca-plugins #34](https://github.com/Paca-AI/paca-plugins/pull/34)), administrators can install `com.paca.gitlab` directly with one click from the Paca Web Admin under **Admin → Plugins**. --- -## Setup +### Method 2: Self-Hosted Release Install + +For self-hosted Docker Compose deployments installing directly from GitHub Release assets: + +#### 1. Host Prerequisites + +| Prerequisite | Purpose | +|--------------|---------| +| Paca **≥ v0.13.3** | Required by `minCoreVersion` in `plugin.json` | +| `ENCRYPTION_KEY` | 64 hex characters (AES-256-GCM) — encrypts GitLab tokens and webhook signing tokens. Generate via `openssl rand -hex 32`. **Do not change** after storing tokens. | +| `PUBLIC_URL` | Public HTTPS URL of Paca without trailing slash (e.g. `https://paca.example.com`), reachable by GitLab for webhook delivery. | +| Admin API Key | API key with administrative privileges (`X-API-Key` header). | + +In official Paca deployments ([`scripts/install.sh`](https://github.com/Paca-AI/paca/blob/master/scripts/install.sh), [`deploy/README.md`](https://github.com/Paca-AI/paca/blob/master/deploy/README.md)), these are configured in `.env`. + +Quick health verification: +```bash +# On the host running Docker: +grep -E '^(ENCRYPTION_KEY|PUBLIC_URL)=' .env +curl -sS "${PUBLIC_URL}/api/v1/health" +``` -### Paca Host Configuration +The production Compose stack (`name: paca`) uses the following named volumes: -| Key | Required | Description | -|-----|----------|-------------| -| `ENCRYPTION_KEY` | Yes | 64 hex characters (AES-256-GCM key for tokens and webhook signing tokens) | -| `PUBLIC_URL` | Yes (for webhooks) | Public URL of the Paca server (webhook callbacks) | +| Docker Volume | Container Mount | Contents | +|---------------|-----------------|----------| +| `paca_backend_plugins` | `/plugins` | `backend.wasm`, `plugin.json`, `migrations/*.sql` | +| `paca_frontend_plugins` | `/plugins-frontend` | Module Federation bundle (`assets/remoteEntry.js`) | +| `paca_mcp_plugins` | `/plugins-mcp` | MCP bundle (`mcp.js`) | +| `paca_skills_plugins` | `/plugins-skills` | Agent skill definitions (`paca-gitlab-workflow/SKILL.md`) | -Without `PUBLIC_URL`, linking a GitLab project will fail because webhook creation requires a reachable callback URL. +#### 2. Automated Installation via Script -Outbound in `plugin.json`: `gitlab.com`. For a Self-Managed instance, the hostname should be added to `allowedOutboundDomains` before building (Self-Managed testing is currently out of scope). +Use the included helper script [`scripts/install-from-release.sh`](scripts/install-from-release.sh): -### GitLab Tokens +```bash +export PACA_URL='https://paca.example.com' # PUBLIC_URL (no trailing slash) +export PACA_API_KEY='your-admin-api-key' -Supported token types: +./scripts/install-from-release.sh \ + --api-url "$PACA_URL" \ + --api-key "$PACA_API_KEY" \ + --version v0.5.1 +``` -1. **Personal Access Token** -2. **Project Access Token** -3. **Group Access Token** +The script verifies SHA256 checksums, populates the `paca_*_plugins` volumes, and calls `POST /api/v1/admin/plugins` (or `PATCH` if updating) to trigger database migrations and runtime loading. -Recommended scope: [`api`](https://docs.gitlab.com/security/tokens/access_token_scopes/). -Creating project webhooks requires the **Maintainer** or **Owner** role on the GitLab project ([webhooks](https://docs.gitlab.com/user/project/integrations/webhooks/)). +#### 3. Manual Installation (Step-by-Step) -In Project Settings → **GitLab**: +If installing manually without the script: -- **Instance URL** (optional): defaults to `https://gitlab.com` -- **Token type**: personal / project / group -- **Access token**: token value +```bash +PLUGIN_ID="com.paca.gitlab" +VERSION="v0.5.1" +BASE="https://github.com/gorlix/paca-plugin-gitlab/releases/download/${VERSION}" +WORKDIR=$(mktemp -d) +cd "$WORKDIR" + +# 1. Download release assets and verify checksums +curl -fsSL -O "$BASE/checksums.txt" +for f in \ + gitlab-backend-wasm.tar.gz \ + gitlab-frontend-dist.tar.gz \ + gitlab-mcp-dist.tar.gz \ + gitlab-migrations.tar.gz \ + gitlab-skills-bundle.tar.gz \ + gitlab-plugin-manifest.tar.gz; do + curl -fsSL -O "$BASE/$f" +done +sha256sum -c checksums.txt + +# 2. Extract into staging layout +STAGE="$WORKDIR/stage" +mkdir -p \ + "$STAGE/backend/$PLUGIN_ID/migrations" \ + "$STAGE/frontend/$PLUGIN_ID" \ + "$STAGE/mcp/$PLUGIN_ID" \ + "$STAGE/skills/$PLUGIN_ID" + +tar -xzf gitlab-backend-wasm.tar.gz -C "$STAGE/backend/$PLUGIN_ID" +mv "$STAGE/backend/$PLUGIN_ID/"*.wasm "$STAGE/backend/$PLUGIN_ID/backend.wasm" +tar -xzf gitlab-plugin-manifest.tar.gz -C "$STAGE/backend/$PLUGIN_ID" +tar -xzf gitlab-migrations.tar.gz -C "$STAGE" +cp "$STAGE/migrations/"*.sql "$STAGE/backend/$PLUGIN_ID/migrations/" + +tar -xzf gitlab-frontend-dist.tar.gz -C "$STAGE" +cp -a "$STAGE/dist/." "$STAGE/frontend/$PLUGIN_ID/" +rm -rf "$STAGE/dist" + +tar -xzf gitlab-mcp-dist.tar.gz -C "$STAGE" +cp -a "$STAGE/dist/." "$STAGE/mcp/$PLUGIN_ID/" +rm -rf "$STAGE/dist" + +tar -xzf gitlab-skills-bundle.tar.gz -C "$STAGE" +cp -a "$STAGE/skills/." "$STAGE/skills/$PLUGIN_ID/" + +# 3. Copy to Docker volumes +docker run --rm \ + -v paca_backend_plugins:/plugins \ + -v paca_frontend_plugins:/plugins-frontend \ + -v paca_mcp_plugins:/plugins-mcp \ + -v paca_skills_plugins:/plugins-skills \ + -v "$STAGE:/stage:ro" \ + alpine:3.20 sh -c " + set -e + mkdir -p /plugins /plugins-frontend /plugins-mcp /plugins-skills + rm -rf /plugins/$PLUGIN_ID /plugins-frontend/$PLUGIN_ID /plugins-mcp/$PLUGIN_ID /plugins-skills/$PLUGIN_ID + cp -a /stage/backend/$PLUGIN_ID /plugins/ + cp -a /stage/frontend/$PLUGIN_ID /plugins-frontend/ + cp -a /stage/mcp/$PLUGIN_ID /plugins-mcp/ + cp -a /stage/skills/$PLUGIN_ID /plugins-skills/ + " + +# 4. Register or update via Admin API +PACA_URL='https://paca.example.com' +PACA_API_KEY='your-admin-api-key' +MANIFEST=$(cat "$STAGE/backend/$PLUGIN_ID/plugin.json") +VERSION_NUM=$(echo "$MANIFEST" | jq -r .version) + +LIST=$(curl -fsS -H "X-API-Key: $PACA_API_KEY" "$PACA_URL/api/v1/plugins") +EXISTING_ID=$(echo "$LIST" | jq -r --arg n "$PLUGIN_ID" '.data.plugins[]? | select(.name==$n) | .id' | head -n1) + +if [[ -n "$EXISTING_ID" && "$EXISTING_ID" != "null" ]]; then + curl -fsS -X PATCH "$PACA_URL/api/v1/admin/plugins/$EXISTING_ID" \ + -H "X-API-Key: $PACA_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"version\":\"$VERSION_NUM\",\"manifest\":$MANIFEST,\"enabled\":true}" +else + curl -fsS -X POST "$PACA_URL/api/v1/admin/plugins" \ + -H "X-API-Key: $PACA_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"$PLUGIN_ID\",\"version\":\"$VERSION_NUM\",\"manifest\":$MANIFEST,\"enabled\":true}" +fi + +# 5. Clean up temporary directory +rm -rf "$WORKDIR" +``` -Project and Group tokens are scoped strictly to their respective project or group: the list of "accessible projects" may be short or show only a single project — this is expected behavior. +If UI assets do not immediately appear in the browser, restart gateway and api containers: +```bash +docker compose restart api gateway +``` -### Webhooks (GitLab 19+) +--- -When linking a project, the plugin creates a webhook with: +## Configuration & Usage -- `push_events`, `merge_requests_events`, `pipeline_events` -- **`signing_token`** (`whsec_…`) — primary verification path (no legacy secret token) +### 1. Connecting GitLab to a Project + +1. Open your Paca project → **Settings** → **GitLab** tab (requires `gitlab.manage` permission). +2. **Instance URL** (optional): leave empty or use `https://gitlab.com`. For Self-Managed, enter the base instance URL (e.g. `https://gitlab.example.com`). +3. **Token Type**: Personal, Project, or Group Access Token. +4. **Access Token**: Enter token with [`api`](https://docs.gitlab.com/security/tokens/access_token_scopes/) scope. +5. Select and **Link** the desired GitLab repositories. + +> [!NOTE] +> - **Personal Access Token**: Can see all repositories accessible to the user. +> - **Project / Group Access Token**: Scoped strictly to that project or group; seeing only one or few projects is expected behavior. +> - Creating project webhooks requires the **Maintainer** or **Owner** role on the GitLab repository. + +### 2. Webhooks (GitLab 19+) + +When a repository is linked, the plugin provisions a GitLab project webhook with: +- Events: `push_events`, `merge_requests_events`, `pipeline_events`. +- **`signing_token`** (`whsec_...`): Standard Webhooks HMAC verification (primary verification path; not legacy secret tokens). + +Callback route: +```text +{PUBLIC_URL}/api/v1/plugins/com.paca.gitlab/projects/{projectId}/webhook +``` Expected inbound headers: `X-Gitlab-Event`, `webhook-id`, `webhook-timestamp`, `webhook-signature`. --- +## Differences Compared to the GitHub Plugin + +| | GitHub (`com.paca.github`) | GitLab (`com.paca.gitlab`) | +|---|---|---| +| **Object** | Pull Request | Merge Request (`iid`) | +| **Authentication** | Personal Access Token (PAT) | Personal, Project, or Group Access Token | +| **API Host** | Fixed `api.github.com` | `instance_url` (defaults to `https://gitlab.com`) | +| **Webhook Verification** | `X-Hub-Signature-256` | HMAC `signing_token` (GitLab **19+**, Standard Webhooks) | +| **CI Integration** | Checks / Commit statuses | Merge Request Pipelines | +| **Reviews** | PR reviews | Note + Approvals API (`APPROVE` / `REQUEST_CHANGES` / `COMMENT`) | +| **UI Localization** | English | **en** + **it** | + +--- + ## Architecture ```text ├── backend/ Go → WASM (plugin-sdk-go), schema plugin_data_com_paca_gitlab ├── frontend/ React Module Federation + i18n (en/it) ├── mcp/ Paca-centric MCP tools -└── skills/ paca-gitlab-workflow +├── skills/ paca-gitlab-workflow +└── scripts/ install-from-release.sh (production installation helper) ``` Plugin ID: `com.paca.gitlab` · Custom permission: `gitlab.manage` @@ -102,38 +251,55 @@ Host prefix: `/api/v1/plugins/com.paca.gitlab/projects/:projectId/…` --- -## i18n +## Troubleshooting -Frontend: `en` and `it` catalogs in `frontend/src/i18n/`. -Locale is determined from host prop `locale` or `navigator.language` (fallback to `en`). +| Symptom | Cause & Solution | +|---------|------------------| +| `PLUGIN_INCOMPATIBLE_HOST_VERSION` | Paca server version is below `minCoreVersion`. Upgrade Paca to ≥ `v0.13.3`. | +| Admin API returns 401/403 | Verify that the API key has administrative permissions and is passed in the `X-API-Key` header. | +| Admin API 500 on migrations/load | Ensure `backend.wasm` and `migrations/*.sql` exist in `/plugins/com.paca.gitlab/`. Check `api` container logs. | +| Repository link fails / webhook error | Verify `PUBLIC_URL` is reachable from GitLab via HTTPS. Verify the token has `api` scope and Maintainer/Owner permissions. | +| Tokens fail after database restore | `ENCRYPTION_KEY` changed after the secrets were encrypted. Reconnect the tokens in project settings. | +| Empty / short list of accessible projects | Project/Group tokens have restricted scope and only see their designated project/group. | +| Plugin UI does not render | Check that `remoteEntry.js` exists in `/plugins-frontend/com.paca.gitlab/assets/`. Run `docker compose restart gateway` and hard-refresh your browser. | +| Outbound requests fail on Self-Managed | For non-gitlab.com hosts, add the instance hostname to `allowedOutboundDomains` in `plugin.json`. | +| Webhook signature verification failed | GitLab version is older than 19 or hook was manually created without a signing token. Relink the repository via the plugin UI. | --- -## Development +## Local Development -### Backend +To run tests and build each component locally: ```bash +# Backend tests & WASM compilation cd backend -go test ./... +go test -race ./... tinygo build -target=wasip1 -buildmode=c-shared -o gitlab.wasm . -``` -### Frontend / MCP +# Frontend build & typecheck +cd ../frontend +bun install && bun run typecheck && bun run build -```bash -cd frontend && bun install && bun run typecheck && bun run build -cd mcp && bun install && bun run typecheck && bun run build +# MCP bundle +cd ../mcp +bun install && bun run typecheck && bun run build ``` -### Release +To install directly into a local development Paca instance, use Paca's development script: +```bash +./scripts/install-local-plugin.sh /path/to/paca-plugin-gitlab \ + --paca-dir /path/to/paca \ + --api-url http://localhost:3000 \ + --api-key "$API_KEY" +``` -GitHub Actions workflow: `.github/workflows/release.yml` on `v*` tags → artifact `gitlab-*.tar.gz`. +Releases are published automatically on tag pushes matching `v*` via [`.github/workflows/release.yml`](.github/workflows/release.yml). --- ## Notes -- **Self-Managed:** `instance_url` field is present; smoke testing on private instances is **deferred** until a dedicated test instance is available. Primary validation targets GitLab.com. -- Coexistence of GitHub and GitLab on the same Paca project: **not** in scope. -- Minimum GitLab version: **19+** (`signing_token` webhook signature support). +- **Self-Managed:** `instance_url` field is implemented and supported; primary CI validation targets GitLab.com. +- **GitHub / GitLab Coexistence:** Running GitHub and GitLab integrations simultaneously on the exact same Paca project is **not** supported. +- **Minimum GitLab Version:** **19+** is required for HMAC `signing_token` webhook signatures. diff --git a/scripts/install-from-release.sh b/scripts/install-from-release.sh new file mode 100755 index 0000000..55b4a1e --- /dev/null +++ b/scripts/install-from-release.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Install com.paca.gitlab from a GitHub Release into a production Paca stack. +# +# Populates Docker named volumes (compose project "paca") and registers the +# plugin via POST/PATCH /api/v1/admin/plugins. The Paca API then runs +# migrations and loads the WASM runtime. +# +# Prerequisites: curl, tar, sha256sum, jq, docker; admin API key. +# +# Usage: +# ./scripts/install-from-release.sh \ +# --api-url https://paca.example.com \ +# --api-key "$PACA_API_KEY" \ +# [--version v0.5.1] \ +# [--compose-project paca] +# +set -euo pipefail + +PLUGIN_ID="com.paca.gitlab" +REPO="gorlix/paca-plugin-gitlab" +VERSION="v0.5.1" +COMPOSE_PROJECT="paca" +API_URL="${PACA_URL:-}" +API_KEY="${PACA_API_KEY:-}" + +usage() { + sed -n '2,18p' "$0" | sed 's/^# \?//' + exit "${1:-0}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage 0 ;; + --api-url) API_URL="$2"; shift 2 ;; + --api-key) API_KEY="$2"; shift 2 ;; + --version) VERSION="$2"; shift 2 ;; + --compose-project) COMPOSE_PROJECT="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; usage 1 ;; + esac +done + +API_URL="${API_URL%/}" + +if [[ -z "$API_URL" ]]; then + echo "error: --api-url (or PACA_URL) is required" >&2 + exit 1 +fi +if [[ -z "$API_KEY" ]]; then + echo "error: --api-key (or PACA_API_KEY) is required" >&2 + exit 1 +fi +for cmd in curl tar sha256sum jq docker; do + command -v "$cmd" >/dev/null || { echo "error: missing dependency: $cmd" >&2; exit 1; } +done + +VOL_BACKEND="${COMPOSE_PROJECT}_backend_plugins" +VOL_FRONTEND="${COMPOSE_PROJECT}_frontend_plugins" +VOL_MCP="${COMPOSE_PROJECT}_mcp_plugins" +VOL_SKILLS="${COMPOSE_PROJECT}_skills_plugins" + +for vol in "$VOL_BACKEND" "$VOL_FRONTEND" "$VOL_MCP" "$VOL_SKILLS"; do + if ! docker volume inspect "$vol" >/dev/null 2>&1; then + echo "error: docker volume '$vol' not found." >&2 + echo " Expected compose project name '${COMPOSE_PROJECT}' (see docker-compose.yml 'name: paca')." >&2 + echo " List volumes: docker volume ls | grep plugins" >&2 + exit 1 + fi +done + +BASE="https://github.com/${REPO}/releases/download/${VERSION}" +WORKDIR=$(mktemp -d) +trap 'rm -rf "$WORKDIR"' EXIT +cd "$WORKDIR" + +echo "==> Downloading ${VERSION} assets from ${REPO}" +curl -fsSL -O "${BASE}/checksums.txt" +ASSETS=( + gitlab-backend-wasm.tar.gz + gitlab-frontend-dist.tar.gz + gitlab-mcp-dist.tar.gz + gitlab-migrations.tar.gz + gitlab-skills-bundle.tar.gz + gitlab-plugin-manifest.tar.gz +) +for f in "${ASSETS[@]}"; do + curl -fsSL -O "${BASE}/${f}" +done +sha256sum -c checksums.txt + +STAGE="$WORKDIR/stage" +mkdir -p \ + "$STAGE/backend/$PLUGIN_ID/migrations" \ + "$STAGE/frontend/$PLUGIN_ID" \ + "$STAGE/mcp/$PLUGIN_ID" \ + "$STAGE/skills/$PLUGIN_ID" + +echo "==> Staging plugin layout for ${PLUGIN_ID}" +tar -xzf gitlab-backend-wasm.tar.gz -C "$STAGE/backend/$PLUGIN_ID" +# Marketplace installer renames any *.wasm → backend.wasm +shopt -s nullglob +wasm_files=("$STAGE/backend/$PLUGIN_ID"/*.wasm) +shopt -u nullglob +if [[ ${#wasm_files[@]} -ne 1 ]]; then + echo "error: expected exactly one .wasm in backend archive, found ${#wasm_files[@]}" >&2 + exit 1 +fi +mv "${wasm_files[0]}" "$STAGE/backend/$PLUGIN_ID/backend.wasm" + +tar -xzf gitlab-plugin-manifest.tar.gz -C "$STAGE/backend/$PLUGIN_ID" +tar -xzf gitlab-migrations.tar.gz -C "$STAGE" +cp "$STAGE/migrations/"*.sql "$STAGE/backend/$PLUGIN_ID/migrations/" + +tar -xzf gitlab-frontend-dist.tar.gz -C "$STAGE" +cp -a "$STAGE/dist/." "$STAGE/frontend/$PLUGIN_ID/" +rm -rf "$STAGE/dist" + +tar -xzf gitlab-mcp-dist.tar.gz -C "$STAGE" +cp -a "$STAGE/dist/." "$STAGE/mcp/$PLUGIN_ID/" +rm -rf "$STAGE/dist" + +tar -xzf gitlab-skills-bundle.tar.gz -C "$STAGE" +cp -a "$STAGE/skills/." "$STAGE/skills/$PLUGIN_ID/" + +if [[ ! -f "$STAGE/frontend/$PLUGIN_ID/assets/remoteEntry.js" ]]; then + echo "error: missing frontend assets/remoteEntry.js after extract" >&2 + exit 1 +fi +if [[ ! -f "$STAGE/mcp/$PLUGIN_ID/mcp.js" ]]; then + echo "error: missing mcp.js after extract" >&2 + exit 1 +fi + +echo "==> Copying into Docker volumes (${COMPOSE_PROJECT}_*_plugins)" +docker run --rm \ + -e PLUGIN_ID="$PLUGIN_ID" \ + -v "${VOL_BACKEND}:/plugins" \ + -v "${VOL_FRONTEND}:/plugins-frontend" \ + -v "${VOL_MCP}:/plugins-mcp" \ + -v "${VOL_SKILLS}:/plugins-skills" \ + -v "${STAGE}:/stage:ro" \ + alpine:3.20 sh -c ' + set -e + mkdir -p /plugins /plugins-frontend /plugins-mcp /plugins-skills + rm -rf \ + "/plugins/${PLUGIN_ID}" \ + "/plugins-frontend/${PLUGIN_ID}" \ + "/plugins-mcp/${PLUGIN_ID}" \ + "/plugins-skills/${PLUGIN_ID}" + cp -a "/stage/backend/${PLUGIN_ID}" /plugins/ + cp -a "/stage/frontend/${PLUGIN_ID}" /plugins-frontend/ + cp -a "/stage/mcp/${PLUGIN_ID}" /plugins-mcp/ + cp -a "/stage/skills/${PLUGIN_ID}" /plugins-skills/ + ' + +MANIFEST=$(cat "$STAGE/backend/$PLUGIN_ID/plugin.json") +VERSION_NUM=$(echo "$MANIFEST" | jq -r .version) +AUTH=(-H "X-API-Key: ${API_KEY}" -H "Content-Type: application/json") + +echo "==> Registering plugin via ${API_URL}/api/v1/admin/plugins" +LIST_HTTP=$(curl -sS -o "$WORKDIR/list.json" -w "%{http_code}" \ + -H "X-API-Key: ${API_KEY}" "${API_URL}/api/v1/plugins" || true) +if [[ "$LIST_HTTP" != "200" ]]; then + echo "error: GET /api/v1/plugins failed (HTTP ${LIST_HTTP})" >&2 + cat "$WORKDIR/list.json" >&2 || true + exit 1 +fi + +EXISTING_ID=$(jq -r --arg n "$PLUGIN_ID" ' + (.data.plugins // .plugins // [])[] | select(.name == $n) | .id +' "$WORKDIR/list.json" | head -n1) + +if [[ -n "$EXISTING_ID" && "$EXISTING_ID" != "null" ]]; then + echo " updating existing plugin id=${EXISTING_ID}" + RESP_HTTP=$(curl -sS -o "$WORKDIR/resp.json" -w "%{http_code}" -X PATCH \ + "${API_URL}/api/v1/admin/plugins/${EXISTING_ID}" \ + "${AUTH[@]}" \ + -d "{\"version\":\"${VERSION_NUM}\",\"manifest\":${MANIFEST},\"enabled\":true}") +else + echo " installing new plugin" + RESP_HTTP=$(curl -sS -o "$WORKDIR/resp.json" -w "%{http_code}" -X POST \ + "${API_URL}/api/v1/admin/plugins" \ + "${AUTH[@]}" \ + -d "{\"name\":\"${PLUGIN_ID}\",\"version\":\"${VERSION_NUM}\",\"manifest\":${MANIFEST},\"enabled\":true}") +fi + +if [[ "$RESP_HTTP" != "200" && "$RESP_HTTP" != "201" ]]; then + echo "error: admin plugins API failed (HTTP ${RESP_HTTP})" >&2 + cat "$WORKDIR/resp.json" >&2 || true + exit 1 +fi + +echo "==> OK: ${PLUGIN_ID} v${VERSION_NUM} installed and enabled" +echo " Next: project Settings → GitLab → token + link repositories" +echo " If the UI bundle looks stale: docker compose restart gateway"