From dda15e602b6a3fceeb3a42b7d6994d51c2dedde7 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:19:57 -0600 Subject: [PATCH 01/60] test --- .github/workflows/prerelease.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index d023af13a71..3bc7c145411 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -2,7 +2,7 @@ name: Pre-release on: push: - branches: [ master ] + branches: [ master, release ] paths-ignore: - '*.md' - '*.json' @@ -55,6 +55,7 @@ jobs: uses: gradle/actions/setup-gradle@v5 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-read-only: false - name: Run Gradle run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar @@ -66,14 +67,24 @@ jobs: SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} + - name: Generate release notes + id: notes + run: | + echo "body<> $GITHUB_OUTPUT + git log --pretty=format:"- %s (%h)" $(git describe --tags --abbrev=0)..HEAD >> $GITHUB_OUTPUT + echo "" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Create pre-release - uses: marvinpinto/action-automatic-releases@latest + uses: softprops/action-gh-release@v3 with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "pre-release" + tag_name: pre-release + name: Pre-release Build prerelease: true - title: "Pre-release Build" + body: ${{ steps.notes.outputs.body }} files: | app/build/outputs/apk/prerelease/release/*.apk app/build/libs/app-sources.jar app/build/classes.jar + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From 11ab2919558e68d924c938e3e97848c584796aa9 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:36:56 -0600 Subject: [PATCH 02/60] Update --- .github/workflows/prerelease.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 3bc7c145411..f39f2142e0e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -70,8 +70,14 @@ jobs: - name: Generate release notes id: notes run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + RANGE="HEAD" + else + RANGE="$LAST_TAG..HEAD" + fi echo "body<> $GITHUB_OUTPUT - git log --pretty=format:"- %s (%h)" $(git describe --tags --abbrev=0)..HEAD >> $GITHUB_OUTPUT + git log --pretty=format:"- %s (%h)" $RANGE >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT From ce79221537287945d731c57afbedb398b9067ebf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:40:33 -0600 Subject: [PATCH 03/60] Try --- .github/workflows/prerelease.yml | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f39f2142e0e..8fa91674d3c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -69,15 +69,40 @@ jobs: - name: Generate release notes id: notes + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | + set -e + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then - RANGE="HEAD" + COMMITS=$(git rev-list HEAD) else - RANGE="$LAST_TAG..HEAD" + COMMITS=$(git rev-list ${LAST_TAG}..HEAD) fi + echo "body<> $GITHUB_OUTPUT - git log --pretty=format:"- %s (%h)" $RANGE >> $GITHUB_OUTPUT + + for sha in $COMMITS; do + SHORT=$(echo $sha | cut -c1-7) + + PR_JSON=$(gh api repos/${GITHUB_REPOSITORY}/commits/$sha/pulls -H "Accept: application/vnd.github.groot-preview+json" || true) + + PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number // empty') + PR_TITLE=$(echo "$PR_JSON" | jq -r '.[0].title // empty') + PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.[0].user.login // empty') + + COMMIT_MSG=$(git show -s --format=%s $sha) + AUTHOR=$(git show -s --format=%an $sha) + + if [ -n "$PR_NUMBER" ]; then + echo "- PR #$PR_NUMBER: $PR_TITLE (@$PR_AUTHOR) \`$SHORT\`" >> $GITHUB_OUTPUT + else + echo "- $COMMIT_MSG — $AUTHOR \`$SHORT\`" >> $GITHUB_OUTPUT + fi + done + echo "" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT From b78cb951f4dcc52dc9090ed453d77e4f7bb1951b Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:50:38 -0600 Subject: [PATCH 04/60] Try --- .github/generate-release-notes.sh | 109 ++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/generate-release-notes.sh diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh new file mode 100644 index 00000000000..8006d95130e --- /dev/null +++ b/.github/generate-release-notes.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ----------------------------- +# CONFIG +# ----------------------------- +REPO="${GITHUB_REPOSITORY}" +GH_TOKEN="${GH_TOKEN:-}" + +if [ -z "$GH_TOKEN" ]; then + echo "GH_TOKEN is required" + exit 1 +fi + +# ----------------------------- +# FIND RANGE +# ----------------------------- +LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + +if [ -z "$LAST_TAG" ]; then + COMMITS=$(git rev-list HEAD) +else + COMMITS=$(git rev-list "${LAST_TAG}..HEAD") +fi + +declare -A GROUPS + +# ----------------------------- +# PROCESS COMMITS +# ----------------------------- +for sha in $COMMITS; do + shortsha="${sha:0:7}" + + msg=$(git show -s --format=%s "$sha") + body=$(git show -s --format=%b "$sha") + author=$(git show -s --format=%an "$sha") + + type=$(echo "$msg" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') + + # ----------------------------- + # PR LOOKUP (GitHub API) + # ----------------------------- + pr_json=$(gh api "repos/${REPO}/commits/${sha}/pulls" \ + -H "Accept: application/vnd.github.groot-preview+json" \ + --silent 2>/dev/null || true) + + prs=$(echo "$pr_json" | jq -r '.[].number' 2>/dev/null | paste -sd "," -) + + if [ -n "$prs" ]; then + prs=" #${prs}" + else + prs="" + fi + + # ----------------------------- + # FORMAT LINE + # ----------------------------- + line="\`${shortsha}\`: ${msg} (${author})${prs}" + + # ----------------------------- + # BREAKING CHANGE FLAG + # ----------------------------- + if echo "$msg$body" | grep -q "BREAKING CHANGE"; then + line="${line} **BREAKING**" + fi + + # ----------------------------- + # SPECIAL CASE: chore(locales) + # ----------------------------- + if [[ "$msg" == chore\(locales\)* ]]; then + GROUPS["Chores"]+="**locales**: ${line}"$'\n' + continue + fi + + # ----------------------------- + # GROUPING (conventional commits) + # ----------------------------- + case "$type" in + feat*) + GROUPS["Features"]+="${line}"$'\n' + ;; + fix*) + GROUPS["Bug Fixes"]+="${line}"$'\n' + ;; + chore*) + GROUPS["Chores"]+="${line}"$'\n' + ;; + *) + GROUPS["Other"]+="${line}"$'\n' + ;; + esac +done + +# ----------------------------- +# OUTPUT FOR GITHUB ACTIONS +# ----------------------------- +{ + echo "body<> "$GITHUB_OUTPUT" From 1c315007eb975dff7f716163b214688f99e7dedb Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:51:42 -0600 Subject: [PATCH 05/60] Try --- .github/workflows/prerelease.yml | 35 ++------------------------------ 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 8fa91674d3c..2cdc32d4a3e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -72,39 +72,8 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - set -e - - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - - if [ -z "$LAST_TAG" ]; then - COMMITS=$(git rev-list HEAD) - else - COMMITS=$(git rev-list ${LAST_TAG}..HEAD) - fi - - echo "body<> $GITHUB_OUTPUT - - for sha in $COMMITS; do - SHORT=$(echo $sha | cut -c1-7) - - PR_JSON=$(gh api repos/${GITHUB_REPOSITORY}/commits/$sha/pulls -H "Accept: application/vnd.github.groot-preview+json" || true) - - PR_NUMBER=$(echo "$PR_JSON" | jq -r '.[0].number // empty') - PR_TITLE=$(echo "$PR_JSON" | jq -r '.[0].title // empty') - PR_AUTHOR=$(echo "$PR_JSON" | jq -r '.[0].user.login // empty') - - COMMIT_MSG=$(git show -s --format=%s $sha) - AUTHOR=$(git show -s --format=%an $sha) - - if [ -n "$PR_NUMBER" ]; then - echo "- PR #$PR_NUMBER: $PR_TITLE (@$PR_AUTHOR) \`$SHORT\`" >> $GITHUB_OUTPUT - else - echo "- $COMMIT_MSG — $AUTHOR \`$SHORT\`" >> $GITHUB_OUTPUT - fi - done - - echo "" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + chmod +x .github/generate-release-notes.sh + .github/generate-release-notes.sh - name: Create pre-release uses: softprops/action-gh-release@v3 From c942c6c7bda6e20b8bad924d36e95b88c9726c1f Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:55:01 -0600 Subject: [PATCH 06/60] Update --- .github/generate-release-notes.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 8006d95130e..05c1e4d0881 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -23,7 +23,8 @@ else COMMITS=$(git rev-list "${LAST_TAG}..HEAD") fi -declare -A GROUPS +declare -g -A GROUPS 2>/dev/null || true +GROUPS=() # ----------------------------- # PROCESS COMMITS From 40544eb1fe843ace32f322bef1cceeb4814d62fd Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:55:59 -0600 Subject: [PATCH 07/60] - --- .github/workflows/prerelease.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 2cdc32d4a3e..f11a0c77fa5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -71,9 +71,7 @@ jobs: id: notes env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - chmod +x .github/generate-release-notes.sh - .github/generate-release-notes.sh + run: bash .github/generate-release-notes.sh - name: Create pre-release uses: softprops/action-gh-release@v3 From e486498d282069e064cc5deaf37b802cb07d547b Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 14:57:13 -0600 Subject: [PATCH 08/60] Update --- .github/generate-release-notes.sh | 67 ++++++------------------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 05c1e4d0881..18f205252ec 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -1,20 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# ----------------------------- -# CONFIG -# ----------------------------- REPO="${GITHUB_REPOSITORY}" -GH_TOKEN="${GH_TOKEN:-}" -if [ -z "$GH_TOKEN" ]; then - echo "GH_TOKEN is required" - exit 1 -fi - -# ----------------------------- -# FIND RANGE -# ----------------------------- LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -z "$LAST_TAG" ]; then @@ -23,12 +11,11 @@ else COMMITS=$(git rev-list "${LAST_TAG}..HEAD") fi -declare -g -A GROUPS 2>/dev/null || true -GROUPS=() +FEATURES="" +FIXES="" +CHORES="" +OTHER="" -# ----------------------------- -# PROCESS COMMITS -# ----------------------------- for sha in $COMMITS; do shortsha="${sha:0:7}" @@ -38,9 +25,6 @@ for sha in $COMMITS; do type=$(echo "$msg" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') - # ----------------------------- - # PR LOOKUP (GitHub API) - # ----------------------------- pr_json=$(gh api "repos/${REPO}/commits/${sha}/pulls" \ -H "Accept: application/vnd.github.groot-preview+json" \ --silent 2>/dev/null || true) @@ -53,57 +37,32 @@ for sha in $COMMITS; do prs="" fi - # ----------------------------- - # FORMAT LINE - # ----------------------------- line="\`${shortsha}\`: ${msg} (${author})${prs}" - # ----------------------------- - # BREAKING CHANGE FLAG - # ----------------------------- if echo "$msg$body" | grep -q "BREAKING CHANGE"; then line="${line} **BREAKING**" fi - # ----------------------------- - # SPECIAL CASE: chore(locales) - # ----------------------------- if [[ "$msg" == chore\(locales\)* ]]; then - GROUPS["Chores"]+="**locales**: ${line}"$'\n' + CHORES+="**locales**: ${line}"$'\n' continue fi - # ----------------------------- - # GROUPING (conventional commits) - # ----------------------------- case "$type" in - feat*) - GROUPS["Features"]+="${line}"$'\n' - ;; - fix*) - GROUPS["Bug Fixes"]+="${line}"$'\n' - ;; - chore*) - GROUPS["Chores"]+="${line}"$'\n' - ;; - *) - GROUPS["Other"]+="${line}"$'\n' - ;; + feat*) FEATURES+="${line}"$'\n' ;; + fix*) FIXES+="${line}"$'\n' ;; + chore*) CHORES+="${line}"$'\n' ;; + *) OTHER+="${line}"$'\n' ;; esac done -# ----------------------------- -# OUTPUT FOR GITHUB ACTIONS -# ----------------------------- { echo "body< Date: Wed, 6 May 2026 15:01:12 -0600 Subject: [PATCH 09/60] Update --- .github/generate-release-notes.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 18f205252ec..17693e9a7ef 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -14,7 +14,7 @@ fi FEATURES="" FIXES="" CHORES="" -OTHER="" +COMMITS="" for sha in $COMMITS; do shortsha="${sha:0:7}" @@ -52,7 +52,7 @@ for sha in $COMMITS; do feat*) FEATURES+="${line}"$'\n' ;; fix*) FIXES+="${line}"$'\n' ;; chore*) CHORES+="${line}"$'\n' ;; - *) OTHER+="${line}"$'\n' ;; + *) COMMITS+="${line}"$'\n' ;; esac done @@ -62,7 +62,7 @@ done [ -n "$FEATURES" ] && echo "## Features" && echo -e "$FEATURES" [ -n "$FIXES" ] && echo "## Bug Fixes" && echo -e "$FIXES" [ -n "$CHORES" ] && echo "## Chores" && echo -e "$CHORES" - [ -n "$OTHER" ] && echo "## Other" && echo -e "$OTHER" + [ -n "$COMMITS" ] && echo "## Commits" && echo -e "$COMMITS" echo "" echo "EOF" From 680341f975f2e8ca6ebcbd78904ebe1a7bd4c521 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:01:40 -0600 Subject: [PATCH 10/60] chore(test): test --- .github/workflows/prerelease.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f11a0c77fa5..9bc9b118848 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -1,3 +1,4 @@ + name: Pre-release on: From 1c3e8c60f902ac504951dd0a2b9674a5a3b0c4d1 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:05:53 -0600 Subject: [PATCH 11/60] chore(test): test --- .github/generate-release-notes.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 17693e9a7ef..302dea38b39 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -14,9 +14,11 @@ fi FEATURES="" FIXES="" CHORES="" -COMMITS="" +COMMITS_SECTION="" + +echo "$COMMITS" | while read -r sha; do + [ -z "$sha" ] && continue -for sha in $COMMITS; do shortsha="${sha:0:7}" msg=$(git show -s --format=%s "$sha") @@ -26,12 +28,12 @@ for sha in $COMMITS; do type=$(echo "$msg" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') pr_json=$(gh api "repos/${REPO}/commits/${sha}/pulls" \ - -H "Accept: application/vnd.github.groot-preview+json" \ + --header "Accept: application/vnd.github+json" \ --silent 2>/dev/null || true) prs=$(echo "$pr_json" | jq -r '.[].number' 2>/dev/null | paste -sd "," -) - if [ -n "$prs" ]; then + if [ -n "${prs:-}" ]; then prs=" #${prs}" else prs="" @@ -52,7 +54,7 @@ for sha in $COMMITS; do feat*) FEATURES+="${line}"$'\n' ;; fix*) FIXES+="${line}"$'\n' ;; chore*) CHORES+="${line}"$'\n' ;; - *) COMMITS+="${line}"$'\n' ;; + *) COMMITS_SECTION+="${line}"$'\n' ;; esac done @@ -62,7 +64,7 @@ done [ -n "$FEATURES" ] && echo "## Features" && echo -e "$FEATURES" [ -n "$FIXES" ] && echo "## Bug Fixes" && echo -e "$FIXES" [ -n "$CHORES" ] && echo "## Chores" && echo -e "$CHORES" - [ -n "$COMMITS" ] && echo "## Commits" && echo -e "$COMMITS" + [ -n "$COMMITS_SECTION" ] && echo "## Commits" && echo -e "$COMMITS_SECTION" echo "" echo "EOF" From 35eaf4448e1f9b38198e290d6d98a154e6707278 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:11:24 -0600 Subject: [PATCH 12/60] chore(test): test --- .github/generate-release-notes.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 302dea38b39..1328072a868 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -16,7 +16,7 @@ FIXES="" CHORES="" COMMITS_SECTION="" -echo "$COMMITS" | while read -r sha; do +while read -r sha; do [ -z "$sha" ] && continue shortsha="${sha:0:7}" @@ -56,7 +56,8 @@ echo "$COMMITS" | while read -r sha; do chore*) CHORES+="${line}"$'\n' ;; *) COMMITS_SECTION+="${line}"$'\n' ;; esac -done + +done <<< "$COMMITS" { echo "body< Date: Wed, 6 May 2026 15:14:56 -0600 Subject: [PATCH 13/60] chore(test): test --- .github/generate-release-notes.sh | 34 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 1328072a868..096a420fd1b 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -27,11 +27,13 @@ while read -r sha; do type=$(echo "$msg" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') - pr_json=$(gh api "repos/${REPO}/commits/${sha}/pulls" \ - --header "Accept: application/vnd.github+json" \ - --silent 2>/dev/null || true) + scope=$(echo "$msg" | sed -n 's/^[a-z]*(\([^)]*\)).*/\1/p') + title=$(echo "$msg" | sed 's/^[a-z]*(\([^)]*\)):\s*//') - prs=$(echo "$pr_json" | jq -r '.[].number' 2>/dev/null | paste -sd "," -) + prs=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/${REPO}/commits/${sha}/pulls" \ + --jq '.[].number' 2>/dev/null | paste -sd "," -) if [ -n "${prs:-}" ]; then prs=" #${prs}" @@ -39,24 +41,26 @@ while read -r sha; do prs="" fi - line="\`${shortsha}\`: ${msg} (${author})${prs}" + if [[ "$msg" == chore* && -n "$scope" ]]; then + line="**${scope}**: \`${shortsha}\`: ${title} (${author})${prs}" + else + line="\`${shortsha}\`: ${msg} (${author})${prs}" + fi if echo "$msg$body" | grep -q "BREAKING CHANGE"; then line="${line} **BREAKING**" fi - if [[ "$msg" == chore\(locales\)* ]]; then - CHORES+="**locales**: ${line}"$'\n' - continue + if [[ "$msg" == chore* ]]; then + CHORES+="${line}"$'\n' + elif [[ "$type" == feat* ]]; then + FEATURES+="${line}"$'\n' + elif [[ "$type" == fix* ]]; then + FIXES+="${line}"$'\n' + else + COMMITS_SECTION+="${line}"$'\n' fi - case "$type" in - feat*) FEATURES+="${line}"$'\n' ;; - fix*) FIXES+="${line}"$'\n' ;; - chore*) CHORES+="${line}"$'\n' ;; - *) COMMITS_SECTION+="${line}"$'\n' ;; - esac - done <<< "$COMMITS" { From f4b62a1c4be83ee55f5e97b3ae308c131b24c51a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:22:03 -0600 Subject: [PATCH 14/60] chore(test): test --- .github/generate-release-notes.sh | 35 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 096a420fd1b..a4dc8145cd3 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -35,31 +35,38 @@ while read -r sha; do "/repos/${REPO}/commits/${sha}/pulls" \ --jq '.[].number' 2>/dev/null | paste -sd "," -) + has_pr=false if [ -n "${prs:-}" ]; then - prs=" #${prs}" + has_pr=true + identity=" #${prs}" else - prs="" + identity="" fi - if [[ "$msg" == chore* && -n "$scope" ]]; then - line="**${scope}**: \`${shortsha}\`: ${title} (${author})${prs}" + if $has_pr; then + prefix="" else - line="\`${shortsha}\`: ${msg} (${author})${prs}" + prefix="\`${shortsha}\`: " fi + if [[ -n "$scope" ]]; then + scope="**${scope}**: " + else + scope="" + fi + + line="${prefix}${scope}${msg}${identity}" + if echo "$msg$body" | grep -q "BREAKING CHANGE"; then line="${line} **BREAKING**" fi - if [[ "$msg" == chore* ]]; then - CHORES+="${line}"$'\n' - elif [[ "$type" == feat* ]]; then - FEATURES+="${line}"$'\n' - elif [[ "$type" == fix* ]]; then - FIXES+="${line}"$'\n' - else - COMMITS_SECTION+="${line}"$'\n' - fi + case "$type" in + feat*) FEATURES+="${line}"$'\n' ;; + fix*) FIXES+="${line}"$'\n' ;; + chore*) CHORES+="${line}"$'\n' ;; + *) COMMITS_SECTION+="${line}"$'\n' ;; + esac done <<< "$COMMITS" From d60b79dca3bc33848b13d70b0d03caf01393ef2b Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:22:19 -0600 Subject: [PATCH 15/60] Test --- .github/workflows/prerelease.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 9bc9b118848..f11a0c77fa5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -1,4 +1,3 @@ - name: Pre-release on: From 3408aff3067a91818c4ce8a80e7d5b6d152f9215 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:31:45 -0600 Subject: [PATCH 16/60] chore(test): test --- .github/generate-release-notes.sh | 398 ++++++++++++++++++++++++++---- 1 file changed, 345 insertions(+), 53 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index a4dc8145cd3..3bb20e0e1ec 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -1,83 +1,375 @@ #!/usr/bin/env bash set -euo pipefail -REPO="${GITHUB_REPOSITORY}" +# ============================================================================= +# generate_changelog.sh +# +# Replicates the changelog format of the automatic-releases GitHub Action. +# Designed to run inside a GitHub Actions workflow — all required values are +# sourced from the standard GHA environment variables automatically. +# +# Usage: +# generate_changelog.sh [PREVIOUS_TAG] +# +# Args: +# PREVIOUS_TAG - Optional. Tag to compare from. Auto-detected via semver if omitted. +# +# Required GHA environment variables (set automatically by GitHub Actions): +# GITHUB_TOKEN - Auth token for API calls (secrets.GITHUB_TOKEN) +# GITHUB_REPOSITORY - "owner/repo" (github.repository) +# GITHUB_SHA - Current commit SHA (github.sha) +# GITHUB_REF - Current ref, e.g. refs/tags/v1.2 (github.ref) +# ============================================================================= -LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- -if [ -z "$LAST_TAG" ]; then - COMMITS=$(git rev-list HEAD) -else - COMMITS=$(git rev-list "${LAST_TAG}..HEAD") +short_sha() { + echo "${1:0:7}" +} + +# Print to stderr so it doesn't pollute changelog output +log() { echo "[generate_changelog] $*" >&2; } + +require() { + command -v "$1" &>/dev/null || { log "ERROR: '$1' is required but not found"; exit 1; } +} + +require git +require curl +require jq + +# --------------------------------------------------------------------------- +# Config — sourced entirely from GHA environment +# --------------------------------------------------------------------------- + +PREVIOUS_TAG="${1:-}" + +# GITHUB_SHA is the SHA of the commit that triggered the workflow +CURRENT_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" + +# GITHUB_TOKEN is injected by Actions; fail fast if missing +if [[ -z "${GITHUB_TOKEN:-}" ]]; then + log "ERROR: GITHUB_TOKEN is not set. Add 'env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}' to your workflow step." + exit 1 fi -FEATURES="" -FIXES="" -CHORES="" -COMMITS_SECTION="" +# GITHUB_REPOSITORY is "owner/repo" +if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + log "ERROR: GITHUB_REPOSITORY is not set — are you running outside of GitHub Actions?" + exit 1 +fi +OWNER="$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1)" +REPO="$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" + +log "Repo: ${OWNER}/${REPO}" +log "Current SHA: ${CURRENT_SHA}" + +# --------------------------------------------------------------------------- +# GitHub API helper +# --------------------------------------------------------------------------- + +gh_api() { + local endpoint="$1" + curl -fsSL \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com${endpoint}" +} -while read -r sha; do - [ -z "$sha" ] && continue +# --------------------------------------------------------------------------- +# Find previous release tag (semver, descending) +# --------------------------------------------------------------------------- - shortsha="${sha:0:7}" +find_previous_tag() { + local current_tag="$1" + log "Searching for previous semver tag before ${current_tag}" - msg=$(git show -s --format=%s "$sha") - body=$(git show -s --format=%b "$sha") - author=$(git show -s --format=%an "$sha") + # Collect all semver tags from git + local tags + tags="$(git tag --list | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' || true)" - type=$(echo "$msg" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]') + if [[ -z "$tags" ]]; then + log "No semver tags found" + echo "" + return + fi + + # Sort descending (semver-aware via sort -V), find first tag strictly less than current + # Strip leading 'v' for comparison, then restore it + local prev="" + while IFS= read -r tag; do + [[ "$tag" == "$current_tag" ]] && continue + # Check tag < current_tag using sort -V + local lower + lower="$(printf '%s\n%s\n' "$current_tag" "$tag" | sort -V | head -1)" + if [[ "$lower" == "$tag" ]]; then + prev="$tag" + break + fi + done < <(echo "$tags" | sort -rV) - scope=$(echo "$msg" | sed -n 's/^[a-z]*(\([^)]*\)).*/\1/p') - title=$(echo "$msg" | sed 's/^[a-z]*(\([^)]*\)):\s*//') + echo "$prev" +} - prs=$(gh api \ - -H "Accept: application/vnd.github+json" \ - "/repos/${REPO}/commits/${sha}/pulls" \ - --jq '.[].number' 2>/dev/null | paste -sd "," -) +# --------------------------------------------------------------------------- +# Get commits between previous tag and current SHA +# --------------------------------------------------------------------------- - has_pr=false - if [ -n "${prs:-}" ]; then - has_pr=true - identity=" #${prs}" +get_commits() { + local base="$1" + local head="$2" + + if [[ -z "$base" ]]; then + log "No previous tag — using full history to ${head}" + git log --format='%H %s' "${head}" else - identity="" + log "Getting commits between ${base} and ${head}" + git log --format='%H %s' "${base}..${head}" + fi +} + +# --------------------------------------------------------------------------- +# Get pull requests associated with a commit SHA (via GitHub API) +# --------------------------------------------------------------------------- + +get_prs_for_commit() { + local sha="$1" + gh_api "/repos/${OWNER}/${REPO}/commits/${sha}/pulls" \ + 2>/dev/null || echo "[]" +} + +# --------------------------------------------------------------------------- +# Conventional commit parser +# Parses "type(scope): subject" or plain subject +# --------------------------------------------------------------------------- + +# Known types -> section headers (matches ConventionalCommitTypes enum order) +declare -a CC_KEYS=(feat fix docs style refactor perf test build ci chore revert) +declare -A CC_LABELS=( + [feat]="Features" + [fix]="Bug Fixes" + [docs]="Documentation" + [style]="Styles" + [refactor]="Code Refactoring" + [perf]="Performance Improvements" + [test]="Tests" + [build]="Builds" + [ci]="Continuous Integration" + [chore]="Chores" + [revert]="Reverts" +) + +parse_commit_type() { + # Returns type if conventional, else empty + echo "$1" | sed -nE 's/^([a-z]+)(\([^)]*\))?!?:.*/\1/p' +} + +parse_commit_scope() { + echo "$1" | sed -nE 's/^[a-z]+\(([^)]*)\)!?:.*/\1/p' +} + +parse_commit_subject() { + echo "$1" | sed -nE 's/^[a-z]+(\([^)]*\))?!?:[[:space:]]*(.*)/\2/p' +} + +is_breaking() { + # Check for '!' in type(scope)!: or BREAKING CHANGE in body/footer + local header="$1" + local body="$2" + echo "$header" | grep -qE '^[a-z]+(\([^)]*\))?!:' && return 0 + echo "$body" | grep -qE '^BREAKING[[:space:]]+CHANGES?:[[:space:]]' && return 0 + return 1 +} + +# --------------------------------------------------------------------------- +# Format a single changelog entry +# +# Matches getFormattedChangelogEntry() in utils.ts: +# +# If type is set (conventional commit): +# - **scope**: subject [author](url) (with optional PR links) +# Else (plain commit): +# - sha: header (author) (with optional PR links) +# --------------------------------------------------------------------------- + +format_entry() { + local sha="$1" + local header="$2" + local author_name="$3" + local commit_url="$4" + local type="$5" + local scope="$6" + local subject="$7" + local pr_json="$8" # JSON array of {number, url} + + local short + short="$(short_sha "$sha")" + + # Build PR string: [#1](url),[#2](url) + local pr_string="" + if [[ "$pr_json" != "[]" && -n "$pr_json" ]]; then + pr_string="$(echo "$pr_json" | jq -r '[.[] | "[#\(.number)](\(.html_url))"] | join(",")')" + [[ -n "$pr_string" ]] && pr_string=" ${pr_string}" fi - if $has_pr; then - prefix="" + if [[ -n "$type" ]]; then + # Conventional commit format + local scope_str="" + [[ -n "$scope" ]] && scope_str="**${scope}**: " + echo "- ${scope_str}${subject}${pr_string} ([${author_name}](${commit_url}))" else - prefix="\`${shortsha}\`: " + # Plain commit format + echo "- ${short}: ${header} (${author_name})${pr_string}" fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +# Determine current tag +# GITHUB_REF is "refs/tags/v1.2.3" on tag push events — extract the tag name directly +CURRENT_TAG="" +if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then + CURRENT_TAG="${BASH_REMATCH[1]}" + log "Detected tag from GITHUB_REF: ${CURRENT_TAG}" +else + # Fallback: check if HEAD is on an exact tag (e.g. workflow_dispatch on a tag) + CURRENT_TAG="$(git describe --exact-match "${CURRENT_SHA}" 2>/dev/null || true)" +fi - if [[ -n "$scope" ]]; then - scope="**${scope}**: " +if [[ -z "$PREVIOUS_TAG" && -n "$CURRENT_TAG" ]]; then + PREVIOUS_TAG="$(find_previous_tag "$CURRENT_TAG")" +fi + +log "Previous tag: ${PREVIOUS_TAG:-}" + +# Collect raw commits +mapfile -t RAW_COMMITS < <(get_commits "$PREVIOUS_TAG" "$CURRENT_SHA") + +if [[ ${#RAW_COMMITS[@]} -eq 0 ]]; then + log "No commits found" + echo "" + exit 0 +fi + +# --------------------------------------------------------------------------- +# Temporary storage: parallel arrays indexed by commit position +# We separate commits into buckets: breaking | by type | other +# --------------------------------------------------------------------------- + +declare -a BREAKING_ENTRIES=() +declare -A TYPE_ENTRIES=() # key = cc type, value = newline-separated entries +declare -a OTHER_ENTRIES=() + +for raw in "${RAW_COMMITS[@]}"; do + sha="${raw%% *}" + message="${raw#* }" + + # Skip merge commits (matches mergePattern: ^Merge pull request #(.*) from (.*)$) + if echo "$message" | grep -qE '^Merge pull request #[0-9]+ from '; then + log "Skipping merge commit: ${sha}" + continue + fi + + log "Processing commit ${sha}: ${message}" + + # Get commit detail from git for author info + author_name="$(git log -1 --format='%an' "$sha" 2>/dev/null || echo "unknown")" + commit_url="https://github.com/${OWNER}/${REPO}/commit/${sha}" + + # Get commit body for breaking change detection + commit_body="$(git log -1 --format='%b' "$sha" 2>/dev/null || true)" + + # Parse conventional commit + type="$(parse_commit_type "$message")" + scope="$(parse_commit_scope "$message")" + subject="$(parse_commit_subject "$message")" + + # Validate type is a known CC type (else treat as plain) + known_type="" + for k in "${CC_KEYS[@]}"; do + [[ "$type" == "$k" ]] && known_type="$type" && break + done + + # Detect breaking change + breaking=false + if is_breaking "$message" "$commit_body"; then + breaking=true + fi + + # Fetch associated PRs + log "Fetching PRs for ${sha}" + pr_json="$(get_prs_for_commit "$sha")" + + entry="$(format_entry "$sha" "$message" "$author_name" "$commit_url" \ + "$known_type" "$scope" "$subject" "$pr_json")" + + if [[ "$breaking" == "true" ]]; then + BREAKING_ENTRIES+=("$entry") + fi + + if [[ -n "$known_type" ]]; then + existing="${TYPE_ENTRIES[$known_type]:-}" + if [[ -n "$existing" ]]; then + TYPE_ENTRIES[$known_type]="${existing}"$'\n'"${entry}" + else + TYPE_ENTRIES[$known_type]="${entry}" + fi else - scope="" + OTHER_ENTRIES+=("$entry") fi +done + +# --------------------------------------------------------------------------- +# Assemble changelog (matches generateChangelogFromParsedCommits order) +# --------------------------------------------------------------------------- - line="${prefix}${scope}${msg}${identity}" +CHANGELOG="" - if echo "$msg$body" | grep -q "BREAKING CHANGE"; then - line="${line} **BREAKING**" +# Breaking Changes section +if [[ ${#BREAKING_ENTRIES[@]} -gt 0 ]]; then + CHANGELOG+="## Breaking Changes"$'\n' + for e in "${BREAKING_ENTRIES[@]}"; do + CHANGELOG+="${e}"$'\n' + done +fi + +# Conventional commit type sections (in enum declaration order) +for key in "${CC_KEYS[@]}"; do + block="${TYPE_ENTRIES[$key]:-}" + [[ -z "$block" ]] && continue + + if [[ -n "$CHANGELOG" ]]; then + CHANGELOG+=$'\n' fi + CHANGELOG+=$'\n'"## ${CC_LABELS[$key]}"$'\n' + CHANGELOG+="${block}"$'\n' +done - case "$type" in - feat*) FEATURES+="${line}"$'\n' ;; - fix*) FIXES+="${line}"$'\n' ;; - chore*) CHORES+="${line}"$'\n' ;; - *) COMMITS_SECTION+="${line}"$'\n' ;; - esac +# Commits section (plain / unrecognised type) +if [[ ${#OTHER_ENTRIES[@]} -gt 0 ]]; then + if [[ -n "$CHANGELOG" ]]; then + CHANGELOG+=$'\n' + fi + CHANGELOG+=$'\n'"## Commits"$'\n' + for e in "${OTHER_ENTRIES[@]}"; do + CHANGELOG+="${e}"$'\n' + done +fi -done <<< "$COMMITS" +# Trim leading/trailing whitespace (matches .trim() at end of TS function) +CHANGELOG="$(echo "$CHANGELOG" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" +# Write to GITHUB_OUTPUT using the multiline format required by GHA +# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs#setting-a-multiline-string +DELIMITER="$(openssl rand -hex 16)" { - echo "body<> "${GITHUB_OUTPUT}" - echo "" - echo "EOF" -} >> "$GITHUB_OUTPUT" +log "Changelog written to GITHUB_OUTPUT as 'changelog'" From 1060df5baf1fb5cd390446952599998fa5ae6645 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:35:16 -0600 Subject: [PATCH 17/60] chore(test): test --- .github/generate-release-notes.sh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh index 3bb20e0e1ec..9110248f910 100644 --- a/.github/generate-release-notes.sh +++ b/.github/generate-release-notes.sh @@ -15,7 +15,6 @@ set -euo pipefail # PREVIOUS_TAG - Optional. Tag to compare from. Auto-detected via semver if omitted. # # Required GHA environment variables (set automatically by GitHub Actions): -# GITHUB_TOKEN - Auth token for API calls (secrets.GITHUB_TOKEN) # GITHUB_REPOSITORY - "owner/repo" (github.repository) # GITHUB_SHA - Current commit SHA (github.sha) # GITHUB_REF - Current ref, e.g. refs/tags/v1.2 (github.ref) @@ -49,12 +48,6 @@ PREVIOUS_TAG="${1:-}" # GITHUB_SHA is the SHA of the commit that triggered the workflow CURRENT_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" -# GITHUB_TOKEN is injected by Actions; fail fast if missing -if [[ -z "${GITHUB_TOKEN:-}" ]]; then - log "ERROR: GITHUB_TOKEN is not set. Add 'env: GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}' to your workflow step." - exit 1 -fi - # GITHUB_REPOSITORY is "owner/repo" if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then log "ERROR: GITHUB_REPOSITORY is not set — are you running outside of GitHub Actions?" @@ -73,7 +66,6 @@ log "Current SHA: ${CURRENT_SHA}" gh_api() { local endpoint="$1" curl -fsSL \ - -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com${endpoint}" } From 243cb864c11f2694551cfe321462cf6f50978e2b Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:41:58 -0600 Subject: [PATCH 18/60] Update --- .github/workflows/prerelease.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index f11a0c77fa5..49f14c99ff3 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -70,8 +70,8 @@ jobs: - name: Generate release notes id: notes env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: bash .github/generate-release-notes.sh + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: python .github/generate_changelog.py - name: Create pre-release uses: softprops/action-gh-release@v3 @@ -79,7 +79,7 @@ jobs: tag_name: pre-release name: Pre-release Build prerelease: true - body: ${{ steps.notes.outputs.body }} + body: ${{ steps.notes.outputs.changelog }} files: | app/build/outputs/apk/prerelease/release/*.apk app/build/libs/app-sources.jar From dac16953da93ddedc0cbbbf31bb6b40051c2e437 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:42:36 -0600 Subject: [PATCH 19/60] feat(ci): python --- .github/generate-release-notes.sh | 367 ------------------------------ .github/generate_changelog.py | 301 ++++++++++++++++++++++++ 2 files changed, 301 insertions(+), 367 deletions(-) delete mode 100644 .github/generate-release-notes.sh create mode 100644 .github/generate_changelog.py diff --git a/.github/generate-release-notes.sh b/.github/generate-release-notes.sh deleted file mode 100644 index 9110248f910..00000000000 --- a/.github/generate-release-notes.sh +++ /dev/null @@ -1,367 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ============================================================================= -# generate_changelog.sh -# -# Replicates the changelog format of the automatic-releases GitHub Action. -# Designed to run inside a GitHub Actions workflow — all required values are -# sourced from the standard GHA environment variables automatically. -# -# Usage: -# generate_changelog.sh [PREVIOUS_TAG] -# -# Args: -# PREVIOUS_TAG - Optional. Tag to compare from. Auto-detected via semver if omitted. -# -# Required GHA environment variables (set automatically by GitHub Actions): -# GITHUB_REPOSITORY - "owner/repo" (github.repository) -# GITHUB_SHA - Current commit SHA (github.sha) -# GITHUB_REF - Current ref, e.g. refs/tags/v1.2 (github.ref) -# ============================================================================= - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -short_sha() { - echo "${1:0:7}" -} - -# Print to stderr so it doesn't pollute changelog output -log() { echo "[generate_changelog] $*" >&2; } - -require() { - command -v "$1" &>/dev/null || { log "ERROR: '$1' is required but not found"; exit 1; } -} - -require git -require curl -require jq - -# --------------------------------------------------------------------------- -# Config — sourced entirely from GHA environment -# --------------------------------------------------------------------------- - -PREVIOUS_TAG="${1:-}" - -# GITHUB_SHA is the SHA of the commit that triggered the workflow -CURRENT_SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" - -# GITHUB_REPOSITORY is "owner/repo" -if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then - log "ERROR: GITHUB_REPOSITORY is not set — are you running outside of GitHub Actions?" - exit 1 -fi -OWNER="$(echo "$GITHUB_REPOSITORY" | cut -d/ -f1)" -REPO="$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" - -log "Repo: ${OWNER}/${REPO}" -log "Current SHA: ${CURRENT_SHA}" - -# --------------------------------------------------------------------------- -# GitHub API helper -# --------------------------------------------------------------------------- - -gh_api() { - local endpoint="$1" - curl -fsSL \ - -H "Accept: application/vnd.github.v3+json" \ - "https://api.github.com${endpoint}" -} - -# --------------------------------------------------------------------------- -# Find previous release tag (semver, descending) -# --------------------------------------------------------------------------- - -find_previous_tag() { - local current_tag="$1" - log "Searching for previous semver tag before ${current_tag}" - - # Collect all semver tags from git - local tags - tags="$(git tag --list | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' || true)" - - if [[ -z "$tags" ]]; then - log "No semver tags found" - echo "" - return - fi - - # Sort descending (semver-aware via sort -V), find first tag strictly less than current - # Strip leading 'v' for comparison, then restore it - local prev="" - while IFS= read -r tag; do - [[ "$tag" == "$current_tag" ]] && continue - # Check tag < current_tag using sort -V - local lower - lower="$(printf '%s\n%s\n' "$current_tag" "$tag" | sort -V | head -1)" - if [[ "$lower" == "$tag" ]]; then - prev="$tag" - break - fi - done < <(echo "$tags" | sort -rV) - - echo "$prev" -} - -# --------------------------------------------------------------------------- -# Get commits between previous tag and current SHA -# --------------------------------------------------------------------------- - -get_commits() { - local base="$1" - local head="$2" - - if [[ -z "$base" ]]; then - log "No previous tag — using full history to ${head}" - git log --format='%H %s' "${head}" - else - log "Getting commits between ${base} and ${head}" - git log --format='%H %s' "${base}..${head}" - fi -} - -# --------------------------------------------------------------------------- -# Get pull requests associated with a commit SHA (via GitHub API) -# --------------------------------------------------------------------------- - -get_prs_for_commit() { - local sha="$1" - gh_api "/repos/${OWNER}/${REPO}/commits/${sha}/pulls" \ - 2>/dev/null || echo "[]" -} - -# --------------------------------------------------------------------------- -# Conventional commit parser -# Parses "type(scope): subject" or plain subject -# --------------------------------------------------------------------------- - -# Known types -> section headers (matches ConventionalCommitTypes enum order) -declare -a CC_KEYS=(feat fix docs style refactor perf test build ci chore revert) -declare -A CC_LABELS=( - [feat]="Features" - [fix]="Bug Fixes" - [docs]="Documentation" - [style]="Styles" - [refactor]="Code Refactoring" - [perf]="Performance Improvements" - [test]="Tests" - [build]="Builds" - [ci]="Continuous Integration" - [chore]="Chores" - [revert]="Reverts" -) - -parse_commit_type() { - # Returns type if conventional, else empty - echo "$1" | sed -nE 's/^([a-z]+)(\([^)]*\))?!?:.*/\1/p' -} - -parse_commit_scope() { - echo "$1" | sed -nE 's/^[a-z]+\(([^)]*)\)!?:.*/\1/p' -} - -parse_commit_subject() { - echo "$1" | sed -nE 's/^[a-z]+(\([^)]*\))?!?:[[:space:]]*(.*)/\2/p' -} - -is_breaking() { - # Check for '!' in type(scope)!: or BREAKING CHANGE in body/footer - local header="$1" - local body="$2" - echo "$header" | grep -qE '^[a-z]+(\([^)]*\))?!:' && return 0 - echo "$body" | grep -qE '^BREAKING[[:space:]]+CHANGES?:[[:space:]]' && return 0 - return 1 -} - -# --------------------------------------------------------------------------- -# Format a single changelog entry -# -# Matches getFormattedChangelogEntry() in utils.ts: -# -# If type is set (conventional commit): -# - **scope**: subject [author](url) (with optional PR links) -# Else (plain commit): -# - sha: header (author) (with optional PR links) -# --------------------------------------------------------------------------- - -format_entry() { - local sha="$1" - local header="$2" - local author_name="$3" - local commit_url="$4" - local type="$5" - local scope="$6" - local subject="$7" - local pr_json="$8" # JSON array of {number, url} - - local short - short="$(short_sha "$sha")" - - # Build PR string: [#1](url),[#2](url) - local pr_string="" - if [[ "$pr_json" != "[]" && -n "$pr_json" ]]; then - pr_string="$(echo "$pr_json" | jq -r '[.[] | "[#\(.number)](\(.html_url))"] | join(",")')" - [[ -n "$pr_string" ]] && pr_string=" ${pr_string}" - fi - - if [[ -n "$type" ]]; then - # Conventional commit format - local scope_str="" - [[ -n "$scope" ]] && scope_str="**${scope}**: " - echo "- ${scope_str}${subject}${pr_string} ([${author_name}](${commit_url}))" - else - # Plain commit format - echo "- ${short}: ${header} (${author_name})${pr_string}" - fi -} - -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- - -# Determine current tag -# GITHUB_REF is "refs/tags/v1.2.3" on tag push events — extract the tag name directly -CURRENT_TAG="" -if [[ "${GITHUB_REF:-}" =~ ^refs/tags/(.+)$ ]]; then - CURRENT_TAG="${BASH_REMATCH[1]}" - log "Detected tag from GITHUB_REF: ${CURRENT_TAG}" -else - # Fallback: check if HEAD is on an exact tag (e.g. workflow_dispatch on a tag) - CURRENT_TAG="$(git describe --exact-match "${CURRENT_SHA}" 2>/dev/null || true)" -fi - -if [[ -z "$PREVIOUS_TAG" && -n "$CURRENT_TAG" ]]; then - PREVIOUS_TAG="$(find_previous_tag "$CURRENT_TAG")" -fi - -log "Previous tag: ${PREVIOUS_TAG:-}" - -# Collect raw commits -mapfile -t RAW_COMMITS < <(get_commits "$PREVIOUS_TAG" "$CURRENT_SHA") - -if [[ ${#RAW_COMMITS[@]} -eq 0 ]]; then - log "No commits found" - echo "" - exit 0 -fi - -# --------------------------------------------------------------------------- -# Temporary storage: parallel arrays indexed by commit position -# We separate commits into buckets: breaking | by type | other -# --------------------------------------------------------------------------- - -declare -a BREAKING_ENTRIES=() -declare -A TYPE_ENTRIES=() # key = cc type, value = newline-separated entries -declare -a OTHER_ENTRIES=() - -for raw in "${RAW_COMMITS[@]}"; do - sha="${raw%% *}" - message="${raw#* }" - - # Skip merge commits (matches mergePattern: ^Merge pull request #(.*) from (.*)$) - if echo "$message" | grep -qE '^Merge pull request #[0-9]+ from '; then - log "Skipping merge commit: ${sha}" - continue - fi - - log "Processing commit ${sha}: ${message}" - - # Get commit detail from git for author info - author_name="$(git log -1 --format='%an' "$sha" 2>/dev/null || echo "unknown")" - commit_url="https://github.com/${OWNER}/${REPO}/commit/${sha}" - - # Get commit body for breaking change detection - commit_body="$(git log -1 --format='%b' "$sha" 2>/dev/null || true)" - - # Parse conventional commit - type="$(parse_commit_type "$message")" - scope="$(parse_commit_scope "$message")" - subject="$(parse_commit_subject "$message")" - - # Validate type is a known CC type (else treat as plain) - known_type="" - for k in "${CC_KEYS[@]}"; do - [[ "$type" == "$k" ]] && known_type="$type" && break - done - - # Detect breaking change - breaking=false - if is_breaking "$message" "$commit_body"; then - breaking=true - fi - - # Fetch associated PRs - log "Fetching PRs for ${sha}" - pr_json="$(get_prs_for_commit "$sha")" - - entry="$(format_entry "$sha" "$message" "$author_name" "$commit_url" \ - "$known_type" "$scope" "$subject" "$pr_json")" - - if [[ "$breaking" == "true" ]]; then - BREAKING_ENTRIES+=("$entry") - fi - - if [[ -n "$known_type" ]]; then - existing="${TYPE_ENTRIES[$known_type]:-}" - if [[ -n "$existing" ]]; then - TYPE_ENTRIES[$known_type]="${existing}"$'\n'"${entry}" - else - TYPE_ENTRIES[$known_type]="${entry}" - fi - else - OTHER_ENTRIES+=("$entry") - fi -done - -# --------------------------------------------------------------------------- -# Assemble changelog (matches generateChangelogFromParsedCommits order) -# --------------------------------------------------------------------------- - -CHANGELOG="" - -# Breaking Changes section -if [[ ${#BREAKING_ENTRIES[@]} -gt 0 ]]; then - CHANGELOG+="## Breaking Changes"$'\n' - for e in "${BREAKING_ENTRIES[@]}"; do - CHANGELOG+="${e}"$'\n' - done -fi - -# Conventional commit type sections (in enum declaration order) -for key in "${CC_KEYS[@]}"; do - block="${TYPE_ENTRIES[$key]:-}" - [[ -z "$block" ]] && continue - - if [[ -n "$CHANGELOG" ]]; then - CHANGELOG+=$'\n' - fi - CHANGELOG+=$'\n'"## ${CC_LABELS[$key]}"$'\n' - CHANGELOG+="${block}"$'\n' -done - -# Commits section (plain / unrecognised type) -if [[ ${#OTHER_ENTRIES[@]} -gt 0 ]]; then - if [[ -n "$CHANGELOG" ]]; then - CHANGELOG+=$'\n' - fi - CHANGELOG+=$'\n'"## Commits"$'\n' - for e in "${OTHER_ENTRIES[@]}"; do - CHANGELOG+="${e}"$'\n' - done -fi - -# Trim leading/trailing whitespace (matches .trim() at end of TS function) -CHANGELOG="$(echo "$CHANGELOG" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - -# Write to GITHUB_OUTPUT using the multiline format required by GHA -# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs#setting-a-multiline-string -DELIMITER="$(openssl rand -hex 16)" -{ - echo "changelog<<${DELIMITER}" - echo "$CHANGELOG" - echo "${DELIMITER}" -} >> "${GITHUB_OUTPUT}" - -log "Changelog written to GITHUB_OUTPUT as 'changelog'" diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py new file mode 100644 index 00000000000..d405f307a58 --- /dev/null +++ b/.github/generate_changelog.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +""" +generate_changelog.py + +Replicates the changelog format of the automatic-releases GitHub Action. +Designed to run inside a GitHub Actions workflow — all required values are +sourced from the standard GHA environment variables automatically. + +Required GHA environment variables (set automatically by GitHub Actions): + GITHUB_TOKEN - Auth token for API calls (secrets.GITHUB_TOKEN) + GITHUB_REPOSITORY - "owner/repo" (github.repository) + GITHUB_SHA - Current commit SHA (github.sha) + GITHUB_REF - Current ref, e.g. refs/tags/v1.2 (github.ref) + GITHUB_OUTPUT - Path to the GHA output file (set by runner) + +Usage: + python generate_changelog.py [PREVIOUS_TAG] + +Args: + PREVIOUS_TAG - Optional. Tag to compare from. Auto-detected via semver if omitted. +""" + +import json +import os +import re +import secrets +import subprocess +import sys +import urllib.error +import urllib.request +from typing import Optional + +# --------------------------------------------------------------------------- +# Config — sourced entirely from GHA environment +# --------------------------------------------------------------------------- + +def require_env(name: str) -> str: + val = os.environ.get(name, "") + if not val: + die(f"{name} is not set — are you running outside of GitHub Actions?") + return val + +def log(msg: str) -> None: + print(f"[generate_changelog] {msg}", file=sys.stderr) + +def die(msg: str) -> None: + log(f"ERROR: {msg}") + sys.exit(1) + +PREVIOUS_TAG: str = sys.argv[1] if len(sys.argv) > 1 else "" +GITHUB_TOKEN: str = require_env("GITHUB_TOKEN") +GITHUB_REPOSITORY: str = require_env("GITHUB_REPOSITORY") +GITHUB_SHA: str = os.environ.get("GITHUB_SHA") or subprocess.check_output( + ["git", "rev-parse", "HEAD"], text=True +).strip() +GITHUB_REF: str = os.environ.get("GITHUB_REF", "") +GITHUB_OUTPUT: str = require_env("GITHUB_OUTPUT") + +OWNER, REPO = GITHUB_REPOSITORY.split("/", 1) + +log(f"Repo: {OWNER}/{REPO}") +log(f"Current SHA: {GITHUB_SHA}") + +# --------------------------------------------------------------------------- +# GitHub API +# --------------------------------------------------------------------------- + +def gh_api(endpoint: str) -> list | dict: + url = f"https://api.github.com{endpoint}" + req = urllib.request.Request( + url, + headers={ + "Authorization": f"token {GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3+json", + }, + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + log(f"API request failed for {url}: {e}") + return [] + +# --------------------------------------------------------------------------- +# Conventional commit types (matches ConventionalCommitTypes enum order) +# --------------------------------------------------------------------------- + +CC_TYPES: dict[str, str] = { + "feat": "Features", + "fix": "Bug Fixes", + "docs": "Documentation", + "style": "Styles", + "refactor": "Code Refactoring", + "perf": "Performance Improvements", + "test": "Tests", + "build": "Builds", + "ci": "Continuous Integration", + "chore": "Chores", + "revert": "Reverts", +} + +CC_HEADER_RE = re.compile(r"^([a-z]+)(\([^)]*\))?(!)?:(.*)$") +BREAKING_BODY_RE = re.compile(r"^BREAKING\s+CHANGES?:\s+", re.MULTILINE) +MERGE_RE = re.compile(r"^Merge pull request #\d+ from ") + +# --------------------------------------------------------------------------- +# Git helpers +# --------------------------------------------------------------------------- + +def git(*args: str) -> str: + return subprocess.check_output(["git", *args], text=True).strip() + +def get_current_tag() -> str: + """Extract tag from GITHUB_REF, or fall back to git describe.""" + m = re.match(r"^refs/tags/(.+)$", GITHUB_REF) + if m: + tag = m.group(1) + log(f"Detected tag from GITHUB_REF: {tag}") + return tag + try: + return git("describe", "--exact-match", GITHUB_SHA) + except subprocess.CalledProcessError: + return "" + +def find_previous_tag(current_tag: str) -> str: + """Return the nearest semver tag strictly less than current_tag.""" + log(f"Searching for previous semver tag before {current_tag}") + raw_tags = git("tag", "--list").splitlines() + semver_re = re.compile(r"^v?\d+\.\d+\.\d+") + tags = [t for t in raw_tags if semver_re.match(t)] + if not tags: + log("No semver tags found") + return "" + + def parse_semver(tag: str) -> tuple[int, ...]: + digits = re.sub(r"^v", "", tag) + parts = re.split(r"[.\-]", digits) + result = [] + for p in parts[:3]: + try: + result.append(int(p)) + except ValueError: + result.append(0) + return tuple(result) + + current_ver = parse_semver(current_tag) + candidates = sorted( + [t for t in tags if t != current_tag and parse_semver(t) < current_ver], + key=parse_semver, + reverse=True, + ) + return candidates[0] if candidates else "" + +def get_commits(base: str, head: str) -> list[tuple[str, str]]: + """Return list of (sha, subject) tuples.""" + if base: + log(f"Getting commits between {base} and {head}") + raw = git("log", "--format=%H %s", f"{base}..{head}") + else: + log(f"No previous tag — using full history to {head}") + raw = git("log", "--format=%H %s", head) + result = [] + for line in raw.splitlines(): + if not line.strip(): + continue + sha, _, subject = line.partition(" ") + result.append((sha, subject)) + return result + +# --------------------------------------------------------------------------- +# Conventional commit parsing +# --------------------------------------------------------------------------- + +def parse_commit(subject: str) -> tuple[str, str, str]: + """Return (type, scope, subject). type is empty if not conventional.""" + m = CC_HEADER_RE.match(subject) + if not m: + return "", "", "" + cc_type = m.group(1) + scope = m.group(2)[1:-1] if m.group(2) else "" # strip parens + subj = m.group(4).strip() + return cc_type, scope, subj + +def is_breaking(header: str, body: str) -> bool: + if re.match(r"^[a-z]+(\([^)]*\))?!:", header): + return True + if BREAKING_BODY_RE.search(body or ""): + return True + return False + +# --------------------------------------------------------------------------- +# Entry formatting (matches getFormattedChangelogEntry) +# --------------------------------------------------------------------------- + +def format_entry( + sha: str, + header: str, + author_name: str, + commit_url: str, + cc_type: str, + scope: str, + subject: str, + prs: list[dict], +) -> str: + short = sha[:7] + + pr_parts = [f"[#{pr['number']}]({pr['html_url']})" for pr in prs] + pr_string = (" " + ",".join(pr_parts)) if pr_parts else "" + + if cc_type: + scope_str = f"**{scope}**: " if scope else "" + return f"- {scope_str}{subject}{pr_string} ([{author_name}]({commit_url}))" + else: + return f"- {short}: {header} ({author_name}){pr_string}" + +# --------------------------------------------------------------------------- +# Changelog assembly (matches generateChangelogFromParsedCommits) +# --------------------------------------------------------------------------- + +def generate_changelog(commits: list[tuple[str, str]]) -> str: + breaking_entries: list[str] = [] + type_entries: dict[str, list[str]] = {k: [] for k in CC_TYPES} + other_entries: list[str] = [] + + for sha, subject in commits: + if MERGE_RE.match(subject): + log(f"Skipping merge commit: {sha}") + continue + + log(f"Processing commit {sha}: {subject}") + + author_name = git("log", "-1", "--format=%an", sha) or "unknown" + commit_url = f"https://github.com/{OWNER}/{REPO}/commit/{sha}" + commit_body = git("log", "-1", "--format=%b", sha) + + cc_type, scope, parsed_subject = parse_commit(subject) + known_type = cc_type if cc_type in CC_TYPES else "" + + breaking = is_breaking(subject, commit_body) + + log(f"Fetching PRs for {sha}") + prs = gh_api(f"/repos/{OWNER}/{REPO}/commits/{sha}/pulls") + if not isinstance(prs, list): + prs = [] + + entry = format_entry(sha, subject, author_name, commit_url, + known_type, scope, parsed_subject, prs) + + if breaking: + breaking_entries.append(entry) + + if known_type: + type_entries[known_type].append(entry) + else: + other_entries.append(entry) + + sections: list[str] = [] + + if breaking_entries: + sections.append("## Breaking Changes\n" + "\n".join(breaking_entries)) + + for key, label in CC_TYPES.items(): + if type_entries[key]: + sections.append(f"## {label}\n" + "\n".join(type_entries[key])) + + if other_entries: + sections.append("## Commits\n" + "\n".join(other_entries)) + + return "\n\n".join(sections).strip() + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + global PREVIOUS_TAG + + current_tag = get_current_tag() + + if not PREVIOUS_TAG and current_tag: + PREVIOUS_TAG = find_previous_tag(current_tag) + + log(f"Previous tag: {PREVIOUS_TAG or ''}") + + commits = get_commits(PREVIOUS_TAG, GITHUB_SHA) + if not commits: + log("No commits found") + changelog = "" + else: + changelog = generate_changelog(commits) + + # Write to GITHUB_OUTPUT using the multiline heredoc format required by GHA + # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs#setting-a-multiline-string + delimiter = secrets.token_hex(16) + with open(GITHUB_OUTPUT, "a") as f: + f.write(f"changelog<<{delimiter}\n{changelog}\n{delimiter}\n") + + log("Changelog written to GITHUB_OUTPUT as 'changelog'") + +if __name__ == "__main__": + main() From 385dc7ecab3fec65b72b7c1ce31f8f544810f1ce Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:47:46 -0600 Subject: [PATCH 20/60] fix(ci): delete --- .github/workflows/prerelease.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 49f14c99ff3..de6bc846032 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -67,6 +67,11 @@ jobs: SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} + - name: Delete existing pre-release + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: gh release delete pre-release --yes --cleanup-tag || true + - name: Generate release notes id: notes env: From d755f0145859a5161375cbf88c5cb4a293e6ae9c Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:50:28 -0600 Subject: [PATCH 21/60] fix(ci): delete --- .github/workflows/prerelease.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index de6bc846032..678cbe61bee 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,8 +68,6 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: gh release delete pre-release --yes --cleanup-tag || true - name: Generate release notes From 7b6376066e689baf1e1d0df09e25d1d14a0f8232 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 15:52:55 -0600 Subject: [PATCH 22/60] fix(ci): delete --- .github/workflows/prerelease.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 678cbe61bee..a8a9fd739dc 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -69,6 +69,8 @@ jobs: - name: Delete existing pre-release run: gh release delete pre-release --yes --cleanup-tag || true + env: + GH_TOKEN: ${{ github.token }} - name: Generate release notes id: notes From ea3d465ff39538b05abf1d06e827ee63148da16a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:02:08 -0600 Subject: [PATCH 23/60] Use classes --- .github/generate_changelog.py | 474 +++++++++++++++------------------- 1 file changed, 205 insertions(+), 269 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index d405f307a58..5dac0d39903 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -1,24 +1,4 @@ #!/usr/bin/env python3 -""" -generate_changelog.py - -Replicates the changelog format of the automatic-releases GitHub Action. -Designed to run inside a GitHub Actions workflow — all required values are -sourced from the standard GHA environment variables automatically. - -Required GHA environment variables (set automatically by GitHub Actions): - GITHUB_TOKEN - Auth token for API calls (secrets.GITHUB_TOKEN) - GITHUB_REPOSITORY - "owner/repo" (github.repository) - GITHUB_SHA - Current commit SHA (github.sha) - GITHUB_REF - Current ref, e.g. refs/tags/v1.2 (github.ref) - GITHUB_OUTPUT - Path to the GHA output file (set by runner) - -Usage: - python generate_changelog.py [PREVIOUS_TAG] - -Args: - PREVIOUS_TAG - Optional. Tag to compare from. Auto-detected via semver if omitted. -""" import json import os @@ -28,274 +8,230 @@ import sys import urllib.error import urllib.request -from typing import Optional - -# --------------------------------------------------------------------------- -# Config — sourced entirely from GHA environment -# --------------------------------------------------------------------------- +from dataclasses import dataclass, field -def require_env(name: str) -> str: - val = os.environ.get(name, "") - if not val: - die(f"{name} is not set — are you running outside of GitHub Actions?") - return val -def log(msg: str) -> None: - print(f"[generate_changelog] {msg}", file=sys.stderr) - -def die(msg: str) -> None: - log(f"ERROR: {msg}") - sys.exit(1) - -PREVIOUS_TAG: str = sys.argv[1] if len(sys.argv) > 1 else "" -GITHUB_TOKEN: str = require_env("GITHUB_TOKEN") -GITHUB_REPOSITORY: str = require_env("GITHUB_REPOSITORY") -GITHUB_SHA: str = os.environ.get("GITHUB_SHA") or subprocess.check_output( - ["git", "rev-parse", "HEAD"], text=True -).strip() -GITHUB_REF: str = os.environ.get("GITHUB_REF", "") -GITHUB_OUTPUT: str = require_env("GITHUB_OUTPUT") - -OWNER, REPO = GITHUB_REPOSITORY.split("/", 1) - -log(f"Repo: {OWNER}/{REPO}") -log(f"Current SHA: {GITHUB_SHA}") - -# --------------------------------------------------------------------------- -# GitHub API -# --------------------------------------------------------------------------- - -def gh_api(endpoint: str) -> list | dict: - url = f"https://api.github.com{endpoint}" - req = urllib.request.Request( - url, - headers={ - "Authorization": f"token {GITHUB_TOKEN}", - "Accept": "application/vnd.github.v3+json", - }, - ) - try: - with urllib.request.urlopen(req) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as e: - log(f"API request failed for {url}: {e}") - return [] - -# --------------------------------------------------------------------------- -# Conventional commit types (matches ConventionalCommitTypes enum order) -# --------------------------------------------------------------------------- +MERGE_RE = re.compile(r'^Merge pull request #\d+ from ') +CC_HEADER_RE = re.compile(r'^([a-z]+)(\([^)]*\))?(!)?:(.*)$') +BREAKING_BODY_RE = re.compile(r'^BREAKING\s+CHANGES?:\s+', re.MULTILINE) CC_TYPES: dict[str, str] = { - "feat": "Features", - "fix": "Bug Fixes", - "docs": "Documentation", - "style": "Styles", - "refactor": "Code Refactoring", - "perf": "Performance Improvements", - "test": "Tests", - "build": "Builds", - "ci": "Continuous Integration", - "chore": "Chores", - "revert": "Reverts", + 'feat': 'Features', + 'fix': 'Bug Fixes', + 'docs': 'Documentation', + 'style': 'Styles', + 'refactor': 'Code Refactoring', + 'perf': 'Performance Improvements', + 'test': 'Tests', + 'build': 'Builds', + 'ci': 'Continuous Integration', + 'chore': 'Chores', + 'revert': 'Reverts', } -CC_HEADER_RE = re.compile(r"^([a-z]+)(\([^)]*\))?(!)?:(.*)$") -BREAKING_BODY_RE = re.compile(r"^BREAKING\s+CHANGES?:\s+", re.MULTILINE) -MERGE_RE = re.compile(r"^Merge pull request #\d+ from ") - -# --------------------------------------------------------------------------- -# Git helpers -# --------------------------------------------------------------------------- - -def git(*args: str) -> str: - return subprocess.check_output(["git", *args], text=True).strip() - -def get_current_tag() -> str: - """Extract tag from GITHUB_REF, or fall back to git describe.""" - m = re.match(r"^refs/tags/(.+)$", GITHUB_REF) - if m: - tag = m.group(1) - log(f"Detected tag from GITHUB_REF: {tag}") - return tag - try: - return git("describe", "--exact-match", GITHUB_SHA) - except subprocess.CalledProcessError: - return "" - -def find_previous_tag(current_tag: str) -> str: - """Return the nearest semver tag strictly less than current_tag.""" - log(f"Searching for previous semver tag before {current_tag}") - raw_tags = git("tag", "--list").splitlines() - semver_re = re.compile(r"^v?\d+\.\d+\.\d+") - tags = [t for t in raw_tags if semver_re.match(t)] - if not tags: - log("No semver tags found") - return "" - - def parse_semver(tag: str) -> tuple[int, ...]: - digits = re.sub(r"^v", "", tag) - parts = re.split(r"[.\-]", digits) - result = [] - for p in parts[:3]: - try: - result.append(int(p)) - except ValueError: - result.append(0) - return tuple(result) - - current_ver = parse_semver(current_tag) - candidates = sorted( - [t for t in tags if t != current_tag and parse_semver(t) < current_ver], - key=parse_semver, - reverse=True, - ) - return candidates[0] if candidates else "" - -def get_commits(base: str, head: str) -> list[tuple[str, str]]: - """Return list of (sha, subject) tuples.""" - if base: - log(f"Getting commits between {base} and {head}") - raw = git("log", "--format=%H %s", f"{base}..{head}") - else: - log(f"No previous tag — using full history to {head}") - raw = git("log", "--format=%H %s", head) - result = [] - for line in raw.splitlines(): - if not line.strip(): - continue - sha, _, subject = line.partition(" ") - result.append((sha, subject)) - return result - -# --------------------------------------------------------------------------- -# Conventional commit parsing -# --------------------------------------------------------------------------- - -def parse_commit(subject: str) -> tuple[str, str, str]: - """Return (type, scope, subject). type is empty if not conventional.""" - m = CC_HEADER_RE.match(subject) - if not m: - return "", "", "" - cc_type = m.group(1) - scope = m.group(2)[1:-1] if m.group(2) else "" # strip parens - subj = m.group(4).strip() - return cc_type, scope, subj - -def is_breaking(header: str, body: str) -> bool: - if re.match(r"^[a-z]+(\([^)]*\))?!:", header): - return True - if BREAKING_BODY_RE.search(body or ""): - return True - return False - -# --------------------------------------------------------------------------- -# Entry formatting (matches getFormattedChangelogEntry) -# --------------------------------------------------------------------------- - -def format_entry( - sha: str, - header: str, - author_name: str, - commit_url: str, - cc_type: str, - scope: str, - subject: str, - prs: list[dict], -) -> str: - short = sha[:7] - - pr_parts = [f"[#{pr['number']}]({pr['html_url']})" for pr in prs] - pr_string = (" " + ",".join(pr_parts)) if pr_parts else "" - - if cc_type: - scope_str = f"**{scope}**: " if scope else "" - return f"- {scope_str}{subject}{pr_string} ([{author_name}]({commit_url}))" - else: - return f"- {short}: {header} ({author_name}){pr_string}" - -# --------------------------------------------------------------------------- -# Changelog assembly (matches generateChangelogFromParsedCommits) -# --------------------------------------------------------------------------- - -def generate_changelog(commits: list[tuple[str, str]]) -> str: - breaking_entries: list[str] = [] - type_entries: dict[str, list[str]] = {k: [] for k in CC_TYPES} - other_entries: list[str] = [] - - for sha, subject in commits: - if MERGE_RE.match(subject): - log(f"Skipping merge commit: {sha}") - continue - - log(f"Processing commit {sha}: {subject}") - - author_name = git("log", "-1", "--format=%an", sha) or "unknown" - commit_url = f"https://github.com/{OWNER}/{REPO}/commit/{sha}" - commit_body = git("log", "-1", "--format=%b", sha) - - cc_type, scope, parsed_subject = parse_commit(subject) - known_type = cc_type if cc_type in CC_TYPES else "" - breaking = is_breaking(subject, commit_body) - - log(f"Fetching PRs for {sha}") - prs = gh_api(f"/repos/{OWNER}/{REPO}/commits/{sha}/pulls") - if not isinstance(prs, list): - prs = [] - - entry = format_entry(sha, subject, author_name, commit_url, - known_type, scope, parsed_subject, prs) - - if breaking: - breaking_entries.append(entry) - - if known_type: - type_entries[known_type].append(entry) +@dataclass +class Commit: + sha: str + subject: str + author: str + url: str + body: str + cc_type: str + scope: str + parsed_subject: str + breaking: bool + prs: list[dict] = field(default_factory=list) + + @property + def known_type(self) -> str: + return self.cc_type if self.cc_type in CC_TYPES else '' + + @property + def short_sha(self) -> str: + return self.sha[:7] + + @property + def entry(self) -> str: + pr_string = '' + if self.prs: + parts = [f"[#{pr['number']}]({pr['html_url']})" for pr in self.prs] + pr_string = ' ' + ','.join(parts) + + if self.known_type: + scope_str = f'**{self.scope}**: ' if self.scope else '' + return f'- {scope_str}{self.parsed_subject}{pr_string} ([{self.author}]({self.url}))' + else: + return f'- {self.short_sha}: {self.subject} ({self.author}){pr_string}' + + +class ChangelogGenerator: + def __init__(self, token: str, repository: str, sha: str, ref: str, output_path: str): + self.token = token + self.owner, self.repo = repository.split('/', 1) + self.sha = sha + self.ref = ref + self.output_path = output_path + + def log(self, msg: str) -> None: + print(f'[generate_changelog] {msg}', file=sys.stderr) + + def git(self, *args: str) -> str: + return subprocess.check_output(['git', *args], text=True).strip() + + def gh_api(self, endpoint: str) -> list | dict: + req = urllib.request.Request( + f'https://api.github.com{endpoint}', + headers={ + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github.v3+json', + }, + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + except urllib.error.HTTPError as e: + self.log(f'API request failed for {endpoint}: {e}') + return [] + + def current_tag(self) -> str: + m = re.match(r'^refs/tags/(.+)$', self.ref) + if m: + tag = m.group(1) + self.log(f'Detected tag from GITHUB_REF: {tag}') + return tag + try: + return self.git('describe', '--exact-match', self.sha) + except subprocess.CalledProcessError: + return '' + + def find_previous_tag(self, current_tag: str) -> str: + self.log(f'Searching for previous semver tag before {current_tag}') + + def parse_semver(tag: str) -> tuple[int, ...]: + parts = re.split(r'[.\-]', re.sub(r'^v', '', tag)) + result = [] + for p in parts[:3]: + try: + result.append(int(p)) + except ValueError: + result.append(0) + return tuple(result) + + semver_re = re.compile(r'^v?\d+\.\d+\.\d+') + tags = [t for t in self.git('tag', '--list').splitlines() if semver_re.match(t)] + if not tags: + self.log('No semver tags found') + return '' + + current_ver = parse_semver(current_tag) + candidates = sorted( + [t for t in tags if t != current_tag and parse_semver(t) < current_ver], + key=parse_semver, + reverse=True, + ) + return candidates[0] if candidates else '' + + def get_raw_commits(self, base: str) -> list[tuple[str, str]]: + if base: + self.log(f'Getting commits between {base} and {self.sha}') + raw = self.git('log', '--format=%H %s', f'{base}..{self.sha}') else: - other_entries.append(entry) + self.log(f'No previous tag — using full history to {self.sha}') + raw = self.git('log', '--format=%H %s', self.sha) + return [(line.split(' ', 1)[0], line.split(' ', 1)[1]) for line in raw.splitlines() if line.strip()] - sections: list[str] = [] + def parse_commit(self, sha: str, subject: str) -> Commit | None: + if MERGE_RE.match(subject): + self.log(f'Skipping merge commit: {sha}') + return None + + self.log(f'Processing commit {sha}: {subject}') + + author = self.git('log', '-1', '--format=%an', sha) or 'unknown' + url = f'https://github.com/{self.owner}/{self.repo}/commit/{sha}' + body = self.git('log', '-1', '--format=%b', sha) + + cc_type, scope, parsed_subject = '', '', '' + m = CC_HEADER_RE.match(subject) + if m: + cc_type = m.group(1) + scope = m.group(2)[1:-1] if m.group(2) else '' + parsed_subject = m.group(4).strip() + + breaking = bool( + re.match(r'^[a-z]+(\([^)]*\))?!:', subject) + or BREAKING_BODY_RE.search(body or '') + ) + + self.log(f'Fetching PRs for {sha}') + prs = self.gh_api(f'/repos/{self.owner}/{self.repo}/commits/{sha}/pulls') + + return Commit( + sha=sha, + subject=subject, + author=author, + url=url, + body=body, + cc_type=cc_type, + scope=scope, + parsed_subject=parsed_subject, + breaking=breaking, + prs=prs if isinstance(prs, list) else [], + ) + + def build_changelog(self, commits: list) -> str: + breaking = [c.entry for c in commits if c.breaking] + by_type = {k: [c.entry for c in commits if c.known_type == k] for k in CC_TYPES} + other = [c.entry for c in commits if not c.known_type] + + sections: list[str] = [] - if breaking_entries: - sections.append("## Breaking Changes\n" + "\n".join(breaking_entries)) + if breaking: + sections.append('## Breaking Changes\n' + '\n'.join(breaking)) - for key, label in CC_TYPES.items(): - if type_entries[key]: - sections.append(f"## {label}\n" + "\n".join(type_entries[key])) + for key, label in CC_TYPES.items(): + if by_type[key]: + sections.append(f'## {label}\n' + '\n'.join(by_type[key])) - if other_entries: - sections.append("## Commits\n" + "\n".join(other_entries)) + if other: + sections.append('## Commits\n' + '\n'.join(other)) - return "\n\n".join(sections).strip() + return '\n\n'.join(sections).strip() -# --------------------------------------------------------------------------- -# Main -# --------------------------------------------------------------------------- + def write_output(self, changelog: str) -> None: + delimiter = secrets.token_hex(16) + with open(self.output_path, 'a') as f: + f.write(f'changelog<<{delimiter}\n{changelog}\n{delimiter}\n') + self.log("Changelog written to GITHUB_OUTPUT as 'changelog'") -def main() -> None: - global PREVIOUS_TAG + def run(self, previous_tag: str = '') -> None: + tag = self.current_tag() + if not previous_tag and tag: + previous_tag = self.find_previous_tag(tag) - current_tag = get_current_tag() + self.log(f"Previous tag: {previous_tag or ''}") - if not PREVIOUS_TAG and current_tag: - PREVIOUS_TAG = find_previous_tag(current_tag) + raw_commits = self.get_raw_commits(previous_tag) + commits = [c for sha, subject in raw_commits if (c := self.parse_commit(sha, subject))] - log(f"Previous tag: {PREVIOUS_TAG or ''}") + changelog = self.build_changelog(commits) if commits else '' + self.write_output(changelog) - commits = get_commits(PREVIOUS_TAG, GITHUB_SHA) - if not commits: - log("No commits found") - changelog = "" - else: - changelog = generate_changelog(commits) - # Write to GITHUB_OUTPUT using the multiline heredoc format required by GHA - # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs#setting-a-multiline-string - delimiter = secrets.token_hex(16) - with open(GITHUB_OUTPUT, "a") as f: - f.write(f"changelog<<{delimiter}\n{changelog}\n{delimiter}\n") +def require_env(name: str) -> str: + val = os.environ.get(name, '') + if not val: + print(f'[generate_changelog] ERROR: {name} is not set', file=sys.stderr) + sys.exit(1) + return val - log("Changelog written to GITHUB_OUTPUT as 'changelog'") -if __name__ == "__main__": - main() +if __name__ == '__main__': + ChangelogGenerator( + token=require_env('GITHUB_TOKEN'), + repository=require_env('GITHUB_REPOSITORY'), + sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), + ref=os.environ.get('GITHUB_REF', ''), + output_path=require_env('GITHUB_OUTPUT'), + ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else '') From b433e3345f85f073fc7600576db2607ac74b1710 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:08:02 -0600 Subject: [PATCH 24/60] Test --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 5dac0d39903..765689327c6 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -212,7 +212,7 @@ def run(self, previous_tag: str = '') -> None: self.log(f"Previous tag: {previous_tag or ''}") - raw_commits = self.get_raw_commits(previous_tag) + raw_commits = self.get_raw_commits(previous_tag or 'v4.0.1') commits = [c for sha, subject in raw_commits if (c := self.parse_commit(sha, subject))] changelog = self.build_changelog(commits) if commits else '' From 3ac17552b4507ac36257750d2ea5517ac2a84c5c Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:11:37 -0600 Subject: [PATCH 25/60] Test --- .github/generate_changelog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 765689327c6..56a5ec53784 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -212,7 +212,7 @@ def run(self, previous_tag: str = '') -> None: self.log(f"Previous tag: {previous_tag or ''}") - raw_commits = self.get_raw_commits(previous_tag or 'v4.0.1') + raw_commits = self.get_raw_commits(previous_tag) commits = [c for sha, subject in raw_commits if (c := self.parse_commit(sha, subject))] changelog = self.build_changelog(commits) if commits else '' @@ -234,4 +234,4 @@ def require_env(name: str) -> str: sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), - ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else '') + ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v4.0.1') From 2a45744663cf8e0f53522f8fec506eb3abe902b0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:15:09 -0600 Subject: [PATCH 26/60] Test tags --- .github/generate_changelog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 56a5ec53784..042baf4021b 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -95,6 +95,7 @@ def gh_api(self, endpoint: str) -> list | dict: return [] def current_tag(self) -> str: + return 'v4.0.1' m = re.match(r'^refs/tags/(.+)$', self.ref) if m: tag = m.group(1) @@ -234,4 +235,4 @@ def require_env(name: str) -> str: sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), - ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v4.0.1') + ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v3.5.0') From ee8f6d82b7a52284eede96079d4c65b99cb4c608 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:18:50 -0600 Subject: [PATCH 27/60] Try --- .github/generate_changelog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 042baf4021b..ffc5cecae1d 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -95,7 +95,6 @@ def gh_api(self, endpoint: str) -> list | dict: return [] def current_tag(self) -> str: - return 'v4.0.1' m = re.match(r'^refs/tags/(.+)$', self.ref) if m: tag = m.group(1) @@ -134,6 +133,10 @@ def parse_semver(tag: str) -> tuple[int, ...]: return candidates[0] if candidates else '' def get_raw_commits(self, base: str) -> list[tuple[str, str]]: + try: + self.git('fetch', '--tags', '--unshallow') + except subprocess.CalledProcessError: + self.git('fetch', '--tags') if base: self.log(f'Getting commits between {base} and {self.sha}') raw = self.git('log', '--format=%H %s', f'{base}..{self.sha}') @@ -235,4 +238,4 @@ def require_env(name: str) -> str: sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), - ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v3.5.0') + ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v4.0.1') From 4e11a1738e30f4abd3a5cef6258d0d3b2fe5fd47 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:52:27 -0600 Subject: [PATCH 28/60] Try --- .github/generate_changelog.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index ffc5cecae1d..df4f3fb62fe 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -133,16 +133,16 @@ def parse_semver(tag: str) -> tuple[int, ...]: return candidates[0] if candidates else '' def get_raw_commits(self, base: str) -> list[tuple[str, str]]: - try: - self.git('fetch', '--tags', '--unshallow') - except subprocess.CalledProcessError: - self.git('fetch', '--tags') if base: + try: + self.git('fetch', '--tags', '--unshallow') + except subprocess.CalledProcessError: + self.git('fetch', '--tags') self.log(f'Getting commits between {base} and {self.sha}') - raw = self.git('log', '--format=%H %s', f'{base}..{self.sha}') + raw = self.git('log', '--format=%H %s', '--max-count=100', f'{base}..{self.sha}') else: self.log(f'No previous tag — using full history to {self.sha}') - raw = self.git('log', '--format=%H %s', self.sha) + raw = self.git('log', '--format=%H %s', '--max-count=100', self.sha) return [(line.split(' ', 1)[0], line.split(' ', 1)[1]) for line in raw.splitlines() if line.strip()] def parse_commit(self, sha: str, subject: str) -> Commit | None: From 67f0febabbb260c01779506972c8c0c8de9b9518 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 16:55:35 -0600 Subject: [PATCH 29/60] 500 --- .github/generate_changelog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index df4f3fb62fe..dc244bb7186 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -139,10 +139,10 @@ def get_raw_commits(self, base: str) -> list[tuple[str, str]]: except subprocess.CalledProcessError: self.git('fetch', '--tags') self.log(f'Getting commits between {base} and {self.sha}') - raw = self.git('log', '--format=%H %s', '--max-count=100', f'{base}..{self.sha}') + raw = self.git('log', '--format=%H %s', '--max-count=500', f'{base}..{self.sha}') else: self.log(f'No previous tag — using full history to {self.sha}') - raw = self.git('log', '--format=%H %s', '--max-count=100', self.sha) + raw = self.git('log', '--format=%H %s', '--max-count=500', self.sha) return [(line.split(' ', 1)[0], line.split(' ', 1)[1]) for line in raw.splitlines() if line.strip()] def parse_commit(self, sha: str, subject: str) -> Commit | None: From e4c3c44c3b5b88acf905562fc1ccc387c50f5ba4 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:07:07 -0600 Subject: [PATCH 30/60] argparse --- .github/generate_changelog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index dc244bb7186..ae8c0faaf07 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import argparse import json import os import re @@ -232,10 +233,14 @@ def require_env(name: str) -> str: if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Generate a changelog and write it to GITHUB_OUTPUT.') + parser.add_argument('--previous-tag', default='', help='Tag to compare from (auto-detected if omitted)') + args = parser.parse_args() + ChangelogGenerator( token=require_env('GITHUB_TOKEN'), repository=require_env('GITHUB_REPOSITORY'), sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), - ).run(previous_tag=sys.argv[1] if len(sys.argv) > 1 else 'v4.0.1') + ).run(previous_tag=args.previous_tag) From ee36235b8a201ca3a918e214fa2bceadd1ab0522 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:08:57 -0600 Subject: [PATCH 31/60] Update --- .github/workflows/prerelease.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index a8a9fd739dc..5aa3eff57af 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -67,16 +67,15 @@ jobs: SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - - name: Delete existing pre-release - run: gh release delete pre-release --yes --cleanup-tag || true - env: - GH_TOKEN: ${{ github.token }} - - name: Generate release notes id: notes env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - run: python .github/generate_changelog.py + run: | + python .github/generate_changelog.py --previous-tag=pre-release + # Delete old release before making a new one + gh release delete pre-release --yes --cleanup-tag || true + - name: Create pre-release uses: softprops/action-gh-release@v3 From 3b5e3d48ecb8510fe140c4dde51fffa4359cbbac Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:14:48 -0600 Subject: [PATCH 32/60] Update --- .github/workflows/prerelease.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 5aa3eff57af..4dd661a5792 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -67,16 +67,17 @@ jobs: SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} + - name: Delete existing pre-release + run: gh release delete pre-release --yes || true + env: + GITHUB_TOKEN: ${{ github.token }} + - name: Generate release notes id: notes + run: python .github/generate_changelog.py --previous-tag=pre-release env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - run: | - python .github/generate_changelog.py --previous-tag=pre-release - # Delete old release before making a new one - gh release delete pre-release --yes --cleanup-tag || true - - name: Create pre-release uses: softprops/action-gh-release@v3 with: From 64deaa3b81392e6c5b737c2904ba054837465168 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:19:12 -0600 Subject: [PATCH 33/60] Try --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4dd661a5792..42f550539e6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,7 +68,7 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes || true + run: gh release delete pre-release --yes --cleanup-tag || true env: GITHUB_TOKEN: ${{ github.token }} From ee38c6ec897a491d968198ad52972b6561d04edf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:23:54 -0600 Subject: [PATCH 34/60] Try fix --- .github/generate_changelog.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index ae8c0faaf07..6a247d404ec 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -133,12 +133,16 @@ def parse_semver(tag: str) -> tuple[int, ...]: ) return candidates[0] if candidates else '' + def tag_exists(self, tag: str) -> bool: + try: + self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + return True + except subprocess.CalledProcessError: + self.log(f'Tag {tag} not found, falling back to full history') + return False + def get_raw_commits(self, base: str) -> list[tuple[str, str]]: - if base: - try: - self.git('fetch', '--tags', '--unshallow') - except subprocess.CalledProcessError: - self.git('fetch', '--tags') + if base and self.tag_exists(base): self.log(f'Getting commits between {base} and {self.sha}') raw = self.git('log', '--format=%H %s', '--max-count=500', f'{base}..{self.sha}') else: @@ -212,6 +216,8 @@ def write_output(self, changelog: str) -> None: def run(self, previous_tag: str = '') -> None: tag = self.current_tag() + if previous_tag and not self.tag_exists(previous_tag): + previous_tag = '' if not previous_tag and tag: previous_tag = self.find_previous_tag(tag) From bee82e8aa6f2b29858825440527c85cc28f38906 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:26:19 -0600 Subject: [PATCH 35/60] Don't delete tag --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 42f550539e6..4dd661a5792 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,7 +68,7 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes --cleanup-tag || true + run: gh release delete pre-release --yes || true env: GITHUB_TOKEN: ${{ github.token }} From 9d1486ca416e1eb6c5da3181b6d2f2cf024cd536 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:28:28 -0600 Subject: [PATCH 36/60] Try --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4dd661a5792..4ad2fba652e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -74,7 +74,7 @@ jobs: - name: Generate release notes id: notes - run: python .github/generate_changelog.py --previous-tag=pre-release + run: python .github/generate_changelog.py --previous-tag=v4.0.1 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From 066db61858b26881cdf838a01f4be401b796128e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:31:47 -0600 Subject: [PATCH 37/60] Update --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 6a247d404ec..bad2b24bcf8 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,7 +135,7 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') From e0af53a1c3b02a58f2ab6899a90369e707d2027a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:35:11 -0600 Subject: [PATCH 38/60] Fix --- .github/generate_changelog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index bad2b24bcf8..d415060921c 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,7 +135,8 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') + self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', '--shallow-exclude', tag, 'origin') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') From 32c5c03391994c2070c739583c990a70afd122d5 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:38:41 -0600 Subject: [PATCH 39/60] Fix --- .github/generate_changelog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index d415060921c..1589cadd3c0 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,8 +135,8 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - self.git('fetch', '--shallow-exclude', tag, 'origin') + self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', f'--shallow-exclude={tag}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') @@ -250,4 +250,3 @@ def require_env(name: str) -> str: sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), - ).run(previous_tag=args.previous_tag) From 74b16792dbec4693bb3cc660c8cf3b8ef187748d Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 17:39:36 -0600 Subject: [PATCH 40/60] Update --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4ad2fba652e..4dd661a5792 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -74,7 +74,7 @@ jobs: - name: Generate release notes id: notes - run: python .github/generate_changelog.py --previous-tag=v4.0.1 + run: python .github/generate_changelog.py --previous-tag=pre-release env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From 78f845079b76032c4ffc200dcd30b1ade460a296 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 6 May 2026 18:22:12 -0600 Subject: [PATCH 41/60] Fix --- .github/generate_changelog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 1589cadd3c0..55cd516421d 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -250,3 +250,4 @@ def require_env(name: str) -> str: sha=os.environ.get('GITHUB_SHA') or subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip(), ref=os.environ.get('GITHUB_REF', ''), output_path=require_env('GITHUB_OUTPUT'), + ).run(previous_tag=args.previous_tag) From 0ad1f183a7b1f2e377dac17908ef5c69c096c5a5 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 10:40:48 -0600 Subject: [PATCH 42/60] Test --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4dd661a5792..42f550539e6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,7 +68,7 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes || true + run: gh release delete pre-release --yes --cleanup-tag || true env: GITHUB_TOKEN: ${{ github.token }} From 8e769d7324a60a9f762b551c223b31b1e7418636 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 10:43:37 -0600 Subject: [PATCH 43/60] - --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 42f550539e6..4dd661a5792 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,7 +68,7 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes --cleanup-tag || true + run: gh release delete pre-release --yes || true env: GITHUB_TOKEN: ${{ github.token }} From 1ad5559e5a4a2803c862a762ed9862a25530bba4 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 10:48:52 -0600 Subject: [PATCH 44/60] test fetch --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 55cd516421d..c4192fc4a4d 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -136,7 +136,7 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - self.git('fetch', f'--shallow-exclude={tag}') + self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') From 09e50c1b14178705bd38f5432ad0718ef1fd8f16 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 10:57:21 -0600 Subject: [PATCH 45/60] try fetch again --- .github/generate_changelog.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index c4192fc4a4d..d4c5c90be84 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,8 +135,10 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') + self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + # self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + # self.git('fetch', f'--shallow-exclude={tag}') + # self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') @@ -147,7 +149,7 @@ def get_raw_commits(self, base: str) -> list[tuple[str, str]]: self.log(f'Getting commits between {base} and {self.sha}') raw = self.git('log', '--format=%H %s', '--max-count=500', f'{base}..{self.sha}') else: - self.log(f'No previous tag — using full history to {self.sha}') + self.log(f'No previous tag - using full history to {self.sha}') raw = self.git('log', '--format=%H %s', '--max-count=500', self.sha) return [(line.split(' ', 1)[0], line.split(' ', 1)[1]) for line in raw.splitlines() if line.strip()] From 5430fcb3d4fd33c7d451d45a7a85c595626addbf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:00:15 -0600 Subject: [PATCH 46/60] Try again --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index d4c5c90be84..691f3e87919 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,7 +135,7 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', 'origin', '--unshallow', f'refs/tags/{tag}:refs/tags/{tag}') # self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') # self.git('fetch', f'--shallow-exclude={tag}') # self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') From aa3cc4ad12bc146d3c157fb90ea3a8f31679599c Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:19:04 -0600 Subject: [PATCH 47/60] - --- .github/generate_changelog.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 691f3e87919..4b28be298dc 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -135,10 +135,8 @@ def parse_semver(tag: str) -> tuple[int, ...]: def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', 'origin', '--unshallow', f'refs/tags/{tag}:refs/tags/{tag}') - # self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - # self.git('fetch', f'--shallow-exclude={tag}') - # self.git('fetch', 'origin', f'refs/tags/{tag}:refs/tags/{tag}', f'--shallow-exclude={tag}') + self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', f'--shallow-exclude={tag}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') From 86402ccc4eb3aa08ddfc1cf6da724657edb89894 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:30:27 -0600 Subject: [PATCH 48/60] Try branch --- .github/generate_changelog.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 4b28be298dc..21b2319aba5 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -133,10 +133,16 @@ def parse_semver(tag: str) -> tuple[int, ...]: ) return candidates[0] if candidates else '' + def ref_branch(self) -> str: + m = re.match(r'^refs/heads/(.+)$', self.ref) + return m.group(1) if m else '' + def tag_exists(self, tag: str) -> bool: try: - self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - self.git('fetch', f'--shallow-exclude={tag}') + # self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + # self.git('fetch', f'--shallow-exclude={tag}') + self.git('fetch', '--depth=1', '--no-tags', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') + self.git('fetch', '--no-tags', f'--shallow-exclude={tag}', 'origin', f'refs/heads/{self.ref_branch()}') return True except subprocess.CalledProcessError: self.log(f'Tag {tag} not found, falling back to full history') From 424b1bc69afc5391bf0c08faf1675c5b59ac5e56 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:35:14 -0600 Subject: [PATCH 49/60] - --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 21b2319aba5..40d00c163fe 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -149,7 +149,7 @@ def tag_exists(self, tag: str) -> bool: return False def get_raw_commits(self, base: str) -> list[tuple[str, str]]: - if base and self.tag_exists(base): + if base: self.log(f'Getting commits between {base} and {self.sha}') raw = self.git('log', '--format=%H %s', '--max-count=500', f'{base}..{self.sha}') else: From 746576185bb63b95537a02039a0ff562987e131e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:37:35 -0600 Subject: [PATCH 50/60] Remove comments --- .github/generate_changelog.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 40d00c163fe..2aba9601c5b 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -139,8 +139,6 @@ def ref_branch(self) -> str: def tag_exists(self, tag: str) -> bool: try: - # self.git('fetch', '--depth=1', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') - # self.git('fetch', f'--shallow-exclude={tag}') self.git('fetch', '--depth=1', '--no-tags', 'origin', f'refs/tags/{tag}:refs/tags/{tag}') self.git('fetch', '--no-tags', f'--shallow-exclude={tag}', 'origin', f'refs/heads/{self.ref_branch()}') return True From 4585a3125f1ea4fdbb703a428525f6c6e4839a24 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:43:11 -0600 Subject: [PATCH 51/60] Extend MERGE_RE --- .github/generate_changelog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 2aba9601c5b..44a6d92ba3e 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -12,7 +12,7 @@ from dataclasses import dataclass, field -MERGE_RE = re.compile(r'^Merge pull request #\d+ from ') +MERGE_RE = re.compile(r'^Merge pull request #\d+ from |^Merge (remote-tracking )?branch ') CC_HEADER_RE = re.compile(r'^([a-z]+)(\([^)]*\))?(!)?:(.*)$') BREAKING_BODY_RE = re.compile(r'^BREAKING\s+CHANGES?:\s+', re.MULTILINE) From 57ed6ac80f1d38b05f02e0ddfea1cd3682d4cadc Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:46:13 -0600 Subject: [PATCH 52/60] Test --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4dd661a5792..42f550539e6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,7 +68,7 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes || true + run: gh release delete pre-release --yes --cleanup-tag || true env: GITHUB_TOKEN: ${{ github.token }} From eb6e54d1001f41635e71a296e36f4a5c1d411d14 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:48:18 -0600 Subject: [PATCH 53/60] Test --- .github/workflows/prerelease.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 42f550539e6..c0515aa6a26 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -68,13 +68,13 @@ jobs: MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - name: Delete existing pre-release - run: gh release delete pre-release --yes --cleanup-tag || true + run: gh release delete pre-release --yes || true env: GITHUB_TOKEN: ${{ github.token }} - name: Generate release notes id: notes - run: python .github/generate_changelog.py --previous-tag=pre-release + run: python .github/generate_changelog.py --previous-tag=4.0.1 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From 95c35b7ee5663c56227be5db75b62636044f8d4a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:49:49 -0600 Subject: [PATCH 54/60] Fix --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index c0515aa6a26..4ad2fba652e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -74,7 +74,7 @@ jobs: - name: Generate release notes id: notes - run: python .github/generate_changelog.py --previous-tag=4.0.1 + run: python .github/generate_changelog.py --previous-tag=v4.0.1 env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From be376ec694ea54bdd49fcf0d93240e478b09a8c8 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 11:57:32 -0600 Subject: [PATCH 55/60] 1000 --- .github/generate_changelog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/generate_changelog.py b/.github/generate_changelog.py index 44a6d92ba3e..9fb04c67d12 100644 --- a/.github/generate_changelog.py +++ b/.github/generate_changelog.py @@ -149,10 +149,10 @@ def tag_exists(self, tag: str) -> bool: def get_raw_commits(self, base: str) -> list[tuple[str, str]]: if base: self.log(f'Getting commits between {base} and {self.sha}') - raw = self.git('log', '--format=%H %s', '--max-count=500', f'{base}..{self.sha}') + raw = self.git('log', '--format=%H %s', '--max-count=1000', f'{base}..{self.sha}') else: self.log(f'No previous tag - using full history to {self.sha}') - raw = self.git('log', '--format=%H %s', '--max-count=500', self.sha) + raw = self.git('log', '--format=%H %s', '--max-count=1000', self.sha) return [(line.split(' ', 1)[0], line.split(' ', 1)[1]) for line in raw.splitlines() if line.strip()] def parse_commit(self, sha: str, subject: str) -> Commit | None: From 411a847413bfbdda7ca2d1d971fe496949d6fb4c Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 12:23:59 -0600 Subject: [PATCH 56/60] token --- .github/workflows/prerelease.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4ad2fba652e..2ea1993fec3 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -60,23 +60,23 @@ jobs: - name: Run Gradle run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar env: - SIGNING_KEY_ALIAS: "key0" + SIGNING_KEY_ALIAS: key0 SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - - name: Delete existing pre-release - run: gh release delete pre-release --yes || true - env: - GITHUB_TOKEN: ${{ github.token }} - - name: Generate release notes id: notes - run: python .github/generate_changelog.py --previous-tag=v4.0.1 + run: python .github/generate_changelog.py --previous-tag=pre-release env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Delete existing pre-release + run: gh release delete pre-release --yes || true + env: + GITHUB_TOKEN: ${{ github.token }} - name: Create pre-release uses: softprops/action-gh-release@v3 @@ -85,9 +85,8 @@ jobs: name: Pre-release Build prerelease: true body: ${{ steps.notes.outputs.changelog }} + token: ${{ steps.app-token.outputs.token }} files: | app/build/outputs/apk/prerelease/release/*.apk app/build/libs/app-sources.jar app/build/classes.jar - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From 48b5805e09781f3fb3260e475cd7b9cd4557b9c0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 12:28:24 -0600 Subject: [PATCH 57/60] Update --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 2ea1993fec3..aa7ec9b34df 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -26,7 +26,7 @@ jobs: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - permission-contents: read + permission-contents: write repositories: secrets - uses: actions/checkout@v6 From 9ee4362526f289a5fd19a7130f6b19d2c6655e18 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 12:31:12 -0600 Subject: [PATCH 58/60] Permissions fix --- .github/workflows/prerelease.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index aa7ec9b34df..6c3ced2c5ff 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true permissions: - contents: write + contents: read jobs: build: @@ -27,7 +27,9 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} permission-contents: write - repositories: secrets + repositories: | + cloudstream + secrets - uses: actions/checkout@v6 @@ -76,7 +78,7 @@ jobs: - name: Delete existing pre-release run: gh release delete pre-release --yes || true env: - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: Create pre-release uses: softprops/action-gh-release@v3 From 8e382083b332e50321da193167e41a80cf89dc82 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 12:37:00 -0600 Subject: [PATCH 59/60] - --- .github/workflows/prerelease.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 6c3ced2c5ff..ef0f953f009 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -13,7 +13,7 @@ concurrency: cancel-in-progress: true permissions: - contents: read + contents: write jobs: build: @@ -26,10 +26,8 @@ jobs: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - permission-contents: write - repositories: | - cloudstream - secrets + permission-contents: read + repositories: secrets - uses: actions/checkout@v6 @@ -78,7 +76,7 @@ jobs: - name: Delete existing pre-release run: gh release delete pre-release --yes || true env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_TOKEN: ${{ github.token }} - name: Create pre-release uses: softprops/action-gh-release@v3 @@ -87,7 +85,6 @@ jobs: name: Pre-release Build prerelease: true body: ${{ steps.notes.outputs.changelog }} - token: ${{ steps.app-token.outputs.token }} files: | app/build/outputs/apk/prerelease/release/*.apk app/build/libs/app-sources.jar From 61d3766a1451cb5046f5f5d99222cef441527d4b Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 7 May 2026 13:11:01 -0600 Subject: [PATCH 60/60] Try --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index ef0f953f009..c84384a6436 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -74,7 +74,7 @@ jobs: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: Delete existing pre-release - run: gh release delete pre-release --yes || true + run: gh release delete pre-release --yes --cleanup-tag || true env: GITHUB_TOKEN: ${{ github.token }}