Merge remote-tracking branch 'gitea/master' into cpp20-modernization

This commit is contained in:
Krystie
2026-05-30 23:00:56 -07:00
4 changed files with 466 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=zWaw
-----END PGP PUBLIC KEY BLOCK-----
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""Krystie Gate — static check stage of the CI gate.
Runs inside the Gitea Actions runner. Inspects all commits that were just
pushed to a krystie-wip/* branch and rejects if any violates the gate rules.
Decision per commit:
* If signed by Krystie's GPG key (fingerprint DCF2579968107984), apply the
full per-repo gate.
* If signed by a different key OR unsigned, allow (Sami's authority).
Per-repo enforcement:
* triangles_v5 : red-list (consensus paths) + test-first + no-clearnet
* triangles-explorer, triangles-api, tridock-web-wallet, sami-chat, tri-pi:
test-first only
* homebrew-triangles: formula syntax check only
Outputs:
* On reject, prints REJECTED lines to stderr and exits 1.
* On accept, sets `is_krystie_commit` GH-actions output to true/false.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
# Krystie's GPG identity. We accept both the primary long-ID and the
# signing subkey because `git log %GK` returns the subkey that was actually
# used to sign, not the primary. The full primary fingerprint is also
# included so a paranoid future check can validate the chain.
KRYSTIE_PRIMARY_FP = "523A81833EB7201573E1EFE1DCF2579968107984"
KRYSTIE_KEY_IDS = {
"DCF2579968107984", # primary long-ID
"C2DC60618C85A159", # signing subkey long-ID
}
RED_LIST_TRIANGLES_V5 = [
re.compile(r"^src/main\.(cpp|h)$"),
re.compile(r"^src/validation.*"),
re.compile(r"^src/kernel\.(cpp|h)$"),
re.compile(r"^src/checkpoints\.(cpp|h)$"),
re.compile(r"^src/consensus/"),
re.compile(r"^src/protocol\.(cpp|h)$"),
re.compile(r"^src/net\.(cpp|h)$"),
re.compile(r"^src/netbase\.(cpp|h)$"),
re.compile(r"^src/net_bootstrap\.(cpp|h)$"),
re.compile(r"^src/chainparams.*"),
re.compile(r"^src/clientversion\.h$"),
re.compile(r"^src/key\.(cpp|h)$"),
re.compile(r"^src/keystore\.(cpp|h)$"),
re.compile(r"^src/onionseed\.h$"),
re.compile(r"^contrib/seeds/"),
re.compile(r"^contrib/devtools/release.*"),
re.compile(r"^doc/release-process\.txt$"),
]
TEST_DIRS = {
"triangles_v5": ["src/test/", "test/"],
"triangles-explorer": ["src/__tests__/", "tests/", "test/"],
"triangles-api": ["test/", "__tests__/", "tests/"],
"tridock-web-wallet": ["test/", "__tests__/", "tests/"],
"sami-chat": ["test/", "__tests__/", "tests/"],
"tri-pi": ["test/", "tests/"],
"homebrew-triangles": [],
}
SOURCE_EXTS = {
"triangles_v5": {".cpp", ".h", ".c"},
"triangles-explorer": {".ts", ".tsx", ".js", ".svelte"},
"triangles-api": {".js", ".ts"},
"tridock-web-wallet": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"sami-chat": {".ts", ".tsx", ".js", ".svelte", ".vue"},
"tri-pi": {".py", ".sh", ".ts", ".js"},
"homebrew-triangles": set(),
}
RED_LIST_REPOS = {"triangles_v5"}
PEER_CONFIG_PATHS = [
re.compile(r"^contrib/seeds/"),
re.compile(r"^src/chainparams.*"),
re.compile(r".*triangles\.conf(\.example)?$"),
]
@dataclass
class GateResult:
ok: bool
reason: str = ""
def repo_name() -> str:
repo = os.environ.get("GITHUB_REPOSITORY", "")
return repo.split("/", 1)[1] if "/" in repo else repo
def commit_signer(sha: str) -> str | None:
try:
out = subprocess.run(
["git", "log", "-1", "--format=%GK", sha],
check=True, capture_output=True, text=True,
).stdout.strip()
return out or None
except subprocess.CalledProcessError:
return None
def is_krystie_commit(sha: str) -> bool:
fp = commit_signer(sha)
if not fp:
return False
# Accept any key ID we know belongs to Krystie. `git log %GK` returns the
# signing subkey, so we have to whitelist both primary and subkey.
return any(fp == known or known.endswith(fp) for known in KRYSTIE_KEY_IDS)
def commits_in_push() -> list[str]:
before = os.environ.get("GITHUB_BEFORE", "")
sha = os.environ.get("GITHUB_SHA", "")
if not sha:
return []
if not before or set(before) == {"0"}:
# New branch — only inspect the head commit (don't walk history)
return [sha]
# On force-push, `before` may have been orphaned and is unreachable in the
# checked-out repo. `git rev-list before..sha` then exits 128. Fall back
# to inspecting the new head only — that's the safest guarantee we can
# make about what just landed.
try:
out = subprocess.run(
["git", "rev-list", f"{before}..{sha}"],
check=True, capture_output=True, text=True,
).stdout
return [c for c in out.split() if c]
except subprocess.CalledProcessError:
return [sha]
def changed_files(sha: str) -> list[str]:
out = subprocess.run(
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", sha],
check=True, capture_output=True, text=True,
).stdout
return [f for f in out.split("\n") if f]
def commit_diff_text(sha: str, paths: list[str]) -> str:
if not paths:
return ""
out = subprocess.run(
["git", "show", "--no-color", sha, "--"] + paths,
check=True, capture_output=True, text=True,
).stdout
return out
def red_list_check(repo: str, files: list[str]) -> GateResult:
if repo not in RED_LIST_REPOS:
return GateResult(True)
for f in files:
for pat in RED_LIST_TRIANGLES_V5:
if pat.match(f):
return GateResult(False, f"red-list violation: '{f}' is consensus/critical-path; needs Sami review (open red-list-labeled issue)")
return GateResult(True)
def _is_test_path(f: str, test_dirs: list[str]) -> bool:
return any(f.startswith(d) for d in test_dirs) or "/test/" in f or "/tests/" in f or "/__tests__/" in f
def test_first_check(repo: str, files: list[str]) -> GateResult:
src_exts = SOURCE_EXTS.get(repo, set())
test_dirs = TEST_DIRS.get(repo, [])
if not src_exts or not test_dirs:
return GateResult(True)
src_changed = any(any(f.endswith(e) for e in src_exts) and not _is_test_path(f, test_dirs) for f in files)
test_changed = any(_is_test_path(f, test_dirs) for f in files)
if src_changed and not test_changed:
return GateResult(False, f"test-first violation: source changed without paired test; expected test under {test_dirs}")
return GateResult(True)
def no_clearnet_check(repo: str, sha: str, files: list[str]) -> GateResult:
if repo != "triangles_v5":
return GateResult(True)
peer_files = [f for f in files if any(p.match(f) for p in PEER_CONFIG_PATHS)]
if not peer_files:
return GateResult(True)
diff = commit_diff_text(sha, peer_files)
for line in diff.split("\n"):
if not line.startswith("+") or line.startswith("+++"):
continue
body = line[1:].strip()
if re.search(r"\b(addnode|seednode|connect)\s*=", body, re.IGNORECASE):
if ".onion" not in body.lower():
return GateResult(False, f"no-clearnet: added peer/seed without .onion: {body[:120]}")
if re.match(r"^\s*(\d{1,3}\.){3}\d{1,3}\b", body) or re.match(r"^\s*[0-9a-fA-F:]{4,}\b", body):
return GateResult(False, f"no-clearnet: clearnet address added: {body[:120]}")
return GateResult(True)
def gate_commit(repo: str, sha: str) -> list[str]:
files = changed_files(sha)
failures = []
for check, args in [
(red_list_check, (repo, files)),
(test_first_check, (repo, files)),
(no_clearnet_check, (repo, sha, files)),
]:
r = check(*args)
if not r.ok:
failures.append(f"commit {sha[:12]}: {r.reason}")
return failures
def emit_output(name: str, value: str):
out_file = os.environ.get("GITHUB_OUTPUT", "")
if out_file:
with open(out_file, "a") as fh:
fh.write(f"{name}={value}\n")
def main() -> int:
repo = repo_name()
if not repo:
print("ERROR: GITHUB_REPOSITORY not set", file=sys.stderr)
return 2
commits = commits_in_push()
if not commits:
print("No commits to inspect", file=sys.stdout)
emit_output("is_krystie_commit", "false")
return 0
krystie_count = 0
all_failures: list[str] = []
for sha in commits:
if not is_krystie_commit(sha):
print(f" {sha[:12]}: not Krystie-signed (allow)")
continue
krystie_count += 1
print(f" {sha[:12]}: Krystie-signed; running gate")
failures = gate_commit(repo, sha)
all_failures.extend(failures)
emit_output("is_krystie_commit", "true" if krystie_count > 0 else "false")
if all_failures:
print(f"\n[KRYSTIE GATE] REJECTED on {repo}:", file=sys.stderr)
for f in all_failures:
print(f" - {f}", file=sys.stderr)
return 1
print(f"[KRYSTIE GATE] PASS on {repo} ({krystie_count} Krystie commit(s) inspected, {len(commits) - krystie_count} non-Krystie)")
return 0
if __name__ == "__main__":
sys.exit(main())
+132
View File
@@ -0,0 +1,132 @@
name: Krystie Gate
# Runs on every push to krystie-wip/* branches.
# Static checks first (cheap), then build + tests.
# If everything green AND the commit is Krystie's, fast-forwards master.
# Sami's pushes (admin) bypass this entire flow — he goes direct to master.
on:
push:
branches:
- 'krystie-wip/**'
jobs:
static-gate:
name: "Static gate (red-list / test-first / no-clearnet)"
runs-on: ubuntu-latest
outputs:
is_krystie_commit: ${{ steps.gate.outputs.is_krystie_commit }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Import Krystie public key (for verification)
run: |
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
if [ -f .gitea/krystie-release.pub.asc ]; then
gpg --import .gitea/krystie-release.pub.asc
# Mark the key as ultimately trusted so `git log %GK` will consider
# signatures valid. Without this, %GK returns empty and the gate
# treats Krystie's commits as unsigned, defeating the whole point.
FP=$(gpg --list-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}')
echo "${FP}:6:" | gpg --import-ownertrust
echo "Imported and trusted Krystie public key: ${FP}"
# Configure git to call gpg for verification (it does by default,
# but explicit doesn't hurt) and not to require signed-by-default.
git config --global gpg.program gpg
else
echo "WARN: .gitea/krystie-release.pub.asc not found — gate will treat all commits as non-Krystie (i.e. allow)"
fi
- name: Run gate
id: gate
env:
GITHUB_REF: ${{ github.ref }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_BEFORE: ${{ github.event.before }}
run: |
python3 .gitea/krystie_gate.py
build-and-test:
name: "Build + ctest"
needs: static-gate
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config \
libssl-dev libboost-all-dev libdb++-dev libleveldb-dev \
librocksdb-dev libevent-dev libsodium-dev \
libsecp256k1-dev || true
# Some packages may not be available; the C++20 / RocksDB modernization
# is in flight, so missing deps are tolerable for v1 of the gate.
- name: Configure (daemon-only, no Qt)
run: |
mkdir -p build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_TESTS=ON \
-DBUILD_ROCKSDB=OFF \
|| (echo "::warning::CMake configure failed — likely WIP modernization. Allowing build skip for v1." && exit 0)
- name: Build
run: |
if [ -f build/build.ninja ]; then
cd build && ninja -j$(nproc) 2>&1 | tail -100 || (echo "::warning::Build failed — flagging for Sami review" && exit 1)
else
echo "::warning::No build.ninja produced; skipping for v1"
fi
- name: ctest
run: |
if [ -f build/CTestTestfile.cmake ]; then
cd build && ctest --output-on-failure -j$(nproc) || exit 1
else
echo "::warning::No ctest produced; skipping for v1 — Krystie should add tests in src/test/"
fi
auto-merge:
name: "Auto-merge to master"
needs: [static-gate, build-and-test]
runs-on: ubuntu-latest
if: ${{ needs.static-gate.result == 'success' && needs.build-and-test.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
- name: Fast-forward master to this branch
env:
GITEA_TOKEN: ${{ secrets.KRYSTIE_GITEA_TOKEN }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
# The wip branch is master + N Krystie commits. A plain push with
# the wip sha onto refs/heads/master succeeds iff the update is a
# fast-forward — which is exactly the safety we want. (Earlier
# versions called PATCH /branches/master which is Gitea's branch-
# rename endpoint, not a ref-update endpoint, and always failed.)
REPO="${GITHUB_REPOSITORY}" # owner/name
GIT_URL="http://localhost:3030/${REPO}.git"
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" "${SHA}:refs/heads/master" \
&& echo "Master fast-forwarded to ${SHA:0:12}" \
|| (echo "::error::Fast-forward push refused — master has likely diverged" && exit 1)
# Clean up the wip branch via the same push channel (delete = empty source).
git -c "http.extraHeader=Authorization: token ${GITEA_TOKEN}" \
push "${GIT_URL}" ":refs/heads/${BRANCH}" \
&& echo "Cleaned up wip branch ${BRANCH}" \
|| echo "::warning::Could not delete wip branch (it'll get pruned later)"
+7
View File
@@ -0,0 +1,7 @@
# Krystie runner log
This file records autonomous-runner activity. Each entry is a doc-only
edit produced by the demo worker; once OpenClaw is wired in this log
will be replaced by real work.
- [2026-04-29T06:57:30Z] triangles_v5#1 — Smoke-test the Krystie loop runner