Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 24 additions & 50 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,31 +131,22 @@ jobs:
if: github.event_name == 'push'
needs: [build-cli]
runs-on: ubuntu-latest
permissions: {} # all repo access comes from the minted App token below
concurrency:
group: docs-regen
cancel-in-progress: true
environment: soothfast-bot
permissions:
contents: read
id-token: write # soothfast-bot token via the broker, see bot/
env:
SOOTHFAST: ./bin/cargo-soothfast
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Mint a soothfast-bot token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }}
private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Get soothfast-bot user id
id: app-user
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # zizmor: ignore[artipacked]
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref_name }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: stable
Expand Down Expand Up @@ -187,28 +178,18 @@ jobs:
sed '/^\*\*Measured:\*\*/d' llms.txt > target/llms-churn/new.txt
git show HEAD:llms.txt | sed '/^\*\*Measured:\*\*/d' > target/llms-churn/old.txt
cmp -s target/llms-churn/old.txt target/llms-churn/new.txt && git checkout -- llms.txt || true
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
id: cpr
with:
token: ${{ steps.app-token.outputs.token }}
add-paths: |
llms.txt
README.md
docs/**
commit-message: "docs: regenerate derived docs"
title: "docs: regenerate derived docs"
body: Automated regeneration of captured output and reference pages.
branch: bot/docs-update
delete-branch: true
author: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<${{ steps.app-user.outputs.id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
- name: Merge pull request
if: steps.cpr.outputs.pull-request-number
- name: Mint a soothfast-bot token
id: bot
run: action/bot-token.sh
- name: Land the regenerated docs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
run: gh pr merge --auto --squash --delete-branch "$PR_NUMBER"
TOKEN: ${{ steps.bot.outputs.token }}
APP_SLUG: ${{ steps.bot.outputs.app_slug }}
BRANCH: bot/docs-update
TITLE: "docs: regenerate derived docs"
BODY: Automated regeneration of captured output and reference pages.
PATHS: llms.txt README.md docs
run: action/land.sh

# Dogfood: gate each self-bench crate's perf against the PR's merge-base.
# Keep this package list in sync with Makefile's BENCH_CRATES.
Expand All @@ -229,11 +210,11 @@ jobs:
]
permissions:
contents: read
pull-requests: write # gate comment
uses: ./.github/workflows/soothfast-gate.yml
with:
package: ${{ matrix.package }}
secrets:
CHANGELOG_APP_PRIVATE_KEY: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
cli-artifact: cargo-soothfast-bin

# One stable context for branch protection; the per-crate gate legs
# change names whenever the crate list does.
Expand All @@ -259,20 +240,13 @@ jobs:
needs: [check, docs, build-cli]
runs-on: ubuntu-latest
permissions:
contents: read
contents: write # push the built site to gh-pages
env:
SOOTHFAST: ./bin/cargo-soothfast
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Mint a soothfast-bot token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }}
private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
permission-contents: write
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
Expand All @@ -295,5 +269,5 @@ jobs:
- run: bin/cargo-soothfast docs build --baseline self
- uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ steps.app-token.outputs.token }}
github_token: ${{ github.token }}
publish_dir: ./site
9 changes: 1 addition & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,6 @@ jobs:
run: |
gh api -X POST "repos/${{ github.repository }}/git/refs" \
-f ref="refs/tags/$TAG" -f sha="${{ github.sha }}"
- name: Mint a soothfast-bot token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }}
private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
permission-contents: write
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
Expand Down Expand Up @@ -226,7 +219,7 @@ jobs:

- name: Create GitHub Release
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.resolve.outputs.tag }}
USE_AUTO: ${{ steps.changelog.outputs.use_auto }}
run: |
Expand Down
50 changes: 32 additions & 18 deletions .github/workflows/soothfast-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ on:
required: false
type: string
default: ""
cli-artifact:
description: >-
Artifact holding a prebuilt cargo-soothfast at bin/cargo-soothfast.
Empty installs the release matching the caller's Cargo.lock.
required: false
type: string
default: ""
secrets:
# Unused. Declared so callers that still pass it keep validating.
CHANGELOG_APP_PRIVATE_KEY:
required: true
required: false

permissions: {}

Expand All @@ -23,18 +31,11 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # gate comment
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Mint a soothfast-bot token
id: app-token
if: github.event_name == 'pull_request'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }}
private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
permission-pull-requests: write
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
Expand All @@ -45,19 +46,22 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
# callgrind fallback + triage artifacts for PMU-less runners
- run: sudo apt-get update -qq && sudo apt-get install -y -qq valgrind
# Built once by ci.yml's build-cli job
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- if: inputs.cli-artifact != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cargo-soothfast-bin
name: ${{ inputs.cli-artifact }}
path: bin
- run: chmod +x bin/cargo-soothfast
- if: inputs.cli-artifact != ''
run: chmod +x bin/cargo-soothfast
- if: inputs.cli-artifact == ''
uses: Verdenroz/soothfast@ead9d50c79606edcf80a438f8fac73229c01120c # v0.2.0
- name: Run gate
id: gate
run: |
REF="${INPUTS_BASE_REF}"
if [ -z "$REF" ]; then REF="origin/${GITHUB_BASE_REF:-master}"; fi
bin/cargo-soothfast \
gate -p "${INPUTS_PACKAGE}" --against-ref "$REF" | tee gate-output.txt
if [ -x bin/cargo-soothfast ]; then set -- bin/cargo-soothfast; else set -- cargo soothfast; fi
"$@" gate -p "${INPUTS_PACKAGE}" --against-ref "$REF" | tee gate-output.txt
env:
INPUTS_BASE_REF: ${{ inputs.base-ref }}
INPUTS_PACKAGE: ${{ inputs.package }}
Expand All @@ -68,17 +72,27 @@ jobs:
name: soothfast-triage
path: .soothfast/triage/
if-no-files-found: ignore
# Fork PRs get a read-only token here; the comment is best effort. The
# marker finds our own comment: github.token's author is shared with
# every other action in the repo, so --edit-last would hit theirs.
- name: PR comment with gate results
if: always() && github.event_name == 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
{
echo '<!-- soothfast-gate -->'
echo '## soothfast gate'
echo '```'
tail -n 60 gate-output.txt
echo '```'
} > comment.md
gh pr comment "${{ github.event.pull_request.number }}" \
--body-file comment.md --edit-last --create-if-none
id=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
--jq '[.[] | select(.body | startswith("<!-- soothfast-gate -->")) | .id][0] // empty')
if [ -n "$id" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${id}" -F body=@comment.md >/dev/null
else
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F body=@comment.md >/dev/null
fi
59 changes: 20 additions & 39 deletions .github/workflows/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,20 @@ jobs:
generate:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions: {} # all repo access comes from the minted App token below
concurrency:
group: spec
cancel-in-progress: true
environment: soothfast-bot
permissions:
contents: read
id-token: write # soothfast-bot token via the broker, see bot/
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- name: Mint a soothfast-bot token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.CHANGELOG_APP_CLIENT_ID }}
private-key: ${{ secrets.CHANGELOG_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Get soothfast-bot user id
id: app-user
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # zizmor: ignore[artipacked]
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.ref_name }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: stable
Expand All @@ -57,28 +48,18 @@ jobs:
run: cargo run --release -p cargo-soothfast -- spec gen -p "$SPEC_PKG"
# Direct pushes to master are blocked by the branch ruleset, so the bot
# opens a PR and squash-merges it itself instead.
- uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
id: cpr
with:
token: ${{ steps.app-token.outputs.token }}
add-paths: |
*.yaml
*.yml
*.json
commit-message: "chore: regenerate API specs"
title: "chore: regenerate API specs"
body: Automated spec regeneration.
branch: bot/spec-update
delete-branch: true
author: >-
${{ steps.app-token.outputs.app-slug }}[bot]
<${{ steps.app-user.outputs.id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>
- name: Merge pull request
if: steps.cpr.outputs.pull-request-number
- name: Mint a soothfast-bot token
id: bot
run: action/bot-token.sh
- name: Land the regenerated specs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
run: gh pr merge --auto --squash --delete-branch "$PR_NUMBER"
TOKEN: ${{ steps.bot.outputs.token }}
APP_SLUG: ${{ steps.bot.outputs.app_slug }}
BRANCH: bot/spec-update
TITLE: "chore: regenerate API specs"
BODY: Automated spec regeneration.
PATHS: "*.yaml *.yml *.json"
run: action/land.sh

# On a PR, prove the committed specs are current and that the API change is
# backwards compatible with the merge-base.
Expand Down
36 changes: 27 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,21 +296,24 @@ claim being enforced, not a bug in the check.
a push.
- `scorecard.yml` — OSSF Scorecard supply-chain analysis, published to the
public Scorecard API and uploaded to code scanning as SARIF.
- `soothfast-gate.yml` — reusable workflow; runs `cargo soothfast gate` for a
given package against the PR's merge-base, uploads `.soothfast/triage/` on
failure, and posts/updates a PR comment with the gate output.
- `soothfast-gate.yml` — reusable workflow, callable from any repo; runs
`cargo soothfast gate` for a given package against the PR's merge-base,
uploads `.soothfast/triage/` on failure, and posts/updates a PR comment
with the gate output using `github.token`. `cli-artifact` names a
prebuilt CLI uploaded earlier in the run (what `ci.yml` does); without it
the root `action.yml` installs the release matching `Cargo.lock`.
- `bot.yml` / `bot-smoke.yml` — type-check, test, and deploy the
soothfast-bot token broker under `bot/` (a Cloudflare Worker holding the
App private key), and prove a deployment by minting, listing, and revoking
a token. See the "soothfast-bot" section below.
- `changelog.yml` — on push to `master`, regenerates the living
`CHANGELOG.md` and lands it through a bot-opened, auto-squash-merged PR on
`bot/changelog-update`. It skips runs whose actor already ends in `[bot]`,
which is what stops its own merge from retriggering it. Per-merge is
affordable because the derived sections drop out when the API surface and
the gate are quiet, so a run has nothing to say unless something merged.
Commits authenticate with a short-lived token from a repo-installed App
(`actions/create-github-app-token`, `CHANGELOG_APP_CLIENT_ID` /
`CHANGELOG_APP_PRIVATE_KEY`) rather than the default `GITHUB_TOKEN` —
GitHub gates every subsequent workflow run on a PR behind manual approval
once a `github-actions[bot]`-authored commit lands on it, and an
explicitly installed App doesn't trip that gate.
A `concurrency` group cancels older regenerations so only the newest tree
lands.
- `spec.yml` — on push to `master`, regenerates `mode = "generate"` spec
files and lands them via a bot-opened, self-merged PR (the branch ruleset
blocks direct pushes to `master`), so nobody has to remember to; on PRs,
Expand All @@ -320,6 +323,21 @@ claim being enforced, not a bug in the check.
- `release.yml` — on `v*` tag push, runs checks + gate, then publishes all 10
workspace crates to crates.io in dependency order.

### soothfast-bot

Every write a workflow makes on the bot's behalf (`changelog.yml`, `spec.yml`,
`ci.yml` docs-regen) is authored by the soothfast-bot GitHub App, but no
workflow holds the App's private key. The job runs `action/bot-token.sh`,
which trades the job's GitHub Actions OIDC token for a one-hour installation
token minted by the broker under `bot/` (a Cloudflare Worker, deployed by
`bot.yml`). The broker mints only for a job in the `soothfast-bot`
environment, on a `push`/`workflow_dispatch`/`schedule` event, on the
repository's default branch, for a repository the App is installed on, and
scopes the token to that repository. `action/land.sh` then commits, pushes,
opens or refreshes the bot PR, enables auto-merge, and revokes the token.
Minting happens after the build step on purpose: no step that compiles the
tree holds a write token.

All third-party actions are pinned to a full commit SHA (never a mutable
tag), every job declares explicit least-privilege `permissions:`, every
step starts with `step-security/harden-runner`, and checkouts that don't
Expand Down
Loading