pin_github_action.py 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. from __future__ import annotations
  2. import argparse
  3. import re
  4. import subprocess
  5. from functools import lru_cache
  6. from typing import Sequence
  7. @lru_cache(maxsize=None)
  8. def get_sha(repo: str, ref: str) -> str:
  9. if len(ref) == 40:
  10. try:
  11. int(ref, 16)
  12. except ValueError:
  13. pass
  14. else:
  15. return ref
  16. cmd = ("git", "ls-remote", "--exit-code", f"https://github.com/{repo}", ref)
  17. out = subprocess.check_output(cmd)
  18. for line in out.decode().splitlines():
  19. sha, refname = line.split()
  20. if refname in (f"refs/tags/{ref}", f"refs/heads/{ref}"):
  21. return sha
  22. else:
  23. raise AssertionError(f"unknown ref: {repo}@{ref}")
  24. def extract_repo(action: str) -> str:
  25. # Some actions can be like `github/codeql-action/init`,
  26. # where init is just a directory. The ref is for the whole repo.
  27. # We only want the repo name though.
  28. parts = action.split("/")
  29. return f"{parts[0]}/{parts[1]}"
  30. def main(argv: Sequence[str] | None = None) -> int:
  31. parser = argparse.ArgumentParser()
  32. parser.add_argument("files", nargs="+", type=str, help="path to github actions file")
  33. args = parser.parse_args(argv)
  34. ACTION_VERSION_RE = re.compile(r"(?<=uses: )(?P<action>.*)@(?P<ref>.+?)\b")
  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())