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
73 changes: 73 additions & 0 deletions .github/workflows/deploy-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Merging is deploying: a push to main that touches the bot or the checks it runs builds a
# new image and rolls it out to Cloud Run. Without this the hosted app silently drifts
# behind the code, which it did three times in one afternoon.
#
# Authentication is keyless: GitHub mints an OIDC token, Google trades it for a short-lived
# credential through Workload Identity Federation, and no service-account key exists to leak.
# The pool is bound to this repository only.
name: deploy-bot
on:
push:
branches: [main]
paths:
- "bot/**"
- "src/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/deploy-bot.yml"
workflow_dispatch:

# A second push while the first is rolling out would race it to the same revision.
concurrency:
group: deploy-bot
cancel-in-progress: false

permissions:
contents: read
id-token: write # required to mint the OIDC token

env:
PROJECT: summer-ranger-205420
REGION: us-central1
SERVICE: polygo-bot

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/987119925117/locations/global/workloadIdentityPools/github/providers/github
service_account: gh-deploy-bot@summer-ranger-205420.iam.gserviceaccount.com

- uses: google-github-actions/setup-gcloud@v2

- name: Build the image
run: |
IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/polygo/${SERVICE}:${GITHUB_SHA::7}"
echo "IMAGE=$IMAGE" >> "$GITHUB_ENV"
gcloud builds submit --config bot/cloudbuild.yaml \
--substitutions="_IMAGE=${IMAGE}" --project "$PROJECT" --quiet .

- name: Roll it out
run: |
gcloud run deploy "$SERVICE" --image "$IMAGE" --region "$REGION" \
--project "$PROJECT" --quiet

- name: Check it came back
run: |
URL=$(gcloud run services describe "$SERVICE" --region "$REGION" \
--project "$PROJECT" --format='value(status.url)')
echo "deployed: $URL"
# A revision that cannot answer /health is a revision that cannot review a pull
# request; fail the job so the notification says so.
for i in $(seq 1 10); do
body=$(curl -fsS --max-time 20 "$URL/health" || true)
[ "$body" = "polygo-bot ok" ] && { echo "health: $body"; exit 0; }
sleep 5
done
echo "the new revision never answered /health" >&2
exit 1
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to polygo. Versions follow [SemVer](https://semver.org); dat

## Unreleased

- Merging is deploying: a push to `main` touching `bot/`, `src/` or the manifests builds the image and rolls out a Cloud Run revision, then fails the job if that revision cannot answer `/health`. Keyless — GitHub's OIDC token is exchanged through Workload Identity Federation for a short-lived credential, bound to this repository, so there is no service-account key to leak. The hosted app had already drifted behind the code three times in an afternoon of hand deploys.

- The GitHub App is [polygolint](https://github.com/apps/polygolint): `polygo` was taken by a dormant account, and app names share the username namespace. The bot comments as `polygolint[bot]`. The crate, binary, container and Cloud Run service keep the name `polygo-bot` — they are internal, and the service URL is wired into the app's webhook.

- The README leads with the bot: what lands on a pull request, then the three ways to run it (install the app, four lines of CI, or the binary), each of them needing no configuration. `docs/img/review-comment-{light,dark}.svg` is the review as it appears on a pull request.
Expand Down
16 changes: 16 additions & 0 deletions bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ The flags that matter, and why:
seconds then shows in the delivery log as timed out; the review is still posted, because
the platform lets the handler finish after GitHub hangs up.

## Deploying a change

A push to `main` that touches `bot/`, `src/` or the manifests builds and rolls out a new
revision (`.github/workflows/deploy-bot.yml`), then fails the job if the new revision cannot
answer `/health`. Authentication is keyless — GitHub's OIDC token is exchanged through
Workload Identity Federation for a short-lived credential, and the pool is bound to this
repository — so no service-account key exists to leak.

To deploy by hand anyway (a rollback, or from a branch):

```sh
IMAGE=us-central1-docker.pkg.dev/summer-ranger-205420/polygo/polygo-bot:$(git rev-parse --short HEAD)
gcloud builds submit --config bot/cloudbuild.yaml --substitutions=_IMAGE=$IMAGE .
gcloud run deploy polygo-bot --image $IMAGE --region us-central1
```

## Run it anywhere else

```sh
Expand Down
9 changes: 9 additions & 0 deletions tests/release_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ fn release_assets_match_every_consumer() {
// A job that actually loads action/action.yml: a manifest GitHub cannot parse fails
// every job that uses the Action, and nothing else in CI reads it.
assert!(ci.contains("uses: ./action"), "{ci}");
// The hosted app is deployed by merging, not by remembering to: keyless, and the
// rollout fails if the new revision cannot answer /health.
let deploy = read(".github/workflows/deploy-bot.yml");
assert!(deploy.contains("workload_identity_provider"), "{deploy}");
assert!(
!deploy.contains("credentials_json"),
"no service-account key belongs here"
);
assert!(deploy.contains("/health"), "{deploy}");
// The formula is generated, not checked in.
assert!(!std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/packaging")).exists());
}
Expand Down
Loading