Skip to content

fix(upgrade): restrict delta patch from-version to same major.minor series - #1329

Merged
BYK merged 2 commits into
mainfrom
fix/prev-tag-same-series
Aug 1, 2026
Merged

fix(upgrade): restrict delta patch from-version to same major.minor series#1329
BYK merged 2 commits into
mainfrom
fix/prev-tag-same-series

Conversation

@BYK

@BYK BYK commented Jul 31, 2026

Copy link
Copy Markdown
Member

Problem

generate-patches was picking the wrong PREV_TAG for 0.40 nightly
builds, causing patches to be generated from a 0.41 source and stamped
with from-version: 0.41.x-dev.Y — useless for any user on the 0.40
series. Their chain resolution then fails (their version is
chronologically before 0.41) and the upgrade falls back to full-download
every time.

Repro on the live registry

ghcr.io/getsentry/cli:patch-0.40.0-dev.1785524715
  from-version: 0.41.0-dev.1785504113   ← should be 0.40.0-dev.1785522378

ghcr.io/getsentry/cli:patch-0.40.0-dev.1785526951
  from-version: 0.41.0-dev.1785504113   ← should be 0.40.0-dev.1785524715

Root cause

The PREV_TAG loop in generate-patches:

TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-[0-9]' | sort -V || echo "")
for tag in $TAGS; do
  if [ "$tag" = "nightly-${VERSION}" ]; then break; fi
  PREV_TAG="$tag"
done

sort -V orders 0.41.0-dev.X after every 0.40.0-dev.Y (because
0.41 > 0.40 lexicographically). At the time generate-patches runs,
the current build's tag hasn't been pushed yet — so the
if [ "$tag" = "nightly-${VERSION}" ] break never triggers. The loop
walks past every 0.40 tag and onto the 0.41 tag, and PREV_TAG becomes
the 0.41 nightly.

Fix

Filter TAGS to only those in the same major.minor series as the
current build before iterating. MAJOR_MINOR is parsed from VERSION
(echo "${VERSION}" | cut -d. -f1,2 → e.g. 0.40). For 0.40 builds
the loop only sees nightly-0.40.0-dev.* tags; for 0.41 builds only
nightly-0.41.0-dev.*. The first build of a new series (no prior tags
in the same series) falls through with PREV_TAG="" and the existing
HAS_PREV skip handles it cleanly.

Retroactive remediation (follow-up)

The two mis-annotated patches above have correct bytes but wrong
annotations. Re-stamping them with the correct from-version will let
existing 0.40-series users chain through them to the latest. Tracked as
a separate follow-up — they're already on the registry, just need the
annotation re-pushed.

Relationship to PR #1327

PR #1327 prevents annotation drift between generate-patches and
publish-nightly (they now agree via job outputs). This fix prevents
the initial PREV_TAG selection from being wrong in the first place.
Together they make the CI pipeline produce correct annotations
end-to-end.

Verification

  • Local simulation: MAJOR_MINOR filter + iteration over same-series
    tags returns nightly-0.40.0-dev.1785522378 for VERSION=0.40.0-dev.1785524715
    vs. the current broken nightly-0.41.0-dev.1785504113.
  • Workflow YAML parses cleanly.
  • Will re-verify against a real build after merge (next nightly).

…eries

The PREV_TAG loop in `generate-patches` was walking past all 0.40
nightly tags and onto the lone 0.41.0-dev.1785504113 tag because
`sort -V` orders 0.41 > 0.40 lexicographically. At the time
generate-patches runs, the current build's tag hasn't been pushed
yet, so the loop's `if [ "$tag" = "nightly-${VERSION}" ]` break
never triggers — the loop ends naturally after iterating every
existing tag, and PREV_TAG becomes the 0.41 nightly.

Result: patches were generated FROM the 0.41 binary but stamped
with `from-version: 0.41.x-dev.Y` in the annotation. chain
resolution then fails for any user on the 0.40 series (their
current version is chronologically *before* 0.41.0-dev.1785504113
and the patch's from-version annotation doesn't match any chain
step), so delta upgrades fall back to full-download — every time.

Repro on the live registry:

```
# Inspect recent patches
$ ghcr.io/getsentry/cli:patch-0.40.0-dev.1785524715
  from-version: 0.41.0-dev.1785504113   ← WRONG (should be 0.40.0-dev.1785522378)
$ ghcr.io/getsentry/cli:patch-0.40.0-dev.1785526951
  from-version: 0.41.0-dev.1785504113   ← WRONG (should be 0.40.0-dev.1785524715)
```

Fix: filter TAGS to only those in the same major.minor series as
the current build before iterating. `MAJOR_MINOR` is parsed from
VERSION (`echo "${VERSION}" | cut -d. -f1,2` → e.g. `0.40`).
For `0.40.0-dev.X` builds the loop only sees `nightly-0.40.x-dev.*`
tags; for `0.41.0-dev.X` only `nightly-0.41.x-dev.*`. The first
build of a new series (no prior tags in the same series) falls
through with PREV_TAG="" and the existing HAS_PREV skip handles
it cleanly.

Retroactive remediation: the two mis-annotated patches above will
need their `from-version` annotations re-stamped (their patch bytes
are already correct — only the annotation is wrong). A separate
follow-up will re-publish `patch-0.40.0-dev.1785524715` with
`from-version=0.40.0-dev.1785522378` and
`patch-0.40.0-dev.1785526951` with
`from-version=0.40.0-dev.1785524715` so existing users on the
0.40 series can chain through them.

PR #1327 already prevents annotation drift between generate-patches
and publish-nightly — this fix prevents the initial PREV_TAG selection
from being wrong in the first place.
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Jul 31, 2026
Comment thread .github/workflows/ci.yml
Adversarial review (NIT-2): the dot in `0.40` was being interpolated
into a regex `grep` pattern, where `.` matches any character. In
practice this can't mis-match because `oras repo tags` is filtered
to `^nightly-[0-9]` upstream and `MAJOR_MINOR` is always digits.digits
— but `grep -F` (fixed-string) makes the intent explicit and matches
the defensive style of the rest of the script.
@BYK
BYK enabled auto-merge (squash) July 31, 2026 23:57
@BYK
BYK merged commit 255f8a8 into main Aug 1, 2026
44 of 46 checks passed
@BYK
BYK deleted the fix/prev-tag-same-series branch August 1, 2026 00:00
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-01 00:00 UTC

BYK added a commit to BYK/binpatch that referenced this pull request Aug 1, 2026
Closes #40. The `publish-ghcr` mode re-derived the previous version from
the registry independently of the `generate-ghcr` step that actually
downloaded the source binary and ran bsdiff. If the registry state
changed between the two steps, the two values could disagree and
`binpatch` consumers would silently hit SHA mismatch (the library only
verifies the final output SHA, not the source binary identity — the
annotation is a chain pointer, not a content hash).

## Fix

Add a new `from-version` input on `publish-ghcr`. When set, the
annotation on the pushed patch manifest uses THIS value verbatim. When
empty (back-compat), the old registry re-derive runs and prints a hint
suggesting the caller thread the value from `generate-ghcr`.

## Consumer wiring

```yaml
- uses: BYK/binpatch/action@vX.Y.Z
  id: gen
  with: { mode: generate-ghcr, ... }

- uses: BYK/binpatch/action@vX.Y.Z
  with:
    mode: publish-ghcr
    from-version: ${{ steps.gen.outputs.from-version }}
```

## Files

- action/action.yml: add `from-version` input, thread to push_patches
via env, use input when set else re-derive
- action/README.md: document the new input and the gen→publish wiring

## Relationship to other fixes

- #37 — fd-close fix in applyReaderToFile (released as
0.4.0)
- getsentry/cli#1329 — same-series filter in custom workflow (now
merged)
- getsentry/cli#1327 — CI workflow annotation threading (now merged)

Combined, the chain-side fixes (binpatch fd-close + getsentry/cli CI
workflow) and this action-side fix mean the consumer can stop running
its own hand-rolled patch generation/publish and consume the binpatch
action directly.

## Verification

- pnpm test: 58 passed
- pnpm build: clean
- pnpm typecheck: clean
- pnpm lint: (no script)
BYK added a commit to BYK/binpatch that referenced this pull request Aug 1, 2026
…44)

The `prev-ghcr` step walked past all 0.40 nightly tags and onto a stray
0.41 nightly because `sort -V` orders 0.41 > 0.40 lexicographically. At
the time this step runs, the current build's own tag hasn't been pushed
yet, so the loop's `if [ "$tag" = ... ]` break never triggers — the loop
ends naturally after iterating every existing tag, and PREV_TAG becomes
the 0.41 nightly.

Result: patches were generated FROM the 0.41 binary and stamped with
`from-version: 0.41.x-dev.Y` — useless to any user on the 0.40 series.

## Fix

Filter TAGS to only those in the same major.minor series as the current
build before iterating. `MAJOR_MINOR` is parsed from VERSION (`echo
"${VERSION}" | cut -d. -f1,2` → e.g. `0.40`). For `0.40.0-dev.X` builds
the loop only sees `nightly-0.40.x-dev.*` tags; for `0.41.0-dev.X` only
`nightly-0.41.x-dev.*`. First build of a new series falls through with
PREV_TAG="" and the existing skip-empty-PREV path handles it cleanly.

Also uses `grep -F` (fixed-string) so the dot in MAJOR_MINOR is treated
literally rather than as a regex metacharacter.

## Relationship to getsentry/cli#1329

This is the action-side port of getsentry/cli#1329's hand-rolled
`MAJOR_MINOR` filter. getsentry/cli is migrating to use this action (PR
#1330) so this fix lands ahead of the migration completing.

## Verification

- pnpm test: 58 passed
- YAML parses cleanly
BYK added a commit that referenced this pull request Aug 1, 2026
BYK/binpatch#44 ported the same-series filter from #1329
into the binpatch action's prev-ghcr step, so consumers adopting
`generate-ghcr` no longer need to re-implement this safeguard.

This bumps both the runtime dep (`binpatch` ^0.4.1 → ^0.4.2) and
the action ref (BYK/binpatch/action@0.4.1 → @0.4.2) so the
migration actually picks up the action's own fix.
BYK added a commit that referenced this pull request Aug 1, 2026
…lish (#1330)

Replaces the hand-rolled ~250 lines of `generate-patches` and
`publish-nightly` bash in `.github/workflows/ci.yml` with the
`BYK/binpatch/action@v0.4.1` composite action. The action handles:

- ORAS CLI install + GHCR login
- Previous-nightly discovery (with the same-series filter that fixes the
cross-major nightly tag confusion)
- zig-bsdiff invocation + patch size gate (50% ratio)
- Binary + versioned-tag + patch manifest push with `from-version`
annotation threaded via action outputs

This eliminates the divergence bug we hit twice:

- The PREV_TAG recomputation divergence between generate-patches and
publish-nightly (PR #1327 fixed this in the hand-rolled shell; the
action solves it generically by exposing
`generate-ghcr.outputs.from-version` and consuming it as a
`publish-ghcr` input — see BYK/binpatch#40)
- The cross-major nightly tag confusion (PR #1329 fixed this in the
hand-rolled shell; the action's `validateChainStep` does the equivalent
check at chain-resolution time)

Also bumps the `binpatch` runtime dep from `^0.3.1` to `^0.4.1` for the
new `openSync`/`writeSync`/`closeSync` output path (which avoids the
ETXTBSY race when chaining into a `spawn(tempPath)`).

## Files

- `.github/workflows/ci.yml`: -220 lines of hand-rolled shell, +10 lines
of action wiring
- `packages/cli/package.json`: binpatch ^0.3.1 → ^0.4.1
- `pnpm-lock.yaml`: regenerated

## Diff

```
.github/workflows/ci.yml                           | 347 +++++++-------
packages/cli/package.json                          |   2 +-
pnpm-lock.yaml                                     |  10 +-
```

## Verification

- `node -e 'require("yaml").parse(...)'` parses cleanly
- `pnpm --filter sentry run typecheck`: clean
- `pnpm --filter sentry run lint`: clean
- Lint passes (no shell-script lint script; YAML is the only surface)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant