name: build-windows-exe on: push: tags: - 'v*.*.*' jobs: build: runs-on: ubuntu-latest permissions: contents: write env: BUILD_VERSION: ${{ github.ref_name }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '24' cache: npm - name: Install dependencies run: npm ci - name: Clean dist run: rm -f dist/*.exe - name: Generate release notes run: | python - <<'PY' import os import pathlib import subprocess tag = os.environ["BUILD_VERSION"] try: previous_tag = subprocess.check_output( ["git", "describe", "--tags", "--abbrev=0", f"{tag}^"], text=True, ).strip() except subprocess.CalledProcessError: previous_tag = "" log_range = f"{previous_tag}..{tag}" if previous_tag else tag subjects = subprocess.check_output( ["git", "log", "--pretty=format:%s", log_range], text=True, ).splitlines() notes = [] for subject in subjects: subject = subject.strip() if not subject: continue if subject.startswith("chore: bump version"): continue if subject.lower() == "no message": continue notes.append(f"- {subject}") if not notes: notes = ["- Release build and version bump."] body = "## What\'s new\n\n" + "\n".join(notes) + "\n" pathlib.Path("release-notes.md").write_text(body, encoding="utf-8") print(body) PY - name: Build Windows executable run: npm run build:win - name: Publish release uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: gSheets-Rate-Assistant ${{ github.ref_name }} body_path: release-notes.md files: dist/*.exe - name: Mirror release to GitHub env: GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} RELEASE_GITHUB_REPO: ${{ secrets.RELEASE_GITHUB_REPO }} run: | python - <<'PY' import json import os import sys import urllib.error import urllib.parse import urllib.request token = os.environ.get("GITHUB_TOKEN", "") repository = os.environ.get("RELEASE_GITHUB_REPO", "") if not token or not repository: print("Skipping GitHub mirror because RELEASE_GITHUB_REPO or RELEASE_GITHUB_TOKEN is missing.") raise SystemExit(0) tag = os.environ["BUILD_VERSION"] release_name = f"gSheets-Rate-Assistant {tag}" asset_path = next((os.path.join("dist", name) for name in os.listdir("dist") if name.endswith(".exe")), None) with open("release-notes.md", "r", encoding="utf-8") as notes_file: release_notes = notes_file.read() if asset_path is None: raise SystemExit("No Windows executable found in dist/") def request_json(url, method="GET", payload=None, headers=None): request_headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } if headers: request_headers.update(headers) data = None if payload is None else json.dumps(payload).encode("utf-8") if data is not None: request_headers["Content-Type"] = "application/json" request = urllib.request.Request(url, data=data, method=method, headers=request_headers) try: with urllib.request.urlopen(request) as response: body = response.read().decode("utf-8") return json.loads(body) if body else {} except urllib.error.HTTPError as error: message = error.read().decode("utf-8", errors="replace") print(message, file=sys.stderr) raise release_url = f"https://api.github.com/repos/{repository}/releases/tags/{urllib.parse.quote(tag)}" release = None try: release = request_json(release_url) except urllib.error.HTTPError as error: if error.code != 404: raise payload = { "tag_name": tag, "name": release_name, "body": release_notes, "draft": False, "prerelease": False, "generate_release_notes": False, } if release is None: release = request_json(f"https://api.github.com/repos/{repository}/releases", method="POST", payload=payload) else: release = request_json(release["url"], method="PATCH", payload=payload) asset_name = os.path.basename(asset_path) for asset in release.get("assets", []): if asset.get("name") == asset_name: request_json(asset["url"], method="DELETE") upload_url = release["upload_url"].split("{", 1)[0] with open(asset_path, "rb") as asset_file: asset_data = asset_file.read() upload_request = urllib.request.Request( f"{upload_url}?name={urllib.parse.quote(asset_name)}", data=asset_data, method="POST", headers={ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "Content-Type": "application/octet-stream", "X-GitHub-Api-Version": "2022-11-28", }, ) try: with urllib.request.urlopen(upload_request) as response: response.read() except urllib.error.HTTPError as error: message = error.read().decode("utf-8", errors="replace") print(message, file=sys.stderr) raise PY