๐Ÿช yoink

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

servicespecimage
โšช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.md

Why 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-run powers the local yoink 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_KEY for age, the configured manager's CLI + token for command).
  • The --tag overrides have to match what your deploy workflow will pass. If staging/prod diverge, run two dry-runs against the right host set.

See also

On this page