dd3be612e3df1450de40a74fadc6df3c2885e9b1

Author
Marc Cornellà <marc@mcornella.com>
Committer
GitHub <noreply@github.com>
Date

Message

ci(dependencies): add automation for updating external dependencies (#12109)

Diff

This diff is truncated to protect this page.

  1diff --git a/.github/dependencies.yml b/.github/dependencies.yml
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..8c46395d7b69790daacd86ae92adcc04325f1dcd
  4--- /dev/null
  5+++ b/.github/dependencies.yml
  6@@ -0,0 +1,20 @@
  7+dependencies:
  8+  plugins/gitfast:
  9+    repo: felipec/git-completion
 10+    branch: master
 11+    version: tag:v1.3.7
 12+    postcopy: |
 13+      set -e
 14+      rm -rf git-completion.plugin.zsh Makefile README.adoc t tools
 15+      test -e git-completion.zsh && mv -f git-completion.zsh _git
 16+  plugins/z:
 17+    branch: master
 18+    repo: agkozak/zsh-z
 19+    version: 6bfe418332866d15373392164df11b4fbec2083f
 20+    precopy: |
 21+      set -e
 22+      test -e README.md && mv -f README.md MANUAL.md
 23+    postcopy: |
 24+      set -e
 25+      test -e _zshz && mv -f _zshz _z
 26+      test -e zsh-z.plugin.zsh && mv -f zsh-z.plugin.zsh z.plugin.zsh
 27diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml
 28new file mode 100644
 29index 0000000000000000000000000000000000000000..c27ad79982808324f1559b5e12d34ef1770d62db
 30--- /dev/null
 31+++ b/.github/workflows/dependencies.yml
 32@@ -0,0 +1,26 @@
 33+name: Update dependencies
 34+on:
 35+  workflow_dispatch: {}
 36+  # schedule:
 37+  #   - cron: '34 3 * * */8'
 38+
 39+jobs:
 40+  check:
 41+    name: Check for updates
 42+    runs-on: ubuntu-latest
 43+    steps:
 44+      - name: Checkout
 45+        if: github.repository == 'ohmyzsh/ohmyzsh'
 46+        uses: actions/checkout@v4
 47+      - name: Authenticate as @ohmyzsh
 48+        uses: ohmyzsh/github-app-token@v2
 49+        with:
 50+          app-id: ${{ secrets.OHMYZSH_APP_ID }}
 51+          private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }}
 52+      - name: Process dependencies
 53+        env:
 54+          GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
 55+          TMP_DIR: ${{ env.RUNNER_TEMP }}
 56+        run: |
 57+          gh auth login --with-token <<< "${GITHUB_TOKEN}"
 58+          python3 .github/workflows/dependencies/updater.py
 59diff --git a/.github/workflows/dependencies/updater.py b/.github/workflows/dependencies/updater.py
 60new file mode 100644
 61index 0000000000000000000000000000000000000000..5af19d14c5308f28f8865e9e0866d39cde24354b
 62--- /dev/null
 63+++ b/.github/workflows/dependencies/updater.py
 64@@ -0,0 +1,450 @@
 65+import os
 66+import subprocess
 67+import sys
 68+import requests
 69+import shutil
 70+import yaml
 71+from copy import deepcopy
 72+from typing import Optional, TypedDict
 73+
 74+# Get TMP_DIR variable from environment
 75+TMP_DIR = os.path.join(os.environ.get("TMP_DIR", "/tmp"), "ohmyzsh")
 76+# Relative path to dependencies.yml file
 77+DEPS_YAML_FILE = ".github/dependencies.yml"
 78+# Dry run flag
 79+DRY_RUN = os.environ.get("DRY_RUN", "0") == "1"
 80+
 81+import timeit
 82+class CodeTimer:
 83+  def __init__(self, name=None):
 84+    self.name = " '"  + name + "'" if name else ''
 85+
 86+  def __enter__(self):
 87+    self.start = timeit.default_timer()
 88+
 89+  def __exit__(self, exc_type, exc_value, traceback):
 90+    self.took = (timeit.default_timer() - self.start) * 1000.0
 91+    print('Code block' + self.name + ' took: ' + str(self.took) + ' ms')
 92+
 93+
 94+### YAML representation
 95+def str_presenter(dumper, data):
 96+  """
 97+  Configures yaml for dumping multiline strings
 98+  Ref: https://stackoverflow.com/a/33300001
 99+  """
100+  if len(data.splitlines()) > 1:  # check for multiline string
101+    return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
102+  return dumper.represent_scalar('tag:yaml.org,2002:str', data)
103+
104+yaml.add_representer(str, str_presenter)
105+yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
106+
107+
108+# Types
109+class DependencyDict(TypedDict):
110+  repo: str
111+  branch: str
112+  version: str
113+  precopy: Optional[str]
114+  postcopy: Optional[str]
115+
116+class DependencyYAML(TypedDict):
117+  dependencies: dict[str, DependencyDict]
118+
119+class UpdateStatus(TypedDict):
120+  has_updates: bool
121+  version: Optional[str]
122+  compare_url: Optional[str]
123+  head_ref: Optional[str]
124+  head_url: Optional[str]
125+
126+
127+class CommandRunner:
128+  class Exception(Exception):
129+    def __init__(self, message, returncode, stage, stdout, stderr):
130+      super().__init__(message)
131+      self.returncode = returncode
132+      self.stage = stage
133+      self.stdout = stdout
134+      self.stderr = stderr
135+
136+  @staticmethod
137+  def run_or_fail(command: list[str], stage: str, *args, **kwargs):
138+    if DRY_RUN and command[0] == "gh":
139+      command.insert(0, "echo")
140+
141+    result = subprocess.run(command, *args, capture_output=True, **kwargs)
142+
143+    if result.returncode != 0:
144+      raise CommandRunner.Exception(
145+        f"{stage} command failed with exit code {result.returncode}", returncode=result.returncode,
146+        stage=stage,
147+        stdout=result.stdout.decode("utf-8"),
148+        stderr=result.stderr.decode("utf-8")
149+      )
150+
151+    return result
152+
153+
154+class DependencyStore:
155+  store: DependencyYAML = {
156+    "dependencies": {}
157+  }
158+
159+  @staticmethod
160+  def set(data: DependencyYAML):
161+    DependencyStore.store = data
162+
163+  @staticmethod