Files
coreci da15bda6d2 docs(milestone): complete cli-packaging
---ci---
phase: 3
milestone: v0.2
status: complete
requirements:
  covered: [REQ-21, REQ-22, REQ-23]
  partial: []
---/ci---
2026-10-11 00:32:26 +00:00

112 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Sanctioned secrets loader for the v0.2 distribution scripts (stdlib only).
Secrets resolve ONLY from env files on disk — never from the process
environment (``GITEA_TOKEN`` sits in ``config.security.bash_allowlist
.blocked_env_vars``, so shell-env tokens are structurally blocked; the
v1.8 lesson: an empty or mis-sourced token makes gitea answer 404 rather
than 401, masking the auth failure).
Parsing mirrors ``coreci_broker._parse_env_file`` semantics exactly:
blank lines and ``#`` comments are skipped, keys/values are whitespace-
stripped, and a value wrapped in matching single or double quotes is
unquoted.
File precedence — later files win. Locations in order (``root``, then the
sanctioned ``.ciagent/`` dir under it); each location in ``config.json``
``secrets.env_files`` pattern order (``.env``, ``.env.secrets``, then the
``.env.*`` glob, sorted)::
<root>/.env, <root>/.env.secrets, <root>/.env.*
<root>/.ciagent/.env, <root>/.ciagent/.env.secrets, <root>/.ciagent/.env.*
``load()`` returns the merged mapping; ``get_token()`` enforces the
non-empty assert that callers MUST let happen before their first API
call. This module never prints secret values.
"""
import glob
import os
ENV_FILE_PATTERNS = (".env", ".env.secrets", ".env.*")
CIAGENT_SUBDIR = ".ciagent"
class SecretsError(RuntimeError):
"""A required secret is missing or empty in the sanctioned env files."""
def default_root():
"""Repo root: the parent directory of this scripts/ folder."""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _parse_env_file(path):
"""Mirror coreci_broker._parse_env_file (comments, blanks, quotes)."""
entries = []
if not os.path.isfile(path):
return entries
with open(path, "r", encoding="utf-8") as handle:
for raw in handle:
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = (part.strip() for part in line.partition("="))
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
value = value[1:-1]
if key:
entries.append((key, value))
return entries
def _candidate_files(root):
"""Ordered, deduplicated env-file paths; later files win."""
seen = set()
files = []
for location in (root, os.path.join(root, CIAGENT_SUBDIR)):
for pattern in ENV_FILE_PATTERNS:
for path in sorted(glob.glob(os.path.join(location, pattern))):
if not os.path.isfile(path):
continue
real = os.path.abspath(path)
if real in seen:
continue
seen.add(real)
files.append(real)
return files
def load(root=None):
"""Merge every sanctioned env file under ``root`` into one dict.
``root`` defaults to the repo root (the parent of scripts/).
"""
base = os.path.abspath(root) if root is not None else default_root()
values = {}
for path in _candidate_files(base):
for key, value in _parse_env_file(path):
values[key] = value
return values
def get_token(var="GITEA_TOKEN", root=None):
"""Return the non-empty value of ``var`` from the env files only.
Raises SecretsError when the value is missing, empty, or whitespace-
only — callers must surface this BEFORE any HTTP call (an empty token
makes gitea answer 404 and masks the auth failure — the v1.8 lesson).
"""
token = load(root).get(var, "")
if not token.strip():
base = os.path.abspath(root) if root is not None else default_root()
searched = ", ".join(
os.path.join(location, pattern)
for location in (base, os.path.join(base, CIAGENT_SUBDIR))
for pattern in ENV_FILE_PATTERNS
)
raise SecretsError(
"{} is missing or empty in the sanctioned env files "
"(searched: {}); it never resolves from the process "
"environment".format(var, searched)
)
return token.strip()