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
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ jobs:
]
permissions:
contents: read
pull-requests: write # gate comment
pull-requests: write # gate comment fallback
id-token: write # soothfast-bot comment token via the broker
uses: ./.github/workflows/soothfast-gate.yml
with:
package: ${{ matrix.package }}
Expand Down Expand Up @@ -239,8 +240,10 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
needs: [check, docs, build-cli]
runs-on: ubuntu-latest
environment: soothfast-bot
permissions:
contents: write # push the built site to gh-pages
contents: read
id-token: write # soothfast-bot token pushes the built site to gh-pages
env:
SOOTHFAST: ./bin/cargo-soothfast
steps:
Expand All @@ -267,7 +270,15 @@ jobs:
- run: make baselines
- run: make docs-pages
- run: bin/cargo-soothfast docs build --baseline self
- name: Mint a soothfast-bot token
id: bot
run: action/bot-token.sh
- uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ github.token }}
github_token: ${{ steps.bot.outputs.token }}
publish_dir: ./site
- name: Revoke the soothfast-bot token
if: always() && steps.bot.outputs.token != ''
env:
GH_TOKEN: ${{ steps.bot.outputs.token }}
run: gh api -X DELETE /installation/token
48 changes: 41 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,6 @@ jobs:
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: stable
- name: Pin the rustdoc toolchain
run: |
TC=$(make -s print-SOOTHFAST_RUSTDOC_TOOLCHAIN)
echo "SOOTHFAST_RUSTDOC_TOOLCHAIN=$TC" >> "$GITHUB_ENV"
rustup toolchain install "$TC"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
lookup-only: true
Expand All @@ -175,6 +170,37 @@ jobs:
sleep 30
done

# The release itself is authored by soothfast-bot; the broker accepts the
# tag because its commit is already on master.
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [resolve, publish]
environment: soothfast-bot
permissions:
contents: read
id-token: write # soothfast-bot token creates the release
outputs:
tag: ${{ needs.resolve.outputs.tag }}
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
egress-policy: audit
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0 # previous-tag lookup + surface diff need history
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: stable
- name: Pin the rustdoc toolchain
run: |
TC=$(make -s print-SOOTHFAST_RUSTDOC_TOOLCHAIN)
echo "SOOTHFAST_RUSTDOC_TOOLCHAIN=$TC" >> "$GITHUB_ENV"
rustup toolchain install "$TC"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
lookup-only: true
# Dogfood: release assets are produced from this tag's own measurements.
- name: Produce release report from recorded measurements
run: |
Expand Down Expand Up @@ -217,9 +243,12 @@ jobs:
printf '%s\n' "$NOTES" > /tmp/release_notes.md
fi

- name: Mint a soothfast-bot token
id: bot
run: action/bot-token.sh
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.bot.outputs.token }}
TAG: ${{ needs.resolve.outputs.tag }}
USE_AUTO: ${{ steps.changelog.outputs.use_auto }}
run: |
Expand All @@ -231,11 +260,16 @@ jobs:
else
gh release create "$TAG" --verify-tag --title "$TAG" --notes-file /tmp/release_notes.md $ASSETS
fi
- name: Revoke the soothfast-bot token
if: always() && steps.bot.outputs.token != ''
env:
GH_TOKEN: ${{ steps.bot.outputs.token }}
run: gh api -X DELETE /installation/token

binaries:
name: Prebuilt cargo-soothfast (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
needs: [resolve, publish]
needs: [resolve, release]
permissions:
contents: write # upload the release asset
strategy:
Expand Down
39 changes: 24 additions & 15 deletions .github/workflows/soothfast-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ permissions: {}
jobs:
gate:
runs-on: ubuntu-latest
environment: soothfast-bot
permissions:
contents: read
pull-requests: write # gate comment
pull-requests: write # fallback for the comment when no bot token
id-token: write # the broker posts the gate comment as soothfast-bot
steps:
- uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
with:
Expand All @@ -40,6 +42,16 @@ jobs:
with:
persist-credentials: false
fetch-depth: 0 # merge-base needs history
# The action scripts at this workflow's own commit, whoever calls it.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: Verdenroz/soothfast
ref: ${{ github.job_workflow_sha }}
path: .soothfast-action
sparse-checkout: |
action
action.yml
persist-credentials: false
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: stable
Expand All @@ -54,7 +66,10 @@ jobs:
- if: inputs.cli-artifact != ''
run: chmod +x bin/cargo-soothfast
- if: inputs.cli-artifact == ''
uses: Verdenroz/soothfast@ead9d50c79606edcf80a438f8fac73229c01120c # v0.2.0
uses: ./.soothfast-action
with:
gate: "false"
changelog: "false"
- name: Run gate
id: gate
run: |
Expand All @@ -72,27 +87,21 @@ 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.
# The broker posts the comment as soothfast-bot; a fork pull request has
# no OIDC token and falls back to github.token. Best effort either way.
- name: PR comment with gate results
if: always() && github.event_name == 'pull_request'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
MARKER: "<!-- soothfast-gate -->"
BODY_FILE: ${{ runner.temp }}/soothfast-gate-comment.md
run: |
{
echo '<!-- soothfast-gate -->'
echo '## soothfast gate'
echo '```'
tail -n 60 gate-output.txt
tail -n 60 gate-output.txt | sed 's/```/` ` `/g'
echo '```'
} > comment.md
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
} > "$BODY_FILE"
.soothfast-action/action/comment.sh
17 changes: 14 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,20 @@ 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,
environment, for a repository the App is installed on, scoped to that
repository: a landing token (contents + pull requests write) on a
`push`/`workflow_dispatch`/`schedule` event on the default branch or on a
tag whose commit (the OIDC `sha`, never the tag name) is already on it. A
`pull_request` run gets no token; it POSTs the gate comment text to the
broker's `/comment`, which posts it as the bot with its own token and
revokes it. Gate comments, `deploy-docs`' gh-pages push, and the GitHub
Release all carry the bot identity this way; a fork pull request has no OIDC
token and its gate comment falls back to `github.token`. This repo's
`soothfast-bot` environment must have no deployment branch policy: the gate
runs on `refs/pull/*` and the release on `refs/tags/*`. Anything a bot comment
quotes from a pull request's build output is untrusted text under a
write-access author: it stays inside a code fence and fence sequences in it
are neutralised first. `action/land.sh` then commits, pushes,
opens or refreshes the bot PR, merges it (queued behind required checks when
the default branch has any, immediately otherwise), and revokes the token.
Minting happens after the build step on purpose: no step that compiles the
Expand Down
3 changes: 2 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ inputs:
required: false
default: soothfast-bot
broker:
description: Token broker URL. Empty uses the default in action/bot-token.sh.
description: Token broker URL. Empty uses the default in action/oidc.sh.
required: false
default: ""

Expand Down Expand Up @@ -160,6 +160,7 @@ runs:
BASE_REF: ${{ github.base_ref }}
GH_TOKEN: ${{ inputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BROKER: ${{ inputs.broker }}
run: "$GITHUB_ACTION_PATH/action/gate.sh"
- name: Upload triage artifacts
if: steps.gate.outputs.failed == 'true'
Expand Down
23 changes: 10 additions & 13 deletions action/bot-token.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,26 @@
# Inputs: BROKER (URL, optional). Outputs: token, app_slug, expires_at.
set -euo pipefail

BROKER=${BROKER:-https://soothfast-bot.verdenroz.workers.dev}
# shellcheck source=action/oidc.sh
source "$(dirname "$0")/oidc.sh"

if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
echo "::error::soothfast-bot needs 'id-token: write' in the job's permissions"
fail() {
echo "::error::$1"
exit 1
fi
}

oidc=$(curl -sSf --max-time 30 -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=soothfast-bot" | jq -r .value)
oidc=$(oidc_token) || fail "soothfast-bot needs 'id-token: write' in the job's permissions"

response=$(curl -sS --max-time 30 -w '\n%{http_code}' -X POST "$BROKER/token" -H "Authorization: Bearer $oidc")
response=$(curl -sS --max-time 30 -w '\n%{http_code}' -X POST "$BROKER/token" -H "Authorization: Bearer $oidc") ||
fail "could not reach the broker at $BROKER"
status=${response##*$'\n'}
body=${response%$'\n'*}

if [ "$status" != 200 ]; then
echo "::error::soothfast-bot refused (HTTP $status): $(jq -r '.reason // .' <<<"$body")"
exit 1
fail "soothfast-bot refused (HTTP $status): $(jq -r '.reason // .' <<<"$body")"
fi

token=$(jq -er .token <<<"$body") || {
echo "::error::soothfast-bot returned no token"
exit 1
}
token=$(jq -er .token <<<"$body") || fail "soothfast-bot returned no token"
echo "::add-mask::$token"
{
echo "token=$token"
Expand Down
36 changes: 28 additions & 8 deletions action/comment.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
#!/usr/bin/env bash
# Create or update this action's one comment on a pull request. A marker
# line identifies it: github.token's author is shared with every other
# action in the repository, so "edit the last comment by me" would hit theirs.
# Inputs: GH_TOKEN PR_NUMBER MARKER BODY_FILE.
# Create or update this action's one comment on a pull request, identified
# by a marker line. The broker posts it as soothfast-bot; a job with no OIDC
# identity (a fork pull request) falls back to GH_TOKEN, whose author is
# shared with every other action in the repository, hence the marker rather
# than "edit my last comment".
# Inputs: GH_TOKEN PR_NUMBER MARKER BODY_FILE, BROKER (optional).
set -euo pipefail

body="${RUNNER_TEMP:-/tmp}/soothfast-comment.md"
{ echo "$MARKER"; cat "$BODY_FILE"; } >"$body"
# shellcheck source=action/oidc.sh
source "$(dirname "$0")/oidc.sh"

if oidc=$(oidc_token); then
payload=$(jq -n --argjson pr "$PR_NUMBER" --arg marker "$MARKER" --rawfile body "$BODY_FILE" \
'{pull_request: $pr, marker: $marker, body: $body}')
response=$(curl -sS --max-time 30 -w '\n%{http_code}' -X POST "$BROKER/comment" \
-H "Authorization: Bearer $oidc" -H "content-type: application/json" --data-binary "$payload") || response=$'\n000'
status=${response##*$'\n'}
body=${response%$'\n'*}
if [ "$status" = 200 ]; then
echo "commented as $(jq -r .app_slug <<<"$body")"
exit 0
fi
reason=$(jq -r '.reason // empty' <<<"$body" 2>/dev/null || true)
echo "::notice::soothfast-bot did not post the comment (HTTP $status${reason:+: $reason}); posting with the job token instead"
fi

with_marker="${RUNNER_TEMP:-/tmp}/soothfast-comment.md"
{ echo "$MARKER"; cat "$BODY_FILE"; } >"$with_marker"
id=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
--jq "[.[] | select(.body | startswith(\"$MARKER\")) | .id][0] // empty")
if [ -n "$id" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${id}" -F "body=@${body}" >/dev/null
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${id}" -F "body=@${with_marker}" >/dev/null
else
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F "body=@${body}" >/dev/null
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F "body=@${with_marker}" >/dev/null
fi
10 changes: 7 additions & 3 deletions action/gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# Gate every package against the pull request's base branch and post the
# tail of each package's output as one PR comment. Never exits non-zero on a
# regression: the caller reads the `failed` output so the comment and triage
# upload still happen first.
# Inputs: CLI PACKAGES BASE_REF GH_TOKEN PR_NUMBER. Output: failed (true|false).
# upload still happen first. Output is untrusted (the PR's own binaries wrote
# it) and the comment is authored by a write-access identity, so nothing in
# it may escape the code fence.
# Inputs: CLI PACKAGES BASE_REF GH_TOKEN PR_NUMBER, BROKER (optional).
# Output: failed (true|false).
set -euo pipefail

read -ra pkgs <<<"$PACKAGES"
Expand All @@ -17,7 +20,8 @@ mkdir -p "$out_dir"
"$CLI" gate -p "$pkg" --against-ref "origin/${BASE_REF}" 2>&1 | tee "$out" >&2 || failed=true
echo "### ${pkg}"
echo '```'
tail -n 40 "$out"
# shellcheck disable=SC2016 # literal backticks, nothing to expand
tail -n 40 "$out" | sed 's/```/` ` `/g'
echo '```'
done
} >"${out_dir}/comment.md"
Expand Down
13 changes: 13 additions & 0 deletions action/oidc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Shared by bot-token.sh and comment.sh: the broker location and the job's
# OIDC token. Source it; do not run it.

BROKER=${BROKER:-https://soothfast-bot.verdenroz.workers.dev}

# Prints the job's OIDC token for the soothfast-bot audience, or nothing when
# the job has no `id-token: write` (a fork pull request, for one).
oidc_token() {
[ -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] || return 1
curl -sSf --max-time 30 -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=soothfast-bot" | jq -r .value
}
Loading
Loading