update_verified_pins.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env python3
  2. """Promote latest-built commits to the verified PIN_* block in main.yml.
  3. Update mode: builds happen at latest tips. After the build, if the components
  4. built this run all passed, their used SHAs overwrite the audited pins. Previous
  5. audited pins are recorded in .github/pins/history/<date>.json before being
  6. replaced. Only components that were enabled AND passed are promoted; unrelated
  7. pins are left untouched.
  8. """
  9. import json
  10. import os
  11. import re
  12. import sys
  13. from datetime import datetime, timezone
  14. REPO = os.environ.get("GITHUB_WORKSPACE", ".")
  15. MAIN = os.path.join(REPO, ".github/workflows/main.yml")
  16. # (pin-var, resolved-sha-env, approval-env)
  17. # approval env is "1"/"true" when the component was enabled and all its builds passed.
  18. PINS = [
  19. ("PIN_NOMOUNT", "NOMOUNT_SHA", "APPROVE_NOMOUNT"),
  20. ("PIN_KERNELSU", "KERNELSU_SHA", "APPROVE_KERNELSU"),
  21. ("PIN_RESUKISU", "RESUKISU_SHA", "APPROVE_RESUKISU"),
  22. # SUSFS is intentionally excluded: it is always resolved at latest so it
  23. # stays API-matched to the always-latest KernelSU-Next tree.
  24. ]
  25. SHA_RE = re.compile(r"^[0-9a-f]{40}$")
  26. def approved(env):
  27. return os.environ.get(env, "").strip() in ("1", "true", "True")
  28. def read_pins():
  29. """Return dict {var_name: sha} from the current PIN_* block in main.yml."""
  30. with open(MAIN, "r", encoding="utf-8") as fh:
  31. text = fh.read()
  32. pins = {}
  33. for var, _, _ in PINS:
  34. m = re.search(r"%s=\"([0-9a-f]{40})\"" % re.escape(var), text)
  35. pins[var] = m.group(1) if m else None
  36. return pins
  37. def build_changes():
  38. changes = [] # (var, old_sha, new_sha)
  39. promoted = {} # key -> sha for pins being set
  40. for var, sha_env, approve_env in PINS:
  41. new_sha = os.environ.get(sha_env, "").strip().lower()
  42. if not SHA_RE.match(new_sha):
  43. print(f" skip {var}: resolved SHA invalid ({new_sha})", file=sys.stderr)
  44. continue
  45. if not approved(approve_env):
  46. print(f" skip {var}: not approved to promote (disabled or a build failed)", file=sys.stderr)
  47. continue
  48. old_sha = read_pins().get(var)
  49. if old_sha == new_sha:
  50. print(f" unchanged {var} ({new_sha[:8]})")
  51. continue
  52. promoted[var] = new_sha
  53. changes.append((var, old_sha, new_sha))
  54. return changes, promoted
  55. def history_path():
  56. ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
  57. d = os.path.join(REPO, ".github/pins/history")
  58. os.makedirs(d, exist_ok=True)
  59. return os.path.join(d, f"{ts}.json")
  60. def write_history(changes, path):
  61. record = {
  62. "updated_at": datetime.now(timezone.utc).isoformat(),
  63. "previous_verified": {key: (old or "") for key, old, _ in changes},
  64. }
  65. with open(path, "w", encoding="utf-8") as fh:
  66. json.dump(record, fh, indent=2)
  67. fh.write("\n")
  68. print(f" history written: {os.path.relpath(path, REPO)}")
  69. def apply_pins(promoted):
  70. with open(MAIN, "r", encoding="utf-8") as fh:
  71. text = fh.read()
  72. for key, sha in promoted.items():
  73. pattern = re.compile(r"%s=\"([0-9a-f]{40})\"" % re.escape(key))
  74. text, n = pattern.subn(lambda m: f"{key}=\"{sha}\"", text, count=1)
  75. if n == 0:
  76. print(f" ERROR: could not locate pin for {key}", file=sys.stderr)
  77. return False
  78. with open(MAIN, "w", encoding="utf-8") as fh:
  79. fh.write(text)
  80. return True
  81. def main():
  82. changes, promoted = build_changes()
  83. if not changes:
  84. print("No pins to promote (nothing changed or nothing approved).")
  85. return 0
  86. hist = history_path()
  87. write_history(changes, hist)
  88. if not apply_pins(promoted):
  89. return 1
  90. print("Promoted pins:")
  91. for key, old, new in changes:
  92. print(f" {key}: {old[:8] if old else 'none'} -> {new[:8]}")
  93. return 0
  94. if __name__ == "__main__":
  95. sys.exit(main())