remove: scrub all woodpecker references from codebase
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m14s
CI / skinny-install (bib) (push) Successful in 5m47s
CI / skinny-install (api) (push) Successful in 37s
CI / skinny-install (bcda) (push) Successful in 37s
CI / skinny-install (bls) (push) Successful in 40s
CI / skinny-install (ccw) (push) Successful in 41s
CI / skinny-install (cli) (push) Successful in 42s
CI / skinny-install (cms) (push) Successful in 39s
CI / skinny-install (conf) (push) Successful in 41s
CI / skinny-install (opps) (push) Successful in 39s
CI / skinny-install (perf) (push) Successful in 40s
CI / skinny-install (pfs) (push) Successful in 50s
CI / skinny-install (rex) (push) Successful in 33s
Deploy / build-scan-report (push) Failing after 12m6s
Infra CI / notebooks (push) Successful in 22s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Successful in 11s
Infra CI / api (push) Successful in 12s
Infra CI / mc (push) Successful in 22s
CI / lint-test (push) Failing after 41m1s
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m14s
CI / skinny-install (bib) (push) Successful in 5m47s
CI / skinny-install (api) (push) Successful in 37s
CI / skinny-install (bcda) (push) Successful in 37s
CI / skinny-install (bls) (push) Successful in 40s
CI / skinny-install (ccw) (push) Successful in 41s
CI / skinny-install (cli) (push) Successful in 42s
CI / skinny-install (cms) (push) Successful in 39s
CI / skinny-install (conf) (push) Successful in 41s
CI / skinny-install (opps) (push) Successful in 39s
CI / skinny-install (perf) (push) Successful in 40s
CI / skinny-install (pfs) (push) Successful in 50s
CI / skinny-install (rex) (push) Successful in 33s
Deploy / build-scan-report (push) Failing after 12m6s
Infra CI / notebooks (push) Successful in 22s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Successful in 11s
Infra CI / api (push) Successful in 12s
Infra CI / mc (push) Successful in 22s
CI / lint-test (push) Failing after 41m1s
Clean 17 files across src/, tests/, dev/, stack.toml, deploy.sh: - api/auth/provision.py: remove WoodpeckerClient, provision_woodpecker, _get_woodpecker_token, woodpecker field from ProvisionResult - api/auth/manifest.py: remove woodpecker from CREDENTIALS + Provisioner - api/diag: remove woodpecker log fetching - sem/hooks.py: remove woodpecker sync step - stack.toml: remove [services.woodpecker] config - deploy.sh: remove woodpecker deploy steps - dev/scripts: remove woodpecker from config gen, secrets, readme - tests: remove all woodpecker assertions and test cases Zero woodpecker references remain in the codebase.
This commit is contained in:
@@ -125,9 +125,7 @@ up mc docs api notebooks zotero
|
|||||||
|
|
||||||
# ── Tier 3: CI (depends on Gitea) ──────────────────────────────────
|
# ── Tier 3: CI (depends on Gitea) ──────────────────────────────────
|
||||||
log "Tier 3: CI"
|
log "Tier 3: CI"
|
||||||
up woodpecker-server act-runner
|
up act-runner
|
||||||
wait_running woodpecker-server
|
|
||||||
up woodpecker-agent
|
|
||||||
|
|
||||||
# ── Tier 4: Observability ──────────────────────────────────────────
|
# ── Tier 4: Observability ──────────────────────────────────────────
|
||||||
log "Tier 4: Observability"
|
log "Tier 4: Observability"
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
Run once after cloning the repo to set up all credentials across:
|
Run once after cloning the repo to set up all credentials across:
|
||||||
- .env file (all secrets)
|
- .env file (all secrets)
|
||||||
- PostgreSQL roles and databases
|
- PostgreSQL roles and databases
|
||||||
- Woodpecker CI repo secrets
|
|
||||||
- RustFS IAM (via mc)
|
- RustFS IAM (via mc)
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
- docker compose services running (postgres, gitea, rustfs, woodpecker-server)
|
- docker compose services running (postgres, gitea, rustfs)
|
||||||
- No prior .env (or willing to overwrite managed keys)
|
- No prior .env (or willing to overwrite managed keys)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -35,12 +34,10 @@ SECRETS = {
|
|||||||
"POSTGRES_PASSWORD": 24,
|
"POSTGRES_PASSWORD": 24,
|
||||||
"GITEA_DB_PASSWORD": 24,
|
"GITEA_DB_PASSWORD": 24,
|
||||||
"GITEA_ADMIN_PASSWORD": 24,
|
"GITEA_ADMIN_PASSWORD": 24,
|
||||||
"WOODPECKER_DB_PASSWORD": 24,
|
|
||||||
"NESSIE_DB_PASSWORD": 24,
|
"NESSIE_DB_PASSWORD": 24,
|
||||||
"POLARIS_DB_PASSWORD": 24,
|
"POLARIS_DB_PASSWORD": 24,
|
||||||
"RUSTFS_ACCESS_KEY": 32,
|
"RUSTFS_ACCESS_KEY": 32,
|
||||||
"RUSTFS_SECRET_KEY": 32,
|
"RUSTFS_SECRET_KEY": 32,
|
||||||
"WOODPECKER_AGENT_SECRET": 32,
|
|
||||||
"POLARIS_ROOT_SECRET": 24,
|
"POLARIS_ROOT_SECRET": 24,
|
||||||
"GF_ADMIN_PASSWORD": 24,
|
"GF_ADMIN_PASSWORD": 24,
|
||||||
"ROOT_KEY": 32,
|
"ROOT_KEY": 32,
|
||||||
@@ -50,29 +47,16 @@ SECRETS = {
|
|||||||
HEX_SECRETS = {
|
HEX_SECRETS = {
|
||||||
"RUSTFS_ACCESS_KEY",
|
"RUSTFS_ACCESS_KEY",
|
||||||
"RUSTFS_SECRET_KEY",
|
"RUSTFS_SECRET_KEY",
|
||||||
"WOODPECKER_AGENT_SECRET",
|
|
||||||
"ROOT_KEY",
|
"ROOT_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
# PostgreSQL roles: role_name → (env_var for password, database_name)
|
# PostgreSQL roles: role_name → (env_var for password, database_name)
|
||||||
PG_ROLES = {
|
PG_ROLES = {
|
||||||
"git": ("GITEA_DB_PASSWORD", "gitea"),
|
"git": ("GITEA_DB_PASSWORD", "gitea"),
|
||||||
"woodpecker": ("WOODPECKER_DB_PASSWORD", "woodpecker"),
|
|
||||||
"nessie": ("NESSIE_DB_PASSWORD", "nessie"),
|
"nessie": ("NESSIE_DB_PASSWORD", "nessie"),
|
||||||
"polaris": ("POLARIS_DB_PASSWORD", "polaris"),
|
"polaris": ("POLARIS_DB_PASSWORD", "polaris"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Woodpecker secrets: wp_secret_name → env_var
|
|
||||||
WP_SECRETS = {
|
|
||||||
"registry_user": ("kert", None), # static value, not from .env
|
|
||||||
"registry_pass": (None, "GITEA_ADMIN_PASSWORD"),
|
|
||||||
"gitea_token": (None, "GITEA_TOKEN"),
|
|
||||||
"gitea_url": (None, "_GITEA_SERVICE_URL"), # resolved from cfg below
|
|
||||||
"s3_access_key": (None, "RUSTFS_ACCESS_KEY"),
|
|
||||||
"s3_secret_key": (None, "RUSTFS_SECRET_KEY"),
|
|
||||||
"root_key": (None, "ROOT_KEY"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alias mappings: these .env vars copy from another
|
# Alias mappings: these .env vars copy from another
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
"GITEA_S3_ACCESS_KEY": "RUSTFS_ACCESS_KEY",
|
"GITEA_S3_ACCESS_KEY": "RUSTFS_ACCESS_KEY",
|
||||||
@@ -224,46 +208,6 @@ def bootstrap_gitea(env: dict[str, str]) -> list[str]:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def bootstrap_woodpecker(env: dict[str, str]) -> list[str]:
|
|
||||||
"""Set Woodpecker repo secrets via database."""
|
|
||||||
from conf import cfg
|
|
||||||
|
|
||||||
env.setdefault("_GITEA_SERVICE_URL", cfg.services.gitea)
|
|
||||||
errors = []
|
|
||||||
wp_pw = env.get("WOODPECKER_DB_PASSWORD", "")
|
|
||||||
for name, (static_val, env_var) in WP_SECRETS.items():
|
|
||||||
val = static_val if static_val else env.get(env_var, "")
|
|
||||||
if not val:
|
|
||||||
errors.append(f"No value for WP secret {name}")
|
|
||||||
continue
|
|
||||||
events = '["push","pull_request","cron","manual","deployment","tag","release"]'
|
|
||||||
r = subprocess.run(
|
|
||||||
[
|
|
||||||
"docker",
|
|
||||||
"exec",
|
|
||||||
"-e",
|
|
||||||
f"PGPASSWORD={wp_pw}",
|
|
||||||
"postgres",
|
|
||||||
"psql",
|
|
||||||
"-U",
|
|
||||||
"woodpecker",
|
|
||||||
"-d",
|
|
||||||
"woodpecker",
|
|
||||||
"-c",
|
|
||||||
f"INSERT INTO secrets (repo_id, name, value, events) "
|
|
||||||
f"VALUES (1, '{name}', '{val}', '{events}') "
|
|
||||||
f"ON CONFLICT (repo_id, name) DO UPDATE SET value='{val}', events='{events}';",
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if r.returncode != 0:
|
|
||||||
errors.append(f"WP secret {name}: {r.stderr.strip()}")
|
|
||||||
else:
|
|
||||||
log.info("WP secret %s set", name)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def verify(env: dict[str, str]) -> list[str]:
|
def verify(env: dict[str, str]) -> list[str]:
|
||||||
"""Verify all secrets are consistent across systems."""
|
"""Verify all secrets are consistent across systems."""
|
||||||
errors = []
|
errors = []
|
||||||
@@ -342,7 +286,7 @@ def verify(env: dict[str, str]) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def bootstrap() -> int:
|
def bootstrap() -> int:
|
||||||
"""Full bootstrap: generate → postgres → gitea → woodpecker → verify."""
|
"""Full bootstrap: generate → postgres → gitea → verify."""
|
||||||
env = generate_secrets()
|
env = generate_secrets()
|
||||||
_write_env(env, ENV_PATH)
|
_write_env(env, ENV_PATH)
|
||||||
log.info("Wrote %d keys to %s", len(env), ENV_PATH)
|
log.info("Wrote %d keys to %s", len(env), ENV_PATH)
|
||||||
@@ -351,7 +295,6 @@ def bootstrap() -> int:
|
|||||||
all_errors.extend(bootstrap_postgres(env))
|
all_errors.extend(bootstrap_postgres(env))
|
||||||
all_errors.extend(bootstrap_gitea(env))
|
all_errors.extend(bootstrap_gitea(env))
|
||||||
_write_env(env, ENV_PATH) # re-write with GITEA_TOKEN
|
_write_env(env, ENV_PATH) # re-write with GITEA_TOKEN
|
||||||
all_errors.extend(bootstrap_woodpecker(env))
|
|
||||||
|
|
||||||
# Verify
|
# Verify
|
||||||
log.info("Verifying...")
|
log.info("Verifying...")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Generate all derived config files from stack.toml.
|
"""Generate all derived config files from stack.toml.
|
||||||
|
|
||||||
Reads [platform], [services], [images], and [ci] from stack.toml.
|
Reads [platform], [services], [images], and [ci] from stack.toml.
|
||||||
Dispatches to the active CI backend emitter (woodpecker or github)
|
Dispatches to the active CI backend emitter (gitea or github)
|
||||||
for pipeline YAML, plus generates coredns config.
|
for pipeline YAML, plus generates coredns config.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
@@ -81,9 +81,8 @@ def gen_coredns() -> dict[str, str]:
|
|||||||
# ── Backend dispatch ─────────────────────────────────────────────
|
# ── Backend dispatch ─────────────────────────────────────────────
|
||||||
|
|
||||||
STALE_DIRS: dict[str, list[str]] = {
|
STALE_DIRS: dict[str, list[str]] = {
|
||||||
"woodpecker": [".github/workflows", ".gitea/workflows"],
|
"github": [".gitea/workflows"],
|
||||||
"github": [".woodpecker", ".gitea/workflows"],
|
"gitea": [".github/workflows"],
|
||||||
"gitea": [".woodpecker", ".github/workflows"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,9 +97,7 @@ def _emit_backend(backend: str) -> dict[str, str]:
|
|||||||
# Propagate top-level coverage_threshold so each backend can read it.
|
# Propagate top-level coverage_threshold so each backend can read it.
|
||||||
ci_cfg.setdefault("coverage_threshold", ci_data.get("coverage_threshold", 99))
|
ci_cfg.setdefault("coverage_threshold", ci_data.get("coverage_threshold", 99))
|
||||||
|
|
||||||
if backend == "woodpecker":
|
if backend == "github":
|
||||||
from backends.woodpecker import emit
|
|
||||||
elif backend == "github":
|
|
||||||
from backends.github import emit
|
from backends.github import emit
|
||||||
elif backend == "gitea":
|
elif backend == "gitea":
|
||||||
from backends.gitea import emit
|
from backends.gitea import emit
|
||||||
@@ -147,7 +144,7 @@ def _emit_dab(*, sql: bool = False) -> dict[str, str]:
|
|||||||
|
|
||||||
def generate(backend: str | None = None, *, dab_sql: bool = False) -> dict[str, str]:
|
def generate(backend: str | None = None, *, dab_sql: bool = False) -> dict[str, str]:
|
||||||
if backend is None:
|
if backend is None:
|
||||||
backend = cfg._data.get("ci", {}).get("backend", "woodpecker")
|
backend = cfg._data.get("ci", {}).get("backend", "gitea")
|
||||||
|
|
||||||
files: dict[str, str] = {}
|
files: dict[str, str] = {}
|
||||||
files.update(gen_coredns())
|
files.update(gen_coredns())
|
||||||
@@ -218,7 +215,7 @@ def main() -> int:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--backend",
|
"--backend",
|
||||||
default=None,
|
default=None,
|
||||||
help="Override CI backend (gitea|github|woodpecker)",
|
help="Override CI backend (gitea|github)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dab-sql",
|
"--dab-sql",
|
||||||
@@ -251,7 +248,7 @@ def main() -> int:
|
|||||||
write_all(files)
|
write_all(files)
|
||||||
|
|
||||||
# Clean stale files from inactive backend
|
# Clean stale files from inactive backend
|
||||||
backend = args.backend or cfg._data.get("ci", {}).get("backend", "woodpecker")
|
backend = args.backend or cfg._data.get("ci", {}).get("backend", "gitea")
|
||||||
stale = _clean_stale(backend, files)
|
stale = _clean_stale(backend, files)
|
||||||
for s in stale:
|
for s in stale:
|
||||||
(ROOT / s).unlink()
|
(ROOT / s).unlink()
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ def _collect_workflows() -> dict:
|
|||||||
workflow_dirs = [
|
workflow_dirs = [
|
||||||
ROOT / ".gitea" / "workflows",
|
ROOT / ".gitea" / "workflows",
|
||||||
ROOT / ".github" / "workflows",
|
ROOT / ".github" / "workflows",
|
||||||
ROOT / ".woodpecker",
|
|
||||||
]
|
]
|
||||||
workflows = []
|
workflows = []
|
||||||
for d in workflow_dirs:
|
for d in workflow_dirs:
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""Two-phase deploy: rotate backends FIRST, then restart services.
|
"""Two-phase deploy: rotate backends FIRST, then restart services.
|
||||||
|
|
||||||
Called from the Woodpecker deploy step to ensure credential consistency:
|
Called from the Gitea Actions deploy step to ensure credential consistency:
|
||||||
1. Derive new credentials from ROOT_KEY + commit SHA
|
1. Derive new credentials from ROOT_KEY + commit SHA
|
||||||
2. Rotate PostgreSQL passwords (ALTER ROLE)
|
2. Rotate PostgreSQL passwords (ALTER ROLE)
|
||||||
3. Rotate Gitea admin password + create fresh token
|
3. Rotate Gitea admin password + create fresh token
|
||||||
4. Write .env with all new credentials
|
4. Write .env with all new credentials
|
||||||
5. Restart services (docker compose up -d)
|
5. Restart services (docker compose up -d)
|
||||||
6. Sync Woodpecker secrets
|
6. Verify all backends are healthy
|
||||||
7. Verify all backends are healthy
|
7. Rollback on failure
|
||||||
8. Rollback on failure
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
uv run python -m api.auth deploy <commit_sha>
|
uv run python -m api.auth deploy <commit_sha>
|
||||||
@@ -129,7 +128,7 @@ def deploy(
|
|||||||
|
|
||||||
Phase 1: Rotate backend credentials (ALTER ROLE, Gitea API)
|
Phase 1: Rotate backend credentials (ALTER ROLE, Gitea API)
|
||||||
Phase 2: Write .env + docker compose up -d
|
Phase 2: Write .env + docker compose up -d
|
||||||
Phase 3: Sync Woodpecker secrets + health check
|
Phase 3: Health check
|
||||||
"""
|
"""
|
||||||
result = ProvisionResult()
|
result = ProvisionResult()
|
||||||
cwd = compose_dir or env_path.parent
|
cwd = compose_dir or env_path.parent
|
||||||
|
|||||||
@@ -49,13 +49,6 @@ CREDENTIALS: tuple[Credential, ...] = (
|
|||||||
Format.PASSWORD,
|
Format.PASSWORD,
|
||||||
Provisioner.POSTGRES,
|
Provisioner.POSTGRES,
|
||||||
),
|
),
|
||||||
Credential(
|
|
||||||
"WOODPECKER_DB_PASSWORD",
|
|
||||||
"postgres/woodpecker",
|
|
||||||
Tier.SERVICE,
|
|
||||||
Format.PASSWORD,
|
|
||||||
Provisioner.POSTGRES,
|
|
||||||
),
|
|
||||||
Credential(
|
Credential(
|
||||||
"NESSIE_DB_PASSWORD",
|
"NESSIE_DB_PASSWORD",
|
||||||
"postgres/nessie",
|
"postgres/nessie",
|
||||||
@@ -133,28 +126,6 @@ CREDENTIALS: tuple[Credential, ...] = (
|
|||||||
Provisioner.ENV_ONLY,
|
Provisioner.ENV_ONLY,
|
||||||
alias_of="RUSTFS_SECRET_KEY",
|
alias_of="RUSTFS_SECRET_KEY",
|
||||||
),
|
),
|
||||||
# ── Woodpecker ───────────────────────────────────────────
|
|
||||||
Credential(
|
|
||||||
"WOODPECKER_AGENT_SECRET",
|
|
||||||
"woodpecker/agent",
|
|
||||||
Tier.SERVICE,
|
|
||||||
Format.HEX,
|
|
||||||
Provisioner.ENV_ONLY,
|
|
||||||
),
|
|
||||||
Credential(
|
|
||||||
"WOODPECKER_GITEA_CLIENT",
|
|
||||||
"woodpecker/oauth-client",
|
|
||||||
Tier.BOOTSTRAP,
|
|
||||||
Format.HEX,
|
|
||||||
Provisioner.SKIP,
|
|
||||||
),
|
|
||||||
Credential(
|
|
||||||
"WOODPECKER_GITEA_SECRET",
|
|
||||||
"woodpecker/oauth-secret",
|
|
||||||
Tier.BOOTSTRAP,
|
|
||||||
Format.HEX,
|
|
||||||
Provisioner.SKIP,
|
|
||||||
),
|
|
||||||
# ── Others ───────────────────────────────────────────────
|
# ── Others ───────────────────────────────────────────────
|
||||||
Credential(
|
Credential(
|
||||||
"POLARIS_ROOT_SECRET",
|
"POLARIS_ROOT_SECRET",
|
||||||
@@ -186,14 +157,12 @@ MANAGED_VARS: frozenset[str] = frozenset(c.env_var for c in CREDENTIALS)
|
|||||||
|
|
||||||
POSTGRES_ROLES: dict[str, str] = {
|
POSTGRES_ROLES: dict[str, str] = {
|
||||||
"git": "GITEA_DB_PASSWORD",
|
"git": "GITEA_DB_PASSWORD",
|
||||||
"woodpecker": "WOODPECKER_DB_PASSWORD",
|
|
||||||
"nessie": "NESSIE_DB_PASSWORD",
|
"nessie": "NESSIE_DB_PASSWORD",
|
||||||
"polaris": "POLARIS_DB_PASSWORD",
|
"polaris": "POLARIS_DB_PASSWORD",
|
||||||
}
|
}
|
||||||
|
|
||||||
POSTGRES_DATABASES: dict[str, str] = {
|
POSTGRES_DATABASES: dict[str, str] = {
|
||||||
"git": "gitea",
|
"git": "gitea",
|
||||||
"woodpecker": "woodpecker",
|
|
||||||
"nessie": "nessie",
|
"nessie": "nessie",
|
||||||
"polaris": "polaris",
|
"polaris": "polaris",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from api.auth.manifest import (
|
|||||||
Tier,
|
Tier,
|
||||||
)
|
)
|
||||||
from api.clients.gitea import GiteaClient
|
from api.clients.gitea import GiteaClient
|
||||||
from api.clients.woodpecker import WoodpeckerClient
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@ class ProvisionResult:
|
|||||||
|
|
||||||
postgres: bool = False
|
postgres: bool = False
|
||||||
gitea: bool = False
|
gitea: bool = False
|
||||||
woodpecker: bool = False
|
|
||||||
env_written: bool = False
|
env_written: bool = False
|
||||||
errors: list[tuple[str, Exception]] = field(default_factory=list)
|
errors: list[tuple[str, Exception]] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -252,82 +250,6 @@ def provision_gitea(
|
|||||||
return resp["sha1"]
|
return resp["sha1"]
|
||||||
|
|
||||||
|
|
||||||
# ── Woodpecker secret sync ──────────────────────────────────────
|
|
||||||
|
|
||||||
WOODPECKER_SECRET_MAP = {
|
|
||||||
"gitea_token": "GITEA_TOKEN",
|
|
||||||
"registry_pass": "GITEA_ADMIN_PASSWORD",
|
|
||||||
"s3_access_key": "RUSTFS_ACCESS_KEY",
|
|
||||||
"s3_secret_key": "RUSTFS_SECRET_KEY",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_woodpecker_token(
|
|
||||||
values: dict[str, str], *, container: str = "postgres"
|
|
||||||
) -> str:
|
|
||||||
"""Read Woodpecker user API token from its database.
|
|
||||||
|
|
||||||
Woodpecker authenticates via Gitea OAuth. Its REST API uses the
|
|
||||||
user's `hash` field from the woodpecker DB, not a Gitea API token.
|
|
||||||
"""
|
|
||||||
wp_db_pw = values.get("WOODPECKER_DB_PASSWORD", "")
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"docker",
|
|
||||||
"exec",
|
|
||||||
"-e",
|
|
||||||
f"PGPASSWORD={wp_db_pw}",
|
|
||||||
container,
|
|
||||||
"psql",
|
|
||||||
"-U",
|
|
||||||
"woodpecker",
|
|
||||||
"-d",
|
|
||||||
"woodpecker",
|
|
||||||
"-t",
|
|
||||||
"-A",
|
|
||||||
"-c",
|
|
||||||
"SELECT hash FROM users WHERE login='kert' LIMIT 1",
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
token = result.stdout.strip()
|
|
||||||
if not token:
|
|
||||||
raise ValueError("Could not read Woodpecker token from DB")
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def provision_woodpecker(
|
|
||||||
values: dict[str, str],
|
|
||||||
*,
|
|
||||||
repo_id: int = 1,
|
|
||||||
base_url: str = "",
|
|
||||||
) -> None:
|
|
||||||
"""Sync rotated credentials to Woodpecker repo secrets."""
|
|
||||||
if not base_url:
|
|
||||||
from conf import cfg
|
|
||||||
|
|
||||||
base_url = f"{cfg.services.woodpecker}/api"
|
|
||||||
token = _get_woodpecker_token(values)
|
|
||||||
|
|
||||||
wp = WoodpeckerClient(token, base_url=base_url)
|
|
||||||
for secret_name, env_var in WOODPECKER_SECRET_MAP.items():
|
|
||||||
val = values.get(env_var, "")
|
|
||||||
if not val:
|
|
||||||
log.warning("Skipping Woodpecker secret %s — no value", secret_name)
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
wp.update_secret(repo_id, secret_name, {"value": val})
|
|
||||||
log.info("Updated Woodpecker secret: %s", secret_name)
|
|
||||||
except Exception:
|
|
||||||
wp.create_secret(
|
|
||||||
repo_id,
|
|
||||||
{"name": secret_name, "value": val, "events": ["push"]},
|
|
||||||
)
|
|
||||||
log.info("Created Woodpecker secret: %s", secret_name)
|
|
||||||
wp.close()
|
|
||||||
|
|
||||||
|
|
||||||
# ── Gitea Actions secret sync ──────────────────────────────────
|
# ── Gitea Actions secret sync ──────────────────────────────────
|
||||||
|
|
||||||
# Gitea reserves the GITEA_ prefix for Actions secrets, so the token
|
# Gitea reserves the GITEA_ prefix for Actions secrets, so the token
|
||||||
@@ -408,14 +330,6 @@ def provision(
|
|||||||
result.errors.append(("gitea", e))
|
result.errors.append(("gitea", e))
|
||||||
log.error("Gitea provisioning failed: %s", e)
|
log.error("Gitea provisioning failed: %s", e)
|
||||||
|
|
||||||
try:
|
|
||||||
_retry(lambda: provision_woodpecker(values), "woodpecker")
|
|
||||||
result.woodpecker = True
|
|
||||||
log.info("Woodpecker secrets synced")
|
|
||||||
except Exception as e:
|
|
||||||
result.errors.append(("woodpecker", e))
|
|
||||||
log.error("Woodpecker secret sync failed: %s", e)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_retry(lambda: provision_gitea_actions(values), "gitea-actions")
|
_retry(lambda: provision_gitea_actions(values), "gitea-actions")
|
||||||
log.info("Gitea Actions secrets synced")
|
log.info("Gitea Actions secrets synced")
|
||||||
@@ -457,13 +371,6 @@ def bootstrap(
|
|||||||
result.errors.append(("gitea", e))
|
result.errors.append(("gitea", e))
|
||||||
log.error("Gitea bootstrap failed: %s", e)
|
log.error("Gitea bootstrap failed: %s", e)
|
||||||
|
|
||||||
try:
|
|
||||||
provision_woodpecker(values)
|
|
||||||
result.woodpecker = True
|
|
||||||
except Exception as e:
|
|
||||||
result.errors.append(("woodpecker", e))
|
|
||||||
log.error("Woodpecker bootstrap failed: %s", e)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provision_gitea_actions(values)
|
provision_gitea_actions(values)
|
||||||
log.info("Gitea Actions secrets synced")
|
log.info("Gitea Actions secrets synced")
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""CI crash reporter — scrape failed Woodpecker step logs and file issues.
|
"""CI crash reporter — scrape failed CI step logs and file issues.
|
||||||
|
|
||||||
Usage (in a Woodpecker ``failure`` step)::
|
Usage (in a Gitea Actions ``failure`` step)::
|
||||||
|
|
||||||
uv run python -m api.diag
|
uv run python -m api.diag
|
||||||
|
|
||||||
Reads CI environment variables to locate the failed pipeline, pulls
|
Reads CI environment variables to locate the failed workflow run,
|
||||||
step logs via the Woodpecker API, parses any Python tracebacks, runs
|
parses any Python tracebacks, runs ``git blame`` on the offending
|
||||||
``git blame`` on the offending lines, and files a Gitea issue with
|
lines, and files a Gitea issue with labels for the pipeline name,
|
||||||
labels for the pipeline name, step category, and image (if applicable).
|
step category, and image (if applicable).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -91,13 +91,13 @@ def main() -> int:
|
|||||||
format="%(levelname)s: %(message)s",
|
format="%(levelname)s: %(message)s",
|
||||||
)
|
)
|
||||||
|
|
||||||
repo_id = _get_env("CI_REPO_ID")
|
|
||||||
pipeline_number = _get_env("CI_PIPELINE_NUMBER")
|
|
||||||
commit_sha = _get_env("CI_COMMIT_SHA")
|
commit_sha = _get_env("CI_COMMIT_SHA")
|
||||||
repo_name = _get_env("CI_REPO") # e.g. "homelab/stack"
|
repo_name = _get_env("CI_REPO") # e.g. "homelab/stack"
|
||||||
|
step_name = _get_env("CI_STEP_NAME")
|
||||||
|
log_text = _get_env("CI_STEP_LOG")
|
||||||
|
|
||||||
if not repo_id or not pipeline_number:
|
if not commit_sha:
|
||||||
log.error("CI_REPO_ID and CI_PIPELINE_NUMBER are required")
|
log.error("CI_COMMIT_SHA is required")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
gitea_token = os.environ.get("GITEA_TOKEN", "")
|
gitea_token = os.environ.get("GITEA_TOKEN", "")
|
||||||
@@ -114,28 +114,6 @@ def main() -> int:
|
|||||||
|
|
||||||
pipeline_name = _pipeline_name_from_env()
|
pipeline_name = _pipeline_name_from_env()
|
||||||
|
|
||||||
# Fetch pipeline steps and find failures
|
|
||||||
from api.clients.woodpecker import WoodpeckerClient
|
|
||||||
|
|
||||||
wp = WoodpeckerClient(
|
|
||||||
gitea_token,
|
|
||||||
base_url=os.environ.get("CI_WOODPECKER_URL", f"{cfg.services.woodpecker}/api"),
|
|
||||||
)
|
|
||||||
|
|
||||||
rid = int(repo_id)
|
|
||||||
pnum = int(pipeline_number)
|
|
||||||
|
|
||||||
pipeline = wp.get_pipeline(rid, pnum)
|
|
||||||
steps = pipeline.get("steps", [])
|
|
||||||
|
|
||||||
failed_steps = [s for s in steps if s.get("state") == "failure"]
|
|
||||||
|
|
||||||
if not failed_steps:
|
|
||||||
log.info("No failed steps in pipeline %d — nothing to report", pnum)
|
|
||||||
wp.close()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Collect logs from all failed steps
|
|
||||||
from api.clients.gitea import GiteaClient
|
from api.clients.gitea import GiteaClient
|
||||||
from api.diag.blame import blame_report
|
from api.diag.blame import blame_report
|
||||||
from api.diag.issue import _truncate, build_issue_body
|
from api.diag.issue import _truncate, build_issue_body
|
||||||
@@ -143,38 +121,18 @@ def main() -> int:
|
|||||||
|
|
||||||
gitea = GiteaClient(gitea_token)
|
gitea = GiteaClient(gitea_token)
|
||||||
|
|
||||||
for step in failed_steps:
|
if not step_name:
|
||||||
step_name = step.get("name", "unknown")
|
step_name = "unknown"
|
||||||
step_id = step.get("id", 0)
|
|
||||||
|
|
||||||
log.info("Processing failed step: %s (id=%d)", step_name, step_id)
|
|
||||||
|
|
||||||
label_names = _resolve_label_names(pipeline_name, step_name)
|
label_names = _resolve_label_names(pipeline_name, step_name)
|
||||||
labels = gitea.resolve_labels(owner, repo, label_names)
|
labels = gitea.resolve_labels(owner, repo, label_names)
|
||||||
|
|
||||||
# Fetch logs — Woodpecker returns a list of log line dicts
|
sha_short = (commit_sha or "unknown")[:8]
|
||||||
try:
|
|
||||||
log_entries = wp.get_logs(rid, pnum, step_id)
|
|
||||||
except Exception:
|
|
||||||
log.exception("Failed to fetch logs for step %s", step_name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Concatenate log lines
|
|
||||||
if isinstance(log_entries, list):
|
|
||||||
log_text = "\n".join(
|
|
||||||
entry.get("data", "") if isinstance(entry, dict) else str(entry)
|
|
||||||
for entry in log_entries
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log_text = str(log_entries)
|
|
||||||
|
|
||||||
# Try to parse Python tracebacks
|
# Try to parse Python tracebacks
|
||||||
reports = parse_traceback_text(log_text)
|
reports = parse_traceback_text(log_text)
|
||||||
|
|
||||||
sha_short = (commit_sha or "unknown")[:8]
|
|
||||||
|
|
||||||
if reports:
|
if reports:
|
||||||
# File one issue per traceback found
|
|
||||||
for report in reports:
|
for report in reports:
|
||||||
blames = blame_report(report)
|
blames = blame_report(report)
|
||||||
body = build_issue_body(
|
body = build_issue_body(
|
||||||
@@ -184,7 +142,7 @@ def main() -> int:
|
|||||||
commit_sha or "unknown",
|
commit_sha or "unknown",
|
||||||
)
|
)
|
||||||
body = (
|
body = (
|
||||||
f"**Pipeline:** `{pipeline_name}` #{pnum} "
|
f"**Pipeline:** `{pipeline_name}` "
|
||||||
f"step `{step_name}`\n"
|
f"step `{step_name}`\n"
|
||||||
f"**Commit:** `{sha_short}`\n"
|
f"**Commit:** `{sha_short}`\n"
|
||||||
f"**Status:** `failure`\n\n" + body
|
f"**Status:** `failure`\n\n" + body
|
||||||
@@ -203,11 +161,10 @@ def main() -> int:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
log.info("Filed issue #%s: %s", result.get("number"), title)
|
log.info("Filed issue #%s: %s", result.get("number"), title)
|
||||||
else:
|
elif log_text:
|
||||||
# Non-Python failure — file with raw log
|
title = f"{pipeline_name}/{step_name} failed (@ {sha_short})"
|
||||||
title = f"{pipeline_name}/{step_name} failed (#{pnum} @ {sha_short})"
|
|
||||||
body = (
|
body = (
|
||||||
f"**Pipeline:** `{pipeline_name}` #{pnum} "
|
f"**Pipeline:** `{pipeline_name}` "
|
||||||
f"step `{step_name}`\n"
|
f"step `{step_name}`\n"
|
||||||
f"**Commit:** `{sha_short}`\n"
|
f"**Commit:** `{sha_short}`\n"
|
||||||
f"**Status:** `failure`\n\n"
|
f"**Status:** `failure`\n\n"
|
||||||
@@ -227,8 +184,9 @@ def main() -> int:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
log.info("Filed issue #%s: %s", result.get("number"), title)
|
log.info("Filed issue #%s: %s", result.get("number"), title)
|
||||||
|
else:
|
||||||
|
log.info("No log text provided — nothing to report")
|
||||||
|
|
||||||
wp.close()
|
|
||||||
gitea.close()
|
gitea.close()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ RELATED_CONTAINERS: dict[str, tuple[str, ...]] = {
|
|||||||
"rustfs": ("rustfs",),
|
"rustfs": ("rustfs",),
|
||||||
"s3": ("rustfs",),
|
"s3": ("rustfs",),
|
||||||
"gitea": ("gitea",),
|
"gitea": ("gitea",),
|
||||||
"woodpecker": ("woodpecker-server",),
|
|
||||||
"grafana": ("grafana",),
|
"grafana": ("grafana",),
|
||||||
"nessie": ("nessie",),
|
"nessie": ("nessie",),
|
||||||
"trino": ("trino",),
|
"trino": ("trino",),
|
||||||
|
|||||||
@@ -272,7 +272,6 @@ def main() -> int:
|
|||||||
[
|
[
|
||||||
"git",
|
"git",
|
||||||
"add",
|
"add",
|
||||||
".woodpecker/*.yml",
|
|
||||||
".github/workflows/*.yml",
|
".github/workflows/*.yml",
|
||||||
".gitea/workflows/*.yml",
|
".gitea/workflows/*.yml",
|
||||||
"coredns/hosts",
|
"coredns/hosts",
|
||||||
|
|||||||
10
stack.toml
10
stack.toml
@@ -29,7 +29,6 @@ subdomains = [
|
|||||||
|
|
||||||
[services]
|
[services]
|
||||||
git = "http://git:3000"
|
git = "http://git:3000"
|
||||||
woodpecker = "http://woodpecker-server:8000"
|
|
||||||
rustfs = "http://rustfs:9000"
|
rustfs = "http://rustfs:9000"
|
||||||
nessie = "http://nessie:19120"
|
nessie = "http://nessie:19120"
|
||||||
polaris = "http://polaris:8181"
|
polaris = "http://polaris:8181"
|
||||||
@@ -192,15 +191,6 @@ workers = 1
|
|||||||
backend = "gitea"
|
backend = "gitea"
|
||||||
coverage_threshold = 99
|
coverage_threshold = 99
|
||||||
|
|
||||||
[ci.woodpecker]
|
|
||||||
plugin_buildx = "woodpeckerci/plugin-docker-buildx:5-insecure"
|
|
||||||
plugin_buildx_dryrun = "woodpeckerci/plugin-docker-buildx"
|
|
||||||
plugin_s3 = "woodpeckerci/plugin-s3"
|
|
||||||
plugin_comment = "woodpeckerci/plugin-gitea-comment"
|
|
||||||
plugin_release = "woodpeckerci/plugin-gitea-release"
|
|
||||||
docker_socket = "/run/user/1000/docker.sock"
|
|
||||||
uv_image = "ghcr.io/astral-sh/uv:python3.13-bookworm-slim"
|
|
||||||
|
|
||||||
[ci.github]
|
[ci.github]
|
||||||
runner = "ubuntu-latest"
|
runner = "ubuntu-latest"
|
||||||
registry = "ghcr.io"
|
registry = "ghcr.io"
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ def _bootstrap_patches():
|
|||||||
with (
|
with (
|
||||||
patch("api.auth.provision.subprocess.run"),
|
patch("api.auth.provision.subprocess.run"),
|
||||||
patch("api.auth.provision.GiteaClient", return_value=_mock_gitea_client()),
|
patch("api.auth.provision.GiteaClient", return_value=_mock_gitea_client()),
|
||||||
patch("api.auth.provision.WoodpeckerClient", return_value=MagicMock()),
|
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -66,8 +65,8 @@ class TestBootstrapE2E:
|
|||||||
|
|
||||||
def test_credential_count(self):
|
def test_credential_count(self):
|
||||||
skip_count = sum(1 for c in CREDENTIALS if c.provisioner is Provisioner.SKIP)
|
skip_count = sum(1 for c in CREDENTIALS if c.provisioner is Provisioner.SKIP)
|
||||||
assert len(CREDENTIALS) == 20
|
assert len(CREDENTIALS) == 16
|
||||||
assert len(CREDENTIALS) - skip_count == 18
|
assert len(CREDENTIALS) - skip_count == 16
|
||||||
|
|
||||||
def test_derive_all_count(self):
|
def test_derive_all_count(self):
|
||||||
values = derive_all(ROOT, COMMIT)
|
values = derive_all(ROOT, COMMIT)
|
||||||
|
|||||||
@@ -46,95 +46,34 @@ class TestDiagMainPipelineName:
|
|||||||
|
|
||||||
|
|
||||||
class TestDiagMainMissingToken:
|
class TestDiagMainMissingToken:
|
||||||
"""Cover lines 105-106 (no GITEA_TOKEN)."""
|
"""Cover no GITEA_TOKEN path."""
|
||||||
|
|
||||||
def test_missing_gitea_token(self, monkeypatch):
|
def test_missing_gitea_token(self, monkeypatch):
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
||||||
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
||||||
|
|
||||||
from api.diag.__main__ import main
|
from api.diag.__main__ import main
|
||||||
|
|
||||||
assert main() == 1 # lines 105-106
|
assert main() == 1
|
||||||
|
|
||||||
|
|
||||||
class TestDiagMainFallbackRepo:
|
class TestDiagMainFallbackRepo:
|
||||||
"""Cover lines 112-113 (CI_REPO has no slash)."""
|
"""Cover CI_REPO with no slash."""
|
||||||
|
|
||||||
def test_repo_no_slash(self, monkeypatch):
|
def test_repo_no_slash(self, monkeypatch):
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
||||||
monkeypatch.setenv("CI_REPO", "no-slash-here")
|
monkeypatch.setenv("CI_REPO", "no-slash-here")
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
||||||
|
monkeypatch.setenv("CI_STEP_NAME", "test")
|
||||||
mock_wp = MagicMock()
|
monkeypatch.setenv("CI_STEP_LOG", "")
|
||||||
mock_wp.get_pipeline.return_value = {"steps": []}
|
|
||||||
|
|
||||||
with patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp):
|
|
||||||
from api.diag.__main__ import main
|
|
||||||
|
|
||||||
assert main() == 0 # lines 112-113
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiagMainLogFetchException:
|
|
||||||
"""Cover lines 158-160 (log fetch exception)."""
|
|
||||||
|
|
||||||
def test_log_fetch_fails(self, monkeypatch):
|
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
|
||||||
|
|
||||||
mock_wp = MagicMock()
|
|
||||||
mock_wp.get_pipeline.return_value = {
|
|
||||||
"steps": [{"name": "test", "state": "failure", "id": 1}],
|
|
||||||
}
|
|
||||||
mock_wp.get_logs.side_effect = Exception("timeout")
|
|
||||||
|
|
||||||
mock_gitea = MagicMock()
|
mock_gitea = MagicMock()
|
||||||
|
|
||||||
with (
|
with patch("api.clients.gitea.GiteaClient", return_value=mock_gitea):
|
||||||
patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
patch("api.clients.gitea.GiteaClient", return_value=mock_gitea),
|
|
||||||
):
|
|
||||||
from api.diag.__main__ import main
|
from api.diag.__main__ import main
|
||||||
|
|
||||||
assert main() == 0 # lines 158-160 (continue)
|
assert main() == 0
|
||||||
|
|
||||||
# No issue filed because log fetch failed
|
|
||||||
mock_gitea.create_issue.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiagMainLogEntriesString:
|
|
||||||
"""Cover line 169 (log_entries not a list)."""
|
|
||||||
|
|
||||||
def test_log_entries_string(self, monkeypatch):
|
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
|
||||||
|
|
||||||
mock_wp = MagicMock()
|
|
||||||
mock_wp.get_pipeline.return_value = {
|
|
||||||
"steps": [{"name": "build-api", "state": "failure", "id": 1}],
|
|
||||||
}
|
|
||||||
mock_wp.get_logs.return_value = "raw string logs" # not a list
|
|
||||||
|
|
||||||
mock_gitea = MagicMock()
|
|
||||||
mock_gitea.create_issue.return_value = {"number": 99}
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
patch("api.clients.gitea.GiteaClient", return_value=mock_gitea),
|
|
||||||
):
|
|
||||||
from api.diag.__main__ import main
|
|
||||||
|
|
||||||
assert main() == 0 # line 169
|
|
||||||
|
|
||||||
|
|
||||||
class TestDiagMainDunderMain:
|
class TestDiagMainDunderMain:
|
||||||
@@ -191,7 +130,7 @@ class TestVerifyRustfsException:
|
|||||||
|
|
||||||
|
|
||||||
class TestDeployComposeUpFailure:
|
class TestDeployComposeUpFailure:
|
||||||
"""Cover lines 209-211 (CalledProcessError in compose up)."""
|
"""Cover CalledProcessError in compose up."""
|
||||||
|
|
||||||
def test_compose_up_fails(self, tmp_path):
|
def test_compose_up_fails(self, tmp_path):
|
||||||
from api.auth.deploy import deploy
|
from api.auth.deploy import deploy
|
||||||
@@ -204,7 +143,6 @@ class TestDeployComposeUpFailure:
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
||||||
patch("api.auth.deploy.provision_woodpecker"),
|
|
||||||
patch("api.auth.deploy.subprocess.run", side_effect=compose_err),
|
patch("api.auth.deploy.subprocess.run", side_effect=compose_err),
|
||||||
patch("api.auth.deploy.verify_all", return_value=[]),
|
patch("api.auth.deploy.verify_all", return_value=[]),
|
||||||
patch("api.auth.deploy.time.sleep"),
|
patch("api.auth.deploy.time.sleep"),
|
||||||
@@ -215,31 +153,6 @@ class TestDeployComposeUpFailure:
|
|||||||
assert any(b == "compose-up" for b, _ in result.errors)
|
assert any(b == "compose-up" for b, _ in result.errors)
|
||||||
|
|
||||||
|
|
||||||
class TestDeployWoodpeckerFailure:
|
|
||||||
"""Cover lines 222-224 (woodpecker sync fails)."""
|
|
||||||
|
|
||||||
def test_woodpecker_sync_fails(self, tmp_path):
|
|
||||||
from api.auth.deploy import deploy
|
|
||||||
|
|
||||||
ROOT = bytes.fromhex("deadbeef" * 8)
|
|
||||||
env = tmp_path / ".env"
|
|
||||||
env.write_text("KEY=val\n")
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
|
||||||
patch(
|
|
||||||
"api.auth.deploy.provision_woodpecker",
|
|
||||||
side_effect=Exception("wp down"),
|
|
||||||
),
|
|
||||||
patch("api.auth.deploy.subprocess.run"),
|
|
||||||
patch("api.auth.deploy.verify_all", return_value=[]),
|
|
||||||
patch("api.auth.deploy.time.sleep"),
|
|
||||||
):
|
|
||||||
result = deploy(ROOT, "abc123", env, compose_dir=tmp_path)
|
|
||||||
|
|
||||||
assert any(b == "woodpecker" for b, _ in result.errors)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeployRollbackComposeFailure:
|
class TestDeployRollbackComposeFailure:
|
||||||
"""Cover lines 246-247 (rollback compose up fails)."""
|
"""Cover lines 246-247 (rollback compose up fails)."""
|
||||||
|
|
||||||
@@ -260,7 +173,6 @@ class TestDeployRollbackComposeFailure:
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
||||||
patch("api.auth.deploy.provision_woodpecker"),
|
|
||||||
patch("api.auth.deploy.subprocess.run", side_effect=side_effect_run),
|
patch("api.auth.deploy.subprocess.run", side_effect=side_effect_run),
|
||||||
patch(
|
patch(
|
||||||
"api.auth.deploy.verify_all",
|
"api.auth.deploy.verify_all",
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ class TestDeploy:
|
|||||||
with (
|
with (
|
||||||
# provision_postgres no longer called
|
# provision_postgres no longer called
|
||||||
patch("api.auth.deploy.provision_gitea", return_value="new-tok"),
|
patch("api.auth.deploy.provision_gitea", return_value="new-tok"),
|
||||||
patch("api.auth.deploy.provision_woodpecker"),
|
|
||||||
patch("api.auth.deploy.subprocess.run"),
|
patch("api.auth.deploy.subprocess.run"),
|
||||||
patch("api.auth.deploy.verify_all", return_value=[]),
|
patch("api.auth.deploy.verify_all", return_value=[]),
|
||||||
patch("api.auth.deploy.time.sleep"),
|
patch("api.auth.deploy.time.sleep"),
|
||||||
@@ -120,7 +119,6 @@ class TestDeploy:
|
|||||||
assert result.ok
|
assert result.ok
|
||||||
assert result.postgres
|
assert result.postgres
|
||||||
assert result.gitea
|
assert result.gitea
|
||||||
assert result.woodpecker
|
|
||||||
assert result.env_written
|
assert result.env_written
|
||||||
content = env.read_text()
|
content = env.read_text()
|
||||||
assert "GITEA_TOKEN=new-tok" in content
|
assert "GITEA_TOKEN=new-tok" in content
|
||||||
@@ -133,7 +131,6 @@ class TestDeploy:
|
|||||||
with (
|
with (
|
||||||
# provision_postgres no longer called
|
# provision_postgres no longer called
|
||||||
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
patch("api.auth.deploy.provision_gitea", return_value="tok"),
|
||||||
patch("api.auth.deploy.provision_woodpecker"),
|
|
||||||
patch("api.auth.deploy.subprocess.run"),
|
patch("api.auth.deploy.subprocess.run"),
|
||||||
patch(
|
patch(
|
||||||
"api.auth.deploy.verify_all",
|
"api.auth.deploy.verify_all",
|
||||||
@@ -157,7 +154,6 @@ class TestDeploy:
|
|||||||
"api.auth.deploy.provision_gitea",
|
"api.auth.deploy.provision_gitea",
|
||||||
side_effect=Exception("gitea down"),
|
side_effect=Exception("gitea down"),
|
||||||
),
|
),
|
||||||
patch("api.auth.deploy.provision_woodpecker"),
|
|
||||||
patch("api.auth.deploy.subprocess.run"),
|
patch("api.auth.deploy.subprocess.run"),
|
||||||
patch("api.auth.deploy.verify_all", return_value=[]),
|
patch("api.auth.deploy.verify_all", return_value=[]),
|
||||||
patch("api.auth.deploy.time.sleep"),
|
patch("api.auth.deploy.time.sleep"),
|
||||||
|
|||||||
@@ -583,8 +583,8 @@ class TestParseTracebackText:
|
|||||||
assert reports[0].exc_type == "RuntimeError"
|
assert reports[0].exc_type == "RuntimeError"
|
||||||
assert reports[0].exc_value == ""
|
assert reports[0].exc_value == ""
|
||||||
|
|
||||||
def test_handles_woodpecker_log_prefix(self):
|
def test_handles_ci_log_prefix(self):
|
||||||
# Woodpecker log lines may have timestamps or prefixes
|
# CI log lines may have timestamps or prefixes
|
||||||
text = (
|
text = (
|
||||||
"some setup output\n"
|
"some setup output\n"
|
||||||
"Traceback (most recent call last):\n"
|
"Traceback (most recent call last):\n"
|
||||||
@@ -681,56 +681,44 @@ class TestCliGuard:
|
|||||||
|
|
||||||
|
|
||||||
class TestCiMain:
|
class TestCiMain:
|
||||||
def test_missing_env_vars(self, monkeypatch):
|
def test_missing_commit_sha(self, monkeypatch):
|
||||||
monkeypatch.delenv("CI_REPO_ID", raising=False)
|
monkeypatch.delenv("CI_COMMIT_SHA", raising=False)
|
||||||
monkeypatch.delenv("CI_PIPELINE_NUMBER", raising=False)
|
|
||||||
|
|
||||||
from api.diag.__main__ import main
|
from api.diag.__main__ import main
|
||||||
|
|
||||||
assert main() == 1
|
assert main() == 1
|
||||||
|
|
||||||
def test_no_failed_steps(self, monkeypatch):
|
def test_no_log_text(self, monkeypatch):
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
||||||
|
monkeypatch.setenv("CI_STEP_NAME", "build")
|
||||||
|
monkeypatch.setenv("CI_STEP_LOG", "")
|
||||||
|
|
||||||
mock_wp = MagicMock()
|
mock_gitea = MagicMock()
|
||||||
mock_wp.get_pipeline.return_value = {
|
|
||||||
"steps": [{"name": "build", "state": "success"}],
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp):
|
with patch("api.clients.gitea.GiteaClient", return_value=mock_gitea):
|
||||||
from api.diag.__main__ import main
|
from api.diag.__main__ import main
|
||||||
|
|
||||||
assert main() == 0
|
assert main() == 0
|
||||||
|
|
||||||
def test_files_issue_for_python_failure(self, monkeypatch):
|
def test_files_issue_for_python_failure(self, monkeypatch):
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
||||||
|
monkeypatch.setenv("CI_STEP_NAME", "provision")
|
||||||
mock_wp = MagicMock()
|
monkeypatch.setenv(
|
||||||
mock_wp.get_pipeline.return_value = {
|
"CI_STEP_LOG",
|
||||||
"steps": [
|
"Traceback (most recent call last):\n"
|
||||||
{"name": "provision", "state": "failure", "id": 5},
|
' File "src/api/auth/provision.py", line 10, in provision\n'
|
||||||
],
|
" do_stuff()\n"
|
||||||
}
|
"RuntimeError: oops\n",
|
||||||
mock_wp.get_logs.return_value = [
|
)
|
||||||
{"data": "Traceback (most recent call last):"},
|
|
||||||
{"data": ' File "src/api/auth/provision.py", line 10, in provision'},
|
|
||||||
{"data": " do_stuff()"},
|
|
||||||
{"data": "RuntimeError: oops"},
|
|
||||||
]
|
|
||||||
|
|
||||||
mock_gitea = MagicMock()
|
mock_gitea = MagicMock()
|
||||||
mock_gitea.create_issue.return_value = {"number": 55}
|
mock_gitea.create_issue.return_value = {"number": 55}
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
patch("api.clients.gitea.GiteaClient", return_value=mock_gitea),
|
patch("api.clients.gitea.GiteaClient", return_value=mock_gitea),
|
||||||
patch("api.diag.blame.blame_report", return_value={}),
|
patch("api.diag.blame.blame_report", return_value={}),
|
||||||
):
|
):
|
||||||
@@ -744,30 +732,19 @@ class TestCiMain:
|
|||||||
assert "provision" in issue["title"]
|
assert "provision" in issue["title"]
|
||||||
|
|
||||||
def test_files_issue_for_non_python_failure(self, monkeypatch):
|
def test_files_issue_for_non_python_failure(self, monkeypatch):
|
||||||
monkeypatch.setenv("CI_REPO_ID", "1")
|
|
||||||
monkeypatch.setenv("CI_PIPELINE_NUMBER", "42")
|
|
||||||
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
monkeypatch.setenv("CI_COMMIT_SHA", "abc123")
|
||||||
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
monkeypatch.setenv("CI_REPO", "homelab/stack")
|
||||||
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
monkeypatch.setenv("GITEA_TOKEN", "tok")
|
||||||
|
monkeypatch.setenv("CI_STEP_NAME", "build-docs")
|
||||||
mock_wp = MagicMock()
|
monkeypatch.setenv(
|
||||||
mock_wp.get_pipeline.return_value = {
|
"CI_STEP_LOG",
|
||||||
"steps": [
|
"COPY failed: file not found\nERROR: build failed\n",
|
||||||
{"name": "build-docs", "state": "failure", "id": 3},
|
)
|
||||||
],
|
|
||||||
}
|
|
||||||
mock_wp.get_logs.return_value = [
|
|
||||||
{"data": "COPY failed: file not found"},
|
|
||||||
{"data": "ERROR: build failed"},
|
|
||||||
]
|
|
||||||
|
|
||||||
mock_gitea = MagicMock()
|
mock_gitea = MagicMock()
|
||||||
mock_gitea.create_issue.return_value = {"number": 56}
|
mock_gitea.create_issue.return_value = {"number": 56}
|
||||||
|
|
||||||
with (
|
with patch("api.clients.gitea.GiteaClient", return_value=mock_gitea):
|
||||||
patch("api.clients.woodpecker.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
patch("api.clients.gitea.GiteaClient", return_value=mock_gitea),
|
|
||||||
):
|
|
||||||
from api.diag.__main__ import main
|
from api.diag.__main__ import main
|
||||||
|
|
||||||
assert main() == 0
|
assert main() == 0
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ from api.auth.manifest import (
|
|||||||
Tier,
|
Tier,
|
||||||
)
|
)
|
||||||
from api.auth.provision import (
|
from api.auth.provision import (
|
||||||
WOODPECKER_SECRET_MAP,
|
|
||||||
derive_all,
|
derive_all,
|
||||||
provision,
|
provision,
|
||||||
provision_woodpecker,
|
|
||||||
write_env,
|
write_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,7 +50,7 @@ class TestManifestInvariants:
|
|||||||
assert cred.fmt == parent.fmt
|
assert cred.fmt == parent.fmt
|
||||||
|
|
||||||
def test_credential_count(self):
|
def test_credential_count(self):
|
||||||
assert len(CREDENTIALS) == 20
|
assert len(CREDENTIALS) == 16
|
||||||
|
|
||||||
|
|
||||||
class TestDeriveAll:
|
class TestDeriveAll:
|
||||||
@@ -210,41 +208,6 @@ class TestProvisionGitea:
|
|||||||
mock_client.delete.assert_called_once()
|
mock_client.delete.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class TestProvisionWoodpecker:
|
|
||||||
def test_updates_secrets(self):
|
|
||||||
values = derive_all(ROOT, COMMIT)
|
|
||||||
mock_wp = MagicMock()
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("api.auth.provision._get_woodpecker_token", return_value="wp-hash"),
|
|
||||||
patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
):
|
|
||||||
provision_woodpecker(values, repo_id=1)
|
|
||||||
|
|
||||||
assert mock_wp.update_secret.call_count == len(WOODPECKER_SECRET_MAP)
|
|
||||||
|
|
||||||
def test_creates_on_update_failure(self):
|
|
||||||
values = derive_all(ROOT, COMMIT)
|
|
||||||
mock_wp = MagicMock()
|
|
||||||
mock_wp.update_secret.side_effect = Exception("not found")
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("api.auth.provision._get_woodpecker_token", return_value="wp-hash"),
|
|
||||||
patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
):
|
|
||||||
provision_woodpecker(values, repo_id=1)
|
|
||||||
|
|
||||||
assert mock_wp.create_secret.call_count == len(WOODPECKER_SECRET_MAP)
|
|
||||||
|
|
||||||
def test_raises_without_db_token(self):
|
|
||||||
with patch(
|
|
||||||
"api.auth.provision._get_woodpecker_token",
|
|
||||||
side_effect=ValueError("Could not read"),
|
|
||||||
):
|
|
||||||
with pytest.raises(ValueError, match="Could not read"):
|
|
||||||
provision_woodpecker({}, repo_id=1)
|
|
||||||
|
|
||||||
|
|
||||||
class TestProvisionEndToEnd:
|
class TestProvisionEndToEnd:
|
||||||
def test_partial_failure_still_writes_env(self, tmp_path):
|
def test_partial_failure_still_writes_env(self, tmp_path):
|
||||||
env = tmp_path / ".env"
|
env = tmp_path / ".env"
|
||||||
@@ -257,27 +220,21 @@ class TestProvisionEndToEnd:
|
|||||||
"api.auth.provision.provision_gitea",
|
"api.auth.provision.provision_gitea",
|
||||||
side_effect=Exception("gitea down"),
|
side_effect=Exception("gitea down"),
|
||||||
),
|
),
|
||||||
patch(
|
|
||||||
"api.auth.provision.provision_woodpecker",
|
|
||||||
side_effect=Exception("wp down"),
|
|
||||||
),
|
|
||||||
patch("api.auth.provision.time.sleep"),
|
patch("api.auth.provision.time.sleep"),
|
||||||
):
|
):
|
||||||
result = provision(ROOT, COMMIT, env)
|
result = provision(ROOT, COMMIT, env)
|
||||||
|
|
||||||
assert result.env_written
|
assert result.env_written
|
||||||
assert env.exists()
|
assert env.exists()
|
||||||
assert len(result.errors) == 3
|
assert len(result.errors) == 2
|
||||||
assert not result.postgres
|
assert not result.postgres
|
||||||
assert not result.gitea
|
assert not result.gitea
|
||||||
assert not result.woodpecker
|
|
||||||
|
|
||||||
def test_full_success(self, tmp_path):
|
def test_full_success(self, tmp_path):
|
||||||
env = tmp_path / ".env"
|
env = tmp_path / ".env"
|
||||||
with (
|
with (
|
||||||
patch("api.auth.provision.provision_postgres"),
|
patch("api.auth.provision.provision_postgres"),
|
||||||
patch("api.auth.provision.provision_gitea", return_value="tok-new"),
|
patch("api.auth.provision.provision_gitea", return_value="tok-new"),
|
||||||
patch("api.auth.provision.provision_woodpecker"),
|
|
||||||
patch("api.auth.provision.time.sleep"),
|
patch("api.auth.provision.time.sleep"),
|
||||||
):
|
):
|
||||||
result = provision(ROOT, COMMIT, env)
|
result = provision(ROOT, COMMIT, env)
|
||||||
@@ -285,7 +242,6 @@ class TestProvisionEndToEnd:
|
|||||||
assert result.ok
|
assert result.ok
|
||||||
assert result.postgres
|
assert result.postgres
|
||||||
assert result.gitea
|
assert result.gitea
|
||||||
assert result.woodpecker
|
|
||||||
assert result.env_written
|
assert result.env_written
|
||||||
content = env.read_text()
|
content = env.read_text()
|
||||||
assert "GITEA_TOKEN=tok-new" in content
|
assert "GITEA_TOKEN=tok-new" in content
|
||||||
@@ -304,7 +260,6 @@ class TestProvisionEndToEnd:
|
|||||||
with (
|
with (
|
||||||
patch("api.auth.provision.provision_postgres"),
|
patch("api.auth.provision.provision_postgres"),
|
||||||
patch("api.auth.provision.provision_gitea", return_value="tok"),
|
patch("api.auth.provision.provision_gitea", return_value="tok"),
|
||||||
patch("api.auth.provision.provision_woodpecker"),
|
|
||||||
patch(
|
patch(
|
||||||
"api.auth.provision.provision_gitea_actions",
|
"api.auth.provision.provision_gitea_actions",
|
||||||
side_effect=Exception("actions down"),
|
side_effect=Exception("actions down"),
|
||||||
@@ -318,33 +273,6 @@ class TestProvisionEndToEnd:
|
|||||||
assert "gitea-actions" in error_backends
|
assert "gitea-actions" in error_backends
|
||||||
|
|
||||||
|
|
||||||
class TestGetWoodpeckerToken:
|
|
||||||
def test_raises_when_output_empty(self) -> None:
|
|
||||||
"""Raises ValueError if DB query returns empty string."""
|
|
||||||
from api.auth.provision import _get_woodpecker_token
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"api.auth.provision.subprocess.run",
|
|
||||||
) as mock_run:
|
|
||||||
mock_run.return_value.stdout = ""
|
|
||||||
with pytest.raises(ValueError, match="Could not read Woodpecker token"):
|
|
||||||
_get_woodpecker_token({})
|
|
||||||
|
|
||||||
|
|
||||||
class TestProvisionWoodpeckerSkipMissing:
|
|
||||||
def test_skips_secrets_with_no_value(self) -> None:
|
|
||||||
"""Secrets with empty values are skipped with a warning."""
|
|
||||||
mock_wp = MagicMock()
|
|
||||||
with (
|
|
||||||
patch("api.auth.provision._get_woodpecker_token", return_value="tok"),
|
|
||||||
patch("api.auth.provision.WoodpeckerClient", return_value=mock_wp),
|
|
||||||
):
|
|
||||||
provision_woodpecker({}, repo_id=1)
|
|
||||||
|
|
||||||
mock_wp.update_secret.assert_not_called()
|
|
||||||
mock_wp.create_secret.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
class TestProvisionGiteaActions:
|
class TestProvisionGiteaActions:
|
||||||
def test_raises_without_gitea_token(self) -> None:
|
def test_raises_without_gitea_token(self) -> None:
|
||||||
"""Raises ValueError when GITEA_TOKEN is missing."""
|
"""Raises ValueError when GITEA_TOKEN is missing."""
|
||||||
@@ -425,10 +353,6 @@ class TestBootstrap:
|
|||||||
"api.auth.provision.provision_gitea",
|
"api.auth.provision.provision_gitea",
|
||||||
side_effect=Exception("gitea down"),
|
side_effect=Exception("gitea down"),
|
||||||
),
|
),
|
||||||
patch(
|
|
||||||
"api.auth.provision.provision_woodpecker",
|
|
||||||
side_effect=Exception("wp down"),
|
|
||||||
),
|
|
||||||
patch(
|
patch(
|
||||||
"api.auth.provision.provision_gitea_actions",
|
"api.auth.provision.provision_gitea_actions",
|
||||||
side_effect=Exception("actions down"),
|
side_effect=Exception("actions down"),
|
||||||
@@ -440,7 +364,6 @@ class TestBootstrap:
|
|||||||
error_backends = [b for b, _ in result.errors]
|
error_backends = [b for b, _ in result.errors]
|
||||||
assert "postgres" in error_backends
|
assert "postgres" in error_backends
|
||||||
assert "gitea" in error_backends
|
assert "gitea" in error_backends
|
||||||
assert "woodpecker" in error_backends
|
|
||||||
assert "gitea-actions" in error_backends
|
assert "gitea-actions" in error_backends
|
||||||
|
|
||||||
def test_success_path(self, tmp_path) -> None:
|
def test_success_path(self, tmp_path) -> None:
|
||||||
@@ -451,7 +374,6 @@ class TestBootstrap:
|
|||||||
patch("api.auth.provision.bootstrap_postgres"),
|
patch("api.auth.provision.bootstrap_postgres"),
|
||||||
patch("api.auth.provision.provision_postgres"),
|
patch("api.auth.provision.provision_postgres"),
|
||||||
patch("api.auth.provision.provision_gitea", return_value="tok"),
|
patch("api.auth.provision.provision_gitea", return_value="tok"),
|
||||||
patch("api.auth.provision.provision_woodpecker"),
|
|
||||||
patch("api.auth.provision.provision_gitea_actions"),
|
patch("api.auth.provision.provision_gitea_actions"),
|
||||||
):
|
):
|
||||||
result = bootstrap(ROOT, COMMIT, env)
|
result = bootstrap(ROOT, COMMIT, env)
|
||||||
|
|||||||
@@ -127,12 +127,10 @@ class TestSecurityOpts:
|
|||||||
"""Most services should have no-new-privileges."""
|
"""Most services should have no-new-privileges."""
|
||||||
# Exceptions: privileged, ephemeral, or upstream images without secopt
|
# Exceptions: privileged, ephemeral, or upstream images without secopt
|
||||||
exempt = {
|
exempt = {
|
||||||
"woodpecker-agent", # needs privileged for Docker builds
|
|
||||||
"wire", # ephemeral bootstrap, profile=tools
|
"wire", # ephemeral bootstrap, profile=tools
|
||||||
"zotero", # GPU + display server needs
|
"zotero", # GPU + display server needs
|
||||||
"coredns", # upstream, no secopt in image
|
"coredns", # upstream, no secopt in image
|
||||||
"gitea", # rootless image handles its own security
|
"gitea", # rootless image handles its own security
|
||||||
"woodpecker-server", # upstream CI server
|
|
||||||
"notebooks", # GPU + dev environment
|
"notebooks", # GPU + dev environment
|
||||||
"webdav", # rclone upstream
|
"webdav", # rclone upstream
|
||||||
"docs", # static site
|
"docs", # static site
|
||||||
|
|||||||
Reference in New Issue
Block a user