Skip to content

Commit 9909098

Browse files
committed
Automated changelogs [WIP]
1 parent 6e823ce commit 9909098

12 files changed

+338
-5
lines changed

.github/pull_request_template.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
3+
- [x] This change is user-facing

.github/workflows/check-changelog.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Changelog
2+
on:
3+
pull_request:
4+
branches:
5+
- main
6+
# Includes "edited" such that we can detect changes to the description
7+
types: [opened, synchronize, reopened, edited]
8+
9+
permissions:
10+
pull-requests: read
11+
12+
jobs:
13+
check:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
# We need to fetch the parents of the HEAD commit (which is a merge),
19+
# because we need to compare the PR against the base branch
20+
# to check whether it added a changelog
21+
fetch-depth: 2
22+
23+
- name: check changelog
24+
run: scripts/check-changelog.sh . ${{ github.event.pull_request.number }}
25+
env:
26+
GH_TOKEN: ${{ github.token }}
27+

.github/workflows/main.yml

+62
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,68 @@ jobs:
3636
env:
3737
GH_TOKEN: ${{ github.token }}
3838

39+
# Creates a release commit and combines the changelog files into a single one
40+
# For PRs it shows the resulting changelog in the step summary
41+
# For pushes to the main branch it updates the release branch
42+
# The release branch is regularly
43+
version-changelog:
44+
runs-on: ubuntu-latest
45+
permissions:
46+
# This job only needs this token to read commit objects to figure out what PR they're associated with.
47+
# A separate fixed token is used to update the release branch after push events.
48+
contents: read
49+
steps:
50+
- uses: actions/checkout@v4
51+
with:
52+
# This fetches the entire Git history.
53+
# This is needed so we can determine the commits (and therefore PRs)
54+
# where the changelogs have been added
55+
fetch-depth: 0
56+
# By default, the github.token is used and stored in the Git config,
57+
# This would override any authentication provided in the URL,
58+
# which we do later to push to a fork.
59+
# So we need to prevent that from being stored.
60+
persist-credentials: false
61+
62+
- uses: cachix/install-nix-action@v26
63+
64+
- name: Increment version and assemble changelog
65+
run: |
66+
nix-build -A autoVersion
67+
# If we're running for a PR, the second argument tells the script to pretend that commits
68+
# from this PR are merged already, such that the generated changelog includes it
69+
version=$(result/bin/auto-version . ${{ github.event.pull_request.number || '' }})
70+
echo "version=$version" >> "$GITHUB_ENV"
71+
72+
# version is the empty string if there were no user-facing changes for a version bump
73+
if [[ -n "$version" ]]; then
74+
# While we commit here, it's only pushed conditionally based on it being a push event later
75+
git config user.name ${{ github.actor }}
76+
git config user.email ${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com
77+
git add --all
78+
git commit --message "Version $version
79+
80+
Automated release"
81+
fi
82+
env:
83+
GH_TOKEN: ${{ github.token }}
84+
85+
- name: Outputting draft release notes
86+
# If we have a new version at all (it's not an empty string)
87+
# And it's not a push event (so it's a PR),
88+
if: ${{ env.version && github.event_name != 'push' }}
89+
# we just output the draft changelog into the step summary
90+
run: cat changes/released/${{ env.version }}.md > "$GITHUB_STEP_SUMMARY"
91+
92+
- name: Update release branch
93+
# But if this is a push to the main branch,
94+
if: ${{ env.version && github.event_name == 'push' }}
95+
# we push to the release branch.
96+
# This continuously updates the release branch to contain the latest release notes,
97+
# so that one can just merge the release branch into main to do a release.
98+
# A PR to do that is opened regularly with another workflow
99+
run: git push https://${{ secrets.MACHINE_USER_PAT }}@github.com/infinixbot/nixpkgs-check-by-name.git HEAD:refs/heads/release -f
100+
39101
# Make sure that all links in Markdown documents are valid
40102
xrefcheck:
41103
runs-on: ubuntu-latest

.github/workflows/regular-release.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Regular Version
2+
on:
3+
workflow_dispatch: # Allows triggering manually
4+
schedule:
5+
- cron: '47 14 * * 2' # runs every Tuesday at 14:47 UTC (chosen somewhat randomly)
6+
7+
jobs:
8+
version:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
with:
13+
repository: infinixbot/nixpkgs-check-by-name
14+
ref: release
15+
16+
- name: Create Pull Request
17+
run: |
18+
# no-maintainer-edit because manually added commits would get overridden
19+
# when the release branch updates again (which is a force push).
20+
# Instead maintainers should push any fixes to the main branch.
21+
gh pr create \
22+
--repo ${{ github.repository }} \
23+
--title "$(git log -1 --format=%s HEAD)" \
24+
--no-maintainer-edit \
25+
--body "Automated release PR.
26+
27+
- [x] This change is user-facing
28+
"
29+
env:
30+
# Needed so that CI triggers
31+
GH_TOKEN: ${{ secrets.MACHINE_USER_PAT }}

CONTRIBUTING.md

+51-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The most important tools and commands in this environment are:
2727
nix-build -A ci
2828
```
2929

30+
Note that pinned dependencies are [regularly updated automatically](./.github/workflows/update.yml).
31+
3032
### Integration tests
3133

3234
Integration tests are declared in [`./tests`](./tests) as subdirectories imitating Nixpkgs with these files:
@@ -61,9 +63,54 @@ Integration tests are declared in [`./tests`](./tests) as subdirectories imitati
6163
A file containing the expected standard output.
6264
The default is expecting an empty standard output.
6365

64-
## Automation
66+
## Releases and changelogs
67+
68+
The following pipeline is used to ensure a smooth releases process with automated changelogs.
69+
70+
### Pull requests
71+
72+
The default [PR template](./.github/pull_request_template.md) adds this line to the description:
73+
74+
> - [x] This change is user-facing
75+
76+
Unless this field is explicitly unchecked, the PR [is checked to](./.github/workflows/check-changelog.yml)
77+
add a [changelog entry](#changelog-entries) to describe the user-facing change.
78+
79+
This ensures that all user-facing changes have a changelog entry.
80+
81+
### Changelog entries
82+
83+
In order to avoid conflicts between different PRs,
84+
a changelog entry is a Markdown file under a directory in
85+
[`changes/unreleased`](./changes/unreleased).
86+
Depending on the effort (see [EffVer](https://jacobtomlinson.dev/effver/))
87+
required for users to update to this change,
88+
a different directory should be used:
89+
90+
- [`changes/unreleased/major`](./changes/unreleased/major):
91+
A large effort. This will cause a version bump from e.g. 0.1.2 to 1.0.0
92+
- [`changes/unreleased/medium`](./changes/unreleased/medium):
93+
Some effort. This will cause a version bump from e.g. 0.1.2 to 1.2.0
94+
- [`changes/unreleased/minor`](./changes/unreleased/minor):
95+
Little/no effort. This will cause a version bump from e.g. 0.1.2 to 0.1.3
96+
97+
The Markdown file must have the `.md` file ending, and be of the form
98+
99+
```markdown
100+
# Some descriptive title of the change
101+
102+
Optionally more information
103+
```
104+
105+
### Release branch
65106

66-
Pinned dependencies are [regularly updated automatically](./.github/workflows/update.yml).
107+
After every push to the main branch, the [infinixbot:release
108+
branch](https://github.com/infinixbot/nixpkgs-check-by-name/tree/release) is rebased such that it
109+
contains one commit on top of master, which:
110+
- Increments the version in `Cargo.toml` according to the unreleased changelog entries.
111+
- Collects all changelog entries in [`./changes/unreleased`](./changes/unreleased)
112+
and combines them into a new `./changes/released/<version>.md` file.
67113

68-
Releases are [automatically created](./.github/workflows/release.yml) when the `version` field in [`Cargo.toml`](./Cargo.toml)
69-
is updated from a push to the main branch.
114+
Regularly a PR is [opened automatically](./.github/workflows/regular-release.yml)
115+
to merge the release branch into the main branch.
116+
When this PR is merged, a GitHub release is [automatically created](./.github/workflows/release.yml).

changes/unreleased/major/.gitkeep

Whitespace-only changes.

changes/unreleased/medium/.gitkeep

Whitespace-only changes.

changes/unreleased/minor/.gitkeep

Whitespace-only changes.

default.nix

+18
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,27 @@ let
127127
echo >&2 "Running ${script}"
128128
${lib.getExe script} "$1"
129129
'') (lib.attrValues updateScripts)}
130+
echo ""
131+
# To not fail the changelog check
132+
printf "%s\n" "- [ ] This change is user-facing"
130133
'';
131134
};
132135

136+
# Run regularly by CI and turned into a PR
137+
autoVersion = pkgs.writeShellApplication {
138+
name = "auto-version";
139+
runtimeInputs = with pkgs; [
140+
coreutils
141+
git
142+
github-cli
143+
jq
144+
cargo
145+
toml-cli
146+
cargo-edit
147+
];
148+
text = builtins.readFile ./scripts/version.sh;
149+
};
150+
133151
# Tests the tool on the pinned Nixpkgs tree, this is a good sanity check
134152
nixpkgsCheck =
135153
pkgs.runCommand "test-nixpkgs-check-by-name"

scripts/check-changelog.sh

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
shopt -s nullglob
5+
6+
root=$1
7+
prNumber=$2
8+
9+
# The PR template has this, selected by default
10+
userFacingString="- [x] This change is user-facing"
11+
nonUserFacingString="- [ ] This change is user-facing"
12+
13+
# Run this first to validate files
14+
for file in "$root"/changes/unreleased/*/*; do
15+
if [[ "$(basename "$file")" == ".gitkeep" ]]; then
16+
continue
17+
fi
18+
if [[ $file != *.md ]]; then
19+
echo "File $file: Must be a markdown file with file ending .md"
20+
exit 1
21+
fi
22+
if [[ "$(sed -n '/^#/=' "$file")" != "1" ]]; then
23+
echo "File $file: The first line must start with #, while all others must not start with #"
24+
exit 1
25+
fi
26+
done
27+
28+
body=$(gh api \
29+
-H "Accept: application/vnd.github+json" \
30+
-H "X-GitHub-Api-Version: 2022-11-28" \
31+
/repos/NixOS/nixpkgs-check-by-name/pulls/"$prNumber" |
32+
jq -r '.body')
33+
34+
if grep -F -- "$userFacingString" <<< "$body" > /dev/null; then
35+
echo "User-facing change, changelog necessary"
36+
elif grep -F -- "$nonUserFacingString" <<< "$body" > /dev/null; then
37+
echo "Not a user-facing change, no changelog necessary"
38+
exit 0
39+
else
40+
echo "Depending on whether this PR has a user-facing change, add one of these lines to the PR description:"
41+
printf "%s\n" "$userFacingString"
42+
printf "%s\n" "$nonUserFacingString"
43+
exit 1
44+
fi
45+
46+
# This checks whether the most recent commit changed any files in changes/unreleased
47+
# This works well for PR's CI because there it runs on the merge commit,
48+
# where HEAD^ is the first parent commit, which is the base branch.
49+
if [[ -z "$(git -C "$root" log HEAD^..HEAD --name-only "$root"/changes/unreleased)" ]]; then
50+
echo "If this PR contains a user-facing change, add a changelog in ./changes/unreleased"
51+
echo "Otherwise, check the checkbox:"
52+
printf "%s\n" "$nonUserFacingString"
53+
exit 1
54+
else
55+
echo "A changelog exists"
56+
fi

scripts/release.sh

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ To import it:
4747
```bash
4848
gzip -cd '"$artifactName"' | nix-store --import | tail -1
4949
```
50-
'
50+
51+
## Changes
52+
53+
'"$(tail -1 "$root"/changes/released/"$version".md)"
5154

5255
echo "Creating draft release"
5356
if ! release=$(gh api \

scripts/version.sh

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
shopt -s nullglob
5+
6+
root=$1
7+
currentPrNumber=${2:-}
8+
9+
[[ "$(toml get --raw "$root"/Cargo.toml package.version)" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]
10+
splitVersion=("${BASH_REMATCH[@]:1}")
11+
12+
majorChanges=("$root"/changes/unreleased/major/*.md)
13+
mediumChanges=("$root"/changes/unreleased/medium/*.md)
14+
minorChanges=("$root"/changes/unreleased/minor/*.md)
15+
16+
if ((${#majorChanges[@]} > 0)); then
17+
# If we didn't have `|| true` this would exit the program due to `set -e`,
18+
# because (( ... )) returns the incremental value, which is treated as the exit code..
19+
((splitVersion[0]++)) || true
20+
splitVersion[1]=0
21+
splitVersion[2]=0
22+
elif ((${#mediumChanges[@]} > 0)); then
23+
((splitVersion[1]++)) || true
24+
splitVersion[2]=0
25+
elif ((${#minorChanges[@]} > 0)); then
26+
((splitVersion[2]++)) || true
27+
else
28+
echo >&2 "No changes"
29+
exit 0
30+
fi
31+
32+
next=${splitVersion[0]}.${splitVersion[1]}.${splitVersion[2]}
33+
releaseFile=$root/changes/released/${next}.md
34+
mkdir -p "$(dirname "$releaseFile")"
35+
36+
echo "# Version $next ($(date --iso-8601 --utc))" > "$releaseFile"
37+
echo "" >> "$releaseFile"
38+
39+
# shellcheck disable=SC2016
40+
for file in "${majorChanges[@]}" "${mediumChanges[@]}" "${minorChanges[@]}"; do
41+
commit=$(git -C "$root" log -1 --format=%H -- "$file")
42+
if ! gh api graphql \
43+
-f sha="$commit" \
44+
-f query='
45+
query ($sha: String) {
46+
repository(owner: "NixOS", name: "nixpkgs-check-by-name") {
47+
commit: object(expression: $sha) {
48+
... on Commit {
49+
associatedPullRequests(first: 100) {
50+
nodes {
51+
merged
52+
baseRefName
53+
baseRepository { nameWithOwner }
54+
number
55+
author { login }
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}' |
62+
jq --exit-status -r ${currentPrNumber:+--argjson currentPrNumber "$currentPrNumber"} --arg file "$file" '
63+
.data.repository.commit.associatedPullRequests?.nodes?[]?
64+
| select(
65+
# We need to make sure to get the right PR, there can be many
66+
(.merged or .number == $ARGS.named.currentPrNumber) and
67+
.baseRepository.nameWithOwner == "NixOS/nixpkgs-check-by-name" and
68+
.baseRefName == "main")
69+
| "\(.number) \(.author.login) \($ARGS.named.file)"'; then
70+
echo >&2 "Couldn't get PR for file $file"
71+
exit 1
72+
fi
73+
done |
74+
sort -n |
75+
while read -r number author file; do
76+
# Replace the first line `# <title>` by `- <title> by @author in #number`
77+
# All other non-empty lines are indented with 2 spaces to make the markdown formatting work
78+
sed "$file" \
79+
-e "1s|#[[:space:]]\(.*\)|- \1 by [@$author](https://github.com/$author) in [#$number](https://github.com/NixOS/nixpkgs-check-by-name/pull/$number)|" \
80+
-e '2,$s/^\(.\)/ \1/' >> "$releaseFile"
81+
82+
rm "$file"
83+
done
84+
85+
cargo set-version --manifest-path "$root"/Cargo.toml "$next"
86+
echo "$next"

0 commit comments

Comments
 (0)