Skip to main content
All posts
Developers6 min read

How to Send a GitHub Issue-Closed Alert to a Shared Room

Trigger on one labeled issue closure, keep the issue details in GitHub, and send one fixed retry-safe alert to a shared PingRoom Room.

Two PingRoom avatars guide a plain task tile through a small wooden gate while Pingo watches beside a closed laptop.

Use GitHub's own notifications when each person only needs updates inside GitHub. When one selected issue closure should reach a separate shared group, add an opt-in label and let a small GitHub Actions workflow send fixed copy to a PingRoom Room.

The recipe below runs on issues.closed, then stops unless the issue has a shared-status label. GitHub remains the record for the title, description, discussion, closure reason, and person who closed it. PingRoom carries only the short shared signal.

This is a webhook recipe, not a native GitHub integration. An issue closing does not prove that the work was correct, deployed, or approved.

Try GitHub's notification controls first

GitHub lets people watch a repository and subscribe to Issue updates. GitHub Mobile also has its own push settings and working hours. Use those controls when the people who care already work in GitHub and want its full context.

Add a shared PingRoom path only when the closure has a separate attention job.

NeedBetter starting point
A maintainer wants normal updates for issues they followGitHub notifications
A selected closure should reach one small shared attention channelGitHub Actions → PingRoom incoming webhook
Someone needs the title, discussion, labels, or closure reasonOpen the issue in GitHub
Closing the issue must deploy, approve, or change another systemKeep that system's own workflow authoritative

The opt-in label matters. Without it, a busy repository can turn every closed issue into noise. Choose a label that means “send the shared status signal,” not “this issue is important forever.” This example uses shared-status.

Create the Room and protect its URL

Create or choose a private Room for the people who need the update. The Room owner needs PingRoom Pro for incoming webhooks.

Open Settings → Connections → From another app, add a webhook, leave Quick Ping on Custom message, and copy the full URL. The incoming-webhook walkthrough covers setup. The webhook reference carries the current request limits and response contract.

In the GitHub repository, open Settings → Secrets and variables → Actions and add a repository secret named PINGROOM_WEBHOOK_URL. Treat that URL as a credential. Do not put it in the workflow file, an issue, a log, an artifact, or browser-side code.

Create the shared-status label and agree who may add it. The label is a routing choice. It does not change the issue's meaning or authorize anyone to close it.

Add one issue-closed workflow

Save this as .github/workflows/issue-closed-room-alert.yml on the repository's default branch. Change REPOSITORY_LABEL to a short name that is safe on a lock screen.

name: Shared issue-closed alert

on:
  issues:
    types: [closed]

permissions: {}

jobs:
  notify-room:
    if: contains(github.event.issue.labels.*.name, 'shared-status')
    runs-on: ubuntu-latest
    env:
      PINGROOM_WEBHOOK_URL: ${{ secrets.PINGROOM_WEBHOOK_URL }}
      REPOSITORY_LABEL: "Selected repository"
      REPOSITORY: ${{ github.repository }}
      ISSUE_ID: ${{ github.event.issue.id }}
      ISSUE_NUMBER: ${{ github.event.issue.number }}
      RUN_ID: ${{ github.run_id }}
    steps:
      - name: Send shared issue-closed alert
        continue-on-error: true
        shell: bash
        run: |
          node <<'NODE'
          const { createHash } = require("node:crypto");

          async function main() {
            const webhookUrl = process.env.PINGROOM_WEBHOOK_URL;
            const label = process.env.REPOSITORY_LABEL;
            const repository = process.env.REPOSITORY;
            const issueId = process.env.ISSUE_ID;
            const issueNumber = process.env.ISSUE_NUMBER;
            const runId = process.env.RUN_ID;

            if (!webhookUrl) throw new Error("Missing PINGROOM_WEBHOOK_URL");
            const destination = new URL(webhookUrl);
            if (destination.protocol !== "https:") throw new Error("PINGROOM_WEBHOOK_URL must use HTTPS");
            if (!label || label.length > 80 || /[\r\n]/.test(label)) throw new Error("REPOSITORY_LABEL must be 1–80 single-line characters");
            if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository ?? "")) throw new Error("Invalid REPOSITORY");
            if (!/^\d+$/.test(issueId ?? "") || !/^\d+$/.test(issueNumber ?? "") || !/^\d+$/.test(runId ?? "")) throw new Error("Invalid issue or run identity");

            const issueUrl = `https://github.com/${repository}/issues/${issueNumber}`;
            const digest = createHash("sha256").update(`${repository}:${issueId}:${runId}`).digest("hex");
            const eventId = `github-issue-closed:${digest}`;
            const payload = {
              action: null,
              title: "GitHub issue closed",
              message: `${label} · issue #${issueNumber}`,
              data: { issue_number: Number(issueNumber), issue_url: issueUrl },
              correlation_id: eventId,
            };

            const response = await fetch(webhookUrl, {
              method: "POST",
              headers: { Accept: "application/json", "Content-Type": "application/json", "Idempotency-Key": eventId },
              body: JSON.stringify(payload),
              redirect: "error",
              signal: AbortSignal.timeout(10000),
            });

            if (!response.ok) throw new Error(`PingRoom returned HTTP ${response.status}`);
            const accepted = await response.json();
            if (accepted.success !== true) throw new Error("PingRoom did not accept the issue alert");
            console.log(`PingRoom accepted ${accepted.notification_id}`);
          }

          main().catch((error) => {
            console.error(error.message);
            process.exit(1);
          });
          NODE

GitHub's issues event reference says the workflow file must exist on the default branch. The closed activity avoids runs for ordinary edits, comments, assignments, and label changes. GitHub's expression reference documents the same contains(github.event.issue.labels.*.name, 'label') pattern used here.

permissions: {} removes repository access from the workflow's GITHUB_TOKEN. The job does not check out code, install a dependency, call GitHub's API, or execute issue content. Its only external credential is the PingRoom URL supplied through the repository secret.

Keep the phone copy deliberately small

The workflow sends a maintainer-written repository label and the numeric issue number. It does not send the issue title, body, labels, comments, closer, author, or repository contents.

The link returns an authorized team member to GitHub for the real record. If the repository name itself is sensitive, use a neutral REPOSITORY_LABEL such as Mobile project. Remember that the issue URL still contains the repository path and will be visible to Room members.

Do not put customer information, security findings, credentials, private incident details, or unreleased plans in the label or notification copy. A lock-screen preview is the wrong place for them.

Two PingRoom avatars inspect a tagged wooden tile at a small verification gate while Pingo stands beside an empty duplicate tray.

Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer, is watching the untagged tile remain outside the gate. He has no repository access, private-Room access, or authority to close an issue or send its alert. A human maintainer chooses the label, workflow, Room, recipients, copy, and failure policy.

These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo. They are not GitHub or PingRoom screenshots and do not depict customers.

Give one closure one replay identity

The idempotency key is a SHA-256 digest of the repository, GitHub's numeric issue ID, and the workflow run ID. Rerunning the same workflow run uses the same key and body, so PingRoom can return the original accepted result instead of creating another Ping.

If an issue is reopened and later closed again, GitHub creates a new workflow run. That new closure gets a new identity and can send a new alert. Do not reuse the first key with changed copy; PingRoom rejects a changed body under an existing idempotency key.

The Ping step is non-blocking because GitHub already owns the closed state. continue-on-error: true leaves the issue record untouched if the shared alert cannot be sent. A team that needs guaranteed escalation should keep its durable project record and established paging path.

Test the boundary before using it

Use a harmless test issue in the selected repository.

  1. Close an issue without shared-status. The job should be skipped.
  2. Add shared-status to another test issue, then close it. One GitHub issue closed Ping should appear.
  3. Open the link as a Room member. GitHub should still enforce repository access.
  4. Rerun the same workflow. It should not create a second Ping.
  5. Reopen and close the issue again. The new closure should create a fresh alert.
  6. Remove the secret and repeat with another test issue. Only the non-blocking notification step should fail.

For this article, we ran the exact embedded Node request against a stateful local mock of PingRoom's documented contract. The checks covered acceptance, exact replay, changed-body conflict, a fresh closure, required headers, field limits, privacy exclusions, insecure URLs, redirects, HTTP rejection, malformed JSON, and missing configuration. They did not use a live GitHub repository, PingRoom Room, phone, or notification receipt.

An HTTP 200 with success: true means PingRoom accepted and queued the Ping. Connectivity, notification permission, Focus settings, Room mutes, and operating-system policy still decide what appears on each phone. Silence is not evidence that an issue stayed open; GitHub remains the record.

If a labeled closure deserves a shared signal, create the destination Room and webhook, add the secret, and test with non-sensitive copy. For a workflow's terminal result instead, use the separate GitHub Actions notification recipe. For review states, keep using the pull-request review guide.

GitHub is a trademark of GitHub, Inc. PingRoom is not affiliated with or endorsed by GitHub.

PingRoom

Short-form communication.

View all posts