From 57ef6289ff5742c15e8edb27fe6b8abb2c2e41b8 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 20 Jul 2026 15:21:12 +0200 Subject: [PATCH] fix: generate changelog via Python subprocess for reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous approaches using temp files or env vars failed in Gitea runner. Python now runs git commands directly via subprocess, reads stdout, and builds the JSON payload — no intermediate files or shell escaping. --- .gitea/workflows/release.yaml | 126 ++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 44923581..15659a7f 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -7,7 +7,7 @@ jobs: release: runs-on: shell steps: - - name: Checkout, build & release + - name: Build & release env: VERSION: ${{ gitea.ref_name }} GITEA_API: ${{ gitea.api_url }} @@ -15,90 +15,114 @@ jobs: run: | set -e - # Use the local checkout (has full history + HEAD) LOCAL="/var/www/atom-nexst" - # ── Generate changelog ──────────────────────────────────────── - PREV_TAG="$(git -C "$LOCAL" tag --sort=-creatordate | head -2 | tail -1 || echo '')" - echo "=== Debug: VERSION=$VERSION PREV_TAG=$PREV_TAG ===" - - if [ -n "$PREV_TAG" ] && [ "$PREV_TAG" != "$VERSION" ]; then - RANGE="${PREV_TAG}..${VERSION}" - echo "=== Range: $RANGE ===" - git -C "$LOCAL" log --oneline --no-decorate "${RANGE}" > /tmp/changelog.txt - else - # First release or only one tag — show all commits up to this tag - echo "=== First release or only one tag, showing all commits ===" - git -C "$LOCAL" log --oneline --no-decorate "${VERSION}" > /tmp/changelog.txt - fi - - echo "=== Changelog ===" - cat /tmp/changelog.txt - echo "=== End changelog ===" - - # ── Build ───────────────────────────────────────────────────── + # Shallow clone for build SRC="/tmp/epicnext-release" rm -rf "$SRC" git clone --branch "${VERSION}" --depth 1 \ /docker/gitea/gitea/git/repositories/remco/epicnext-cms.git \ "$SRC" - cd "$SRC" if [ -f /var/www/atom-nexst/.env ]; then cp /var/www/atom-nexst/.env "$SRC/.env" fi + cd "$SRC" pnpm install --frozen-lockfile pnpm prisma:generate export SKIP_ENV_VALIDATION=1 pnpm build - # ── Create release (Python handles JSON properly) ───────────── + # Create release — Python reads changelog directly from git python3 << 'PYEOF' -import json, os, urllib.request +import json, os, subprocess, urllib.request, urllib.error -version = os.environ['VERSION'] -api = os.environ['GITEA_API'] -repo = os.environ['GITEA_REPO'] +version = os.environ["VERSION"] +api = os.environ["GITEA_API"] +repo = os.environ["GITEA_REPO"] token = "${{ secrets.GITEA_TOKEN }}" +local = "/var/www/atom-nexst" -with open('/tmp/changelog.txt') as f: - notes = f.read().strip() +# Get previous tag +tags = subprocess.run( + ["git", "-C", local, "tag", "--sort=-creatordate"], + capture_output=True, text=True, check=False, +).stdout.strip().split("\n") +tags = [t for t in tags if t] -if not notes: - notes = "Initial release" +prev_tag = tags[1] if len(tags) > 1 else "" +print(f"Tags: {tags}") +print(f"Version: {version}, Previous: {prev_tag}") + +if prev_tag and prev_tag != version: + r = subprocess.run( + ["git", "-C", local, "log", "--oneline", "--no-decorate", f"{prev_tag}..{version}"], + capture_output=True, text=True, check=False, + ) + notes_raw = r.stdout.strip() + print(f"Range: {prev_tag}..{version}, stderr: {r.stderr.strip()}") +else: + r = subprocess.run( + ["git", "-C", local, "log", "--oneline", "--no-decorate", version], + capture_output=True, text=True, check=False, + ) + notes_raw = r.stdout.strip() + print(f"Single tag {version}, stderr: {r.stderr.strip()}") + +if not notes_raw: + notes_raw = "Initial release" + +print(f"Notes ({len(notes_raw)} chars):") +print(notes_raw) +print("=== End notes ===") body = f"""## EpicNext-CMS {version} ### Changes -{notes} +{notes_raw} --- *Automated release from Gitea Actions* """ payload = json.dumps({ - 'tag_name': version, - 'name': version, - 'body': body, - 'draft': False, - 'prerelease': False, -}).encode('utf-8') + "tag_name": version, + "name": version, + "body": body, + "draft": False, + "prerelease": False, +}).encode("utf-8") -req = urllib.request.Request( - f'{api}/repos/{repo}/releases', - data=payload, - headers={ - 'Authorization': f'token {token}', - 'Content-Type': 'application/json', - }, - method='POST', -) +def api_call(method, path, data=None): + url = f"{api}/repos/{repo}/{path}" + print(f"API {method} {url}") + req = urllib.request.Request( + url, data=data, + headers={ + "Authorization": f"token {token}", + "Content-Type": "application/json", + }, + method=method, + ) + resp = urllib.request.urlopen(req) + return json.loads(resp.read().decode("utf-8")) try: - resp = urllib.request.urlopen(req) - result = json.loads(resp.read().decode('utf-8')) + result = api_call("POST", "releases", payload) print(f"Release created: {result.get('html_url', result.get('id', '?'))}") except urllib.error.HTTPError as e: - print(f"API error {e.code}: {e.read().decode('utf-8')}") - raise + err_body = e.read().decode("utf-8") + print(f"POST error {e.code}: {err_body}") + if e.code == 409: + releases = api_call("GET", "releases") + for rel in releases: + if rel["tag_name"] == version: + result = api_call("PATCH", f"releases/{rel['id']}", payload) + print(f"Release updated: {result.get('html_url', result.get('id', '?'))}") + break + else: + print(f"ERROR: no existing release found for {version}") + raise + else: + raise PYEOF