Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions deploy/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,25 @@ services:
- "${FORGEJO_TEST_HTTP_PORT:-3000}:3000"
restart: unless-stopped

runner:
image: data.forgejo.org/forgejo/runner:13
profiles: ["test-runner"]
user: "0:0"
working_dir: /data
environment:
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner-data:/data
- ${FMCP_RUNNER_CONFIG_FILE:-./secrets/runner-config.yml}:/data/runner-config.yml:ro
command: forgejo-runner daemon --config /data/runner-config.yml
restart: unless-stopped
depends_on:
forgejo:
condition: service_started

volumes:
postgres-data:
forgejo-data:
forgejo-config:
runner-data:
40 changes: 37 additions & 3 deletions scripts/test-full-docker-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export FMCP_ALLOW_INSECURE_FORGEJO_HTTP=true
export FMCP_ENVIRONMENT=test
export FMCP_ADMIN_PASSWORD_FILE="$temporary_dir/admin_password"
export FMCP_CREDENTIAL_KEY_FILE="$temporary_dir/credential_key"
export FMCP_RUNNER_CONFIG_FILE="$temporary_dir/runner-config.yml"
export FMCP_E2E_ADMIN_PASSWORD="Admin-E2E-pass-123!"
export FMCP_E2E_DEVELOPER_PASSWORD="Developer-E2E-pass-123!"
export FMCP_E2E_REVIEWER_PASSWORD="Reviewer-E2E-pass-123!"
Expand All @@ -44,22 +45,23 @@ PY
chmod 0600 "$FMCP_ADMIN_PASSWORD_FILE" "$FMCP_CREDENTIAL_KEY_FILE"

compose() {
docker compose -p "$project" -f deploy/compose.yaml --profile test-forgejo "$@"
docker compose -p "$project" -f deploy/compose.yaml \
--profile test-forgejo --profile test-runner "$@"
}

cleanup() {
status=$?
if [ "$status" -ne 0 ]; then
echo "Full Docker E2E failed; recent service logs:" >&2
compose logs --tail=120 app postgres forgejo >&2 || true
compose logs --tail=120 app postgres forgejo runner >&2 || true
fi
compose down -v --remove-orphans --rmi local >/dev/null 2>&1 || true
rm -rf "$temporary_dir"
exit "$status"
}
trap cleanup EXIT INT TERM

compose up -d --build
compose up -d --build app postgres forgejo

ready=false
for _ in $(seq 1 120); do
Expand Down Expand Up @@ -107,4 +109,36 @@ compose exec -T forgejo forgejo admin user create \
--email reviewer@example.test \
--must-change-password=false >/dev/null

runner_secret=$(uv run python -c 'import secrets; print(secrets.token_hex(20))')
runner_uuid=$(compose exec -T forgejo forgejo forgejo-cli actions register \
--name full-docker-e2e --secret "$runner_secret" | tr -d '\r\n')
if [ -z "$runner_uuid" ]; then
echo "Forgejo runner registration did not return a UUID" >&2
exit 1
fi
cat > "$FMCP_RUNNER_CONFIG_FILE" <<EOF
log:
level: info
runner:
capacity: 1
timeout: 5m
shutdown_timeout: 30s
fetch_timeout: 10s
fetch_interval: 1s
report_interval: 1s
labels:
- docker:docker://docker.io/library/alpine:3.20
container:
network: ${project}_default
docker_host: "-"
server:
connections:
e2e:
url: http://forgejo:3000/
uuid: $runner_uuid
token: $runner_secret
EOF
chmod 0644 "$FMCP_RUNNER_CONFIG_FILE"
compose up -d runner

uv run python tests/e2e/full_docker_flow.py
109 changes: 107 additions & 2 deletions tests/e2e/full_docker_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,12 @@ def run_mcp_flow(mcp_tokens: dict[str, str]) -> None:
"path": ".forgejo/workflows/ci.yml",
"content": (
"name: CI\non:\n workflow_dispatch:\n"
" inputs:\n delay:\n required: false\n"
" default: '0'\n"
"jobs:\n test:\n runs-on: docker\n"
" steps:\n - run: echo ok\n"
" steps:\n - name: Produce result\n run: |\n"
" sleep '${{ inputs.delay }}'\n"
" echo ok | tee result.txt\n"
),
},
],
Expand Down Expand Up @@ -569,6 +573,19 @@ def run_mcp_flow(mcp_tokens: dict[str, str]) -> None:
{**repository, "ref": feature["commit_sha"]},
)
assert status["state"] in {"pending", "success", "failure", "error", "warning"}

# Forgejo calculates PR mergeability asynchronously. A merge attempted while
# that check is pending returns HTTP 405 ("Please try again later").
for _ in range(30):
merge_candidate = developer.call(
"forgejo_get_pull_request", {**repository, "number": pull["number"]}
)
if merge_candidate["mergeable"] is True:
break
time.sleep(1)
else:
raise RuntimeError("pull request did not become mergeable within 30 seconds")

developer.call(
"forgejo_merge_pull_request",
{
Expand All @@ -590,8 +607,96 @@ def run_mcp_flow(mcp_tokens: dict[str, str]) -> None:
assert merged_status == {"number": pull["number"], "merged": True}
developer.call(
"forgejo_dispatch_workflow",
{**repository, "workflow": "ci.yml", "ref": "main", "inputs": {}},
{
**repository,
"workflow": "ci.yml",
"ref": "main",
"inputs": {"delay": "0"},
},
)
completed_run: dict[str, Any] | None = None
for _ in range(120):
action_runs = developer.call(
"forgejo_list_action_runs",
{
**repository,
"event": ["workflow_dispatch"],
"workflow_id": "ci.yml",
"limit": 30,
},
)
if action_runs["items"]:
candidate = action_runs["items"][0]
if candidate["status"] == "success":
completed_run = candidate
break
if candidate["status"] in {"failure", "cancelled", "skipped", "blocked"}:
raise RuntimeError(f"action run ended with unexpected status {candidate['status']}")
time.sleep(1)
if completed_run is None:
raise RuntimeError("action run did not complete within 120 seconds")

run_id = completed_run["id"]
loaded_run = developer.call("forgejo_get_action_run", {**repository, "run_id": run_id})
assert loaded_run["status"] == "success"
jobs = developer.call("forgejo_list_action_run_jobs", {**repository, "run_id": run_id})
assert len(jobs["items"]) == 1
job = jobs["items"][0]
assert job["status"] == "success"
job_log = developer.call(
"forgejo_get_action_job_log",
{
**repository,
"job_id": job["id"],
"attempt": job["attempt"],
},
)
assert "ok" in job_log["content"]
run_logs = developer.call("forgejo_get_action_run_logs", {**repository, "run_id": run_id})
assert run_logs["files"]
artifacts = developer.call(
"forgejo_list_action_run_artifacts",
{**repository, "run_id": run_id, "limit": 30},
)
assert artifacts["items"] == []

developer.call(
"forgejo_dispatch_workflow",
{
**repository,
"workflow": "ci.yml",
"ref": "main",
"inputs": {"delay": "60"},
},
)
cancellable_run: dict[str, Any] | None = None
for _ in range(30):
action_runs = developer.call(
"forgejo_list_action_runs",
{**repository, "event": ["workflow_dispatch"], "limit": 30},
)
cancellable_run = next(
(item for item in action_runs["items"] if item["id"] != run_id), None
)
if cancellable_run is not None:
break
time.sleep(1)
if cancellable_run is None:
raise RuntimeError("second action run did not appear within 30 seconds")
cancelled_run_id = cancellable_run["id"]
developer.call("forgejo_cancel_action_run", {**repository, "run_id": cancelled_run_id})
for _ in range(30):
cancelled_run = developer.call(
"forgejo_get_action_run", {**repository, "run_id": cancelled_run_id}
)
if cancelled_run["status"] == "cancelled":
break
time.sleep(1)
else:
raise RuntimeError("action run did not become cancelled within 30 seconds")
developer.call("forgejo_delete_action_run", {**repository, "run_id": cancelled_run_id})
print("PASS MCP Actions runs, jobs, logs, artifacts, cancellation, and deletion")

tag = developer.call(
"forgejo_create_tag",
{
Expand Down
Loading