Add the release pipeline, matching the other PolyKybd repos - #2
Conversation
The fork had no way to publish a release: build.yml is compile-only (its own comment says packaging is out of scope) and uploads the portable zip as a workflow artifact, so nothing ever produced WinCompose-Setup-<ver>.exe. That is the asset PolyKybdHost's new "Install WinCompose…" tray entry looks for, so the entry can only fall back to the releases page today. This mirrors the firmware/host mechanics exactly: a release is created by PUBLISHING it, which fires `release: published`; CI then applies the crafted notes from the `release-notes` branch and attaches the built assets. - .github/workflows/release.yml — builds on windows-latest: InsertIcons, the Release build, the (dormant) signing step, then the installer via installer.csproj, which shells out to iscc — hence `choco install innosetup`, since that target hardcodes "%ProgramFiles(x86)%\Inno Setup 6\iscc". Packages the portable layout the same way build.yml does, writes a SHA256SUMS.txt covering BOTH assets (the docs tell users to verify against it), and uploads everything as artifacts as well, so workflow_dispatch is a safe smoke test that touches no release. - scripts/publish_release.py — the shared script with a wincompose branch added to detect()/parse_version(): version from the csproj AssemblyVersion, tag prefix PK- (GitVersion.yml), target branch main. - src/Makefile — its `all:` target printed download URLs pointing at ell1010/wincompose (inherited from the fork it was based on) and used a `v` tag prefix; both now match this repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PNCWo57UmMMfaNWyfJMBG
📝 WalkthroughWalkthroughThe changes add a Windows release workflow, a Python release publisher, and updated WinCompose release URLs. The workflow builds and packages binaries, creates checksums, optionally signs outputs, uploads artifacts, and publishes GitHub releases. ChangesRelease automation
Estimated code review effort: 4 (Complex) | ~60 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideAdds a GitHub Actions-based release pipeline for WinCompose aligned with other PolyKybd repos, including automated building, packaging, checksumming, and publishing of installer and portable assets, plus shared release tooling and URL/tag updates. Sequence diagram for publishing a WinCompose release via publish_release.py and CIsequenceDiagram
actor Maintainer
participant publish_release_py as publish_release.py
participant GitHub_API
participant Release_Workflow as GitHub_Actions_Release
Maintainer->>publish_release_py: main(--dry-run| )
publish_release_py->>publish_release_py: repo_root
publish_release_py->>publish_release_py: detect
publish_release_py->>publish_release_py: prepared_tags
publish_release_py->>publish_release_py: parse_version
publish_release_py->>publish_release_py: owner_repo
publish_release_py->>GitHub_API: api(GET /repos/{owner}/{repo}/releases/tags/{tag})
alt release exists
publish_release_py->>GitHub_API: api(PATCH /repos/{owner}/{repo}/releases/{id})
else release missing
publish_release_py->>GitHub_API: api(POST /repos/{owner}/{repo}/releases)
end
note over GitHub_API,Release_Workflow: Publishing a wincompose release fires the release: published event
GitHub_API-->>Release_Workflow: [release: published]
Release_Workflow->>Release_Workflow: Build InsertIcons
Release_Workflow->>Release_Workflow: msbuild wincompose.csproj (Release)
Release_Workflow->>Release_Workflow: msbuild installer.csproj
Release_Workflow->>Release_Workflow: Package portable
Release_Workflow->>Release_Workflow: Assemble release assets
Release_Workflow->>GitHub_API: gh release upload {tag} (exe, zip, SHA256SUMS.txt)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
The script is meant to be byte-identical across the repos it serves, and thpoll83/wincompose#2 moved its copy ahead by teaching detect()/parse_version() about a third repo: version from the csproj <AssemblyVersion>, tag prefix PK- (GitVersion.yml), target branch main. Nothing changes for this repo — the host branch of detect() is untouched and a dry-run still resolves the same tag: repo : thpoll83/PolyKybdHost (host) tag : v0.10.1 target: main Verified byte-identical to wincompose scripts/publish_release.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011PNCWo57UmMMfaNWyfJMBG
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
.github/workflows/release.yml (1)
69-83: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConsider pinning the signing action and checking its current name.
azure/trusted-signing-action@v0is referenced by a floating major-version tag. For a step that handles signing credentials (azure-client-secret, tenant/account/profile), pinning to a specific tag or commit SHA reduces the risk of an unreviewed upstream change silently altering what runs in this job. Separately, this action's repository has been renamed; confirm whether the project should track the renamed repository going forward instead of the original name.Do you want a search on the current recommended action name and version for Azure code-signing in GitHub Actions, to confirm the right long-term reference?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release.yml around lines 69 - 83, Update the “Sign binaries” step to use the current repository name for Azure Trusted Signing and replace the floating `@v` reference with a reviewed, immutable version tag or commit SHA. Preserve the existing signing inputs and conditional execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 40-47: Add persist-credentials: false to the actions/checkout@v4
step’s with configuration, alongside submodules and fetch-depth. Preserve the
existing recursive submodule checkout and full-history behavior.
In `@scripts/publish_release.py`:
- Around line 105-112: Update get_token to handle FileNotFoundError from the gh
auth token invocation and return None when the gh binary is unavailable,
allowing the existing no-token handling to produce its intended message.
Preserve the environment-token checks and successful gh output behavior.
- Around line 226-248: Update the release lookup flow around the GET request and
existing-release branch: handle status 200 as the update path, status 404 as the
only condition that proceeds to creation, and call die() for every other non-200
status with a “checking existing release failed” message before the POST
request.
- Around line 115-133: Update the GitHub request in api to pass a finite timeout
to urllib.request.urlopen, using the script’s existing timeout configuration or
a reasonable constant if none exists. Keep the current response and HTTPError
handling unchanged.
- Around line 173-190: Update the auto-detect branch after assigning notes from
show() to apply the same non-empty validation used by the --tag path, calling
die() with the prepared-notes error message when notes is missing or blank. Keep
the existing tag selection and reporting behavior unchanged.
In `@src/Makefile`:
- Around line 45-48: Update the installer and portable download links in
README.md to use the thpoll83/wincompose repository, the PK-$(VERSION)-style
release tag, and the current installer/archive filenames matching the Makefile
targets; remove the stale ell1010 repository and v0.9.15.1 references.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 69-83: Update the “Sign binaries” step to use the current
repository name for Azure Trusted Signing and replace the floating `@v` reference
with a reviewed, immutable version tag or commit SHA. Preserve the existing
signing inputs and conditional execution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b9e17a80-3511-4216-9338-a8cc179538ad
📒 Files selected for processing (3)
.github/workflows/release.ymlscripts/publish_release.pysrc/Makefile
| - uses: actions/checkout@v4 | ||
| with: | ||
| # installer.iss pulls its wizard resources from the issrc submodule | ||
| # (INNODIR ../3rdparty/innosetup/Files), and the build embeds data | ||
| # files from the other 3rdparty submodules — so recursive, and full | ||
| # history for GitVersion (see build.yml for the LibGit2Sharp crash). | ||
| submodules: recursive | ||
| fetch-depth: 0 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Set persist-credentials: false on the checkout step.
actions/checkout@v4 persists the runner's Git credentials to disk for the rest of the job by default. Static analysis flags this as a credential-persistence risk (artipacked pattern). The current Upload build artifacts step only uploads release/, so the specific artifact-leak vector this rule targets is not present today. However, the job also runs third-party tooling (choco, MSBuild, Inno Setup, the Azure signing PowerShell module) with the credentials sitting on disk for the whole run, which is unnecessary exposure since none of the later gh release calls rely on the checked-out credentials — they use GH_TOKEN from secrets.GITHUB_TOKEN explicitly.
Add persist-credentials: false to reduce the blast radius if any of that tooling is compromised.
🔒️ Proposed fix
- uses: actions/checkout@v4
with:
+ persist-credentials: false
# installer.iss pulls its wizard resources from the issrc submodule
# (INNODIR ../3rdparty/innosetup/Files), and the build embeds data
# files from the other 3rdparty submodules — so recursive, and full
# history for GitVersion (see build.yml for the LibGit2Sharp crash).
submodules: recursive
fetch-depth: 0📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - uses: actions/checkout@v4 | |
| with: | |
| # installer.iss pulls its wizard resources from the issrc submodule | |
| # (INNODIR ../3rdparty/innosetup/Files), and the build embeds data | |
| # files from the other 3rdparty submodules — so recursive, and full | |
| # history for GitVersion (see build.yml for the LibGit2Sharp crash). | |
| submodules: recursive | |
| fetch-depth: 0 | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| # installer.iss pulls its wizard resources from the issrc submodule | |
| # (INNODIR ../3rdparty/innosetup/Files), and the build embeds data | |
| # files from the other 3rdparty submodules — so recursive, and full | |
| # history for GitVersion (see build.yml for the LibGit2Sharp crash). | |
| submodules: recursive | |
| fetch-depth: 0 |
🧰 Tools
🪛 zizmor (1.28.0)
[warning] 40-47: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 40 - 47, Add persist-credentials:
false to the actions/checkout@v4 step’s with configuration, alongside submodules
and fetch-depth. Preserve the existing recursive submodule checkout and
full-history behavior.
Source: Linters/SAST tools
| def get_token(): | ||
| for var in ("GH_TOKEN", "GITHUB_TOKEN"): | ||
| if os.environ.get(var): | ||
| return os.environ[var] | ||
| r = run(["gh", "auth", "token"]) | ||
| if r.returncode == 0 and r.stdout.strip(): | ||
| return r.stdout.strip() | ||
| return None |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Handle a missing gh binary gracefully.
The docstring states: Auth: uses GH_TOKEN / GITHUB_TOKEN if set, else gh auth token. Actually let me not cite the docstring itself since it's from the file under review, not a search result — the file content isn't a search result to cite. I'll remove antml:cite here.
If no env token is set and gh is not installed, run(["gh", "auth", "token"]) calls subprocess.run with an executable that does not exist, which raises FileNotFoundError. This exception is not caught anywhere, so it propagates out of get_token() and crashes the script with a raw traceback instead of the intended die("no GitHub token. ...") message at Line 224. Line 22 of the module docstring states gh is optional, but the script does not actually behave that way when gh is absent.
Wrap the gh invocation in a try/except FileNotFoundError (or centralize this in run()) so a missing gh binary falls through to return None, matching the documented fallback.
🩹 Proposed fix
def get_token():
for var in ("GH_TOKEN", "GITHUB_TOKEN"):
if os.environ.get(var):
return os.environ[var]
- r = run(["gh", "auth", "token"])
- if r.returncode == 0 and r.stdout.strip():
- return r.stdout.strip()
- return None
+ try:
+ r = run(["gh", "auth", "token"])
+ except FileNotFoundError:
+ return None
+ if r.returncode == 0 and r.stdout.strip():
+ return r.stdout.strip()
+ return None📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def get_token(): | |
| for var in ("GH_TOKEN", "GITHUB_TOKEN"): | |
| if os.environ.get(var): | |
| return os.environ[var] | |
| r = run(["gh", "auth", "token"]) | |
| if r.returncode == 0 and r.stdout.strip(): | |
| return r.stdout.strip() | |
| return None | |
| def get_token(): | |
| for var in ("GH_TOKEN", "GITHUB_TOKEN"): | |
| if os.environ.get(var): | |
| return os.environ[var] | |
| try: | |
| r = run(["gh", "auth", "token"]) | |
| except FileNotFoundError: | |
| return None | |
| if r.returncode == 0 and r.stdout.strip(): | |
| return r.stdout.strip() | |
| return None |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/publish_release.py` around lines 105 - 112, Update get_token to
handle FileNotFoundError from the gh auth token invocation and return None when
the gh binary is unavailable, allowing the existing no-token handling to produce
its intended message. Preserve the environment-token checks and successful gh
output behavior.
| def api(token, method, path, payload=None): | ||
| url = "https://api.github.com" + path | ||
| data = json.dumps(payload).encode() if payload is not None else None | ||
| req = urllib.request.Request(url, data=data, method=method, headers={ | ||
| "Authorization": f"Bearer {token}", | ||
| "Accept": "application/vnd.github+json", | ||
| "X-GitHub-Api-Version": "2022-11-28", | ||
| "Content-Type": "application/json", | ||
| "User-Agent": "polykybd-publish-release", | ||
| }) | ||
| try: | ||
| with urllib.request.urlopen(req) as resp: | ||
| return resp.status, json.load(resp) | ||
| except urllib.error.HTTPError as e: | ||
| try: | ||
| return e.code, json.load(e) | ||
| except Exception: | ||
| return e.code, {"message": e.read().decode(errors="replace")} | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a timeout to the GitHub API request.
urllib.request.urlopen(req) at Line 126 has no timeout argument. If the network stalls or GitHub is unresponsive, this call blocks indefinitely, hanging the publish script (and any CI job invoking it) with no way to recover.
🩹 Proposed fix
try:
- with urllib.request.urlopen(req) as resp:
+ with urllib.request.urlopen(req, timeout=30) as resp:
return resp.status, json.load(resp)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def api(token, method, path, payload=None): | |
| url = "https://api.github.com" + path | |
| data = json.dumps(payload).encode() if payload is not None else None | |
| req = urllib.request.Request(url, data=data, method=method, headers={ | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "Content-Type": "application/json", | |
| "User-Agent": "polykybd-publish-release", | |
| }) | |
| try: | |
| with urllib.request.urlopen(req) as resp: | |
| return resp.status, json.load(resp) | |
| except urllib.error.HTTPError as e: | |
| try: | |
| return e.code, json.load(e) | |
| except Exception: | |
| return e.code, {"message": e.read().decode(errors="replace")} | |
| def api(token, method, path, payload=None): | |
| url = "https://api.github.com" + path | |
| data = json.dumps(payload).encode() if payload is not None else None | |
| req = urllib.request.Request(url, data=data, method=method, headers={ | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github+json", | |
| "X-GitHub-Api-Version": "2022-11-28", | |
| "Content-Type": "application/json", | |
| "User-Agent": "polykybd-publish-release", | |
| }) | |
| try: | |
| with urllib.request.urlopen(req, timeout=30) as resp: | |
| return resp.status, json.load(resp) | |
| except urllib.error.HTTPError as e: | |
| try: | |
| return e.code, json.load(e) | |
| except Exception: | |
| return e.code, {"message": e.read().decode(errors="replace")} |
🧰 Tools
🪛 ast-grep (0.45.0)
[info] 116-116: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
[warning] 125-125: Request-controlled URL passed to urlopen; validate against an allowlist to prevent SSRF.
Context: urllib.request.urlopen(req)
Note: [CWE-918] Server-Side Request Forgery (SSRF).
(urlopen-unsanitized-data)
🪛 Ruff (0.16.0)
[error] 126-126: Audit URL open for permitted schemes. Allowing use of file: or custom schemes is often unexpected.
(S310)
[warning] 131-131: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/publish_release.py` around lines 115 - 133, Update the GitHub request
in api to pass a finite timeout to urllib.request.urlopen, using the script’s
existing timeout configuration or a reasonable constant if none exists. Keep the
current response and HTTPError handling unchanged.
| if args.tag: | ||
| tag = args.tag | ||
| notes = show(f"origin/release-notes:{tag}.md") | ||
| if not notes or not notes.strip(): | ||
| die(f"no prepared notes for {tag} on the release-notes branch " | ||
| f"(expected release-notes:{tag}.md).") | ||
| else: | ||
| prepared = prepared_tags(tag_prefix) | ||
| if not prepared: | ||
| die("no prepared release notes on the release-notes branch " | ||
| f"(no {tag_prefix}<X.Y.Z>.md files).\n" | ||
| " Draft + stage them first with the polykybd-github-release skill.") | ||
| tag = prepared[-1][1] | ||
| notes = show(f"origin/release-notes:{tag}.md") | ||
| if len(prepared) > 1: | ||
| others = ", ".join(t for _, t in prepared[:-1]) | ||
| print(f"note: newest prepared tag is {tag}. Others on the branch: {others}") | ||
| print(f" (use --tag to publish a specific one.)") |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate notes consistently between the --tag path and the auto-detect path.
The --tag branch (Lines 173-178) checks if not notes or not notes.strip(): die(...) before using notes. The auto-detect branch (Lines 179-190) assigns notes = show(f"origin/release-notes:{tag}.md") at Line 186 with no equivalent check. prepared_tags() only confirms the file is present in the origin/release-notes tree via git ls-tree; it does not guarantee git show succeeds or that the file is non-empty. If show() returns None (git show failure) or an empty string (an accidentally empty staged file), Line 202's notes.splitlines() raises AttributeError, or Line 203's lines[0] raises IndexError, crashing the script instead of producing the friendly die() message used elsewhere.
Add the same guard used in the --tag branch after Line 186.
🩹 Proposed fix
tag = prepared[-1][1]
notes = show(f"origin/release-notes:{tag}.md")
+ if not notes or not notes.strip():
+ die(f"no prepared notes for {tag} on the release-notes branch "
+ f"(expected release-notes:{tag}.md).")
if len(prepared) > 1:📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if args.tag: | |
| tag = args.tag | |
| notes = show(f"origin/release-notes:{tag}.md") | |
| if not notes or not notes.strip(): | |
| die(f"no prepared notes for {tag} on the release-notes branch " | |
| f"(expected release-notes:{tag}.md).") | |
| else: | |
| prepared = prepared_tags(tag_prefix) | |
| if not prepared: | |
| die("no prepared release notes on the release-notes branch " | |
| f"(no {tag_prefix}<X.Y.Z>.md files).\n" | |
| " Draft + stage them first with the polykybd-github-release skill.") | |
| tag = prepared[-1][1] | |
| notes = show(f"origin/release-notes:{tag}.md") | |
| if len(prepared) > 1: | |
| others = ", ".join(t for _, t in prepared[:-1]) | |
| print(f"note: newest prepared tag is {tag}. Others on the branch: {others}") | |
| print(f" (use --tag to publish a specific one.)") | |
| if args.tag: | |
| tag = args.tag | |
| notes = show(f"origin/release-notes:{tag}.md") | |
| if not notes or not notes.strip(): | |
| die(f"no prepared notes for {tag} on the release-notes branch " | |
| f"(expected release-notes:{tag}.md).") | |
| else: | |
| prepared = prepared_tags(tag_prefix) | |
| if not prepared: | |
| die("no prepared release notes on the release-notes branch " | |
| f"(no {tag_prefix}<X.Y.Z>.md files).\n" | |
| " Draft + stage them first with the polykybd-github-release skill.") | |
| tag = prepared[-1][1] | |
| notes = show(f"origin/release-notes:{tag}.md") | |
| if not notes or not notes.strip(): | |
| die(f"no prepared notes for {tag} on the release-notes branch " | |
| f"(expected release-notes:{tag}.md).") | |
| if len(prepared) > 1: | |
| others = ", ".join(t for _, t in prepared[:-1]) | |
| print(f"note: newest prepared tag is {tag}. Others on the branch: {others}") | |
| print(f" (use --tag to publish a specific one.)") |
🧰 Tools
🪛 Ruff (0.16.0)
[error] 190-190: f-string without any placeholders
Remove extraneous f prefix
(F541)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/publish_release.py` around lines 173 - 190, Update the auto-detect
branch after assigning notes from show() to apply the same non-empty validation
used by the --tag path, calling die() with the prepared-notes error message when
notes is missing or blank. Keep the existing tag selection and reporting
behavior unchanged.
| status, rel = api(token, "GET", f"/repos/{owner}/{repo}/releases/tags/{tag}") | ||
| if status == 200: | ||
| st, res = api(token, "PATCH", f"/repos/{owner}/{repo}/releases/{rel['id']}", | ||
| {"name": title, "body": body, "make_latest": "true", "draft": False}) | ||
| if st >= 300: | ||
| die(f"updating existing release failed ({st}): {res.get('message')}") | ||
| print(f"updated existing release {tag}") | ||
| print(res.get("html_url")) | ||
| print("note: an already-published release does not re-trigger the build; " | ||
| "assets are only (re)built when the release is first published.") | ||
| return | ||
|
|
||
| st, res = api(token, "POST", f"/repos/{owner}/{repo}/releases", { | ||
| "tag_name": tag, | ||
| "target_commitish": default_branch, | ||
| "name": title, | ||
| "body": body, | ||
| "make_latest": "true", | ||
| "draft": False, | ||
| "prerelease": False, | ||
| }) | ||
| if st >= 300: | ||
| die(f"creating release failed ({st}): {res.get('message')}") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Distinguish "release not found" from other GET failures.
Line 226 checks the release status with a GET, then only branches to update on status == 200 (Line 227). Any other status — including 401 (bad token), 403 (rate limit), or 5xx (transient GitHub outage) — falls through to the POST create call at Line 238, exactly as if the release did not exist. rel is discarded in that case, so the real cause of the GET failure is lost. If the POST also fails, the user sees "creating release failed (...)" instead of the actual root cause ("checking existing release failed (...)"), which is misleading for automation debugging.
Check specifically for status == 404 in the "release doesn't exist" branch and die() for any other non-200 status before attempting to create.
🩹 Proposed fix
status, rel = api(token, "GET", f"/repos/{owner}/{repo}/releases/tags/{tag}")
if status == 200:
st, res = api(token, "PATCH", f"/repos/{owner}/{repo}/releases/{rel['id']}",
{"name": title, "body": body, "make_latest": "true", "draft": False})
if st >= 300:
die(f"updating existing release failed ({st}): {res.get('message')}")
print(f"updated existing release {tag}")
print(res.get("html_url"))
print("note: an already-published release does not re-trigger the build; "
"assets are only (re)built when the release is first published.")
return
+ elif status != 404:
+ die(f"checking for existing release {tag} failed ({status}): {rel.get('message')}")
st, res = api(token, "POST", f"/repos/{owner}/{repo}/releases", {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| status, rel = api(token, "GET", f"/repos/{owner}/{repo}/releases/tags/{tag}") | |
| if status == 200: | |
| st, res = api(token, "PATCH", f"/repos/{owner}/{repo}/releases/{rel['id']}", | |
| {"name": title, "body": body, "make_latest": "true", "draft": False}) | |
| if st >= 300: | |
| die(f"updating existing release failed ({st}): {res.get('message')}") | |
| print(f"updated existing release {tag}") | |
| print(res.get("html_url")) | |
| print("note: an already-published release does not re-trigger the build; " | |
| "assets are only (re)built when the release is first published.") | |
| return | |
| st, res = api(token, "POST", f"/repos/{owner}/{repo}/releases", { | |
| "tag_name": tag, | |
| "target_commitish": default_branch, | |
| "name": title, | |
| "body": body, | |
| "make_latest": "true", | |
| "draft": False, | |
| "prerelease": False, | |
| }) | |
| if st >= 300: | |
| die(f"creating release failed ({st}): {res.get('message')}") | |
| status, rel = api(token, "GET", f"/repos/{owner}/{repo}/releases/tags/{tag}") | |
| if status == 200: | |
| st, res = api(token, "PATCH", f"/repos/{owner}/{repo}/releases/{rel['id']}", | |
| {"name": title, "body": body, "make_latest": "true", "draft": False}) | |
| if st >= 300: | |
| die(f"updating existing release failed ({st}): {res.get('message')}") | |
| print(f"updated existing release {tag}") | |
| print(res.get("html_url")) | |
| print("note: an already-published release does not re-trigger the build; " | |
| "assets are only (re)built when the release is first published.") | |
| return | |
| elif status != 404: | |
| die(f"checking for existing release {tag} failed ({status}): {rel.get('message')}") | |
| st, res = api(token, "POST", f"/repos/{owner}/{repo}/releases", { | |
| "tag_name": tag, | |
| "target_commitish": default_branch, | |
| "name": title, | |
| "body": body, | |
| "make_latest": "true", | |
| "draft": False, | |
| "prerelease": False, | |
| }) | |
| if st >= 300: | |
| die(f"creating release failed ({st}): {res.get('message')}") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/publish_release.py` around lines 226 - 248, Update the release lookup
flow around the GET request and existing-release branch: handle status 200 as
the update path, status 404 as the only condition that proceeds to creation, and
call die() for every other non-200 status with a “checking existing release
failed” message before the POST request.
| @echo Installer: https://github.com/thpoll83/wincompose/releases/download/PK-$(VERSION)/WinCompose-Setup-$(VERSION).exe | ||
| @echo InstallerMD5: $(shell certutil -hashfile WinCompose-Setup-$(VERSION).exe MD5) | ||
| @echo InstallerSHA256: $(shell certutil -hashfile WinCompose-Setup-$(VERSION).exe sha256) | ||
| @echo Portable: https://github.com/ell1010/wincompose/releases/download/v$(VERSION)/WinCompose-NoInstall-$(VERSION).zip | ||
| @echo Portable: https://github.com/thpoll83/wincompose/releases/download/PK-$(VERSION)/WinCompose-NoInstall-$(VERSION).zip |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo files (top level and relevant)"
git ls-files | sed -n '1,160p' | rg -n '(^README\.md$|^src/Makefile$|installer\.iss$|\.github/workflows/|Makefile|WinCompose|README)' || true
echo
echo "== src/Makefile around relevant lines"
if [ -f src/Makefile ]; then
cat -n src/Makefile | sed -n '1,90p'
fi
echo
echo "== README download links"
if [ -f README.md ]; then
rg -n "github\.com/.*/releases/download/|WinCompose [0-9]+\.[0-9]+|Download latest|Installer|Portable" README.md || true
else
echo "README.md not found"
fi
echo
echo "== installer.iss AppPublisherURL"
if [ -f installer.iss ]; then
rg -n "AppPublisherURL|AppName|AppVersion" installer.iss -C 2 || true
fi
echo
echo "== workflow references to repository/release/tag"
git ls-files '.github/workflows/*' | while read -r f; do
echo "--- $f"
rg -n "github\.com/|pull_request_target|gh release|release|tag|PK-|VERSION|owner|repo|wincompose|thpoll83|ell1010" "$f" -C 2 || true
doneRepository: thpoll83/wincompose
Length of output: 1927
🌐 Web query:
GitHub PR code review src Makefile WinCompose-Setup-VERSION PK-VERSION README download links thpoll83 wincompose
💡 Result:
The term WinCompose refers to a free, open-source compose key application for Windows developed by Sam Hocevar [1][2]. It is unrelated to the GitHub user thpoll83 [3], whose projects primarily focus on the PolyKybd mechanical keyboard [4][5]. Regarding the specific technical components mentioned in your query: WinCompose Build and Release Structure The WinCompose repository contains a Makefile used to automate build processes [6], including the generation of installer and portable packages [7]. The installer files are typically named following the convention WinCompose-Setup-VERSION.exe (e.g., WinCompose-Setup-0.9.11.exe) [1][8]. The project uses Inno Setup for creating these installers [6]. While the term PK-VERSION is not a standard component of the WinCompose repository [1][6], it may appear in other build systems or scripts related to different projects. WinCompose Download Links The official and authoritative download links for WinCompose are maintained on the GitHub releases page for the samhocevar/wincompose repository [1][9]. - Latest Installable version: WinCompose-Setup-0.9.11.exe [1][8]. - Latest Portable version: WinCompose-NoInstall-0.9.11.zip [1]. Association with thpoll83 The GitHub user thpoll83 is the maintainer of the PolyKybd project [4][10]. In the documentation for the PolyKybd firmware, thpoll83 explicitly references WinCompose as an optional tool for users who wish to enable unicode input via a [WinC] key mode [4]. The download link provided in their documentation points directly to the official WinCompose releases on Sam Hocevar's repository [4]. There is no evidence suggesting thpoll83 maintains or alters the source code for WinCompose [1][3].
Citations:
- 1: https://github.com/samhocevar/wincompose
- 2: https://wincompose.info/
- 3: https://github.com/thpoll83
- 4: https://github.com/thpoll83/PolyKybd
- 5: https://www.crowdsupply.com/polykybd/polykybd
- 6: https://github.com/samhocevar/wincompose/
- 7: https://github.com/samhocevar/wincompose/blob/cdec50332e02d085c21cf7cbbf64dfdf1fdf990e/RELEASE.md
- 8: https://github.com/samhocevar/wincompose/releases/tag/v0.9.11
- 9: https://github.com/samhocevar/wincompose/releases/
- 10: https://github.com/thpoll83/qmk_firmware/blob/PolyKybd/keyboards/handwired/polykybd/readme.md
🌐 Web query:
site:github.com/thpoll83/wincompose src/Makefile InstallerMD5 certutil PK-$(VERSION)
💡 Result:
The repository’s src/Makefile contains:
VERSION = $(shell gitversion -showvariable MajorMinorPatch)- Release URLs using
PK-$(VERSION) InstallerMD5andInstallerSHA256viacertutil- The portable checksum labels are reversed:
PortableSHA256runsMD5PortableMD5runssha256
This appears to be a likely bug. (raw.githubusercontent.com)
Citations:
🌐 Web query:
site:github.com/thpoll83/wincompose README.md Download latest WinCompose-Setup 0.9.15.1
💡 Result:
The README lists WinCompose 0.9.15.1:
- Installer:
WinCompose-Setup-0.9.15.1.exe - Portable:
WinCompose-NoInstall-0.9.15.1.zip - Release page:
github.com/ell1010/wincompose/releases/tag/v0.9.15.1
Note: the repository’s current latest release is 0.9.16, dated August 1, 2026. (github.com)
Citations:
Update README download links to use PK- release tags.
src/Makefile now points at https://github.com/thpoll83/wincompose/releases/download/PK-$(VERSION)/..., but README.md still uses ell1010/wincompose and v0.9.15.1. Update the installer and portable links so users are pointed at the current release scheme.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/Makefile` around lines 45 - 48, Update the installer and portable
download links in README.md to use the thpoll83/wincompose repository, the
PK-$(VERSION)-style release tag, and the current installer/archive filenames
matching the Makefile targets; remove the stale ell1010 repository and v0.9.15.1
references.
Summary
The fork had no way to publish a release.
build.ymlis compile-only by its own admission ("Packaging … is not covered here") and uploads the portable zip as a workflow artifact; nothing ever producedWinCompose-Setup-<ver>.exe. That is exactly the asset PolyKybdHost's new "Install WinCompose…" tray entry looks for, so today the entry can only fall back to opening the releases page.This adds the same mechanics the firmware and host already use: a release is created by publishing it, which fires
release: published; CI then applies the crafted notes from therelease-notesbranch and attaches the built assets..github/workflows/release.ymlBuilds on
windows-latest: InsertIcons → Release build → the (dormant) signing step → the installer. The installer comes fromsrc/installer/installer.csproj, whoseBuildtarget shells out to"%ProgramFiles(x86)%\Inno Setup 6\iscc"— a hardcoded path, hencechoco install innosetuprather than relying on the runner image.installer.issderives the version from the built exe's version resource and writesWinCompose-Setup-<ver>.exetosrc/.Packaging notes:
build.ymldoes (the rawbindir isn't runnable — a.pdbnext to the exe makesUtilsthink it's a debug build and the rule lookup losesEmoji.txt/WinCompose.txt). Those two steps must stay in step; change them together.SHA256SUMS.txtnow covers both assets, not just the portable zip — the docs tell users to verify their download against it, and the installer is the download most people will take.release/tag, soworkflow_dispatchis a safe end-to-end smoke test that touches no release.submodules: recursiveis required:installer.isspulls its wizard resources from theissrcsubmodule (INNODIR ../3rdparty/innosetup/Files).scripts/publish_release.pyThe shared script from PolyKybdHost/qmk_firmware with a
wincomposebranch added todetect()/parse_version(): version from the csproj<AssemblyVersion>, tag prefixPK-(perGitVersion.yml), target branchmain. Verified against this repo:src/MakefileIts
all:target printed download URLs pointing at ell1010/wincompose (inherited from the fork this was based on) and used avtag prefix. Both now match this repo and itsPK-tags.Release notes
PK-0.9.16.mdis staged on the newrelease-notesbranch, covering the fork's own 24 commits (tray tooltip, tray icon state, settings dialog styling, own update-check status file, de-CH/it-CH catalogues), plus the unsigned-build/SmartScreen caveat and the SHA256 verification steps.Testing
CI on this PR builds via
build.ymlonly —release.ymldoesn't run on PRs by design. The packaging path is best verified with aworkflow_dispatchrun of Release on this branch once merged (or from the branch, if you'd rather check before merging): it builds all three assets and uploads them as artifacts without creating or touching a release.I can't verify the Windows packaging locally, so the Inno Setup step and the installer-path assertions are the parts most likely to need a follow-up commit.
Publishing, once this is in
Publishing is what starts the build and attaches the assets. It needs
GH_TOKEN/GITHUB_TOKENorgh auth login— I can't do it from Claude Code on the web (no create-release API, and the git proxy rejectsrefs/tags/*).Generated by Claude Code
Summary by Sourcery
Introduce an automated release pipeline for WinCompose that builds, signs (when configured), packages, and attaches release assets to GitHub releases, aligned with other PolyKybd repositories.
New Features:
Enhancements:
Summary by CodeRabbit
New Features
Bug Fixes