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
ghcommand-line tool installed:brew install gh, thengh auth login.
How it fits together
Steps
1. Create the organisation
- On GitHub, open your profile menu, then Your organizations, then New organization.
- 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.
- 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. - Contact email: the team mailbox.
- "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:
| Setting | Where | Value | Why |
|---|---|---|---|
| Require two-factor authentication | Authentication security | On | A stolen password alone cannot get in |
| Base permissions | Member privileges | No permission | Access comes only from teams |
| Repository creation | Member privileges | Private only, and only for admins | Stops stray public repos |
| Repository visibility change | Member privileges | Admins only | A repo cannot be made public by mistake |
| Forking private repos | Member privileges | Off | Private code stays in the org |
| Secret scanning and push protection | Code security | On for all repos | Blocks pushes that contain keys |
| Dependabot alerts | Code security | On for all repos | Warns about vulnerable packages |
3. Create teams
Settings is not where teams live. Go to the organisation's Teams tab, then New team:
| Team | Members | Repo access |
|---|---|---|
admins | Tech lead, project lead | Admin |
developers | Everyone writing code | Write |
read-only | Client stakeholders, auditors | Read |
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 has | Do this |
|---|---|
| Repos in their own GitHub account or organisation | In 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 DevOps | git 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 keep | Start 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:
| Component | Repo | What it holds |
|---|---|---|
api | acme-api | The main backend |
<name>-api | acme-search-api | A second backend |
web | acme-web | Customer website or web app |
admin | acme-admin | Staff panel |
mobile | acme-mobile | Flutter or native app |
infra | acme-infra | Terraform |
docs | acme-docs | Developer or public docs |
sdk-<lang> | acme-sdk-node | Client libraries |
prefix proto- | proto-acme-web | Throwaway 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):
| Rule | main | staging, dev |
|---|---|---|
| Require a pull request | Yes, 1 approval (2 for prod-critical repos) | Yes, 1 approval |
| Require status checks to pass | Yes: lint, typecheck, test | Yes |
| Block force pushes | Yes | Yes |
| Restrict deletions | Yes | Yes |
7. Add the standard files
Every repo gets, in its first pull request:
| File | Why |
|---|---|
README.md | What it is, how to run it, how to deploy |
.github/CODEOWNERS | Who must review which paths, for example migrations |
.github/pull_request_template.md | What changed, how it was tested, screenshots |
.github/dependabot.yml | Weekly dependency updates |
.editorconfig | Same spacing and line endings for everyone |
SECURITY.md | How 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 see | It means |
|---|---|
| "Upgrade to GitHub Team" when adding a ruleset | The org is on the free plan. Step 1.2 |
| A member cannot see any repo | They are not in a team, because base permission is "No permission". Step 3 |
gitleaks reports findings | Rotate each key first, then decide whether to rewrite history |
| A pipeline stops deploying after a repo rename | A deploy role trusts the old repo name. Update it in acme-infra |
Rules
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.
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.
mainandmasterboth 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.