From 2f9c0a58eba1b725dd91f951161001941bb56655 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Tue, 7 Jul 2026 21:04:48 +0530 Subject: [PATCH 1/3] feat(ci): auto-open downstream bump PRs on package release When a package release is published (undrafted) in this repo, open a version-bump PR against the relevant downstream consumers, authored and GPG-signed by the same openfga-releaser-bot identity used by release-please. - pkg/js -> openfga/vscode-ext (@openfga/syntax-transformer) - pkg/java -> openfga/intellij-plugin (dev.openfga:openfga-language) - pkg/go -> openfga/cli, openfga/terraform-provider-openfga Adds scripts/downstream-bump.sh (per-ecosystem edit logic, idempotent) with unit tests, and reusable-downstream-bump.yaml which mints a target-scoped App token, applies the bump, and opens the PR. Chained after each finalize-release job so it is not subject to the GITHUB_TOKEN no-retrigger rule. --- .github/workflows/pkg-go-release.yaml | 25 ++ .github/workflows/pkg-java-release.yaml | 24 ++ .github/workflows/pkg-js-release.yaml | 24 ++ .../workflows/reusable-downstream-bump.yaml | 164 +++++++++++++ .github/workflows/scripts/downstream-bump.sh | 90 ++++++++ .../workflows/scripts/downstream-bump.test.sh | 215 ++++++++++++++++++ .github/workflows/test-release-scripts.yml | 6 + 7 files changed, 548 insertions(+) create mode 100644 .github/workflows/reusable-downstream-bump.yaml create mode 100755 .github/workflows/scripts/downstream-bump.sh create mode 100755 .github/workflows/scripts/downstream-bump.test.sh diff --git a/.github/workflows/pkg-go-release.yaml b/.github/workflows/pkg-go-release.yaml index 2c436783..57dac934 100644 --- a/.github/workflows/pkg-go-release.yaml +++ b/.github/workflows/pkg-go-release.yaml @@ -40,3 +40,28 @@ jobs: sleep 6 done gh release edit "$TAG" --repo "$REPO" --draft=false + + # Once the release is published, open a bump PR against downstream consumers + # of github.com/openfga/language/pkg/go. + downstream-bump: + needs: [finalize-release] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + strategy: + fail-fast: false + matrix: + target-repo: + - openfga/cli + - openfga/terraform-provider-openfga + uses: ./.github/workflows/reusable-downstream-bump.yaml + with: + target-repo: ${{ matrix.target-repo }} + ecosystem: go + version: ${{ github.ref_name }} + artifact: openfga-language + 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/pkg-java-release.yaml b/.github/workflows/pkg-java-release.yaml index 41dfa38c..d1f9fac4 100644 --- a/.github/workflows/pkg-java-release.yaml +++ b/.github/workflows/pkg-java-release.yaml @@ -121,3 +121,27 @@ jobs: sleep 6 done gh release edit "$TAG" --repo "$REPO" --draft=false + + # Once the release is published, open a bump PR against downstream consumers + # of dev.openfga:openfga-language. + downstream-bump: + needs: [finalize-release] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + strategy: + fail-fast: false + matrix: + target-repo: + - openfga/intellij-plugin + uses: ./.github/workflows/reusable-downstream-bump.yaml + with: + target-repo: ${{ matrix.target-repo }} + ecosystem: java + version: ${{ github.ref_name }} + artifact: openfga-language + 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/pkg-js-release.yaml b/.github/workflows/pkg-js-release.yaml index 54f11c72..892d9b65 100644 --- a/.github/workflows/pkg-js-release.yaml +++ b/.github/workflows/pkg-js-release.yaml @@ -107,3 +107,27 @@ jobs: sleep 6 done gh release edit "$TAG" --repo "$REPO" --draft=false + + # Once the release is published, open a bump PR against downstream consumers + # of @openfga/syntax-transformer. + downstream-bump: + needs: [finalize-release] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: read + strategy: + fail-fast: false + matrix: + target-repo: + - openfga/vscode-ext + uses: ./.github/workflows/reusable-downstream-bump.yaml + with: + target-repo: ${{ matrix.target-repo }} + ecosystem: js + version: ${{ github.ref_name }} + artifact: syntax-transformer + 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-downstream-bump.yaml b/.github/workflows/reusable-downstream-bump.yaml new file mode 100644 index 00000000..43149129 --- /dev/null +++ b/.github/workflows/reusable-downstream-bump.yaml @@ -0,0 +1,164 @@ +name: reusable-downstream-bump + +# Opens a "bump openfga-language" PR against a downstream repo, authored and +# GPG-signed by the same openfga-releaser-bot identity used by release-please. +# +# Invoked (via a matrix) from the pkg-*-release workflows once a release has +# been published, so downstream consumers pick up the new version automatically. +# +# NOTE: the releaser GitHub App must be installed on each target repo with +# `contents: write` + `pull-requests: write`, otherwise token minting fails. + +on: + workflow_call: + inputs: + target-repo: + description: "Downstream repo to bump, e.g. openfga/cli" + type: string + required: true + ecosystem: + description: "js | java | go" + type: string + required: true + version: + description: "Released version (plain semver or a tag ref; normalised)" + type: string + required: true + artifact: + description: "Human name of the bumped artifact, for PR text" + type: string + required: true + 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 + +jobs: + bump: + runs-on: ubuntu-latest + steps: + - name: Normalise version + id: ver + env: + RAW: ${{ inputs.version }} + run: | + set -euo pipefail + v="${RAW##*/}" # strip any pkg// prefix + v="${v#v}" # strip leading v + echo "version=${v}" >> "$GITHUB_OUTPUT" + + - name: Split target owner/repo + id: repo + env: + TARGET: ${{ inputs.target-repo }} + run: | + set -euo pipefail + echo "owner=${TARGET%%/*}" >> "$GITHUB_OUTPUT" + echo "name=${TARGET##*/}" >> "$GITHUB_OUTPUT" + + # Token scoped to the *target* repo so the bot can push a branch + open a PR. + - name: Generate GitHub App token (target repo) + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.RELEASER_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }} + owner: ${{ steps.repo.outputs.owner }} + repositories: ${{ steps.repo.outputs.name }} + + # The bump script lives in *this* repo; check it out separately so we can + # run it against the downstream checkout regardless of downstream layout. + - name: Checkout scripts (this repo) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: _scripts + sparse-checkout: .github/workflows/scripts + sparse-checkout-cone-mode: false + + - name: Checkout target repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ${{ inputs.target-repo }} + token: ${{ steps.app-token.outputs.token }} + path: target + fetch-depth: 0 + + - name: Set up Node (js) + if: inputs.ecosystem == 'js' + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24.x' + registry-url: "https://registry.npmjs.org" + + - name: Set up Go (go) + if: inputs.ecosystem == 'go' + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version: ">=1.25.0" + check-latest: true + + - name: Import GPG key and enable signed commits + id: import-gpg + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + workdir: target + + - name: Apply bump and open PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + ECOSYSTEM: ${{ inputs.ecosystem }} + VERSION: ${{ steps.ver.outputs.version }} + ARTIFACT: ${{ inputs.artifact }} + TARGET_REPO: ${{ inputs.target-repo }} + BOT_EMAIL: ${{ steps.import-gpg.outputs.email }} + working-directory: target + run: | + set -euo pipefail + + git config user.name "openfga-releaser-bot" + git config user.email "${BOT_EMAIL}" + git config commit.gpgSign true + + BRANCH="chore/bump-${ARTIFACT}-${VERSION}" + + # Idempotency: if a bump PR for this exact version is already open, stop. + if gh pr list --repo "${TARGET_REPO}" --state open --head "${BRANCH}" \ + --json number --jq '.[0].number' | grep -q '[0-9]'; then + echo "PR for ${BRANCH} already open; nothing to do." + exit 0 + fi + + git checkout -b "${BRANCH}" + + # Apply the edit (script checked out in the sibling _scripts dir). + bash "${GITHUB_WORKSPACE}/_scripts/.github/workflows/scripts/downstream-bump.sh" \ + "${ECOSYSTEM}" "${VERSION}" + + if git diff --quiet; then + echo "No changes after bump (already up to date); nothing to do." + exit 0 + fi + + TITLE="chore(deps): bump ${ARTIFACT} to ${VERSION}" + git commit -am "${TITLE}" + git push -f origin "${BRANCH}" + + BODY="$(printf 'Automated bump of \x60%s\x60 to \x60%s\x60, published from openfga/language.\n\nOpened by openfga-releaser-bot as part of the language release flow.' "${ARTIFACT}" "${VERSION}")" + + gh pr create --repo "${TARGET_REPO}" \ + --head "${BRANCH}" \ + --base "$(gh repo view "${TARGET_REPO}" --json defaultBranchRef --jq '.defaultBranchRef.name')" \ + --title "${TITLE}" \ + --body "${BODY}" diff --git a/.github/workflows/scripts/downstream-bump.sh b/.github/workflows/scripts/downstream-bump.sh new file mode 100755 index 00000000..685640ca --- /dev/null +++ b/.github/workflows/scripts/downstream-bump.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Applies a version bump of an openfga-language artifact inside a checked-out +# downstream repository. Kept out of the workflow YAML so the edit logic can be +# unit-tested independently (mirrors parse-release.sh). +# +# Run from the root of the *downstream* repo checkout. +# +# Usage: +# downstream-bump.sh js # bumps @openfga/syntax-transformer in +# # client/ and server/ workspaces +# downstream-bump.sh java # bumps dev.openfga:openfga-language in +# # build.gradle.kts +# downstream-bump.sh go # bumps github.com/openfga/language/pkg/go +# # in the module containing go.mod +# +# is the plain semver (e.g. 0.2.2), without a leading "v". +# +# Requires the relevant toolchain on PATH: npm for js, go for go. java needs +# nothing beyond coreutils. +set -euo pipefail + +ecosystem="${1:?ecosystem required: js|java|go}" +version="${2:?version required}" + +# Normalise: callers may pass a tag ref like "pkg/js/v0.2.2" or "v0.2.2". +version="${version##*/}" # strip any "pkg//" prefix +version="${version#v}" # strip a leading "v" + +case "${ecosystem}" in + js) + # Update each workspace that declares the dependency. npm rewrites both + # package.json (to ^) and package-lock.json (with the correct + # integrity hash + refreshed transitive deps), matching the manual PRs. + found=0 + for dir in client server; do + if [[ -f "${dir}/package.json" ]] \ + && grep -q '@openfga/syntax-transformer' "${dir}/package.json"; then + echo "Bumping @openfga/syntax-transformer in ${dir}/" + ( cd "${dir}" && npm install "@openfga/syntax-transformer@^${version}" ) + found=1 + fi + done + if [[ "${found}" -eq 0 ]]; then + echo "No workspace declares @openfga/syntax-transformer" >&2 + exit 1 + fi + ;; + + java) + file="build.gradle.kts" + [[ -f "${file}" ]] || file="build.gradle" + if [[ ! -f "${file}" ]]; then + echo "No build.gradle(.kts) found" >&2 + exit 1 + fi + if ! grep -q 'dev\.openfga:openfga-language:' "${file}"; then + echo "${file} does not depend on dev.openfga:openfga-language" >&2 + exit 1 + fi + echo "Bumping dev.openfga:openfga-language to ${version} in ${file}" + # Replace the version segment of the coordinate, whatever it currently is + # (e.g. 0.2.0-beta.2 -> 0.2.1). Only touch chars up to the closing quote. + # perl for portability (GNU vs BSD sed differ on -i / backrefs). + perl -i -pe "s/(dev\.openfga:openfga-language:)[^\"]+/\${1}${version}/g" "${file}" + ;; + + go) + module="github.com/openfga/language/pkg/go" + if [[ ! -f "go.mod" ]]; then + echo "No go.mod in current directory" >&2 + exit 1 + fi + if ! grep -q "${module}" go.mod; then + echo "go.mod does not require ${module}" >&2 + exit 1 + fi + echo "Bumping ${module} to v${version}" + # proxy.golang.org first; fall back to direct if the tag isn't indexed yet. + export GOPROXY="proxy.golang.org,direct" + go get "${module}@v${version}" + go mod tidy + ;; + + *) + echo "Unknown ecosystem: ${ecosystem} (expected js|java|go)" >&2 + exit 1 + ;; +esac + +echo "Bump applied for ${ecosystem} -> ${version}" diff --git a/.github/workflows/scripts/downstream-bump.test.sh b/.github/workflows/scripts/downstream-bump.test.sh new file mode 100755 index 00000000..8635c2b9 --- /dev/null +++ b/.github/workflows/scripts/downstream-bump.test.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Tests for downstream-bump.sh +# Run: bash .github/workflows/scripts/downstream-bump.test.sh +# +# The java path is pure text substitution and is exercised end-to-end. The js +# and go paths need a toolchain + network to actually resolve a version, so we +# only assert their hermetic behaviour here: version normalisation and the +# guard/error paths (missing files, missing dependency). + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUMP="$SCRIPT_DIR/downstream-bump.sh" + +PASS=0 +FAIL=0 + +assert_eq() { + local name="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo " PASS - $name" + PASS=$((PASS + 1)) + else + echo " FAIL - $name" + echo " expected: $expected" + echo " actual: $actual" + FAIL=$((FAIL + 1)) + fi +} + +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 +} + +assert_not_contains() { + local name="$1" needle="$2" haystack="$3" + if [[ "$haystack" != *"$needle"* ]]; then + echo " PASS - $name" + PASS=$((PASS + 1)) + else + echo " FAIL - $name" + echo " expected NOT to contain: $needle" + echo " actual: $haystack" + FAIL=$((FAIL + 1)) + fi +} + +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 +} + +# Run the script inside a throwaway dir, echoing its output. Usage: +# out=$(run_in ); code=$? +run_in() { + local dir="$1"; shift + ( cd "$dir" && bash "$BUMP" "$@" ) +} + +fresh() { mktemp -d; } + +##################################################################### +# java: full end-to-end text substitution +##################################################################### +echo +echo "=== java ===" + +# 1. beta coordinate -> stable, only the language line changes +d=$(fresh) +cat >"$d/build.gradle.kts" <<'EOF' +dependencies { + implementation("org.apache.commons:commons-lang3:3.20.0") + implementation("dev.openfga:openfga-language:0.2.0-beta.2") + implementation("com.diffplug.spotless:spotless-plugin-gradle:8.8.0") +} +EOF +run_in "$d" java 0.2.1 >/dev/null 2>&1 +assert_contains "java bumps language coordinate" \ + 'dev.openfga:openfga-language:0.2.1' "$(cat "$d/build.gradle.kts")" +assert_contains "java leaves other deps untouched (commons)" \ + 'commons-lang3:3.20.0' "$(cat "$d/build.gradle.kts")" +assert_contains "java leaves other deps untouched (spotless)" \ + 'spotless-plugin-gradle:8.8.0' "$(cat "$d/build.gradle.kts")" +assert_not_contains "old java version gone" \ + '0.2.0-beta.2' "$(cat "$d/build.gradle.kts")" +rm -rf "$d" + +# 2. version normalisation: full tag ref is accepted +d=$(fresh) +echo 'implementation("dev.openfga:openfga-language:0.2.0")' >"$d/build.gradle.kts" +run_in "$d" java pkg/java/v0.3.1 >/dev/null 2>&1 +assert_contains "java accepts pkg/java/vX.Y.Z tag ref" \ + 'dev.openfga:openfga-language:0.3.1' "$(cat "$d/build.gradle.kts")" +rm -rf "$d" + +# 3. leading-v normalisation +d=$(fresh) +echo 'implementation("dev.openfga:openfga-language:0.2.0")' >"$d/build.gradle.kts" +run_in "$d" java v0.4.0 >/dev/null 2>&1 +assert_contains "java strips leading v" \ + 'dev.openfga:openfga-language:0.4.0' "$(cat "$d/build.gradle.kts")" +rm -rf "$d" + +# 4. groovy build.gradle (no .kts) is picked up +d=$(fresh) +echo 'implementation "dev.openfga:openfga-language:0.2.0"' >"$d/build.gradle" +run_in "$d" java 0.2.1 >/dev/null 2>&1 +assert_contains "java falls back to build.gradle" \ + 'dev.openfga:openfga-language:0.2.1' "$(cat "$d/build.gradle")" +rm -rf "$d" + +# 5. missing build file -> error exit +d=$(fresh) +run_in "$d" java 0.2.1 >/dev/null 2>&1 +assert_exit_code "java missing build file fails" 1 $? +rm -rf "$d" + +# 6. build file without the language dep -> error exit +d=$(fresh) +echo 'implementation("org.other:thing:1.0.0")' >"$d/build.gradle.kts" +run_in "$d" java 0.2.1 >/dev/null 2>&1 +assert_exit_code "java without language dep fails" 1 $? +rm -rf "$d" + +##################################################################### +# js: guard/error paths (npm resolution needs network, not tested here) +##################################################################### +echo +echo "=== js (guards) ===" + +# 7. no workspace declares the dep -> error before touching npm +d=$(fresh) +mkdir -p "$d/client" +echo '{"name":"client","dependencies":{"other":"^1.0.0"}}' >"$d/client/package.json" +err=$(run_in "$d" js 0.2.2 2>&1); code=$? +assert_exit_code "js no matching workspace fails" 1 "$code" +assert_contains "js error mentions syntax-transformer" \ + "syntax-transformer" "$err" +rm -rf "$d" + +# 8. no package.json at all -> error +d=$(fresh) +run_in "$d" js 0.2.2 >/dev/null 2>&1 +assert_exit_code "js no package.json fails" 1 $? +rm -rf "$d" + +##################################################################### +# go: guard/error paths (go get needs network, not tested here) +##################################################################### +echo +echo "=== go (guards) ===" + +# 9. no go.mod -> error +d=$(fresh) +run_in "$d" go 0.3.1 >/dev/null 2>&1 +assert_exit_code "go no go.mod fails" 1 $? +rm -rf "$d" + +# 10. go.mod without the language module -> error before touching go +d=$(fresh) +cat >"$d/go.mod" <<'EOF' +module example.com/foo + +go 1.25.0 + +require github.com/other/thing v1.0.0 +EOF +err=$(run_in "$d" go 0.3.1 2>&1); code=$? +assert_exit_code "go without language module fails" 1 "$code" +assert_contains "go error mentions the module path" \ + "github.com/openfga/language/pkg/go" "$err" +rm -rf "$d" + +##################################################################### +# generic +##################################################################### +echo +echo "=== generic ===" + +# 11. unknown ecosystem -> error +d=$(fresh) +err=$(run_in "$d" rust 1.0.0 2>&1); code=$? +assert_exit_code "unknown ecosystem fails" 1 "$code" +assert_contains "unknown ecosystem message" "Unknown ecosystem" "$err" +rm -rf "$d" + +##################################################################### +# summary +##################################################################### +echo +echo "================================" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo "================================" + +if [[ "$FAIL" -gt 0 ]]; then + exit 1 +fi diff --git a/.github/workflows/test-release-scripts.yml b/.github/workflows/test-release-scripts.yml index a1b8cd07..f0046b28 100644 --- a/.github/workflows/test-release-scripts.yml +++ b/.github/workflows/test-release-scripts.yml @@ -5,12 +5,16 @@ on: paths: - .github/workflows/scripts/parse-release.sh - .github/workflows/scripts/parse-release.test.sh + - .github/workflows/scripts/downstream-bump.sh + - .github/workflows/scripts/downstream-bump.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 + - .github/workflows/scripts/downstream-bump.sh + - .github/workflows/scripts/downstream-bump.test.sh permissions: contents: read @@ -22,4 +26,6 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run parse-release tests run: bash .github/workflows/scripts/parse-release.test.sh + - name: Run downstream-bump tests + run: bash .github/workflows/scripts/downstream-bump.test.sh From 7edfd39e1d9f086e65e5a9725de7e8a55fcd4b92 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 16 Jul 2026 14:43:32 +0530 Subject: [PATCH 2/3] fix(ci): address review feedback on downstream bump flow --- .github/workflows/reusable-downstream-bump.yaml | 15 ++++++++++----- .github/workflows/scripts/downstream-bump.sh | 9 +++++---- .github/workflows/scripts/downstream-bump.test.sh | 8 ++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/reusable-downstream-bump.yaml b/.github/workflows/reusable-downstream-bump.yaml index 43149129..d8aecf2a 100644 --- a/.github/workflows/reusable-downstream-bump.yaml +++ b/.github/workflows/reusable-downstream-bump.yaml @@ -102,8 +102,10 @@ jobs: if: inputs.ecosystem == 'go' uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: - go-version: ">=1.25.0" - check-latest: true + # Use the toolchain the downstream repo declares, so go.mod/go.sum + # churn from a newer Go doesn't leak into the bump PR. + go-version-file: target/go.mod + cache-dependency-path: target/go.sum - name: Import GPG key and enable signed commits id: import-gpg @@ -146,14 +148,17 @@ jobs: bash "${GITHUB_WORKSPACE}/_scripts/.github/workflows/scripts/downstream-bump.sh" \ "${ECOSYSTEM}" "${VERSION}" - if git diff --quiet; then + # Stage everything (gitignore still applies) so newly created files + # (e.g. a first package-lock.json) make it into the PR too. + git add -A + if git diff --cached --quiet; then echo "No changes after bump (already up to date); nothing to do." exit 0 fi TITLE="chore(deps): bump ${ARTIFACT} to ${VERSION}" - git commit -am "${TITLE}" - git push -f origin "${BRANCH}" + git commit -m "${TITLE}" + git push --force-with-lease origin "${BRANCH}" BODY="$(printf 'Automated bump of \x60%s\x60 to \x60%s\x60, published from openfga/language.\n\nOpened by openfga-releaser-bot as part of the language release flow.' "${ARTIFACT}" "${VERSION}")" diff --git a/.github/workflows/scripts/downstream-bump.sh b/.github/workflows/scripts/downstream-bump.sh index 685640ca..f9f2e94a 100755 --- a/.github/workflows/scripts/downstream-bump.sh +++ b/.github/workflows/scripts/downstream-bump.sh @@ -15,8 +15,8 @@ # # is the plain semver (e.g. 0.2.2), without a leading "v". # -# Requires the relevant toolchain on PATH: npm for js, go for go. java needs -# nothing beyond coreutils. +# Requires the relevant toolchain on PATH: npm for js, go for go. java uses +# perl for the in-place build.gradle(.kts) edit. set -euo pipefail ecosystem="${1:?ecosystem required: js|java|go}" @@ -59,9 +59,10 @@ case "${ecosystem}" in fi echo "Bumping dev.openfga:openfga-language to ${version} in ${file}" # Replace the version segment of the coordinate, whatever it currently is - # (e.g. 0.2.0-beta.2 -> 0.2.1). Only touch chars up to the closing quote. + # (e.g. 0.2.0-beta.2 -> 0.2.1). Only touch chars up to the closing quote, + # which may be single (groovy) or double (groovy/kts). # perl for portability (GNU vs BSD sed differ on -i / backrefs). - perl -i -pe "s/(dev\.openfga:openfga-language:)[^\"]+/\${1}${version}/g" "${file}" + perl -i -pe "s/(dev\.openfga:openfga-language:)[^\"']+([\"'])/\${1}${version}\${2}/g" "${file}" ;; go) diff --git a/.github/workflows/scripts/downstream-bump.test.sh b/.github/workflows/scripts/downstream-bump.test.sh index 8635c2b9..13893a29 100755 --- a/.github/workflows/scripts/downstream-bump.test.sh +++ b/.github/workflows/scripts/downstream-bump.test.sh @@ -126,6 +126,14 @@ assert_contains "java falls back to build.gradle" \ 'dev.openfga:openfga-language:0.2.1' "$(cat "$d/build.gradle")" rm -rf "$d" +# 4b. groovy build.gradle with single quotes keeps its closing quote intact +d=$(fresh) +echo "implementation 'dev.openfga:openfga-language:0.2.0'" >"$d/build.gradle" +run_in "$d" java 0.2.1 >/dev/null 2>&1 +assert_contains "java handles single-quoted build.gradle" \ + "'dev.openfga:openfga-language:0.2.1'" "$(cat "$d/build.gradle")" +rm -rf "$d" + # 5. missing build file -> error exit d=$(fresh) run_in "$d" java 0.2.1 >/dev/null 2>&1 From 5a80ec10412b09d83891e958ddd2beea2e50eabd Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 16 Jul 2026 17:34:59 +0530 Subject: [PATCH 3/3] fix(ci): keep app token and gpg key away from untrusted toolchain steps --- .../workflows/reusable-downstream-bump.yaml | 87 ++++++++++++++----- 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/.github/workflows/reusable-downstream-bump.yaml b/.github/workflows/reusable-downstream-bump.yaml index d8aecf2a..cf13cbc7 100644 --- a/.github/workflows/reusable-downstream-bump.yaml +++ b/.github/workflows/reusable-downstream-bump.yaml @@ -82,7 +82,11 @@ jobs: path: _scripts sparse-checkout: .github/workflows/scripts sparse-checkout-cone-mode: false + persist-credentials: false + # persist-credentials: false so the App token never lands in .git/config, + # where npm lifecycle scripts could read it. Pushes authenticate + # explicitly in the final step instead. - name: Checkout target repo uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -90,6 +94,7 @@ jobs: token: ${{ steps.app-token.outputs.token }} path: target fetch-depth: 0 + persist-credentials: false - name: Set up Node (js) if: inputs.ecosystem == 'js' @@ -107,41 +112,39 @@ jobs: go-version-file: target/go.mod cache-dependency-path: target/go.sum - - name: Import GPG key and enable signed commits - id: import-gpg - uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - workdir: target - - - name: Apply bump and open PR + - name: Check for existing bump PR + id: precheck env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - ECOSYSTEM: ${{ inputs.ecosystem }} - VERSION: ${{ steps.ver.outputs.version }} - ARTIFACT: ${{ inputs.artifact }} TARGET_REPO: ${{ inputs.target-repo }} - BOT_EMAIL: ${{ steps.import-gpg.outputs.email }} - working-directory: target + ARTIFACT: ${{ inputs.artifact }} + VERSION: ${{ steps.ver.outputs.version }} run: | set -euo pipefail - - git config user.name "openfga-releaser-bot" - git config user.email "${BOT_EMAIL}" - git config commit.gpgSign true - BRANCH="chore/bump-${ARTIFACT}-${VERSION}" - + echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" # Idempotency: if a bump PR for this exact version is already open, stop. if gh pr list --repo "${TARGET_REPO}" --state open --head "${BRANCH}" \ --json number --jq '.[0].number' | grep -q '[0-9]'; then echo "PR for ${BRANCH} already open; nothing to do." - exit 0 + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" fi + # Deliberately no token in the environment: the js path runs npm install, + # which executes third-party lifecycle scripts. + - name: Apply bump + id: bump + if: steps.precheck.outputs.skip != 'true' + env: + ECOSYSTEM: ${{ inputs.ecosystem }} + VERSION: ${{ steps.ver.outputs.version }} + BRANCH: ${{ steps.precheck.outputs.branch }} + working-directory: target + run: | + set -euo pipefail + git checkout -b "${BRANCH}" # Apply the edit (script checked out in the sibling _scripts dir). @@ -153,11 +156,47 @@ jobs: git add -A if git diff --cached --quiet; then echo "No changes after bump (already up to date); nothing to do." - exit 0 + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" fi + # Imported only after the untrusted toolchain work above has finished, + # so npm lifecycle scripts never run alongside the loaded key. + - name: Import GPG key and enable signed commits + id: import-gpg + if: steps.bump.outputs.changed == 'true' + uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + workdir: target + + - name: Commit, push and open PR + if: steps.bump.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.ver.outputs.version }} + ARTIFACT: ${{ inputs.artifact }} + TARGET_REPO: ${{ inputs.target-repo }} + BRANCH: ${{ steps.precheck.outputs.branch }} + BOT_EMAIL: ${{ steps.import-gpg.outputs.email }} + working-directory: target + run: | + set -euo pipefail + + git config user.name "openfga-releaser-bot" + git config user.email "${BOT_EMAIL}" + git config commit.gpgSign true + TITLE="chore(deps): bump ${ARTIFACT} to ${VERSION}" git commit -m "${TITLE}" + + # Checkout ran with persist-credentials: false; authenticate the + # push with the short-lived App token here instead. + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${TARGET_REPO}.git" git push --force-with-lease origin "${BRANCH}" BODY="$(printf 'Automated bump of \x60%s\x60 to \x60%s\x60, published from openfga/language.\n\nOpened by openfga-releaser-bot as part of the language release flow.' "${ARTIFACT}" "${VERSION}")"