pin_github_action.py 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. from __future__ import annotations
  2. import argparse
  3. import re
  4. import subprocess
  5. from collections.abc import Sequence
  6. from functools import lru_cache
  7. ACTION_VERSION_RE = re.compile(r"(?<=uses: )(?P<action>.*)@(?P<ref>[^#\s]+)")
  8. @lru_cache(maxsize=None)
  9. def get_sha(repo: str, ref: str) -> str:
  10. if len(ref) == 40:
  11. try:
  12. int(ref, 16)
  13. except ValueError:
  14. pass
  15. else:
  16. return ref
  17. cmd = ("git", "ls-remote", "--exit-code", f"https://github.com/{repo}", ref)
  18. out = subprocess.check_output(cmd)
  19. for line in out.decode().splitlines():
  20. sha, refname = line.split()
  21. if refname in (f"refs/tags/{ref}", f"refs/heads/{ref}"):
  22. return sha
  23. else:
  24. raise AssertionError(f"unknown ref: {repo}@{ref}")
  25. def extract_repo(action: str) -> str:
  26. # Some actions can be like `github/codeql-action/init`,
  27. # where init is just a directory. The ref is for the whole repo.
  28. # We only want the repo name though.
  29. parts = action.split("/")
  30. return f"{parts[0]}/{parts[1]}"
  31. def main(argv: Sequence[str] | None = None) -> int:
  32. parser = argparse.ArgumentParser()
  33. parser.add_argument("files", nargs="+", type=str, help="path to github actions file")
  34. args = parser.parse_args(argv)
  35. for fp in args.files:
  36. with open(fp, "r+") as f:
  37. newlines = []
  38. for line in f:
  39. m = ACTION_VERSION_RE.search(line)
  40. if not m:
  41. newlines.append(line)
  42. continue
  43. d = m.groupdict()
  44. sha = get_sha(extract_repo(d["action"]), ref=d["ref"])
  45. if sha != d["ref"]:
  46. line = ACTION_VERSION_RE.sub(rf"\1@{sha} # \2", line)
  47. newlines.append(line)
  48. f.seek(0)
  49. f.truncate()
  50. f.writelines(newlines)
  51. return 0
  52. if __name__ == "__main__":
  53. raise SystemExit(main())