Skip to content

Commit 3a64026

Browse files
committed
Automated changelogs [WIP]
1 parent 62d820f commit 3a64026

12 files changed

+306
-1
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

+66
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ on:
33
pull_request:
44
branches:
55
- main
6+
push:
7+
branches:
8+
- main
69

710
jobs:
811
build:
@@ -15,6 +18,69 @@ jobs:
1518
- name: build
1619
run: nix-build -A ci
1720

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

.github/workflows/regular-release.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
gh pr create \
19+
--repo ${{ github.repository }} \
20+
--title "$(git log -1 --format=%s HEAD)" \
21+
--body "Automated release PR.
22+
23+
- [x] This change is user-facing
24+
"
25+
env:
26+
# Needed so that CI triggers
27+
GH_TOKEN: ${{ secrets.MACHINE_USER_PAT }}

changes/unreleased/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Changelogs
2+
3+
To add a changelog, add a Markdown file to a subdirectory depending on the effort required to update to
4+
that version:
5+
6+
- [Major](./major): A large effort. This will cause a version bump from e.g. 0.1.2 to 1.0.0
7+
- [Medium](./medium): Some effort. This will cause a version bump from e.g. 0.1.2 to 1.2.0
8+
- [Minor](./minor): Little/no effort. This will cause a version bump from e.g. 0.1.2 to 0.1.3
9+
10+
Therefore, the versions use [EffVer](https://jacobtomlinson.dev/effver/).
11+
12+
The Markdown file must have the `.md` file ending, and be of the form
13+
14+
```markdown
15+
# Some descriptive title of the change
16+
17+
Optionally more information
18+
```

changes/unreleased/major/.gitkeep

Whitespace-only changes.

changes/unreleased/medium/.gitkeep

Whitespace-only changes.

changes/unreleased/minor/.gitkeep

Whitespace-only changes.

default.nix

+19
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,28 @@ let
109109
echo >&2 "Running ${script}"
110110
${lib.getExe script} "$1"
111111
'') (lib.attrValues updateScripts)}
112+
echo ""
113+
# To not fail the changelog check
114+
printf "%s\n" "- [ ] This change is user-facing"
112115
'';
113116
};
114117

118+
# Run regularly by CI and turned into a PR
119+
autoVersion =
120+
pkgs.writeShellApplication {
121+
name = "auto-version";
122+
runtimeInputs = with pkgs; [
123+
coreutils
124+
git
125+
github-cli
126+
jq
127+
cargo
128+
toml-cli
129+
cargo-edit
130+
];
131+
text = builtins.readFile ./scripts/version.sh;
132+
};
133+
115134
# Tests the tool on the pinned Nixpkgs tree, this is a good sanity check
116135
nixpkgsCheck = pkgs.runCommand "test-nixpkgs-check-by-name" {
117136
nativeBuildInputs = [

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
@@ -46,7 +46,10 @@ To import it:
4646
```bash
4747
gzip -cd '"$artifactName"' | nix-store --import | tail -1
4848
```
49-
'
49+
50+
## Changes
51+
52+
'"$(tail -1 "$root"/changes/released/"$version".md)"
5053

5154
echo "Creating draft release"
5255
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" >> "$releaseFile" \
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/'
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)