Merge pull request 'config: load public cc-ci runtime environment' (#14) from chore/public-runtime-config into main

This commit was merged in pull request #14.
This commit is contained in:
2026-08-31 16:50:25 +00:00
15 changed files with 60 additions and 28 deletions
+10 -5
View File
@@ -1114,19 +1114,24 @@ def markdown(rep: dict) -> str:
def _gitea_auth(url: str) -> dict:
"""Basic auth for the private mirror, from /srv/cc-ci/.testenv.
"""Basic auth for the private mirror, using public config plus .testenv.
Sent as a HEADER, never embedded in the URL: in-URL credentials leak into shell history, process
lists and error messages, and urllib mis-parses a password containing a colon."""
host = re.sub(r"^https?://", "", url).split("/")[0]
env = {}
try:
for ln in open(os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv")):
for path in (
"/srv/cc-ci/cc-ci/.env.public",
os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv"),
):
try:
lines = open(path)
except OSError:
continue
for ln in lines:
if "=" in ln and not ln.strip().startswith("#"):
k, v = ln.strip().split("=", 1)
env[k] = v.strip().strip("\"'")
except OSError:
return {}
if host != env.get("GITEA_URL", "git.autonomic.zone"):
return {}
u, pw = env.get("GITEA_USERNAME"), env.get("GITEA_PASSWORD")
+1 -1
View File
@@ -34,7 +34,7 @@ supports_resume = false
prompt_delivery = "ping" # send after the TUI connects
connect_delay = 12
submit_key = "C-m"
preamble = "set -a; . /srv/cc-ci/.testenv; set +a"
preamble = "set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a"
stall_idle = 900
active_re = "esc interrupt|thinking|inferring|running tool|tool call|preparing patch|reading|searching"
limit_re = "spend limit|usage limit|limit reached|reached your .*limit|out of (credits|tokens)"
+1 -1
View File
@@ -112,7 +112,7 @@ def start(mode="resume"):
if not Path(OPENCODE_BIN).exists():
die(f"opencode not found at {OPENCODE_BIN}")
cmd = (
f"set -a; . /srv/cc-ci/.testenv; set +a; "
f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; "
f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {WORKDIR}"
)
log(f"starting {SESSION} (backend=opencode, model={LOOP_MODEL or 'default'})")
+1 -1
View File
@@ -154,7 +154,7 @@ def start(mode="resume"):
f"OPENCODE_CONFIG_CONTENT={shlex.quote(json.dumps({'model': LOOP_MODEL}))} "
)
cmd = (
f"set -a; . /srv/cc-ci/.testenv; set +a; "
f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; "
f"{model_env}NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {shlex.quote(WORKDIR)}"
)
log(f"starting {SESSION} (backend=opencode, model={LOOP_MODEL or 'default'})")
+1 -1
View File
@@ -109,7 +109,7 @@ def start(mode, date):
cwd = "/srv/cc-ci-orch/cc-ci"
share_flag = "--share" if OPENCODE_SHARE else ""
cmd = (
f"set -a; . /srv/cc-ci/.testenv; set +a; "
f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; "
f"{OPENCODE_BIN} run {model_flag} {share_flag} --attach '{OPENCODE_SERVER}' "
f"--title '{SESSION}' --dir {cwd} \"$(cat '{kf}')\""
)
+1 -1
View File
@@ -107,7 +107,7 @@ def spawn_supervisor(sid, reason):
kf = Path(LOG_DIR) / f".kickoff-{SUP_SESSION}.txt"
kf.write_text(build_kickoff(sid, reason))
share = "--share" if OPENCODE_SHARE else ""
cmd = (f"set -a; . {WORKDIR}/.testenv; set +a; {OPENCODE_BIN} run --model '{MODEL}' {share} "
cmd = (f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; {OPENCODE_BIN} run --model '{MODEL}' {share} "
f"--attach '{OPENCODE_SERVER}' --title '{SUP_SESSION}' --dir {WORKDIR} \"$(cat '{kf}')\"")
_sh(["tmux", "new-session", "-d", "-s", SUP_SESSION, "-c", WORKDIR, cmd])
_sh(["tmux", "pipe-pane", "-o", "-t", SUP_SESSION, f"cat >> '{LOG_DIR}/{SUP_SESSION}.log'"])
+2 -2
View File
@@ -216,7 +216,7 @@ def start(mode="use-or-create"):
# so they must come AFTER `run` (a global `opencode --model X run` is ignored).
share_flag = "--share" if OPENCODE_SHARE else ""
cmd = (
f"set -a; . /srv/cc-ci/.testenv; set +a; "
f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; "
f"{OPENCODE_BIN} run {model_flag} {share_flag} --attach '{OPENCODE_SERVER}' "
f"--title '{SESSION}' \"$(cat '{kf}')\""
)
@@ -521,7 +521,7 @@ def resume(reason="manual"):
"write+push the weekly summary, then `python3 /srv/cc-ci/cc-ci-plan/launch-report.py fresh`, print "
"'" + DONE_MARKER + "', and go idle.")
share = "--share" if OPENCODE_SHARE else ""
cmd = (f"set -a; . /srv/cc-ci/.testenv; set +a; {OPENCODE_BIN} run -s {sid} --continue "
cmd = (f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; {OPENCODE_BIN} run -s {sid} --continue "
f"--model '{MODEL}' {share} --attach '{OPENCODE_SERVER}' --dir '{WORKDIR}' \"$(cat '{kf}')\"")
subprocess.run(["tmux", "new-session", "-d", "-s", SESSION, "-c", WORKDIR, cmd])
subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_FILE}'"])
+1 -1
View File
@@ -323,7 +323,7 @@ def start_agent(role, session, workdir):
# absolute repo paths for builder/adversary work.
session_cwd = "/srv/cc-ci-orch/cc-ci"
cmd = (
f"set -a; . /srv/cc-ci/.testenv; set +a; "
f"set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a; "
f"NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {session_cwd}"
)
log(f"starting {session} (backend=opencode, phase={pid}, model={model or 'default'})")
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Load cc-ci runtime configuration. Source this file; do not execute it.
#
# Callers enable `set -a` so both files become exported environment variables.
# Public configuration is loaded first, then the secret file. This preserves a
# local secret override only where an emergency migration explicitly needs it.
if [ -r /srv/cc-ci/cc-ci/.env.public ]; then
. /srv/cc-ci/cc-ci/.env.public
fi
if [ ! -r /srv/cc-ci/.testenv ]; then
echo "cc-ci secret environment is missing: /srv/cc-ci/.testenv" >&2
return 1
fi
. /srv/cc-ci/.testenv
+1 -1
View File
@@ -71,7 +71,7 @@ log "writing report $REPORT ..."
if [ -f "$SUMMARY" ]; then cat "$SUMMARY"; else echo "(no dated summary at $SUMMARY — /upgrade-all may have stalled on the usage limit or errored; check the cc-ci-upgrader session + $LOG)"; fi
echo
echo "## Open PRs per recipe (post-run)"
set -a; . /srv/cc-ci/.testenv 2>/dev/null; set +a
set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a
G="https://${GITEA_USERNAME}:${GITEA_PASSWORD}@${GITEA_URL}/api/v1"
for r in $(curl -fsS "$G/orgs/recipe-maintainers/repos?limit=100" 2>/dev/null | python3 -c "import sys,json;print('\n'.join(sorted(x['name'] for x in json.load(sys.stdin))))" 2>/dev/null); do
case "$r" in cc-ci|cc-ci-orchestrator|cc-ci-secrets|archived-*) continue;; esac
+3 -2
View File
@@ -83,9 +83,10 @@ repo, a commit, a log, or the dashboard** (§9) — reference locations only.
| What | Where | How to use |
|---|---|---|
| **cc-ci SSH (root)** | private key `~/.ssh/cc-ci-root-ed25519`; `Host cc-ci` in `~/.ssh/config` (HostName `100.90.116.4`, no ProxyCommand) | Just run `ssh cc-ci` (logs in as **root**). The orchestrator VM is a direct tailnet peer — direct route, no proxy. Pubkey already in cc-ci's `/root/.ssh/authorized_keys`. |
| **Gitea bot account** | `/srv/cc-ci/.testenv``GITEA_USERNAME` (`autonomic-bot`), `GITEA_PASSWORD`, `GITEA_URL` (`git.autonomic.zone`) | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to push the `cc-ci` project repo, read recipe repos, comment on PRs, and poll for `!testme` (read-level; the bot does not register webhooks). |
| **Gitea bot account** | `/srv/cc-ci/cc-ci/.env.public``GITEA_USERNAME` (`autonomic-bot`); `/srv/cc-ci/.testenv` `GITEA_PASSWORD`; `GITEA_URL` defaults to `git.autonomic.zone` | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to push the `cc-ci` project repo, read recipe repos, comment on PRs, and poll for `!testme` (read-level; the bot does not register webhooks). |
Load them in a shell with: `set -a; . /srv/cc-ci/.testenv; set +a` (don't echo the values).
Load runtime configuration in a shell with: `set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a`
(don't echo secret values). The helper loads tracked public configuration first and then `.testenv`.
### The Tailscale connection (how `ssh cc-ci` works)
+7 -4
View File
@@ -40,19 +40,22 @@ from datetime import datetime, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
USED_RECIPES = os.path.join(HERE, "used-recipes.md")
TESTENV = os.environ.get("CCCI_TESTENV", "/srv/cc-ci/.testenv")
PUBLIC_ENV = "/srv/cc-ci/cc-ci/.env.public"
NS = "recipe-maintainers"
def _env() -> dict:
e = {}
try:
for ln in open(TESTENV):
for path in (PUBLIC_ENV, TESTENV):
try:
lines = open(path)
except OSError:
continue
for ln in lines:
ln = ln.strip()
if "=" in ln and not ln.startswith("#"):
k, v = ln.split("=", 1)
e[k] = v.strip().strip('"').strip("'")
except OSError:
pass
return e
+7 -4
View File
@@ -46,6 +46,7 @@ from datetime import datetime, timezone
LOGDIR = "/srv/cc-ci/.cc-ci-logs"
TESTENV = "/srv/cc-ci/.testenv"
PUBLIC_ENV = "/srv/cc-ci/cc-ci/.env.public"
INFRA = {"cc-ci", "cc-ci-orchestrator", "cc-ci-secrets"}
HOST_REPORTS = "/var/lib/cc-ci-reports"
# Both kinds live in ONE archive, distinguished by a suffix on a common title.
@@ -56,14 +57,16 @@ KINDS = {"week": ("The Recipe Report", "Week of {d} — full"),
def _env():
e = {}
try:
for ln in open(TESTENV):
for path in (PUBLIC_ENV, TESTENV):
try:
lines = open(path)
except FileNotFoundError:
continue
for ln in lines:
ln = ln.strip()
if "=" in ln and not ln.startswith("#"):
k, v = ln.split("=", 1)
e[k] = v.strip().strip('"').strip("'")
except FileNotFoundError:
pass
return e
+1 -2
View File
@@ -30,12 +30,11 @@ set -o errexit -o nounset -o pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ORCH="$(dirname "$HERE")"
SSH="${SSH:-cc-ci}"
TESTENV="${TESTENV:-/srv/cc-ci/.testenv}"
RECONCILE="${RECONCILE:-$ORCH/.claude/skills/recipe-upgrade/open-recipe-pr.sh}"
USED_RECIPES="${USED_RECIPES:-$HERE/used-recipes.md}"
[ -f "$RECONCILE" ] || { echo "ERROR: reconcile helper not found: $RECONCILE" >&2; exit 1; }
set -a; . "$TESTENV"; set +a
set -a; . /srv/cc-ci/cc-ci-plan/load-env.sh; set +a
: "${GITEA_USERNAME:?}"; : "${GITEA_PASSWORD:?}"; : "${GITEA_URL:=git.autonomic.zone}"
if [ "${1:-}" = "--all" ]; then
@@ -131,7 +131,8 @@ SSHCFG
# opencode web server — one shared instance; agent sessions attach to it for web visibility.
# Serves the web UI at http://oc.commoninternet.net (via nginx below, tailscale-only).
# Provider creds are read from /srv/cc-ci/.testenv at startup.
# Public provider configuration is tracked in the cc-ci repo; credentials
# remain in /srv/cc-ci/.testenv.
systemd.services.opencode-web = {
description = "opencode web server for cc-ci agents";
wantedBy = [ "multi-user.target" ];
@@ -141,7 +142,10 @@ SSHCFG
Type = "simple";
User = "loops"; Group = "users";
WorkingDirectory = "/srv/cc-ci-orch/cc-ci";
EnvironmentFile = "/srv/cc-ci/.testenv";
EnvironmentFile = [
"-/srv/cc-ci/cc-ci/.env.public"
"/srv/cc-ci/.testenv"
];
ExecStartPre = "${pkgs.coreutils}/bin/rm -rf /tmp/opencode";
ExecStart = "/home/loops/.local/bin/opencode serve --hostname 127.0.0.1 --port 4096";
Restart = "on-failure";