Skip to content

Upstreamer Port

Upstreamer Port #2

name: Upstreamer Port
# Ports @openrouter/agent into this repo.
#
# The port tracks upstream's **default-branch HEAD**, not the latest published npm
# release. Release tracking sounds safer but produces exactly the failure this
# pipeline exists to prevent: upstream can sit for weeks with large unreleased
# work on main (doom-loop detection, #73, was ~7.5k lines) and the port stays
# blind to it, then absorbs the whole delta in one automated run touching the most
# load-bearing modules. Tracking HEAD keeps each delta small enough to review.
#
# Consequence to keep in mind: the port is then routinely AHEAD of the latest
# release, so its declared version legitimately lags upstream's package.json. The
# verifier reports that rather than failing, and publishing is gated on it — see
# the Package Version section of .upstreamer/upstreamer.md.
#
# Three triggers, all resolving to HEAD unless given an explicit ref:
# 1. Weekly cron — the primary path now that releases are not the trigger.
# 2. repository_dispatch from typescript-agent's publish.yaml on a new npm
# release. Still useful as a "something just shipped, sync promptly" nudge,
# but it no longer pins the ref to that release tag: doing so would port
# BACKWARDS once the port is ahead of the release. scripts/upstream refuses
# an ancestor ref outright.
# 3. Manual dispatch, optionally with an explicit ref.
#
# Opens a PR. Never pushes to main. A failed parity eval leaves
# .upstreamer/state.yaml unchanged, so the next run retries the same delta.
on:
repository_dispatch:
types: [openrouter-agent-published]
schedule:
- cron: "23 6 * * 1"
workflow_dispatch:
inputs:
ref:
description: "Upstream ref to port (blank = upstream default branch HEAD)"
required: false
type: string
force:
description: "Re-run even if the upstream commit is unchanged"
required: false
default: false
type: boolean
permissions:
contents: write
pull-requests: write
actions: write # to dispatch ci.yaml onto the generated PR branch
concurrency:
group: upstreamer-port
cancel-in-progress: false
jobs:
port:
runs-on: ubuntu-latest
timeout-minutes: 150
env:
# Surfaced as env because the `secrets` context is NOT available in a
# step-level `if:` (only github/needs/strategy/matrix/job/runner/env/vars/
# steps/inputs are). Referencing secrets.* there evaluates to empty and the
# condition silently never matches — so the App-token gate below tests this
# variable instead. Only ever compared against '' ; never echoed.
HAS_APP_KEY: ${{ secrets.PORT_BOT_PRIVATE_KEY != '' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- name: Install opencode
run: bun install -g opencode-ai
- name: Set up language toolchain
uses: ./.github/actions/port-toolchain
# Blank ref = upstream default-branch HEAD, which scripts/upstream resolves
# itself. That is the normal case for both the cron and a publish dispatch.
#
# Only an EXPLICIT manual `ref` input is honored. The publish dispatch's
# client_payload.ref is deliberately ignored: it carries the release tag,
# which is an ancestor of HEAD once the port is ahead of the release, so
# honoring it would revert landed work. The dispatch still does its real
# job — waking the pipeline promptly after a release — it just syncs to HEAD
# like every other trigger. (scripts/upstream also refuses an ancestor ref
# outright, so this is defense in depth, not the only guard.)
- name: Resolve target ref
id: target
run: |
set -euo pipefail
REF="${{ inputs.ref }}"
if [ -n "$REF" ]; then
echo "Explicit ref requested: $REF"
else
PAYLOAD_REF="${{ github.event.client_payload.ref }}"
if [ -n "$PAYLOAD_REF" ]; then
echo "::notice::Ignoring dispatch payload ref '$PAYLOAD_REF' — this port tracks upstream HEAD, and a release tag is an ancestor once the port is ahead of it. Syncing to HEAD instead."
fi
echo "No explicit ref — porting upstream default-branch HEAD."
fi
echo "ref=$REF" >> "$GITHUB_OUTPUT"
- name: Run port
env:
# Provide these in repo settings:
# Secret OPENROUTER_API_KEY — sk-or-... key opencode uses for inference
# Variable OPENCODE_MODEL — e.g. openrouter/~anthropic/claude-opus-latest
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENCODE_MODEL: ${{ vars.OPENCODE_MODEL }}
UPSTREAMER_TIMEOUT_SECONDS: 7200
run: |
set -euo pipefail
if [ -z "${OPENROUTER_API_KEY:-}" ]; then
echo "::error::OPENROUTER_API_KEY secret is not set. See .upstreamer/port.env.example."
exit 1
fi
# Only pass --ref when there is actually a ref. `--ref ""` is not the
# same as omitting it: the arg parser consumes the empty value and the
# script would target an empty ref instead of defaulting to HEAD.
args=()
REF="${{ steps.target.outputs.ref }}"
[ -n "$REF" ] && args+=(--ref "$REF")
[ "${{ inputs.force }}" = "true" ] && args+=(--force)
./scripts/upstream ${args[@]+"${args[@]}"}
- name: Check for changes
id: diff
run: |
if [ -n "$(git status --porcelain -- . ':!tmp')" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No changes — upstream unchanged or port was a no-op."
fi
# State only advances when the verifier AND the parity eval passed, so an
# unchanged state file next to a changed tree means the port did not pass.
# Label the PR accordingly instead of letting it look green.
- name: Detect eval failure
if: steps.diff.outputs.changed == 'true'
id: gate
run: |
if git diff --quiet -- .upstreamer/state.yaml; then
echo "passed=false" >> "$GITHUB_OUTPUT"
echo "::warning::state.yaml did not advance — parity eval did not pass. See .upstreamer/eval-report.md."
else
echo "passed=true" >> "$GITHUB_OUTPUT"
fi
# Mint a GitHub App installation token so the PR is created by the App
# rather than by the native GITHUB_TOKEN.
#
# Why this exists: GitHub does not trigger workflows from events created
# with GITHUB_TOKEN (recursion guard), so a PR opened with it gets no
# pull_request-event checks — and `main`'s required status checks are
# satisfied ONLY by pull_request-event runs. Measured on PR #24: the commit
# had 14 check-runs, the PR's rollup showed 7; the workflow_dispatch half was
# invisible to branch protection. So the previous "dispatch ci.yaml
# explicitly" workaround produced green runs that could never satisfy the
# required checks, leaving an automated port PR permanently unmergeable.
#
# An App installation token is not recursion-guarded, so the PR gets real
# pull_request checks. Preferred over a PAT: scoped to this repo, not tied to
# a person's account, and independently revocable.
#
# Optional by design — see the fallback below.
- name: Mint App token
id: app-token
# Gate on BOTH halves. Gating on the App ID alone is a trap: the ID is a
# variable and the key is a secret, so they are added in separate places
# and one routinely lands first (it did here — the ID arrived first). With
# only the ID set, this step would run and fail on the missing key, turning
# a working fallback into a broken pipeline — worse than no App at all.
if: >-
steps.diff.outputs.changed == 'true'
&& vars.PORT_BOT_APP_ID != ''
&& env.HAS_APP_KEY == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.PORT_BOT_APP_ID }}
private-key: ${{ secrets.PORT_BOT_PRIVATE_KEY }}
- name: Open PR
id: open-pr
if: steps.diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
# App token when configured; GITHUB_TOKEN otherwise. With the fallback
# the PR still opens, but its checks will not attach — the guard step
# below says so loudly rather than leaving a silently stuck PR.
token: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }}
branch: upstreamer/sync
delete-branch: true
title: >-
${{ steps.gate.outputs.passed == 'true'
&& 'port: sync with @openrouter/agent upstream'
|| 'port: sync with @openrouter/agent upstream (EVAL FAILED — do not merge)' }}
commit-message: "port: sync with @openrouter/agent upstream"
labels: >-
${{ steps.gate.outputs.passed == 'true'
&& 'upstreamer, automated'
|| 'upstreamer, automated, eval-failed' }}
body: |
Automated Upstreamer port of `@openrouter/agent` into this repo.
- Contract: `.upstreamer/upstreamer.md`
- Run log: `.upstreamer/logs/`
- Parity eval: `.upstreamer/eval-report.md`
- Parity eval passed: **${{ steps.gate.outputs.passed }}**
Review the diff as a port, not as a normal PR: check behavioral parity
against the TypeScript reference, not just that it compiles. If
`.upstreamer/state.yaml` did not advance, the eval did not pass and this
PR must not be merged as-is.
# Fallback path only. With the App configured, the PR above already has real
# pull_request checks and nothing here runs.
#
# Without it, the PR exists but can never satisfy `main`'s required checks.
# Dispatching ci.yaml still gives a human something to read, but the run does
# NOT attach to the PR — so say that plainly instead of leaving a green-looking
# PR that will not merge and no explanation of why.
- name: Trigger CI on the port PR (no App token — checks will not attach)
if: >-
steps.diff.outputs.changed == 'true'
&& steps.open-pr.outputs.pull-request-operation != 'none'
&& steps.app-token.outputs.token == ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::warning::PORT_BOT_APP_ID / PORT_BOT_PRIVATE_KEY are not configured, so this PR was opened with GITHUB_TOKEN and will receive NO pull_request-event checks. main's required status checks cannot be satisfied, so the PR cannot merge as-is. The dispatched run below is informational only. Configure the App (see PORTING.md) or close and reopen the PR by hand to generate real checks."
gh workflow run ci.yaml --repo "$GITHUB_REPOSITORY" --ref upstreamer/sync
- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: upstreamer-logs
path: .upstreamer/logs/
if-no-files-found: ignore