Skip to content

Commit 7af0d4f

Browse files
Timna BrownTimna Brown
authored andcommitted
Add discussion automation
1 parent e60780b commit 7af0d4f

11 files changed

Lines changed: 1220 additions & 0 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Archive Discussions
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '20 6 1 * *'
7+
8+
permissions:
9+
contents: write
10+
discussions: read
11+
12+
jobs:
13+
archive:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v4
22+
with:
23+
python-version: '3.x'
24+
25+
- name: Install dependencies
26+
run: pip install requests
27+
28+
- name: Update archive index
29+
env:
30+
DISCUSSIONS_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
31+
ARCHIVE_CONFIG: docs/discussions/archive.json
32+
run: python .github/workflows/update_archive_index.py
33+
34+
- name: Archive discussions
35+
env:
36+
DISCUSSIONS_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
37+
ARCHIVE_CONFIG: docs/discussions/archive.json
38+
run: python .github/workflows/archive_discussions.py
39+
40+
- name: Commit and push changes
41+
env:
42+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
run: |
44+
git config --global user.name "github-actions[bot]"
45+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
46+
git add -A
47+
git commit -m "Archive discussions" || echo "No changes to commit"
48+
git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }}
49+
git push origin HEAD:${{ github.ref_name }}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import json
2+
import os
3+
import re
4+
import sys
5+
from datetime import datetime, timezone
6+
7+
import requests
8+
9+
CONFIG_PATH = os.environ.get("ARCHIVE_CONFIG", "docs/discussions/archive.json")
10+
TOKEN = os.environ.get("DISCUSSIONS_TOKEN") or os.environ.get("GITHUB_TOKEN")
11+
12+
QUERY = """
13+
query($org: String!, $number: Int!) {
14+
organization(login: $org) {
15+
discussion(number: $number) {
16+
title
17+
body
18+
url
19+
updatedAt
20+
}
21+
}
22+
}
23+
"""
24+
25+
REPLACEMENTS = {
26+
"\u2018": "'",
27+
"\u2019": "'",
28+
"\u201c": '"',
29+
"\u201d": '"',
30+
"\u2013": "-",
31+
"\u2014": "-",
32+
"\u2026": "...",
33+
"\u00a0": " ",
34+
}
35+
36+
37+
def normalize_ascii(text):
38+
for src, dst in REPLACEMENTS.items():
39+
text = text.replace(src, dst)
40+
return text.encode("ascii", "ignore").decode("ascii")
41+
42+
43+
def parse_org_and_number(url, default_org):
44+
if not url:
45+
return default_org, None
46+
47+
org_match = re.search(r"github\.com/orgs/([^/]+)/discussions/(\d+)", url)
48+
if org_match:
49+
return org_match.group(1), int(org_match.group(2))
50+
51+
repo_match = re.search(r"github\.com/([^/]+)/[^/]+/discussions/(\d+)", url)
52+
if repo_match:
53+
return repo_match.group(1), int(repo_match.group(2))
54+
55+
number_match = re.search(r"/discussions/(\d+)", url)
56+
if number_match:
57+
return default_org, int(number_match.group(1))
58+
59+
return default_org, None
60+
61+
62+
def load_config(path):
63+
if not os.path.exists(path):
64+
print(f"Config not found: {path}")
65+
sys.exit(1)
66+
67+
with open(path, "r", encoding="utf-8") as handle:
68+
return json.load(handle)
69+
70+
71+
def fetch_discussion(org, number, token):
72+
response = requests.post(
73+
"https://api.github.com/graphql",
74+
headers={"Authorization": f"bearer {token}"},
75+
json={"query": QUERY, "variables": {"org": org, "number": number}},
76+
timeout=30,
77+
)
78+
response.raise_for_status()
79+
payload = response.json()
80+
if "errors" in payload:
81+
print(f"GraphQL error for discussion {number}: {payload['errors']}")
82+
return None
83+
84+
org_data = payload.get("data", {}).get("organization")
85+
if not org_data:
86+
print(f"Organization not found: {org}")
87+
return None
88+
89+
return org_data.get("discussion")
90+
91+
92+
def render_discussion(discussion):
93+
title = normalize_ascii(discussion.get("title", "Untitled"))
94+
body = normalize_ascii(discussion.get("body", "")).strip()
95+
url = normalize_ascii(discussion.get("url", ""))
96+
updated_at = discussion.get("updatedAt")
97+
98+
if updated_at:
99+
last_updated = updated_at.split("T")[0]
100+
else:
101+
last_updated = datetime.now(timezone.utc).strftime("%Y-%m-%d")
102+
103+
parts = [
104+
f"# {title}",
105+
"",
106+
f"Source discussion: {url}",
107+
f"Last updated: {last_updated}",
108+
"",
109+
body,
110+
"",
111+
]
112+
return "\n".join(parts)
113+
114+
115+
def write_if_changed(path, content):
116+
existing = ""
117+
if os.path.exists(path):
118+
with open(path, "r", encoding="utf-8") as handle:
119+
existing = handle.read()
120+
121+
if existing == content:
122+
print(f"No changes: {path}")
123+
return False
124+
125+
os.makedirs(os.path.dirname(path), exist_ok=True)
126+
with open(path, "w", encoding="utf-8") as handle:
127+
handle.write(content)
128+
print(f"Updated: {path}")
129+
return True
130+
131+
132+
def main():
133+
if not TOKEN:
134+
print("DISCUSSIONS_TOKEN or GITHUB_TOKEN is required")
135+
sys.exit(1)
136+
137+
config = load_config(CONFIG_PATH)
138+
default_org = config.get("organization")
139+
items = config.get("items", [])
140+
141+
if not items:
142+
print("No items to archive")
143+
return
144+
145+
for item in items:
146+
output_path = item.get("output_path")
147+
if not output_path:
148+
print("Skipping item with no output_path")
149+
continue
150+
151+
org, number = parse_org_and_number(item.get("discussion_url", ""), default_org)
152+
if not org or not number:
153+
print(f"Skipping item with no discussion number: {output_path}")
154+
continue
155+
156+
discussion = fetch_discussion(org, number, TOKEN)
157+
if not discussion:
158+
print(f"Discussion not found: {org} #{number}")
159+
continue
160+
161+
content = render_discussion(discussion)
162+
write_if_changed(output_path, content)
163+
164+
165+
if __name__ == "__main__":
166+
main()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Create Monthly Catalog Discussion
2+
3+
on:
4+
schedule:
5+
- cron: '5 6 1 * *'
6+
workflow_dispatch:
7+
inputs:
8+
target_month:
9+
description: 'Target month (1-12)'
10+
required: false
11+
target_year:
12+
description: 'Target year (YYYY)'
13+
required: false
14+
15+
permissions:
16+
contents: write
17+
discussions: write
18+
19+
jobs:
20+
create:
21+
runs-on: ubuntu-latest
22+
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v4
29+
with:
30+
python-version: '3.x'
31+
32+
- name: Install dependencies
33+
run: pip install requests
34+
35+
- name: Create monthly discussion
36+
env:
37+
DISCUSSIONS_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
38+
ARCHIVE_CONFIG: docs/discussions/archive.json
39+
TEMPLATE_PATH: docs/discussions/templates/catalog-index-template.md
40+
DISCUSSION_CATEGORY: Announcements
41+
DISCUSSION_LABEL: documentation
42+
TARGET_MONTH: ${{ inputs.target_month }}
43+
TARGET_YEAR: ${{ inputs.target_year }}
44+
run: python .github/workflows/create_monthly_discussion.py
45+
46+
- name: Update archive index
47+
env:
48+
DISCUSSIONS_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
49+
ARCHIVE_CONFIG: docs/discussions/archive.json
50+
run: python .github/workflows/update_archive_index.py
51+
52+
- name: Archive discussions
53+
env:
54+
DISCUSSIONS_TOKEN: ${{ secrets.TRAFFIC_TOKEN }}
55+
ARCHIVE_CONFIG: docs/discussions/archive.json
56+
run: python .github/workflows/archive_discussions.py
57+
58+
- name: Commit and push changes
59+
env:
60+
TOKEN: ${{ secrets.GITHUB_TOKEN }}
61+
run: |
62+
git config --global user.name "github-actions[bot]"
63+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
64+
git add -A
65+
git commit -m "Archive discussions" || echo "No changes to commit"
66+
git remote set-url origin https://x-access-token:${TOKEN}@github.com/${{ github.repository }}
67+
git push origin HEAD:${{ github.ref_name }}

0 commit comments

Comments
 (0)