diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 476d6e5..f568314 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -47,7 +47,7 @@ on: type: string required: true version-file: - description: File holding the version (one line, without the tag prefix), or a package.json (its `version`). + description: File holding the version (one line, without the tag prefix), a package.json (its `version`) or a pyproject.toml (its `[project] version`). type: string required: true tag-prefix: diff --git a/.github/workflows/release-intent.yml b/.github/workflows/release-intent.yml index 63f503a..6950611 100644 --- a/.github/workflows/release-intent.yml +++ b/.github/workflows/release-intent.yml @@ -15,7 +15,7 @@ on: workflow_call: inputs: version-file: - description: File holding the version (one line, without the tag prefix), or a package.json (its `version`). + description: File holding the version (one line, without the tag prefix), a package.json (its `version`) or a pyproject.toml (its `[project] version`). type: string required: true tag-prefix: diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 27221e3..2bd2bf2 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -19,7 +19,7 @@ on: workflow_call: inputs: version-file: - description: File holding the version (one line, without the tag prefix), or a package.json (its `version`). + description: File holding the version (one line, without the tag prefix), a package.json (its `version`) or a pyproject.toml (its `[project] version`). type: string required: true tag-prefix: @@ -35,6 +35,10 @@ on: missing; never force-pushed. type: string default: "" + github-release: + description: Create the GitHub release of the tag (generated notes) after the tag. + type: boolean + default: false harness-ref: description: Ref of IronFlock/workflows the harness scripts are taken from. type: string @@ -89,9 +93,13 @@ jobs: COMMIT_TITLE: ${{ github.event.head_commit.message }} SHA: ${{ github.sha }} run: '"$HARNESS/scripts/release.sh" mode' + # `released`: a re-run after the tag; the steps after the tag catch up + # (each passes when its work is already done). - name: release App token (contents of this repository) id: app - if: steps.version.outputs.mode == 'release' + if: >- + steps.version.outputs.mode == 'release' || (steps.version.outputs.mode == 'released' && + (inputs.promote-branch != '' || inputs.github-release)) uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} @@ -110,10 +118,26 @@ jobs: MESSAGE=$(head -n1 <<<"$MESSAGE") MESSAGE=${MESSAGE% (#*)} "$HARNESS/scripts/release.sh" tag - name: fast-forward ${{ inputs.promote-branch }} - if: steps.version.outputs.mode == 'release' && inputs.promote-branch != '' + if: >- + (steps.version.outputs.mode == 'release' || steps.version.outputs.mode == 'released') && + inputs.promote-branch != '' env: GH_TOKEN: ${{ steps.app.outputs.token }} REPO: ${{ github.repository }} BRANCH: ${{ inputs.promote-branch }} SHA: ${{ github.sha }} run: '"$HARNESS/scripts/release.sh" promote' + - name: GitHub release ${{ steps.version.outputs.tag }} + if: >- + (steps.version.outputs.mode == 'release' || steps.version.outputs.mode == 'released') && + inputs.github-release + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ steps.version.outputs.tag }} + run: | + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + echo "the GitHub release $TAG exists" + else + gh release create "$TAG" --repo "$REPO" --verify-tag --title "$TAG" --generate-notes + fi diff --git a/README.md b/README.md index be3ae29..780b5a5 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,9 @@ With `promote-branch` set, the tag job then fast-forwards that branch to the released commit (never a force push), for a hosting service that deploys from a branch: only release merges reach it. Protect that branch with a ruleset whose only bypass actor is the release App. +With `github-release: true` it also creates the GitHub release of the tag +(generated notes), with the same token, so callers need no write rights. Both +steps catch up on a re-run that finds the tag already set. ## Pull requests from a laptop diff --git a/scripts/pr.sh b/scripts/pr.sh index ddbbe55..7dccd82 100755 --- a/scripts/pr.sh +++ b/scripts/pr.sh @@ -30,8 +30,9 @@ wait_s=${PR_WAIT_SECONDS:-20} die() { echo "$*" >&2; exit 1; } -# version []: a package.json's `version`, otherwise the content -# without whitespace (same rule as release.sh). +# version []: a package.json's `version`, a pyproject.toml's +# `[project] version`, otherwise the content without whitespace (same rule as +# release.sh). version() { local content if [ -n "${2:-}" ]; then @@ -39,11 +40,11 @@ version() { else content=$(cat "$1") fi - if [ "$(basename "$1")" = package.json ]; then - jq -r '.version // empty' <<<"$content" - else - tr -d '[:space:]' <<<"$content" - fi + case "$(basename "$1")" in + package.json) jq -r '.version // empty' <<<"$content" ;; + pyproject.toml) awk '/^\[/ { p = ($0 ~ /^\[project\][[:space:]]*(#.*)?$/) } p && /^version[[:space:]]*=/ { sub(/^version[[:space:]]*=[[:space:]]*/, ""); sub(/[[:space:]]*#.*$/, ""); gsub(/["'"'"']/, ""); print; exit }' <<<"$content" ;; + *) tr -d '[:space:]' <<<"$content" ;; + esac } next_version() { # next_version diff --git a/scripts/release.sh b/scripts/release.sh index 6685a36..77cd8b0 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -3,8 +3,9 @@ # from the repository root of a full checkout (history and tags). # # release.sh version [] -# print the version in : a package.json's `version`, otherwise the -# file's content without whitespace. With the file is read at that +# print the version in : a package.json's `version`, a +# pyproject.toml's `[project] version`, otherwise the file's content +# without whitespace. With the file is read at that # commit; a file missing there prints nothing. # # release.sh intent @@ -55,6 +56,7 @@ version() { fi case "$file" in package.json | */package.json) jq -r '.version // empty' <<<"$content" ;; + pyproject.toml | */pyproject.toml) awk '/^\[/ { p = ($0 ~ /^\[project\][[:space:]]*(#.*)?$/) } p && /^version[[:space:]]*=/ { sub(/^version[[:space:]]*=[[:space:]]*/, ""); sub(/[[:space:]]*#.*$/, ""); gsub(/["'"'"']/, ""); print; exit }' <<<"$content" ;; *) tr -d '[:space:]' <<<"$content" ;; esac } diff --git a/scripts/test_pr.sh b/scripts/test_pr.sh index 938059f..db30d86 100755 --- a/scripts/test_pr.sh +++ b/scripts/test_pr.sh @@ -41,6 +41,8 @@ echo "just $*" >>"$STUB/log" vf=$(cat "$STUB/vf") if [ "$(basename "$vf")" = package.json ]; then jq --arg v "$2" '.version = $v' "$vf" >"$vf.tmp" && mv "$vf.tmp" "$vf" +elif [ "$(basename "$vf")" = pyproject.toml ]; then + sed -i.bak "/^\[project\]/,/^\[/ s/^version = .*/version = \"$2\"/" "$vf" && rm -f "$vf.bak" else echo "$2" >"$vf" fi @@ -65,6 +67,8 @@ setup() { if [ "$(basename "$1")" = package.json ]; then mkdir -p "$repo/$(dirname "$1")" printf '{\n "name": "x",\n "version": "%s"\n}\n' "$2" >"$repo/$1" + elif [ "$(basename "$1")" = pyproject.toml ]; then + printf '[project]\nname = "x"\nversion = "%s"\n\n[tool.later]\nversion = "9.9.9"\n' "$2" >"$repo/$1" else mkdir -p "$repo/$(dirname "$1")" echo "$2" >"$repo/$1" @@ -211,5 +215,11 @@ ci_tag v1.4.23 check "release: package.json in a subdirectory" 0 release patch reauth-login backend/package.json release.yml backend/package.json expect "release: package.json at 1.4.23" test "$(git -C "$origin" show feat:backend/package.json | jq -r .version)" = 1.4.23 +setup pyproject.toml 1.10.0 +ci_tag v1.10.1 +check "release: pyproject.toml" 0 release patch x pyproject.toml release.yml pyproject.toml +expect "release: [project] version at 1.10.1" grep -qx 'version = "1.10.1"' <(git -C "$origin" show feat:pyproject.toml) +expect "release: other tables untouched" grep -qx 'version = "9.9.9"' <(git -C "$origin" show feat:pyproject.toml) + [ "$fail" = 0 ] && echo "all pr.sh tests passed" exit "$fail" diff --git a/scripts/test_release.sh b/scripts/test_release.sh index 427a0c6..3ae1dde 100755 --- a/scripts/test_release.sh +++ b/scripts/test_release.sh @@ -45,6 +45,8 @@ repo() { setv() { # setv case "$2" in package.json) printf '{\n "name": "x",\n "version": "%s"\n}\n' "$3" > "$1/$2" ;; + # Other tables with a version key: only [project] counts. + pyproject.toml) printf '[tool.other]\nversion = "0.0.1"\n\n[project]\nname = "x"\nversion = "%s" # uv version\n\n[tool.later]\nversion = "9.9.9"\n' "$3" > "$1/$2" ;; *) printf '%s\n' "$3" > "$1/$2" ;; esac } @@ -60,7 +62,7 @@ intent() { # intent (cd "$1" && VERSION_FILE=$2 TAG_PREFIX=v BASE_SHA=$3 HEAD_SHA=$4 TITLE=$5 "$script" intent) } -for vf in VERSION package.json; do +for vf in VERSION package.json pyproject.toml; do r="$work/intent-$vf" repo "$r" "$vf" 1.2.3 base=$(git -C "$r" rev-parse HEAD)