Skip to content

Commit bcca819

Browse files
committed
✨ add CI scripts for automatic guild features updates
1 parent 2078cef commit bcca819

File tree

9 files changed

+331
-0
lines changed

9 files changed

+331
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: "Sync Guild Features"
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '0 0 * * 1,5'
7+
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions: write-all
14+
15+
jobs:
16+
sync-guild-features:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: "Checkout Repository"
20+
uses: actions/checkout@v4
21+
- name: "Setup Python"
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.13"
25+
- name: "Install uv"
26+
uses: astral-sh/setup-uv@v6
27+
with:
28+
enable-cache: true
29+
- name: Sync dependencies
30+
run: uv sync --no-python-downloads --group dev --group ci
31+
- name: "Run guild features sync"
32+
run: uv run python -m scripts.sync_guild_features
33+
env:
34+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ dev = [
7474
"pytest-asyncio~=0.24.0",
7575
"ruff>=0.11.9",
7676
]
77+
ci = [
78+
"pygithub>=2.7.0",
79+
]
7780

7881
[tool.hatch.version]
7982
source = "vcs"

scripts/__init__.py

Whitespace-only changes.

scripts/sync_guild_features/__init__.py

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import sys
3+
import json
4+
from pathlib import Path
5+
from github import Github
6+
from github import Auth
7+
import re
8+
9+
from .utils import get_features_blob, format_path, lint_path, GUILD_FEATURES_GIST_URL
10+
from ..utils import create_update_pr
11+
12+
CI = os.environ.get("CI", "false").lower() in ("true", "1", "yes")
13+
14+
GUILD_FEATURES_PATH = Path.cwd() / "discord" / "types" / "guild.py"
15+
GUILD_FEATURES_VARIABLE_NAME = "GuildFeature"
16+
GUILD_FEATURES_PATTERN = re.compile(rf"{GUILD_FEATURES_VARIABLE_NAME}\s*=\s*Literal\[(.*?)\]", re.DOTALL)
17+
18+
19+
def main():
20+
with GUILD_FEATURES_PATH.open(encoding="utf-8") as file:
21+
content = file.read()
22+
23+
features_blob = get_features_blob()
24+
features_blob.sort()
25+
features_blob_str = ", ".join(f'"{feature}"' for feature in features_blob)
26+
new_content = GUILD_FEATURES_PATTERN.sub(f"{GUILD_FEATURES_VARIABLE_NAME} = Literal[{features_blob_str}]", content)
27+
28+
with GUILD_FEATURES_PATH.open("w", encoding="utf-8") as file:
29+
file.write(new_content)
30+
31+
format_path(GUILD_FEATURES_PATH)
32+
lint_path(GUILD_FEATURES_PATH)
33+
with GUILD_FEATURES_PATH.open(encoding="utf-8") as file:
34+
updated_content = file.read()
35+
if updated_content == content:
36+
print("No changes made to guild features.")
37+
return
38+
if CI:
39+
create_update_pr(
40+
commit_message="chore: Update guild features",
41+
branch_prefix="sync-guild-features",
42+
title="Update guild features",
43+
body=f"This pull request automatically updates the guild features type, based on {GUILD_FEATURES_GIST_URL.split('/raw')[0]}. Please review the changes. and merge if everything looks good.",
44+
path=GUILD_FEATURES_PATH,
45+
)
46+
else:
47+
print("Not running in CI, skipping PR creation.")
48+
49+
50+
if __name__ == "__main__":
51+
main()

scripts/sync_guild_features/utils.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import requests
2+
import subprocess
3+
from pathlib import Path
4+
from ruff.__main__ import ( # type: ignore[import-untyped]
5+
find_ruff_bin,
6+
)
7+
8+
# https://gist.github.com/advaith1/a82065c4049345b38f526146d6ecab96
9+
GUILD_FEATURES_GIST_URL = (
10+
"https://gist.githubusercontent.com/advaith1/a82065c4049345b38f526146d6ecab96/raw/guildfeatures.json"
11+
)
12+
13+
14+
def get_features_blob() -> list[str]:
15+
"""
16+
Fetches the latest guild features from the Gist URL.
17+
18+
Returns
19+
-------
20+
list[str]: A list of guild feature strings.
21+
"""
22+
response = requests.get(GUILD_FEATURES_GIST_URL, timeout=10)
23+
24+
if response.status_code != 200:
25+
raise ValueError(f"Failed to fetch guild features: {response.status_code}")
26+
27+
return response.json()
28+
29+
30+
def format_path(path: Path) -> str:
31+
"""
32+
Formats the given path with ruff.
33+
34+
Parameters
35+
----------
36+
path (Path): The path to format.
37+
"""
38+
result = subprocess.run(
39+
[find_ruff_bin(), "format", str(path.absolute())],
40+
text=True,
41+
capture_output=True,
42+
check=True,
43+
cwd=Path.cwd(),
44+
)
45+
result.check_returncode()
46+
return result.stdout
47+
48+
49+
def lint_path(path: Path) -> str:
50+
"""
51+
Lints the given path with ruff.
52+
53+
Parameters
54+
----------
55+
path (Path): The path to format.
56+
"""
57+
result = subprocess.run(
58+
[find_ruff_bin(), "check", "--fix", str(path.absolute())],
59+
text=True,
60+
capture_output=True,
61+
check=True,
62+
cwd=Path.cwd(),
63+
)
64+
result.check_returncode()
65+
return result.stdout
66+
67+
68+
__all__ = ("get_features_blob", "format_path", "lint_path", "GUILD_FEATURES_GIST_URL")

scripts/utils/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .pr import create_update_pr
2+
3+
__all__ = ("create_update_pr",)

scripts/utils/pr.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from __future__ import annotations
2+
import os
3+
import sys
4+
import datetime
5+
import subprocess
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING
8+
9+
from github import Github
10+
from github import Auth
11+
12+
if TYPE_CHECKING:
13+
from github.PullRequest import PullRequest
14+
15+
16+
def ident() -> None:
17+
subprocess.run(["/usr/bin/git", "config", "--global", "user.name", "github-actions[bot]"], check=True)
18+
subprocess.run(
19+
["/usr/bin/git", "config", "--global", "user.email", "github-actions[bot]@users.noreply.github.com"], check=True
20+
)
21+
22+
23+
def create_update_pr(commit_message: str, branch_prefix: str, title: str, body: str, path: str | Path) -> None:
24+
"""
25+
Creates or updates a pull request with the given title and body.
26+
27+
Parameters
28+
----------
29+
title (str): The title of the pull request.
30+
branch_prefix (str): The prefix for the branch name.
31+
body (str): The body of the pull request.
32+
base_branch (str): The base branch to create the PR against. Defaults to "main".
33+
"""
34+
github = Github(os.environ["GITHUB_TOKEN"])
35+
repo = github.get_repo(os.environ["GITHUB_REPOSITORY"])
36+
base_branch = subprocess.run(
37+
["/usr/bin/git", "rev-parse", "--abbrev-ref", "HEAD"],
38+
check=True,
39+
capture_output=True,
40+
text=True,
41+
).stdout.strip()
42+
ident()
43+
print(f"Creating/updating PR in {repo.full_name} on branch {base_branch} with prefix {branch_prefix}")
44+
45+
# get all prs and see if there is one whose branch starts with the branch_prefix
46+
prs = repo.get_pulls(state="open", sort="created", base=base_branch)
47+
pull_request: None | PullRequest = None
48+
for pr in prs:
49+
if pr.head.ref.startswith(branch_prefix):
50+
branch_name: str = pr.head.ref
51+
subprocess.run(
52+
["/usr/bin/git", "checkout", branch_name],
53+
check=False,
54+
)
55+
pull_request = pr
56+
break
57+
else:
58+
branch_name = f"{branch_prefix}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
59+
subprocess.run(
60+
["/usr/bin/git", "checkout", "-b", branch_name],
61+
check=False,
62+
)
63+
64+
if not subprocess.run(["/usr/bin/git", "status", "--porcelain"], check=False, capture_output=True).stdout:
65+
print("No changes to commit.")
66+
return
67+
68+
subprocess.run(
69+
["/usr/bin/git", "add", str(path)],
70+
check=False,
71+
)
72+
subprocess.run(
73+
["/usr/bin/git", "commit", "-m", title],
74+
check=False,
75+
)
76+
subprocess.run(
77+
["/usr/bin/git", "push", "-u", "origin", branch_name],
78+
check=False,
79+
)
80+
81+
if not pull_request:
82+
pull_request = repo.create_pull(
83+
title=title,
84+
body=body,
85+
head=branch_name,
86+
base=base_branch,
87+
)
88+
print(f"Created new PR #{pull_request.number}: {pull_request.title}")
89+
90+
91+
__all__ = ("create_update_pr",)

0 commit comments

Comments
 (0)