Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions .github/workflows/bump-openrouter-sdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,23 @@ jobs:
noop: ${{ steps.bump.outputs.noop }}
pr_number: ${{ steps.open.outputs.pr_number }}
steps:
# GH_TOKEN is a PAT (not the Actions GITHUB_TOKEN) so the opened PR
# triggers Perry + CI — GITHUB_TOKEN would suppress those downstream runs.
# Short-lived App token (same App as publish.yaml / release-train.yaml),
# minted before checkout because checkout persists it for the branch
# push below. An App token (not the Actions GITHUB_TOKEN) so the opened
# PR triggers Perry + CI — GITHUB_TOKEN would suppress those downstream
# runs. Replaces the GH_TOKEN PAT the enterprise lifetime policy killed.
- name: Mint release-bot token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: OpenRouterTeam
repositories: typescript-agent

- uses: actions/checkout@v6
with:
token: ${{ secrets.GH_TOKEN }}
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
Comment thread
LukasParke marked this conversation as resolved.

- uses: pnpm/action-setup@v6
Expand All @@ -66,7 +78,7 @@ jobs:
id: bump
env:
TARGET_VERSION: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
chmod +x .github/scripts/bump-sdk.sh
./.github/scripts/bump-sdk.sh
Expand All @@ -75,7 +87,7 @@ jobs:
id: open
if: steps.bump.outputs.noop != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
PR_URL=$(gh pr create \
Expand Down Expand Up @@ -105,9 +117,21 @@ jobs:
steps:
- uses: actions/checkout@v6

# Minted per-job: installation tokens cannot be passed between jobs
# (GitHub drops job outputs that contain secrets), and the bump job's
# token may expire during a long gate anyway (1h lifetime, 30m poll).
- name: Mint release-bot token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: OpenRouterTeam
repositories: typescript-agent

- name: Wait for Perry + CI, then merge or alert
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR: ${{ needs.bump.outputs.pr_number }}
REPO: OpenRouterTeam/typescript-agent
AUTO_MERGE: ${{ github.event.inputs.dry_run != 'true' }}
Expand Down
68 changes: 45 additions & 23 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ jobs:
inputs.mode == 'publish' && inputs.dry-run)
steps:
# No credentials persisted: install/build/test below execute dependency
# lifecycle scripts and test code, which must not be able to read a
# long-lived cross-repo PAT out of .git/config. Git push credentials are
# injected by the "Configure git push credentials" step *after* those,
# lifecycle scripts and test code, which must not be able to read the
# cross-repo App token out of .git/config. Push credentials are injected
# by the "Configure git push credentials" step *after* those,
# immediately before the only steps that push.
- uses: actions/checkout@v6
with:
Expand Down Expand Up @@ -113,27 +113,47 @@ jobs:

- run: pnpm run test

# Short-lived GitHub App installation token replacing the old GH_TOKEN
# PAT (which the OpenRouter enterprise now rejects for lifetime > 366
# days — the failure that silently stalled releases in Aug 2026). Same
# pattern as the port repos' openrouter-port-bot. Expires after 1 hour
# and is auto-revoked at job end, so the exfiltration surface the PAT
# reviews worried about shrinks to a job-scoped credential.
#
# The App must be installed on ALL FOUR repos listed: this one (push +
# Version PR) and the HOP B/C dispatch targets. Minting fails loudly if
# it isn't — no silent fallback to a broken release.
- name: Mint release-bot token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: OpenRouterTeam
repositories: typescript-agent,openrouter-web,python-agent,go-agent
Comment thread
LukasParke marked this conversation as resolved.
Outdated

# Deliberately after install/build/test (see the checkout comment).
# changesets/action pushes changeset-release/main (and release tags on
# the publish leg) with plain `git push`, which uses the remote's
# embedded credentials. It must be the PAT, not the Actions
# embedded credentials. It must be the App token, not the Actions
# GITHUB_TOKEN: GITHUB_TOKEN-attributed pushes never trigger workflows,
# so Version PR updates would get no CI / perry/review and the release
# train (release-train.yaml) could merge on checks from the first
# revision.
- name: Configure git push credentials (PAT)
# revision. App-token pushes trigger workflows normally.
- name: Configure git push credentials (App token)
run: |
git remote set-url origin \
"https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git"
"https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository }}.git"

- name: Version PR or Publish (changesets)
id: changesets
if: >
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.mode == 'version')
# SHA-pinned (= v1.9.0): this step receives the cross-repo PAT and
# runs with it in .git/config, so a floating tag would let a
# compromised action release exfiltrate it. Bump deliberately.
# SHA-pinned (= v1.9.0): this step receives the cross-repo App token
# and runs with it in .git/config, so a floating tag would let a
# compromised action release exfiltrate it (bounded to the token's
# 1-hour lifetime, but still). Bump deliberately.
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d
with:
title: 'chore: version packages'
Expand All @@ -148,12 +168,12 @@ jobs:
version: pnpm run version
publish: pnpm exec changeset publish --no-git-checks
env:
# PAT, not the Actions GITHUB_TOKEN: PRs created with GITHUB_TOKEN
# never trigger other workflows, so the Version Packages PR would get
# no CI and no perry/review — and the release-train workflow
# (release-train.yaml) gates its auto-merge on exactly those checks.
# Same reasoning as the checkout token in bump-openrouter-sdk.yaml.
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
# App token, not the Actions GITHUB_TOKEN: PRs created with
# GITHUB_TOKEN never trigger other workflows, so the Version Packages
# PR would get no CI and no perry/review — and the release-train
# workflow (release-train.yaml) gates its auto-merge on exactly those
# checks. Same reasoning as in bump-openrouter-sdk.yaml.
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
Comment thread
LukasParke marked this conversation as resolved.
# No NODE_AUTH_TOKEN / NPM_TOKEN: auth comes from OIDC trusted
# publishing. changesets/action logs "No NPM_TOKEN found, but OIDC is
# available" and leaves .npmrc alone; npm then exchanges the Actions
Expand Down Expand Up @@ -187,9 +207,11 @@ jobs:
run: git push origin --tags

# Nothing below pushes via git (the HOP dispatches use `gh api` with
# GH_TOKEN from env), so drop the PAT from .git/config the moment the
# the App token from env), so drop it from .git/config the moment the
# last push consumer is done. `always()`: scrub even when a publish
# step failed, since later/rerun steps still see the workspace.
# step failed, since later/rerun steps still see the workspace. The
# token also expires on its own within the hour; this just narrows the
# in-job window.
- name: Scrub git push credentials
if: always()
run: |
Expand Down Expand Up @@ -244,9 +266,9 @@ jobs:
- name: Dispatch monorepo bump
if: ${{ !inputs.dry-run && steps.published.outputs.version != '' }}
env:
# Cross-repo PAT; needs contents:write on
# OpenRouterTeam/openrouter-web so the repository_dispatch is accepted.
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# Cross-repo App token; the App's contents:write on
# OpenRouterTeam/openrouter-web makes the repository_dispatch accepted.
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -euo pipefail
gh api repos/OpenRouterTeam/openrouter-web/dispatches \
Expand Down Expand Up @@ -282,9 +304,9 @@ jobs:
if: ${{ !cancelled() && !inputs.dry-run && steps.published.outputs.version != '' }}
continue-on-error: true
env:
# Same cross-repo PAT as HOP B; additionally needs contents:write on
# Same cross-repo App token as HOP B; the App must also cover
# OpenRouterTeam/python-agent and OpenRouterTeam/go-agent.
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.published.outputs.version }}
SOURCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
Expand Down
20 changes: 17 additions & 3 deletions .github/workflows/release-train.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,24 @@ jobs:
steps:
- uses: actions/checkout@v6

# Short-lived App installation token (same App as publish.yaml /
# bump-openrouter-sdk.yaml). Replaces the old GH_TOKEN PAT, which the
# enterprise's 366-day-lifetime policy started rejecting — that outage
# surfaced as every train run failing at the first gh call. Only this
# repo is needed here: the train reads, gates, and merges locally.
- name: Mint release-bot token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
owner: OpenRouterTeam
repositories: typescript-agent
Comment thread
LukasParke marked this conversation as resolved.
Comment thread
LukasParke marked this conversation as resolved.

- name: Find Version Packages PR
id: find
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
SLACK_BOT_TOKEN: ${{ secrets.CI_RELEASE_ALERT_SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.CI_RELEASE_ALERT_SLACK_CHANNEL_ID }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Expand Down Expand Up @@ -175,7 +189,7 @@ jobs:
id: scope
if: steps.find.outputs.pr_number != '' && steps.find.outputs.held != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR: ${{ steps.find.outputs.pr_number }}
SLACK_BOT_TOKEN: ${{ secrets.CI_RELEASE_ALERT_SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.CI_RELEASE_ALERT_SLACK_CHANNEL_ID }}
Expand All @@ -202,7 +216,7 @@ jobs:
- name: Gate and merge Version Packages PR
if: steps.find.outputs.pr_number != '' && steps.find.outputs.held != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Comment thread
LukasParke marked this conversation as resolved.
PR: ${{ steps.find.outputs.pr_number }}
# REPO comes from the workflow-level env block.
# On schedule runs `inputs` is empty, so dry_run != true → AUTO_MERGE=true.
Expand Down