Skip to content

ci: let release-please PRs run checks without approval - #69

Merged
ANT1OFF merged 2 commits into
mainfrom
feat/release-pr-ci-approval
Nov 2, 2026
Merged

ANT1OFF merged 2 commits into
mainfrom
feat/release-pr-ci-approval

Conversation

@ANT1OFF

@ANT1OFF ANT1OFF commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Why

Every push to the release-please PR (#66) leaves "Application checks" and "Scaffold boundaries" at action_required, so they only run after someone clicks Approve workflows to run.

Cause. Since June 2026, GitHub holds pull_request runs (opened, synchronize, reopened) for approval when GITHUB_TOKEN created or updated the PR (changelog, GITHUB_TOKEN docs). The repository's fork-PR approval policy (all_external_contributors) plays no part:

  • run 41381041436 (head 698fd0c): event=pull_request, actor=github-actions[bot] (Bot), triggering_actor=github-actions[bot], head repository ANT1OFF/gram, so the PR comes from a branch in this repository, not a fork;
  • all 10 recent action_required runs on release-please's branch look the same.

The docs name only one way around it: create or update the PR with a GitHub App installation token or a PAT. No repository setting exempts the bot, so the approval policy stays as it is.

Repository setting changed

  • Environments. Before: none (total_count: 0). After: release-please, with deployment branch policy custom_branch_policies: true and one branch policy, main (type branch), and can_admins_bypass set to false (it was created as true). There are no reviewers or wait timers and no secrets yet.
  • Unchanged: fork-pr-contributor-approval is still all_external_contributors. Allowed actions, SHA pinning and default workflow permissions are untouched.

What

  • release-please.yml: the propose job now runs in the release-please environment, so the app key can be read only from main. If the environment variable RELEASE_PLEASE_APP_CLIENT_ID is set, the propose job first mints a GitHub App installation token with actions/create-github-app-token, pinned to the v3.3.0 SHA. The token is scoped to this repository, carries only contents: write and pull-requests: write, and is revoked when the job ends. release-please then uses that token. Without the variable, it uses github.token exactly as it does today.
  • Hardening is unchanged. The job is still triggered only by pushes to main or workflow_dispatch and is still API-only, with no checkout or script while it holds a write token. Default and top-level permissions stay read-only. No PR-triggered workflow can read the secret. actions/* is already in the allowed-actions list. Fork approval still applies to new external contributors.
  • tools/test-release-please.mjs: the workflow assertions now cover the new step, its pin, its exact inputs and the token fallback.
  • docs/RELEASING.md: a short "Release app" section with the one-time setup steps.

Setup (the owner has to do this; until then nothing changes)

The steps are in docs/RELEASING.md#release-app: create a GitHub App with Contents and Pull requests read/write and no webhook, install it on ANT1OFF/gram only, then set the secret RELEASE_PLEASE_APP_PRIVATE_KEY and the variable RELEASE_PLEASE_APP_CLIENT_ID in the release-please environment (--env release-please).

Verification

  • node tools/test-release-please.mjs <pinned action npm-ci dir>: 10/10 pass
  • node tools/check-release-please.mjs: OK
  • bun run check:scaffold: pass
  • bun run fmt:check: pass

Not verified live. Proving that the app token removes the approval needs the app and its secret, which only the owner can create. After setup, run gh workflow run release-please.yml --repo ANT1OFF/gram, then confirm the app-token step ran and that the release PR's runs on its next push are not action_required.

Independent review of 60a252e, then a re-review of 6fade81: nothing blocking either time. The re-review also confirmed that the step if resolves environment-level vars and that runs on main pass the branch policy. Following its advice, admin bypass is off. It also cautions that any future main-branch workflow on an untrusted trigger (pull_request_target, workflow_run) must not name this environment. First review: Its two non-blocking points are fixed in 6fade81: the key now sits in a main-only environment, and docs step 5 says what happens to runs already held.

Known limits:

  • Every release-please run now records a deployment to the release-please environment.
  • Release-please pushes to the PR only when the proposed content changes, so the first run that clears approval may be the one after the next feat:/fix: merge.

GitHub holds pull_request runs on PRs that GITHUB_TOKEN opens or pushes for
approval. Mint a short-lived GitHub App token for release-please when the
release app is configured, falling back to GITHUB_TOKEN otherwise.
@ANT1OFF

ANT1OFF commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Merging under the "local ci is good" rule, with CI not green on this head.

  • Failure: run 41385284674 attempt 1 failed e2e 7/8 on one test, e2e/smoke.spec.ts:4 (t-flaky), at getByText("I'll start by reading the test"): 1 failed, 87 passed. qualification failed only because it reports that job.
  • Unrelated to this PR: the diff touches .github/workflows/release-please.yml, docs/RELEASING.md and tools/test-release-please.mjs, with no app code.
  • Seen on main: the same test failed at af3fab7, ce352e6 and c91d9ac, then passed 88/88 at 77b0cbb. It was reported as a known order-dependent failure; I did not reproduce the cause.
  • Local check: Gram's bun install --frozen-lockfile && bun run check:ci passed on head 6fade81 merged onto 77b0cbb.
  • Not waited for: attempt 2 of e2e 7/8 was still running at merge time.

No test was changed or loosened. The flake is still open.

@ANT1OFF
ANT1OFF merged commit e2062a4 into main Nov 2, 2026
36 of 39 checks passed
@ANT1OFF
ANT1OFF deleted the feat/release-pr-ci-approval branch November 2, 2026 04:21
ANT1OFF added a commit that referenced this pull request Nov 2, 2026
#80)

Simplifies how orchestrator work shows in the sidebar and on the Board,
and makes the two agree. Presentation only: features and groups stay in
the server and the ledger, and nothing in `packages/` changes.

### Sidebar
- **Orchestrator → workers.** The feature headings are gone, in the open
sidebar and the compact rail's flyout. A worker's feature stays on its
hover card.
- **Order** was spawn order; it is now needs you, working, waiting on CI
or a review, then the rest, stable within each, with held briefs last.
- **Plain status lines.** The sidebar showed the raw activity (`Running
grep -n …`, `Reading …/scratch/…`, `Running: a background shell • ssh
…`). It now goes through `activityGist`, the Board's own words, so a
command or path can't reach a row. `Waiting on CI · #69` and other
headlines are unchanged.

### Board
- **Ungrouped lines** get an Other heading under the bands, so they no
longer read as the last group's. No heading when no band is above them.
- **Line name** is the worker's title, with `+N` for more workers. The
slug moves to the hover. A line's only worker no longer repeats that
title on its chip.
- **Finished work leaves for Done.** A line goes when its workers are
merged, delivered or archived, or when the ledger no longer has them and
nothing runs, the rule the sidebar already used. The second part is what
clears the stale "reported done" lines: a closed worker's card stays as
it last was. A held brief, a worker still running or asking, and a
merged worker that still has work going all keep a line on the Board.
- **Show archived became Show finished**, and brings all of it back. It
keeps the existing `boardArchivedShown` setting.

### Decision: one-feature groups stay as bands
Stages and the plan editor live on the band. Now that finished lines
leave, a group shrinks to one line mid-effort, so flattening it would
make its stage disappear and reappear as work moved. Bands also stay
stable when a second feature joins.

### Known limits
- A worker that only reported done, and is still in the ledger, stays on
the Board because the orchestrator may act on its report. The sidebar
already hides those, so the two differ there.
- Reading is worded "Reading code" rather than "Reading a file", because
this reuses the Board's words so both surfaces agree.
- Sidebar order follows tone, so a row can move when its state changes.
- Not run against a live provider or real workspace data: screenshots
and e2e use the synthetic mock.

### Verification
On this head, rebased onto `main`:
- `bun run typecheck`, `bun run fmt:check`, `bun run check:scaffold`:
pass
- `vitest run src/orchestration src/views src/thread src/sidebar
src/shell`: 1082 passed
- `bun run --filter @gram/web e2e overview-sidebar board-parity
board-rail board-virtual feature-sheet orchestration workers-list
held-briefs worker-questions getting-around --workers=1`: 105 passed, 1
failed
- The failure is `orchestration.spec.ts:1870`, the digest's report
evidence, which this change doesn't touch. It passed in the earlier run
and 3/3 with `--repeat-each=3` alone, so I read it as a flake under load
on a shared machine.
- Not run: the full `bun run check`, left to the merge gate.

The e2e caught one real bug on the way: an Other heading as its own list
entry had no tab stop, so Tab died there. The heading now belongs to the
first ungrouped line's entry, and `board-virtual`'s Tab test covers it.
Specs that pinned the old headings and `Show archived` are updated, not
deleted.

### Screenshots

**The sidebar's workers under their orchestrator, plain status lines,
and the Board beside** (dark)

| Before | After |
| --- | --- |
| ![overview-board-dark
before](https://github.com/user-attachments/assets/4d802012-bc47-4ad0-90b6-330531db0e1d)
| ![overview-board-dark
after](https://github.com/user-attachments/assets/00ac5c80-fa86-44ad-b2d6-85246fa18602)
|

**Board: finished work gone to Done, Show finished in the bar brings it
back** (dark)

| Before | After |
| --- | --- |
| ![board-finished-dark
before](https://github.com/user-attachments/assets/196799cc-081b-4ce3-9fcf-872fb117a254)
| ![board-finished-dark
after](https://github.com/user-attachments/assets/fd499851-7f48-4dbd-9d93-871cb9d6464f)
|
ANT1OFF added a commit that referenced this pull request Nov 3, 2026
## What

When a worker or an orchestrator is at rest waiting on something, the
sidebar now says what, at a glance: an hourglass and a short line under
its name, and the whole list on hover. The Board's chips and ⌘J's
Workers list read the same selector, so the three surfaces say the same
words.

Presentation only. Nothing in `packages/` changes: everything comes from
the ledger the web already gets (PRs, wakes, watched runs, limit waits)
and the threads store.

### One "waiting on" model
`apps/web/src/orchestration/waiting-on.ts` turns a calm worker's or
orchestrator's waits into:
- `label`: the row's line, e.g. `Waiting · checks on #69`
- `reasons`: every wait, each with a kind, a short form and a full line
- `hint`: the hover, i.e. the label with `+N` when more apply, then
every wait's line

| Kind | Label | Comes from |
| --- | --- | --- |
| checks | `Waiting · checks on #69`, `Waiting · local check #69` |
required checks pending; Gram's local check running |
| review | `Waiting · review on #69` | the review bot reviewing the head
|
| merge | `Waiting · merge of #69`, `Waiting · #69 held` | an armed
`merge_when_ready`; a hold on an agent's work |
| dependency | `Waiting · #102 to merge`, `Waiting · acme/api#7 to
merge`, `Waiting · #645 to move` | `depends_on`; a wake on another PR |
| run | `Waiting · Release run` | `run_monitor` |
| condition | `Waiting · main to move`, `Waiting · thread to finish`;
any other kind as its chip reads it (`wakeConditionLabel`) | #108's
condition wakes; the hover names the thread and says `wakeConditionWhen`
|
| timer | `Waiting · until 08:10`, `Waiting · until Wed 09:00` | a timed
wake |
| limit | `Paused · limit until 08:10`, `Paused · retry at 07:05` | a
usage-limit or rate-limit wait |
| held | `Held · by you` | the user holds the worker |
| frozen | (reserved) | nothing produces it yet; the merge freeze will |

**Which wait the label names.** What holds everything else comes first
(a limit, a frozen base, your hold). Then what clears soonest by its
nature (CI, the review bot, Gram's merge), then the longer waits
(another PR, a run, a condition). A timed wake comes last, since it's
mostly the fallback beside the others. Within a kind, the soonest wins.

**Waiting is never "needs you".** A wait is only said of a calm tone
(waiting, idle, or finished with a run still watched), never beside
work, an ask or a failure. Nothing here touches the Needs you list or
counts.

### Surfaces
- **Sidebar worker rows**: the hourglass line replaces the card's now
line ("checks running", "you hold it") while the worker waits. The hover
card lists every wait.
- **Orchestrator line**: a second line under the workspace name for its
own pending wakes, or a limit it waits out. The line's hover adds the
list. It's hidden while the workspace is paused or parked.
- **Board**: a chip's line is the same label, and its hover the same
list.
- **⌘J**: a row's now line is the same label, and its hover the same
list.
- The waiting tone's label is now "Waiting" rather than "Waiting on
checks", since it covers more than checks.

### Behaviour change
A worker waiting out a usage limit (`limitWait` in the ledger) now reads
as waiting on automation, with the hollow dot. Before, its activity
ignored the limit, so it showed idle (or attention).

### Labels kept to four words
The Board keeps a chip's line to four words. Every label fits, with the
` · ` separator counted as punctuation; `board-rail.spec.ts` now counts
it that way.

### Condition kinds still to come
`conditionShort` special-cases `branchMoved` and `threadFinished` only.
Any other kind falls back to contracts' `wakeConditionLabel`, and its
hover line to `wakeConditionWhen`. So #110 (`freezeLifted`), #111
(`ciStuck`, machine load and disk) and #112 (`releasePublished`,
`versionChanged`) show up without a change here. Whichever lands last
adds any icon, words or priority of its own. The `frozen` kind is
reserved for #110's frozen-main state.

## Screenshots
From `pr-shots` on synthetic mock data. The "kinds" scene
(`simulateWaitingOn`) adds a timed wake, a watched release run, a usage
limit, an armed merge, a wake on another PR and one on `main` moving,
and the orchestrator resting on two wakes of its own (a thread to
finish, ranked first, and a timed one).


**The sidebar over `casefiles`: workers waiting on CI, the review bot
and a dependency say what they wait on** (dark)

_After only: the base timed out staging it; the light pair below has the
before._

![sidebar-waiting-dark
after](https://github.com/user-attachments/assets/8cc9172d-160d-4f3a-8544-e82574065fc2)

**The sidebar over `casefiles`: workers waiting on CI, the review bot
and a dependency say what they wait on** (light)

| Before | After |
| --- | --- |
| ![sidebar-waiting-light
before](https://github.com/user-attachments/assets/8e4579f5-a2dc-4621-8fe9-01d367de281b)
| ![sidebar-waiting-light
after](https://github.com/user-attachments/assets/29058cf0-2c0c-499a-8b52-56cd887be7b5)
|

**Every kind of wait in the sidebar, and the orchestrator resting on a
wake of its own** (dark)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-kinds-dark
after](https://github.com/user-attachments/assets/0beada9b-6c10-4254-a7d9-98bfe7fe50ea)

**Every kind of wait in the sidebar, and the orchestrator resting on a
wake of its own** (light)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-kinds-light
after](https://github.com/user-attachments/assets/34353279-1bc1-4030-a5f8-35354028e9e0)

**A waiting worker's hover card lists every wait, the one its row names
first** (dark)

_After only: the scene's mock scenario is new in this PR._

![sidebar-waiting-hover-dark
after](https://github.com/user-attachments/assets/605089f6-e232-47e4-933b-d626fd010bf2)

**The same waits on the Board's chips, in the sidebar's words** (dark)

_After only: the scene's mock scenario is new in this PR._

![board-waiting-kinds-dark
after](https://github.com/user-attachments/assets/7f7e95fd-cd77-473d-93cc-8741f318a85f)

**The same waits on the Board's chips, in the sidebar's words** (light)

_After only: the scene's mock scenario is new in this PR._

![board-waiting-kinds-light
after](https://github.com/user-attachments/assets/8325dda3-5043-4bd4-813e-52b88bb33567)

**⌘J's Workers list says the same** (dark)

_After only: the scene's mock scenario is new in this PR._

![workers-list-waiting-dark
after](https://github.com/user-attachments/assets/c91f9952-74e7-4c0d-98b5-2f834c58522c)

**The same `casefiles` workers on the Board: the chips agree with
Quiet's dots** (dark)

| Before | After |
| --- | --- |
| ![quiet-busy-board-dark
before](https://github.com/user-attachments/assets/ba5d04eb-5035-4fbb-8a44-2f65f8deb895)
| ![quiet-busy-board-dark
after](https://github.com/user-attachments/assets/0a43c050-93f4-4562-bb81-e46e2b1297ac)
|

**The same `casefiles` workers on the Board: the chips agree with
Quiet's dots** (light)

| Before | After |
| --- | --- |
| ![quiet-busy-board-light
before](https://github.com/user-attachments/assets/d317ead1-7c3c-4404-8976-a7bda03e991d)
| ![quiet-busy-board-light
after](https://github.com/user-attachments/assets/1200bee4-f0c0-4feb-bb26-b0d73beef83f)
|


## Verification
On `fc8e877`, rebased onto `main` after #108:
- `bun run typecheck`, `bun run lint`, `bun run fmt:check`, `bun run
check:scaffold`: pass
- `vitest run src/orchestration src/views src/sidebar src/thread
src/shell src/api`: 1228 passed (on `767c1af`); `waiting-on.test.ts`: 30
passed on the head
- Affected e2e, `--workers=2` (`overview-sidebar board-parity board-rail
board-virtual feature-sheet orchestration workers-list quiet-view
overview-extras workspace-density reaching-workers worker-questions
one-thread-budgets ask-stack`): 135 passed, 3 failed. Two were tests to
update for this change (the orchestrator's waits read in rank order;
⌘J's four-word cap counting the ` · `), fixed in `fc8e877`. The third,
`overview-sidebar.spec.ts:851` (a Needs you count of 9 for 8), is
untouched by this and passed alone. All three passed alone on the head.
- Specs updated, not deleted: `board-rail` and `workers-list` count
words without the separator; `overview-sidebar` reads a row's name with
its wait, and tells the agent's glyph from the hourglass.
- Not run: the full `bun run check`, left to the merge gate.

## Known limits
- `frozen` has a label and a rank but no source until the merge freeze
lands.
- A thread-finished condition shows "thread to finish" on the row; the
thread's title is in the hover only, to keep the line short.
- Synthetic mock data only; not run against a live provider or real
workspace.
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.

1 participant