From 3bae89a20330a0acfadd44765251b88884484561 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Tue, 11 Aug 2026 13:41:38 -0500 Subject: [PATCH] Extract release JSON building into helper script (avoid YAML/bash heredoc trap) --- .github/workflows/release.yml | 62 ++++------------------------ scripts/release-helper.py | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 scripts/release-helper.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67d0ebf..8e2ca76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,17 +76,12 @@ jobs: REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} GH_API_URL: ${{ github.api_url }} + GH_SHA: ${{ github.sha }} run: | set -euo pipefail - - # Branch on host: github.com sets GITHUB_API_URL; Gitea leaves it empty - # (or set to the Gitea base, which we detect by path). We treat any - # api_url that is the github.com default as GitHub and anything else as Gitea. - zip_path=( dist/projects-portfolio-*.zip ) zip_path="${zip_path[0]}" asset_name=$(basename "$zip_path") - asset_bytes=$(wc -c < "$zip_path") if [[ "$GH_API_URL" == "https://api.github.com" ]]; then # ---- GitHub.com path ---- @@ -94,35 +89,16 @@ jobs: echo "::error::GITHUB_TOKEN is not available on this runner. Check repo permissions." >&2 exit 1 fi - - # Build the JSON payload to a temp file via Python heredoc. - python3 - > /tmp/release-payload.json <<'PYEOF' - import json, os - print(json.dumps({ - "tag_name": os.environ["TAG"], - "name": os.environ["TAG"], - "body": ( - "Automated release.\n\n" - "Built from commit " + os.environ.get("GH_SHA", "unknown") + ".\n\n" - "See the workflow run for the artifact." - ), - "draft": False, - "prerelease": False, - "generate_release_notes": True, - })) - PYEOF - + payload=$(python3 scripts/release-helper.py build-payload --github) release_json=$(curl -fsS -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -H "Content-Type: application/json" \ - -d @/tmp/release-payload.json \ + -d "$payload" \ "${GH_API_URL}/repos/${REPO}/releases") - release_id=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])' <<<"$release_json") - upload_url=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["upload_url"])' <<<"$release_json") + release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id) + upload_url=$(echo "$release_json" | python3 scripts/release-helper.py extract-upload-url) echo "Created GitHub release id=$release_id" - - # Asset upload uses the per-release upload_url (template with {?name,label}). curl -fsS -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github+json" \ @@ -136,41 +112,19 @@ jobs: echo "::error::GITEA_TOKEN secret is not set. Create a personal access token in Gitea with 'write:repository' scope and add it as a repository secret named GITEA_TOKEN." >&2 exit 1 fi - - # Gitea's api_url may be unset or set to the Gitea base. if [ -z "${GH_API_URL:-}" ] || [[ "$GH_API_URL" == "https://git.keithsolomon.net"* ]]; then API="https://git.keithsolomon.net/api/v1" else API="${GH_API_URL%/}/api/v1" fi - - python3 - > /tmp/release-payload.json <<'PYEOF' - import json, os - print(json.dumps({ - "tag_name": os.environ["TAG"], - "name": os.environ["TAG"], - "body": ( - "Automated release.\n\n" - "Built from commit " + os.environ.get("GH_SHA", "unknown") + ".\n\n" - "See the workflow run for the artifact." - ), - "draft": False, - "prerelease": False, - })) - PYEOF - + payload=$(python3 scripts/release-helper.py build-payload) release_json=$(curl -fsS -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ - -d @/tmp/release-payload.json \ + -d "$payload" \ "${API}/repos/${REPO}/releases") - release_id=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])' <<<"$release_json") - if [ -z "$release_id" ]; then - echo "::error::Failed to create Gitea release. Response: $release_json" >&2 - exit 1 - fi + release_id=$(echo "$release_json" | python3 scripts/release-helper.py extract-id) echo "Created Gitea release id=$release_id" - curl -fsS -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/zip" \ diff --git a/scripts/release-helper.py b/scripts/release-helper.py new file mode 100644 index 0000000..df4d6cc --- /dev/null +++ b/scripts/release-helper.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Helper script for the release workflow (.github/workflows/release.yml). + +Subcommands: + + build-payload [--github] Build the JSON payload for creating a release. + Reads TAG, GH_SHA from env. + With --github, includes generate_release_notes=True. + Writes JSON to stdout. + + extract-id < response.json Print the release id from a release API response. + extract-upload-url < response.json Print the upload_url from a GitHub release + response (used as the asset upload endpoint template). + +This script exists to avoid bash heredoc-inside-YAML-literal indentation traps. +""" + +import json +import os +import sys + + +def build_payload(github: bool) -> int: + tag = os.environ.get("TAG", "") + sha = os.environ.get("GH_SHA", "unknown") + payload = { + "tag_name": tag, + "name": tag, + "body": ( + "Automated release.\n\n" + f"Built from commit {sha}.\n\n" + "See the workflow run for the artifact." + ), + "draft": False, + "prerelease": False, + } + if github: + payload["generate_release_notes"] = True + print(json.dumps(payload)) + return 0 + + +def extract_field(field: str) -> int: + raw = sys.stdin.read() + try: + data = json.loads(raw) + except json.JSONDecodeError as e: + print(f"::error::Failed to parse JSON response: {e}", file=sys.stderr) + return 1 + value = data.get(field, "") + if not value: + print(f"::error::Field {field!r} missing from response: {raw}", file=sys.stderr) + return 1 + print(value) + return 0 + + +def usage() -> int: + print(__doc__, file=sys.stderr) + return 2 + + +def main() -> int: + if len(sys.argv) < 2: + return usage() + cmd = sys.argv[1] + if cmd == "build-payload": + return build_payload(github="--github" in sys.argv) + if cmd == "extract-id": + return extract_field("id") + if cmd == "extract-upload-url": + return extract_field("upload_url") + return usage() + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file