Skip to content

fleet-write fw-20261002T033353Z-2bc49e #27136

fleet-write fw-20261002T033353Z-2bc49e

fleet-write fw-20261002T033353Z-2bc49e #27136

Workflow file for this run

# Fleet write relay — the broker a cloud seat can reach.
#
# A cloud seat container cannot act as the fleet App `objectstack-fleet`: its
# egress proxy replaces every `Authorization` header with the session's own
# token and refuses the `/app/**` path a mint needs, so `scripts/pm/fleet-token.mjs`
# cannot mint there and a minted token could not be used there. What the proxy
# does pass is `POST /repos/objectstack-ai/objectstack/dispatches` with the
# session token. So a seat packs ONE stroke — the same requests it would have
# sent directly — into one `repository_dispatch`, and this workflow executes
# it here as `objectstack-fleet[bot]` against the repository the payload names.
#
# The rule, the closed op list, the payload schema, the sender gate and the
# executor's exit contract all live in `scripts/pm/fleet-write/`; each header
# there is authoritative and this file is the invocation. Four steps, no
# logic of its own: validate → mint → execute, with the checkout the scripts
# need in front.
#
# ⛔ The token this run WRITES with is the App installation token step 3
# mints — narrowed to the payload's repository (and, for a `transfer` stroke
# alone, that transfer's one target too: `transferIssue` needs issues write on
# both ends) and to the permissions the op table declares (`issues: write`,
# `pull-requests: write`, the `contents: write` GitHub's auto-merge mutations
# require, and the `metadata: read` the sender gate's permission read needs)
# — and revoked by that action's post step. The workflow's own `GITHUB_TOKEN` gets nothing
# (`permissions: {}` below); the job grants it `contents: read` for the
# checkout and nothing else, so the run's ONLY write path is the App token,
# and it exists only inside the execute step's environment.
#
# The private key never reaches a script: `actions/create-github-app-token`
# reads the organization secret and hands the executor a token through `env:`.
# The executor prints no token, writes none to a file, and scrubs every line
# it emits; the runner's own secret masking is the second net.
#
# Authorization is the TARGET repo's: the executor asks
# `GET /repos/{payload.repo}/collaborators/{sender}/permission` for the login
# GitHub resolved from the dispatching token (`github.event.sender.login`)
# and refuses unless the answer is write, maintain or admin — a run failure
# with zero writes. There is no sender allowlist to keep: a seat that can push
# to a repository can write to it as the fleet, and one that cannot, cannot.
#
# ⛔ Only `repository_dispatch` with `types: [fleet-write]` starts this. No
# push, no schedule, no manual dispatch: every run is a seat's stroke, named
# after its request id so the seat's run-poller finds it by name.
name: Fleet Write
run-name: 'fleet-write ${{ github.event.client_payload.request_id }}'
on:
repository_dispatch:
types: [fleet-write]
# Nothing for the workflow's own token — see the header. The job re-grants
# exactly the read the checkout needs.
permissions: {}
jobs:
relay:
name: Fleet write relay
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false
# Pinned to the same major and spelling as every other setup-node in this
# repo, for the measured reason the sibling guards record: a setup-node
# major whose package-manager-cache default is on shells out to pnpm and
# kills the job in the SETUP step. This job installs no package manager:
# the three scripts are dependency-free and import sibling modules only.
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
# Everything reaches the scripts through `env:`, never through `${{ }}`
# inside a `run:` line. A payload is seat-authored text, and an
# expression interpolated into a shell line is substituted before bash
# sees it; through `env:` it is inert data. The validator refuses an
# unknown key, an unknown op, a target outside the organization, a
# non-integer number, an over-cap string or an over-cap payload with
# every reason in the step summary and zero writes; on success it hands
# the mint below the repository NAMES its token must reach through
# `$GITHUB_OUTPUT` — one for every stroke, two for a transfer stroke.
- name: Validate the payload (closed schema, zero writes on refusal)
id: validate
env:
FLEET_WRITE_PAYLOAD: ${{ toJSON(github.event.client_payload) }}
run: node scripts/pm/fleet-write/validate.mjs --from-env --github-output
# The fleet identity, minted on the runner and never held by a seat:
# `OS_FLEET_APP_ID` is an organization variable, `OS_FLEET_PRIVATE_KEY`
# an organization secret (both set by the maintainer). `repositories`
# narrows the token to the list the validator computed — the payload's
# ONE repository, or for a `transfer` stroke that repository plus the
# transfer's one target from the governed roster — the validator's
# output, never the payload read again here; its `--self-test` pins one
# name for every other op. The `permission-*` inputs narrow it to what
# `scripts/pm/fleet-write/ops.mjs` declares. A target the App's
# installation does not cover fails THIS step, loudly, with zero writes.
# The action revokes the token in its post step.
- name: Mint the fleet App token, narrowed to the target and the op table
id: token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.OS_FLEET_APP_ID }}
private-key: ${{ secrets.OS_FLEET_PRIVATE_KEY }}
owner: objectstack-ai
repositories: ${{ steps.validate.outputs.repositories }}
permission-issues: write
permission-pull-requests: write
# ONE consumer: GitHub's `enablePullRequestAutoMerge` / `disablePullRequestAutoMerge`
# mutations require `contents: write` on the App token (measured: without it the
# relay's `automerge_enable` answered "Resource not accessible by integration").
# The op table in `scripts/pm/fleet-write/ops.mjs` has NO contents op — no file, ref
# or branch write — so this grant is spent only by those two mutations.
permission-contents: write
permission-metadata: read
# The sender gate, then the actions in order, then the step summary —
# request id · sender and role · session · target · one row per request.
# The first failure stops the run and the summary says which later
# actions were NOT attempted. Exit codes: `scripts/pm/fleet-write/execute.mjs`.
- name: Execute the actions as the fleet (sender gate first, stop at the first failure)
env:
FLEET_WRITE_PAYLOAD: ${{ toJSON(github.event.client_payload) }}
FLEET_WRITE_SENDER: ${{ github.event.sender.login }}
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
run: node scripts/pm/fleet-write/execute.mjs