Integrations
GitHub Actions
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
bootstrapcapability; - 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.
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:
| Secret | Value |
|---|---|
SEMAFORE_BOOTSTRAP_TOKEN | The one-time SemaFore token with bootstrap capability. |
SEMAFORE_GITHUB_SECRET_TOKEN | A 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:
- Delete the bootstrap workflow.
- Revoke the temporary GitHub PAT or App installation token.
- Delete
SEMAFORE_GITHUB_SECRET_TOKENandSEMAFORE_BOOTSTRAP_TOKENfrom the repository. - 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:
orgfor all eligible user-owned devices in the token’s organisation;group:<id>for eligible devices belonging to a group; anduser:<id>for one organisation member.
Organisation-owned integration devices are senders and are not eligible notification recipients.
Inputs
| Input | Required | Modes | Accepted value |
|---|---|---|---|
token | Yes | notify, execute | Runtime SemaFore service token, supplied from a GitHub Actions secret. |
bootstrap_token | Yes | bootstrap | One-time SemaFore service token with bootstrap capability. |
device_key | Yes | notify | JSON device state written to SEMAFORE_DEVICE_KEY by bootstrap. |
mode | Yes for the root Action | All | notify, execute, or bootstrap; defaults to notify. The bootstrap sub-action infers bootstrap. |
target | Yes | notify | org, group:<id>, or user:<id>. |
template | Yes | notify | Plaintext template encrypted in the runner after supported placeholders are expanded. |
action | Yes | execute | create_thread, archive_thread, or audit_event. |
params | No | execute | JSON object for the selected execute action; defaults to {}. |
severity | No | notify | Reserved for a future wire-contract revision; omit for v1. |
api_base_url | No | All | HTTPS API origin; defaults to https://api.semafore.io. |
github_token | Yes | bootstrap | Temporary 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.