pin_github_action.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. import os
  5. import re
  6. import urllib.request
  7. from functools import lru_cache
  8. from typing import Sequence
  9. from urllib.error import HTTPError
  10. @lru_cache(maxsize=None)
  11. def get_sha(repo: str, ref: str, github_token: str) -> str:
  12. if len(ref) == 40:
  13. try:
  14. int(ref, 16)
  15. return ref
  16. except ValueError:
  17. pass
  18. try:
  19. resp = urllib.request.urlopen(
  20. urllib.request.Request(
  21. f"https://api.github.com/repos/{repo}/commits/{ref}",
  22. method="GET",
  23. headers={
  24. "Accept": "application/vnd.github+json",
  25. "Authorization": f"token {github_token}",
  26. # A user agent is required. Lol.
  27. "User-Agent": "python-requests/2.26.0",
  28. },
  29. )
  30. )
  31. except HTTPError as e:
  32. print(f"Status {e.code} while resolving {ref} for {repo}.")
  33. if e.code == 403:
  34. print("You most likely didn't authorize your token for SAML to the getsentry org.")
  35. return ref
  36. data: dict[str, str] = json.load(resp)
  37. return data["sha"]
  38. def extract_repo(action: str) -> str:
  39. # Some actions can be like `github/codeql-action/init`,
  40. # where init is just a directory. The ref is for the whole repo.
  41. # We only want the repo name though.
  42. parts = action.split("/")
  43. return f"{parts[0]}/{parts[1]}"
  44. def main(argv: Sequence[str] | None = None) -> int:
  45. parser = argparse.ArgumentParser()
  46. parser.add_argument("files", nargs="+", type=str, help="path to github actions file")
  47. args = parser.parse_args(argv)
  48. GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
  49. if not GITHUB_TOKEN:
  50. raise SystemExit("GITHUB_TOKEN not set.")
  51. ACTION_VERSION_RE = re.compile(r"(?<=uses: )(?P<action>.*)@(?P<ref>.+?)\b")
  52. for fp in args.files:
  53. with open(fp, "r+") as f:
  54. newlines = []
  55. for line in f:
  56. m = ACTION_VERSION_RE.search(line)
  57. if not m:
  58. newlines.append(line)
  59. continue
  60. d = m.groupdict()
  61. sha = get_sha(extract_repo(d["action"]), ref=d["ref"], github_token=GITHUB_TOKEN)
  62. if sha != d["ref"]:
  63. line = ACTION_VERSION_RE.sub(rf"\1@{sha} # \2", line)
  64. newlines.append(line)
  65. f.seek(0)
  66. f.truncate()
  67. f.writelines(newlines)
  68. return 0
  69. if __name__ == "__main__":
  70. raise SystemExit(main())