ci: add PyPI/npm/Homebrew publishing, harden release.yml, install contributors-please - #46
Conversation
PyPI: maturin bindings=bin packages the toggle/togl binaries as wheel entry points (package name: togl). npm: esbuild-style layout — unscoped togl-cli wrapper + 4 @smorinlabs/togl-* platform packages; versions are stamped from the tag at publish time. Claude-Session: https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
Deny-all top-level permissions with per-job grants, no-cancel concurrency, and a guards job (tag-on-main ancestry check + tag==workspace-version check) that every job needs. Release archives are renamed togl-<target> and now ship both binaries. New jobs: build-wheels (maturin matrix), publish-testpypi -> publish-pypi (OIDC), publish-npm (OIDC trusted publishing, 5 packages), update-homebrew (Formula/togl.rb in smorinlabs/homebrew-tap via HOMEBREW_TAP_TOKEN). Claude-Session: https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
CONTRIBUTORS.md automation via smorinlabs/contributors-please-action (mode: pull-request). Loop protection via paths-ignore on the generated files; App credentials provisioned separately as repo secrets. Claude-Session: https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
Also fixes the stale RELEASE_PLEASE_APP_ID mention (the workflow migrated to client-id auth in #43). Claude-Session: https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Caution Review failedAn error occurred during the review process. Please try again later. 📝 WalkthroughWalkthroughThe changes add PyPI and npm packaging, expand guarded release publishing to multiple registries and Homebrew, document distribution procedures, and introduce automated contributor-file updates through GitHub Actions. ChangesDistribution pipeline
Contributor automation
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Tag
participant Guards
participant BuildRelease
participant Registries
participant Homebrew
Tag->>Guards: trigger release workflow
Guards->>Guards: validate ref, ancestry, and version
Guards->>BuildRelease: authorize build
BuildRelease->>Registries: publish release artifacts
Registries->>Homebrew: provide Darwin archives
Homebrew->>Homebrew: update formula
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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 |
There was a problem hiding this comment.
Pull request overview
This PR implements a multi-destination release pipeline triggered by v* tags, adding publishing to PyPI (maturin bin wheels), npm (wrapper + platform packages), and Homebrew (tap formula), plus hardening release.yml and introducing contributors-please automation.
Changes:
- Extend
release.ymlto build release archives once and publish to crates.io, TestPyPI/PyPI, npm, and a Homebrew tap, with additional provenance guards and reduced default permissions. - Add packaging scaffolding for PyPI (
pyproject.toml) and npm (npm/wrapper + platform packages). - Add contributors-please configuration and workflow, and update docs/planning notes.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
RELEASE.md |
Documents new publish destinations and release pipeline behavior. |
pyproject.toml |
Adds PyPI packaging configuration using maturin bindings = "bin". |
PROJECTS.md |
Records the distribution plan and implementation checklist (P12). |
npm/togl-cli/package.json |
Defines the npm wrapper package with optional platform dependencies. |
npm/togl-cli/lib/run.js |
Adds runtime shim to locate and spawn the platform-specific binary. |
npm/togl-cli/bin/togl.js |
Node entrypoint shim for togl. |
npm/togl-cli/bin/toggle.js |
Node entrypoint shim for toggle. |
npm/README.md |
Documents the esbuild-style npm package layout and publish flow. |
npm/platform/win32-x64/package.json |
Declares the Windows x64 platform package metadata. |
npm/platform/linux-x64/package.json |
Declares the Linux x64 (musl) platform package metadata. |
npm/platform/darwin-x64/package.json |
Declares the macOS x64 platform package metadata. |
npm/platform/darwin-arm64/package.json |
Declares the macOS arm64 platform package metadata. |
CONTRIBUTORS.md |
Seeds contributors file with contributors-please markers. |
.github/workflows/update-contributors.yml |
Adds contributors-please automation workflow (pinned action). |
.github/workflows/release.yml |
Implements hardened release pipeline + new publish jobs (crates/PyPI/npm/Homebrew). |
.contributors.yml |
Adds contributors-please configuration for classification and rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -6,14 +20,63 @@ on: | |||
| - 'v[0-9]+.[0-9]+.[0-9]+*' | |||
| TAG_VERSION="${GITHUB_REF_NAME#v}" | ||
| PKG_VERSION="$(sed -n '/^\[workspace\.package\]/,/^\[/p' Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -1)" |
| # TestPyPI publishes automatically as the smoke test; PyPI (below) sits | ||
| # behind the `pypi` environment's required reviewer. |
| Every registry that supports OIDC trusted publishing uses it — no stored | ||
| registry tokens. Production publishes run in GitHub environments; the `pypi`, | ||
| `npm`, and `crates` environments have a required reviewer as the final human | ||
| gate (TestPyPI publishes automatically as the smoke test). |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
CONTRIBUTORS.md (1)
3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate project name to match the new CLI name.
The PR notes that the release archives and package names are being standardized to
togl. Consider updating the project name here for consistency.📝 Proposed change
-Thanks to everyone who has contributed to toggle! This file is maintained +Thanks to everyone who has contributed to togl! This file is maintained automatically by [contributors-please](https://github.com/smorinlabs/contributors-please) — entries between the markers are generated; do not edit them by hand.🤖 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 `@CONTRIBUTORS.md` around lines 3 - 5, Update the project name reference in CONTRIBUTORS.md from “toggle” to the standardized CLI and package name “togl,” while preserving the contributors-please maintenance notice and generated-content markers.
🤖 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 25-27: Update the workflow’s concurrency configuration to
serialize all release runs that update the Homebrew tap, rather than grouping by
github.ref. Use a single release-wide concurrency group while preserving
cancel-in-progress: false, so older in-flight publishes cannot overwrite newer
formula versions.
- Around line 331-343: Update the publish steps for all four platform packages
in the “Publish platform packages” loop and for togl-cli in “Publish togl-cli”
to check whether each package’s name@version already exists via npm view before
publishing. Skip successful existence checks and publish only missing versions,
while preserving failures for other registry errors and keeping the existing
package order.
- Around line 280-286: Update the “Ensure npm >= 11.5.1 (trusted publishing
floor)” release step to install a fixed npm version at or above 11.5.1 instead
of using npm@latest, ensuring deterministic workflow behavior while preserving
the existing global installation.
- Around line 12-13: Update the update-homebrew job to declare environment:
homebrew and reference the Homebrew token through that environment’s secret
rather than the repository-level secrets.HOMEBREW_TAP_TOKEN, preserving the
existing token usage and job behavior.
In @.github/workflows/update-contributors.yml:
- Around line 25-27: Update the actions/checkout step in the update-contributors
workflow to disable credential persistence by setting its persist-credentials
option to false. Keep fetch-depth: 0 and the existing checkout action unchanged.
In `@PROJECTS.md`:
- Around line 308-317: Align P12’s documented target version with the release
version stated in RELEASE.md, or add an explicit explanation in the P12 entry
for targeting a later release; update the relevant version references while
preserving the existing release goals and decisions.
---
Nitpick comments:
In `@CONTRIBUTORS.md`:
- Around line 3-5: Update the project name reference in CONTRIBUTORS.md from
“toggle” to the standardized CLI and package name “togl,” while preserving the
contributors-please maintenance notice and generated-content markers.
🪄 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
Run ID: 5b9d2677-4a90-4557-9340-98495d075b90
📒 Files selected for processing (16)
.contributors.yml.github/workflows/release.yml.github/workflows/update-contributors.ymlCONTRIBUTORS.mdPROJECTS.mdRELEASE.mdnpm/README.mdnpm/platform/darwin-arm64/package.jsonnpm/platform/darwin-x64/package.jsonnpm/platform/linux-x64/package.jsonnpm/platform/win32-x64/package.jsonnpm/togl-cli/bin/toggle.jsnpm/togl-cli/bin/togl.jsnpm/togl-cli/lib/run.jsnpm/togl-cli/package.jsonpyproject.toml
| # - production-publish jobs run in GitHub environments so a required reviewer | ||
| # is the final human gate |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files .github/workflows/release.yml
wc -l .github/workflows/release.yml
cat -n .github/workflows/release.yml | sed -n '1,260p'Repository: smorin/toggle
Length of output: 11506
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant workflow sections with line numbers.
sed -n '1,120p' .github/workflows/release.yml | cat -n
printf '\n---\n'
sed -n '320,380p' .github/workflows/release.yml | cat -nRepository: smorin/toggle
Length of output: 8427
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the full update-homebrew job and line numbers around the cited range.
sed -n '300,380p' .github/workflows/release.yml | cat -nRepository: smorin/toggle
Length of output: 4110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '300,380p' .github/workflows/release.yml | cat -nRepository: smorin/toggle
Length of output: 4110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "HOMEBREW_TAP_TOKEN|update-homebrew|environment:" .github/workflows/release.ymlRepository: smorin/toggle
Length of output: 540
Add the homebrew environment gate to update-homebrew. The job still reads secrets.HOMEBREW_TAP_TOKEN directly, so the required reviewer never blocks access to the PAT; move it to the homebrew environment secret and add environment: homebrew.
🤖 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 12 - 13, Update the
update-homebrew job to declare environment: homebrew and reference the Homebrew
token through that environment’s secret rather than the repository-level
secrets.HOMEBREW_TAP_TOKEN, preserving the existing token usage and job
behavior.
| concurrency: | ||
| group: release-${{ github.ref }} | ||
| cancel-in-progress: false # never cancel an in-flight publish |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='.github/workflows/release.yml'
wc -l "$file"
nl -ba "$file" | sed -n '1,220p'Repository: smorin/toggle
Length of output: 225
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='.github/workflows/release.yml'
printf '\n== top of file ==\n'
sed -n '1,120p' "$file"
printf '\n== middle / Homebrew-related matches ==\n'
rg -n -C 4 'homebrew|brew|concurrency|publish|release' "$file"
printf '\n== release job block around the publish steps ==\n'
sed -n '120,260p' "$file"Repository: smorin/toggle
Length of output: 20243
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file='.github/workflows/release.yml'
sed -n '401,418p' "$file"Repository: smorin/toggle
Length of output: 871
Serialize Homebrew tap updates across release runs. Lines 25-27 let different tag releases run concurrently, and this job blindly overwrites tap/Formula/togl.rb before pushing. An older run can land after a newer one and downgrade the formula; use a single release-wide concurrency group or reject stale versions before pushing.
🤖 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 25 - 27, Update the workflow’s
concurrency configuration to serialize all release runs that update the Homebrew
tap, rather than grouping by github.ref. Use a single release-wide concurrency
group while preserving cancel-in-progress: false, so older in-flight publishes
cannot overwrite newer formula versions.
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 24 | ||
| registry-url: https://registry.npmjs.org | ||
|
|
||
| - name: Ensure npm >= 11.5.1 (trusted publishing floor) | ||
| run: npm install -g npm@latest |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== release workflow excerpt ==="
sed -n '240,320p' .github/workflows/release.yml
echo
echo "=== search for setup-node usage in workflows ==="
rg -n "actions/setup-node|package-manager-cache|npm install -g npm@latest|trusted publishing|trusted-publishing" .github/workflows .github -g '*.yml' -g '*.yaml' || trueRepository: smorin/toggle
Length of output: 5013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== package.json files ==="
git ls-files '**/package.json'
echo
echo "=== packageManager fields ==="
python3 - <<'PY'
import json, pathlib
for path in pathlib.Path('.').rglob('package.json'):
try:
data = json.loads(path.read_text())
except Exception:
continue
if 'packageManager' in data or 'packageManagers' in data:
print(path)
print(' packageManager =', data.get('packageManager'))
PY
echo
echo "=== setup-node docs references in repository (if any) ==="
rg -n "package-manager-cache|setup-node|cache" . -g 'README*' -g '*.md' -g '*.yml' -g '*.yaml' || trueRepository: smorin/toggle
Length of output: 5099
🌐 Web query:
actions/setup-node package-manager-cache default automatic caching package.json packageManager npm trusted publishing
💡 Result:
The actions/setup-node action features automatic caching for npm dependencies, which is enabled by default [1][2]. Automatic Caching Behavior - Detection: Caching is automatically enabled for npm when either the top-level packageManager field or the devEngines.packageManager field in package.json is set to npm, and no explicit cache input is provided [1][2]. - Control: This behavior is governed by the package-manager-cache input, which defaults to true [1][3]. - Security Recommendation: For workflows with elevated privileges or sensitive information, it is recommended to disable automatic caching by setting package-manager-cache: false if caching is not explicitly required [1][2]. Trusted Publishing and Authentication When using npm's Trusted Publisher (OIDC) setup, there are specific considerations regarding actions/setup-node configuration: - Authentication Conflict: The action's registry-url input automatically writes an _authToken placeholder to the .npmrc file to support classic token-based authentication [4][5]. - Trusted Publisher Impact: In some cases, the presence of this _authToken line in .npmrc can interfere with the npm Trusted Publisher (OIDC) flow, potentially leading to authentication errors (such as E404 or ENEEDAUTH) if NODE_AUTH_TOKEN is not set [4][5]. - Status: Documentation regarding the interaction between actions/setup-node and npm Trusted Publishing has been updated to clarify these requirements [4]. Users utilizing OIDC should ensure their workflow configuration appropriately manages NODE_AUTH_TOKEN or aligns with recommended practices for Trusted Publisher environments [4][5].
Citations:
- 1: https://github.com/actions/setup-node?tab=readme-ov-file
- 2: https://github.com/actions/setup-node/tree/refs/heads/main
- 3: https://github.com/actions/setup-node
- 4: Trusted Publisher documentation actions/setup-node#1445
- 5: registry-url writes _authToken line that breaks npm Trusted Publisher OIDC when no NODE_AUTH_TOKEN is set actions/setup-node#1551
Pin the npm CLI. npm install -g npm@latest makes the release job nondeterministic; install a fixed version at or above the trusted-publishing floor instead.
🧰 Tools
🪛 zizmor (1.26.1)
[error] 280-280: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default
(cache-poisoning)
[warning] 286-286: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile
(adhoc-packages)
🤖 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 280 - 286, Update the “Ensure npm
>= 11.5.1 (trusted publishing floor)” release step to install a fixed npm
version at or above 11.5.1 instead of using npm@latest, ensuring deterministic
workflow behavior while preserving the existing global installation.
Source: Linters/SAST tools
| # Platform packages first — the wrapper's optionalDependencies must be | ||
| # resolvable the moment it lands. | ||
| - name: Publish platform packages | ||
| run: | | ||
| set -euo pipefail | ||
| for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do | ||
| (cd "npm/platform/${plat}" && npm publish --access public) | ||
| done | ||
|
|
||
| - name: Publish togl-cli | ||
| run: | | ||
| set -euo pipefail | ||
| cd npm/togl-cli && npm publish |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
# Show the relevant workflow section with line numbers.
sed -n '300,370p' .github/workflows/release.yml
# Inspect package manifests for the published packages.
for f in npm/platform/linux-x64/package.json \
npm/platform/darwin-x64/package.json \
npm/platform/darwin-arm64/package.json \
npm/platform/win32-x64/package.json \
npm/togl-cli/package.json; do
echo "===== $f ====="
cat "$f"
echo
doneRepository: smorin/toggle
Length of output: 5510
🏁 Script executed:
sed -n '300,370p' .github/workflows/release.yml
for f in npm/platform/linux-x64/package.json \
npm/platform/darwin-x64/package.json \
npm/platform/darwin-arm64/package.json \
npm/platform/win32-x64/package.json \
npm/togl-cli/package.json; do
echo "===== $f ====="
cat "$f"
echo
doneRepository: smorin/toggle
Length of output: 5510
🏁 Script executed:
set -euo pipefail
sed -n '300,370p' .github/workflows/release.ymlRepository: smorin/toggle
Length of output: 3119
🌐 Web query:
npm publish re-run same version already published behavior npm view check existing version workflow restart-safe
💡 Result:
When you attempt to run npm publish for a package name and version combination that has already been published to the registry, the command will fail with an error [1][2]. Specifically, npm will return a 403 Forbidden or 409 Conflict error, often described as Cannot publish over existing version [3][4][5]. Once a specific name and version combination has been published, it is permanent; it cannot be reused or overwritten, even if the version is subsequently unpublished [1][2]. To implement a restart-safe workflow that avoids this error, you should verify if the version already exists before attempting to publish. Common approaches include: 1. Manual Verification: You can use the npm view command to check the current version of the package in the registry [6]. For example, running npm view version will return the latest published version, which you can then compare against your local package.json [7]. 2. Automated Checks in CI/CD: - Scripted Checks: You can write custom scripts that fetch the latest version from the registry (e.g., using an HTTP request to the registry URL) and compare it against your local package.json version before executing npm publish [8]. - GitHub Actions: There are pre-built community actions, such as npm-published-version-check, designed to output whether a package version already exists, allowing your workflow to conditionally skip the publish step [9]. By integrating these checks, you can ensure your deployment pipelines are restart-safe and prevent build failures caused by EPUBLISHCONFLICT errors [3][4].
Citations:
- 1: https://docs.npmjs.com/cli/v11/commands/npm-publish/
- 2: https://docs.npmjs.com/cli/v10/commands/npm-publish/
- 3: https://blog.npmjs.org/post/81614852121/error-code-epublishconflict-and-cannot-publish-over.html
- 4: https://stackoverflow.com/questions/22870569/cant-publish-version-to-npm
- 5: https://stackoverflow.com/questions/51392276/npm-err-publish-you-cannot-publish-over-the-previously-published
- 6: https://docs.npmjs.com/cli/v8/commands/npm-view/
- 7: https://stackoverflow.com/questions/58623833/is-there-a-way-to-check-if-an-npm-package-needs-a-publish-and-only-do-a-publish
- 8: https://github.com/activepieces/activepieces/blob/4bfad273/tools/scripts/utils/package-pre-publish-checks.ts
- 9: https://github.com/marketplace/actions/npm-published-version-check
🏁 Script executed:
set -euo pipefail
sed -n '300,370p' .github/workflows/release.ymlRepository: smorin/toggle
Length of output: 3119
Make the five npm publishes idempotent. A rerun after a partial failure will hit already-published name@version pairs and stop on the first duplicate; skip existing versions before each publish, including togl-cli, e.g. by checking npm view <name>@<version> first.
🤖 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 331 - 343, Update the publish
steps for all four platform packages in the “Publish platform packages” loop and
for togl-cli in “Publish togl-cli” to check whether each package’s name@version
already exists via npm view before publishing. Skip successful existence checks
and publish only missing versions, while preserving failures for other registry
errors and keeping the existing package order.
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| fetch-depth: 0 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Disable credential persistence in the checkout step.
Since the downstream step uses custom credentials (App ID / PAT) to authenticate Git operations, it's a security best practice to prevent actions/checkout from persisting the default GITHUB_TOKEN in the local Git configuration.
🔒️ Proposed fix
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
+ persist-credentials: false📝 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 25-27: 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/update-contributors.yml around lines 25 - 27, Update the
actions/checkout step in the update-contributors workflow to disable credential
persistence by setting its persist-credentials option to false. Keep
fetch-depth: 0 and the existing checkout action unchanged.
Source: Linters/SAST tools
| ## [-] Project P12: Distribution — PyPI, npm, Homebrew (v0.6.0) | ||
| **Goal**: Ship the togl CLI to PyPI (`togl`), npm (`togl-cli` + | ||
| `@smorinlabs/togl-*` platform packages), and Homebrew | ||
| (`smorinlabs/tap/togl`) from the existing tag-triggered `release.yml`, | ||
| with OIDC trusted publishing everywhere a registry supports it. Also | ||
| hardens `release.yml` (guards job: tag-on-main ancestry + tag==version, | ||
| deny-all top-level permissions, no-cancel concurrency) and installs | ||
| contributors-please. | ||
|
|
||
| **Decisions** (locked 2026-06-07) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Align the documented release version.
PROJECTS.md targets v0.6.0, while RELEASE.md documents v0.3.0 as the first managed release from the 0.2.3 baseline. Update one source or explicitly explain why P12 targets a later release.
🤖 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 `@PROJECTS.md` around lines 308 - 317, Align P12’s documented target version
with the release version stated in RELEASE.md, or add an explicit explanation in
the P12 entry for targeting a later release; update the relevant version
references while preserving the existing release goals and decisions.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9415ce3e16
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - name: Publish togl-cli | ||
| run: | | ||
| set -euo pipefail | ||
| cd npm/togl-cli && npm publish |
There was a problem hiding this comment.
Use a prerelease dist-tag for npm RCs
Because this workflow still matches prerelease tags such as v0.6.0-rc.1, this npm publish runs for RC releases too. The npm publish docs list the default tag as latest (https://docs.npmjs.com/cli/v8/commands/npm-publish/#tag), so an RC publish here would make npm install togl-cli resolve to the prerelease; compute a non-latest tag such as --tag rc/next for prerelease tags before publishing the wrapper and platform packages.
Useful? React with 👍 / 👎.
| for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do | ||
| (cd "npm/platform/${plat}" && npm publish --access public) |
There was a problem hiding this comment.
Make npm publishes safe to retry
If any later publish in this loop or the wrapper publish fails after an earlier platform package has succeeded, rerunning the failed job starts here again and npm publish will fail on the already-published name/version before reaching the remaining packages. npm documents that an existing package name/version combination makes publish fail (https://docs.npmjs.com/cli/v8/commands/npm-publish/#description), so this release job needs to skip versions already present in the registry or split the publishes so partial successes can be retried.
Useful? React with 👍 / 👎.
| - name: Render Formula/togl.rb | ||
| run: | | ||
| set -euo pipefail | ||
| VERSION="${GITHUB_REF_NAME#v}" |
There was a problem hiding this comment.
Skip Homebrew tap updates for RC tags
When the planned vX.Y.Z-rc.1 prerelease tag exercises the pipeline, this job still derives VERSION directly from the tag and pushes Formula/togl.rb to the stable tap. That makes normal Homebrew installs pick up the RC as the tap's current formula, unlike registries that have prerelease channels/tags; skip this job for prerelease versions or publish RCs through a separate Homebrew channel.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
8 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".contributors.yml">
<violation number="1" location=".contributors.yml:36">
P0: Schema validation will reject this config: the `classification` block requires a `default` property (an object with `id` and `label`), but the file uses `default_category: code` — neither the key name nor the value type matches the schema. This means the tool will fail at runtime when it tries to validate the config, blocking the update-contributors workflow entirely.</violation>
</file>
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:233">
P2: TestPyPI publication never smoke-tests the uploaded wheel, so a wheel with broken entry points can be promoted to PyPI after a successful upload. Add an install-and-invoke check from the TestPyPI index before allowing `publish-pypi` to proceed.</violation>
<violation number="2" location=".github/workflows/release.yml:337">
P2: If a later platform package publish (or the wrapper publish) fails after one or more platform packages have already been published, re-running this job will fail immediately on the already-published package with a 403 error (`You cannot publish over the previously published version`). This makes partial failures unrecoverable without manual intervention. Consider checking whether each version already exists in the registry before calling `npm publish` (e.g. `npm view <pkg>@<version>` returning 0 means skip), or tolerating the 403 exit code for already-published versions.</violation>
<violation number="3" location=".github/workflows/release.yml:343">
P1: When a prerelease tag like `v0.6.0-rc.1` triggers this workflow, `npm publish` here (and the platform packages loop above) will tag the RC as `latest` because npm defaults to `--tag latest`. This means `npm install togl-cli` would resolve to the RC for all users. Consider computing a dist-tag from the version string (e.g. `--tag next` or `--tag rc` when the version contains a hyphen) before publishing both the platform packages and the wrapper.</violation>
<violation number="4" location=".github/workflows/release.yml:349">
P1: The `update-homebrew` job runs unconditionally for any tag matching `v[0-9]+.[0-9]+.[0-9]+*`, including prerelease tags like `v0.6.0-rc.1`. Since Homebrew taps have no concept of prerelease channels, this would push an RC formula as the stable version, causing `brew install smorinlabs/tap/togl` to install the RC for all users. Add an `if` condition to skip this job when the tag contains a hyphen (prerelease indicator), e.g. `if: "!contains(github.ref_name, '-')"`, or gate it behind a dedicated environment with required reviewer.</violation>
</file>
<file name="npm/togl-cli/package.json">
<violation number="1" location="npm/togl-cli/package.json:14">
P2: Missing `"preferUnplugged": true` on the wrapper and all four platform packages. This is the same architecture esbuild uses (wrapper + per-platform binary packages), and esbuild's canonical fix is to set this field on every package. Without it, Yarn PnP (v2+) keeps these packages in zip archives, causing `require.resolve` to fail because the binary file isn't extracted to disk. Consider adding `"preferUnplugged": true` to all five `package.json` files.</violation>
</file>
<file name="npm/togl-cli/lib/run.js">
<violation number="1" location="npm/togl-cli/lib/run.js:34">
P2: The wrapper masks signal-based child termination (e.g., SIGSEGV, SIGTERM, SIGKILL) as exit code 1 instead of propagating the signal. When the Rust binary is killed by a signal, `spawnSync` returns `{ signal: 'SIGTERM', status: null }`, but this code always exits with 1. Consider propagating the signal via `process.kill(process.pid, result.signal)` or at minimum using `128 + signal_number` to preserve the real exit context for CI and shell consumers.</violation>
</file>
<file name="pyproject.toml">
<violation number="1" location="pyproject.toml:20">
P2: The `"Operating System :: OS Independent"` classifier is incorrect for a maturin `bindings = "bin"` project. Maturin builds platform-specific wheels containing native Rust binaries, so the distribution is OS-dependent. Either remove this classifier entirely (cleanest option), or replace it with the specific OS classifiers matching your build targets (e.g., `"Operating System :: POSIX :: Linux"`, `"Operating System :: MacOS :: MacOS X"`, `"Operating System :: Microsoft :: Windows"`).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| - "tests/**" | ||
| - "benches/**" | ||
| - "nix/**" | ||
| default_category: code |
There was a problem hiding this comment.
P0: Schema validation will reject this config: the classification block requires a default property (an object with id and label), but the file uses default_category: code — neither the key name nor the value type matches the schema. This means the tool will fail at runtime when it tries to validate the config, blocking the update-contributors workflow entirely.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .contributors.yml, line 36:
<comment>Schema validation will reject this config: the `classification` block requires a `default` property (an object with `id` and `label`), but the file uses `default_category: code` — neither the key name nor the value type matches the schema. This means the tool will fail at runtime when it tries to validate the config, blocking the update-contributors workflow entirely.</comment>
<file context>
@@ -0,0 +1,36 @@
+ - "tests/**"
+ - "benches/**"
+ - "nix/**"
+ default_category: code
</file context>
| # HOMEBREW_TAP_TOKEN fine-grained PAT (owner smorinlabs, only homebrew-tap, | ||
| # Contents R/W) is the one non-OIDC credential in this pipeline — a | ||
| # cross-repo git push has no OIDC path. | ||
| update-homebrew: |
There was a problem hiding this comment.
P1: The update-homebrew job runs unconditionally for any tag matching v[0-9]+.[0-9]+.[0-9]+*, including prerelease tags like v0.6.0-rc.1. Since Homebrew taps have no concept of prerelease channels, this would push an RC formula as the stable version, causing brew install smorinlabs/tap/togl to install the RC for all users. Add an if condition to skip this job when the tag contains a hyphen (prerelease indicator), e.g. if: "!contains(github.ref_name, '-')", or gate it behind a dedicated environment with required reviewer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 349:
<comment>The `update-homebrew` job runs unconditionally for any tag matching `v[0-9]+.[0-9]+.[0-9]+*`, including prerelease tags like `v0.6.0-rc.1`. Since Homebrew taps have no concept of prerelease channels, this would push an RC formula as the stable version, causing `brew install smorinlabs/tap/togl` to install the RC for all users. Add an `if` condition to skip this job when the tag contains a hyphen (prerelease indicator), e.g. `if: "!contains(github.ref_name, '-')"`, or gate it behind a dedicated environment with required reviewer.</comment>
<file context>
@@ -108,3 +163,256 @@ jobs:
+ # HOMEBREW_TAP_TOKEN fine-grained PAT (owner smorinlabs, only homebrew-tap,
+ # Contents R/W) is the one non-OIDC credential in this pipeline — a
+ # cross-repo git push has no OIDC path.
+ update-homebrew:
+ name: Update Homebrew tap
+ needs: build-release
</file context>
| - name: Publish togl-cli | ||
| run: | | ||
| set -euo pipefail | ||
| cd npm/togl-cli && npm publish |
There was a problem hiding this comment.
P1: When a prerelease tag like v0.6.0-rc.1 triggers this workflow, npm publish here (and the platform packages loop above) will tag the RC as latest because npm defaults to --tag latest. This means npm install togl-cli would resolve to the RC for all users. Consider computing a dist-tag from the version string (e.g. --tag next or --tag rc when the version contains a hyphen) before publishing both the platform packages and the wrapper.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 343:
<comment>When a prerelease tag like `v0.6.0-rc.1` triggers this workflow, `npm publish` here (and the platform packages loop above) will tag the RC as `latest` because npm defaults to `--tag latest`. This means `npm install togl-cli` would resolve to the RC for all users. Consider computing a dist-tag from the version string (e.g. `--tag next` or `--tag rc` when the version contains a hyphen) before publishing both the platform packages and the wrapper.</comment>
<file context>
@@ -108,3 +163,256 @@ jobs:
+ - name: Publish togl-cli
+ run: |
+ set -euo pipefail
+ cd npm/togl-cli && npm publish
+
+ # Bump the prebuilt-binary formula in smorinlabs/homebrew-tap. The
</file context>
|
|
||
| - name: Publish to TestPyPI | ||
| run: | | ||
| uv publish --trusted-publishing always \ |
There was a problem hiding this comment.
P2: TestPyPI publication never smoke-tests the uploaded wheel, so a wheel with broken entry points can be promoted to PyPI after a successful upload. Add an install-and-invoke check from the TestPyPI index before allowing publish-pypi to proceed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 233:
<comment>TestPyPI publication never smoke-tests the uploaded wheel, so a wheel with broken entry points can be promoted to PyPI after a successful upload. Add an install-and-invoke check from the TestPyPI index before allowing `publish-pypi` to proceed.</comment>
<file context>
@@ -108,3 +163,256 @@ jobs:
+
+ - name: Publish to TestPyPI
+ run: |
+ uv publish --trusted-publishing always \
+ --publish-url https://test.pypi.org/legacy/ \
+ --check-url https://test.pypi.org/simple/ \
</file context>
| "togl": "bin/togl.js", | ||
| "toggle": "bin/toggle.js" | ||
| }, | ||
| "files": [ |
There was a problem hiding this comment.
P2: Missing "preferUnplugged": true on the wrapper and all four platform packages. This is the same architecture esbuild uses (wrapper + per-platform binary packages), and esbuild's canonical fix is to set this field on every package. Without it, Yarn PnP (v2+) keeps these packages in zip archives, causing require.resolve to fail because the binary file isn't extracted to disk. Consider adding "preferUnplugged": true to all five package.json files.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At npm/togl-cli/package.json, line 14:
<comment>Missing `"preferUnplugged": true` on the wrapper and all four platform packages. This is the same architecture esbuild uses (wrapper + per-platform binary packages), and esbuild's canonical fix is to set this field on every package. Without it, Yarn PnP (v2+) keeps these packages in zip archives, causing `require.resolve` to fail because the binary file isn't extracted to disk. Consider adding `"preferUnplugged": true` to all five `package.json` files.</comment>
<file context>
@@ -0,0 +1,27 @@
+ "togl": "bin/togl.js",
+ "toggle": "bin/toggle.js"
+ },
+ "files": [
+ "bin",
+ "lib"
</file context>
| console.error(`togl-cli: failed to launch ${exe}: ${result.error.message}`); | ||
| process.exit(1); | ||
| } | ||
| process.exit(result.status === null ? 1 : result.status); |
There was a problem hiding this comment.
P2: The wrapper masks signal-based child termination (e.g., SIGSEGV, SIGTERM, SIGKILL) as exit code 1 instead of propagating the signal. When the Rust binary is killed by a signal, spawnSync returns { signal: 'SIGTERM', status: null }, but this code always exits with 1. Consider propagating the signal via process.kill(process.pid, result.signal) or at minimum using 128 + signal_number to preserve the real exit context for CI and shell consumers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At npm/togl-cli/lib/run.js, line 34:
<comment>The wrapper masks signal-based child termination (e.g., SIGSEGV, SIGTERM, SIGKILL) as exit code 1 instead of propagating the signal. When the Rust binary is killed by a signal, `spawnSync` returns `{ signal: 'SIGTERM', status: null }`, but this code always exits with 1. Consider propagating the signal via `process.kill(process.pid, result.signal)` or at minimum using `128 + signal_number` to preserve the real exit context for CI and shell consumers.</comment>
<file context>
@@ -0,0 +1,37 @@
+ console.error(`togl-cli: failed to launch ${exe}: ${result.error.message}`);
+ process.exit(1);
+ }
+ process.exit(result.status === null ? 1 : result.status);
+}
+
</file context>
| classifiers = [ | ||
| "Environment :: Console", | ||
| "Programming Language :: Rust", | ||
| "Operating System :: OS Independent", |
There was a problem hiding this comment.
P2: The "Operating System :: OS Independent" classifier is incorrect for a maturin bindings = "bin" project. Maturin builds platform-specific wheels containing native Rust binaries, so the distribution is OS-dependent. Either remove this classifier entirely (cleanest option), or replace it with the specific OS classifiers matching your build targets (e.g., "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows").
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 20:
<comment>The `"Operating System :: OS Independent"` classifier is incorrect for a maturin `bindings = "bin"` project. Maturin builds platform-specific wheels containing native Rust binaries, so the distribution is OS-dependent. Either remove this classifier entirely (cleanest option), or replace it with the specific OS classifiers matching your build targets (e.g., `"Operating System :: POSIX :: Linux"`, `"Operating System :: MacOS :: MacOS X"`, `"Operating System :: Microsoft :: Windows"`).</comment>
<file context>
@@ -0,0 +1,30 @@
+classifiers = [
+ "Environment :: Console",
+ "Programming Language :: Rust",
+ "Operating System :: OS Independent",
+]
+dynamic = ["version"]
</file context>
| run: | | ||
| set -euo pipefail | ||
| for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do | ||
| (cd "npm/platform/${plat}" && npm publish --access public) |
There was a problem hiding this comment.
P2: If a later platform package publish (or the wrapper publish) fails after one or more platform packages have already been published, re-running this job will fail immediately on the already-published package with a 403 error (You cannot publish over the previously published version). This makes partial failures unrecoverable without manual intervention. Consider checking whether each version already exists in the registry before calling npm publish (e.g. npm view <pkg>@<version> returning 0 means skip), or tolerating the 403 exit code for already-published versions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 337:
<comment>If a later platform package publish (or the wrapper publish) fails after one or more platform packages have already been published, re-running this job will fail immediately on the already-published package with a 403 error (`You cannot publish over the previously published version`). This makes partial failures unrecoverable without manual intervention. Consider checking whether each version already exists in the registry before calling `npm publish` (e.g. `npm view <pkg>@<version>` returning 0 means skip), or tolerating the 403 exit code for already-published versions.</comment>
<file context>
@@ -108,3 +163,256 @@ jobs:
+ run: |
+ set -euo pipefail
+ for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do
+ (cd "npm/platform/${plat}" && npm publish --access public)
+ done
+
</file context>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 721ca2ce02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 24 | ||
| registry-url: https://registry.npmjs.org |
There was a problem hiding this comment.
Remove setup-node auth before OIDC publishes
In the npm job as configured for trusted publishing (no NODE_AUTH_TOKEN, only id-token: write), this registry-url input makes setup-node create an .npmrc auth entry for ${NODE_AUTH_TOKEN}. npm's trusted-publishing docs say npm detects OIDC before falling back to tokens, but npm/documentation#1960 reproduces this exact setup and shows the unset token expands to empty, causing npm publish to try classic auth and fail with ENEEDAUTH/E404 before OIDC. Drop registry-url/write only the registry line, or strip _authToken before publishing.
Useful? React with 👍 / 👎.
Mint a short-lived tap-scoped token via create-github-app-token@v3 (client-id auth, owner=smorinlabs, repositories=homebrew-tap) instead of the HOMEBREW_TAP_TOKEN fine-grained PAT — the same App technique as release-please.yml; the pipeline now holds zero long-lived credentials. Extends the actionlint stale-metadata suppression to release.yml. Claude-Session: https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)
351-358: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd
environmentandconcurrencycontrols to theupdate-homebrewjob.
- Environment gate: The job reads repository-level App Token secrets (
TAP_PUSH_APP_*) directly, meaning a required reviewer never blocks access. Addenvironment: homebrewto enforce a human gate (similar to thenpmjob).- Concurrency serialization: Different tags can trigger concurrent release workflows. To prevent out-of-order execution where an older release overwrites
tap/Formula/togl.rbafter a newer one, add a job-levelconcurrencygroup.🔒️ Proposed fix
update-homebrew: name: Update Homebrew tap needs: build-release runs-on: ubuntu-latest timeout-minutes: 15 + environment: homebrew + concurrency: homebrew-tap permissions: contents: read # gh release download🤖 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 351 - 358, Update the update-homebrew job to use the homebrew environment so its required reviewer gate applies before accessing TAP_PUSH_APP_* secrets. Add a job-level concurrency group keyed to the Homebrew update workflow, preventing concurrent release jobs from updating the tap out of order while preserving the existing job behavior.
♻️ Duplicate comments (2)
.github/workflows/release.yml (2)
285-286: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winPin the npm CLI.
npm install -g npm@latestmakes the release job nondeterministic. Install a fixed version at or above the trusted-publishing floor instead.📌 Proposed fix
- name: Ensure npm >= 11.5.1 (trusted publishing floor) - run: npm install -g npm@latest + run: npm install -g npm@11.5.1🤖 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 285 - 286, Update the “Ensure npm >= 11.5.1 (trusted publishing floor)” workflow step to install a fixed npm version at or above 11.5.1 instead of using the floating npm@latest tag.
333-344: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winMake the five npm publishes idempotent.
A rerun after a partial failure will hit already-published
name@versionpairs and stop on the first duplicate. Skip existing versions before each publish, includingtogl-cli, e.g., by checkingnpm view <name>@<version>first.🔄 Proposed fix to check version existence before publishing
- - name: Publish platform packages - run: | - set -euo pipefail - for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do - (cd "npm/platform/${plat}" && npm publish --access public) - done - - - name: Publish togl-cli - run: | - set -euo pipefail - cd npm/togl-cli && npm publish + - name: Publish platform packages + run: | + set -euo pipefail + for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do + (cd "npm/platform/${plat}" && { + name=$(npm pkg get name | tr -d '"') + version=$(npm pkg get version | tr -d '"') + if npm view "${name}@${version}" version >/dev/null 2>&1; then + echo "Skipping ${name}@${version}, already published." + else + npm publish --access public + fi + }) + done + + - name: Publish togl-cli + run: | + set -euo pipefail + (cd npm/togl-cli && { + name=$(npm pkg get name | tr -d '"') + version=$(npm pkg get version | tr -d '"') + if npm view "${name}@${version}" version >/dev/null 2>&1; then + echo "Skipping ${name}@${version}, already published." + else + npm publish + fi + })🤖 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 333 - 344, Update the “Publish platform packages” loop and “Publish togl-cli” step so each package checks whether its current name@version already exists via npm view before running npm publish. Skip existing versions and publish only missing ones, preserving failure handling for unexpected npm errors so reruns complete without stopping on duplicates.
🤖 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 403-411: Update the “Mint tap-push app token” step using
actions/create-github-app-token@v3 to add permission-contents: write,
restricting the generated token to repository contents write access while
preserving the existing owner, repository, and credential configuration.
---
Outside diff comments:
In @.github/workflows/release.yml:
- Around line 351-358: Update the update-homebrew job to use the homebrew
environment so its required reviewer gate applies before accessing
TAP_PUSH_APP_* secrets. Add a job-level concurrency group keyed to the Homebrew
update workflow, preventing concurrent release jobs from updating the tap out of
order while preserving the existing job behavior.
---
Duplicate comments:
In @.github/workflows/release.yml:
- Around line 285-286: Update the “Ensure npm >= 11.5.1 (trusted publishing
floor)” workflow step to install a fixed npm version at or above 11.5.1 instead
of using the floating npm@latest tag.
- Around line 333-344: Update the “Publish platform packages” loop and “Publish
togl-cli” step so each package checks whether its current name@version already
exists via npm view before running npm publish. Skip existing versions and
publish only missing ones, preserving failure handling for unexpected npm errors
so reruns complete without stopping on duplicates.
🪄 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
Run ID: 59fe7b3f-541a-404c-8073-0bd94782fadd
📒 Files selected for processing (5)
.github/actionlint.yaml.github/workflows/release.ymlPROJECTS.mdRELEASE.mdpyproject.toml
🚧 Files skipped from review as they are similar to previous changes (3)
- pyproject.toml
- PROJECTS.md
- RELEASE.md
| - name: Mint tap-push app token | ||
| id: tap-token | ||
| uses: actions/create-github-app-token@v3 | ||
| with: | ||
| client-id: ${{ secrets.TAP_PUSH_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.TAP_PUSH_APP_PRIVATE_KEY }} | ||
| owner: smorinlabs | ||
| repositories: homebrew-tap | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
How to restrict permissions using the permissions input in actions/create-github-app-token?
💡 Result:
To restrict permissions using the actions/create-github-app-token action, you should explicitly define the required permissions using inputs prefixed with permission- [1][2][3]. By default, if no specific permission inputs are provided, the generated token inherits all permissions currently granted to the GitHub App installation [4][3][5]. To implement granular, downscoped permissions: 1. Identify the permissions required for your workflow (e.g., contents, issues, pull-requests) [3]. 2. Add these as inputs to the action, using the naming convention permission-<scope> [1][3]. 3. Set the value to either read or write (or admin, depending on the specific permission) [6][2][7]. For example, to grant only read access to repository contents and write access to issues, your workflow configuration would look like this [3]: - uses: actions/create-github-app-token@v3 with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} permission-contents: read permission-issues: write Important considerations: - Principle of Least Privilege: Explicitly listing only the necessary permissions is considered a security best practice [1][5]. - Installation Constraints: The permissions you request via these inputs must already be granted to the GitHub App installation [8][3]. If you request a permission that the installation does not possess, the action will fail [2][3]. - Mapping: The action maps these inputs (e.g., permission-pull-requests) to the corresponding GitHub API scopes (e.g., pull_requests) [1]. You can find all available permission inputs defined in the action's official action.yml file [6][7][9].
Citations:
- 1: https://deepwiki.com/actions/create-github-app-token/4.2-permission-system
- 2: https://deepwiki.com/actions/create-github-app-token/4.1-action-inputs
- 3: https://github.com/actions/create-github-app-token?tab=readme-ov-file
- 4: https://github.com/actions/create-github-app-token/blob/34c66235f6ccc38eb01b7aed860890bdf40db33c/README.md
- 5: https://deepwiki.com/actions/create-github-app-token/2-getting-started
- 6: https://github.com/actions/create-github-app-token/blob/df432cee/action.yml
- 7: https://github.com/actions/create-github-app-token/blob/93c1f04d6f14ea5b416e7a8dfd80446101c6adef/action.yml
- 8: https://github.com/actions/create-github-app-token
- 9: https://github.com/actions/create-github-app-token/blob/29824e69f54612133e76f7eaac726eef6c875baf/action.yml
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant workflow section and any top-level permissions settings.
sed -n '1,140p' .github/workflows/release.yml
printf '\n--- snip ---\n'
sed -n '380,430p' .github/workflows/release.yml
printf '\n--- action inputs ---\n'
python3 - <<'PY'
from pathlib import Path
import re
p = Path('.github/workflows/release.yml')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
if 'create-github-app-token@v3' in line or 'permission-' in line or 'permissions:' in line:
print(f'{i}: {line}')
PYRepository: smorin/toggle
Length of output: 7422
Scope this app token to contents: write. This step only clones and pushes to smorinlabs/homebrew-tap, so pass permission-contents: write to actions/create-github-app-token instead of minting a token with every app permission.
🧰 Tools
🪛 zizmor (1.26.1)
[error] 405-405: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions
(github-app)
🤖 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 403 - 411, Update the “Mint
tap-push app token” step using actions/create-github-app-token@v3 to add
permission-contents: write, restricting the generated token to repository
contents write access while preserving the existing owner, repository, and
credential configuration.
Source: Linters/SAST tools
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:405">
P2: Homebrew publishing executes mutable third-party action code with the tap App private key. Pin `actions/create-github-app-token` to a reviewed full commit SHA so a retargeted `v3` tag cannot exfiltrate the key or mint tap-write tokens.</violation>
<violation number="2" location=".github/workflows/release.yml:405">
P2: The app token minted here inherits every permission the GitHub App installation grants by default. Since this step only needs to clone and push to the tap repo, scope it down by adding `permission-contents: write`. This follows the principle of least privilege and silences the zizmor `github-app` finding.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
|
|
||
| - name: Mint tap-push app token | ||
| id: tap-token | ||
| uses: actions/create-github-app-token@v3 |
There was a problem hiding this comment.
P2: Homebrew publishing executes mutable third-party action code with the tap App private key. Pin actions/create-github-app-token to a reviewed full commit SHA so a retargeted v3 tag cannot exfiltrate the key or mint tap-write tokens.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 405:
<comment>Homebrew publishing executes mutable third-party action code with the tap App private key. Pin `actions/create-github-app-token` to a reviewed full commit SHA so a retargeted `v3` tag cannot exfiltrate the key or mint tap-write tokens.</comment>
<file context>
@@ -398,9 +400,18 @@ jobs:
+ - name: Mint tap-push app token
+ id: tap-token
+ uses: actions/create-github-app-token@v3
+ with:
+ client-id: ${{ secrets.TAP_PUSH_APP_CLIENT_ID }}
</file context>
|
|
||
| - name: Mint tap-push app token | ||
| id: tap-token | ||
| uses: actions/create-github-app-token@v3 |
There was a problem hiding this comment.
P2: The app token minted here inherits every permission the GitHub App installation grants by default. Since this step only needs to clone and push to the tap repo, scope it down by adding permission-contents: write. This follows the principle of least privilege and silences the zizmor github-app finding.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 405:
<comment>The app token minted here inherits every permission the GitHub App installation grants by default. Since this step only needs to clone and push to the tap repo, scope it down by adding `permission-contents: write`. This follows the principle of least privilege and silences the zizmor `github-app` finding.</comment>
<file context>
@@ -398,9 +400,18 @@ jobs:
+ - name: Mint tap-push app token
+ id: tap-token
+ uses: actions/create-github-app-token@v3
+ with:
+ client-id: ${{ secrets.TAP_PUSH_APP_CLIENT_ID }}
</file context>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b87bf44de8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - name: Build wheel | ||
| uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 | ||
| with: | ||
| target: ${{ matrix.target }} | ||
| args: --release --locked --out dist |
There was a problem hiding this comment.
Sync the path dependency before building wheels
This new wheel job runs maturin against crates/togl-cli without the version-sync step used later in publish-crates. On the first release after 0.5.0 (for example v0.6.0), release-please bumps [workspace.package], so the local togl-lib package is 0.6.0 while crates/togl-cli still requires togl-lib = "0.5.0"; Cargo rejects that path dependency before maturin can produce any wheels, blocking both TestPyPI and PyPI publishing. Mirror the dependency-version patch before this build step or have release-please update the dependency requirement.
Useful? React with 👍 / 👎.
Summary
Implements the locked distribution plan (2026-06-07) plus release-pipeline hardening and contributors-please, rendered from the canonical
repo-please-setuptemplates.Publish destinations (release.yml, fires on the v* tag)
togl): maturinbindings=binwheels (bothtoggle+toglcommands, no sdist) —build-wheelsmatrix →publish-testpypi(envtestpypi, auto smoke test) →publish-pypi(envpypi, required reviewer). OIDC trusted publishing on both registries.togl-cli+ 4 platform packages@smorinlabs/togl-{linux-x64,darwin-x64,darwin-arm64,win32-x64}(linux = musl). Binaries unpacked from release tarballs, versions stamped from the tag, all 5 published via OIDC trusted publishing (envnpm).Formula/togl.rbpushed to smorinlabs/homebrew-tap viaHOMEBREW_TAP_TOKENfine-grained PAT (the one non-OIDC credential).Hardening (security invariants)
permissions: {}top-level; per-job grants onlyguardsjob: tag-on-main ancestry check + tag==workspace-version check, needed by every jobtoggle-<target>→togl-<target>and now contain both binaries (npm/Homebrew consume them); nothing referenced the old namescontributors-please
update-contributors.yml(pinned v1.3.9) +.contributors.yml+ seededCONTRIBUTORS.mdmarkers; loop protection via paths-ignoreDocs
RELEASE_PLEASE_APP_IDmention (client-id migration landed in ci: migrate release-please to client-id auth + bump to @v3 #43)Follow-ups outside this PR (infra/secrets, done via gh api + repo-secrets)
Environment reviewers + v* tag rules (crates/npm/testpypi), branch protection on main, contributors-please App secrets, HOMEBREW_TAP_TOKEN, and the registry trusted-publisher forms (PyPI/TestPyPI/npm — exact values in the session checklist).
Note: with the required reviewer on the
cratesenv, crates.io publishing changes from automatic to approval-gated.https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej
Summary by CodeRabbit
New Features
CONTRIBUTORS.md.togl/toggleCLI wrapper.Security
Documentation