Created
February 8, 2026 07:30
-
-
Save eonist/cdeb7034a8f6c4c2c237b3b8c3dbd53d to your computer and use it in GitHub Desktop.
oauth in GitHub action for cli
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Running an OAuth-based CLI app in GitHub Actions is tricky because OAuth flows (device flow or browser-based) require interactive user input, which isn't available in a headless CI environment. Here are several strategies to work around this. | |
| ## Bypass OAuth with a Token Environment Variable | |
| Most well-designed CLI tools that use OAuth also support reading a pre-existing token from an environment variable or config file. For example, GitHub's own `gh` CLI checks for `GH_TOKEN` or `GITHUB_TOKEN` before triggering the OAuth flow. If your CLI app supports this pattern, the simplest approach is: [cli.github](https://cli.github.com/manual/gh_auth_login) | |
| ```yaml | |
| jobs: | |
| my-job: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - run: my-cli-app do-something | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| ``` | |
| The `GITHUB_TOKEN` is automatically generated by GitHub Actions for each workflow run and is scoped to the repository. If your CLI needs broader permissions (e.g., access to other repos or orgs), use a **Personal Access Token (PAT)** stored as a repository secret instead. [docs.github](https://docs.github.com/actions/reference/authentication-in-a-workflow) | |
| ## If You Control the CLI App | |
| If you're the author of the CLI, the best practice is to add a code path that **skips the interactive OAuth flow** when a token is already available. A common pattern: | |
| 1. Check for an environment variable (e.g., `GITHUB_TOKEN` or `MY_CLI_TOKEN`) at startup. | |
| 2. If the variable is set, use it directly as the Bearer token for API requests, bypassing the device/browser flow entirely. | |
| 3. If not set, fall back to the interactive OAuth flow for local development. | |
| ```python | |
| import os | |
| token = os.environ.get("GITHUB_TOKEN") | |
| if not token: | |
| token = run_oauth_device_flow() # interactive | |
| # Use token for API calls | |
| headers = {"Authorization": f"Bearer {token}"} | |
| ``` | |
| This is the pattern GitHub's own CLI uses — it checks environment variables first and only prompts for login if none are found. [github](https://github.com/cli/cli/issues/3799) | |
| ## Use a GitHub App for Fine-Grained Access | |
| If your use case requires acting as a specific identity with specific permissions (rather than the workflow's default token), you can create a **GitHub App** and generate installation tokens in the workflow: [dev](https://dev.to/dtinth/authenticating-as-a-github-app-in-a-github-actions-workflow-27co) | |
| 1. Register a GitHub App with the needed permissions. | |
| 2. Store the App ID and private key as repository secrets. | |
| 3. Use an action like `tibdex/github-app-token` or `actions/create-github-app-token` to generate a short-lived installation token at runtime. | |
| ```yaml | |
| steps: | |
| - uses: actions/create-github-app-token@v1 | |
| id: app-token | |
| with: | |
| app-id: ${{ secrets.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - run: my-cli-app do-something | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| ``` | |
| ## Pre-Authenticate and Cache the Token | |
| If you absolutely cannot modify the CLI and it doesn't support environment-variable-based auth, you can: | |
| - **Pre-generate an OAuth token** locally using the device flow and store the resulting access token as a GitHub Actions secret. [stackoverflow](https://stackoverflow.com/questions/68005219/web-browser-authentication-in-github-actions-for-oauth-token) | |
| - In the workflow, write that token to wherever the CLI expects to find its cached credentials (e.g., a config file like `~/.config/my-cli/token`). | |
| ```yaml | |
| steps: | |
| - run: | | |
| mkdir -p ~/.config/my-cli | |
| echo "${{ secrets.MY_CLI_OAUTH_TOKEN }}" > ~/.config/my-cli/token | |
| - run: my-cli-app do-something | |
| ``` | |
| The downside is that OAuth tokens can expire (GitHub user access tokens expire after 8 hours by default if using device flow with expiration enabled), so you'd need to handle refresh tokens or regenerate periodically. [stackoverflow](https://stackoverflow.com/questions/77385245/how-to-use-refresh-tokens-in-device-flow) | |
| ## Recommended Approach | |
| | Scenario | Best Option | | |
| |---|---| | |
| | You control the CLI code | Add env var support to skip OAuth [cli.github](https://cli.github.com/manual/gh_auth_login) | | |
| | CLI already reads `GITHUB_TOKEN` | Pass `${{ secrets.GITHUB_TOKEN }}` directly [docs.github](https://docs.github.com/actions/reference/authentication-in-a-workflow) | | |
| | Need cross-repo or org access | Use a PAT stored as a secret [stackoverflow](https://stackoverflow.com/questions/68005219/web-browser-authentication-in-github-actions-for-oauth-token) | | |
| | Need fine-grained, rotating credentials | Use a GitHub App installation token [dev](https://dev.to/dtinth/authenticating-as-a-github-app-in-a-github-actions-workflow-27co) | | |
| | Can't modify CLI at all | Pre-cache an OAuth token as a secret | | |
| The most robust and maintainable solution is to have the CLI support token-based auth via environment variables, which is the standard pattern for headless/CI environments. [polpiella](https://www.polpiella.dev/how-to-use-the-github-cli-from-github-actions-workflows) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment