From 2a8ca056dbddadc9ff77fa586c6f630d730801c2 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 4 Jun 2026 22:18:07 +0530 Subject: [PATCH 1/3] chore(ci): add release-please configs for monorepo --- .github/workflows/release-please.yaml | 60 ++++ .../workflows/reusable-release-please.yaml | 314 ++++++++++++++++++ .github/workflows/scripts/parse-release.sh | 125 +++++++ .../workflows/scripts/parse-release_test.sh | 115 +++++++ .release-please-manifest.json | 6 + pkg/js/.prettierignore | 4 +- release-please-config.json | 23 ++ 7 files changed, 646 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-please.yaml create mode 100644 .github/workflows/reusable-release-please.yaml create mode 100755 .github/workflows/scripts/parse-release.sh create mode 100755 .github/workflows/scripts/parse-release_test.sh create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 00000000..2707a989 --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,60 @@ +name: Release Please + +on: + workflow_dispatch: + inputs: + target-package: + description: "Component to target" + type: choice + required: false + default: "all" + options: + - all + - pkg/go + - pkg/js + - pkg/java + bump-type: + description: "Version bump" + type: choice + required: false + default: "auto" + options: + - auto + - patch + - minor + - major + - explicit + release-version: + description: "Explicit version (only used when bump-type=explicit)" + type: string + required: false + default: "" + # Phase 2 (automatic): publish releases when a release commit lands on main. + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + uses: ./.github/workflows/reusable-release-please.yaml + permissions: + contents: write + pull-requests: write + with: + trigger-event: ${{ github.event_name == 'workflow_dispatch' && 'workflow_dispatch' || 'push' }} + target-package: ${{ inputs.target-package || 'all' }} + bump-type: ${{ inputs.bump-type || 'auto' }} + release-version: ${{ inputs.release-version || '' }} + secrets: + RELEASER_APP_CLIENT_ID: ${{ secrets.RELEASER_APP_CLIENT_ID }} + RELEASER_APP_PRIVATE_KEY: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + diff --git a/.github/workflows/reusable-release-please.yaml b/.github/workflows/reusable-release-please.yaml new file mode 100644 index 00000000..689e9774 --- /dev/null +++ b/.github/workflows/reusable-release-please.yaml @@ -0,0 +1,314 @@ +name: reusable-release-please + +on: + workflow_call: + inputs: + trigger-event: + description: "What kicked off the run: workflow_dispatch | push" + type: string + required: true + target-package: + description: "Component to target: all | pkg/go | pkg/js | pkg/java" + type: string + required: false + default: "all" + bump-type: + description: "Version bump: auto | patch | minor | major | explicit" + type: string + required: false + default: "auto" + release-version: + description: "Explicit version to release (used when bump-type=explicit)" + type: string + required: false + default: "" + secrets: + RELEASER_APP_CLIENT_ID: + required: true + RELEASER_APP_PRIVATE_KEY: + required: true + GPG_PRIVATE_KEY: + required: true + GPG_PASSPHRASE: + required: false + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +jobs: + ########################################################################### + # Phase 1: create / retarget the release-please PR(s). + ########################################################################### + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Generate GitHub App token + id: app-token + # Pin to a SHA via your dependency tooling (Renovate/StepSecurity). + uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v2.1.4 + with: + app-id: ${{ secrets.RELEASER_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} + + - name: Checkout (full history + tags) + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Configure git identity + run: | + git config user.name "openfga-releaser[bot]" + git config user.email "openfga-releaser[bot]@users.noreply.github.com" + + - name: Prepare dispatch run + if: inputs.trigger-event == 'workflow_dispatch' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + TARGET_PACKAGE: ${{ inputs.target-package }} + BUMP_TYPE: ${{ inputs.bump-type }} + RELEASE_VERSION: ${{ inputs.release-version }} + run: | + set -euo pipefail + PARSE=".github/workflows/scripts/parse-release.sh" + + git fetch origin main --tags --prune + + # 1) Reset the `release` branch from origin/main. + git branch -f release origin/main + git push -f origin release + + # 2) Ensure a baseline tag exists for every component in the manifest. + while read -r component version; do + tag="${component}/v${version}" + if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Creating baseline tag ${tag}" + git tag "${tag}" origin/main + git push origin "refs/tags/${tag}" + fi + done < <(jq -r 'to_entries[] | "\(.key) \(.value)"' .release-please-manifest.json) + + # 3) Close stale release-please PRs. + for pr in $(gh pr list --state open \ + --search "head:release-please--branches--release" \ + --json number --jq '.[].number'); do + echo "Closing stale release PR #${pr}" + gh pr close "${pr}" --delete-branch || true + done + + # 4) Drop stale `autorelease: pending` labels on non-main merged PRs. + for pr in $(gh pr list --state merged --label "autorelease: pending" \ + --json number,baseRefName \ + --jq '.[] | select(.baseRefName != "main") | .number'); do + echo "Removing stale label from PR #${pr}" + gh pr edit "${pr}" --remove-label "autorelease: pending" || true + done + + # 5) Optionally force a version via a Release-As marker commit. + if [[ "${BUMP_TYPE}" != "auto" ]]; then + git checkout release + + components=() + if [[ "${TARGET_PACKAGE}" == "all" ]]; then + while read -r c; do components+=("$c"); done \ + < <(jq -r 'keys[]' .release-please-manifest.json) + else + components=("${TARGET_PACKAGE}") + fi + + for component in "${components[@]}"; do + current="$(jq -r --arg c "${component}" '.[$c]' .release-please-manifest.json)" + case "${BUMP_TYPE}" in + explicit) + newver="${RELEASE_VERSION}" + ;; + patch|minor|major) + newver="$("${PARSE}" next-version "${current}" "${BUMP_TYPE}")" + ;; + *) + echo "Unsupported bump-type: ${BUMP_TYPE}" >&2 + exit 1 + ;; + esac + + if [[ -z "${newver}" ]]; then + echo "Could not resolve a version for ${component}" >&2 + exit 1 + fi + + echo "Forcing ${component} -> ${newver} via Release-As" + marker="${component}/.release-please-trigger" + printf 'Release-As %s requested at %s\n' "${newver}" "$(date -u +%FT%TZ)" >"${marker}" + git add "${marker}" + git commit -m "chore(${component}): release ${newver}" -m "Release-As: ${newver}" + done + + git push -f origin release + fi + + - name: Resolve target branch + id: target + run: | + if [[ "${{ inputs.trigger-event }}" == "workflow_dispatch" ]]; then + echo "branch=release" >> "$GITHUB_OUTPUT" + else + echo "branch=main" >> "$GITHUB_OUTPUT" + fi + + - name: Run release-please + id: release + uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 + with: + token: ${{ steps.app-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ steps.target.outputs.branch }} + + - name: Retarget release PRs onto main + if: inputs.trigger-event == 'workflow_dispatch' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + + # Move every release-please PR from the `release` branch onto `main` + # and rename its head ref `--branches--release` -> `--branches--main`. + for pr in $(gh pr list --state open \ + --search "head:release-please--branches--release" \ + --json number --jq '.[].number'); do + head="$(gh pr view "${pr}" --json headRefName --jq '.headRefName')" + new_head="${head//--branches--release/--branches--main}" + + echo "Retargeting PR #${pr} (${head} -> ${new_head}, base -> main)" + + # Recreate the head ref under the new name pointing at the same commit. + if [[ "${new_head}" != "${head}" ]]; then + sha="$(git rev-parse "origin/${head}" 2>/dev/null || echo "")" + if [[ -n "${sha}" ]]; then + git push origin "${sha}:refs/heads/${new_head}" --force + gh api --method PATCH "repos/${GITHUB_REPOSITORY}/pulls/${pr}" \ + -f head="${new_head}" >/dev/null 2>&1 || true + git push origin --delete "${head}" || true + fi + fi + + gh pr edit "${pr}" --base main || true + done + + ########################################################################### + # Phase 2: publish releases after a release commit lands on main. + ########################################################################### + post-release: + runs-on: ubuntu-latest + needs: [release-please] + if: >- + inputs.trigger-event == 'push' && + startsWith(github.event.head_commit.message, 'release:') + permissions: + contents: write + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v2.1.4 + with: + app-id: ${{ secrets.RELEASER_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} + + - name: Checkout (full history + tags) + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Import GPG key and enable signed tags + # Imports the key, caches the passphrase, and configures git so that + # `git tag -s` produces signed tags non-interactively in CI. + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_tag_gpgsign: true + + - name: Create signed tags and draft releases + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + PARSE=".github/workflows/scripts/parse-release.sh" + + # Diff the manifest committed by the release commit against its parent. + git show "HEAD~1:.release-please-manifest.json" > /tmp/prev-manifest.json 2>/dev/null \ + || echo '{}' > /tmp/prev-manifest.json + cp .release-please-manifest.json /tmp/cur-manifest.json + + releases="$("${PARSE}" manifest-diff /tmp/cur-manifest.json /tmp/prev-manifest.json)" + echo "Releases to publish: ${releases}" + + count="$(jq 'length' <<<"${releases}")" + if [[ "${count}" -eq 0 ]]; then + echo "No changed components; nothing to release." + exit 0 + fi + + for i in $(seq 0 $((count - 1))); do + component="$(jq -r ".[$i].component" <<<"${releases}")" + version="$(jq -r ".[$i].version" <<<"${releases}")" + tag="$(jq -r ".[$i].tag_name" <<<"${releases}")" + + # 1) Ensure the signed tag exists. + if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Creating signed tag ${tag}" + git tag -s "${tag}" -m "Release ${tag}" + git push origin "refs/tags/${tag}" + else + echo "Tag ${tag} already exists" + fi + + # 2) Notes from the package CHANGELOG. + NOTES="$("${PARSE}" changelog-notes "${component}/CHANGELOG.md" "${version}" || true)" + + # 3) GitHub auto-generated notes (previous tag = previous version of + # the same component, when one exists). + PREV_TAG="$(git tag --list "${component}/v*" --sort=-version:refname \ + | grep -vx "${tag}" | head -1 || true)" + GEN_PAYLOAD=$(jq -nc \ + --arg tag "${tag}" \ + --arg target "main" \ + --arg prev "${PREV_TAG}" \ + '{tag_name: $tag, target_commitish: $target} + + (if $prev != "" then {previous_tag_name: $prev} else {} end)') + + AUTO_NOTES=$(gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + "repos/${REPO}/releases/generate-notes" \ + --input - <<<"${GEN_PAYLOAD}" \ + --jq '.body' 2>/dev/null || true) + + # 4) Combine: changelog notes + blank line + API notes. + if [[ -n "${AUTO_NOTES}" ]]; then + NOTES=$(printf '%s\n\n%s\n' "${NOTES}" "${AUTO_NOTES}") + fi + + # 5) Create a draft GitHub release. + if gh release view "${tag}" >/dev/null 2>&1; then + echo "Release ${tag} already exists; skipping" + else + echo "Creating draft release ${tag}" + gh release create "${tag}" --title "${tag}" --notes "${NOTES}" --draft + fi + done + + # Keep the `release` branch in sync with main for the next dispatch run. + git push -f origin "HEAD:refs/heads/release" + + diff --git a/.github/workflows/scripts/parse-release.sh b/.github/workflows/scripts/parse-release.sh new file mode 100755 index 00000000..8664824d --- /dev/null +++ b/.github/workflows/scripts/parse-release.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# +# parse-release.sh - helpers used by the release-please automation. +# +# Subcommands: +# manifest-diff +# Print a JSON array of the components whose version changed between two +# release-please manifests. Each entry has the shape: +# {"component":"pkg/js","version":"0.2.3","tag_name":"pkg/js/v0.2.3"} +# +# changelog-notes +# Print the markdown body of the section that documents . +# Leading/trailing blank lines are trimmed. +# +# next-version +# Print the next semantic version after for the given bump +# type. Any pre-release / build metadata on is dropped. +# +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +Usage: + parse-release.sh manifest-diff + parse-release.sh changelog-notes + parse-release.sh next-version +EOF + exit 64 +} + +manifest_diff() { + local current="$1" previous="$2" + + [[ -f "$current" ]] || { echo "current manifest not found: $current" >&2; exit 1; } + # A missing previous manifest is treated as "everything is new". + if [[ ! -f "$previous" ]]; then + previous="/dev/null" + fi + + jq -nc \ + --slurpfile cur "$current" \ + --slurpfile prev "$previous" ' + ($cur[0] // {}) as $c + | ($prev[0] // {}) as $p + | [ $c + | to_entries[] + | select(.value != ($p[.key] // null)) + | { component: .key, + version: .value, + tag_name: (.key + "/v" + .value) } ] + ' +} + +changelog_notes() { + local file="$1" version="$2" + + [[ -f "$file" ]] || { echo "changelog not found: $file" >&2; exit 1; } + + awk -v ver="$version" ' + function is_header(line) { return line ~ /^## / } + BEGIN { found = 0 } + { + if (is_header($0)) { + if (found) { exit } + # Match the section header for this version (with or without a "v" + # prefix, e.g. "## [0.2.1]" or "## pkg/go/v0.2.1"). + if (index($0, "v" ver) > 0 || index($0, ver) > 0) { + found = 1 + next + } + } + if (found) { lines[n++] = $0 } + } + END { + # Trim leading blank lines. + start = 0 + while (start < n && lines[start] ~ /^[[:space:]]*$/) start++ + # Trim trailing blank lines. + end = n - 1 + while (end >= start && lines[end] ~ /^[[:space:]]*$/) end-- + for (i = start; i <= end; i++) print lines[i] + } + ' "$file" +} + +next_version() { + local base="$1" bump="$2" + + # Strip a leading "v" and any pre-release / build metadata. + base="${base#v}" + base="${base%%-*}" + base="${base%%+*}" + + if [[ ! "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "invalid base version: $1" >&2 + exit 1 + fi + + local major minor patch + IFS='.' read -r major minor patch <<<"$base" + + case "$bump" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + *) echo "invalid bump type: $bump (expected patch|minor|major)" >&2; exit 1 ;; + esac + + printf '%s.%s.%s\n' "$major" "$minor" "$patch" +} + +main() { + [[ $# -ge 1 ]] || usage + local cmd="$1"; shift + + case "$cmd" in + manifest-diff) [[ $# -eq 2 ]] || usage; manifest_diff "$@" ;; + changelog-notes) [[ $# -eq 2 ]] || usage; changelog_notes "$@" ;; + next-version) [[ $# -eq 2 ]] || usage; next_version "$@" ;; + *) usage ;; + esac +} + +main "$@" + diff --git a/.github/workflows/scripts/parse-release_test.sh b/.github/workflows/scripts/parse-release_test.sh new file mode 100755 index 00000000..1aa65409 --- /dev/null +++ b/.github/workflows/scripts/parse-release_test.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# parse-release_test.sh - lightweight tests for parse-release.sh. +# +# Usage: .github/workflows/scripts/parse-release_test.sh +# Requires: bash, jq +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARSE="${SCRIPT_DIR}/parse-release.sh" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +pass=0 +fail=0 + +assert_eq() { + local name="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + pass=$((pass + 1)) + echo "ok - $name" + else + fail=$((fail + 1)) + echo "FAIL - $name" + echo " expected: |$expected|" + echo " actual: |$actual|" + fi +} + +##### Fixtures ##### + +cat >"$TMP/prev.json" <<'EOF' +{ + "pkg/go": "0.2.1", + "pkg/js": "0.2.1", + "pkg/java": "0.2.0-beta.2" +} +EOF + +cat >"$TMP/cur.json" <<'EOF' +{ + "pkg/go": "0.2.1", + "pkg/js": "0.2.3", + "pkg/java": "0.3.0" +} +EOF + +cat >"$TMP/CHANGELOG.md" <<'EOF' +# Changelog + +## pkg/js/v0.2.3 + +### [v0.2.3](https://example.com/compare/pkg/js/v0.2.1...pkg/js/v0.2.3) (2026-06-04) + +Added: + +- A shiny new feature (#123) + +Fixed: + +- A pesky bug (#124) + +## pkg/js/v0.2.1 + +### [v0.2.1](https://example.com) (2026-02-11) + +Added: + +- Older stuff +EOF + +##### manifest-diff ##### + +# Only pkg/js and pkg/java changed; output sorted by component for stable compare. +diff_out="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" | jq -S -c 'sort_by(.component)')" +expected_diff='[{"component":"pkg/java","tag_name":"pkg/java/v0.3.0","version":"0.3.0"},{"component":"pkg/js","tag_name":"pkg/js/v0.2.3","version":"0.2.3"}]' +assert_eq "manifest-diff reports only changed components" "$expected_diff" "$diff_out" + +# Missing previous manifest => everything is new. +all_new="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/does-not-exist.json" | jq -c 'length')" +assert_eq "manifest-diff treats missing previous manifest as all-new" "3" "$all_new" + +# No changes => empty array. +no_change="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/cur.json")" +assert_eq "manifest-diff with identical manifests is empty" "[]" "$no_change" + +##### changelog-notes ##### + +notes="$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.3")" +expected_notes='### [v0.2.3](https://example.com/compare/pkg/js/v0.2.1...pkg/js/v0.2.3) (2026-06-04) + +Added: + +- A shiny new feature (#123) + +Fixed: + +- A pesky bug (#124)' +assert_eq "changelog-notes extracts the matching section body" "$expected_notes" "$notes" + +##### next-version ##### + +assert_eq "next-version patch" "0.2.2" "$("$PARSE" next-version 0.2.1 patch)" +assert_eq "next-version minor" "0.3.0" "$("$PARSE" next-version 0.2.1 minor)" +assert_eq "next-version major" "1.0.0" "$("$PARSE" next-version 0.2.1 major)" +assert_eq "next-version drops pre-release metadata" "0.3.0" "$("$PARSE" next-version v0.2.0-beta.2 minor)" + +##### Summary ##### + +echo +echo "passed: $pass, failed: $fail" +[[ "$fail" -eq 0 ]] + diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..a063489a --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,6 @@ +{ + "pkg/go": "0.2.1", + "pkg/js": "0.2.1", + "pkg/java": "0.2.0-beta.2" +} + diff --git a/pkg/js/.prettierignore b/pkg/js/.prettierignore index 222e0aeb..f09472d4 100644 --- a/pkg/js/.prettierignore +++ b/pkg/js/.prettierignore @@ -1,3 +1,5 @@ tests/__snapshots__/* gen -.npm-upgrade.json \ No newline at end of file +.npm-upgrade.json +# Generated by release-please; formatting is managed by the release tooling. +CHANGELOG.md diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..4ca98738 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "packages": { + "pkg/go": { + "component": "pkg/go", + "changelog-path": "pkg/go/CHANGELOG.md" + }, + "pkg/js": { + "component": "pkg/js", + "changelog-path": "pkg/js/CHANGELOG.md" + }, + "pkg/java": { + "component": "pkg/java", + "changelog-path": "pkg/java/CHANGELOG.md" + } + }, + "separate-pull-requests": true, + "include-component-in-tag": true, + "tag-separator": "/", + "bootstrap-sha": "" +} + From 309e4917228f30cdd63031fa648f6ebf70ddcb7b Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 4 Jun 2026 22:20:59 +0530 Subject: [PATCH 2/3] feat: add CI test scripts --- ...-release_test.sh => parse-release.test.sh} | 0 .github/workflows/test-release-scripts.yml | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+) rename .github/workflows/scripts/{parse-release_test.sh => parse-release.test.sh} (100%) create mode 100644 .github/workflows/test-release-scripts.yml diff --git a/.github/workflows/scripts/parse-release_test.sh b/.github/workflows/scripts/parse-release.test.sh similarity index 100% rename from .github/workflows/scripts/parse-release_test.sh rename to .github/workflows/scripts/parse-release.test.sh diff --git a/.github/workflows/test-release-scripts.yml b/.github/workflows/test-release-scripts.yml new file mode 100644 index 00000000..a752ff0d --- /dev/null +++ b/.github/workflows/test-release-scripts.yml @@ -0,0 +1,25 @@ +name: Test release parsing scripts + +on: + pull_request: + paths: + - .github/workflows/scripts/parse-release.sh + - .github/workflows/scripts/parse-release.test.sh + - .github/workflows/test-release-scripts.yml + push: + branches: [main] + paths: + - .github/workflows/scripts/parse-release.sh + - .github/workflows/scripts/parse-release.test.sh + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Run parse-release tests + run: bash .github/workflows/scripts/parse-release.test.sh + From dddf0d206ac2277ec4c44ad4b6d54517a96ad8b3 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 4 Jun 2026 23:10:46 +0530 Subject: [PATCH 3/3] fix: remove the duplicate draft release logic per registry --- .github/workflows/pkg-go-release.yaml | 36 +- .github/workflows/pkg-java-release.yaml | 37 +- .github/workflows/pkg-js-release.yaml | 36 +- .../workflows/reusable-release-please.yaml | 22 +- .github/workflows/scripts/parse-release.sh | 201 ++++----- .../workflows/scripts/parse-release.test.sh | 415 +++++++++++++++--- pkg/java/build.gradle | 2 +- release-please-config.json | 40 +- 8 files changed, 541 insertions(+), 248 deletions(-) diff --git a/.github/workflows/pkg-go-release.yaml b/.github/workflows/pkg-go-release.yaml index 5a3b1c82..2c436783 100644 --- a/.github/workflows/pkg-go-release.yaml +++ b/.github/workflows/pkg-go-release.yaml @@ -13,22 +13,30 @@ jobs: uses: ./.github/workflows/pkg-go-build.yaml secrets: inherit - release: + # The release-please post-release job already created a draft GitHub release + # (CHANGELOG notes + auto-generated notes) when it pushed this tag. Go has no + # registry publish step, so once tests pass on the tag we publish (undraft) it. + finalize-release: runs-on: ubuntu-latest needs: [test] + if: startsWith(github.ref, 'refs/tags/') permissions: - contents: write - packages: write # publish a new github release + contents: write # publish (undraft) the github release steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - - - uses: Roang-zero1/github-create-release-action@57eb9bdce7a964e48788b9e78b5ac766cb684803 # v3.0.1 - with: - version_regex: ^pkg/go/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ - prerelease_regex: ^pkg/go/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-(alpha|beta)\.[[:digit:]]+$ - changelog_file: pkg/go/CHANGELOG.md - create_draft: true + - name: Publish the draft GitHub release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # The draft is created right after the tag is pushed; wait briefly in + # case this job gets there first. + for _ in $(seq 1 10); do + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + break + fi + echo "Waiting for draft release $TAG ..." + sleep 6 + done + gh release edit "$TAG" --repo "$REPO" --draft=false diff --git a/.github/workflows/pkg-java-release.yaml b/.github/workflows/pkg-java-release.yaml index ddfd5828..b6fb3d1e 100644 --- a/.github/workflows/pkg-java-release.yaml +++ b/.github/workflows/pkg-java-release.yaml @@ -93,23 +93,30 @@ jobs: ORG_GRADLE_PROJECT_SIGNINGKEY: ${{ secrets.GPG_PRIVATE_KEY }} ORG_GRADLE_PROJECT_SIGNINGPASSWORD: ${{ secrets.GPG_PASSPHRASE }} - create-release: + # The release-please post-release job already created a draft GitHub release + # (CHANGELOG notes + auto-generated notes) when it pushed this tag. Publish + # (undraft) it only after both Maven publishes above have succeeded. + finalize-release: runs-on: ubuntu-latest needs: [publish-maven-central, publish-github-packages] + if: startsWith(github.ref, 'refs/tags/') permissions: - contents: write - packages: write # publish a new github release + contents: write # publish (undraft) the github release steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - - - name: Create Release - uses: Roang-zero1/github-create-release-action@57eb9bdce7a964e48788b9e78b5ac766cb684803 # v3.0.1 - with: - version_regex: ^pkg/java/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ - prerelease_regex: ^pkg/java/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-(alpha|beta)\.[[:digit:]]+$ - changelog_file: pkg/java/CHANGELOG.md - create_draft: true + - name: Publish the draft GitHub release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # The draft is created right after the tag is pushed; wait briefly in + # case this job gets there first. + for _ in $(seq 1 10); do + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + break + fi + echo "Waiting for draft release $TAG ..." + sleep 6 + done + gh release edit "$TAG" --repo "$REPO" --draft=false diff --git a/.github/workflows/pkg-js-release.yaml b/.github/workflows/pkg-js-release.yaml index f973dd4c..f3587e4f 100644 --- a/.github/workflows/pkg-js-release.yaml +++ b/.github/workflows/pkg-js-release.yaml @@ -80,22 +80,30 @@ jobs: - name: Publish to npm run: npx -y npm@11.9.0 publish --tag ${{ steps.npm-tag.outputs.tag }} - release: + # The release-please post-release job already created a draft GitHub release + # (CHANGELOG notes + auto-generated notes) when it pushed this tag. Publish + # (undraft) it only after the npm publish above has succeeded. + finalize-release: runs-on: ubuntu-latest needs: [publish] + if: startsWith(github.ref, 'refs/tags/') permissions: - contents: write - packages: write # publish a new github release + contents: write # publish (undraft) the github release steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - - - uses: Roang-zero1/github-create-release-action@57eb9bdce7a964e48788b9e78b5ac766cb684803 # v3.0.1 - with: - version_regex: ^pkg/js/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ - prerelease_regex: ^pkg/js/v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+-(alpha|beta|rc)\.[[:digit:]]+$ - changelog_file: pkg/js/CHANGELOG.md - create_draft: true + - name: Publish the draft GitHub release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # The draft is created right after the tag is pushed; wait briefly in + # case this job gets there first. + for _ in $(seq 1 10); do + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + break + fi + echo "Waiting for draft release $TAG ..." + sleep 6 + done + gh release edit "$TAG" --repo "$REPO" --draft=false diff --git a/.github/workflows/reusable-release-please.yaml b/.github/workflows/reusable-release-please.yaml index 689e9774..2a16dbd8 100644 --- a/.github/workflows/reusable-release-please.yaml +++ b/.github/workflows/reusable-release-please.yaml @@ -35,10 +35,6 @@ on: permissions: contents: read -concurrency: - group: release - cancel-in-progress: false - jobs: ########################################################################### # Phase 1: create / retarget the release-please PR(s). @@ -163,7 +159,9 @@ jobs: fi - name: Run release-please - id: release + # We intentionally do not consume this action's outputs: skip-github-release + # is set in the config, so release/tag outputs are not produced. The + # post-release job derives everything from the manifest diff instead. uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 with: token: ${{ steps.app-token.outputs.token }} @@ -250,14 +248,15 @@ jobs: || echo '{}' > /tmp/prev-manifest.json cp .release-please-manifest.json /tmp/cur-manifest.json - releases="$("${PARSE}" manifest-diff /tmp/cur-manifest.json /tmp/prev-manifest.json)" - echo "Releases to publish: ${releases}" - - count="$(jq 'length' <<<"${releases}")" - if [[ "${count}" -eq 0 ]]; then + # manifest-diff exits non-zero when nothing changed; treat that as a + # no-op rather than a failure. + if ! releases="$("${PARSE}" manifest-diff /tmp/cur-manifest.json /tmp/prev-manifest.json 2>/dev/null)"; then echo "No changed components; nothing to release." exit 0 fi + echo "Releases to publish: ${releases}" + + count="$(jq 'length' <<<"${releases}")" for i in $(seq 0 $((count - 1))); do component="$(jq -r ".[$i].component" <<<"${releases}")" @@ -312,3 +311,6 @@ jobs: git push -f origin "HEAD:refs/heads/release" + + + diff --git a/.github/workflows/scripts/parse-release.sh b/.github/workflows/scripts/parse-release.sh index 8664824d..a15d7692 100755 --- a/.github/workflows/scripts/parse-release.sh +++ b/.github/workflows/scripts/parse-release.sh @@ -1,125 +1,92 @@ #!/usr/bin/env bash +# Extracted from .github/workflows/reusable-release-please.yaml so the parsing +# logic can be unit-tested independently of GitHub Actions. # -# parse-release.sh - helpers used by the release-please automation. +# Usage: +# parse-release.sh manifest-diff +# -> prints JSON array of {component, version, tag_name} for changed pkgs +# exits 1 if no changes detected # -# Subcommands: -# manifest-diff -# Print a JSON array of the components whose version changed between two -# release-please manifests. Each entry has the shape: -# {"component":"pkg/js","version":"0.2.3","tag_name":"pkg/js/v0.2.3"} -# -# changelog-notes -# Print the markdown body of the section that documents . -# Leading/trailing blank lines are trimmed. -# -# next-version -# Print the next semantic version after for the given bump -# type. Any pre-release / build metadata on is dropped. +# parse-release.sh changelog-notes +# -> prints the release notes section for the given version +# falls back to "Release " if no section found # +# parse-release.sh next-version +# -> prints the next semantic version after for the given +# bump type (pre-release/build metadata on is dropped). +# Used by the workflow_dispatch path to force a Release-As version. set -euo pipefail -usage() { - cat >&2 <<'EOF' -Usage: - parse-release.sh manifest-diff - parse-release.sh changelog-notes - parse-release.sh next-version -EOF - exit 64 -} - -manifest_diff() { - local current="$1" previous="$2" - - [[ -f "$current" ]] || { echo "current manifest not found: $current" >&2; exit 1; } - # A missing previous manifest is treated as "everything is new". - if [[ ! -f "$previous" ]]; then - previous="/dev/null" - fi - - jq -nc \ - --slurpfile cur "$current" \ - --slurpfile prev "$previous" ' - ($cur[0] // {}) as $c - | ($prev[0] // {}) as $p - | [ $c - | to_entries[] - | select(.value != ($p[.key] // null)) - | { component: .key, - version: .value, - tag_name: (.key + "/v" + .value) } ] - ' -} - -changelog_notes() { - local file="$1" version="$2" - - [[ -f "$file" ]] || { echo "changelog not found: $file" >&2; exit 1; } - - awk -v ver="$version" ' - function is_header(line) { return line ~ /^## / } - BEGIN { found = 0 } - { - if (is_header($0)) { - if (found) { exit } - # Match the section header for this version (with or without a "v" - # prefix, e.g. "## [0.2.1]" or "## pkg/go/v0.2.1"). - if (index($0, "v" ver) > 0 || index($0, ver) > 0) { - found = 1 - next - } +cmd="${1:-}" +shift || true + +case "$cmd" in + manifest-diff) + current_file="$1" + previous_file="$2" + + releases=$(jq -c -n \ + --argjson cur "$(cat "$current_file")" \ + --argjson prev "$(cat "$previous_file")" \ + '[ $cur | to_entries[] + | select(.value != $prev[.key]) + | { component: .key, version: .value, tag_name: "\(.key)/v\(.value)" } ]') + + if [[ $(jq 'length' <<<"$releases") -eq 0 ]]; then + echo "::error::No version changes detected." >&2 + exit 1 + fi + + echo "$releases" + ;; + + changelog-notes) + changelog="$1" + version="$2" + + notes=$(awk -v ver="$version" ' + BEGIN { + gsub(/\./, "\\.", ver) + pattern = "(^|[^0-9A-Za-z.-])" ver "([^0-9A-Za-z.-]|$)" } - if (found) { lines[n++] = $0 } - } - END { - # Trim leading blank lines. - start = 0 - while (start < n && lines[start] ~ /^[[:space:]]*$/) start++ - # Trim trailing blank lines. - end = n - 1 - while (end >= start && lines[end] ~ /^[[:space:]]*$/) end-- - for (i = start; i <= end; i++) print lines[i] - } - ' "$file" -} - -next_version() { - local base="$1" bump="$2" - - # Strip a leading "v" and any pre-release / build metadata. - base="${base#v}" - base="${base%%-*}" - base="${base%%+*}" - - if [[ ! "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "invalid base version: $1" >&2 - exit 1 - fi - - local major minor patch - IFS='.' read -r major minor patch <<<"$base" - - case "$bump" in - major) major=$((major + 1)); minor=0; patch=0 ;; - minor) minor=$((minor + 1)); patch=0 ;; - patch) patch=$((patch + 1)) ;; - *) echo "invalid bump type: $bump (expected patch|minor|major)" >&2; exit 1 ;; - esac - - printf '%s.%s.%s\n' "$major" "$minor" "$patch" -} - -main() { - [[ $# -ge 1 ]] || usage - local cmd="$1"; shift - - case "$cmd" in - manifest-diff) [[ $# -eq 2 ]] || usage; manifest_diff "$@" ;; - changelog-notes) [[ $# -eq 2 ]] || usage; changelog_notes "$@" ;; - next-version) [[ $# -eq 2 ]] || usage; next_version "$@" ;; - *) usage ;; - esac -} - -main "$@" + /^## / { + if (found) exit + if ($0 ~ pattern) { found=1; next } + } + found { print } + ' "$changelog") + + if [[ -z "$notes" ]]; then + echo "Release ${version}" + else + echo "$notes" + fi + ;; + + next-version) + base="$1" + bump="$2" + + # Strip a leading "v" and any pre-release / build metadata. + base="${base#v}" + base="${base%%-*}" + base="${base%%+*}" + + IFS='.' read -r major minor patch <<<"$base" + + case "$bump" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + *) echo "Usage: $0 next-version " >&2; exit 2 ;; + esac + + echo "${major}.${minor}.${patch}" + ;; + + *) + echo "Usage: $0 {manifest-diff|changelog-notes|next-version} ..." >&2 + exit 2 + ;; +esac diff --git a/.github/workflows/scripts/parse-release.test.sh b/.github/workflows/scripts/parse-release.test.sh index 1aa65409..68f87203 100755 --- a/.github/workflows/scripts/parse-release.test.sh +++ b/.github/workflows/scripts/parse-release.test.sh @@ -1,115 +1,394 @@ #!/usr/bin/env bash -# -# parse-release_test.sh - lightweight tests for parse-release.sh. -# -# Usage: .github/workflows/scripts/parse-release_test.sh -# Requires: bash, jq -# -set -euo pipefail +# Tests for parse-release.sh +# Run: bash .github/workflows/scripts/parse-release.test.sh -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PARSE="${SCRIPT_DIR}/parse-release.sh" +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARSE="$SCRIPT_DIR/parse-release.sh" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT -pass=0 -fail=0 +PASS=0 +FAIL=0 assert_eq() { local name="$1" expected="$2" actual="$3" if [[ "$expected" == "$actual" ]]; then - pass=$((pass + 1)) - echo "ok - $name" + echo " PASS - $name" + PASS=$((PASS + 1)) else - fail=$((fail + 1)) - echo "FAIL - $name" - echo " expected: |$expected|" - echo " actual: |$actual|" + echo " FAIL - $name" + echo " expected: $expected" + echo " actual: $actual" + FAIL=$((FAIL + 1)) fi } -##### Fixtures ##### +assert_contains() { + local name="$1" needle="$2" haystack="$3" + if [[ "$haystack" == *"$needle"* ]]; then + echo " PASS - $name" + PASS=$((PASS + 1)) + else + echo " FAIL - $name" + echo " expected to contain: $needle" + echo " actual: $haystack" + FAIL=$((FAIL + 1)) + fi +} -cat >"$TMP/prev.json" <<'EOF' -{ - "pkg/go": "0.2.1", - "pkg/js": "0.2.1", - "pkg/java": "0.2.0-beta.2" +assert_exit_code() { + local name="$1" expected="$2" actual="$3" + if [[ "$expected" -eq "$actual" ]]; then + echo " PASS - $name (exit=$actual)" + PASS=$((PASS + 1)) + else + echo " FAIL - $name" + echo " expected exit: $expected" + echo " actual exit: $actual" + FAIL=$((FAIL + 1)) + fi } -EOF -cat >"$TMP/cur.json" <<'EOF' -{ - "pkg/go": "0.2.1", - "pkg/js": "0.2.3", - "pkg/java": "0.3.0" +##################################################################### +# manifest-diff tests +##################################################################### +echo +echo "=== manifest-diff ===" + +diff_run() { + echo "$1" >"$TMP/cur.json" + echo "$2" >"$TMP/prev.json" + "$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" 2>/dev/null } -EOF + +# 1. Only pkg/go bumped +out=$(diff_run \ + '{"pkg/go":"0.3.0","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +assert_eq "only pkg/go bumped" \ + '[{"component":"pkg/go","version":"0.3.0","tag_name":"pkg/go/v0.3.0"}]' \ + "$out" + +# 2. Only pkg/js bumped +out=$(diff_run \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.5","pkg/java":"0.2.0-beta.0"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +assert_eq "only pkg/js bumped" \ + '[{"component":"pkg/js","version":"0.2.5","tag_name":"pkg/js/v0.2.5"}]' \ + "$out" + +# 3. Only pkg/java bumped +out=$(diff_run \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.1"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +assert_eq "only pkg/java bumped (pre-release)" \ + '[{"component":"pkg/java","version":"0.2.0-beta.1","tag_name":"pkg/java/v0.2.0-beta.1"}]' \ + "$out" + +# 4. Two packages bumped simultaneously +out=$(diff_run \ + '{"pkg/go":"0.3.0","pkg/js":"0.2.5","pkg/java":"0.2.0-beta.0"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +assert_eq "pkg/go + pkg/js bumped together" \ + '[{"component":"pkg/go","version":"0.3.0","tag_name":"pkg/go/v0.3.0"},{"component":"pkg/js","version":"0.2.5","tag_name":"pkg/js/v0.2.5"}]' \ + "$out" + +# 5. All three packages bumped +out=$(diff_run \ + '{"pkg/go":"0.3.0","pkg/js":"0.3.0","pkg/java":"0.3.0"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +n=$(echo "$out" | jq 'length') +assert_eq "all three packages bumped (count=3)" "3" "$n" + +# 6. No changes; expect exit 1 with error +echo '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}' >"$TMP/cur.json" +cp "$TMP/cur.json" "$TMP/prev.json" +err=$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" 2>&1 >/dev/null) +code=$? +assert_exit_code "no changes exits non-zero" 1 "$code" +assert_contains "no changes error message" "No version changes detected" "$err" + +# 7. New package added (not in previous); expect a release +out=$(diff_run \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0","pkg/rust":"0.1.0"}' \ + '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}') +assert_eq "new package added to manifest" \ + '[{"component":"pkg/rust","version":"0.1.0","tag_name":"pkg/rust/v0.1.0"}]' \ + "$out" + +# 8. Package removed from manifest is ignored (not in current) +echo '{"pkg/go":"0.2.2","pkg/js":"0.2.4"}' >"$TMP/cur.json" +echo '{"pkg/go":"0.2.2","pkg/js":"0.2.4","pkg/java":"0.2.0-beta.0"}' >"$TMP/prev.json" +"$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" >/dev/null 2>&1 +code=$? +assert_exit_code "removed package does not trigger release" 1 "$code" + +# 9. Pre-release to stable bump +out=$(diff_run \ + '{"pkg/java":"1.0.0"}' \ + '{"pkg/java":"1.0.0-rc.1"}') +assert_eq "pre-release to stable bump" \ + '[{"component":"pkg/java","version":"1.0.0","tag_name":"pkg/java/v1.0.0"}]' \ + "$out" + +# 10. Patch bump that overlaps a substring of the older version +out=$(diff_run \ + '{"pkg/js":"0.2.40"}' \ + '{"pkg/js":"0.2.4"}') +assert_eq "version 0.2.4 -> 0.2.40 detected as change" \ + '[{"component":"pkg/js","version":"0.2.40","tag_name":"pkg/js/v0.2.40"}]' \ + "$out" + +##################################################################### +# changelog-notes tests +##################################################################### +echo +echo "=== changelog-notes ===" cat >"$TMP/CHANGELOG.md" <<'EOF' # Changelog -## pkg/js/v0.2.3 +## Unreleased -### [v0.2.3](https://example.com/compare/pkg/js/v0.2.1...pkg/js/v0.2.3) (2026-06-04) +## [0.2.40](https://github.com/foo/bar/compare/pkg/js/v0.2.39...pkg/js/v0.2.40) (2026-06-01) -Added: -- A shiny new feature (#123) +### Added -Fixed: +* feature for 0.2.40 ([#999](https://github.com/foo/bar/issues/999)) -- A pesky bug (#124) -## pkg/js/v0.2.1 +## [0.2.4](https://github.com/foo/bar/compare/pkg/js/v0.2.3...pkg/js/v0.2.4) (2026-05-28) -### [v0.2.1](https://example.com) (2026-02-11) +> [!NOTE] +> Manual note added between version headings. -Added: +### Fixed + +* bug fix for 0.2.4 ([#283](https://github.com/foo/bar/issues/283)) -- Older stuff -EOF -##### manifest-diff ##### +## [0.2.3](https://github.com/foo/bar/compare/pkg/js/v0.2.2...pkg/js/v0.2.3) (2026-05-26) -# Only pkg/js and pkg/java changed; output sorted by component for stable compare. -diff_out="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" | jq -S -c 'sort_by(.component)')" -expected_diff='[{"component":"pkg/java","tag_name":"pkg/java/v0.3.0","version":"0.3.0"},{"component":"pkg/js","tag_name":"pkg/js/v0.2.3","version":"0.2.3"}]' -assert_eq "manifest-diff reports only changed components" "$expected_diff" "$diff_out" -# Missing previous manifest => everything is new. -all_new="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/does-not-exist.json" | jq -c 'length')" -assert_eq "manifest-diff treats missing previous manifest as all-new" "3" "$all_new" +### Miscellaneous -# No changes => empty array. -no_change="$("$PARSE" manifest-diff "$TMP/cur.json" "$TMP/cur.json")" -assert_eq "manifest-diff with identical manifests is empty" "[]" "$no_change" +* release 0.2.3 -##### changelog-notes ##### -notes="$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.3")" -expected_notes='### [v0.2.3](https://example.com/compare/pkg/js/v0.2.1...pkg/js/v0.2.3) (2026-06-04) +## [0.2.0-beta.1](https://github.com/foo/bar/compare/pkg/js/v0.2.0-beta.0...pkg/js/v0.2.0-beta.1) (2026-04-10) -Added: -- A shiny new feature (#123) +### Added + +* beta feature +EOF + +# 1. Latest version captures its own section and ### subheadings +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.40") +assert_contains "0.2.40 captures ### Added subheading" "### Added" "$out" +assert_contains "0.2.40 captures its body" "feature for 0.2.40" "$out" +if [[ "$out" != *"0.2.4 "* && "$out" != *"v0.2.4)"* ]]; then + echo " PASS - 0.2.40 does not leak into 0.2.4 section" + PASS=$((PASS + 1)) +else + echo " FAIL - 0.2.40 leaked into 0.2.4 section: $out" + FAIL=$((FAIL + 1)) +fi + +# 2. Substring collision: 0.2.4 must not capture 0.2.40 +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.4") +assert_contains "0.2.4 captures manual NOTE block" "Manual note added" "$out" +assert_contains "0.2.4 captures ### Fixed" "### Fixed" "$out" +assert_contains "0.2.4 captures bug fix line" "bug fix for 0.2.4" "$out" +if [[ "$out" == *"feature for 0.2.40"* ]]; then + echo " FAIL - 0.2.4 incorrectly captured 0.2.40 content" + FAIL=$((FAIL + 1)) +else + echo " PASS - 0.2.4 does not capture 0.2.40 content" + PASS=$((PASS + 1)) +fi + +# 3. Older version +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.3") +assert_contains "0.2.3 captures Miscellaneous section" "release 0.2.3" "$out" + +# 4. Pre-release version +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.0-beta.1") +assert_contains "pre-release 0.2.0-beta.1 captured" "beta feature" "$out" + +# 5. A version that only appears in a compare URL must not match a heading +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "0.2.39") +assert_eq "URL-only version falls back to default" "Release 0.2.39" "$out" + +# 6. Missing version falls back +out=$("$PARSE" changelog-notes "$TMP/CHANGELOG.md" "9.9.9") +assert_eq "missing version falls back" "Release 9.9.9" "$out" + +# 7. GitHub markdown alerts must be preserved verbatim +cat >"$TMP/ALERTS.md" <<'EOF' +# Changelog + +## [1.0.0](https://github.com/foo/bar/compare/v0.9.0...v1.0.0) (2026-07-01) -Fixed: +> [!WARNING] +> Breaking change: API endpoint renamed. -- A pesky bug (#124)' -assert_eq "changelog-notes extracts the matching section body" "$expected_notes" "$notes" +> [!IMPORTANT] +> You must run migrations before upgrading. -##### next-version ##### +> [!TIP] +> See the migration guide for details. -assert_eq "next-version patch" "0.2.2" "$("$PARSE" next-version 0.2.1 patch)" -assert_eq "next-version minor" "0.3.0" "$("$PARSE" next-version 0.2.1 minor)" -assert_eq "next-version major" "1.0.0" "$("$PARSE" next-version 0.2.1 major)" -assert_eq "next-version drops pre-release metadata" "0.3.0" "$("$PARSE" next-version v0.2.0-beta.2 minor)" +> [!CAUTION] +> Do not skip the manual step in section 4. -##### Summary ##### +> [!NOTE] +> This release was tested on Linux, macOS, and Windows. +### Added + +* new shiny feature + +## [0.9.0](https://github.com/foo/bar/compare/v0.8.0...v0.9.0) (2026-06-15) + +prior content +EOF + +out=$("$PARSE" changelog-notes "$TMP/ALERTS.md" "1.0.0") +for kind in WARNING IMPORTANT TIP CAUTION NOTE; do + assert_contains "alert [!${kind}] preserved" "[!${kind}]" "$out" +done +assert_contains "alert body line preserved" "Breaking change: API endpoint renamed." "$out" +assert_contains "trailing section after alerts preserved" "new shiny feature" "$out" +if [[ "$out" == *"prior content"* ]]; then + echo " FAIL - capture leaked into 0.9.0 section" + FAIL=$((FAIL + 1)) +else + echo " PASS - capture stops cleanly at next version heading" + PASS=$((PASS + 1)) +fi + +##################################################################### +# explicit / pre-release version handling +# Explicit bumps (e.g. 1.5.8-beta.1) are released verbatim, so manifest-diff +# and changelog-notes must handle pre-release identifiers without colliding +# with the matching stable version. +##################################################################### echo -echo "passed: $pass, failed: $fail" -[[ "$fail" -eq 0 ]] +echo "=== explicit / pre-release versions ===" + +# 11. Pre-release identifier bump (beta.1 -> beta.2) +out=$(diff_run \ + '{"pkg/go":"1.5.8-beta.2"}' \ + '{"pkg/go":"1.5.8-beta.1"}') +assert_eq "pre-release identifier bump (beta.1 -> beta.2)" \ + '[{"component":"pkg/go","version":"1.5.8-beta.2","tag_name":"pkg/go/v1.5.8-beta.2"}]' \ + "$out" + +# 12. Stable to pre-release of the next version +out=$(diff_run \ + '{"pkg/js":"1.5.8-beta.1"}' \ + '{"pkg/js":"1.5.7"}') +assert_eq "stable -> pre-release of next version" \ + '[{"component":"pkg/js","version":"1.5.8-beta.1","tag_name":"pkg/js/v1.5.8-beta.1"}]' \ + "$out" + +# 13. Numeric ordering is irrelevant (string compare): beta.9 -> beta.10 +out=$(diff_run \ + '{"pkg/java":"2.0.0-beta.10"}' \ + '{"pkg/java":"2.0.0-beta.9"}') +assert_eq "beta.9 -> beta.10 detected (string compare)" \ + '[{"component":"pkg/java","version":"2.0.0-beta.10","tag_name":"pkg/java/v2.0.0-beta.10"}]' \ + "$out" + +# 14. Same pre-release version unchanged; no release +echo '{"pkg/go":"3.0.0-rc.1"}' >"$TMP/cur.json" +cp "$TMP/cur.json" "$TMP/prev.json" +"$PARSE" manifest-diff "$TMP/cur.json" "$TMP/prev.json" >/dev/null 2>&1 +code=$? +assert_exit_code "unchanged pre-release does not trigger release" 1 "$code" + +cat >"$TMP/PRE.md" <<'EOF' +# Changelog + +## [1.5.8](https://github.com/foo/bar/compare/v1.5.8-beta.2...v1.5.8) (2026-06-04) + +### Fixed + +* stable fix for 1.5.8 + +## [1.5.8-beta.2](https://github.com/foo/bar/compare/v1.5.8-beta.1...v1.5.8-beta.2) (2026-06-03) +### Added + +* second beta feature + +## [1.5.8-beta.1](https://github.com/foo/bar/compare/v1.5.7...v1.5.8-beta.1) (2026-06-02) + +### Added + +* first beta feature + +## [1.0.0-alpha.10](https://github.com/foo/bar/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2026-05-01) + +### Added + +* alpha ten feature +EOF + +# 15. Exact pre-release match captures its own body, no leak from newer beta +out=$("$PARSE" changelog-notes "$TMP/PRE.md" "1.5.8-beta.1") +assert_contains "beta.1 captures its own body" "first beta feature" "$out" +if [[ "$out" == *"second beta feature"* ]]; then + echo " FAIL - beta.1 leaked beta.2 content" + FAIL=$((FAIL + 1)) +else + echo " PASS - beta.1 does not leak beta.2 content" + PASS=$((PASS + 1)) +fi + +# 16. Newer beta captures its own body, stops before older beta +out=$("$PARSE" changelog-notes "$TMP/PRE.md" "1.5.8-beta.2") +assert_contains "beta.2 captures its own body" "second beta feature" "$out" +if [[ "$out" == *"first beta feature"* ]]; then + echo " FAIL - beta.2 leaked beta.1 content" + FAIL=$((FAIL + 1)) +else + echo " PASS - beta.2 does not leak beta.1 content" + PASS=$((PASS + 1)) +fi + +# 17. Stable 1.5.8 must not match the 1.5.8-beta.* headings (boundary on '-') +out=$("$PARSE" changelog-notes "$TMP/PRE.md" "1.5.8") +assert_contains "stable 1.5.8 captures stable body" "stable fix for 1.5.8" "$out" +if [[ "$out" == *"beta feature"* ]]; then + echo " FAIL - stable 1.5.8 incorrectly captured a beta section" + FAIL=$((FAIL + 1)) +else + echo " PASS - stable 1.5.8 does not capture beta sections" + PASS=$((PASS + 1)) +fi + +# 18. Double-digit pre-release identifier (alpha.10) +out=$("$PARSE" changelog-notes "$TMP/PRE.md" "1.0.0-alpha.10") +assert_contains "alpha.10 captured exactly" "alpha ten feature" "$out" + +# 19. Partial pre-release prefix must not match (1.5.8-beta -> fallback) +out=$("$PARSE" changelog-notes "$TMP/PRE.md" "1.5.8-beta") +assert_eq "partial pre-release prefix falls back" "Release 1.5.8-beta" "$out" + +##################################################################### +# summary +##################################################################### +echo +echo "================================" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo "================================" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi \ No newline at end of file diff --git a/pkg/java/build.gradle b/pkg/java/build.gradle index 114ed2fb..80c23e5f 100644 --- a/pkg/java/build.gradle +++ b/pkg/java/build.gradle @@ -19,7 +19,7 @@ plugins { apply from: 'publish.gradle' group = 'dev.openfga' -version = 'v0.2.0-beta.0' +version = '0.2.0-beta.2' // x-release-please-version repositories { mavenCentral() diff --git a/release-please-config.json b/release-please-config.json index 4ca98738..20dedba4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,23 +1,45 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "simple", + "separate-pull-requests": true, + "pull-request-title-pattern": "release: ${component} v${version}", + "skip-github-release": true, + "include-component-in-tag": true, + "tag-separator": "/", + "changelog-path": "CHANGELOG.md", + "changelog-type": "default", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelog-sections": [ + { "type": "feat", "section": "Added", "hidden": false }, + { "type": "fix", "section": "Fixed", "hidden": false }, + { "type": "perf", "section": "Changed", "hidden": false }, + { "type": "refactor", "section": "Changed", "hidden": false }, + { "type": "revert", "section": "Removed", "hidden": false }, + { "type": "docs", "section": "Documentation", "hidden": false }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true }, + { "type": "release", "section": "Miscellaneous", "hidden": true } + ], "packages": { "pkg/go": { + "release-type": "go", "component": "pkg/go", - "changelog-path": "pkg/go/CHANGELOG.md" + "package-name": "pkg/go" }, "pkg/js": { + "release-type": "node", "component": "pkg/js", - "changelog-path": "pkg/js/CHANGELOG.md" + "package-name": "@openfga/syntax-transformer" }, "pkg/java": { + "release-type": "java", "component": "pkg/java", - "changelog-path": "pkg/java/CHANGELOG.md" + "package-name": "openfga-language", + "extra-files": ["build.gradle"] } - }, - "separate-pull-requests": true, - "include-component-in-tag": true, - "tag-separator": "/", - "bootstrap-sha": "" + } } + +