Skip to content

Commit 88706fa

Browse files
mhduiyBLumia
authored andcommitted
chore: Add workflow to automate changelog updates
This workflow automates the process of updating the changelog based on commits since the last changelog entry. It includes options for versioning, maintainer details, and creating a pull request for the updated changelog. Log: add changelog-update changelog
1 parent c7a5a86 commit 88706fa

35 files changed

Lines changed: 589 additions & 0 deletions
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
name: update changelog
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: "Target version; leave empty to auto bump from debian/changelog"
8+
required: false
9+
type: string
10+
name:
11+
description: "Maintainer name used in debian/changelog"
12+
required: true
13+
type: string
14+
email:
15+
description: "Maintainer email used in debian/changelog"
16+
required: true
17+
type: string
18+
base_branch:
19+
description: "Base branch to read and target"
20+
required: false
21+
default: master
22+
type: string
23+
distribution:
24+
description: "Changelog distribution"
25+
required: false
26+
default: unstable
27+
type: string
28+
create_pr:
29+
description: "Create a pull request after generating changelog"
30+
required: false
31+
default: true
32+
type: boolean
33+
workflow_call:
34+
inputs:
35+
version:
36+
required: false
37+
type: string
38+
name:
39+
required: true
40+
type: string
41+
email:
42+
required: true
43+
type: string
44+
base_branch:
45+
required: false
46+
type: string
47+
default: master
48+
distribution:
49+
required: false
50+
type: string
51+
default: unstable
52+
create_pr:
53+
required: false
54+
type: boolean
55+
default: true
56+
secrets:
57+
APP_ID:
58+
required: false
59+
APP_PRIVATE_KEY:
60+
required: false
61+
outputs:
62+
pr_url:
63+
value: ${{ jobs.update_changelog.outputs.pr_url }}
64+
pr_number:
65+
value: ${{ jobs.update_changelog.outputs.pr_number }}
66+
version:
67+
value: ${{ jobs.update_changelog.outputs.version }}
68+
69+
permissions:
70+
contents: write
71+
pull-requests: write
72+
73+
jobs:
74+
update_changelog:
75+
runs-on: ubuntu-latest
76+
outputs:
77+
pr_url: ${{ steps.create_pr.outputs.pull-request-url }}
78+
pr_number: ${{ steps.create_pr.outputs.pull-request-number }}
79+
version: ${{ steps.prepare.outputs.version }}
80+
steps:
81+
- name: Create GitHub App token
82+
id: app_token
83+
if: ${{ inputs.create_pr }}
84+
uses: actions/create-github-app-token@v3
85+
with:
86+
app-id: ${{ secrets.APP_ID }}
87+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
88+
owner: ${{ github.repository_owner }}
89+
permission-contents: write
90+
permission-pull-requests: write
91+
92+
- name: Checkout base branch
93+
uses: actions/checkout@v7
94+
with:
95+
ref: ${{ inputs.base_branch }}
96+
fetch-depth: 1
97+
persist-credentials: false
98+
99+
- name: Install changelog tools
100+
run: |
101+
sudo apt-get update
102+
sudo apt-get install -y devscripts
103+
104+
- name: Prepare changelog data
105+
id: prepare
106+
shell: python
107+
env:
108+
INPUT_VERSION: ${{ inputs.version }}
109+
INPUT_BASE_BRANCH: ${{ inputs.base_branch }}
110+
INPUT_DISTRIBUTION: ${{ inputs.distribution }}
111+
GITHUB_TOKEN: ${{ steps.app_token.outputs.token || github.token }}
112+
GITHUB_REPOSITORY: ${{ github.repository }}
113+
run: |
114+
import json
115+
import os
116+
import pathlib
117+
import re
118+
import subprocess
119+
import urllib.error
120+
import urllib.parse
121+
import urllib.request
122+
123+
def output_line(name, value):
124+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
125+
fh.write(f"{name}={value}\n")
126+
127+
def get_current_version(changelog_path):
128+
try:
129+
result = subprocess.run(
130+
["dpkg-parsechangelog", "-l", str(changelog_path), "-S", "Version"],
131+
capture_output=True,
132+
text=True,
133+
check=True,
134+
)
135+
return result.stdout.strip()
136+
except Exception:
137+
first_line = changelog_path.read_text(encoding="utf-8").splitlines()[0].strip()
138+
match = re.match(r"^[^(]+\(([^)]+)\)", first_line)
139+
if not match:
140+
raise RuntimeError(f"cannot parse version from {changelog_path}")
141+
return match.group(1)
142+
143+
def bump_version(version):
144+
epoch = ""
145+
value = version
146+
if ":" in value:
147+
epoch_part, value = value.split(":", 1)
148+
if epoch_part.isdigit():
149+
epoch = epoch_part + ":"
150+
if "-" in value:
151+
value = value.rsplit("-", 1)[0]
152+
parts = value.split(".")
153+
if len(parts) < 2 or any(not part.isdigit() for part in parts):
154+
raise RuntimeError(
155+
"cannot auto bump complex Debian version "
156+
f"{version!r}; pass the workflow input 'version' explicitly"
157+
)
158+
parts[-1] = str(int(parts[-1]) + 1)
159+
return epoch + ".".join(parts)
160+
161+
def github_get(url):
162+
headers = {
163+
"Accept": "application/vnd.github+json",
164+
"X-GitHub-Api-Version": "2022-11-28",
165+
"User-Agent": "deepin-autopack-update-changelog",
166+
"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
167+
}
168+
req = urllib.request.Request(url, headers=headers)
169+
try:
170+
with urllib.request.urlopen(req, timeout=30) as resp:
171+
body = resp.read().decode("utf-8")
172+
data = json.loads(body) if body else None
173+
resp_headers = {k.lower(): v for k, v in resp.headers.items()}
174+
return data, resp_headers
175+
except urllib.error.HTTPError as exc:
176+
body = exc.read().decode("utf-8", errors="replace")
177+
if len(body) > 2000:
178+
body = body[:2000] + "...<truncated>"
179+
raise RuntimeError(
180+
f"GitHub API request failed: status={exc.code}, url={url}, body={body}"
181+
) from exc
182+
183+
def parse_link_header(value):
184+
links = {}
185+
if not value:
186+
return links
187+
for item in value.split(","):
188+
item = item.strip()
189+
match = re.match(r'<([^>]+)>;\s*rel="([^"]+)"', item)
190+
if match:
191+
links[match.group(2)] = match.group(1)
192+
return links
193+
194+
def commit_titles_since_changelog(owner, repo, base_branch, head_sha):
195+
base_url = f"https://api.github.com/repos/{owner}/{repo}"
196+
commits_url = f"{base_url}/commits?{urllib.parse.urlencode({'sha': base_branch, 'path': 'debian/changelog', 'per_page': 1})}"
197+
commits, _ = github_get(commits_url)
198+
if not commits:
199+
raise RuntimeError(f"no commit found for debian/changelog on {base_branch}")
200+
201+
base_sha = commits[0]["sha"]
202+
compare_url = f"{base_url}/compare/{urllib.parse.quote(base_sha, safe='')}...{urllib.parse.quote(head_sha, safe='')}?per_page=100"
203+
titles = []
204+
next_url = compare_url
205+
while next_url:
206+
page, page_headers = github_get(next_url)
207+
for item in page.get("commits", []):
208+
message = item.get("commit", {}).get("message", "")
209+
title = message.splitlines()[0].strip()
210+
if title:
211+
titles.append(title)
212+
next_url = parse_link_header(page_headers.get("link")).get("next")
213+
return base_sha, titles
214+
215+
repo_root = pathlib.Path(os.environ["GITHUB_WORKSPACE"])
216+
changelog_path = repo_root / "debian" / "changelog"
217+
if not changelog_path.exists():
218+
raise RuntimeError(f"missing changelog: {changelog_path}")
219+
220+
requested_version = os.environ.get("INPUT_VERSION", "").strip()
221+
distribution = os.environ.get("INPUT_DISTRIBUTION", "").strip() or "unstable"
222+
base_branch = os.environ.get("INPUT_BASE_BRANCH", "").strip() or "master"
223+
owner, repo = os.environ["GITHUB_REPOSITORY"].split("/", 1)
224+
head_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
225+
226+
current_version = get_current_version(changelog_path)
227+
version = requested_version or bump_version(current_version)
228+
if not version:
229+
raise RuntimeError(f"unable to determine version from {current_version!r}")
230+
231+
base_sha, titles = commit_titles_since_changelog(owner, repo, base_branch, head_sha)
232+
safe_version = re.sub(r"[^A-Za-z0-9._-]+", "-", version).strip("-")
233+
234+
output_line("version", version)
235+
output_line("safe_version", safe_version)
236+
output_line("distribution", distribution)
237+
output_line("base_branch", base_branch)
238+
output_line("base_sha", base_sha)
239+
output_line("head_sha", head_sha)
240+
output_line("titles_json", json.dumps(titles, ensure_ascii=False))
241+
242+
- name: Generate changelog
243+
shell: python
244+
env:
245+
VERSION: ${{ steps.prepare.outputs.version }}
246+
DISTRIBUTION: ${{ steps.prepare.outputs.distribution }}
247+
TITLES_JSON: ${{ steps.prepare.outputs.titles_json }}
248+
DEBFULLNAME: ${{ inputs.name }}
249+
DEBEMAIL: ${{ inputs.email }}
250+
TZ: Asia/Shanghai
251+
run: |
252+
import json
253+
import os
254+
import subprocess
255+
256+
version = os.environ["VERSION"].strip()
257+
distribution = os.environ["DISTRIBUTION"].strip() or "unstable"
258+
titles = json.loads(os.environ["TITLES_JSON"])
259+
if not titles:
260+
titles = [f"Release {version}"]
261+
262+
subprocess.run(
263+
["dch", "-v", version, "-D", distribution, titles[0]],
264+
check=True,
265+
)
266+
for title in titles[1:]:
267+
subprocess.run(["dch", "-a", title], check=True)
268+
269+
- name: Show changelog diff
270+
if: ${{ !inputs.create_pr }}
271+
run: git diff -- debian/changelog
272+
273+
- name: Upload generated changelog
274+
if: ${{ !inputs.create_pr }}
275+
uses: actions/upload-artifact@v4
276+
with:
277+
name: generated-changelog
278+
path: debian/changelog
279+
280+
- name: Create pull request
281+
id: create_pr
282+
if: ${{ inputs.create_pr }}
283+
uses: peter-evans/create-pull-request@v6
284+
with:
285+
token: ${{ steps.app_token.outputs.token }}
286+
branch: automation/update-changelog/${{ steps.prepare.outputs.safe_version }}
287+
title: "chore: update changelog to ${{ steps.prepare.outputs.version }}"
288+
commit-message: "chore: update changelog to ${{ steps.prepare.outputs.version }}"
289+
body: |
290+
Automated changelog update.
291+
292+
- Version: `${{ steps.prepare.outputs.version }}`
293+
- Base branch: `${{ steps.prepare.outputs.base_branch }}`
294+
- Base changelog commit: `${{ steps.prepare.outputs.base_sha }}`
295+
- Head commit: `${{ steps.prepare.outputs.head_sha }}`
296+
add-paths: |
297+
debian/changelog
298+
delete-branch: true
299+
300+
- name: Show pull request result
301+
if: ${{ inputs.create_pr }}
302+
run: |
303+
echo "Version: ${{ steps.prepare.outputs.version }}"
304+
echo "Pull Request Number: ${{ steps.create_pr.outputs.pull-request-number }}"
305+
echo "Pull Request URL: ${{ steps.create_pr.outputs.pull-request-url }}"

repos/linuxdeepin/dcc-insider-plugin.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,12 @@
6969
".github/workflows/call-tag-build.yml"
7070
],
7171
"dest": "linuxdeepin/dcc-insider-plugin/.github/workflows/call-tag-build.yml"
72+
},
73+
{
74+
"branch": [
75+
"master"
76+
],
77+
"src": "workflow-templates/call-update-changelog.yml",
78+
"dest": "linuxdeepin/dcc-insider-plugin/.github/workflows/call-update-changelog.yml"
7279
}
7380
]

repos/linuxdeepin/dde-account-faces.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@
6767
".github/workflows/call-tag-build.yml"
6868
],
6969
"dest": "linuxdeepin/dde-account-faces/.github/workflows/call-tag-build.yml"
70+
},
71+
{
72+
"branch": [
73+
"master"
74+
],
75+
"src": "workflow-templates/call-update-changelog.yml",
76+
"dest": "linuxdeepin/dde-account-faces/.github/workflows/call-update-changelog.yml"
7077
}
7178
]

repos/linuxdeepin/dde-api-proxy.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,12 @@
5757
".github/workflows/call-tag-build.yml"
5858
],
5959
"dest": "linuxdeepin/dde-api-proxy/.github/workflows/call-tag-build.yml"
60+
},
61+
{
62+
"branch": [
63+
"master"
64+
],
65+
"src": "workflow-templates/call-update-changelog.yml",
66+
"dest": "linuxdeepin/dde-api-proxy/.github/workflows/call-update-changelog.yml"
6067
}
6168
]

repos/linuxdeepin/dde-api.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@
6767
".github/workflows/call-tag-build.yml"
6868
],
6969
"dest": "linuxdeepin/dde-api/.github/workflows/call-tag-build.yml"
70+
},
71+
{
72+
"branch": [
73+
"master"
74+
],
75+
"src": "workflow-templates/call-update-changelog.yml",
76+
"dest": "linuxdeepin/dde-api/.github/workflows/call-update-changelog.yml"
7077
}
7178
]

repos/linuxdeepin/dde-app-services.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,12 @@
9292
],
9393
"src": "issue-templates/unit-test-report.md",
9494
"dest": "linuxdeepin/dde-app-services/.github/ISSUE_TEMPLATE/unit-test-report.md"
95+
},
96+
{
97+
"branch": [
98+
"master"
99+
],
100+
"src": "workflow-templates/call-update-changelog.yml",
101+
"dest": "linuxdeepin/dde-app-services/.github/workflows/call-update-changelog.yml"
95102
}
96103
]

repos/linuxdeepin/dde-appearance.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,12 @@
5757
".github/workflows/call-tag-build.yml"
5858
],
5959
"dest": "linuxdeepin/dde-appearance/.github/workflows/call-tag-build.yml"
60+
},
61+
{
62+
"branch": [
63+
"master"
64+
],
65+
"src": "workflow-templates/call-update-changelog.yml",
66+
"dest": "linuxdeepin/dde-appearance/.github/workflows/call-update-changelog.yml"
6067
}
6168
]

0 commit comments

Comments
 (0)