Import and synchronization
teamapi import github-org <org> --out <dir> bootstraps teamapi.yml files from an org that already exists on GitHub, instead of hand-writing them: one <team-id>/teamapi.yml per GitHub team, with members resolved from GitHub's user profiles (name, email, and the githubUsername that Sync with GitHub teams needs) and a services[] entry per repo the team owns.
$ teamapi import github-org acme-example --out ./imported
Wrote 4 team(s) to ./imported/ — every team defaulted to type: stream-aligned with no roles[]; review and adjust both by hand, then run `teamapi validate`.
GitHub teams carry no Team Topologies typing or role hierarchy, so every generated team defaults to type: stream-aligned with an empty roles[] — both are meant to be corrected by hand, not taken as ground truth. Run teamapi validate ./imported next, then fill in roles[], fix each team's type, and add cognitiveLoad/interactions/dependencies as you would for any hand-authored team. Requires a GitHub token via --token or GITHUB_TOKEN/GH_TOKEN.
Five import sources#
How much an org must enter by hand depends on the data it already has:
| source | argument | needs | gets you |
|---|---|---|---|
github-org |
the org login | GITHUB_TOKEN |
teams, members with githubUsername, services from repos |
backstage |
a catalog file or URL | — | groups, members, owned components/APIs, a guessed team type |
okta |
your Okta org URL | OKTA_TOKEN |
one team per directory group, with its people |
slack |
— | SLACK_BOT_TOKEN |
a skeleton per channel: name, topic as focus, channel declared |
csv |
a file | — | teams, people, and roles[] from a job-title column |
# Already have a Backstage catalog? Everything a Team API document wants is in it.
teamapi import backstage http://backstage.internal/api/catalog/entities --out ./teams
# Four hundred teams in Okta, none of them written down anywhere else.
teamapi import okta https://acme.okta.com --prefix eng- --out ./teams
# An HRIS export. The only source that can populate roles[].
teamapi import csv ./people.csv --out ./teams
# The list of teams exists nowhere but a channel sidebar. It happens.
teamapi import slack --match '^team-' --prefix team- --out ./teams
The importers make the following choices:
- CSV creates one role per distinct job title, shared by everybody holding it. A job-title column records a person's position, matching the schema's separation of roles and members. Shared job titles therefore produce shared roles. The CSV reader handles quoted fields such as
"Engineer, Payments". - Okta drops deactivated accounts.
okta-driftreports those as findings on an existing org, because a name still listed for somebody who left is the dangerous case. On a fresh import there's nothing to report against, and importing them would create the exact drift the tool exists to catch. - Slack imports no members, even though the API lists them. Channel membership includes everyone who wanted visibility and does not reliably represent a team. Importing it would produce an authoritative-looking but inaccurate
members[]. - Backstage reads
spec.memberOfandrelations[]alike, so a rawcatalog-info.yamland the processed entities the catalog API returns behave the same.--prefixstrips a naming convention off group and channel names, and matches whatokta-driftalready takes.
Every source is deliberately incomplete in the same way: nothing outside the source is invented, and what each one couldn't know is printed after the run.
Sync with GitHub teams#
Everything above reads the spec; teamapi apply is the one command that writes back to a real system — it reconciles actual GitHub teams and memberships in a GitHub org with what the spec declares, the way terraform plan/apply reconciles infrastructure. One GitHub team per Team API team, matched by slug === team id; members are resolved via each member's githubUsername (add it alongside contact — see the spec). A member with no githubUsername set is reported as skipped, not silently dropped from the plan.
It always prints a plan first. ACME Org's members don't carry a githubUsername (they're fictional), so running it as-is reports every member as skipped; add the field to see adds/removes, e.g.:
$ teamapi apply examples/acme-org --org acme-example
+ create team 'stream-checkout' in acme-example
+ add @diego-alves to 'stream-checkout'
! 'stream-checkout': 2 member(s) skipped, no githubUsername set: yuki-tanaka, fatima-al-sayed
Re-run with --yes to apply this plan.
Nothing is written until you re-run with --yes. A team that doesn't exist yet in GitHub is created (named after the team id, so its slug matches — rename it in GitHub afterward for a friendlier display name); an existing team's membership is diffed and only the difference (adds/removes) is applied. Requires a GitHub token with admin:org scope, via --token or GITHUB_TOKEN/GH_TOKEN.
Write back to Slack, Okta and PagerDuty#
apply has always written to GitHub teams. Slack, Okta, and PagerDuty began as read-only drift reports. Fixing every finding by hand in a UI did not keep up with the drift, so teamapi apply-to <slack|okta|pagerduty> now closes those loops with the same plan-then---yes flow as apply:
teamapi apply-to slack ./org
+ create @stream-checkout (stream-checkout)
+ add diego.alves@acme.example to @stream-checkout
- remove U03KX9 from @platform-payments
! @enabling-devex: 1 member(s) with no matching Slack account: alex-tran
Re-run with --yes to apply this plan.
| target | what's written | never written |
|---|---|---|
slack |
usergroup membership, and the usergroup itself | channels, channel membership |
okta |
group membership | groups themselves — created or deleted |
pagerduty |
team membership | schedules, escalation policies, teams |
The Slack one is the one that pays for itself: @platform-payments in a message is how people actually reach a team, it's maintained by hand, and it's wrong within weeks of anybody joining or leaving — silently, in the one place where being wrong means the message reaches nobody.
The command leaves several operations out because the source data cannot perform them safely:
- PagerDuty schedules are never written. Schedules record temporary facts such as swaps and holidays;
teamapi.ymlrecords structure. Generating a schedule from team membership could silently overwrite an incident-time override during the next CI run. Team membership is safe to sync because the org graph already holds that fact, and stale membership commonly leaves escalation policies pointing to people who moved teams. The plan repeats this limit on every run. - Directory groups are never created or deleted. A missing group is reported, because creating one is how a directory quietly acquires a second grouping scheme nobody governs; and deleting one can revoke access to everything mapped onto it, which no static document should do as a side effect.
- Deactivated accounts aren't removals.
okta-driftreports those, because offboarding is a different operation usually owned by somebody else. - Members are matched by email, the only field both systems reliably carry. Anything unresolved is listed rather than guessed at — a fuzzy name match that picks the wrong Ana is worse than a line in a report.
None of these APIs has a transaction, so a failure partway through says so and tells you to re-run, rather than reporting success over a half-applied change.