GitHub Action that reconciles GitHub Enterprise membership against the assignments of a single Okta application. Any enterprise member whose verified domain email is not present in the Okta app's user list is removed from the enterprise via the GraphQL removeEnterpriseMember mutation.
Matching is performed on lowercased email addresses from the github_com_verified_domain_emails field returned by GET /enterprises/{enterprise}/consumed-licenses (REST API version 2026-03-10).
| Name | Required | Default | Description |
|---|---|---|---|
enterprise |
yes | — | Enterprise slug. |
github-token |
yes | — | Token with enterprise:admin scope. |
okta-domain |
yes | — | Okta domain (e.g. example.okta.com, no scheme). |
okta-token |
yes | — | Okta API token (SSWS) with rights to read app users. |
okta-app-id |
yes | — | Okta application ID whose assigned users are the source of truth. |
dry-run |
no | true |
If true, only logs intended removals. Set to false to enforce. |
email-domain-filter |
no | "" |
Optional comma-separated allowlist of email domains to consider when matching. |
| Name | Description |
|---|---|
removed-count |
Count of users removed (or that would be removed in dry-run). |
removed-logins |
JSON array of GitHub logins removed (or that would be). |
drift-spared-logins |
JSON array of GitHub logins spared from removal because a re-check found them in Okta. |
- Lists Okta app assignments via
GET /api/v1/apps/{appId}/users(covers users assigned directly and via group push). - Iterates
consumed-licensespaginated; only entries withgithub_com_user == trueandlicense_type == "enterprise"are considered. - Users with no usable
github_com_verified_domain_emailscause the run to fail after the report (so they are surfaced for manual triage). - Removal uses GraphQL: looks up the enterprise node ID via
enterprise(slug:)and the user node ID viauser(login:), then callsremoveEnterpriseMember.
There is an unavoidable window between fetching the Okta snapshot, fetching GitHub membership, and issuing each removal. To bias toward preserving access, the action applies two layered checks immediately before each removal:
- Refreshed Okta snapshot — the app's assigned users are re-fetched right before the removal phase. If a candidate's verified email now appears in the refreshed snapshot, removal is skipped.
- Per-user authoritative re-check — for each remaining candidate, the action calls
GET /api/v1/users?search=…to resolve the Okta user, thenGET /api/v1/apps/{appId}/users/{userId}to confirm they are not currently assigned to the app.
Fail-safe: if the per-user re-check itself errors (Okta 5xx, rate limit, network), the user is spared, not removed (logged as a warning). All spared logins are reported in drift-spared-logins and in the job summary.
name: Reconcile enterprise membership
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
inputs:
dry-run:
description: Dry run
required: false
default: "true"
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actionsdesk/user-sync-okta@v1
with:
enterprise: my-enterprise
github-token: ${{ secrets.ENTERPRISE_ADMIN_PAT }}
okta-domain: example.okta.com
okta-token: ${{ secrets.OKTA_API_TOKEN }}
okta-app-id: 0oa1abcd2EFG3hijK4l5
dry-run: ${{ inputs.dry-run || 'true' }}
email-domain-filter: example.com,corp.example.comnpm install
npm run build # type-check + bundle to dist/The bundled dist/index.js must be committed for the action to run.