Pre-merge dry-run on every PR
Post a sticky GitHub PR comment showing exactly what yoink up would change before the merge.
yoink up --dry-run --format=markdown connects to each host, computes the diff between the running spec and what would deploy, and emits a markdown summary. Pipe that into a sticky GitHub PR comment and reviewers see exactly what the deploy will change before merging.
What the comment looks like
yoink dry-run
Plan: 0 to create ยท 2 to update ยท 4 unchanged ยท 1 orphan
prod-host-1
service spec image โช oteld3eb14f(unchanged)(unchanged) โช redisdcef8ac(unchanged)(unchanged) ๐ก api0ba920dโ4d8e123ghcr.io/you/api:abc1234โghcr.io/you/api:def5678๐ก web67a2fa0โa8b3c91ghcr.io/you/web:abc1234โghcr.io/you/web:def5678โช caddy8edf75f(unchanged)(unchanged)
prod-host-1@api-old-version(service:api)
Reviewers can see at a glance: which services this PR rolls, which stay untouched, and whether the image: change matches their expectation.
The workflow
.github/workflows/yoink-pr-diff.yml:
name: "Yoink: PR dry-run"
on:
pull_request:
branches: [main]
paths:
- "services/**"
- "yoink.yaml"
- ".github/workflows/yoink-pr-diff.yml"
concurrency:
group: yoink-pr-diff-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write # marocchino/sticky-pull-request-comment
jobs:
dry-run:
runs-on: ubuntu-latest # or your tailnet-connected runner
steps:
- uses: actions/checkout@v4
# Bring whatever your auth shape is. Tailscale + age-sealed
# secrets here; swap in your `provider: command` setup if you
# use a managed store instead.
- uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Install yoink
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
YOINK_VERSION: v0.7.0
run: |
gh release download "$YOINK_VERSION" --repo oddur/yoink \
--pattern 'yoink-x86_64-unknown-linux-gnu.tar.xz' --output - \
| tar -xJ --strip-components=1 -C /usr/local/bin yoink-x86_64-unknown-linux-gnu/yoink
- name: Compute diff
env:
PR_SHA: ${{ github.event.pull_request.head.sha }}
YOINK_AGE_KEY: ${{ secrets.YOINK_AGE_KEY }}
run: |
yoink --config yoink.yaml up --dry-run --format=markdown \
--tag api=$PR_SHA --tag web=$PR_SHA > diff.md
cat diff.md
- uses: marocchino/sticky-pull-request-comment@v2
with:
header: yoink-pr-diff
path: diff.mdWhy this is good
- Fast feedback. Reviewers see the deploy plan in the PR thread itself, no need to mentally simulate.
- Catches the silent-redeploy class of bugs. A v0.5.0-style change that flips a default and reroles every service is impossible to miss when the dry-run says "6 to update."
- Composes with the deploy. The same
yoink up --dry-runpowers the localyoink diff <service>command; operators can rerun the same check at their terminal before merging.
Limitations
- Dry-run reads from the host (it computes the diff against running containers), so the PR runner needs the same auth path your deploy runner does: tailnet membership + whatever secret-resolution your
provider:setup needs (YOINK_AGE_KEYfor age, the configured manager's CLI + token forcommand). - The
--tagoverrides have to match what your deploy workflow will pass. If staging/prod diverge, run two dry-runs against the right host set.
See also
- AGE secrets in GitHub Actions โ wiring
YOINK_AGE_KEYinto the runner so dry-run can decrypt. - Driving yoink from an AI agent โ how the dry-run output reads to a reviewer (human or otherwise).