Skip to content

Commit 23184be

Browse files
authored
Merge pull request #18 from Kilo-Org/ci/maintainer-app-committer
ci(release) use maintainer app committer
2 parents 3e16818 + 868f90b commit 23184be

5 files changed

Lines changed: 170 additions & 104 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: "Setup Git Committer"
2+
description: |
3+
Exchange the kilo-maintainer GitHub App credentials for a short-lived
4+
installation token, configure git to commit as `kilo-maintainer[bot]`,
5+
and rewrite the origin remote so subsequent `git push` calls from the
6+
workflow use the App token.
7+
8+
This is what lets the publish workflow push release commits to `main`
9+
on a repo whose branch ruleset restricts direct pushes: the ruleset is
10+
configured to bypass the `kilo-maintainer` App specifically, not the
11+
generic `github-actions[bot]` identity every workflow shares.
12+
13+
Mirror of Kilo-Org/kilocode's action of the same name so the two repos
14+
stay in sync on the committer identity.
15+
inputs:
16+
kilo-maintainer-app-id:
17+
description: "Kilo Maintainer GitHub App ID (numeric)"
18+
required: true
19+
kilo-maintainer-app-secret:
20+
description: "Kilo Maintainer GitHub App private key (full PEM)"
21+
required: true
22+
outputs:
23+
token:
24+
description: "Short-lived installation token for the kilo-maintainer App"
25+
value: ${{ steps.apptoken.outputs.token }}
26+
app-slug:
27+
description: "App slug (expected: kilo-maintainer)"
28+
value: ${{ steps.apptoken.outputs.app-slug }}
29+
runs:
30+
using: "composite"
31+
steps:
32+
- name: Create app token
33+
id: apptoken
34+
uses: actions/create-github-app-token@v2
35+
with:
36+
app-id: ${{ inputs['kilo-maintainer-app-id'] }}
37+
private-key: ${{ inputs['kilo-maintainer-app-secret'] }}
38+
owner: ${{ github.repository_owner }}
39+
# Scope the minted installation token to the current repo
40+
# only, even when the kilo-maintainer App is installed on
41+
# multiple repos in the org. Without this, the token would
42+
# carry the App's full installation scope (e.g. both
43+
# shell-security and kilocode) and a compromised workflow
44+
# could push to unrelated repos.
45+
repositories: ${{ github.event.repository.name }}
46+
47+
- name: Configure git user
48+
shell: bash
49+
run: |
50+
slug="${{ steps.apptoken.outputs.app-slug }}"
51+
git config --global user.name "${slug}[bot]"
52+
git config --global user.email "${slug}[bot]@users.noreply.github.com"
53+
54+
# actions/checkout@v4 leaves an `http.https://github.com/.extraheader`
55+
# config entry pointing at its own (GITHUB_TOKEN) authorization. Any
56+
# `git push` would still use that header unless we drop it here —
57+
# otherwise the App token we embed in the remote URL below would be
58+
# shadowed and the bypass list wouldn't apply.
59+
- name: Clear checkout auth
60+
shell: bash
61+
run: |
62+
git config --local --unset-all http.https://github.com/.extraheader || true
63+
64+
- name: Configure git remote
65+
shell: bash
66+
run: |
67+
git remote set-url origin https://x-access-token:${{ steps.apptoken.outputs.token }}@github.com/${{ github.repository }}

.github/workflows/publish.yml

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.channel }}-${{
3434
# id-token:write is required for npm provenance (SLSA attestation).
3535
# This workflow must run on GitHub-hosted runners (not Blacksmith) for
3636
# provenance to work — GitHub's OIDC token is only issued on their infra.
37+
#
38+
# contents: read is sufficient. Post-publish pushes to `main` are
39+
# authenticated by the kilo-maintainer App token (minted by the
40+
# setup-git-committer composite action), not by GITHUB_TOKEN. If a
41+
# future edit accidentally introduces a git/gh call that falls back
42+
# to GITHUB_TOKEN for a write, we want it to fail loudly here rather
43+
# than silently succeed with broader privilege.
3744
permissions:
3845
id-token: write
39-
contents: write
46+
contents: read
4047

4148
jobs:
4249
publish:
@@ -136,11 +143,19 @@ jobs:
136143
echo "::warning::Could not verify $VERSION on the registry after 60s of polling. The publish step itself reported success; verification is informational only and the workflow will continue to the tag/release steps."
137144
exit 0
138145
139-
- name: Configure git identity
146+
# Swap from the default `github-actions[bot]` identity to the
147+
# `kilo-maintainer` GitHub App. Its installation token is what
148+
# authorizes the post-publish push to `main` (the app is the sole
149+
# principal on main's branch-ruleset bypass list). Using a
150+
# purpose-built app keeps the bypass narrowly scoped to the
151+
# publish flow instead of every workflow in this repo.
152+
- name: Setup git committer (kilo-maintainer App)
153+
id: committer
140154
if: steps.publish.outcome == 'success'
141-
run: |
142-
git config user.name "github-actions[bot]"
143-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
155+
uses: ./.github/actions/setup-git-committer
156+
with:
157+
kilo-maintainer-app-id: ${{ secrets.KILO_MAINTAINER_APP_ID }}
158+
kilo-maintainer-app-secret: ${{ secrets.KILO_MAINTAINER_APP_SECRET }}
144159

145160
# Atomic git/GH operations bundled into ONE step:
146161
# 1. Build the local commit (on main for stable, on detached HEAD for dev)
@@ -153,15 +168,25 @@ jobs:
153168
# All network operations have internal 3x retries with 5s backoff.
154169
# If anything fails after retries, the next step prints recovery
155170
# instructions.
171+
# Explicit guard on BOTH publish AND committer succeeding. Without
172+
# the committer half, a skipped committer step (e.g. someone edits
173+
# its `if:` in the future) would leave `steps.committer.outputs.token`
174+
# as an empty string — gh/git calls here would then fail with an
175+
# opaque 401 instead of a clear "committer was skipped/failed"
176+
# signal. Guard is cheap; diagnostic value is high.
156177
- name: Tag and release (post-publish)
157178
id: tag_and_release
158-
if: steps.publish.outcome == 'success'
179+
if: steps.publish.outcome == 'success' && steps.committer.outcome == 'success'
159180
env:
160181
TAG: ${{ steps.version.outputs.tag }}
161182
VERSION: ${{ steps.version.outputs.version }}
162183
CHANNEL: ${{ steps.version.outputs.channel }}
163184
PREVIEW: ${{ steps.version.outputs.preview }}
164-
GH_TOKEN: ${{ github.token }}
185+
# Use the kilo-maintainer App token for both `git push` (via the
186+
# origin remote URL rewritten by the committer step above) and
187+
# `gh release create` (this env var). The default GITHUB_TOKEN
188+
# would be refused by main's branch ruleset.
189+
GH_TOKEN: ${{ steps.committer.outputs.token }}
165190
run: |
166191
set -euo pipefail
167192

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ node_modules/
22
dist/
33
*.tgz
44

5+
# Local release/bootstrap scripts that may contain npm classic tokens.
6+
# Never commit these. The real release path is `bun script/publish.ts`
7+
# driven by the GitHub Actions workflow (OIDC); any local `release.sh`
8+
# or `publish-local.sh` is a one-off bootstrap artifact.
9+
release.sh
10+
release-*.sh
11+
publish-local.sh
12+
513
# Local editor / agent state. Contributors using Claude Code, Cursor, or
614
# similar tools get a per-machine settings file under .claude/. Ignore
715
# the whole directory so nobody accidentally commits their local prefs

AGENTS.md

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,33 +78,30 @@ For full step-by-step release instructions see [RELEASING.md](./RELEASING.md).
7878

7979
### Branch protection and the release commit
8080

81-
The publish workflow pushes commits and/or tags to `main` as
82-
`github-actions[bot]`, using the default `GITHUB_TOKEN`.
81+
The publish workflow pushes commits and/or tags to `main` as the
82+
`kilo-maintainer` GitHub App (the same App used by the kilocode
83+
monorepo), via a short-lived installation token minted by
84+
`.github/actions/setup-git-committer/action.yml`.
8385

8486
- **Stable releases** (`channel=latest`) commit the `package.json` version
8587
bump back to `main` AND push the tag.
8688
- **Dev releases** (`channel=dev`) push only the tag (pointing at an
8789
orphan commit). `main` history stays clean.
8890

89-
Once branch protection / repository rulesets are enabled on `main`, the
90-
`github-actions[bot]` actor **must be added to the ruleset's bypass actors
91-
list**, otherwise stable releases will fail at the push step _after_
92-
`npm publish` has already succeeded — leaving npm and GitHub out of sync.
93-
Dev releases are less affected (no commit to `main`) but still need tag
94-
push to be allowed, which most rulesets permit by default.
95-
96-
This is a stopgap. The long-term plan is to adopt the same `kilo-maintainer`
97-
GitHub App pattern used by the kilocode monorepo
98-
(`kilocode/.github/actions/setup-git-committer/action.yml`), which signs
99-
release commits as the App and has explicit bypass permissions. That
100-
migration requires Kilo-Org admin access to install the App on this repo
101-
and configure secrets, so it's deferred until an org admin is available.
102-
103-
Until then, release commits:
104-
105-
- are authored by `github-actions[bot]`
106-
- are unsigned
107-
- bypass branch protection via the ruleset allowlist (not via an App token)
91+
`main`'s branch ruleset bypass list includes **only** the
92+
`kilo-maintainer` App — not the generic `github-actions[bot]`. This is
93+
deliberate: it keeps the bypass narrowly scoped to the publish flow
94+
(the one place that holds the App's private-key secret) instead of
95+
granting every workflow in the repo push-to-main capability.
96+
97+
Release commits:
98+
99+
- are authored by `kilo-maintainer[bot]`
100+
- are web-signed by GitHub (the App token commits go through the REST
101+
API signing path, not a local git signature)
102+
- bypass branch protection via the App's ruleset bypass
103+
104+
Full setup and troubleshooting in [RELEASING.md](./RELEASING.md#branch-protection).
108105

109106
## Code layout
110107

RELEASING.md

Lines changed: 45 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,22 @@ Releases are cut from the `publish` workflow in GitHub Actions. There is no
44
local release script, no automated release on push, and no changesets tool.
55
Every release is a manual `workflow_dispatch`.
66

7-
> ⚠️ **Current state (as of 2026-04-17):** `github-actions[bot]` is not on
8-
> the `main` branch ruleset's bypass list. As a result, **every stable
9-
> (`channel=latest`) publish run will fail at the "Tag and release
10-
> (post-publish)" step** with `GH013: Repository rule violations found for
11-
refs/heads/main — Changes must be made through a pull request`.
7+
> **Committer identity:** post-publish commits to `main` are made by the
8+
> `kilo-maintainer` GitHub App (not the generic `github-actions[bot]`),
9+
> using a short-lived installation token minted inside the workflow. The
10+
> app is the sole principal on `main`'s branch-ruleset bypass list — see
11+
> [Branch protection](#branch-protection). If you see a release commit
12+
> authored by anyone else, something's wrong; stop and investigate.
1213
>
13-
> **Recovery after a failed stable run:**
14+
> **Required secrets** (already configured, documented here for
15+
> disaster-recovery reference):
1416
>
15-
> 1. Check what actually landed on origin:
17+
> - `KILO_MAINTAINER_APP_ID` — the numeric App ID.
18+
> - `KILO_MAINTAINER_APP_SECRET` — the App's private key (full PEM).
1619
>
17-
> ```bash
18-
> git fetch origin --tags
19-
> git ls-remote --tags origin "vX.Y.Z" # tag?
20-
> gh release view "vX.Y.Z" --repo Kilo-Org/shell-security # release?
21-
> ```
22-
>
23-
> 2. **If the tag exists and only the GitHub release is missing** (the
24-
> typical outcome — tags aren't covered by the branch ruleset and
25-
> usually land even when the `main` push is rejected), create the
26-
> release against the existing tag:
27-
>
28-
> ```bash
29-
> gh release create vX.Y.Z \
30-
> --repo Kilo-Org/shell-security \
31-
> --title vX.Y.Z \
32-
> --generate-notes \
33-
> --verify-tag
34-
> ```
35-
>
36-
> `--verify-tag` makes `gh` fail fast if the tag is missing instead of
37-
> silently creating a new one at current `main` HEAD. That's the whole
38-
> recovery — no other steps needed.
39-
>
40-
> 3. **If the tag is also missing** (rare), follow
41-
> [Scenario 4](#scenario-4-publish-succeeded-but-commit--tag-push-failed)
42-
> below. It rebuilds the release commit locally (you can't tag the
43-
> runner-side SHA; that commit lived only in the Actions runner),
44-
> tags, pushes, then creates the release. The workflow's
45-
> `Print recovery instructions on partial failure` step also prints
46-
> this full sequence inline in the failed run's logs for copy-paste.
47-
>
48-
> After step 2 (common case), `main`'s `package.json` will be one version
49-
> behind. Leave it alone — `script/version.ts` computes the next version
50-
> from git tags, not from `package.json`, so the drift is cosmetic.
51-
>
52-
> This banner can be removed once the ruleset bypass is configured (see
53-
> [Branch protection](#branch-protection)) or the workflow is refactored
54-
> so stable publishes don't push to `main` at all.
20+
> Both are consumed by `.github/actions/setup-git-committer/action.yml`.
21+
> The same pair is used in `Kilo-Org/kilocode`; regenerating the
22+
> private key there would require updating it here too.
5523
5624
## Channels
5725

@@ -282,9 +250,9 @@ This is the most dangerous failure mode. Symptom: `npm publish` succeeds
282250
step passes, but the workflow fails at the **"Commit version bump (stable
283251
only)"** or **"Tag release"** step with a `remote rejected` error.
284252

285-
Most common cause: branch protection on `main` does not include
286-
`github-actions[bot]` in the bypass actors list. See **Branch protection**
287-
below.
253+
Most common cause: `main`'s branch ruleset bypass list does not include
254+
the `kilo-maintainer` GitHub App, or the App's installation token is
255+
expired/revoked. See **Branch protection** below.
288256

289257
Recovery steps:
290258

@@ -329,37 +297,38 @@ Recovery steps:
329297
# Add --prerelease for dev releases.
330298
```
331299

332-
5. Fix the underlying cause (branch protection bypass) before the next release.
300+
5. Verify the App token, App installation, and bypass list are still
301+
valid (see **Branch protection** below) before the next release.
333302

334303
## Branch protection
335304

336-
When branch protection / rulesets are enabled on `main`, the
337-
`github-actions[bot]` actor **must** be added to the ruleset's bypass actors
338-
list. Without it, the publish workflow's stable-channel commit step fails,
339-
triggering the recovery procedure above.
340-
341-
> **Status today:** the `Main branch protection` ruleset on this repo
342-
> (`Settings → Rules → Rulesets`) is active but does NOT include
343-
> `github-actions[bot]` as a bypass actor. This is why the banner at the
344-
> top of this document describes the manual recovery step as expected
345-
> behavior for every stable publish. Two viable durable fixes, pick one:
346-
>
347-
> 1. **Add the bot to the bypass list** (Settings → Rules → the ruleset
348-
>Bypass list → add the `github-actions` app with bypass mode
349-
> `Always`). Fastest; keeps the current workflow unchanged.
350-
> 2. **Refactor the stable publish path** to match the dev-channel flow:
351-
> detach HEAD, commit, tag, push only the tag — never touch `main`.
352-
> Keeps the ruleset strict with no carve-outs; the trade-off is that
353-
> `main`'s `package.json` version drifts behind the latest release
354-
> (cosmetic only, since `version.ts` reads from tags).
355-
356-
Dev-channel publishes don't push to `main` (only push the tag), so they're
357-
less affected by branch protection on `main` itself. The tag push still
358-
needs to be allowed — most rulesets allow tag pushes by default, but if
359-
yours blocks them, allowlist `github-actions[bot]` for tag operations too.
360-
361-
See [AGENTS.md](./AGENTS.md#branch-protection-and-the-release-commit) for the
362-
longer-term plan to replace the bot bypass with a dedicated GitHub App.
305+
The `Main branch protection` ruleset on `main` (Settings → Rules →
306+
Rulesets) restricts direct pushes. The publish workflow bypasses this
307+
restriction by authenticating as the `kilo-maintainer` GitHub App rather
308+
than the default `github-actions[bot]`. Requirements for this to work:
309+
310+
1. **`kilo-maintainer` App is installed on `Kilo-Org/shell-security`.**
311+
Verify at Settings → GitHub Apps → Installed GitHub Apps. The App is
312+
also installed on `Kilo-Org/kilocode`; same App, same credentials.
313+
2. **Two secrets exist in this repo** (or at org level, exposed to this
314+
repo): `KILO_MAINTAINER_APP_ID` (numeric) and `KILO_MAINTAINER_APP_SECRET`
315+
(full PEM private key). Regenerating the App's private key requires
316+
updating `KILO_MAINTAINER_APP_SECRET` in every repo that uses it.
317+
3. **Bypass actor configured on the ruleset.** Settings → Rules →
318+
`Main branch protection`**Bypass list** must include the
319+
`kilo-maintainer` App with bypass mode `Always`. Do NOT add the
320+
generic `github-actions` app — that would grant bypass to every
321+
workflow in the repo; the point of using the dedicated app is
322+
to keep the bypass narrowly scoped to the publish flow.
323+
324+
If a stable publish fails at the push step and all three above are in
325+
place, check the workflow run's `Setup git committer` step output — a
326+
revoked installation or an expired/rotated private key would fail there
327+
rather than at the push.
328+
329+
Dev-channel publishes don't touch `main` (they push only the tag via an
330+
orphan commit), so they don't depend on item 3 above — but they still
331+
need items 1 and 2 for the same token minting flow.
363332

364333
## First publish of a newly-named npm package (OIDC bootstrap)
365334

0 commit comments

Comments
 (0)