Skip to content

Commit e526c6f

Browse files
authored
feat: ✨ add CI scripts for automatic guild features updates (#51)
2 parents e85d528 + 976eb6c commit e526c6f

File tree

10 files changed

+341
-0
lines changed

10 files changed

+341
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
with:
22+
fetch-depth: 0 # Fetch all history for all branches
23+
- name: "Setup Python"
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.13"
27+
- name: "Install uv"
28+
uses: astral-sh/setup-uv@v6
29+
with:
30+
enable-cache: true
31+
- name: Sync dependencies
32+
run: uv sync --no-python-downloads --group dev --group ci
33+
- name: "Run guild features sync"
34+
run: uv run python -m scripts.sync_guild_features
35+
env:
36+
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, GUILD_FEATURES_GIST_URL
10+
from ..utils import create_update_pr, format_path, lint_path
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import requests
2+
3+
# https://gist.github.com/advaith1/a82065c4049345b38f526146d6ecab96
4+
GUILD_FEATURES_GIST_URL = (
5+
"https://gist.githubusercontent.com/advaith1/a82065c4049345b38f526146d6ecab96/raw/guildfeatures.json"
6+
)
7+
8+
9+
def get_features_blob() -> list[str]:
10+
"""
11+
Fetches the latest guild features from the Gist URL.
12+
13+
Returns
14+
-------
15+
list[str]: A list of guild feature strings.
16+
"""
17+
response = requests.get(GUILD_FEATURES_GIST_URL, timeout=10)
18+
19+
if response.status_code != 200:
20+
raise ValueError(f"Failed to fetch guild features: {response.status_code}")
21+
22+
return response.json()
23+
24+
25+
__all__ = ("get_features_blob", "GUILD_FEATURES_GIST_URL")

scripts/utils/__init__.py

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

scripts/utils/automated_ruff.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import subprocess
2+
from pathlib import Path
3+
from ruff.__main__ import ( # type: ignore[import-untyped]
4+
find_ruff_bin,
5+
)
6+
7+
8+
def format_path(path: Path) -> str:
9+
"""
10+
Formats the given path with ruff.
11+
12+
Parameters
13+
----------
14+
path (Path): The path to format.
15+
"""
16+
result = subprocess.run(
17+
[find_ruff_bin(), "format", str(path.absolute())],
18+
text=True,
19+
capture_output=True,
20+
check=True,
21+
cwd=Path.cwd(),
22+
)
23+
result.check_returncode()
24+
return result.stdout
25+
26+
27+
def lint_path(path: Path) -> str:
28+
"""
29+
Lints the given path with ruff.
30+
31+
Parameters
32+
----------
33+
path (Path): The path to format.
34+
"""
35+
result = subprocess.run(
36+
[find_ruff_bin(), "check", "--fix", str(path.absolute())],
37+
text=True,
38+
capture_output=True,
39+
check=True,
40+
cwd=Path.cwd(),
41+
)
42+
result.check_returncode()
43+
return result.stdout
44+
45+
46+
__all__ = ("format_path", "lint_path")

scripts/utils/pr.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
commit_message (str): The commit message to use.
30+
branch_prefix (str): The prefix for the branch name.
31+
title (str): The title of the pull request.
32+
body (str): The body of the pull request.
33+
path (str | Path): The path or glob to the file to commit.
34+
"""
35+
github = Github(os.environ["GITHUB_TOKEN"])
36+
repo = github.get_repo(os.environ["GITHUB_REPOSITORY"])
37+
base_branch = subprocess.run(
38+
["/usr/bin/git", "rev-parse", "--abbrev-ref", "HEAD"],
39+
check=True,
40+
capture_output=True,
41+
text=True,
42+
).stdout.strip()
43+
ident()
44+
print(f"Creating/updating PR in {repo.full_name} on branch {base_branch} with prefix {branch_prefix}")
45+
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", "fetch", "origin", branch_name],
53+
check=False,
54+
)
55+
subprocess.run(
56+
["/usr/bin/git", "checkout", branch_name],
57+
check=False,
58+
)
59+
pull_request = pr
60+
break
61+
else:
62+
branch_name = f"{branch_prefix}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
63+
subprocess.run(
64+
["/usr/bin/git", "checkout", "-b", branch_name],
65+
check=False,
66+
)
67+
68+
if not subprocess.run(["/usr/bin/git", "status", "--porcelain"], check=False, capture_output=True).stdout:
69+
print("No changes to commit.")
70+
return
71+
72+
subprocess.run(
73+
["/usr/bin/git", "add", str(path)],
74+
check=False,
75+
)
76+
subprocess.run(
77+
["/usr/bin/git", "commit", "-m", title],
78+
check=False,
79+
)
80+
subprocess.run(
81+
["/usr/bin/git", "push", "-u", "origin", branch_name],
82+
check=False,
83+
)
84+
85+
if not pull_request:
86+
pull_request = repo.create_pull(
87+
title=title,
88+
body=body,
89+
head=branch_name,
90+
base=base_branch,
91+
)
92+
print(f"Created new PR #{pull_request.number}: {pull_request.title}")
93+
94+
95+
__all__ = ("create_update_pr",)

0 commit comments

Comments
 (0)