1. Resources
  2. /
  3. Plugins
  4. /
  5. create-github-app-token-buildkite-plugin

Create GitHub App Token Buildkite Plugin

A Buildkite plugin that creates GitHub App installation access tokens for authenticating with GitHub APIs in your Buildkite pipelines.

This plugin is inspired by and provides similar functionality to the actions/create-github-app-token GitHub Action.

Example

Generate a GitHub App token and use it to authenticate with GitHub:

steps:
  - label: "Generate GitHub App Token"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
    command: |
      # Token is now available in GITHUB_TOKEN environment variable
      curl -H "Authorization: Bearer \$GITHUB_TOKEN" https://api.github.com/user

Configuration

Required

app-id (string)

The GitHub App ID or a reference to an environment variable containing the App ID. You can find the App ID in your GitHub App’s settings page.

  • Direct value: "123456"
  • Environment variable reference: "$$GITHUB_APP_ID" (recommended for security)

Note: Use $$ to prevent Buildkite from interpolating the variable. The plugin will handle the environment variable expansion internally.

private-key (string)

The GitHub App’s private key in PEM format or a reference to an environment variable containing the private key. This should be stored securely using Buildkite secrets.

  • Environment variable reference: "$$GITHUB_APP_PRIVATE_KEY" (recommended)
  • Direct value: Not recommended for security reasons

Note: Use $$ to prevent Buildkite from interpolating the variable. The plugin will handle the environment variable expansion internally.

Optional

owner (string)

Repository owner. If not specified, defaults to the current repository owner extracted from BUILDKITE_REPO.

Example: "octocat"

repositories (string)

Comma or newline-separated list of repository names to scope the token access. If not specified, the token will have access to all repositories the GitHub App is installed on.

Example: "repo1,repo2" or:

repositories: |
  repo1
  repo2

skip-token-revoke (string)

Skip automatic token revocation during cleanup. Set to "true" to skip revocation.

Default: "false"

github-api-url (string)

GitHub REST API URL. Useful for GitHub Enterprise Server instances.

Default: "https://api.github.com"

Example: "https://github.example.com/api/v3"

output-variable (string)

The name of the environment variable to store the generated token. This is a Buildkite-specific parameter.

Default: GITHUB_TOKEN

Example: "CUSTOM_TOKEN_VAR"

max-attempts (integer)

How many times each GitHub API request is attempted before the step fails. Only transient failures are retried; see Retries.

Default: 5

max-retry-wait (integer)

The most seconds one request may spend waiting between attempts. A retry that would take the total past this limit, such as a long Retry-After, fails the step instead.

Default: 120

Permission parameters

Each permission-* option sets one permission on the minted token, to read or write (a few, such as permission-workflows, only accept write). The option name maps to the API permission of the same name with dashes replaced by underscores, so permission-pull-requests requests pull_requests.

The list mirrors the inputs of actions/create-github-app-token, which generates it from GitHub’s OpenAPI description of the create installation access token endpoint.

When no permission-* option is set, the token inherits every permission the app installation has. Setting even one narrows the token to exactly what you list, so list everything the step needs.

[!IMPORTANT] permission-actions and permission-workflows are different permissions. actions covers workflow runs and artifacts; workflows is what lets a token create or update files under .github/workflows/. A bot that opens pull requests touching workflow files — Renovate, for instance — needs permission-workflows: write, and GitHub rejects the push with a refusing to allow a GitHub App to create or update workflow error without it.

Repository permissions

  • permission-actions
  • permission-administration
  • permission-artifact-metadata
  • permission-attestations
  • permission-checks
  • permission-codespaces
  • permission-contents
  • permission-dependabot-secrets
  • permission-deployments
  • permission-discussions
  • permission-environments
  • permission-issues
  • permission-merge-queues
  • permission-metadata
  • permission-packages
  • permission-pages
  • permission-pull-requests
  • permission-repository-custom-properties
  • permission-repository-hooks
  • permission-repository-projects
  • permission-secret-scanning-alerts
  • permission-secrets
  • permission-security-events
  • permission-single-file
  • permission-statuses
  • permission-vulnerability-alerts
  • permission-workflows

Organization and user permissions

  • permission-custom-properties-for-organizations
  • permission-email-addresses
  • permission-enterprise-custom-properties-for-organizations
  • permission-followers
  • permission-git-ssh-keys
  • permission-gpg-keys
  • permission-interaction-limits
  • permission-members
  • permission-organization-administration
  • permission-organization-announcement-banners
  • permission-organization-copilot-seat-management
  • permission-organization-custom-org-roles
  • permission-organization-custom-properties
  • permission-organization-custom-roles
  • permission-organization-events
  • permission-organization-hooks
  • permission-organization-packages
  • permission-organization-personal-access-token-requests
  • permission-organization-personal-access-tokens
  • permission-organization-plan
  • permission-organization-projects
  • permission-organization-secrets
  • permission-organization-self-hosted-runners
  • permission-organization-user-blocking
  • permission-profile
  • permission-starring
  • permission-team-discussions

Deprecated

  • permission-organization-members — this plugin’s original name for permission-members. Still honoured, and still mapped to the members API permission, but it logs a warning; prefer permission-members.

Usage Examples

Basic Usage

steps:
  - label: "Use GitHub App Token"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
    command: |
      # Use the token to make authenticated requests
      gh api user

With Permissions and Repository Scope

steps:
  - label: "Scoped GitHub App Token"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
          repositories: "repo1,repo2"
          permission-contents: "read"
          permission-issues: "write"
          permission-pull-requests: "write"
    command: |
      # Token has limited scope and permissions
      gh api repos/owner/repo1/contents/README.md

With Organization Members Permission

steps:
  - label: "Organization Members Access"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
          permission-members: "read"
    command: |
      # Token can access organization member information
      gh api orgs/myorg/members

For a Bot That Updates Workflow Files

Editing .github/workflows/** needs permission-workflows, which is separate from permission-actions:

steps:
  - label: ":robot_face: renovate"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$RENOVATE_APP_ID"
          private-key: "$$RENOVATE_APP_PRIVATE_KEY"
          permission-contents: "write"
          permission-pull-requests: "write"
          permission-issues: "write"
          permission-workflows: "write"
      - altana-ai/renovate#v0.1.1: ~

GitHub Enterprise Server

steps:
  - label: "Enterprise GitHub App Token"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
          github-api-url: "https://github.company.com/api/v3"
    command: |
      # Works with GitHub Enterprise Server
      gh api user

Skip Token Revocation

steps:
  - label: "Long-lived Token"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
          skip-token-revoke: "true"
    command: |
      # Token won't be revoked automatically
      echo "Token will remain active until expiration"

Custom Output Variable

steps:
  - label: "Custom Token Variable"
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
          output-variable: "MY_GITHUB_TOKEN"
    command: |
      # Token is available in MY_GITHUB_TOKEN
      curl -H "Authorization: Bearer \$MY_GITHUB_TOKEN" https://api.github.com/repos/owner/repo

Map Buildkite secrets to the environment variables this plugin reads with the secrets step attribute. It needs agent 3.106.0 or later, secrets are scoped to the agent’s cluster, and their values are redacted from build logs automatically:

steps:
  - label: "Generate Token with Secrets"
    secrets:
      GITHUB_APP_ID: my_github_app_id
      GITHUB_APP_PRIVATE_KEY: my_github_app_private_key
    plugins:
      - altana-ai/create-github-app-token#v0.6.0:
          app-id: "$$GITHUB_APP_ID"
          private-key: "$$GITHUB_APP_PRIVATE_KEY"
    command: |
      gh api user

The $$ stops Buildkite interpolating the variable when the pipeline is uploaded, so the plugin resolves it on the agent, where the secret exists.

Restricting Who Can Read the Private Key

The private key is long-lived, and anything that can read it can mint tokens for every repository the app is installed on. A Buildkite secret is readable by every agent in its cluster by default, so attach an access policy to it — the cluster’s secret → Access tab → Restrict access to agents matching a policy:

# Only this pipeline, and only when Buildkite itself started the build.
- pipeline_id:
    - "01234567-89ab-cdef-0123-456789abcdef"
  build_source:
    - "schedule"
    - "ui"

Claims within a rule are ANDed, values within a claim are ORed, rules are ORed, and access is denied when nothing matches. Prefer the first-party claims (pipeline_id, build_source, cluster_queue_id), which Buildkite generates; the third-party ones (pipeline_slug, build_branch, build_creator) are convenient but derive from user- or tool-supplied values, so they make a weaker boundary.

Restricting build_source is what stops a pull request against the pipeline’s own repository from reading the key — an untrusted branch cannot mint a token even if it rewrites the pipeline.

Security Features

  • Token Redaction: The generated token is automatically added to Buildkite’s CLI redactor to prevent accidental logging
  • Automatic Token Revocation: Tokens are automatically revoked after command execution (unless skip-token-revoke is set to true)
  • Environment Cleanup: Tokens are removed from the environment after command execution
  • Temporary Key Storage: Private keys are stored in temporary files that are immediately cleaned up
  • Scoped Permissions: Use permission parameters to limit token access to only what’s needed
  • Repository Scoping: Use the repositories parameter to limit token access to specific repositories

How It Works

  1. Environment Variable Expansion: Resolves environment variable references (e.g., $GITHUB_APP_ID) to their actual values
  2. JWT Generation: Creates a JSON Web Token (JWT) using your GitHub App ID and private key
  3. Installation Discovery: Finds the GitHub App installation for the current repository
  4. Token Generation: Uses the JWT to request an installation access token from GitHub’s API
  5. Security Setup: Adds the token to Buildkite’s redactor and exports it as an environment variable
  6. Cleanup: Removes the token from the environment after command execution

Retries

The installation lookup and the token request each retry transient failures:

  • curl transport errors: exits 5, 6, 7, 16, 18, 28, 35, 52, 55, 56 and 92, which cover DNS, connection, HTTP/2, partial transfers, timeouts, the TLS handshake and dropped connections. Some of these, such as an unresolvable host (6), a refused connection (7) or a failed TLS handshake (35), can also mean a misconfigured github-api-url or proxy, which no retry fixes. They are retried anyway, because DNS failures and refused connections are common during GitHub incidents. Other exits, such as a malformed URL (3) or a certificate that fails verification (60), are configuration errors and fail at once.
  • HTTP 429 and 5xx
  • HTTP 403 when it is a rate limit: a Retry-After header, x-ratelimit-remaining: 0, or a body that names a secondary rate limit (or, in older GitHub and GHES wording, an abuse detection mechanism)

When a status arrives before the connection fails, the status decides: a 404 followed by a timeout is treated as a 404 and fails at once.

The wait before the next attempt is chosen in this order:

  1. the server’s Retry-After
  2. the time until x-ratelimit-reset, when a rate-limit response says the limit is exhausted (a 5xx carrying these headers gets ordinary backoff)
  3. 60s, for a rate limit that gives neither, as GitHub asks, doubled for each consecutive rate limit (60s, 120s, …)
  4. exponential backoff with jitter: 2-3s, 4-7s, 8-15s, then 16-30s for every later attempt

A wait is never shorter than that attempt’s backoff step (2s, 4s, 8s, then 16s), so a Retry-After: 0 or a reset already in the past still backs off. The first three get up to 25% extra jitter, and at least 1s, so parallel jobs told to wait the same time do not all retry at once. The budget below is checked against the wait before jitter, and jitter never takes the total past it, so a hint that fits the budget is always honoured.

max-retry-wait caps the total wait for one request. The default of 120s fits the whole backoff (at most 55s over five attempts), and also fits one secondary-rate-limit wait (at most 75s) after the backoff that comes before it, so with the defaults a request waits at most two minutes. A second consecutive secondary rate limit (120-150s) does not fit and fails the step. Each attempt beyond five can add up to 30s, so raise max-retry-wait along with max-attempts.

Every request, including the revocation in post-command, has a 10s connect timeout and a 30s overall timeout. The JWT is signed once and signed again only when less than 2 minutes of its 10-minute lifetime remain, so no attempt sends an expired one.

Anything else, such as 401, 404 or 422, fails at once. Each retried attempt logs its attempt number, the HTTP status or curl exit code (with any status received before the connection failed), and the response body. The final failure is logged once, with its body. Anything token-shaped in a logged body ("token": "...", gho_, ghp_, ghr_, ghs_, ghu_ and github_pat_ values) is masked, and the JWT and the minted token are never logged. After a transport error the body is not logged at all, because a token response cut off mid-transfer may still hold the token. A token from any 2xx response is accepted and added to the redactor before anything else is logged.

[!NOTE] Creating a token is not idempotent. When the token request fails after GitHub has minted a token, either with a 5xx or with a connection dropped mid-response (logged as, for example, curl exit 56 after HTTP 201), the retry mints a second one. The first token is never used or revoked, even when part of it arrived in the cut-off response. It keeps the scope the step requested and expires within an hour.

Environment Variable Expansion

The plugin supports referencing environment variables using the $VARIABLE_NAME syntax for the app-id and private-key parameters. This allows you to keep sensitive values out of your pipeline configuration:

# In your pipeline YAML (secure - no secrets in config)
app-id: "$$GITHUB_APP_ID"
private-key: "$$GITHUB_APP_PRIVATE_KEY"

The plugin will automatically expand $GITHUB_APP_ID to the value of the GITHUB_APP_ID environment variable. Use the double-dollar ($$) syntax in Buildkite to prevent Buildkite from interpolating the variables before the plugin receives them.

Requirements

  • openssl - Used for JWT signature generation
  • curl - Used for GitHub API requests
  • jq - Used for JSON parsing
  • buildkite-agent - Required for token redaction
  • Repository must have the GitHub App installed

Troubleshooting

Error: Unable to parse repository from BUILDKITE_REPO

This error occurs when the plugin cannot determine the GitHub repository from the BUILDKITE_REPO environment variable. Ensure your repository URL is in one of these formats:

  • git@github.com:owner/repo.git
  • https://github.com/owner/repo.git

Error: GitHub App is not installed

If you see the error “GitHub App is not installed on owner/repository”, the plugin has detected that your GitHub App is not installed on the target repository or organization. Follow the provided instructions:

  1. Go to your GitHub App settings
  2. Click ‘Install App’ in the left sidebar
  3. Select the organization/account
  4. Choose to install on selected repositories or all repositories

This error replaces the generic “Failed to get installation ID” message and provides clear next steps.

Error: Failed to generate installation token

The message names the HTTP status, or the curl exit code when the connection failed, with any status received before it (for example curl exit 18 after HTTP 503); the attempt log above it shows each retried failure. A transient failure that outlasted the retries reads still failing after N attempts. Otherwise this typically indicates:

  1. Invalid private key format
  2. Clock skew (JWT timestamps are invalid)
  3. Insufficient GitHub App permissions

Development

To contribute to this plugin:

  1. Clone the repository
  2. Make your changes
  3. Run the tests (below) and, for agent-side behaviour, a Buildkite pipeline
  4. Submit a pull request

Tests

The hooks are covered by bats tests that stub curl and buildkite-agent, so they never reach the GitHub API:

bats tests

tests/revocation.bats drives pre-command and post-command the way buildkite-agent does — the hook body is sourced, then the next hook runs as a child process — because only exported variables survive that boundary. Any hand-off between the two hooks has to be tested through it to be meaningful.

shellcheck hooks/* tests/test_helper.bash tests/stubs/* must also be clean. Both run in CI on every pull request.

The plugins listed on this webpage are provided for informational purposes only. They have not undergone any formal security review or assessment. While we strive to provide useful resources, we cannot guarantee the safety, reliability, or integrity of these plugins. Users are strongly advised to conduct their own security evaluations before downloading, installing, or using any plugin. By using these plugins, you acknowledge and accept any risks associated with their use. We disclaim any liability for any harm or damages arising from the use of the plugins listed.

You deserve better CI.

Get started for free with a full month of our all-access trial.

Buildkite Pipelines

Platform

  1. Pipelines
  2. Public pipelines
  3. Test Engine
  4. Package Registries
  5. Mobile Delivery Cloud
  6. Pricing

Hosting options

  1. Self-hosted agents
  2. Mac hosted agents
  3. Linux hosted agents

Resources

  1. Docs
  2. Blog
  3. Changelog
  4. Example pipelines
  5. Plugins
  6. Webinars
  7. Case studies
  8. Events
  9. Migration Services

Company

  1. About
  2. Careers
  3. Press
  4. Security
  5. Brand assets
  6. Contact

Solutions

  1. Replace Jenkins
  2. Workflows for MLOps
  3. Testing at scale
  4. Monorepo mojo
  5. Bazel orchestration

Compare

  1. Buildkite vs GitHub Actions
  2. Buildkite vs Jenkins
  3. Buildkite vs CircleCI
  4. Buildkite vs Harness
  5. Buildkite vs CloudBees
  6. Buildkite vs Blacksmith
  7. Buildkite vs Depot

Legal

  1. Terms of Service
  2. Acceptable Use Policy
  3. Privacy Policy
  4. Subprocessors
  5. Service Level Agreement
  6. Supplier Code of Conduct
  7. Modern Slavery Statement

Support

  1. System status
  2. Forum
© Buildkite Pty Ltd 2026