feat(drone): enroll drone + gitea SCM dep (M1 implementation)
Some checks failed
continuous-integration/drone/push Build is failing

- tests/gitea/recipe_meta.py: gitea as install-time dep provider; sqlite3
  overlay EXTRA_ENV, health path /api/healthz, relaxed access for CI use
- tests/drone/recipe_meta.py: DEPS=["gitea"]; health /healthz; 600s timeout
- tests/drone/install_steps.sh: wires GITEA_CLIENT_ID + GITEA_DOMAIN +
  client_secret Docker secret + DRONE_USER_CREATE before single drone deploy
- tests/drone/functional/test_scm_configured.py: Playwright-free SCM test —
  follows /login redirect, asserts final URL is gitea dep's OAuth2 authorize
  endpoint with matching client_id (per Adversary pre-probe REVIEW-drone.md)
- tests/drone/PARITY.md: backup structural-skip justified (no backupbot labels)
- runner/harness/sso.py: setup_gitea_oauth() — creates gitea admin user via
  CLI + OAuth2 app via API, returns {admin_user, admin_password, client_id,
  client_secret} for install_steps.sh consumption
- runner/run_recipe_ci.py: _enrich_deps_with_sso now handles gitea dep (calls
  setup_gitea_oauth; keycloak path unchanged)
- tests/unit/test_gitea_dep.py: unit tests for gitea dep path — meta loading,
  SSO routing, SCM redirect assertion logic (parametrized)
- machine-docs: STATUS/JOURNAL/BACKLOG-drone.md phase state files initialized

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-06-11 21:31:43 +00:00
parent 8ca5b44186
commit 51c3280163
12 changed files with 684 additions and 3 deletions

44
tests/drone/PARITY.md Normal file
View File

@ -0,0 +1,44 @@
# PARITY — drone
Tracks which lifecycle rungs are covered and why any are skipped.
## Tiers
| Tier | Status | Notes |
|------|--------|-------|
| Install | COVERED | Fresh deploy with gitea dep pre-provisioned |
| Upgrade | COVERED | 1.8.0+2.25.0 → 1.9.0+2.26.0 (two published versions; viable) |
| Backup/Restore | STRUCTURAL SKIP | See below |
| Functional | COVERED | SCM-configured test (gitea OAuth redirect) |
| Lint | COVERED | `abra recipe lint` (L5 target) |
| Screenshot | COVERED | Drone login/landing page |
## Backup rung — structural skip
**Justification:** The drone recipe declares no backupbot labels in `compose.yml` and ships
no `abra_backup*` functions in `abra.sh` (which only exports `DRONE_ENV_VERSION=v2`).
Therefore `backup_capable=False` is auto-detected by the harness — the backup rung is an
intentional structural skip, not a gap.
**Evidence:**
```
# compose.yml — no backupbot.* labels anywhere
grep -i backupbot ~/.abra/recipes/drone/compose.yml # → (no output)
# abra.sh — no backup functions
cat ~/.abra/recipes/drone/abra.sh
# → export DRONE_ENV_VERSION=v2
# (no abra_backup / abra_restore functions)
```
**Level impact:** With backup_capable=False (structural skip), the backup rung is an
EXPECTED_NA-class intentional skip. The recipe can still reach L5 if all other rungs pass,
because the backup rung's skip is declared-and-justified, not a surprise omission.
**Path to L5:** install + upgrade + functional + lint + screenshot all PASS.
## Gitea dep teardown
The gitea dep is co-deployed per run. Both gitea AND drone are torn down in the
orchestrator's `finally` block (deps in reverse order: drone first, then gitea). A drone
test failure mid-run still triggers the `finally` — the teardown guarantee is sacred.

View File

View File

@ -0,0 +1,66 @@
"""drone — SCM-configured functional test (phase drone).
Proves that drone is wired to the per-run gitea dep, not just healthy.
The negative control: a drone deployed WITHOUT DRONE_GITEA_CLIENT_ID + DRONE_GITEA_SERVER
(i.e., without compose.gitea.yml) would NOT redirect /login to the gitea dep's OAuth
authorize endpoint — it would error or redirect elsewhere. This test is therefore falsified
by a misconfigured drone.
Test: GET https://<drone>/login (following redirects) must land at the per-run gitea dep's
/login/oauth/authorize URL, and the client_id query param must match the OAuth2 app the
harness created in the gitea dep (recorded in deps["gitea"]["client_id"]).
Per the Adversary's pre-probe (REVIEW-drone.md): this redirect mechanism is the correct
SCM-configured tooth — verified against the live drone.ci.commoninternet.net instance.
"""
from __future__ import annotations
import ssl
import urllib.parse
import urllib.request
import pytest
@pytest.mark.requires_deps
def test_login_redirects_to_gitea_dep(live_app, deps):
"""Drone's /login must redirect to the per-run gitea dep's OAuth2 authorize endpoint.
Proves: (a) gitea is the SCM backend (not github or unconfigured); (b) the OAuth2
client_id matches the app the harness created in the dep gitea instance; (c) the
redirect targets the TEST-RUN gitea, not any hardcoded external provider.
"""
assert "gitea" in deps, (
f"gitea dep not in deps — dep provisioning should have populated this. "
f"Got keys: {list(deps.keys())}"
)
gitea = deps["gitea"]
gitea_domain: str = gitea["domain"]
expected_client_id: str = gitea["client_id"]
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Follow all redirects from /login — the final URL must be gitea's OAuth2 authorize page.
req = urllib.request.Request(f"https://{live_app}/login", method="GET")
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
final_url = resp.geturl()
parsed = urllib.parse.urlparse(final_url)
assert parsed.scheme == "https", f"Unexpected scheme in final URL: {final_url!r}"
assert parsed.netloc == gitea_domain, (
f"Drone /login did not redirect to the gitea dep ({gitea_domain!r}); "
f"final URL: {final_url!r} — check GITEA_DOMAIN + COMPOSE_FILE in drone's .env"
)
assert parsed.path == "/login/oauth/authorize", (
f"Final URL path is {parsed.path!r}, expected /login/oauth/authorize — "
f"drone may not have gitea SCM configured"
)
params = urllib.parse.parse_qs(parsed.query)
actual_client_id = params.get("client_id", [None])[0]
assert actual_client_id == expected_client_id, (
f"OAuth2 client_id mismatch: drone is using {actual_client_id!r} but the harness "
f"created app {expected_client_id!r} in the dep gitea — check install_steps.sh"
)

74
tests/drone/install_steps.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# drone — INSTALL-TIME gitea SCM wiring hook (rcust P2b).
#
# Runs AFTER `abra app new` + EXTRA_ENV + `abra app secret generate`, BEFORE `abra app deploy`.
# Reads the gitea dep creds from $CCCI_DEPS_FILE (written by the orchestrator's dep provisioning
# step), then:
# 1. Switches drone to gitea SCM mode (COMPOSE_FILE includes compose.gitea.yml).
# 2. Sets GITEA_CLIENT_ID + GITEA_DOMAIN in drone's .env.
# 3. Sets CLIENT_SECRET_VERSION and inserts the gitea OAuth2 client_secret as a swarm secret.
# 4. Sets DRONE_USER_CREATE so the gitea ci_admin becomes drone's first admin on login.
#
# If the deps file is absent or has no gitea entry, drone is still deployed (without SCM wiring);
# the functional/test_scm_configured.py test then FAILS, which is the correct signal.
#
# Env supplied by the harness:
# CCCI_APP_DOMAIN — the per-run drone app domain
# CCCI_APP_ENV — path to the app's .env
# CCCI_DEPS_FILE — JSON {gitea: {domain, admin_user, admin_password, client_id, client_secret}}
set -euo pipefail
: "${CCCI_APP_DOMAIN:?missing}"
ENV_PATH="${CCCI_APP_ENV:?missing}"
if [ -z "${CCCI_DEPS_FILE:-}" ] || [ ! -s "${CCCI_DEPS_FILE}" ]; then
echo " drone install_steps: no deps file — deploying drone WITHOUT gitea SCM wiring"
exit 0
fi
GITEA_DOMAIN=$(jq -r '.gitea.domain // empty' "$CCCI_DEPS_FILE")
GITEA_CLIENT_ID=$(jq -r '.gitea.client_id // empty' "$CCCI_DEPS_FILE")
GITEA_SECRET=$(jq -r '.gitea.client_secret // empty' "$CCCI_DEPS_FILE")
GITEA_ADMIN=$(jq -r '.gitea.admin_user // empty' "$CCCI_DEPS_FILE")
if [ -z "$GITEA_DOMAIN" ] || [ -z "$GITEA_CLIENT_ID" ] || [ -z "$GITEA_SECRET" ]; then
echo " drone install_steps: deps file missing gitea domain/client_id/secret — no SCM wiring"
exit 0
fi
echo " drone install_steps: wiring gitea SCM (domain=${GITEA_DOMAIN}, client_id=${GITEA_CLIENT_ID})"
# Helper: write or replace a key=value line in the drone .env file.
write_env() {
local key="$1" val="$2"
sed -i "/^\s*#\?\s*${key}=/d" "$ENV_PATH"
[ -z "$(tail -c1 "$ENV_PATH" 2>/dev/null)" ] || printf '\n' >>"$ENV_PATH"
printf '%s=%s\n' "$key" "$val" >>"$ENV_PATH"
}
# 1. Switch COMPOSE_FILE to include the gitea overlay (activates DRONE_GITEA_CLIENT_ID +
# DRONE_GITEA_SERVER env and the client_secret swarm secret).
write_env COMPOSE_FILE "compose.yml:compose.gitea.yml"
# 2. Wire gitea identity into drone's .env.
write_env GITEA_CLIENT_ID "$GITEA_CLIENT_ID"
write_env GITEA_DOMAIN "$GITEA_DOMAIN"
# 3. Insert the gitea OAuth2 client_secret as a swarm secret at version v1.
# The secret does not exist yet (abra secret generate only creates secrets declared in the
# active COMPOSE_FILE; we just switched to compose.gitea.yml which adds client_secret).
write_env CLIENT_SECRET_VERSION "v1"
INSERT_LOG=$(abra app secret insert "$CCCI_APP_DOMAIN" client_secret v1 "$GITEA_SECRET" --no-input -C -o 2>&1) ||
INSERT_LOG=$(script -qec "abra app secret insert $CCCI_APP_DOMAIN client_secret v1 $GITEA_SECRET --no-input -C -o" /dev/null 2>&1) ||
{
echo " drone install_steps: abra app secret insert client_secret@v1 failed: $INSERT_LOG"
exit 1
}
echo " drone install_steps: client_secret inserted at v1"
# 4. DRONE_USER_CREATE: when ci_admin first logs in via gitea OAuth, drone promotes them to admin.
# Uses the gitea admin username from the dep provisioning step.
ADMIN_USER="${GITEA_ADMIN:-ci_admin}"
write_env DRONE_USER_CREATE "username:${ADMIN_USER},admin:true"
echo " drone install_steps: gitea SCM wired (DRONE_USER_CREATE=username:${ADMIN_USER},admin:true)"

View File

@ -0,0 +1,16 @@
# Per-recipe harness config for drone (CI server with gitea SCM dependency).
# Drone requires a gitea SCM backend to boot; the harness provisions gitea as an install-time
# dep, creates an admin user + OAuth2 app in it, and wires DRONE_GITEA_* via install_steps.sh
# before the single drone deploy. Upgrade tier: viable (1.8.0 → 1.9.0).
#
# The backup rung is a structural skip: the drone recipe ships no backupbot labels and abra.sh
# exports only DRONE_ENV_VERSION (no backup functions). Documented in PARITY.md.
HEALTH_PATH = "/healthz"
HEALTH_OK = (200,)
DEPLOY_TIMEOUT = 600
HTTP_TIMEOUT = 600
# Gitea is deployed as an install-time dep. The orchestrator provisions it before drone, runs
# install_steps.sh (which wires GITEA_CLIENT_ID + GITEA_DOMAIN + client_secret into drone's env
# and compose), then deploys drone once with SCM already configured.
DEPS = ["gitea"]