Recipe · CI/CD

Approve the prod deploy from your phone.

The pipeline pauses, asks on WhatsApp, and continues only on your tap. One step in your GitHub Actions workflow sends the question with ship it / abort buttons (POST /v1/ask under the hood) and blocks until you answer; the deploy step runs only when the answer is the first button. No reply means no deploy — silence never ships. And when the job finishes, a one-line ping (POST /v1/notify) tells you how it went.

§ 1

The gate: ship it or abort

Two steps: the gate asks and waits (up to 90 seconds, the server maximum), the deploy runs behind an if on the gate's button-id output. That's the whole wiring — the action is grzgrzgrz3/pingwa-action, your key sits in a repo secret.

.github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]

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

      - name: Ask before shipping
        id: gate
        uses: grzgrzgrz3/pingwa-action@v1
        with:
          api-key: ${{ secrets.PINGWA_KEY }}
          mode: ask
          message: "Deploy ${{ github.repository }} @ ${{ github.sha }} to prod?"
          buttons: "ship it,abort"

      - name: Deploy
        if: steps.gate.outputs.button-id == 'b0'
        run: ./deploy.sh

The fail-safe reading: if you don't answer within the 90 seconds, /v1/ask returns HTTP 408 and the gate step fails — the deploy step never runs. Tapping abort keeps the gate green with button-id=b1 and the deploy is simply skipped. button-id is positional: b0 is the first label you passed, b1 the second — only an explicit b0 ships. The question was still delivered to your phone either way; a late tap is retrievable afterwards via GET /v1/messages/<id>/reply.

If the ask lands outside your 24h reply window (likely, for deploys that arrive out of the blue), the buttons arrive as a numbered list instead of tap buttons — replying 1 (or the button's text, ship it) works the same and still maps to b0.

The action lives at github.com/grzgrzgrz3/pingwa-action — composite, curl + jq, ~40 lines. Get a key: send join on WhatsApp, store it as the PINGWA_KEY repo secret.
§ 2

The done-ping: how did it go?

Same action, mode: notify (the default). With if: always() the ping fires on success and failure, and job.status puts the verdict in the message — the deploy you approved from the couch reports back to the couch.

deploy.yml — last step of the job
      - name: Ping when done
        if: always()
        uses: grzgrzgrz3/pingwa-action@v1
        with:
          api-key: ${{ secrets.PINGWA_KEY }}
          message: "${{ github.workflow }} on ${{ github.repository }}: ${{ job.status }}"
§ 3

Not on GitHub? Two curls

GitLab CI, Jenkins, Woodpecker, a bare deploy script — the gate is the same /v1/ask call with a jq check on .reply.button_id. The GitHub action is exactly this curl inside; there's nothing platform-specific about the pattern.

deploy-gate.sh — GitLab CI / Jenkins / anything
reply=$(curl -sS --max-time 95 -X POST https://pingwa.dev/v1/ask \
  -H "Authorization: Bearer $PINGWA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Deploy to prod?", "buttons": ["ship it","abort"], "timeout": 90}')

[ "$(printf '%s' "$reply" | jq -r '.reply.button_id')" = "b0" ] || exit 1
./deploy.sh

curl -sS -X POST https://pingwa.dev/v1/notify \
  -H "Authorization: Bearer $PINGWA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "deploy: done"}'

Same fail-safe shape: the gate compares against b0, so anything else fails it — a 408 or error body makes jq print null, an abort tap yields b1, and either way exit 1 stops the script before the deploy line. One difference from the GitHub variant: that exit 1 also skips the final notify (a bare script has no if: always()) — move the notify before the exit, or trap it, if you want failure pings too.

§ 4

FAQ

GitHub Actions manual approval without environments?

Honest answer: GitHub's protected environments with required reviewers are the right tool for org-grade compliance — audit trails, review groups, policy. This recipe is the solo-dev phone tap: no environment configuration, no plan requirement, and the approval reaches you where you already are — on WhatsApp, away from the laptop. If you're one person shipping your own project, a tap beats opening github.com on mobile to hunt for the review button.

Will the job hang while it waits?

At most 90 seconds — that's the server maximum for /v1/ask's long-poll, after which the gate step fails and the job ends without deploying. The action can't wait longer than that, so a forgotten question costs you a couple of runner minutes, not hours. Belt and braces: set a job-level timeout-minutes anyway, as you would for any job.

Can I really approve a deployment from my phone?

Yes — that's § 1. The workflow sends the question with buttons to your WhatsApp and genuinely blocks on the gate step; your tap is the approval. Tap ship it and the deploy step runs, tap abort or ignore it and nothing ships. Get a key by sending join once, add it as the PINGWA_KEY secret, paste the workflow.

Is the action safe to put in my pipeline?

It's a composite action: one bash step, curl + jq only (both preinstalled on GitHub runners), zero third-party dependencies, ~40 lines — read action.yml yourself, it fits on a screen. If your supply-chain policy is strict, pin it by full commit SHA instead of @v1.

QR code to join pingwa on WhatsApp

Scan the code and get instant access. It opens WhatsApp with “join” ready to send, and your API key comes straight back.

Deploy gates are exactly the messages you want rare and deliberate — the free plan's 30 paid messages a month cover a healthy release cadence, and your button-tap replies inside the 24h window are free.