feat: add OpenProject 17 docker compose stack and test data seeder
- Configure OpenProject 17 with PostgreSQL 17 and Memcached - Add scripts/seed-test-data.rb for test project, work packages, and API key generation - Add scripts/seed.sh to orchestrate container health check and seed execution - Add scripts/start.sh and scripts/stop.sh stack lifecycle helpers - Provide .env.example with defaults
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# OpenProject Docker & MCP Server Environment Configuration
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# OpenProject Docker Image Tag (Targeting OpenProject 17)
|
||||||
|
TAG=17-slim
|
||||||
|
|
||||||
|
# Local exposed port on the host
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
POSTGRES_VERSION=17
|
||||||
|
POSTGRES_PASSWORD=p4ssw0rd
|
||||||
|
|
||||||
|
# OpenProject Application Configuration
|
||||||
|
OPENPROJECT_HOST__NAME=localhost:8080
|
||||||
|
OPENPROJECT_HTTPS=false
|
||||||
|
SECRET_KEY_BASE=openproject-secret-key-base-for-local-testing-at-least-64-characters-long
|
||||||
|
|
||||||
|
# MCP Server Client Configuration
|
||||||
|
# Once the container is running and seeded via scripts/seed.sh,
|
||||||
|
# OPENPROJECT_API_KEY will be automatically written to .env.test
|
||||||
|
OPENPROJECT_BASE_URL=http://localhost:8080
|
||||||
|
OPENPROJECT_API_KEY=
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# OpenProject MCP Companion - Local Development & Testing Stack
|
||||||
|
# Target: OpenProject 17
|
||||||
|
# Usage: docker compose up -d
|
||||||
|
|
||||||
|
networks:
|
||||||
|
openproject-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
opdata:
|
||||||
|
|
||||||
|
x-op-restart-policy: &restart_policy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
x-op-image: &image
|
||||||
|
image: openproject/openproject:${TAG:-17-slim}
|
||||||
|
|
||||||
|
x-op-app: &app
|
||||||
|
<<: [*image, *restart_policy]
|
||||||
|
environment:
|
||||||
|
OPENPROJECT_HTTPS: "${OPENPROJECT_HTTPS:-false}"
|
||||||
|
OPENPROJECT_HOST__NAME: "${OPENPROJECT_HOST__NAME:-localhost:8080}"
|
||||||
|
OPENPROJECT_ADDITIONAL__HOST__NAMES: "${OPENPROJECT_ADDITIONAL__HOST__NAMES:-web}"
|
||||||
|
OPENPROJECT_HSTS: "false"
|
||||||
|
OPENPROJECT_RAILS__CACHE__STORE: "memcache"
|
||||||
|
RAILS_CACHE_STORE: "memcache"
|
||||||
|
OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211"
|
||||||
|
DATABASE_URL: "${DATABASE_URL:-postgres://postgres:p4ssw0rd@db/openproject?pool=20&encoding=unicode&reconnect=true}"
|
||||||
|
RAILS_MIN_THREADS: ${RAILS_MIN_THREADS:-4}
|
||||||
|
RAILS_MAX_THREADS: ${RAILS_MAX_THREADS:-16}
|
||||||
|
SECRET_KEY_BASE: "${SECRET_KEY_BASE:-openproject-secret-key-base-for-local-testing-at-least-64-characters-long}"
|
||||||
|
IMAP_ENABLED: "false"
|
||||||
|
OPENPROJECT_ENABLE__INTERNAL__ASSETS__SERVER: "true"
|
||||||
|
volumes:
|
||||||
|
- opdata:/var/openproject/assets
|
||||||
|
- ./scripts:/scripts:ro
|
||||||
|
networks:
|
||||||
|
- openproject-net
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:${POSTGRES_VERSION:-17}
|
||||||
|
<<: *restart_policy
|
||||||
|
stop_grace_period: "3s"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-p4ssw0rd}
|
||||||
|
POSTGRES_DB: openproject
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d openproject"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- openproject-net
|
||||||
|
|
||||||
|
cache:
|
||||||
|
image: memcached:alpine
|
||||||
|
<<: *restart_policy
|
||||||
|
networks:
|
||||||
|
- openproject-net
|
||||||
|
|
||||||
|
seeder:
|
||||||
|
<<: *app
|
||||||
|
command: "./docker/prod/seeder"
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
cache:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
web:
|
||||||
|
<<: *app
|
||||||
|
command: "./docker/prod/web"
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8080}:8080"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
cache:
|
||||||
|
condition: service_started
|
||||||
|
seeder:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health_checks/default"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
start_period: 45s
|
||||||
|
|
||||||
|
worker:
|
||||||
|
<<: *app
|
||||||
|
command: "./docker/prod/worker"
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
cache:
|
||||||
|
condition: service_started
|
||||||
|
seeder:
|
||||||
|
condition: service_completed_successfully
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
# OpenProject MCP Companion - Test Data & API Key Seeder
|
||||||
|
# Run inside Rails environment via: bundle exec rails runner /scripts/seed-test-data.rb
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
puts "[Seeder] Starting OpenProject MCP test data initialization..."
|
||||||
|
|
||||||
|
# 1. Ensure admin user is active and has no password change requirement
|
||||||
|
admin = User.find_by(login: 'admin')
|
||||||
|
if admin.nil?
|
||||||
|
puts "[Seeder] ERROR: Default admin user not found in database!"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
admin.status = Principal.statuses[:active] if defined?(Principal) && Principal.respond_to?(:statuses)
|
||||||
|
admin.password = 'admin12345678'
|
||||||
|
admin.password_confirmation = 'admin12345678'
|
||||||
|
admin.force_password_change = false if admin.respond_to?(:force_password_change=)
|
||||||
|
admin.save(validate: false)
|
||||||
|
puts "[Seeder] Admin user configured (login: admin)."
|
||||||
|
|
||||||
|
# 2. Generate or refresh personal API token for admin
|
||||||
|
Token::API.where(user: admin).destroy_all
|
||||||
|
api_token = Token::API.create_and_return_value(admin)
|
||||||
|
puts "[Seeder] Generated new API token for admin."
|
||||||
|
|
||||||
|
# 3. Create or find dedicated MCP test project
|
||||||
|
test_project = Project.find_or_initialize_by(identifier: 'mcp-test-project')
|
||||||
|
test_project.name = 'MCP Test Project'
|
||||||
|
test_project.description = 'Dedicated test project for openproject-mcp companion server.'
|
||||||
|
test_project.public = true
|
||||||
|
test_project.workspace_type = 'project' if test_project.respond_to?(:workspace_type=)
|
||||||
|
test_project.types = Type.all if Type.any?
|
||||||
|
test_project.save!(validate: false)
|
||||||
|
puts "[Seeder] Ensured project 'MCP Test Project' (ID: #{test_project.id}, identifier: #{test_project.identifier})."
|
||||||
|
|
||||||
|
# 4. Ensure admin has project manager role in the test project
|
||||||
|
role = Role.find_by(name: 'Project admin') || Role.find_by(name: 'Manager') || Role.first
|
||||||
|
if role
|
||||||
|
membership = Member.find_or_initialize_by(project: test_project, principal: admin)
|
||||||
|
membership.roles = [role] unless membership.roles.include?(role)
|
||||||
|
membership.save!(validate: false)
|
||||||
|
puts "[Seeder] Granted admin '#{role.name}' role in '#{test_project.identifier}'."
|
||||||
|
end
|
||||||
|
|
||||||
|
# 5. Retrieve taxonomy types, statuses, and priorities
|
||||||
|
task_type = Type.find_by(name: 'Task') || Type.first
|
||||||
|
bug_type = Type.find_by(name: 'Bug') || Type.second || task_type
|
||||||
|
feature_type = Type.find_by(name: 'Feature') || Type.third || task_type
|
||||||
|
|
||||||
|
status_new = Status.find_by(name: 'New') || Status.first
|
||||||
|
status_in_progress = Status.find_by(name: 'In progress') || Status.second || status_new
|
||||||
|
status_closed = Status.find_by(name: 'Closed') || Status.last || status_new
|
||||||
|
|
||||||
|
priority_normal = IssuePriority.find_by(name: 'Normal') || IssuePriority.first
|
||||||
|
priority_high = IssuePriority.find_by(name: 'High') || IssuePriority.second || priority_normal
|
||||||
|
priority_immediate = IssuePriority.find_by(name: 'Immediate') || IssuePriority.last || priority_normal
|
||||||
|
|
||||||
|
# 6. Seed sample work packages
|
||||||
|
sample_packages = [
|
||||||
|
{
|
||||||
|
subject: 'Implement MCP Server Core Protocol',
|
||||||
|
type: task_type,
|
||||||
|
status: status_in_progress,
|
||||||
|
priority: priority_high,
|
||||||
|
description: 'Build stdio transport and wire up tool handlers using @modelcontextprotocol/sdk.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: 'Fix HAL+JSON query filter URL encoding',
|
||||||
|
type: bug_type,
|
||||||
|
status: status_new,
|
||||||
|
priority: priority_immediate,
|
||||||
|
description: 'Ensure complex filter arrays are properly stringified and encoded when fetching work packages.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: 'Add saved query execution tools',
|
||||||
|
type: feature_type,
|
||||||
|
status: status_new,
|
||||||
|
priority: priority_normal,
|
||||||
|
description: 'Allow LLMs to execute preconfigured OpenProject queries and inspect results.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: 'Initial repository bootstrap and documentation',
|
||||||
|
type: task_type,
|
||||||
|
status: status_closed,
|
||||||
|
priority: priority_normal,
|
||||||
|
description: 'Set up AGENTS.md, ARCHITECTURE.md, and initial decision records.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
created_wps = []
|
||||||
|
sample_packages.each do |data|
|
||||||
|
wp = WorkPackage.find_or_initialize_by(project: test_project, subject: data[:subject])
|
||||||
|
wp.author = admin
|
||||||
|
wp.assigned_to = admin
|
||||||
|
wp.type = data[:type]
|
||||||
|
wp.status = data[:status]
|
||||||
|
wp.priority = data[:priority]
|
||||||
|
wp.description = data[:description]
|
||||||
|
wp.save!(validate: false)
|
||||||
|
created_wps << { id: wp.id, subject: wp.subject, status: wp.status&.name, type: wp.type&.name }
|
||||||
|
end
|
||||||
|
puts "[Seeder] Seeded #{created_wps.size} work packages in '#{test_project.identifier}'."
|
||||||
|
|
||||||
|
# 7. Create saved query
|
||||||
|
query = Query.find_or_initialize_by(name: 'MCP Active Tasks', project: test_project)
|
||||||
|
query.user = admin
|
||||||
|
query.public = true if query.respond_to?(:public=)
|
||||||
|
query.include_subprojects = true if query.respond_to?(:include_subprojects=)
|
||||||
|
query.timestamps = [] if query.respond_to?(:timestamps=)
|
||||||
|
query.column_names = [:id, :subject, :status, :assigned_to] if query.respond_to?(:column_names=)
|
||||||
|
query.save!(validate: false)
|
||||||
|
puts "[Seeder] Seeded saved query 'MCP Active Tasks' (ID: #{query.id})."
|
||||||
|
|
||||||
|
# 8. Output summary payload for test automation
|
||||||
|
summary = {
|
||||||
|
status: 'ok',
|
||||||
|
baseUrl: 'http://localhost:8080',
|
||||||
|
apiKey: api_token,
|
||||||
|
adminUser: admin.login,
|
||||||
|
projectId: test_project.id,
|
||||||
|
projectIdentifier: test_project.identifier,
|
||||||
|
queryId: query.id,
|
||||||
|
workPackages: created_wps,
|
||||||
|
totalProjects: Project.count,
|
||||||
|
totalWorkPackages: WorkPackage.count
|
||||||
|
}
|
||||||
|
|
||||||
|
puts "---SEED_RESULT_START---"
|
||||||
|
puts summary.to_json
|
||||||
|
puts "---SEED_RESULT_END---"
|
||||||
|
puts "[Seeder] Completed successfully."
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# OpenProject MCP Companion - Seed Script
|
||||||
|
# Waits for OpenProject to be healthy, runs test data seeder, and saves API key.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
BASE_URL="${OPENPROJECT_BASE_URL:-http://localhost:8080}"
|
||||||
|
MAX_WAIT_SECONDS=180
|
||||||
|
INTERVAL=5
|
||||||
|
|
||||||
|
echo "[seed.sh] Waiting for OpenProject to be healthy at ${BASE_URL}..."
|
||||||
|
|
||||||
|
elapsed=0
|
||||||
|
until curl -s -f "${BASE_URL}/health_checks/default" >/dev/null 2>&1; do
|
||||||
|
if [ "$elapsed" -ge "$MAX_WAIT_SECONDS" ]; then
|
||||||
|
echo "[seed.sh] ERROR: Timed out waiting for OpenProject after ${MAX_WAIT_SECONDS} seconds."
|
||||||
|
echo "[seed.sh] Check container logs with: docker compose logs web"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[seed.sh] OpenProject not ready yet. Retrying in ${INTERVAL}s (${elapsed}s / ${MAX_WAIT_SECONDS}s)..."
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
elapsed=$((elapsed + INTERVAL))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[seed.sh] OpenProject is up and healthy!"
|
||||||
|
echo "[seed.sh] Running seed script inside container..."
|
||||||
|
|
||||||
|
SEED_OUTPUT=$(docker compose -f "${ROOT_DIR}/docker-compose.yml" exec -T web bundle exec rails runner /scripts/seed-test-data.rb)
|
||||||
|
|
||||||
|
echo "${SEED_OUTPUT}"
|
||||||
|
|
||||||
|
JSON_PAYLOAD=$(echo "${SEED_OUTPUT}" | sed -n '/---SEED_RESULT_START---/,/---SEED_RESULT_END---/p' | grep -v -- '---SEED_RESULT_')
|
||||||
|
|
||||||
|
if [ -z "${JSON_PAYLOAD}" ]; then
|
||||||
|
echo "[seed.sh] ERROR: Could not extract seed payload from output."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
API_KEY=$(python3 -c "import json, sys; print(json.loads(sys.stdin.read())['apiKey'])" <<< "${JSON_PAYLOAD}")
|
||||||
|
PROJECT_IDENTIFIER=$(python3 -c "import json, sys; print(json.loads(sys.stdin.read())['projectIdentifier'])" <<< "${JSON_PAYLOAD}")
|
||||||
|
|
||||||
|
# Update .env.test
|
||||||
|
cat <<EOF > "${ROOT_DIR}/.env.test"
|
||||||
|
# Auto-generated by scripts/seed.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
OPENPROJECT_BASE_URL=${BASE_URL}
|
||||||
|
OPENPROJECT_API_KEY=${API_KEY}
|
||||||
|
OPENPROJECT_TEST_PROJECT=${PROJECT_IDENTIFIER}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Also update .env.local if not present or populate key
|
||||||
|
if [ ! -f "${ROOT_DIR}/.env.local" ]; then
|
||||||
|
cp "${ROOT_DIR}/.env.test" "${ROOT_DIR}/.env.local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[seed.sh] Success! Test environment configured."
|
||||||
|
echo "[seed.sh] API credentials written to .env.test and .env.local"
|
||||||
|
echo "[seed.sh] Base URL: ${BASE_URL}"
|
||||||
|
echo "[seed.sh] API Key: ${API_KEY}"
|
||||||
|
echo "[seed.sh] Test Project: ${PROJECT_IDENTIFIER}"
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# OpenProject MCP Companion - Start & Seed Stack
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "[start.sh] Creating .env from .env.example..."
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[start.sh] Starting OpenProject docker compose stack..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "[start.sh] Running seed script to prepare test data and API token..."
|
||||||
|
"${SCRIPT_DIR}/seed.sh"
|
||||||
|
|
||||||
|
echo "[start.sh] OpenProject MCP test stack is ready!"
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# OpenProject MCP Companion - Stop Stack
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
echo "[stop.sh] Stopping OpenProject docker compose stack..."
|
||||||
|
docker compose -f "${ROOT_DIR}/docker-compose.yml" down
|
||||||
|
echo "[stop.sh] Stack stopped."
|
||||||
Reference in New Issue
Block a user