Someone recently asked me how to notarize a Mac app for manual distribution.
The first answer was Xcode. If the app is being built locally, Xcode can archive it, sign it, submit it to Apple’s notarization service, and return a distributable build. It is a good place to learn the flow because the moving parts stay visible: Apple identity, signing certificate, archive, notarization request, result, and distribution package.
Then they mentioned they were already using GitHub. If the release artifact is already being built in GitHub Actions, notarization does not have to remain a separate manual step on someone’s Mac. It can become part of the release workflow. I showed them lando/notarize-action, a GitHub Action for submitting a built macOS app or plug-in to Apple’s notarization service.
The concern was credentials. Putting Apple Developer credentials into GitHub and letting a third-party action submit a release artifact should get scrutiny. The review had to answer a few practical questions: what does the action run, what credentials does it need, what GitHub permissions does the job require, and how closely does the action stay to the notarization process I would run manually?
The README, action metadata, and built runtime file made the boundary clear enough for a scoped release workflow.
What the action is actually doing
The README describes the action pretty plainly: it submits a built .app or non-app bundle to Apple’s notarization service. It uses notarytool by default, with altool still available as an option.
The basic example from the README is this:
- name: Notarize Release Build
uses: lando/notarize-action@v2
with:
product-path: "/dist/MyApp.app"
appstore-connect-username: $
appstore-connect-password: $
appstore-connect-team-id: FY8GAUX283
The snippet shows the trust boundary. The action needs the app path, an App Store Connect username, a password value, and the Apple Developer team ID. The username and password are passed from GitHub Actions secrets, not hardcoded into the workflow file.
The team ID is not the same kind of secret as a password, but it still belongs in a secret or repository variable. Keeping it out of the YAML also avoids sprinkling account identifiers through examples that get copied later.
The README also says notarization is not the final step. After Apple accepts the submission, the release process still needs to staple the notarization ticket to the product before distribution. The action is not pretending to own the whole release process. It is handling the notarization submission.
The action metadata is small enough to review
The next file I checked was action.yml. That file tells GitHub what inputs the action accepts and what entrypoint it runs.
The required inputs match the README:
inputs:
product-path:
description: "The path to the product to notarize."
required: true
appstore-connect-username:
description: "The AppStore Connect username."
required: true
appstore-connect-password:
description: "The AppStore Connect password."
required: true
appstore-connect-team-id:
description: "The Apple Developer account Team ID. Only required when using notarytool."
required: true
The runtime declaration is just as important:
runs:
using: "node20"
main: "dist/index.js"
GitHub runs the built JavaScript file at dist/index.js under Node 20. For a JavaScript action, the built entrypoint matters more than the friendly source file because action.yml defines what GitHub executes.
One detail stood out. The metadata includes optional appstore-connect-api-key and appstore-connect-api-issuer inputs, but the runtime path I checked reads the username, password, team ID, product path, bundle ID, tool, and verbose setting. For this workflow, the username/password/team-id path shown in the README is the verified path. API key authentication needs separate verification before relying on those inputs.
The runtime wraps Apple tooling instead of inventing its own process
The runtime behavior is narrow. The action does not need broad GitHub repository access. It does not try to publish a release, push a tag, comment on a pull request, or mutate the repository.
It reads the inputs:
const configuration = {
productPath: core.getInput('product-path', {required: true}),
username: core.getInput('appstore-connect-username', {required: true}),
password: core.getInput('appstore-connect-password', {required: true}),
primaryBundleId: core.getInput('primary-bundle-id'),
tool: core.getInput('tool'),
verbose: core.getInput('verbose') === 'true',
};
configuration.team = core.getInput('appstore-connect-team-id', {
required: configuration.tool === 'notarytool'
});
Then it checks that the product path exists. A missing or incorrectly named artifact fails before anything is submitted to Apple.
Before submitting to Apple, the action archives the product with ditto:
const archivePath = '/tmp/archive.zip';
const args = [
'-c',
'-k',
'--keepParent',
productPath,
archivePath,
];
The .app at product-path becomes /tmp/archive.zip on the runner.
The default notarytool path then calls Apple’s command-line tooling:
const args = [
'notarytool',
'submit',
archivePath,
'--apple-id', username,
'--password', password,
'--team-id', team,
'--output-format', 'json',
'--wait',
];
The action is not creating an opaque notarization path. It wraps xcrun notarytool submit and waits for Apple’s result, close to the same process I would explain from Xcode or from the command line.
It also has understandable failure behavior. If the product path is wrong, it fails. If the configured tool is not supported, it fails. If notarytool returns a non-zero exit code, it parses the JSON response and reports the status and message.
The action still carries risk, but the risk is explainable.
Pin the action before using it in release
The README uses the normal version-tag form:
uses: lando/notarize-action@v2
The version tag is convenient, and it is what most people will copy first. A real release workflow should pin the action to the reviewed commit SHA instead.
At the time I checked it, the v2 tag resolved to:
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed
Pinning does not make the action safe by itself. It makes the reviewed code the code that runs. Updating the action becomes a deliberate diff review instead of a silent tag movement.
Wire it into a release workflow, not every build
Do not run notarization on every push or pull request. It belongs in a deliberate release workflow after the app has already been built and signed.
A minimal GitHub Actions shape would look like this:
name: Notarize macOS App
on:
workflow_dispatch:
permissions:
contents: read
jobs:
notarize:
runs-on: macos-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
# Build and sign your app before this step.
# The notarize action expects the finished .app path.
- name: Notarize Release Build
uses: lando/notarize-action@b5c3ef16cf2fbcf2af26dc58c90255ec242abeed
with:
product-path: "dist/MyApp.app"
appstore-connect-username: $
appstore-connect-password: $
appstore-connect-team-id: $
The permissions: contents: read line is intentional. This job does not need broad repository write permissions just to submit an already-built app to Apple. If another job creates a GitHub release or uploads release assets, that job can carry the permission it needs.
The workflow_dispatch trigger is also intentional. A manual release trigger is a good starting point before moving notarization behind a tag-driven release. Once the workflow is proven, a release tag trigger is a smaller change.
Where the Apple credentials should go
The Apple credentials belong in GitHub Actions secrets or protected environment secrets, not in the workflow file.
For the README-style path, I would create:
NOTARIZATION_USERNAME
NOTARIZATION_PASSWORD
NOTARIZATION_TEAM_ID
NOTARIZATION_USERNAME is the Apple ID or App Store Connect username used for notarization.
NOTARIZATION_PASSWORD should be an Apple app-specific password, not the normal interactive account password. That matters because an app-specific password can be revoked without changing the account’s main password.
NOTARIZATION_TEAM_ID is the Apple Developer team ID used with notarytool.
In GitHub, these go under Settings > Secrets and variables > Actions. If the repository has environments, put them behind a release environment and require approval before the job can access them. The environment gate adds a deliberate approval step before Apple credentials are exposed to a runner.
Leave verbose mode off unless actively troubleshooting:
verbose: false
The action metadata says verbose mode prints notarization API responses. Verbose output can help during a failure, but release logs should not contain extra credential-adjacent output by default.
Stapling comes after notarization
Submitting the app to Apple is not the last step.
After the action reports a successful notarization, the release workflow still needs to staple the ticket to the app or disk image so the notarization ticket travels with the product when someone downloads it outside the Mac App Store.
Keep the release sequence obvious:
# 1. Build the app.
# 2. Sign the app.
# 3. Notarize the signed app.
# 4. Staple the notarization ticket.
# 5. Package or upload the release artifact.
The GitHub workflow can make the process safer than a remembered manual sequence. If the build fails, notarization never runs. If notarization fails, stapling never runs. If stapling fails, publishing should not run.
Prefer native commands when the wrapper is unnecessary
The reviewed action was understandable and usable. It still should not become the automatic default forever.
Some Marketplace actions are wrappers around commands that are now straightforward to run directly on GitHub’s macOS runners. Some older related actions linked around this space are stale or no longer maintained. Stale links do not prove why those actions stopped being maintained, but they are a signal worth respecting. If the platform and runner already provide the command, adding a third-party dependency needs a reason.
For notarization, the native path is not a special GitHub-provided “notarize” action. It is the Apple tooling already available in the macOS build environment: xcrun notarytool for submission and xcrun stapler for stapling.
For simple workflows, native commands are the better default. Use the Marketplace action if it removes real complexity and has been reviewed. Use native commands when they are readable, supported, and easy to keep in the workflow.
Secrets are not optional plumbing
The bigger rule is credential handling. Whether I use the Marketplace action or call notarytool myself, the Apple credentials do not belong in the workflow file.
GitHub Secrets provide controls that plain YAML cannot. They keep the value out of source control. They allow credential changes without rewriting the workflow. They reduce accidental disclosure in logs because GitHub treats configured secrets as sensitive values. Environment secrets also put release credentials behind an environment gate instead of exposing them to every workflow run.
The failure mode without secrets is ugly. A password hardcoded into .github/workflows/release.yml is now part of git history. Even if I remove it later, I have to assume it was exposed and rotate it. A credential placed in a normal variable is easier to print by accident. A credential passed through a debug-heavy workflow can leak through logs if it is transformed or echoed in a way the platform cannot mask.
For this workflow, use:
NOTARIZATION_USERNAME
NOTARIZATION_PASSWORD
NOTARIZATION_TEAM_ID
NOTARIZATION_PASSWORD should be an Apple app-specific password. Do not put a normal interactive Apple ID password into CI. An app-specific password is still sensitive, but it is scoped and revocable in a way that fits automation better.
If the repository supports environments, put these under a release environment and require approval before the job can access them. The environment requirement turns notarization into a deliberate release step instead of a secret that any matching workflow run can touch.
A native notarization workflow
For a simple app, the preferred workflow uses GitHub Actions with Apple’s native commands directly.
There is no extra action to pin, no third-party runtime to inspect, and no wrapper maintenance question. The workflow still needs review, but the important parts are the runner, the Apple commands, and the secrets.
name: Notarize macOS App
on:
workflow_dispatch:
permissions:
contents: read
jobs:
notarize:
runs-on: macos-latest
environment: release
steps:
- name: Check out repository
uses: actions/checkout@v4
# Build and sign your app before notarization.
# Replace this with the project's real build/archive command.
- name: Build signed app
run: |
xcodebuild \
-scheme "MyApp" \
-configuration Release \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
archive
# Export or copy the signed .app to dist/MyApp.app before this point.
- name: Create notarization archive
run: |
ditto -c -k --keepParent "dist/MyApp.app" "$RUNNER_TEMP/MyApp.zip"
- name: Submit app for notarization
env:
NOTARIZATION_USERNAME: $
NOTARIZATION_PASSWORD: $
NOTARIZATION_TEAM_ID: $
run: |
xcrun notarytool submit "$RUNNER_TEMP/MyApp.zip" \
--apple-id "$NOTARIZATION_USERNAME" \
--password "$NOTARIZATION_PASSWORD" \
--team-id "$NOTARIZATION_TEAM_ID" \
--wait
- name: Staple notarization ticket
run: |
xcrun stapler staple "dist/MyApp.app"
xcrun stapler validate "dist/MyApp.app"
The Marketplace action is still a useful example because it shows the same process in wrapper form: archive the app, call notarytool, wait for the result. Once the native commands are this readable, the native workflow should be the default. A third-party action has to remove enough complexity to justify the dependency.
The concern about credentials was valid. The fix is not to avoid automation. The fix is to make the release path explicit: secrets in GitHub’s secret manager, narrow job permissions, native Apple notarization and stapling commands, and no publishing step unless every earlier step succeeds. A workflow with those boundaries is easier to review than a manual release process that only one person remembers how to run.
Sources
- lando/notarize-action Marketplace page
- lando/notarize-action README
- lando/notarize-action action.yml
- lando/notarize-action reviewed v2 commit
- Apple Developer Documentation: Notarizing macOS software before distribution
- Apple notarytool manual page
- Apple stapler manual page
- GitHub Docs: Using secrets in GitHub Actions
- GitHub Docs: Installing an Apple certificate on macOS runners for Xcode development
- GitHub Docs: Secure use reference
AI Usage Transparency Report
AI Era · Written during widespread use of AI tools
AI Signal Composition
Score: 0.29 · Moderate AI Influence
Summary
A GitHub Action for notarizing macOS apps using Apple's notarization service.
Related Posts
Automating Script Versioning, Releases, and ChatGPT Integration with GitHub Actions
Managing and maintaining a growing collection of scripts in a GitHub repository can quickly become cumbersome without automation. Whether you're writing bash scripts for JAMF deployments, maintenance tasks, or DevOps workflows, it's critical to keep things well-documented, consistently versioned, and easy to track over time. This includes ensuring that changes are properly recorded, dependencies are up-to-date, and the overall structure remains organized.
When a Local AI Tool Belongs in My Workflow and When It Stays in the Lab
Running AI locally on a Mac has become a real part of my workflow, but only once I stopped treating local models like general-purpose answers and started treating them like constrained components inside a system I can still inspect.
Scoring AI Influence in Jekyll Posts with Local LLMs
There’s a moment that kind of sneaks up on you when you’ve been writing for a while, especially if you’ve started using AI tools regularly. You stop asking whether AI was used at all, and instead start wondering how much it actually shaped what you’re reading. That shift is subtle, but once you notice it, you can’t really unsee it.
Leaving Flickr: Migrating 20,000+ Photos to Synology and Taking Back Control
There’s a certain kind of friction you start to notice when you’ve been using a service for a long time. Not enough to make you leave immediately, but enough to make you pause. Flickr had been that kind of service for me. It quietly held years of photos, uploads from old phones, albums I hadn’t looked at in ages, and a massive "Auto Upload" collection that had grown into something I didn’t fully understand anymore.
Running Image Generation Locally on macOS with Draw Things (2026)
Local LLMs have rapidly evolved beyond text and are now capable of producing high-quality images directly on-device. For users running Apple Silicon machines—especially M-series Mac Studios and MacBook Pros—this represents a major shift in what’s possible without relying on cloud services. Just a few years ago, image generation required powerful remote GPUs, subscriptions, and long processing times. Today, thanks to optimized models and Apple’s Metal acceleration, you can generate and edit images locally with impressive speed and quality. The result is a workflow that is faster, private, and entirely under...
Cleaning House in Jamf Pro: A Friendly Auditor Script for Real-World Hygiene
There’s a tipping point in every Jamf Pro environment where the policy list begins to feel like a junk drawer. Everyone means well. Nobody deletes anything. And then, months later, you’re trying to answer simple questions like: *Which policies are actually scoped? What’s no longer referenced? Why are there five versions of the same script?* This post covers a small, practical script I wrote to help you **see** what’s stale, **explain** why it’s stale, and (optionally) **park** it safely out of the way—without deleting a thing.
Turn Jamf Compliance Output into Real Audit Evidence
Most teams use Apple’s macOS Security Compliance Project (mSCP) baselines because they scale and they’re repeatable. Jamf’s tooling makes deployment straightforward and the Extension Attribute (EA) output is a convenient place to capture drift. What you don’t automatically get is the artifact an auditor will accept on a specific date—an actual document you can file that shows which endpoints are failing which items, plus a concise roll-up of failure counts you can act on. Smart Groups answer scope; they don’t produce evidence.
10 Things You Didn't Know You Could Do With Apple Configurator (That Save Mac Admins Hours)
Most of us treat Apple Configurator like a fire extinguisher: break glass, DFU, restore, move on. But it can do a lot more, and when you know the edges, you can turn a bricked morning into a ship-it afternoon. Below are ten things I regularly use (or wish I’d used sooner) that demonstrate its capabilities beyond just emergency recovery.
The Power of Scripting App Updates Without Deploying Packages
Keeping macOS environments up-to-date in a seamless, efficient, and low-maintenance way has always been a challenge for IT admins. Traditional package deployment workflows can be time-consuming, prone to versioning issues, and require extensive testing and repackaging. This can lead to frustration and wasted resources as IT teams struggle to keep pace with the latest updates and patches. But there's another way—a more elegant, nimble approach: scripting.
Detecting Invalid Characters and Long Paths in OneDrive on macOS
Microsoft OneDrive is widely used for syncing documents across devices, but on macOS, it can silently fail to sync certain files if they violate Windows filesystem rules — like overly long paths or invalid characters. This creates frustrating experiences for end users who don’t know why files aren’t syncing.