Skip to content

Make macOS release resilient to stapler failures and add assistant.zip artifact#1860

Merged
dvargasfuertes merged 2 commits into
mainfrom
devin/1771051613-stapler-resilience
Feb 14, 2026
Merged

Make macOS release resilient to stapler failures and add assistant.zip artifact#1860
dvargasfuertes merged 2 commits into
mainfrom
devin/1771051613-stapler-resilience

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Feb 14, 2026

Captures xcrun stapler staple errors (previously swallowed), makes the release step resilient to macOS deployment failures via continue-on-error + always(), and adds a zipped assistant/ directory as a release artifact. Also bumps assistant version to 0.1.11.


Summary

  • Stapler error visibility: xcrun stapler staple output is now captured into a variable and printed on every attempt, with ::warning:: annotations showing attempt number and exit code (previously the error was swallowed by the if conditional).
  • Release resilience: The stapler step has continue-on-error: true, and the release step uses if: always() so it runs even if upstream steps fail. Release assets are dynamically built from whichever files exist on disk.
  • Assistant zip artifact: A new step early in the pipeline (before bun install / macOS build) zips the assistant/ directory and includes it as a release asset.
  • Version bump: assistant/package.json 0.1.10 → 0.1.11.

Review & Testing Checklist for Human

  • always() scope is broader than just stapler: The release step will now also run if notarization or earlier build steps fail. This could produce a release with only assistant.zip (no DMG) if the build itself fails. Confirm this is acceptable vs. gating on build success.
  • Zip path resolution: The zip step uses (cd ../.. && zip -r clients/macos/build/assistant.zip assistant/) relative to the job's working-directory: clients/macos. This hasn't been tested on the actual macOS runner — consider triggering a workflow_dispatch run to verify.
  • assistant.zip contents: The zip is created before bun install, so it contains only source files (no node_modules or built artifacts). Confirm "fully zipped assistant directory" means repo source, not a built bundle.

Notes


Open with Devin

…p artifact

Co-Authored-By: vargas@vellum.ai <vargas@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Co-Authored-By: vargas@vellum.ai <vargas@vellum.ai>
[ -f "$DMG_PATH" ] && ASSETS+=("$DMG_PATH")
[ -f "dist/vellum-assistant.zip" ] && ASSETS+=("dist/vellum-assistant.zip")
[ -f "$APPCAST" ] && ASSETS+=("$APPCAST#appcast.xml")
[ -f "build/assistant.zip" ] && ASSETS+=("build/assistant.zip#assistant.zip")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DevinAI this looks redundant? Looks like we have the assistant artifact on here twice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are actually two different files:

  • dist/vellum-assistant.zip — the Sparkle auto-update ZIP containing the built Vellum.app (macOS app bundle, created in the "Build ZIP for Sparkle auto-update" step)
  • build/assistant.zip — the new zip of the assistant/ source directory (your third request)

Want me to remove one, or rename build/assistant.zip to something more distinct?

@dvargasfuertes dvargasfuertes marked this pull request as ready for review February 14, 2026 07:06
@dvargasfuertes dvargasfuertes merged commit 7e4343f into main Feb 14, 2026
1 check passed
@dvargasfuertes dvargasfuertes deleted the devin/1771051613-stapler-resilience branch February 14, 2026 07:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3523019852

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


- name: Create GitHub Release on public repos
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
if: always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate release creation on successful build outcomes

Using always() here causes the release step to run after any upstream failure on main/tags, not just stapling issues. This step later deletes the existing latest release and tag before publishing new assets, so a failed build/notarization run can replace a healthy release with an incomplete one (for example, only assistant.zip), which can break normal download and update flows until another successful run.

Useful? React with 👍 / 👎.

Comment on lines +231 to +232
STAPLE_OUTPUT=$(xcrun stapler staple "$DMG_PATH" 2>&1)
STAPLE_EXIT=$?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve stapler retries when stapling initially fails

This assignment runs outside an if guard, and GitHub Actions bash steps use -e, so a non-zero stapler exit aborts the script immediately on the first failed attempt. As a result, the retry loop and its diagnostics are skipped for transient CDN propagation failures, and because the step is continue-on-error, the workflow can proceed to release without ever completing the intended stapling/validation retries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review


- name: Create GitHub Release on public repos
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
if: always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 always() on release step can replace a good release with a broken one after build/notarization failures

The release step at line 324 uses if: always() && (...), which causes it to run even when critical upstream steps like build, signing, or notarization fail. The step first deletes the existing latest release and tag (lines 346-347), then creates a new release from whichever files happen to exist on disk (lines 349-354).

Root Cause and Impact

If the build or notarization step fails (e.g., Xcode compilation error, notarization rejection), the DMG, Sparkle ZIP, and appcast won't exist. The intermediate steps ("Build ZIP for Sparkle", "Generate appcast.xml") use if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') which implicitly includes success(), so they'll be skipped on failure. But the release step with always() still runs.

The result is:

  1. The existing good latest release is deleted (gh release delete latest)
  2. The existing latest tag is deleted
  3. A new release is created containing only build/assistant.zip (the source zip created early in the pipeline before any build steps)

This replaces a fully functional release (with DMG, Sparkle auto-update ZIP, and appcast) with one containing only a source archive. Users downloading the latest release would get no installable artifact, and Sparkle auto-update would break.

The intent was to be resilient only to stapler failures (the step with continue-on-error: true). The condition should be scoped to allow the staple step to fail while still requiring all other steps to succeed, e.g.:

if: (success() || steps.staple.outcome == 'failure') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
Suggested change
if: always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
if: (success() || steps.staple.outcome == 'failure') && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

ashleeradka added a commit that referenced this pull request Feb 14, 2026
Resolved modify/delete conflict: IPCMessages.swift was moved to shared/
in this branch, while main had no modifications to port.

Changes from main:
- Tool result images for OpenAI/Gemini providers (#1864)
- Browser screenshot in tool call results (#1863)
- Qdrant vector DB clear (#1862)
- Debug prompt logger truncation removal (#1861)
- macOS release stapler resilience (#1860)

No conflicts with shared library changes.
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