Skip to content

Add the release pipeline, matching the other PolyKybd repos - #2

Merged
thpoll83 merged 1 commit into
mainfrom
claude/wincompose-install-menu-3f8cy8
Aug 1, 2026
Merged

Add the release pipeline, matching the other PolyKybd repos#2
thpoll83 merged 1 commit into
mainfrom
claude/wincompose-install-menu-3f8cy8

Conversation

@thpoll83

@thpoll83 thpoll83 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

The fork had no way to publish a release. build.yml is compile-only by its own admission ("Packaging … is not covered here") and uploads the portable zip as a workflow artifact; nothing ever produced WinCompose-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 the release-notes branch and attaches the built assets.

.github/workflows/release.yml

Builds on windows-latest: InsertIcons → Release build → the (dormant) signing step → the installer. The installer comes from src/installer/installer.csproj, whose Build target shells out to "%ProgramFiles(x86)%\Inno Setup 6\iscc" — a hardcoded path, hence choco install innosetup rather than relying on the runner image. installer.iss derives the version from the built exe's version resource and writes WinCompose-Setup-<ver>.exe to src/.

Packaging notes:

  • The portable layout is assembled exactly as build.yml does (the raw bin dir isn't runnable — a .pdb next to the exe makes Utils think it's a debug build and the rule lookup loses Emoji.txt/WinCompose.txt). Those two steps must stay in step; change them together.
  • SHA256SUMS.txt now 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.
  • Assets are uploaded as workflow artifacts too, and the release steps are guarded on release/tag, so workflow_dispatch is a safe end-to-end smoke test that touches no release.
  • submodules: recursive is required: installer.iss pulls its wizard resources from the issrc submodule (INNODIR ../3rdparty/innosetup/Files).

scripts/publish_release.py

The shared script from PolyKybdHost/qmk_firmware with a wincompose branch added to detect() / parse_version(): version from the csproj <AssemblyVersion>, tag prefix PK- (per GitVersion.yml), target branch main. Verified against this repo:

repo    : thpoll83/wincompose  (wincompose)
tag     : PK-0.9.16   target: main
title   : WinCompose 0.9.16 — the first PolyKybd build
body    : 3134 chars, 66 lines

⚠️ That script is documented as byte-identical across the three repos. This PR moves the wincompose copy ahead of the other two — they still work correctly for their own repos, but resyncing them is a loose end worth closing.

src/Makefile

Its all: target printed download URLs pointing at ell1010/wincompose (inherited from the fork this was based on) and used a v tag prefix. Both now match this repo and its PK- tags.

Release notes

PK-0.9.16.md is staged on the new release-notes branch, 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.yml only — release.yml doesn't run on PRs by design. The packaging path is best verified with a workflow_dispatch run 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

python scripts/publish_release.py --dry-run   # preview
python scripts/publish_release.py             # publish PK-0.9.16

Publishing is what starts the build and attaches the assets. It needs GH_TOKEN/GITHUB_TOKEN or gh auth login — I can't do it from Claude Code on the web (no create-release API, and the git proxy rejects refs/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:

  • Add a cross-repo publish_release.py script that prepares and publishes GitHub releases based on staged release-notes content.
  • Provide a GitHub Actions release workflow to build the WinCompose installer, portable zip, and checksum file and attach them to tagged or published releases.

Enhancements:

  • Update Makefile release URLs and tag format to point to the thpoll83/wincompose repository and PK- prefixed tags.
  • Ensure both installer and portable artifacts are included in checksum generation and uploaded as workflow artifacts for inspection.

Summary by CodeRabbit

  • New Features

    • Added automated Windows release builds for installers and portable packages.
    • Releases now include SHA256 checksum files and may include signed binaries.
    • Published releases can automatically include generated release notes and downloadable build artifacts.
  • Bug Fixes

    • Updated download links to point to the current repository and release format, ensuring users can access the correct installer and portable archive.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @thpoll83, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Release automation

Layer / File(s) Summary
Publisher detection and release inputs
scripts/publish_release.py
Detects repository type, parses versions, finds prepared tags, resolves authentication, and supports --dry-run and --tag.
Publisher selection and GitHub release updates
scripts/publish_release.py
Parses release notes, selects a release tag, and creates or updates GitHub releases with validation and API error handling.
Windows build and artifact assembly
.github/workflows/release.yml
Builds WinCompose and installer assets, optionally signs binaries, creates the portable ZIP and SHA256 checksums, and uploads artifacts.
Workflow release publication and download links
.github/workflows/release.yml, src/Makefile
Retrieves crafted notes, publishes or updates releases, and changes download URLs to the PK-$(VERSION) release path.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding a release pipeline aligned with the other PolyKybd repositories.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds 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 CI

sequenceDiagram
    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)
Loading

File-Level Changes

Change Details Files
Introduce a Windows-based GitHub Actions release workflow that builds, signs, packages, and publishes WinCompose release assets tied to PK-* tags and release events.
  • Add Release workflow triggers for PK-* tag pushes, published releases, and manual workflow_dispatch runs
  • Configure checkout with recursive submodules and full history for GitVersion compatibility
  • Install Inno Setup via Chocolatey to match installer.csproj's hardcoded iscc path
  • Build InsertIcons helper, main WinCompose project, and installer via msbuild Release configuration
  • Optionally sign binaries using Azure Trusted Signing when signing secrets are present
  • Package a portable layout by copying Release binaries, stripping .pdbs, and including required rules and helper .bat files
  • Derive version from built wincompose.exe, assemble installer and portable zip, and generate SHA256SUMS.txt for both assets
  • Upload release assets as workflow artifacts on all runs
  • Fetch crafted release notes from the release-notes branch when present and fall back to generated notes otherwise
  • Create or update GitHub Releases and attach assets, guarded to avoid touching releases on workflow_dispatch runs
.github/workflows/release.yml
Add a shared Python script to auto-detect the PolyKybd repo, read staged release notes, and create or update GitHub Releases using the PK- tag scheme for wincompose.
  • Implement repo detection across firmware, host, and wincompose based on repo layout and set tag prefixes and default branches accordingly
  • Read prepared release notes from the release-notes branch, choose the newest prepared tag or a specific tag via CLI, and parse title/body
  • Parse version information from firmware config.h, host _version.py, or wincompose csproj AssemblyVersion
  • Determine owner/repo from git remote and ensure execution inside a git repository
  • Obtain GitHub auth token from environment or gh auth token, with clear failure messaging when unavailable
  • Call GitHub REST API to GET, PATCH, or POST releases, updating existing releases or creating new ones with make_latest=true
  • Provide a --dry-run mode that prints the intended release metadata and notes without modifying GitHub state
scripts/publish_release.py
Align Makefile-generated release URLs and tag scheme with the thpoll83/wincompose fork and PK-* tags.
  • Update installer download URL to point at thpoll83/wincompose releases using PK- tag prefix instead of ell1010/wincompose v-prefixed tags
  • Update portable zip download URL similarly to use thpoll83/wincompose and PK- tags
src/Makefile

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@thpoll83
thpoll83 merged commit 34def87 into main Aug 1, 2026
2 of 4 checks passed
thpoll83 pushed a commit to thpoll83/PolyKybdHost that referenced this pull request Aug 1, 2026
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
.github/workflows/release.yml (1)

69-83: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider pinning the signing action and checking its current name.

azure/trusted-signing-action@v0 is 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

📥 Commits

Reviewing files that changed from the base of the PR and between b2f1011 and e894a3a.

📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • scripts/publish_release.py
  • src/Makefile

Comment on lines +40 to +47
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Suggested change
- 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

Comment on lines +105 to +112
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +115 to +133
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")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +173 to +190
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.)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

Comment on lines +226 to +248
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')}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread src/Makefile
Comment on lines +45 to +48
@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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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
done

Repository: 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:


🌐 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)
  • InstallerMD5 and InstallerSHA256 via certutil
  • The portable checksum labels are reversed:
    • PortableSHA256 runs MD5
    • PortableMD5 runs sha256

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants