# How to Send a New GitHub Issue Alert to a Shared Room

> Use one opt-in triage label, a no-checkout GitHub Actions workflow, and one stable replay key to send fixed shared-phone copy.

- Published: 2026-11-09
- Author: PingRoom
- Category: Developers
- Topics: GitHub new issue notification, GitHub Actions issue alert, GitHub push notification
- Canonical: https://pingroom.io/blog/github-new-issue-push-notifications
- Cover image: https://pingroom.io/blog/github-new-issue-push-notifications-cover.webp
- Cover description: Two PingRoom avatars guide a blank issue tile toward a small triage gate while Pingo watches beside a closed detail box.

**Use GitHub's own notifications when maintainers only need updates inside GitHub. When one selected kind of new issue should reach a small shared group, add an opt-in triage label and let a GitHub Actions workflow send fixed copy to a PingRoom Room.**

The recipe below listens for `issues.opened` and `issues.labeled`, but sends only when `shared-intake` is present. GitHub remains the record for the title, body, author, labels, discussion, and assignment. PingRoom carries only the repository label, issue number, and link back to GitHub.

This is a webhook recipe, not a native GitHub integration. A new issue is an unreviewed submission, not proof of urgency, validity, or work accepted.

## Try GitHub's notification paths first

GitHub lets people [watch a repository and subscribe to issue activity](https://docs.github.com/en/subscriptions-and-notifications/get-started/configuring-notifications). GitHub Mobile has notification settings and working hours. GitHub's official integrations can also send issue activity to [Microsoft Teams with label filters](https://docs.github.com/en/integrations/how-tos/teams/customize-notifications). Start there when the people who need the update already use those destinations and want GitHub context.

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

| Need                                                             | Better starting point                              |
| ---------------------------------------------------------------- | -------------------------------------------------- |
| A maintainer wants the normal issue stream                       | GitHub notifications and repository watch settings |
| A Teams channel should receive selected issue activity           | GitHub's Teams integration and label filter        |
| One selected intake lane should reach a small shared phone group | GitHub Actions → PingRoom incoming webhook         |
| Someone needs the title, body, author, labels, or discussion     | Open the issue in GitHub                           |

The opt-in label is the noise boundary. This example uses `shared-intake`: it means “send the short shared intake signal,” not “this issue is important” or “this issue is safe.” Decide who may apply the label, and use repository permissions and rules for the actual triage policy.

## Create the Room and protect its URL

Create or choose a private Room for the people who need the signal. 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](/blog/webhook-to-push-notification) covers setup. The [webhook reference](/documents/webhooks) 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 full 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-intake` label. Then choose a short `REPOSITORY_LABEL` that is safe on a lock screen, such as **Mobile project**.

## Add one label-gated workflow

Save this as `.github/workflows/new-issue-room-alert.yml` on the repository's default branch.

```yaml
name: Shared new-issue alert

on:
  issues:
    types: [opened, labeled]

permissions: {}

jobs:
  notify-room:
    if: >
      contains(github.event.issue.labels.*.name, 'shared-intake') &&
      (github.event.action == 'opened' || github.event.label.name == 'shared-intake')
    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 }}
      SERVER_URL: ${{ github.server_url }}
    steps:
      - name: Send shared new-issue alert
        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 serverUrl = process.env.SERVER_URL;

            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 ?? "")) throw new Error("Invalid issue identity");
            const server = new URL(serverUrl);
            if (server.protocol !== "https:" || server.username || server.password || server.pathname !== "/" || server.search || server.hash) throw new Error("Invalid SERVER_URL");

            const issueUrl = `${server.origin}/${repository}/issues/${issueNumber}`;
            const digest = createHash("sha256").update(`${repository}:${issueId}:shared-intake`).digest("hex");
            const eventId = `github-new-issue:${digest}`;
            const payload = {
              action: null,
              title: "New GitHub issue",
              message: `${label} · issue #${issueNumber}`,
              data: { url: issueUrl, button_label: "Open issue", issue_number: Number(issueNumber) },
              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](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#issues) documents both `opened` and `labeled` activity and requires the workflow file to exist on the default branch. `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.

The two event types cover both ways the lane can begin:

- An issue created with `shared-intake` already attached qualifies on `opened`.
- An existing issue qualifies when a maintainer later adds exactly `shared-intake`.
- Adding another label later does not qualify, even while `shared-intake` remains present.

## Make both paths converge on one Ping

The replay key is a SHA-256 digest of the repository, GitHub's numeric issue ID, and the fixed lane name. It does not use the workflow run ID. If GitHub emits both a qualifying `opened` event and a `labeled` event, both requests have the same key and body, so PingRoom can return the first accepted result instead of creating a second Ping.

Removing and re-adding `shared-intake` also reuses that identity. This recipe therefore means **one shared signal per issue for this lane**, not one signal every time the label changes. Use a different explicit workflow if repeated state transitions are the real task.

The Ping step is allowed to fail the workflow. That makes a missed shared alert visible in GitHub Actions while leaving the issue record unchanged. Do not treat this small signal as a guaranteed escalation path; keep durable triage and paging in the systems that own them.

![Two PingRoom avatars compare one accepted clay issue marker with a duplicate stopped at a small gate while Pingo observes.](/blog/github-new-issue-push-notifications-1.webp)

[Pingo the Pigeon](/Pingo), PingRoom's fictional Chief Marketing Officer, is watching the second marker stop at the gate. He has no GitHub access, private-Room access, or authority to label an issue or send its alert. Human maintainers choose the lane, 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._

## Keep issue content out of the phone copy

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

The Ping carries an **Open issue** button that returns an authorized Room member to GitHub for the real record. If the repository name is sensitive, use a neutral `REPOSITORY_LABEL`; the issue URL still contains the repository path and is visible to Room members. Do not route private reports, credentials, security findings, customer data, or unreleased plans through a lock-screen preview.

## Rehearse the actual boundary

Use harmless test issues in the selected repository.

1. Open an issue without `shared-intake`. The job should be skipped.
2. Add `shared-intake`. One **New GitHub issue** Ping should appear.
3. Add a different label. No new Ping should appear.
4. Remove and re-add `shared-intake`. PingRoom should return the original accepted result, not create another Ping.
5. Open a second test issue with `shared-intake` already attached. It should create one new Ping.
6. Tap **Open issue** as a Room member. GitHub should still enforce repository access.
7. Remove the secret and try a fresh test issue. The workflow should fail, while the issue remains in GitHub.

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 distinct issue, required headers, HTTPS and input validation, privacy exclusions, HTTP rejection, and malformed JSON. 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. GitHub remains the source of truth.

If one selected intake lane deserves a shared signal, [create the destination Room and webhook](/app/rooms), add the secret, and rehearse it with non-sensitive issues. For a selected issue leaving the workflow, use the separate [issue-closed alert recipe](/blog/github-issue-closed-push-notifications).

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