Skip to main content

3. GitHub organisation

What it is​

A GitHub organisation is a shared account for a company. Repos belong to the organisation, people are members, and members are grouped into teams that get access to repos. When someone leaves, you remove them from the organisation and they lose everything at once.

Why we do it​

  • Code in a personal account leaves with that person, or has to be moved later. Moves lose settings and leak history.
  • Branch rules, required reviews and secret scanning are set once for the organisation.
  • At handover we make the client's person an owner and step out. The repos never move.

Before you start​

  • A GitHub account for yourself, with two-factor authentication on.
  • The team mailbox for the project, for example dev@example.com.
  • The client's short name, agreed with the project lead. It becomes the organisation name and appears in every URL and package name.
  • The list of repos the project needs. See the naming table in step 5.
  • The gh command-line tool installed: brew install gh, then gh auth login.

How it fits together​

Steps​

1. Create the organisation​

  1. On GitHub, open your profile menu, then Your organizations, then New organization.
  2. Choose the Team plan. On the free plan you cannot protect branches in private repos, so none of the rules in step 6 would work.
  3. Organisation name: the client's short name in lowercase with hyphens, for example example-co. Read it back letter by letter before you click create. We have an organisation with a typo in its name; the typo is now in every package name.
  4. Contact email: the team mailbox.
  5. "This organisation belongs to": A business or institution, with the client's legal name.

2. Lock down the basics​

In the organisation, go to Settings:

SettingWhereValueWhy
Require two-factor authenticationAuthentication securityOnA stolen password alone cannot get in
Base permissionsMember privilegesNo permissionAccess comes only from teams
Repository creationMember privilegesPrivate only, and only for adminsStops stray public repos
Repository visibility changeMember privilegesAdmins onlyA repo cannot be made public by mistake
Forking private reposMember privilegesOffPrivate code stays in the org
Secret scanning and push protectionCode securityOn for all reposBlocks pushes that contain keys
Dependabot alertsCode securityOn for all reposWarns about vulnerable packages

3. Create teams​

Settings is not where teams live. Go to the organisation's Teams tab, then New team:

TeamMembersRepo access
adminsTech lead, project leadAdmin
developersEveryone writing codeWrite
read-onlyClient stakeholders, auditorsRead

Invite people from People, then Invite member, and add them to a team in the same dialog. Never add someone directly to a single repo; it is easy to forget and hard to audit.

4. Bring in existing code, if there is any​

The client hasDo this
Repos in their own GitHub account or organisationIn each repo: Settings, General, Danger Zone, Transfer. Choose our new organisation. History, issues and pull requests move, and the old URL redirects
Repos on GitLab, Bitbucket or Azure DevOpsgit clone --mirror <old-url>, then cd into it and git push --mirror https://github.com/example-co/<new-name>.git
A zip of code, or code we will not keepStart fresh in step 5. Keep their code as an archived, read-only repo for reference

After any import, scan the history for secrets before you give anyone else access:

brew install gitleaks
gitleaks detect --source . --log-opts="--all"

If it finds a key, rotate the key with its provider. Removing it from history is not enough once it has been pushed anywhere.

5. Create the repos with standard names​

We name repos <product>-<component>, lowercase, with hyphens:

ComponentRepoWhat it holds
apiacme-apiThe main backend
<name>-apiacme-search-apiA second backend
webacme-webCustomer website or web app
adminacme-adminStaff panel
mobileacme-mobileFlutter or native app
infraacme-infraTerraform
docsacme-docsDeveloper or public docs
sdk-<lang>acme-sdk-nodeClient libraries
prefix proto-proto-acme-webThrowaway prototypes only
gh repo create example-co/acme-api --private --add-readme \
--description "Acme backend API"

Keep names short: some AWS resource names derived from them are limited to 32 characters.

If the client already has a naming scheme, use theirs and write it in the organisation's profile README. Consistency inside one organisation matters more than matching our other clients.

6. Set each repo's rules​

In each repo, Settings, General, under Pull Requests:

  • Allow squash merging only. Turn off merge commits and rebase merging.
  • Turn on Automatically delete head branches.

Create the long-lived branches:

git switch -c dev && git push -u origin dev
git switch -c staging && git push -u origin staging

Then Settings, Rules, Rulesets, New branch ruleset, targeting main (and a second one for staging and dev):

Rulemainstaging, dev
Require a pull requestYes, 1 approval (2 for prod-critical repos)Yes, 1 approval
Require status checks to passYes: lint, typecheck, testYes
Block force pushesYesYes
Restrict deletionsYesYes

7. Add the standard files​

Every repo gets, in its first pull request:

FileWhy
README.mdWhat it is, how to run it, how to deploy
.github/CODEOWNERSWho must review which paths, for example migrations
.github/pull_request_template.mdWhat changed, how it was tested, screenshots
.github/dependabot.ymlWeekly dependency updates
.editorconfigSame spacing and line endings for everyone
SECURITY.mdHow to report a security problem

Check it worked​

gh repo list example-co
gh api orgs/example-co --jq '.two_factor_requirement_enabled' # true
gh api repos/example-co/acme-api/rulesets --jq '.[].name'

Then try to push directly to main. It must be refused.

If it goes wrong​

You seeIt means
"Upgrade to GitHub Team" when adding a rulesetThe org is on the free plan. Step 1.2
A member cannot see any repoThey are not in a team, because base permission is "No permission". Step 3
gitleaks reports findingsRotate each key first, then decide whether to rewrite history
A pipeline stops deploying after a repo renameA deploy role trusts the old repo name. Update it in acme-infra

Rules​

Rule

Never fork another product's repo to start this one. If you copy code, rename every CI name first: image repository, container name, secret prefix, deploy files. A forked repo's first tag once nearly deployed over a live product.

Rule

No tokens in git remote URLs. A remote such as https://<user>:<token>@github.com/... stores the token in plain text. Use gh auth login or SSH keys.

Mistakes we made​

  • One product ended up in two organisations with two spellings, and package names inherited a typo.
  • Branch rules existed only in a document, because the organisation was on the free plan.
  • Over a thousand old branches were never deleted, so the branch list was useless.
  • main and master both existed in four repos, and tags used three formats.
  • A token was found in a cloned repo's remote URL and had to be revoked.

Done when​

  • Organisation on the Team plan, name checked, 2FA required.
  • Base permission "No permission"; teams for admins, developers, read-only.
  • Secret scanning, push protection and Dependabot on.
  • Existing code brought in and scanned.
  • Repos named to the standard (or the client's, documented).
  • Squash only, auto-delete branches, rulesets on main, staging, dev.
  • Standard files in every repo.