@@ -11,8 +11,10 @@ concurrency:
1111 cancel-in-progress : true
1212
1313# packages:write is needed for publish-nightly to push to GHCR
14+ # issues:write is needed for generate-patches to file issues on failure
1415permissions :
1516 contents : read
17+ issues : write
1618 packages : write
1719
1820env :
@@ -312,11 +314,164 @@ jobs:
312314 name : sentry-${{ matrix.target }}-gz
313315 path : dist-bin/*.gz
314316
317+ generate-patches :
318+ name : Generate Delta Patches
319+ needs : [changes, build-binary]
320+ # Only on main (nightlies) and release branches (stable) — skip PRs
321+ if : github.event_name != 'pull_request'
322+ runs-on : ubuntu-latest
323+ continue-on-error : true
324+ steps :
325+ - name : Install ORAS CLI
326+ # Only needed on main (to fetch previous nightly from GHCR)
327+ if : github.ref == 'refs/heads/main'
328+ run : |
329+ VERSION=1.2.3
330+ EXPECTED_SHA256="b4efc97a91f471f323f193ea4b4d63d8ff443ca3aab514151a30751330852827"
331+ TARBALL="oras_${VERSION}_linux_amd64.tar.gz"
332+ curl -sfLo "$TARBALL" "https://github.com/oras-project/oras/releases/download/v${VERSION}/${TARBALL}"
333+ echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c -
334+ tar -xz -C /usr/local/bin oras < "$TARBALL"
335+ rm "$TARBALL"
336+
337+ - name : Install zig-bsdiff
338+ run : |
339+ VERSION=0.1.19
340+ EXPECTED_SHA256="9f1ac75a133ee09883ad2096a86d57791513de5fc6f262dfadee8dcee94a71b9"
341+ TARBALL="zig-bsdiff-linux-x64.tar.gz"
342+ curl -sfLo "$TARBALL" "https://github.com/blackboardsh/zig-bsdiff/releases/download/v${VERSION}/${TARBALL}"
343+ echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c -
344+ tar -xz -C /usr/local/bin < "$TARBALL"
345+ rm "$TARBALL"
346+
347+ - name : Download current binaries
348+ uses : actions/download-artifact@v8
349+ with :
350+ pattern : sentry-*
351+ path : new-binaries
352+ merge-multiple : true
353+
354+ - name : Download previous nightly binaries (main)
355+ if : github.ref == 'refs/heads/main'
356+ run : |
357+ VERSION="${{ needs.changes.outputs.nightly-version }}"
358+ REPO="ghcr.io/getsentry/cli"
359+
360+ echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
361+
362+ # Find previous nightly tag
363+ TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-' | sort -V || echo "")
364+ PREV_TAG=""
365+ for tag in $TAGS; do
366+ # Current tag may not exist yet (publish-nightly hasn't run),
367+ # but that's fine — we want the latest existing nightly tag
368+ if [ "$tag" = "nightly-${VERSION}" ]; then
369+ break
370+ fi
371+ PREV_TAG="$tag"
372+ done
373+
374+ if [ -z "$PREV_TAG" ]; then
375+ echo "No previous versioned nightly found, skipping patches"
376+ exit 0
377+ fi
378+ echo "HAS_PREV=true" >> "$GITHUB_ENV"
379+ echo "Previous nightly: ${PREV_TAG}"
380+
381+ # Download and decompress previous nightly binaries
382+ mkdir -p old-binaries
383+ PREV_MANIFEST_JSON=$(oras manifest fetch "${REPO}:${PREV_TAG}")
384+ echo "$PREV_MANIFEST_JSON" | jq -r '.layers[] | select(.annotations["org.opencontainers.image.title"] | endswith(".gz")) | .annotations["org.opencontainers.image.title"] + " " + .digest' | while read -r filename digest; do
385+ basename="${filename%.gz}"
386+ echo " Downloading previous ${basename}..."
387+ oras blob fetch "${REPO}@${digest}" --output "old-binaries/${filename}"
388+ gunzip -f "old-binaries/${filename}"
389+ done
390+
391+ - name : Download previous release binaries (release/**)
392+ if : startsWith(github.ref, 'refs/heads/release/')
393+ env :
394+ GH_TOKEN : ${{ github.token }}
395+ run : |
396+ PREV_TAG=$(gh api "repos/${{ github.repository }}/releases?per_page=5" \
397+ --jq '[.[] | select(.prerelease == false and .draft == false)] | .[0].tag_name // empty')
398+
399+ if [ -z "$PREV_TAG" ]; then
400+ echo "No previous stable release found — skipping patch generation"
401+ exit 0
402+ fi
403+ echo "HAS_PREV=true" >> "$GITHUB_ENV"
404+ echo "Previous release: ${PREV_TAG}"
405+
406+ mkdir -p old-binaries
407+ for gz in new-binaries/*.gz; do
408+ name=$(basename "${gz%.gz}")
409+ echo " Downloading ${name}.gz from ${PREV_TAG}..."
410+ gh release download "${PREV_TAG}" \
411+ --repo "${{ github.repository }}" \
412+ --pattern "${name}.gz" \
413+ --dir old-binaries || echo " Warning: not found, skipping"
414+ done
415+ gunzip old-binaries/*.gz 2>/dev/null || true
416+
417+ - name : Generate delta patches
418+ if : env.HAS_PREV == 'true'
419+ run : |
420+ mkdir -p patches
421+ GENERATED=0
422+
423+ for new_binary in new-binaries/sentry-*; do
424+ name=$(basename "$new_binary")
425+ case "$name" in *.gz) continue ;; esac
426+
427+ old_binary="old-binaries/${name}"
428+ [ -f "$old_binary" ] || continue
429+
430+ echo "Generating patch: ${name}.patch"
431+ bsdiff "$old_binary" "$new_binary" "patches/${name}.patch" --use-zstd
432+
433+ patch_size=$(stat --printf='%s' "patches/${name}.patch")
434+ new_size=$(stat --printf='%s' "$new_binary")
435+ ratio=$(awk "BEGIN { printf \"%.1f\", ($patch_size / $new_size) * 100 }")
436+ echo " ${patch_size} bytes (${ratio}% of binary)"
437+ GENERATED=$((GENERATED + 1))
438+ done
439+
440+ echo "Generated ${GENERATED} patches"
441+
442+ - name : Upload patch artifacts
443+ if : env.HAS_PREV == 'true'
444+ uses : actions/upload-artifact@v7
445+ with :
446+ name : sentry-patches
447+ path : patches/*.patch
448+
449+ - name : File issue on failure
450+ if : failure()
451+ env :
452+ GH_TOKEN : ${{ github.token }}
453+ run : |
454+ TITLE="Delta patch generation failed"
455+ BODY="The \`generate-patches\` job failed on [\`${GITHUB_REF_NAME}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}).
456+
457+ **Branch:** \`${GITHUB_REF_NAME}\`
458+ **Commit:** ${GITHUB_SHA}
459+ **Run:** ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
460+
461+ # Check for existing open issue with same title to avoid duplicates
462+ EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE" --json number --jq '.[0].number // empty')
463+ if [ -n "$EXISTING" ]; then
464+ echo "Issue #${EXISTING} already open, adding comment"
465+ gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
466+ else
467+ gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
468+ fi
469+
315470 publish-nightly :
316471 name : Publish Nightly to GHCR
317472 # Only run on pushes to main, not on PRs or release branches
318473 if : github.ref == 'refs/heads/main' && github.event_name == 'push'
319- needs : [changes, build-binary]
474+ needs : [changes, build-binary, generate-patches ]
320475 runs-on : ubuntu-latest
321476 steps :
322477 - name : Download compressed artifacts
@@ -326,13 +481,21 @@ jobs:
326481 path : artifacts
327482 merge-multiple : true
328483
329- - name : Download uncompressed artifacts (for patch generation )
484+ - name : Download uncompressed artifacts (for SHA-256 computation )
330485 uses : actions/download-artifact@v8
331486 with :
332487 pattern : sentry-*
333488 path : binaries
334489 merge-multiple : true
335490
491+ - name : Download patch artifacts
492+ uses : actions/download-artifact@v8
493+ continue-on-error : true
494+ id : download-patches
495+ with :
496+ name : sentry-patches
497+ path : patches
498+
336499 - name : Install ORAS CLI
337500 run : |
338501 VERSION=1.2.3
@@ -343,20 +506,10 @@ jobs:
343506 tar -xz -C /usr/local/bin oras < "$TARBALL"
344507 rm "$TARBALL"
345508
346- - name : Install zig-bsdiff
347- run : |
348- VERSION=0.1.19
349- EXPECTED_SHA256="9f1ac75a133ee09883ad2096a86d57791513de5fc6f262dfadee8dcee94a71b9"
350- TARBALL="zig-bsdiff-linux-x64.tar.gz"
351- curl -sfLo "$TARBALL" "https://github.com/blackboardsh/zig-bsdiff/releases/download/v${VERSION}/${TARBALL}"
352- echo "${EXPECTED_SHA256} ${TARBALL}" | sha256sum -c -
353- tar -xz -C /usr/local/bin < "$TARBALL"
354- rm "$TARBALL"
355-
356509 - name : Log in to GHCR
357510 run : echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
358511
359- - name : Push to GHCR
512+ - name : Push binaries to GHCR
360513 # Push from inside the artifacts directory so ORAS records bare
361514 # filenames (e.g. "sentry-linux-x64.gz") as layer titles, not
362515 # "artifacts/sentry-linux-x64.gz". The CLI matches layers by
@@ -377,77 +530,43 @@ jobs:
377530 VERSION="${{ needs.changes.outputs.nightly-version }}"
378531 oras tag ghcr.io/getsentry/cli:nightly "nightly-${VERSION}"
379532
380- - name : Generate and push delta patches
381- # Download the previous nightly's binaries, generate TRDIFF10 patches,
382- # and push a :patch-<version> manifest with SHA-256 annotations.
533+ - name : Push delta patches to GHCR
534+ # Upload pre-generated patches from generate-patches job as a
535+ # :patch-<version> manifest with SHA-256 annotations.
383536 # Failure here is non-fatal — delta upgrades are optional.
537+ if : steps.download-patches.outcome == 'success'
384538 continue-on-error : true
385539 run : |
386540 VERSION="${{ needs.changes.outputs.nightly-version }}"
387541 REPO="ghcr.io/getsentry/cli"
388542
389- # Find the previous nightly by listing versioned tags and picking
390- # the one immediately before ours in version-sorted order.
391- TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-' | sort -V || echo "")
392- PREV_TAG=""
393- for tag in $TAGS; do
394- if [ "$tag" = "nightly-${VERSION}" ]; then
395- break
396- fi
397- PREV_TAG="$tag"
398- done
399-
400- if [ -z "$PREV_TAG" ]; then
401- echo "No previous versioned nightly found, skipping patches"
402- exit 0
403- fi
404-
405- PREV_VERSION="${PREV_TAG#nightly-}"
406- echo "Generating patches: ${PREV_VERSION} → ${VERSION}"
407-
408- # Download previous nightly binaries
409- mkdir -p prev-binaries
410- PREV_MANIFEST_JSON=$(oras manifest fetch "${REPO}:${PREV_TAG}")
411- # Extract .gz layer digests and download + decompress each
412- echo "$PREV_MANIFEST_JSON" | jq -r '.layers[] | select(.annotations["org.opencontainers.image.title"] | endswith(".gz")) | .annotations["org.opencontainers.image.title"] + " " + .digest' | while read -r filename digest; do
413- basename="${filename%.gz}"
414- echo " Downloading previous ${basename}..."
415- oras blob fetch "${REPO}@${digest}" --output "prev-binaries/${filename}"
416- gunzip -f "prev-binaries/${filename}"
417- done
418-
419- # Generate patches and compute SHA-256 hashes of NEW binaries
420- mkdir -p patches
543+ # Compute SHA-256 annotations from new binaries and collect patch files
421544 ANNOTATIONS=""
422545 PATCH_FILES=""
423-
424- for new_binary in binaries/sentry-*; do
425- basename=$(basename "$new_binary")
426- # Skip .gz files
427- case "$basename" in *.gz) continue ;; esac
428-
429- old_binary="prev-binaries/${basename}"
430- if [ ! -f "$old_binary" ]; then
431- echo " No previous binary for ${basename}, skipping"
432- continue
546+ for patch_file in patches/*.patch; do
547+ basename_patch=$(basename "$patch_file" .patch)
548+ new_binary="binaries/${basename_patch}"
549+ if [ -f "$new_binary" ]; then
550+ sha256=$(sha256sum "$new_binary" | cut -d' ' -f1)
551+ ANNOTATIONS="${ANNOTATIONS} --annotation sha256-${basename_patch}=${sha256}"
433552 fi
434-
435- patch_file="patches/${basename}.patch"
436- echo " Generating patch for ${basename}..."
437- bsdiff "$old_binary" "$new_binary" "$patch_file" --use-zstd
438-
439- # Compute SHA-256 of the NEW (target) binary for verification
440- sha256=$(sha256sum "$new_binary" | cut -d' ' -f1)
441- ANNOTATIONS="${ANNOTATIONS} --annotation sha256-${basename}=${sha256}"
442- PATCH_FILES="${PATCH_FILES} ${basename}.patch"
553+ PATCH_FILES="${PATCH_FILES} $(basename "$patch_file")"
443554 done
444555
445556 if [ -z "$PATCH_FILES" ]; then
446- echo "No patches generated "
557+ echo "No patches to push "
447558 exit 0
448559 fi
449560
450- # Push patch manifest with from-version and SHA-256 annotations
561+ # Find from-version by listing GHCR nightly tags
562+ TAGS=$(oras repo tags "${REPO}" 2>/dev/null | grep '^nightly-' | sort -V || echo "")
563+ PREV_TAG=""
564+ for tag in $TAGS; do
565+ if [ "$tag" = "nightly-${VERSION}" ]; then break; fi
566+ PREV_TAG="$tag"
567+ done
568+ PREV_VERSION="${PREV_TAG#nightly-}"
569+
451570 cd patches
452571 eval oras push "${REPO}:patch-${VERSION}" \
453572 --artifact-type application/vnd.sentry.cli.patch \
@@ -547,15 +666,15 @@ jobs:
547666 ci-status :
548667 name : CI Status
549668 if : always()
550- needs : [changes, check-skill, eval-skill, build-binary, build-npm, build-docs, test-e2e, publish-nightly]
669+ needs : [changes, check-skill, eval-skill, build-binary, build-npm, build-docs, test-e2e, generate-patches, publish-nightly]
551670 runs-on : ubuntu-latest
552671 permissions : {}
553672 steps :
554673 - name : Check CI status
555674 run : |
556675 # Check for explicit failures or cancellations in all jobs
557- # publish-nightly is skipped on PRs (if: github.ref == 'refs/heads/main') — that's expected
558- results="${{ needs.check-skill.result }} ${{ needs.eval-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.publish-nightly.result }}"
676+ # generate-patches and publish-nightly are skipped on PRs — that's expected
677+ results="${{ needs.check-skill.result }} ${{ needs.eval-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.generate-patches.result }} ${{ needs. publish-nightly.result }}"
559678 for result in $results; do
560679 if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
561680 echo "::error::CI failed"
0 commit comments