update_verified_pins.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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. # KernelSU and ReSukiSU always build at latest (no pins to promote).
  21. # SUSFS is intentionally excluded: it is always resolved at latest so it
  22. # stays API-matched to the always-latest KernelSU-Next tree.
  23. ]
  24. SHA_RE = re.compile(r"^[0-9a-f]{40}$")
  25. def approved(env):
  26. return os.environ.get(env, "").strip() in ("1", "true", "True")
  27. def read_pins():
  28. """Return dict {var_name: sha} from the current PIN_* block in main.yml."""
  29. with open(MAIN, "r", encoding="utf-8") as fh:
  30. text = fh.read()
  31. pins = {}
  32. for var, _, _ in PINS:
  33. m = re.search(r"%s=\"([0-9a-f]{40})\"" % re.escape(var), text)
  34. pins[var] = m.group(1) if m else None
  35. return pins
  36. def build_changes():
  37. changes = [] # (var, old_sha, new_sha)
  38. promoted = {} # key -> sha for pins being set
  39. for var, sha_env, approve_env in PINS:
  40. new_sha = os.environ.get(sha_env, "").strip().lower()
  41. if not SHA_RE.match(new_sha):
  42. print(f" skip {var}: resolved SHA invalid ({new_sha})", file=sys.stderr)
  43. continue
  44. if not approved(approve_env):
  45. print(f" skip {var}: not approved to promote (disabled or a build failed)", file=sys.stderr)
  46. continue
  47. old_sha = read_pins().get(var)
  48. if old_sha == new_sha:
  49. print(f" unchanged {var} ({new_sha[:8]})")
  50. continue
  51. promoted[var] = new_sha
  52. changes.append((var, old_sha, new_sha))
  53. return changes, promoted
  54. def history_path():
  55. ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")
  56. d = os.path.join(REPO, ".github/pins/history")
  57. os.makedirs(d, exist_ok=True)
  58. return os.path.join(d, f"{ts}.json")
  59. def write_history(changes, path):
  60. record = {
  61. "updated_at": datetime.now(timezone.utc).isoformat(),
  62. "previous_verified": {key: (old or "") for key, old, _ in changes},
  63. }
  64. with open(path, "w", encoding="utf-8") as fh:
  65. json.dump(record, fh, indent=2)
  66. fh.write("\n")
  67. print(f" history written: {os.path.relpath(path, REPO)}")
  68. def apply_pins(promoted):
  69. with open(MAIN, "r", encoding="utf-8") as fh:
  70. text = fh.read()
  71. for key, sha in promoted.items():
  72. pattern = re.compile(r"%s=\"([0-9a-f]{40})\"" % re.escape(key))
  73. text, n = pattern.subn(lambda m: f"{key}=\"{sha}\"", text, count=1)
  74. if n == 0:
  75. print(f" ERROR: could not locate pin for {key}", file=sys.stderr)
  76. return False
  77. with open(MAIN, "w", encoding="utf-8") as fh:
  78. fh.write(text)
  79. return True
  80. def main():
  81. changes, promoted = build_changes()
  82. if not changes:
  83. print("No pins to promote (nothing changed or nothing approved).")
  84. return 0
  85. hist = history_path()
  86. write_history(changes, hist)
  87. if not apply_pins(promoted):
  88. return 1
  89. print("Promoted pins:")
  90. for key, old, new in changes:
  91. print(f" {key}: {old[:8] if old else 'none'} -> {new[:8]}")
  92. return 0
  93. if __name__ == "__main__":
  94. sys.exit(main())