Skip to content

ci: add PyPI/npm/Homebrew publishing, harden release.yml, install contributors-please - #46

Merged
smorin merged 7 commits into
mainfrom
ci/publish-destinations
Jul 16, 2026
Merged

ci: add PyPI/npm/Homebrew publishing, harden release.yml, install contributors-please#46
smorin merged 7 commits into
mainfrom
ci/publish-destinations

Conversation

@smorin

@smorin smorin commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the locked distribution plan (2026-06-07) plus release-pipeline hardening and contributors-please, rendered from the canonical repo-please-setup templates.

Publish destinations (release.yml, fires on the v* tag)

  • PyPI (togl): maturin bindings=bin wheels (both toggle+togl commands, no sdist) — build-wheels matrix → publish-testpypi (env testpypi, auto smoke test) → publish-pypi (env pypi, required reviewer). OIDC trusted publishing on both registries.
  • npm: esbuild-style — unscoped wrapper 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 (env npm).
  • Homebrew: prebuilt-binary Formula/togl.rb pushed to smorinlabs/homebrew-tap via HOMEBREW_TAP_TOKEN fine-grained PAT (the one non-OIDC credential).

Hardening (security invariants)

  • permissions: {} top-level; per-job grants only
  • no-cancel concurrency group
  • new guards job: tag-on-main ancestry check + tag==workspace-version check, needed by every job
  • release archives renamed toggle-<target>togl-<target> and now contain both binaries (npm/Homebrew consume them); nothing referenced the old names

contributors-please

  • update-contributors.yml (pinned v1.3.9) + .contributors.yml + seeded CONTRIBUTORS.md markers; loop protection via paths-ignore

Docs

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 crates env, crates.io publishing changes from automatic to approval-gated.

https://claude.ai/code/session_014DjTALJi3LJhYmrdpw6Rej

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added automated contributor tracking with generated CONTRIBUTORS.md.
    • Introduced expanded distribution pipeline for PyPI/TestPyPI, npm, and Homebrew.
    • Added platform-specific npm packages and togl/toggle CLI wrapper.
    • Release archives now package both binaries per target.
  • Security

    • Hardened release workflow with deny-all permissions and tag/version provenance checks before publishing.
    • Publishing to registries uses trusted authentication and gated environments.
  • Documentation

    • Updated release and packaging documentation for publish destinations and npm layout.

smorin added 4 commits July 15, 2026 16:39
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
Copilot AI review requested due to automatic review settings July 15, 2026 23:41
@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

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

Changes

Distribution pipeline

Layer / File(s) Summary
Packaging contracts
pyproject.toml, npm/...
PyPI metadata and maturin settings are added, alongside npm wrapper and platform package manifests.
npm runtime entrypoints
npm/togl-cli/...
The npm wrapper selects platform binaries, exposes togl and toggle, and propagates execution results.
Guarded release and publishing pipeline
.github/workflows/release.yml, .github/actionlint.yaml
Release tags are validated for ref, ancestry, and version alignment before GitHub Release, crates.io, PyPI, TestPyPI, npm, and Homebrew publishing.
Release planning and documentation
RELEASE.md, PROJECTS.md
Release destinations, archive naming, credential setup, and distribution tasks are documented.

Contributor automation

Layer / File(s) Summary
Contributor generation configuration
.contributors.yml, CONTRIBUTORS.md
Contributor generation inputs, formatting, filtering, classification, and insertion markers are defined.
Contributor update workflow
.github/workflows/update-contributors.yml
Push and manual triggers run the contributor action with permissions to update files and open pull requests.

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
Loading

Possibly related PRs

  • smorin/toggle#41: Updates the crates.io release path to use Trusted Publishing via OIDC.

Poem

I’m a rabbit with packages to spare,
Sending little binaries everywhere.
Tags guard the gate, wheels take flight,
npm and Homebrew sparkle bright.
Contributors hop into place—
Release day wears a carrot face!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: release publishing, release hardening, and adding contributors-please automation.
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.

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.

Copilot AI 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.

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.yml to 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.

Comment on lines 17 to 20
@@ -6,14 +20,63 @@ on:
- 'v[0-9]+.[0-9]+.[0-9]+*'
Comment on lines +65 to +66
TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_VERSION="$(sed -n '/^\[workspace\.package\]/,/^\[/p' Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -1)"
Comment on lines +209 to +210
# TestPyPI publishes automatically as the smoke test; PyPI (below) sits
# behind the `pypi` environment's required reviewer.
Comment thread RELEASE.md Outdated
Comment on lines +23 to +26
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).

@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)
CONTRIBUTORS.md (1)

3-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0eaec74 and 9415ce3.

📒 Files selected for processing (16)
  • .contributors.yml
  • .github/workflows/release.yml
  • .github/workflows/update-contributors.yml
  • CONTRIBUTORS.md
  • PROJECTS.md
  • RELEASE.md
  • npm/README.md
  • npm/platform/darwin-arm64/package.json
  • npm/platform/darwin-x64/package.json
  • npm/platform/linux-x64/package.json
  • npm/platform/win32-x64/package.json
  • npm/togl-cli/bin/toggle.js
  • npm/togl-cli/bin/togl.js
  • npm/togl-cli/lib/run.js
  • npm/togl-cli/package.json
  • pyproject.toml

Comment on lines +12 to +13
# - production-publish jobs run in GitHub environments so a required reviewer
# is the final human gate

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

🧩 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 -n

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

Repository: smorin/toggle

Length of output: 4110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '300,380p' .github/workflows/release.yml | cat -n

Repository: smorin/toggle

Length of output: 4110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "HOMEBREW_TAP_TOKEN|update-homebrew|environment:" .github/workflows/release.yml

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

Comment on lines +25 to +27
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false # never cancel an in-flight publish

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

Comment on lines +280 to +286
- 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

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 | 🟡 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' || true

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

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


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

Comment on lines +331 to +343
# 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

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

🧩 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
done

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

Repository: smorin/toggle

Length of output: 5510


🏁 Script executed:

set -euo pipefail
sed -n '300,370p' .github/workflows/release.yml

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


🏁 Script executed:

set -euo pipefail

sed -n '300,370p' .github/workflows/release.yml

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

Comment on lines +25 to +27
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
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 | 🟡 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.

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

Comment thread PROJECTS.md
Comment on lines +308 to +317
## [-] 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)

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

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +336 to +337
for plat in linux-x64 darwin-x64 darwin-arm64 win32-x64; do
(cd "npm/platform/${plat}" && npm publish --access public)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@cubic-dev-ai cubic-dev-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.

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

Comment thread .contributors.yml
- "tests/**"
- "benches/**"
- "nix/**"
default_category: code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread npm/togl-cli/package.json
"togl": "bin/togl.js",
"toggle": "bin/toggle.js"
},
"files": [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread npm/togl-cli/lib/run.js
console.error(`togl-cli: failed to launch ${exe}: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status === null ? 1 : result.status);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread pyproject.toml Outdated
classifiers = [
"Environment :: Console",
"Programming Language :: Rust",
"Operating System :: OS Independent",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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

@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: 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 win

Add environment and concurrency controls to the update-homebrew job.

  • Environment gate: The job reads repository-level App Token secrets (TAP_PUSH_APP_*) directly, meaning a required reviewer never blocks access. Add environment: homebrew to enforce a human gate (similar to the npm job).
  • Concurrency serialization: Different tags can trigger concurrent release workflows. To prevent out-of-order execution where an older release overwrites tap/Formula/togl.rb after a newer one, add a job-level concurrency group.
🔒️ 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 win

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.

📌 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 win

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.

🔄 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9415ce3 and b87bf44.

📒 Files selected for processing (5)
  • .github/actionlint.yaml
  • .github/workflows/release.yml
  • PROJECTS.md
  • RELEASE.md
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • pyproject.toml
  • PROJECTS.md
  • RELEASE.md

Comment on lines +403 to +411
- 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

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


🏁 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}')
PY

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

@cubic-dev-ai cubic-dev-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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +196 to +200
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
target: ${{ matrix.target }}
args: --release --locked --out dist

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@smorin
smorin merged commit 55395a4 into main Jul 16, 2026
19 of 20 checks passed
@smorin
smorin deleted the ci/publish-destinations branch July 17, 2026 05:16
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