Integrations

GitHub Actions

Last updated: 15 September 2026

The SemaFore GitHub Action sends end-to-end encrypted notifications from a GitHub Actions workflow to a SemaFore organisation, group, or user. The workflow runner encrypts a separate envelope for every recipient device before the request leaves GitHub.

For copy-ready deployment, pull request, release, and security-alert examples, see the SemaFore starter workflows.

Before you begin

You need:

  • a SemaFore organisation on a free or paid plan;
  • a GitHub repository with Actions enabled;
  • at least one approved SemaFore mobile device in the target organisation;
  • a one-time SemaFore service token with the bootstrap capability;
  • a separate SemaFore service token with notify, execute, or both; and
  • a fine-grained GitHub personal access token or GitHub App installation token scoped to the repository with Secrets: write permission.
Warning
Use separate bootstrap and runtime service tokens. A bootstrap token is consumed after successful device registration and cannot be used for later notifications.

The SemaFore portal does not yet expose service-token management. An organisation administrator can issue tokens through the portal API. The raw token appears once in the response, so store it immediately as a GitHub Actions secret.

curl -sS -X POST \
  "https://api.semafore.io/api/portal/orgs/$SEMAFORE_ORG_ID/service-tokens" \
  -H "Authorization: Bearer $SEMAFORE_PORTAL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "GitHub Actions bootstrap",
    "capabilities": ["bootstrap"]
  }'

Issue a second token for ongoing use, granting only the modes the workflow needs:

{
  "display_name": "GitHub Actions runtime",
  "capabilities": ["notify", "execute"]
}

Bootstrap once

Add these repository secrets before running the bootstrap workflow:

SecretValue
SEMAFORE_BOOTSTRAP_TOKENThe one-time SemaFore token with bootstrap capability.
SEMAFORE_GITHUB_SECRET_TOKENA temporary fine-grained PAT or GitHub App installation token with repository Secrets: write permission.

Create .github/workflows/semafore-bootstrap.yml:

name: Bootstrap SemaFore

on:
  workflow_dispatch:

permissions: {}

concurrency:
  group: semafore-bootstrap
  cancel-in-progress: false

jobs:
  bootstrap:
    runs-on: ubuntu-latest
    steps:
      - uses: Attomus/semafore-github-action/bootstrap@v1
        with:
          bootstrap_token: ${{ secrets.SEMAFORE_BOOTSTRAP_TOKEN }}
          github_token: ${{ secrets.SEMAFORE_GITHUB_SECRET_TOKEN }}

Run the workflow manually once. The Action generates the integration device’s private key material in the runner, registers its public key bundle with SemaFore, and writes the private state to the SEMAFORE_DEVICE_KEY repository secret. The built-in GITHUB_TOKEN cannot write Actions secrets, which is why the separate temporary token is required.

After the run succeeds:

  1. Delete the bootstrap workflow.
  2. Revoke the temporary GitHub PAT or App installation token.
  3. Delete SEMAFORE_GITHUB_SECRET_TOKEN and SEMAFORE_BOOTSTRAP_TOKEN from the repository.
  4. Keep SEMAFORE_DEVICE_KEY; notify mode needs it.

Send a notification

Store the runtime SemaFore token as SEMAFORE_TOKEN, then add an ongoing workflow:

name: Notify SemaFore

on:
  push:
    branches: [main]

permissions: {}

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - id: semafore
        uses: Attomus/semafore-github-action@v1
        with:
          token: ${{ secrets.SEMAFORE_TOKEN }}
          device_key: ${{ secrets.SEMAFORE_DEVICE_KEY }}
          mode: notify
          target: org
          template: 'Build {{run_id}} on {{ref}} completed at {{sha}}'

The template supports {{run_id}}, {{ref}}, {{sha}}, {{actor}}, {{workflow}}, and {{repository}}. Notify mode sets the message_id output to the identifier accepted by SemaFore.

Targets are:

  • org for all eligible user-owned devices in the token’s organisation;
  • group:<id> for eligible devices belonging to a group; and
  • user:<id> for one organisation member.

Organisation-owned integration devices are senders and are not eligible notification recipients.

Inputs

InputRequiredModesAccepted value
tokenYesnotify, executeRuntime SemaFore service token, supplied from a GitHub Actions secret.
bootstrap_tokenYesbootstrapOne-time SemaFore service token with bootstrap capability.
device_keyYesnotifyJSON device state written to SEMAFORE_DEVICE_KEY by bootstrap.
modeYes for the root ActionAllnotify, execute, or bootstrap; defaults to notify. The bootstrap sub-action infers bootstrap.
targetYesnotifyorg, group:<id>, or user:<id>.
templateYesnotifyPlaintext template encrypted in the runner after supported placeholders are expanded.
actionYesexecutecreate_thread, archive_thread, or audit_event.
paramsNoexecuteJSON object for the selected execute action; defaults to {}.
severityNonotifyReserved for a future wire-contract revision; omit for v1.
api_base_urlNoAllHTTPS API origin; defaults to https://api.semafore.io.
github_tokenYesbootstrapTemporary fine-grained PAT or GitHub App token with repository Secrets: write permission.

Sensitive values must come from GitHub Actions secrets. The Action masks them before use and rejects common placeholder values.

Execute actions

Execute mode requires a service token with the execute capability. It sends the selected params object without notification-content encryption. Use notify mode for ordinary plaintext workflow messages.

The Action writes each execute response as JSON to its result output. Give the step an id, then read steps.<id>.outputs.result in a later step.

create_thread

Creates a thread from content that has already been encrypted to its recipient devices. The Action does not turn plaintext thread content into these fields.

- id: create-thread
  uses: Attomus/semafore-github-action@v1
  with:
    token: ${{ secrets.SEMAFORE_TOKEN }}
    mode: execute
    action: create_thread
    params: ${{ secrets.SEMAFORE_CREATE_THREAD_PARAMS }}

SEMAFORE_CREATE_THREAD_PARAMS must contain this shape:

{
  "title_ciphertext": "<base64url ciphertext>",
  "title_envelope_recipients": ["<recipient device id>"],
  "initial_message_envelopes": [
    {
      "recipient_user_id": "<user id>",
      "recipient_device_id": "<device id>",
      "ciphertext": "<base64url ciphertext>",
      "dr_header": "534d4431<hex-encoded header>"
    }
  ]
}

The response contains thread_id, status, and created_at.

archive_thread

Archives a thread owned by the token’s organisation:

- id: archive-thread
  uses: Attomus/semafore-github-action@v1
  with:
    token: ${{ secrets.SEMAFORE_TOKEN }}
    mode: execute
    action: archive_thread
    params: '{"thread_id":"thread-123"}'

The response contains thread_id, status, and archived_at.

audit_event

Records bounded GitHub run metadata without message content. Repeating the same event kind and run ID with the same service token is deduplicated for 24 hours.

- id: event-time
  shell: bash
  run: echo "value=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"

- id: audit-event
  uses: Attomus/semafore-github-action@v1
  with:
    token: ${{ secrets.SEMAFORE_TOKEN }}
    mode: execute
    action: audit_event
    params: >-
      {"event_kind":"workflow.completed","github_context":{"run_id":"${{ github.run_id }}","ref":"${{ github.ref }}","sha":"${{ github.sha }}","repo":"${{ github.repository }}"},"occurred_at":"${{ steps.event-time.outputs.value }}"}

The response contains event_id, status, and recorded_at.

Security model

Notify mode resolves recipient public key bundles, establishes or advances an X3DH/Double Ratchet session for each device, and emits only encrypted envelopes. SemaFore can see the organisation-scoped routing metadata needed for delivery, but it does not receive the notification plaintext or recipient private keys. The SemaFore crypto library is bundled into the Action so the runner does not install code at execution time.

Bootstrap generates private key material in the runner. Before it writes that state to GitHub Actions secrets, it encrypts the value to GitHub’s repository public key. SemaFore stores the integration device’s public bundle only. Audit records contain operational metadata such as token use and delivery identifiers, not notification content.

Troubleshooting

Bootstrap says SEMAFORE_DEVICE_KEY already exists

Revoke the old integration device in SemaFore, delete the old SEMAFORE_DEVICE_KEY repository secret, issue a new one-time bootstrap token, and run bootstrap again. The Action refuses to overwrite existing private state.

Notify returns HTTP 403

Check that the runtime service token has the notify capability and has not expired or been revoked. The target group or user must belong to the token’s organisation.

Notify cannot resolve a recipient device

Check that the target includes at least one approved, signed-in iOS or Android device. Organisation-owned integration devices are deliberately excluded from recipient lookup.

The push notification does not arrive

Confirm the recipient has completed account security enrolment, the device is approved in the correct organisation, and notification permission is enabled for SemaFore in the phone’s system settings.

Pricing, licence, and support

The Action is free to use under the Apache-2.0 licence. A SemaFore organisation account is required; see SemaFore pricing for available plans.

For setup help, contact support@semafore.io. Report suspected security issues privately to security@attomus.com or through the Action repository’s Security tab.