From pulp
Sign, notarize, package, and distribute Pulp plugins and apps across macOS, Windows, and Android
npx claudepluginhub danielraffel/pulp --plugin pulpThis skill uses the workspace's default tool permissions.
The `pulp ship` command handles the full distribution pipeline: code signing, Apple notarization, platform-specific packaging, update feed generation, and Android APK/AAB builds.
Monitors deployed URLs for regressions after deploys, merges, or upgrades by checking HTTP status, console errors, network failures, performance (LCP/CLS/INP), content, and API health.
Share bugs, ideas, or general feedback.
The pulp ship command handles the full distribution pipeline: code signing, Apple notarization, platform-specific packaging, update feed generation, and Android APK/AAB builds.
Before running pulp ship ..., source the shared skew-check helper so
a user on an outdated CLI sees a one-line hint (stderr, once per
session) when the installed CLI is older than the plugin's declared
min_cli_version:
source "$(git rev-parse --show-toplevel)/tools/scripts/cli_version_check.sh"
pulp_cli_version_check
Advisory only — never blocks. Full contract + override knobs in the
upgrade skill. Release-discovery Slice 6 (#551).
| Command | Platform | What It Does |
|---|---|---|
sign | macOS, Windows, Android | Sign plugin bundles or APK/AAB |
notarize | macOS only | Submit to Apple notarization, poll, staple |
package | All | Create .pkg (macOS), NSIS (Windows), APK+AAB (Android) |
appcast | All | Generate Sparkle-compatible XML update feed |
check | All | Verify signing status of built artifacts |
Settings are resolved in order: CLI flag > environment variable > ~/.pulp/config.toml
pulp config init # Create config from template
pulp config set signing.apple.identity "Developer ID Application: Name (TEAMID)"
pulp config set signing.apple.team_id "ABCDE12345"
pulp config set signing.apple.apple_id "you@example.com"
pulp config set signing.android.keystore "~/keystores/release.jks"
~/.pulp/config.toml (override with $PULP_HOME)
See config.example.toml in the repo root for all options with documentation.
Before executing any signing, notarization, or packaging action, the /ship command uses AskUserQuestion to:
pulp config setWhen invoked via skill trigger (not slash command), apply the same pattern: always show what will happen and confirm before executing.
pulp build # Must build first
pulp ship sign # Uses identity from config
pulp ship notarize # Uses apple_id/team_id from config
pulp ship package --version 1.0.0 # Creates .pkg in artifacts/
pulp ship appcast --url https://example.com/Plugin.pkg --version 1.0.0
pulp build --target android # Build native libs
pulp ship package --target android --keystore ~/key.jks # Gradle build + sign APK/AAB
pulp ship check --target android # Verify APK/AAB signatures
Note: pulp ship package --target android invokes Gradle which builds AND signs in one step when a keystore is provided. Use pulp ship sign --target android only to re-sign existing artifacts in artifacts/.
pulp build # Must build first
pulp ship sign --identity "Your Company" # Uses signtool
pulp ship package --version 1.0.0 # Creates NSIS .exe installer
--abi arm64-v8a # Default — ARM64 phones + tablets
--abi x86_64 # Emulator / Chromebook
--abi all # arm64-v8a + x86_64 + armeabi-v7a
No special flags needed. ARM64 covers phones and tablets. AAB with split APKs handles screen density automatically.
keytool -genkey -v -keystore release.jks -keyalg RSA -keysize 2048 -validity 10000
Never store passwords in plaintext config. Use environment variable references:
store_pass = "@env:ANDROID_STORE_PASS"
Run security find-identity -v -p codesigning (macOS) to find your identity, then:
pulp config set signing.apple.identity "Developer ID Application: ..."
Install Android Studio or set ANDROID_HOME:
export ANDROID_HOME=~/Library/Android/sdk # macOS
xcrun notarytool log <UUID>pulp doctor to check Android SDK/NDK/Java versionsandroid/ project exists (pulp create --targets android)pulp ship appcast feeds can be generated by older tools or edited by hand.
When parsing existing Sparkle appcast XML, malformed optional enclosure
metadata must fail soft. In particular, a non-numeric or overflowing
length="..." attribute should leave the item's file size at 0 and keep
parsing the item instead of throwing out of Appcast::from_xml. Keep this
behavior covered in test/test_appcast.cpp when changing
ship/src/appcast.cpp.
Pulp executes Android package helpers through cmd.exe /c on Windows. Android
SDK tools and Gradle wrappers may resolve to .bat files there, so command
strings that begin with a quoted batch path need an outer command quote so
cmd.exe does not strip the executable quote while preserving the remaining
quoted arguments. For Gradle and bundletool name=value parameters that
contain paths or passwords, quote the whole name=value token rather than only
the value ("--output=C:\path\file.apks", not --output="C:\path\file.apks"),
because Windows batch %~1/shift parsing does not normalize embedded quotes
the same way POSIX shells do. Gradle wrapper invocations should use the
ChildProcess working_directory option instead of inlining cd ... && gradlew
into the shell string, so fake wrappers and real Gradle builds write artifacts
relative to the project root consistently. Keep this in mind when touching
ship/platform/android/package_android.cpp.
Symptom: dyld: Library not loaded: @rpath/libwgpu_native.dylib or
error while loading shared libraries: libwgpu_native.so after the
user extracts pulp-<platform>.tar.gz from a GitHub Release.
Root cause: release-cli.yml historically uploaded the bare
build/tools/cli/pulp binary, which carries an LC_RPATH /
DT_RUNPATH pointing at the build runner's home directory (e.g.
/Users/runner/Library/Caches/Pulp/... on macOS, the
GitHub-hosted-runner workspace on Linux). That path doesn't exist on
user machines.
Fix (active since v0.20.x): tools/scripts/package_cli.py is invoked
by release-cli.yml to:
libwgpu_native.{dylib,so,dll} next to the binary.install_name_tool -delete_rpath every absolute LC_RPATH
and add @loader_path.patchelf --set-rpath '$ORIGIN'.The portable-binary smoke gate in release-cli.yml runs the produced
artifact on a clean runner that did not build it, catching the bug
class before tagging. If you change rpath logic, run the smoke job
locally first or it will fail in CI for everyone else.
After the Rust CLI flip, release artifacts must contain both pulp
and pulp-cpp in the same archive. pulp is the user-facing Rust CLI;
pulp-cpp is the C++ delegate used for fallthrough commands that still
link framework libraries. Do not ship pulp-rs as the public binary
name, and do not drop pulp-cpp from tarballs or zips.
Smoke both paths when touching .github/workflows/release-cli.yml or
tools/scripts/package_cli.py: run pulp version --json against the
Rust binary, then exercise a C++-owned command through
PULP_RS_CPP_BINARY=/path/to/pulp-cpp pulp ... or invoke pulp-cpp
directly. This preserves rollback/debug workflows such as
PULP_USE_CPP=1 pulp <args> for existing Pulp projects.
Symptom: a consumer links against a downloaded pulp-sdk-<platform>
release artifact and fails to resolve pulp::view::WebViewPanel::create
or pulp::view::make_webview_embedded_resource_fetcher.
Root cause: core/view/CMakeLists.txt defaults PULP_BUILD_WEBVIEW=OFF,
so any release SDK build that forgets to opt in will ship a
libpulp-view / pulp-view.lib without the native WebView objects.
Fix (active since pulp #695): keep the release SDK path aligned with the GitHub release workflow:
-DPULP_BUILD_WEBVIEW=ON.libgtk-3-dev and libwebkit2gtk-4.1-dev before
configuring.WebViewPanel and make_webview_embedded_resource_fetcher.This applies to both .github/workflows/release-cli.yml and the local
helper tools/scripts/release-cli-local.sh. If one changes without the
other, GitHub releases and local release drills diverge.
Pulp's release automation depends on the pinned Shipyard CLI in two places:
tools/shipyard.toml is the source-of-truth pin for local installs and
shipyard pr..github/workflows/release-cli.yml and .github/workflows/post-tag-sync.yml
carry a SHIPYARD_VERSION env used by the release-side workflows.If you bump the Shipyard pin, update both workflows in the same PR and keep the
existing string format in each file (v0.29.0 in tools/shipyard.toml,
0.29.0 in the workflow envs — the v prefix is intentional only on the
toml). Otherwise local shipping and tag-time release jobs quietly diverge
onto different Shipyard versions, which is how release-only behavior
changes get missed.
sign-and-release.ymlPulp's notarized macOS release workflow clones the Steinberg VST3 SDK directly
inside .github/workflows/sign-and-release.yml. Keep that workflow pinned to
the same upstream tag used everywhere else in the repo: v3.7.12_build_20.
The shortened v3.7.12 ref does not exist on Steinberg's repo and causes the
tag-triggered macOS release job to fail immediately at Clone VST3 SDK, before
configure, build, or signing begin.
validation ctest tests in sign-and-release.yml (#720)The Test step in .github/workflows/sign-and-release.yml MUST pass
-LE validation to ctest. Without that flag, the suite includes
auval-Pulp* tests that copy a freshly-built .component to
$HOME/Library/Audio/Plug-Ins/Components/ and immediately invoke
auval -v aufx <code> Pulp. On hosted GitHub macOS runners the
AudioComponentRegistrar does not pick up the new bundle reliably, so
auval emits:
ERROR: Cannot get Component's Name strings
ERROR: Error from retrieving Component Version: -50
FATAL ERROR: didn't find the component
The Test step then exits non-zero and the entire sign / notarize / publish pipeline silently fails. This is the failure mode that lost ~30 consecutive sign-and-release runs across v0.20.x → v0.41.0 (see issue #720 for the full backlog).
The validation gates already run in .github/workflows/validate.yml
on PR with the documented codesigning caveat. Re-running them in the
release workflow on a runner that cannot satisfy the prereqs adds zero
protection and only adds a silent-failure surface.
tools/scripts/test_release_workflow_test_step.py is the regression
test that asserts -LE validation stays in the workflow; it is wired
into .github/workflows/workflow-lint.yml so any future PR touching
.github/workflows/** runs it automatically.
sign-and-release.yml must declare contents: write (#724)Every release workflow that uses softprops/action-gh-release@v2 with
generate_release_notes: true — or that otherwise PATCHes the release
entry — needs an explicit job-level permissions: contents: write
block. Without it the job inherits a read-only token on push: tags
events in many repo configurations, and the final Create GitHub Release step fails with:
Skip retry — your GitHub token/PAT does not have the required
permission to create a release
##[error]Resource not accessible by integration
Everything up to that point — checkout, VST3 SDK clone, configure, build, codesign, notarize, artifact upload — succeeds, and the pipeline still exits non-zero. macOS-signed artifacts never land on the release. Classic silent-release-failure pattern.
The fix is a four-line addition at the job header:
jobs:
build-and-sign-macos:
runs-on: macos-14
permissions:
contents: write
Cross-reference: release-cli.yml already sets this on its
release-creating job (line ~360). If you add a new release-time
workflow, do the same. The regression test
tools/scripts/test_release_workflow_test_step.py now includes
SignAndReleaseContentsWriteTest to block reintroduction.
pulp doctor validates Android toolchain: