Skip to content

ci(port): track upstream HEAD, guard backwards ports, open PRs via GitHub App - #5

Merged
LukasParke merged 1 commit into
mainfrom
LukasParke/port-head-tracking
Aug 3, 2026
Merged

ci(port): track upstream HEAD, guard backwards ports, open PRs via GitHub App#5
LukasParke merged 1 commit into
mainfrom
LukasParke/port-head-tracking

Conversation

@LukasParke

Copy link
Copy Markdown
Contributor

Brings go-agent's port automation to parity with the changes landing in python-agent (PR #24 there). Both ports run the same pipeline design, so the port logic must not diverge — a fix found in one repo has to land in both, or the two language ports drift apart.

1. Track upstream HEAD, not the latest npm release

The workflow curled registry.npmjs.org and ported the resolved release tag, making it structurally blind to unreleased work. Upstream currently sits 4 commits past the 0.8.0 tag with the doom-loop change (~7,500 lines) unreleased — a release-tracking port reports "no changes", then absorbs the whole delta in one run.

Blank ref now means upstream default-branch HEAD. Only an explicit inputs.ref is honored; the publish dispatch's client_payload.ref is deliberately ignored with a ::notice:: — it carries the release tag, an ancestor of HEAD once the port is ahead, so honoring it would revert landed work.

2. Fixed the --ref "" bug

The Run port step passed --ref unconditionally. --ref "" is not equivalent to omitting it — the arg parser consumes the empty value. Now only passed when non-empty, with ${args[@]+"${args[@]}"} for safe empty expansion under set -u.

3. Refuse to port backwards

scripts/upstream exits 3 when the target is an ancestor of state.yaml's upstream_commit. Verified by pointing state at HEAD and targeting the 0.8.0 tag:

ERROR: refusing to port backwards.
  target:      680bceb… (@openrouter/agent@0.8.0)
  last ported: d030602…
  target is 4 commit(s) BEHIND what is already ported.

4. Release-tag position — translated, not copied

This is where Go needed different thinking. python-agent compares pyproject.toml against upstream's package.json; a Go module declares no version, so that drift check has no analogue here and is deliberately absent.

What does apply is position relative to the release tag: git tag v0.8.0 on a HEAD-tracking tree would publish post-0.8.0 behavior under a released number, and a pushed tag is immutable in the module proxy's cache — the Go equivalent of an irreversible PyPI publish. The verifier now reports how many commits ahead the tree is and warns not to tag from that state. Not a failure; being ahead is the intended state.

Verified in both positions against a real upstream clone:

State Result
level with 0.8.0 tag PASS: ported tree is level with the 0.8.0 release tag (taggable)
4 commits ahead NOTE: ... do NOT cut a v0.8.0 tag from this state — exit 0

5. verify-port is blocking, plus advisory test-parity reporting

continue-on-error removed. It was advisory while the port sat a version behind and the required-API check failed by design; that closed with the 0.8.0 port, so advisory only bought the ability to regress the parity floor with a green PR. Confirmed the verifier passes clean before making it blocking.

Test parity is reported by upstream test cluster, not filename — Go keeps no 1:1 file mapping with upstream's tests, so a filename heuristic would be noise. It currently flags one real gap: shared-*.test.ts has no counterpart anywhere in the Go suite.

6. Open port PRs with a GitHub App

Only pull_request-event runs satisfy required status checks, and GitHub doesn't trigger workflows from GITHUB_TOKEN-created events — so a bot PR opened with it can never merge. Measured on python-agent: 14 check-runs on the commit, 7 in the PR's rollup; the dispatched half was invisible to branch protection.

The mint step is gated on both the App ID (variable) and key (secret), via a job-level env boolean — because the secrets context is NOT available in a step-level if: and silently evaluates to empty there, which would make the gate never match. The old dispatch step is retained as a fallback that warns loudly when no App token was minted, rather than leaving a silently unmergeable PR.

PORT_BOT_APP_ID and PORT_BOT_PRIVATE_KEY are both configured on this repo, so the App path is live.

Verification

verify.sh PASS (0 failures) — gofmt, build, vet, test, test -race, 24 required symbols, coverage 72.2% over a 72.0% floor, every required-API symbol test-covered · both workflows parse · both shell scripts pass bash -n · backwards guard and release-tag position each verified against a real upstream clone in both states · state.yaml untouched.

Note on the next run

The next cron will see HEAD (4 commits ahead) and port the doom-loop delta — a large first delta, because it's the backlog release-tracking was hiding. Per the Test Parity rules already in this repo, it must port the upstream tests alongside the source, not just the source.

Follow-up: this repo's main is unprotected

main has no branch protection, so none of these gates block a merge yet. python-agent now requires six checks with enforce_admins. Worth applying the equivalent here — with the App token in place, automated port PRs will satisfy required checks correctly.

🤖 Generated with Claude Code

…tHub App

Brings go-agent's port automation to parity with python-agent. The two ports share
one pipeline design, so the port *logic* should not diverge between them — a fix
found in one repo has to land in both or the ports drift out of sync.

1. Track upstream HEAD, not the latest npm release

The workflow curled registry.npmjs.org and ported the resolved release tag, so it
was structurally blind to unreleased work. Upstream currently sits 4 commits past
the 0.8.0 tag with the doom-loop change (~7.5k lines) unreleased; a release-tracking
port sees "no changes" and then absorbs the whole delta in one run.

Blank ref now means upstream default-branch HEAD. Only an explicit inputs.ref is
honored; the publish dispatch's client_payload.ref is deliberately ignored with a
::notice:: explaining why — it carries the release tag, which is an ancestor of
HEAD once the port is ahead, so honoring it would revert landed work.

2. Fixed the --ref "" bug

The Run port step passed --ref unconditionally. `--ref ""` is not the same as
omitting it: the arg parser consumes the empty value. Now only passed when
non-empty, with ${args[@]+"${args[@]}"} for safe empty expansion under set -u.

3. Refuse to port backwards

scripts/upstream exits 3 when the target is an ancestor of state.yaml's
upstream_commit, naming how many commits behind and pointing at --force. Verified
by pointing state at HEAD and targeting the 0.8.0 tag: refused, 4 commits behind.

4. Release-tag position (the Go analogue of Python's version-drift check)

This needed translation rather than copying. A Go module declares no version, so
there is no downstream version field to drift against upstream's package.json —
that check has no analogue here. What does apply is position relative to the
release tag: `git tag v0.8.0` on a HEAD-tracking tree would publish post-0.8.0
behavior under a released number, and a pushed tag is immutable in the module
proxy's cache. So the verifier reports how many commits ahead the tree is and warns
not to tag from that state. Not a failure — being ahead is the intended state.

5. verify-port is now blocking, plus advisory test-parity reporting

continue-on-error is gone. It was advisory while the port sat a version behind and
the required-API check failed by design; that closed with the 0.8.0 port, so
advisory only bought the ability to regress the parity floor with a green PR.

Test parity is reported by upstream test *cluster* rather than by filename: Go does
not keep a 1:1 file mapping with upstream's tests, so a filename heuristic would be
noise. Currently flags one real gap — shared-*.test.ts has no counterpart anywhere
in the Go suite.

6. Open port PRs with a GitHub App

Only pull_request-event runs satisfy required status checks, and GitHub does not
trigger workflows from GITHUB_TOKEN-created events, so a bot PR opened with it can
never merge. Measured on python-agent: 14 check-runs on the commit, 7 in the PR's
rollup — the dispatched half invisible to branch protection. An App installation
token is not recursion-guarded.

The mint step is gated on BOTH the App ID (variable) and key (secret), via a
job-level env boolean: the `secrets` context is NOT available in a step-level `if:`
and silently evaluates to empty there, which would make the gate never match. The
old dispatch step is retained as a fallback that warns loudly when no App token was
minted, rather than leaving a silently unmergeable PR.

Verification: verify.sh PASS (0 failures) — gofmt, build, vet, test, test -race,
24 required symbols, coverage 72.2% over a 72.0% floor, every required-API symbol
test-covered · both workflows parse · both shell scripts pass bash -n · backwards
guard and release-tag position each verified against a real upstream clone in both
states · state.yaml untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
@LukasParke
LukasParke merged commit 93a9343 into main Aug 3, 2026
4 checks passed
@LukasParke
LukasParke deleted the LukasParke/port-head-tracking branch August 3, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant