Skip to content

Commit 661ea02

Browse files
vorporealoz-agent
andcommitted
Generate GitHub App tokens internally in reusable workflows
Replace auth_token secret with app_id + app_private_key. Each reusable workflow now generates its own short-lived installation token using actions/create-github-app-token, eliminating the need for consumers to manage token lifecycle. Consumers store REPO_SYNC_APP_ID and REPO_SYNC_APP_PRIVATE_KEY as repo secrets (long-lived), and the workflows handle the rest. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 41f242e commit 661ea02

5 files changed

Lines changed: 64 additions & 42 deletions

File tree

.github/workflows/escalation.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ on:
3232
required: true
3333
type: string
3434
secrets:
35-
auth_token:
36-
description: "GitHub App installation token with cross-repo push and PR permissions."
35+
app_id:
36+
description: "GitHub App ID."
37+
required: true
38+
app_private_key:
39+
description: "GitHub App private key."
3740
required: true
3841

3942
jobs:
@@ -44,14 +47,21 @@ jobs:
4447
pull-requests: write
4548
actions: write
4649
env:
47-
GH_TOKEN: ${{ secrets.auth_token }}
4850
ESCALATE_TO: ${{ inputs.escalate_to }}
4951
ESCALATE_AFTER: ${{ inputs.escalate_after }}
5052
steps:
53+
- name: Generate installation token
54+
id: token
55+
uses: actions/create-github-app-token@v1
56+
with:
57+
app-id: ${{ secrets.app_id }}
58+
private-key: ${{ secrets.app_private_key }}
59+
owner: ${{ github.repository_owner }}
60+
- run: echo "GH_TOKEN=${{ steps.token.outputs.token }}" >> "$GITHUB_ENV"
5161
- uses: actions/checkout@v4
52-
with: { token: "${{ secrets.auth_token }}" }
62+
with: { token: "${{ steps.token.outputs.token }}" }
5363
- uses: actions/checkout@v4
54-
with: { repository: warpdotdev/repo-sync, ref: v1, path: .repo-sync, token: "${{ secrets.auth_token }}" }
64+
with: { repository: warpdotdev/repo-sync, ref: v1, path: .repo-sync, token: "${{ steps.token.outputs.token }}" }
5565
- uses: actions/setup-python@v5
5666
with: { python-version: "3.12" }
5767
- run: pip install -e .repo-sync

.github/workflows/restack.yml

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@ on:
2525
type: string
2626
default: "@oncall-client-primary"
2727
secrets:
28-
auth_token:
29-
description: "GitHub App installation token with cross-repo push and PR permissions."
28+
app_id:
29+
description: "GitHub App ID."
3030
required: true
31-
# workflow_dispatch is used for stuck-stack recovery. GitHub Actions does
32-
# not support secrets under workflow_dispatch, so the dispatched run must
33-
# generate its own token (e.g. via actions/create-github-app-token). The
34-
# consuming repo should store REPO_SYNC_APP_ID and
35-
# REPO_SYNC_APP_PRIVATE_KEY as repo-level secrets.
31+
app_private_key:
32+
description: "GitHub App private key."
33+
required: true
34+
# workflow_dispatch is used for stuck-stack recovery.
3635
workflow_dispatch:
3736
inputs:
3837
pr_number:
@@ -75,38 +74,32 @@ jobs:
7574
PRIVATE_REPO: ${{ inputs.private_repo }}
7675
ESCALATE_TO: ${{ inputs.escalate_to }}
7776
steps:
78-
# For workflow_dispatch, generate a token since secrets aren't available.
79-
# For workflow_call, use the passed auth_token.
80-
- name: Generate token (workflow_dispatch)
81-
if: github.event_name == 'workflow_dispatch'
82-
id: gen_token
77+
# Generate a token for both workflow_call and workflow_dispatch.
78+
# For workflow_call, secrets are passed from the caller.
79+
# For workflow_dispatch, secrets come from the repo's own secrets.
80+
- name: Generate installation token
81+
id: token
8382
uses: actions/create-github-app-token@v1
8483
with:
85-
app-id: ${{ secrets.REPO_SYNC_APP_ID }}
86-
private-key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
84+
app-id: ${{ secrets.app_id || secrets.REPO_SYNC_APP_ID }}
85+
private-key: ${{ secrets.app_private_key || secrets.REPO_SYNC_APP_PRIVATE_KEY }}
8786
owner: ${{ github.repository_owner }}
88-
8987
- name: Set GH_TOKEN
90-
run: |
91-
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
92-
echo "GH_TOKEN=${{ steps.gen_token.outputs.token }}" >> "$GITHUB_ENV"
93-
else
94-
echo "GH_TOKEN=${{ secrets.auth_token }}" >> "$GITHUB_ENV"
95-
fi
88+
run: echo "GH_TOKEN=${{ steps.token.outputs.token }}" >> "$GITHUB_ENV"
9689

9790
- name: Checkout repo
9891
uses: actions/checkout@v4
9992
with:
10093
fetch-depth: 0
101-
token: ${{ env.GH_TOKEN }}
94+
token: ${{ steps.token.outputs.token }}
10295

10396
- name: Checkout repo-sync tooling
10497
uses: actions/checkout@v4
10598
with:
10699
repository: warpdotdev/repo-sync
107100
ref: v1
108101
path: .repo-sync
109-
token: ${{ env.GH_TOKEN }}
102+
token: ${{ steps.token.outputs.token }}
110103

111104
- name: Set up Python
112105
uses: actions/setup-python@v5

.github/workflows/sync.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ on:
2626
type: string
2727
default: ""
2828
secrets:
29-
auth_token:
29+
app_id:
30+
description: "GitHub App ID."
31+
required: true
32+
app_private_key:
33+
description: "GitHub App private key."
3034
required: true
3135

3236
concurrency:
@@ -37,17 +41,25 @@ jobs:
3741
sync:
3842
runs-on: ubuntu-latest
3943
env:
40-
GH_TOKEN: ${{ secrets.auth_token }}
4144
PUBLIC_REPO: ${{ inputs.public_repo }}
4245
PRIVATE_REPO: ${{ inputs.private_repo }}
4346
steps:
47+
- name: Generate installation token
48+
id: token
49+
uses: actions/create-github-app-token@v1
50+
with:
51+
app-id: ${{ secrets.app_id }}
52+
private-key: ${{ secrets.app_private_key }}
53+
owner: ${{ github.repository_owner }}
4454
- uses: actions/checkout@v4
45-
with: { fetch-depth: 0, token: "${{ secrets.auth_token }}" }
55+
with: { fetch-depth: 0, token: "${{ steps.token.outputs.token }}" }
4656
- uses: actions/checkout@v4
47-
with: { repository: warpdotdev/repo-sync, ref: v1, path: .repo-sync, token: "${{ secrets.auth_token }}" }
57+
with: { repository: warpdotdev/repo-sync, ref: v1, path: .repo-sync, token: "${{ steps.token.outputs.token }}" }
4858
- uses: actions/setup-python@v5
4959
with: { python-version: "3.12" }
5060
- run: pip install -e .repo-sync
61+
- name: Set GH_TOKEN
62+
run: echo "GH_TOKEN=${{ steps.token.outputs.token }}" >> "$GITHUB_ENV"
5163

5264
# Derive peer_repo and source_is_private from the two repo names.
5365
- name: Derive sync context
@@ -98,7 +110,7 @@ jobs:
98110
ref: ${{ github.event.repository.default_branch }}
99111
path: peer
100112
fetch-depth: 0
101-
token: ${{ secrets.auth_token }}
113+
token: ${{ steps.token.outputs.token }}
102114

103115
- name: Find existing stack top
104116
if: steps.unsynced.outputs.count != '0'

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ the marker lines and everything between them are stripped. markers must be prop
4444

4545
before integrating, ensure the consuming repos have:
4646

47-
1. **a GitHub App** installed on both repos with `contents:write`, `pull_requests:write`, and `metadata:read` permissions. store the App ID and private key as repo secrets (`REPO_SYNC_APP_ID`, `REPO_SYNC_APP_PRIVATE_KEY`).
47+
1. **a GitHub App** installed on both repos (and on the `repo-sync` repo itself) with `contents:write`, `pull_requests:write`, `workflows:write`, and `metadata:read` permissions. store the App ID and private key as repo secrets (`REPO_SYNC_APP_ID`, `REPO_SYNC_APP_PRIVATE_KEY`) in both repos. the reusable workflows generate short-lived installation tokens internally.
4848
2. **auto-merge enabled** as a repo-level setting.
4949
3. **squash merge** as the merge strategy for PRs, configured to **preserve the PR description** in the commit message.
5050
4. **branch protection rules** on `repo-sync/*` branches, so only the sync workflow's token can create or push to them.
@@ -114,7 +114,8 @@ jobs:
114114
public_repo: warpdotdev/warp-public
115115
private_repo: warpdotdev/warp-internal
116116
secrets:
117-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
117+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
118+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
118119
119120
restack:
120121
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'repo-sync/')
@@ -123,7 +124,8 @@ jobs:
123124
public_repo: warpdotdev/warp-public
124125
private_repo: warpdotdev/warp-internal
125126
secrets:
126-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
127+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
128+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
127129
128130
escalation:
129131
if: github.event_name == 'schedule'
@@ -134,7 +136,8 @@ jobs:
134136
public_repo: warpdotdev/warp-public
135137
private_repo: warpdotdev/warp-internal
136138
secrets:
137-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
139+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
140+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
138141
```
139142

140143
the same workflow file works in both repos -- the workflows derive which repo is which by comparing `github.repository` against the `private_repo` input.

examples/consuming-repo-sync.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
# Prerequisites for the consuming repo:
77
# - A GitHub App installed on both repos with contents:write, pull_requests:write,
88
# and metadata:read permissions.
9-
# - A GitHub App installation token stored as a repository secret (REPO_SYNC_TOKEN).
10-
# Generate it using the App's ID and private key, or use a fine-grained PAT.
9+
# - The App's ID and private key stored as repository secrets (REPO_SYNC_APP_ID,
10+
# REPO_SYNC_APP_PRIVATE_KEY). The reusable workflows generate short-lived
11+
# installation tokens internally.
1112
# - Auto-merge enabled as a repo-level setting.
1213
# - Squash merge configured to preserve the PR description in the commit message.
1314
# - Branch protection rules on repo-sync/* branches (only the sync workflow's
@@ -45,7 +46,8 @@ jobs:
4546
escalate_to: "@oncall-client-primary"
4647
# slack_webhook_url: ${{ secrets.REPO_SYNC_SLACK_WEBHOOK }}
4748
secrets:
48-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
49+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
50+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
4951

5052
# -----------------------------------------------------------------------
5153
# Restack: triggered when a sync PR is merged.
@@ -61,7 +63,8 @@ jobs:
6163
private_repo: warpdotdev/warp-internal
6264
escalate_to: "@oncall-client-primary"
6365
secrets:
64-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
66+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
67+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
6568

6669
# -----------------------------------------------------------------------
6770
# Escalation cron: runs on schedule.
@@ -75,4 +78,5 @@ jobs:
7578
public_repo: warpdotdev/warp-public
7679
private_repo: warpdotdev/warp-internal
7780
secrets:
78-
auth_token: ${{ secrets.REPO_SYNC_TOKEN }}
81+
app_id: ${{ secrets.REPO_SYNC_APP_ID }}
82+
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}

0 commit comments

Comments
 (0)