Skip to content

Commit befb1be

Browse files
committed
ci: automatic releases
1 parent de1ea49 commit befb1be

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

.github/workflows/release.yml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: release
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 1 */1 *' # @monthly
6+
workflow_dispatch:
7+
inputs:
8+
release_tag:
9+
type: string
10+
required: false
11+
environment:
12+
type: environment
13+
required: true
14+
default: release
15+
16+
concurrency:
17+
group: ${{ github.workflow }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
verify:
22+
runs-on: ubuntu-latest
23+
outputs:
24+
release_tag: ${{ steps.verify.outputs.release_tag }}
25+
release_commit: ${{ steps.verify.outputs.release_commit }}
26+
steps:
27+
- uses: actions/checkout@v4
28+
with:
29+
fetch-tags: true
30+
# FIXME: workaround for git 2.48, see https://github.com/actions/checkout/issues/1467
31+
fetch-depth: 100
32+
ref: ${{ github.ref }}
33+
34+
- name: Verify release tag
35+
id: verify
36+
env:
37+
INPUT_RELEASE_TAG: ${{ inputs.release_tag }}
38+
REPOSITORY_URL: '${{ github.server_url }}/${{ github.repository }}'
39+
run: |
40+
set -eu
41+
42+
latest_tag="$(git describe --tags --abbrev=0)"
43+
latest_tag_fmt="[\`${latest_tag}\`](${REPOSITORY_URL}/releases/tag/${latest_tag})"
44+
release_tag="${INPUT_RELEASE_TAG:-$(date +'%Y.%m.%d')}"
45+
release_commit="$(git log --format="%H" -n 1)"
46+
47+
# note: this comparison only works with lexicographic dates
48+
if [[ ! "${release_tag}" > "${latest_tag}" ]]; then
49+
echo "## Skipping release \`${release_tag}\`" >> "$GITHUB_STEP_SUMMARY"
50+
echo "Latest tag ${latest_tag_fmt} already covers this date." >> "$GITHUB_STEP_SUMMARY"
51+
exit 0
52+
elif [[ "$(git log --format="%H" -n 1 "${latest_tag}")" = "${release_commit}" ]]; then
53+
echo "## Skipping release \`${release_tag}\`" >> "$GITHUB_STEP_SUMMARY"
54+
echo "No changes since ${latest_tag_fmt}." >> "$GITHUB_STEP_SUMMARY"
55+
exit 0
56+
fi
57+
58+
{
59+
echo "## Proposing new release \`${release_tag}\`"
60+
echo "Commit: [${release_commit}](${REPOSITORY_URL}/commit/${release_commit})"
61+
62+
echo "### Changes since ${latest_tag_fmt}"
63+
echo '```log'
64+
git diff --histogram --stat "${latest_tag}..${release_commit}"
65+
echo '```'
66+
67+
echo '### Git log'
68+
echo '```log'
69+
git log --no-merges "${latest_tag}..${release_commit}"
70+
echo '```'
71+
} >> "$GITHUB_STEP_SUMMARY"
72+
73+
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
74+
echo "release_commit=${release_commit}" >> "$GITHUB_OUTPUT"
75+
76+
release:
77+
runs-on: ubuntu-latest
78+
needs: [verify]
79+
if: needs.verify.outputs.release_tag != ''
80+
environment: ${{ inputs.environment || 'release' }}
81+
steps:
82+
- uses: actions/checkout@v4
83+
with:
84+
ref: ${{ needs.verify.outputs.release_commit }}
85+
86+
- name: Create release
87+
uses: softprops/action-gh-release@v2
88+
with:
89+
tag_name: ${{ needs.verify.outputs.release_tag }}
90+
name: ${{ needs.verify.outputs.release_tag }}
91+
target_commitish: ${{ needs.verify.outputs.release_commit }}
92+
generate_release_notes: true
93+
make_latest: true
94+
env:
95+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

PR/PR.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<!-- enh(CI): Automatic Release Workflow -->
2+
3+
# Automatic Release Workflow
4+
5+
This PR introduces a `release` workflow that runs on the first day of every month, check for changes on `master`, and, if so, creates a new release after approval.
6+
7+
- Closes #57
8+
9+
## Tag Selection
10+
11+
The `verify` job selects a tag name with format `%Y.%m.%d` (e.g. `2025.08.01`), which always ends in `01` since it runs on the first day of each month. If the tag is newer then the previous one and there are changes in `master`, then the workflow generates a change summary and continues to the `release` job, which request a review and awaits for approval.
12+
13+
*The examples below use format `%Y.%m.%d-%H` for faster (test) release cycles, but everything else is the same.*
14+
15+
### Summary
16+
17+
The summary is generated with `git diff --histogram --stat $latest_tag..HEAD` and `git log --no-merges $latest_tag..HEAD`, so merge commits are not displayed.
18+
19+
<details><summary><b>Release Summary</b></summary>
20+
21+
````md
22+
## Proposing new release `2025.07.14-05`
23+
### Changes since [`2025.07.13-23`](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.13-23)
24+
```log
25+
git.nanorc | 17 ++++++++---------
26+
readme.md | 7 +++----
27+
2 files changed, 11 insertions(+), 13 deletions(-)
28+
```
29+
### Git log
30+
```log
31+
commit ce788aa4e727ea09e542c8b91e59574645dc75a0
32+
Author: Tiago de Paula <[email protected]>
33+
Date: Sun Jul 13 23:32:45 2025 -0300
34+
35+
enh(gitcommit): improve colors for kitty+fish combo
36+
37+
commit 22592a37ddc9639b206f4c6e5859d4421348b4dd
38+
Author: Tiago de Paula <[email protected]>
39+
Date: Sun Jul 13 23:24:01 2025 -0300
40+
41+
chore(readme): add release badge
42+
43+
commit dd9e3ca50d430f6ffc1b8c4d79af76e37b52f754
44+
Author: Tiago de Paula <[email protected]>
45+
Date: Sun Jul 13 23:17:13 2025 -0300
46+
47+
chore(readme): restore package manager information
48+
```
49+
````
50+
51+
---
52+
53+
![message-ok](https://github.com/user-attachments/assets/81142ded-cb8a-438f-96b7-68be3d573291)
54+
55+
</details>
56+
57+
- Examples: [#30](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16254545342), [#35](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16258745959), [#37](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16260703509)
58+
59+
### Checks
60+
61+
Each tag is checked to be newer using lexicographical order, which works for `%Y.%m.%d`. Just be aware of any additional text after the date. For example, `2025.07.01-2` is considered newer than `2025.07.01-10`.
62+
63+
After that, `master` is checked for code changes with `git diff --quiet $latest_tag`, and the release is skipped if no change is reported.
64+
65+
In both cases, skipping the relase is not considered a failure. The workflow just finishes early and no notification is sent anywhere.
66+
67+
<details><summary><b>Outdated Release</b></summary>
68+
69+
````md
70+
## Skipping release `2025.07.13-23`
71+
Latest tag [`2025.07.13-23`](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.13-23) already covers this date.
72+
````
73+
74+
---
75+
76+
![message-outdated](https://github.com/user-attachments/assets/459b8d78-fcb1-4698-98cb-d2bf7e387787)
77+
78+
</details>
79+
80+
<details><summary><b>Release with No Changes</b></summary>
81+
82+
````md
83+
## Skipping release `2025.07.14-02`
84+
No changes since [`2025.07.13-23`](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.13-23).
85+
````
86+
87+
---
88+
89+
![message-no-changes](https://github.com/user-attachments/assets/f3c4429a-baea-47e8-b378-b4ef287b7f82)
90+
91+
</details>
92+
93+
- Examples: [#31](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16256097841), [#32](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16256116191), [#33](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16256260221), [#36](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16259810098)
94+
95+
### Manual Release
96+
97+
The job can also be triggered manually on [Actions page](https://github.com/galenguyer/nano-syntax-highlighting/actions/workflows/release.yml), with an optional `release_tag` different from `%Y.%m.%d`. You can also change the [deploy `environment`](#github-environment) for this manual run.
98+
99+
![manual-options](https://github.com/user-attachments/assets/45a5eb0f-d7f9-4dd8-8a05-52d126715cbe)
100+
101+
<details><summary><b>Full Page</b></summary>
102+
103+
![message-release](https://github.com/user-attachments/assets/394ade6d-e5d6-4759-b5e5-640e598c373c)
104+
105+
</details>
106+
107+
## Workflow Review
108+
109+
Once `verify` completes with a non-empty `release_tag`, the workflow request reviews from defined members before starting the `release` job.
110+
111+
<details><summary>Release Verified, Awaiting Review</summary>
112+
113+
![release-verified](https://github.com/user-attachments/assets/25a0cd7f-ce0e-4729-936e-3e6157fe1d10)
114+
115+
</details>
116+
117+
<details><summary>Review Notification Sent to Email</summary>
118+
119+
![release-review-request](https://github.com/user-attachments/assets/e2653a43-9da0-4a92-bfa3-b14070cb65a1)
120+
121+
</details>
122+
123+
<details><summary>Review Page</summary>
124+
125+
![release-review](https://github.com/user-attachments/assets/b7cd1f0e-6364-4751-a72d-5ae2e7e64527)
126+
127+
</details>
128+
129+
<details><summary>Release Approved</summary>
130+
131+
![release-approved](https://github.com/user-attachments/assets/16058072-7563-488d-a7ae-e2679a399da2)
132+
133+
</details>
134+
135+
<details><summary>Release Created</summary>
136+
137+
![released](https://github.com/user-attachments/assets/48a5e30a-818e-4dd4-a864-bf2492d25c5f)
138+
139+
</details>
140+
141+
### GitHub Environment
142+
143+
> [!IMPORTANT]
144+
> This step requires manual configuration in the repository Settings.
145+
146+
The review system uses [deploy environments on Github](https://docs.github.com/en/actions/how-tos/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment). The workflow doesn't actually use any information from the environment, but if the environment is configured with "Required reviewers", then it won't run by itself. Here I used `release` as the name of the environment.
147+
148+
<details><summary><b>Environment Settings</b></summary>
149+
150+
![released](https://github.com/user-attachments/assets/6ffa8b33-50ad-438f-9854-ea1282ca259a)
151+
152+
</details>
153+
154+
### Job Concurrency
155+
156+
To avoid multiple competing releases, I've set up a `concurrency` `group` that disallows concurrent runs of the `release.yml` workflow. If a new workflow starts while the last one is still running, then the new one has priority and the other is cancelled. In practice, this should only happen if a release is left unapproved for more than a month.
157+
158+
<details><summary><b>Unapproved Release</b></summary>
159+
160+
![released](https://github.com/user-attachments/assets/86ec04a6-b9a1-495b-befc-afd7a75908a0)
161+
162+
</details>
163+
164+
- Example: [#24](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16251594856), [#25](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16252200601), [#26](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16252593840), [#27](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16253110094), [#28](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16253570186), [#29](https://github.com/marmitar/nano-syntax-highlighting/actions/runs/16254058395)
165+
166+
## Release Creation
167+
168+
Once the `release` job is approved and runs, it creates a tag with the name `release_tag` provided by `verify`, pointing to the last commit on `master`. It also creates a [GitHub release](https://github.com/galenguyer/nano-syntax-highlighting/releases) with [automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes), which can be configured in a `.github/release.yml` file. By default, the generated notes list all pull requests since the last release (which might be a lot for the first one here, you can create a first release to avoid this).
169+
170+
<details><summary><b>Changelog Example</b></summary>
171+
172+
![Release `2025.07.14-05`](https://github.com/user-attachments/assets/10d0a435-8050-4f57-8b2c-6462ff6b8829)
173+
174+
![Release `2025.07.14-07`](https://github.com/user-attachments/assets/0c753175-19e5-4aa2-8659-cb8ca93e2e42)
175+
176+
</details>
177+
178+
- Examples: [2025.07.13-23](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.13-23), [2025.07.14-05](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.14-05), [2025.07.14-07](https://github.com/marmitar/nano-syntax-highlighting/releases/tag/2025.07.14-07)
179+
180+
## Implementation Notes
181+
182+
There's currently an issue (actions/checkout#1467) with `fetch_tags` on Git 2.48 (`ubunut-latest`), which requires explicit fetching of the git history using `fetch-depth` and `ref`. I used the last 100 commits for `fetch-depth`, which should be more than enough for a month. The workaround implemented here can be removed after the fix actions/checkout#2200 lands.

0 commit comments

Comments
 (0)