From b1166b31a9d70672c1b55c1eb04dbbacbbc83818 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Tue, 16 Apr 2024 03:34:00 +0200 Subject: [PATCH] Automated changelogs [WIP] --- .github/workflows/check-changelog.yml | 21 +++++++ .github/workflows/regular-release.yml | 42 +++++++++++++ changes/unreleased/README.md | 18 ++++++ changes/unreleased/major/.gitkeep | 0 changes/unreleased/medium/.gitkeep | 0 changes/unreleased/minor/.gitkeep | 0 default.nix | 16 +++++ scripts/check-changelog.sh | 39 ++++++++++++ scripts/release.sh | 5 +- scripts/version.sh | 85 +++++++++++++++++++++++++++ 10 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-changelog.yml create mode 100644 .github/workflows/regular-release.yml create mode 100644 changes/unreleased/README.md create mode 100644 changes/unreleased/major/.gitkeep create mode 100644 changes/unreleased/medium/.gitkeep create mode 100644 changes/unreleased/minor/.gitkeep create mode 100755 scripts/check-changelog.sh create mode 100755 scripts/version.sh diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml new file mode 100644 index 0000000..4f731fb --- /dev/null +++ b/.github/workflows/check-changelog.yml @@ -0,0 +1,21 @@ +name: Changelog +on: + pull_request: + branches: + - main + # Edited such that we can detect changes to the description + types: [opened, synchronize, reopened, edited] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + depth: 2 + + - name: check changelog + run: scripts/check-changelog.sh . ${{ github.event.pull_request.number }} + env: + GH_TOKEN: ${{ github.token }} + diff --git a/.github/workflows/regular-release.yml b/.github/workflows/regular-release.yml new file mode 100644 index 0000000..7a0b718 --- /dev/null +++ b/.github/workflows/regular-release.yml @@ -0,0 +1,42 @@ +name: Regular Version +on: + workflow_dispatch: # Allows triggering manually + schedule: + - cron: '47 14 * * 2' # runs every Tuesday at 14:47 UTC (chosen somewhat randomly) + +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # This fetches the entire Git history. + # This is needed so we can determine the commits (and therefore PRs) + # where the changelogs have been added + depth: 0 + + - uses: cachix/install-nix-action@v26 + + - name: Increment version and assemble changelog + id: version + run: | + version=$(./scripts/version.sh .) + echo "version=$version" >> "$GITHUB_OUTPUTS" + env: + GH_TOKEN: ${{ github.token }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + # To trigger CI for automated PRs, we use a separate machine account + # See https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#workarounds-to-trigger-further-workflow-runs + # and https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#push-pull-request-branches-to-a-fork + token: ${{ secrets.MACHINE_USER_PAT }} + path: repo + push-to-fork: infinixbot/nixpkgs-check-by-name + committer: infinixbot + author: infinixbot + commit-message: "Version ${{ github.steps.version.outputs.version }}" + branch: version + title: "Version ${{ github.steps.version.outputs.version }}" + body: "Automated version update. Merging this PR will trigger a release." diff --git a/changes/unreleased/README.md b/changes/unreleased/README.md new file mode 100644 index 0000000..62705c9 --- /dev/null +++ b/changes/unreleased/README.md @@ -0,0 +1,18 @@ +# Changelogs + +To add a changelog, add a Markdown file to a subdirectory depending on the effort required to update to +that version: + +- [Major](./major): A large effort. This will cause a version bump from e.g. 0.1.2 to 1.0.0 +- [Medium](./medium): Some effort. This will cause a version bump from e.g. 0.1.2 to 1.2.0 +- [Minor](./minor): Little/no effort. This will cause a version bump from e.g. 0.1.2 to 0.1.3 + +Therefore, the versions use [EffVer](https://jacobtomlinson.dev/effver/). + +The Markdown file must have the `.md` file ending, and be of the form + +```markdown +# Some descriptive title of the change + +Optionally more information +``` diff --git a/changes/unreleased/major/.gitkeep b/changes/unreleased/major/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/changes/unreleased/medium/.gitkeep b/changes/unreleased/medium/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/changes/unreleased/minor/.gitkeep b/changes/unreleased/minor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default.nix b/default.nix index a684206..bc07968 100644 --- a/default.nix +++ b/default.nix @@ -112,6 +112,22 @@ let ''; }; + # Run regularly by CI and turned into a PR + autoVersion = + pkgs.writeShellApplication { + name = "auto-version"; + runtimeInputs = with pkgs; [ + coreutils + git + github-cli + jq + cargo + toml-cli + cargo-edit + ]; + text = builtins.readFile ./scripts/version.sh; + }; + # Tests the tool on the pinned Nixpkgs tree, this is a good sanity check nixpkgsCheck = pkgs.runCommand "test-nixpkgs-check-by-name" { nativeBuildInputs = [ diff --git a/scripts/check-changelog.sh b/scripts/check-changelog.sh new file mode 100755 index 0000000..9a1a5c2 --- /dev/null +++ b/scripts/check-changelog.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail +shopt -s nullglob + +root=$1 +prNumber=$2 + +nonUserFacingString='this is not a user-facing change' + +# Run this first to validate files +for file in "$root"/changes/unreleased/*/*; do + if [[ "$(basename "$file")" == ".gitkeep" ]]; then + continue + fi + if [[ ! "$file" == *.md ]]; then + echo "File $file: Must be a markdown file with file ending .md" + exit 1 + fi + if [[ "$(sed -n '/^#/=' "$file")" != "1" ]]; then + echo "File $file: The first line must start with #, while all others must not start with #" + exit 1 + fi +done + +if gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/NixOS/nixpkgs-check-by-name/pulls/"$prNumber" \ + | jq -r '.body' \ + | grep -i "$nonUserFacingString"; then + echo "Not a user-facing change, no changelog necessary" +elif [[ -z "$(git -C "$root" log HEAD^..HEAD --name-only "$root"/changes/unreleased)" ]]; then + echo "If this PR contains a user-facing change, add a changelog in ./changes/unreleased" + echo "Otherwise, add \"$nonUserFacingString\" to the PR description" + exit 1 +else + echo "This is a user-facing change and there is a changelog" +fi diff --git a/scripts/release.sh b/scripts/release.sh index b512e07..c61ab7d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -46,7 +46,10 @@ To import it: ```bash gzip -cd '"$artifactName"' | nix-store --import | tail -1 ``` -' + +## Changes + +'"$(tail -1 "$root"/changes/released/"$version".md)" echo "Creating draft release" if ! release=$(gh api \ diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 0000000..53e55c8 --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -euo pipefail +shopt -s nullglob + +root=$1 + +[[ "$(toml get --raw Cargo.toml package.version)" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]] +splitVersion=( "${BASH_REMATCH[@]:1}" ) + +majorChanges=( "$root"/changes/unreleased/major/*.md ) +mediumChanges=( "$root"/changes/unreleased/medium/*.md ) +minorChanges=( "$root"/changes/unreleased/minor/*.md ) + +if (( ${#majorChanges[@]} > 0 )); then + # If we didn't have `|| true` this would exit the program due to `set -e`, + # because (( ... )) returns the incremental value, which is treated as the exit code.. + (( splitVersion[0]++ )) || true + splitVersion[1]=0 + splitVersion[2]=0 +elif (( ${#mediumChanges[@]} > 0 )); then + (( splitVersion[1]++ )) || true + splitVersion[2]=0 +elif (( ${#minorChanges[@]} > 0 )); then + (( splitVersion[2]++ )) || true +else + echo >&2 "No changes" + exit 0 +fi + +next=${splitVersion[0]}.${splitVersion[1]}.${splitVersion[2]} +releaseFile=$root/changes/released/${next}.md +mkdir -p "$(dirname "$releaseFile")" + +echo "# Version $next ($(date --iso-8601 --utc))" > "$releaseFile" +echo "" >> "$releaseFile" + +# shellcheck disable=SC2016 +for file in "${majorChanges[@]}" "${mediumChanges[@]}" "${minorChanges[@]}"; do + commit=$(git log -1 --format=%H -- "$file") + if ! gh api graphql \ + -f sha="$commit" \ + -f query=' + query ($sha: String) { + repository(owner: "NixOS", name: "nixpkgs-check-by-name") { + commit: object(expression: $sha) { + ... on Commit { + associatedPullRequests(first: 100) { + nodes { + merged + baseRefName + baseRepository { nameWithOwner } + number + author { login } + } + } + } + } + } + }' | \ + jq --exit-status -r --arg file "$file" ' + .data.repository.commit.associatedPullRequests?.nodes?[]? + | select( + # We need to make sure to get the right PR, there can be many + .merged and + .baseRepository.nameWithOwner == "NixOS/nixpkgs-check-by-name" and + .baseRefName == "main") + | "\(.number) \(.author.login) \($ARGS.named.file)"'; then + echo >&2 "Couldn't get PR for file $file" + exit 1 + fi +done | \ +sort -n | \ +while read -r number author file; do + # Replace the first line `# ` by `- <title> by @author in #number` + # All other non-empty lines are indented with 2 spaces to make the markdown formatting work + sed "$file" >> "$releaseFile" \ + -e "1s|#[[:space:]]\(.*\)|- \1 by [@$author](https://github.com/$author) in [#$number](https://github.com/NixOS/nixpkgs-check-by-name/pull/$number)|" \ + -e '2,$s/^\(.\)/ \1/' + + rm "$file" +done + +cargo set-version "$next" +echo "$next"