Skip to content

Commit 5f611b5

Browse files
committed
Add workflow to publish release notes directly to discourse
1 parent 42243c6 commit 5f611b5

File tree

3 files changed

+173
-0
lines changed

3 files changed

+173
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Publish Release Notes to Discourse
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish-to-discourse:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
with:
15+
persist-credentials: false
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v4
19+
with:
20+
python-version: "3.11"
21+
22+
- name: Install dependencies
23+
run: pip install requests
24+
25+
- name: Publish release to Discourse
26+
env:
27+
DISCOURSE_API_KEY: ${{ secrets.DISCOURSE_API_KEY }}
28+
DISCOURSE_USERNAME: "pymc-bot"
29+
DISCOURSE_URL: "https://discourse.pymc.io"
30+
DISCOURSE_CATEGORY: "Development"
31+
RELEASE_TAG: ${{ github.event.release.tag_name }}
32+
RELEASE_BODY: ${{ github.event.release.body }}
33+
RELEASE_URL: ${{ github.event.release.html_url }}
34+
REPO_NAME: ${{ github.repository }}
35+
run: python ./scripts/publish_release_notes_to_discourse.py

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ lines-between-types = 1
8585
"scripts/run_mypy.py" = [
8686
"T201", # No print statements
8787
]
88+
"scripts/publish_release_notes_to_discourse.py" = [
89+
"T201", # No print statements
90+
]
8891
"*.ipynb" = [
8992
"T201", # No print statements
9093
]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
5+
import requests
6+
7+
8+
def load_config() -> dict[str, str]:
9+
env_config = {
10+
"DISCOURSE_URL": os.getenv("DISCOURSE_URL"),
11+
"DISCOURSE_API_KEY": os.getenv("DISCOURSE_API_KEY"),
12+
"DISCOURSE_USERNAME": os.getenv("DISCOURSE_USERNAME"),
13+
"DISCOURSE_CATEGORY": os.getenv("DISCOURSE_CATEGORY"),
14+
# Release information from GitHub
15+
"RELEASE_TAG": os.getenv("RELEASE_TAG"),
16+
"RELEASE_BODY": os.getenv("RELEASE_BODY"),
17+
"RELEASE_URL": os.getenv("RELEASE_URL"),
18+
"REPO_NAME": os.getenv("REPO_NAME"),
19+
}
20+
21+
missing_env_values = {key: value for key, value in env_config.items() if value is None}
22+
if missing_env_values:
23+
raise RuntimeError(
24+
f"Missing required environment variables: {', '.join(missing_env_values.keys())}"
25+
)
26+
return env_config
27+
28+
29+
def find_category_id(config: dict[str, str]) -> int:
30+
headers = {
31+
"Api-Key": config["DISCOURSE_API_KEY"],
32+
"Api-Username": config["DISCOURSE_USERNAME"],
33+
"Content-Type": "application/json",
34+
}
35+
36+
category_to_find = config["DISCOURSE_CATEGORY"].lower()
37+
url = f"{config['DISCOURSE_URL']}/categories.json"
38+
try:
39+
response = requests.get(url, headers=headers)
40+
response.raise_for_status()
41+
data = response.json()
42+
except Exception as e:
43+
print("Error fetching categories")
44+
raise
45+
46+
if data.get("category_list") and data["category_list"].get("categories"):
47+
categories = data["category_list"]["categories"]
48+
49+
for category in categories:
50+
cat_id = category.get("id")
51+
cat_name = category.get("name")
52+
if cat_name.lower() == category_to_find:
53+
return int(cat_id)
54+
55+
raise ValueError(f"Category '{category_to_find}' not found")
56+
57+
58+
def format_release_content(config: dict[str, str]) -> tuple[str, str]:
59+
title = f"🚀 Release {config['RELEASE_TAG']}"
60+
repo_name = config["REPO_NAME"].split("/")[1]
61+
content = f"""A new release of **{repo_name}** is now available!
62+
63+
## 📦 Release Information
64+
65+
- **Version:** `{config["RELEASE_TAG"]}`
66+
- **Repository:** [{config["REPO_NAME"]}](https://github.com/{config["REPO_NAME"]})
67+
- **Release Page:** [View on GitHub]({config["RELEASE_URL"]})
68+
- Note: It may take some time for the release to appear on PyPI and conda-forge.
69+
70+
## 📋 Release Notes
71+
72+
{config["RELEASE_BODY"]}
73+
74+
---
75+
76+
*This post was automatically generated from the GitHub release.*
77+
"""
78+
79+
return title, content
80+
81+
82+
def publish_release_to_discourse(config: dict[str, str]) -> bool:
83+
print("🎯 GitHub Release to Discourse Publisher")
84+
print(f"Release: {config['RELEASE_TAG']}")
85+
print(f"Repository: {config['REPO_NAME']}")
86+
print(f"Target Forum: {config['DISCOURSE_URL']}")
87+
print(f"Target Category: {config['DISCOURSE_CATEGORY']}")
88+
print("-" * 50)
89+
90+
category_id = find_category_id(config)
91+
print(f"Publishing to category: {config['DISCOURSE_CATEGORY']} (ID: {category_id})")
92+
93+
# Format the release content
94+
title, content = format_release_content(config)
95+
96+
# Create the topic data
97+
topic_data = {"title": title, "raw": content, "category": category_id}
98+
99+
# Post to Discourse
100+
headers = {
101+
"Api-Key": config["DISCOURSE_API_KEY"],
102+
"Api-Username": config["DISCOURSE_USERNAME"],
103+
"Content-Type": "application/json",
104+
}
105+
url = f"{config['DISCOURSE_URL']}/posts.json"
106+
107+
try:
108+
response = requests.post(url, headers=headers, data=topic_data)
109+
response.raise_for_status()
110+
111+
data = response.json()
112+
topic_id = data.get("topic_id")
113+
post_id = data.get("id")
114+
115+
print("✅ Release published successfully!")
116+
print(f"Topic ID: {topic_id}")
117+
print(f"Post ID: {post_id}")
118+
print(f"URL: {config['DISCOURSE_URL']}/t/{topic_id}")
119+
return True
120+
121+
except requests.exceptions.RequestException as e:
122+
print(f"❌ Error publishing release: {e}")
123+
if hasattr(e, "response") and e.response is not None:
124+
print(f"Response status: {e.response.status_code}")
125+
try:
126+
error_data = e.response.json()
127+
print(f"Error details: {error_data}")
128+
except Exception:
129+
print(f"Response content: {e.response.text}")
130+
raise
131+
132+
133+
if __name__ == "__main__":
134+
config = load_config()
135+
publish_release_to_discourse(config)

0 commit comments

Comments
 (0)