-
Notifications
You must be signed in to change notification settings - Fork 9
274 lines (235 loc) · 11 KB
/
Copy pathcreate-release.yml
File metadata and controls
274 lines (235 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version number (e.g., 0.1.7). Leave empty to auto-increment patch version.'
required: false
type: string
env:
CARGO_TERM_COLOR: always
jobs:
create-tag:
name: Create Release Tag
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
# Use PAT to allow tag push to trigger release workflow
# (default GITHUB_TOKEN doesn't trigger other workflows)
token: ${{ secrets.RELEASE_TOKEN }}
- name: Determine version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
# Get the latest tag and increment patch version
LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
if [ -z "$LATEST_TAG" ]; then
VERSION="0.1.0"
else
# Remove 'v' prefix and split version
CURRENT="${LATEST_TAG#v}"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Increment patch
PATCH=$((PATCH + 1))
VERSION="${MAJOR}.${MINOR}.${PATCH}"
fi
fi
# Ensure version doesn't have 'v' prefix for consistency
VERSION="${VERSION#v}"
TAG="v${VERSION}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "📦 Creating release: $TAG"
- name: Check if tag already exists
run: |
if git rev-parse "${{ steps.version.outputs.tag }}" >/dev/null 2>&1; then
echo "❌ Error: Tag ${{ steps.version.outputs.tag }} already exists!"
exit 1
fi
echo "✅ Tag ${{ steps.version.outputs.tag }} is available"
# Reconstruct "What's Changed" from the merged-PR list rather than
# GitHub's releases/generate-notes API. generate-notes matches PRs to the
# range by their recorded merge-commit SHA; the 2026-07 Git LFS history
# rewrite gave every pre-rewrite commit a new SHA, so the API silently
# dropped every PR merged before the rewrite (v0.3.4..v1.0.0 collapsed to
# 2 entries instead of 158). Listing merged PRs by merge date is immune to
# SHA changes and stays correct for every future release. Bot logins come
# back as "app/<name>"; rewrite them to "<name>[bot]" to match the handles
# GitHub itself renders.
- name: Generate release notes
id: release_notes
run: |
TAG="${{ steps.version.outputs.tag }}"
PREVIOUS_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^${TAG}$" | head -1)
echo "Reconstructing notes from $PREVIOUS_TAG to HEAD for tag $TAG"
{
echo "## What's Changed"
if [ -n "$PREVIOUS_TAG" ]; then
PREV_DATE=$(git log -1 --format=%cI "$PREVIOUS_TAG")
gh pr list --state merged --base "${{ github.ref_name }}" --limit 1000 \
--json number,title,author,mergedAt \
--jq "[.[] | select(.mergedAt > \"$PREV_DATE\")] | sort_by(.mergedAt) | .[] | \"* \(.title) by @\(if (.author.login | startswith(\"app/\")) then (.author.login[4:] + \"[bot]\") else .author.login end) in ${{ github.server_url }}/${{ github.repository }}/pull/\(.number)\""
echo ""
echo "**Full Changelog**: ${{ github.server_url }}/${{ github.repository }}/compare/${PREVIOUS_TAG}...${TAG}"
else
gh pr list --state merged --base "${{ github.ref_name }}" --limit 1000 \
--json number,title,author,mergedAt \
--jq "sort_by(.mergedAt) | .[] | \"* \(.title) by @\(if (.author.login | startswith(\"app/\")) then (.author.login[4:] + \"[bot]\") else .author.login end) in ${{ github.server_url }}/${{ github.repository }}/pull/\(.number)\""
fi
} > release_notes.txt
if [ ! -s release_notes.txt ] || ! grep -q '^\* ' release_notes.txt; then
echo "No PRs found, using fallback"
echo "Bug fixes and improvements." > release_notes.txt
fi
echo "📝 Release notes generated"
env:
GH_TOKEN: ${{ github.token }}
- name: Update Cargo.toml version
run: |
VERSION="${{ steps.version.outputs.version }}"
# Only replace the first 'version = ' line (the package version, not dependency versions)
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml
echo "✅ Updated Cargo.toml to version $VERSION"
# Adds a <release> entry to the metainfo file. scripts/gen-metadata.py also
# writes this file, but only the translated values: it leaves <releases>
# alone precisely so this step owns it. Keep that split intact when
# changing either one.
- name: Update metainfo.xml
run: |
VERSION="${{ steps.version.outputs.version }}"
python3 << 'EOF'
import xml.etree.ElementTree as ET
from datetime import date
import re
import os
version = os.environ.get("VERSION", "${{ steps.version.outputs.version }}")
today = date.today().isoformat()
# Read release notes
with open("release_notes.txt", "r") as f:
body = f.read()
# Parse the "What's Changed" section
notes = []
match = re.search(r"## What's Changed\n(.*?)(?=\n\*\*Full Changelog\*\*|\n## New Contributors|\n## Installation|\Z)", body, re.DOTALL)
if match:
changes = match.group(1).strip()
for line in changes.split('\n'):
# Extract description from "* description by @user in URL"
pr_match = re.match(r'\* (.+?) by @', line)
if pr_match:
notes.append(pr_match.group(1).strip())
# Parse metainfo.xml
metainfo_path = "resources/io.github.cosmic_utils.camera.metainfo.xml"
# Register namespace to avoid ns0 prefix
ET.register_namespace('', '')
# insert_comments=True keeps XML comments in the tree. Without it
# ElementTree drops every comment on write, which silently deleted the
# Git-LFS/Flathub screenshot-storage note above <screenshots> during
# the v1.0.0 release run.
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(metainfo_path, parser)
root = tree.getroot()
# Find releases element
releases = root.find("releases")
if releases is None:
releases = ET.SubElement(root, "releases")
# Check if version already exists
existing_release = None
for release in releases.findall("release"):
if release.get("version") == version:
existing_release = release
break
if existing_release is not None:
release_elem = existing_release
desc = release_elem.find("description")
if desc is not None:
release_elem.remove(desc)
else:
release_elem = ET.Element("release")
release_elem.set("version", version)
release_elem.set("date", today)
releases.insert(0, release_elem)
# Add description
desc = ET.SubElement(release_elem, "description")
if notes:
p = ET.SubElement(desc, "p")
p.text = f"Release {version} includes the following changes:"
ul = ET.SubElement(desc, "ul")
for note in notes:
if note.strip():
li = ET.SubElement(ul, "li")
li.text = note.strip()
else:
p = ET.SubElement(desc, "p")
p.text = "Bug fixes and improvements."
# Write back
ET.indent(tree, space=" ")
tree.write(metainfo_path, encoding="UTF-8", xml_declaration=True)
print(f"✅ Updated metainfo.xml with version {version}")
EOF
env:
VERSION: ${{ steps.version.outputs.version }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Update Cargo.lock
run: |
cargo update --workspace
echo "✅ Updated Cargo.lock"
- name: Install just
uses: extractions/setup-just@v4
- name: Install Python dependencies
run: pip install aiohttp toml tomlkit
# Must happen here rather than in regenerate.yml: the tag is created a few
# steps below, so cargo-sources.json has to match the Cargo.lock written
# above before the tagged commit exists.
- name: Generate cargo-sources.json for Flathub
run: just flatpak-cargo-sources
- name: Commit release changes
id: commit
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.toml
git add Cargo.lock
git add resources/io.github.cosmic_utils.camera.metainfo.xml
git add cargo-sources.json
if git diff --staged --quiet; then
echo "No changes to commit"
COMMIT_SHA="${{ github.sha }}"
else
git commit -m "Release ${{ steps.version.outputs.tag }}: update version and metainfo"
git push origin ${{ github.ref_name }}
COMMIT_SHA=$(git rev-parse HEAD)
echo "📝 Committed release changes: $COMMIT_SHA"
fi
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
- name: Create and push tag
run: |
TAG="${{ steps.version.outputs.tag }}"
COMMIT="${{ steps.commit.outputs.sha }}"
git tag -a "$TAG" "$COMMIT" -m "Release $TAG"
git push origin "$TAG"
echo "🏷️ Created tag $TAG pointing to $COMMIT"
echo ""
echo "🚀 The Release workflow will now be triggered automatically!"
- name: Summary
run: |
echo "## Release Created 🎉" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** ${{ steps.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** ${{ steps.commit.outputs.sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "The [Release workflow](${{ github.server_url }}/${{ github.repository }}/actions/workflows/release.yml) will now build and publish the release." >> $GITHUB_STEP_SUMMARY