PostHog Handbook Library / Engineering

2,138 words. Estimated reading time: 10 min.

# SDK releases

Auto TL;DR

## At a Glance

This long page covers these main areas. The list is generated from the article headings, so it updates with every handbook rebuild.

1. How it works
2. Setting up releases for a new SDK
3. 1. Create a GitHub App
4. 2. Expose proper access to client libraries teams
5. 3. Create a release environment
6. 4. Add app to bypass lists
7. CodeQL bypass
8. Repository PR bypass

This guide documents our semi-automated release process for PostHog SDKs. Each SDK repository uses a GitHub App with restricted permissions to handle releases securely, requiring team approval before any release is published. Commits must be signed, as in every PostHog repository.

If you're creating a new SDK/repo that must be published you MUST implement this approach.

### How it works

Our SDK release process uses a dedicated GitHub App per repository that can push directly to the main branch (bypassing branch protections) while still requiring human approval through GitHub Environments. This gives us:

- Security: The app only has access to the specific repository it needs
- Auditability: All releases require approval from the Client Libraries team
- Automation: Changelog generation, version bumping, and publishing are handled automatically

### Setting up releases for a new SDK

When creating a new SDK, or migrating an existing one to the new workflow, follow these steps to set up the release infrastructure.

Most of these steps require super administrator privileges on GitHub. Organization-level access is temporarily required to complete the setup — request it before you start, and it can be revoked once the release infrastructure is in place.

#### 1. Create a GitHub App

Create a new GitHub App:

1. Name: `Releaser (<sdk_name>)` (e.g., `Releaser (posthog-go)`)
2. Description: Should be "Used to release new versions of `posthog-<sdk_name>` (e.g. "Used to release new versions of `posthog-go`.")
3. Homepage URL: Point to the SDK's docs page on posthog.com (e.g., `https://posthog.com/docs/libraries/go`)
4. Webhook: Disable (uncheck "Active")
5. Permissions: Under "Repository permissions", set only:

- `Contents`: Read and write

> Note: If your app needs to open PRs in other repositories and assign teams or members as reviewers (e.g., the posthog-js upgrader opens PRs from `posthog-js` to `posthog` and assigns the `client-libraries` and `client-libraries-approvers` teams), you also need to add under "Organization permissions":

> - `Members`: Read-only

1. Where can this GitHub App be installed? Keep it restricted to "Only on this account"
2. Click Create GitHub App

After creating the app:

1. Download this image and upload it as the app icon

Image: SDK Releaser bot icon

1. Set the background color to `#D97148`
2. Click the big "Generate a private key" button to generate a private key and save it locally — you'll need it later
3. Also save the "App ID" number - you'll need it later
4. Go to Install App in the sidebar
5. Install the app in the PostHog organization, restricting it to only the SDK repository

#### 2. Expose proper access to client libraries teams

In your SDK repository settings:

1. Verify that both `@PostHog/client-libraries-approvers` and `@PostHog/team-client-libraries` teams have at least read access to the repository. This is required for them to be able to approve release workflows.
2. Access "Collaborators and teams"
3. Make sure both teams are added as collaborators with at least write access
4. For new repositories, make sure Enable release immutability is enabled in the repository settings — you'll find it under Settings → General

#### 3. Create a release environment

In your SDK repository settings:

1. Go to Environments and create a new environment named `Release`
2. Configure protection rules:

- Required reviewers: Add `PostHog/client-libraries-approvers` and `PostHog/team-client-libraries` as the only teams allowed to approve this release
- Prevent self-review: Check this box
- Allow administrators to bypass: Uncheck this box

1. Click Save protection rules to enforce them before configuring deployment branches — the deployment branches and tags settings below save on their own, and clicking "Save protection rules" afterwards resets them
2. Configure deployment branches and tags:

- Deployment branches and tags: Select Selected branches and tags and add `main` as the only selected branch or tag
- If the package manager requires publishing from tags, such as pub.dev, also add `[0-9]*.[0-9]*.[0-9]*` as an allowed deployment branch or tag pattern

Image: Protection rules)

1. Add environment secrets:

- `GH_APP_POSTHOG_<SDK_NAME>_RELEASER_APP_ID` — Copy the App ID from your GitHub App settings
- `GH_APP_POSTHOG_<SDK_NAME>_RELEASER_PRIVATE_KEY` — Paste the private key you downloaded, include the trailing newline
- Easiest way to get the private key value with the correct formatting is via `cat ~/Downloads/release-posthog-<sdk_name>-private-key.pem | pbcopy` on Mac or `type release-posthog-<sdk_name>-private-key.pem | pbcopy`

> Replace `<SDK_NAME>` with your SDK name in uppercase with underscores (e.g., `GH_APP_POSTHOG_GO_RELEASER_APP_ID`, `GH_APP_POSTHOG_GO_RELEASER_PRIVATE_KEY`)

Image: Environment secrets

#### 4. Add app to bypass lists

The GitHub App needs to bypass certain protections to push release commits directly.

##### CodeQL bypass

1. Access the CodeQL ruleset
2. Under Bypass list, click Add bypass
3. Select your newly created GitHub App (`Releaser (<sdk_name>)`)
4. Click the three-dot menu and choose Exempt
5. Save the ruleset

Image: CodeQL bypass exemption

##### Repository PR bypass

1. Go back to your SDK repository settings
2. Navigate to Rules → Rulesets
3. Open the ruleset that requires PRs (may have various names)
4. If this ruleset doesn't exist, create one requiring PRs and reviews from codeowners which should be `@PostHog/client-libraries-approvers` for all files
5. Under Bypass list, click Add bypass
6. Select your GitHub App (`Releaser (<sdk_name>)`)
7. Click the three-dot menu and choose Exempt
8. Save the ruleset

Image: Repository PR bypass

> Don't add a Require signed commits rule at the repo level. The org-wide ruleset already requires signed commits in every repo, and a duplicate repo rule only adds another bypass list to maintain. Note that this bypass exempts the app from the pull request requirement, not from signing: the org signing ruleset has no bypass actors, and the release workflows create their commits through the GitHub API, so they are signed like any other.

> Check for classic branch protection too. The steps above exempt the app in a ruleset, but a repository can also have classic branch protection under Settings → Branches → Branch protection rules, which GitHub enforces alongside rulesets and which has its own separate bypass list. If a classic rule requires pull requests, the release still fails with `protected branch 'main' check failed: Changes must be made through a pull request` even after the ruleset exemption. Remove the classic rule, since the ruleset already covers it, or add the app to its bypass settings as well. Older repos are most likely to have this; newer ones tend to be rulesets-only.

#### 5. Grant access to organization secrets

The release workflow needs access to shared organization secrets. Grant your SDK repository access to the below organization secrets in the organization settings:

Secrets:

- `SLACK_CLIENT_LIBRARIES_BOT_TOKEN`
- `POSTHOG_PROJECT_API_KEY`

Variables:

- `GROUP_CLIENT_LIBRARIES_SLACK_GROUP_ID`
- `SLACK_APPROVALS_CLIENT_LIBRARIES_CHANNEL_ID`

#### 6. Add the release workflow

> Important: Our release workflows use GitHub Actions OIDC tokens for secure authentication with package registries. Make sure your workflow uses a version that supports OIDC for your registry:

> - npm: Node.js v22+

Copy the release workflow from posthog-php — it's the reference implementation for new SDKs and includes security hardening the older workflows lack:

- The release candidate is prepared once, uploaded as a patch artifact, and verified by sha256 before publishing — the approved diff is exactly what ships
- `persist-credentials: false` on every checkout
- The version bump script is pinned by sha256, so it can't be swapped out between approval and publish
- Checks that the tag and GitHub release don't already exist, and that `main` hasn't moved since the release candidate was prepared
- Minimal `permissions` on every job

Adapt it to your SDK:

1. Update the environment variable prefix to match your SDK name
2. Modify the changelog generation logic if needed for your language's conventions
3. Update the version bumping logic for your package manager (npm, pip, etc.)
4. Update the publishing steps for your package registry

> Don't re-run build/test in the release workflow. PR CI and the CI run on push to `main` already gate what lands on `main`, so the release workflow should only version, verify, and publish — re-running the build and test suite there just slows down releases without adding safety.

##### npm packages: set up trusted publishing before enabling the workflow

This applies only to npm publishing (not other package registries).

If your SDK publishes to npm using OIDC trusted publishing, set it up before allowing your GitHub Actions workflow to publish.

###### New package (never published before)

OIDC trusted publishing can only be _configured_ for a package that already exists on npm. For a brand-new package there's nothing to attach the trusted publisher to yet, so the workflow's very first publish can't use OIDC and will fail with a `404`.

To avoid this, bootstrap the package and configure its trusted publisher before enabling the workflow:

1. Log in to npm (`npm login`) as a member of the npm org.
2. Run `setup-npm-trusted-publish` to publish a placeholder so the package exists on the registry. This is a manual, authenticated publish (not OIDC). The command is self-contained and publishes from its own temp directory using your global npm auth, so the working directory doesn't matter.

       npx setup-npm-trusted-publish @posthog/<package-name>

1. Follow the CLI prompts to configure the Trusted Publisher for the placeholder:

- Choose GitHub Actions as the publisher.
- Fill in the repo details (`PostHog/<repo>`) and the release workflow filename (e.g. `release.yml`).
- Only set the environment name (e.g. `NPM Release`) if the publish job itself runs inside that GitHub environment (see the callout below).
- Under Allowed actions, check Allow npm publish.

This bootstraps npm trusted publishing for the package so future automated releases can publish successfully.

###### Existing package

If the package has already been published, you can configure trusted publishing directly in npm package settings instead.

> Casing matters when configuring the trusted publisher. npm validates the GitHub organization, repository, and workflow filename against the values GitHub puts in the OIDC token, and those values are case-sensitive. The PostHog GitHub organization is `PostHog` (capital `P` and `H`) — entering `posthog` causes the publish to fail with a misleading `404 Not Found` from npm, even though the package and the workflow exist. Use `PostHog` exactly when setting up or editing the trusted publisher, both via `setup-npm-trusted-publish` and in the npm package settings UI.

> Only set an environment if the publish job runs in it. npm also validates the environment claim in the OIDC token against the trusted publisher settings. Fill in an environment name only if the job that runs `npm publish` has `environment: <name>` set in the workflow. If a GitHub environment is only used to gate approval in an earlier job, leave the environment field blank, otherwise the publish fails with a misleading `404 Not Found` error.

#### 7. Update the README

Add a section to your SDK's README explaining that releases are semi-automatic and link to the `#approvals-client-libraries` Slack channel where approval requests are posted.

#### 8. Create required labels

Make sure the repository includes the `release` label, it's used to trigger new releases.

If you're not using something like `changesets` or `sampo` that automatically generates version bump labels, create the following labels as well to indicate the type of release:

- `bump-patch`
- `bump-minor`
- `bump-major`

#### 9. Open a PR

Create a PR with the new `release.yml` workflow and request a review from `@PostHog/client-libraries-approvers`. There is now a small, dedicated SDK team at PostHog (@PostHog/team-client-libraries) that helps drive direction and coordination. However, SDK development and maintenance remains a collaborative effort across the engineering organization.

### Triggering a release

Once set up, releases are triggered by having a `release` label added to the PR alongside a `changesets`(or matching `bump-*` tag). Once a PR is merged, the environment workflow will kick up and someone from the `@PostHog/client-libraries-approvers` team will have to approve it on `#approvals-client-libraries`.

For changelog and version management, prefer JS `changesets` (`@changesets/cli`) even for non-JS SDKs — it only needs Node in CI, which the release tooling already uses. This is what posthog-php does. Some SDKs use `sampo`, a language-agnostic take on `changesets`, but it pulls in a Rust toolchain and is slower in CI, so we don't recommend it for new SDKs.

### Troubleshooting

#### `Access token expired or revoked` when running `npm publish`

If you see the error "Access token expired or revoked. Please try logging in again" when publishing with `npm publish` — even though your credentials and tokens are correctly configured — the issue may be with npm's token handling itself.

Solution: Migrate your project to use a pnpm workspace and publish with `pnpm publish` instead. pnpm handles authentication differently and isn't affected by this issue.

Canonical URL: https://posthog.com/handbook/engineering/sdks/releases

GitHub source: contents/handbook/engineering/sdks/releases.mdx

Content hash: `df50154cb45bbf7e`

Static reader notes

- `MDX_COMPONENT_STATIC_ADAPTER`: Adapted interactive MDX components for static reading: CalloutBox, SDK_NAME.