Your Vibe-Coded App Still Needs a Trustworthy Release Path

Over the last few months, more people have been asking me the same kind of question: I built a thing. How do I ship it?

Sometimes it is a small Mac utility. Sometimes it is a script that grew into an app. Sometimes it is a project that started as an AI-assisted experiment and turned into something other people might actually download.

That is the point where the conversation has to change. While you are building for yourself, the risk is mostly yours. Once you publish a binary, app, package, archive, or release asset for other people to run, you have taken on a different responsibility.

The question is not only “does it work?” The question becomes: how can someone trust that the thing they downloaded is the thing you intended to ship?

This is an integrity problem

The classic cybersecurity triad is confidentiality, integrity, and availability. Confidentiality is about keeping information from the wrong people. Availability is about making sure systems and data are usable when needed. Integrity is about making sure something has not been changed in an unauthorized or unexpected way.

Software release trust sits heavily in the integrity part of that triad.

If I download your app, I want to know a few things:

  1. Did this come from you?
  2. Was it changed after you built it?
  3. Did the operating system accept it as signed or notarized?
  4. Can I compare what I downloaded against a known value?
  5. If it was built in CI, can I trace it back to the workflow that produced it?

Those questions sound abstract until someone downloads the wrong file, runs an unsigned build, or gets a warning that makes the whole project feel sketchy. That is why signing, notarization, checksums, and attestations exist. They are not busywork. They are ways to preserve and communicate integrity.

Manual Release Is the First Trust Boundary

Every application has to be released for the first time, and for many developers that process is completely manual. There’s nothing inherently wrong with that. In fact, for a personal project or a new application, I’d rather understand every step of the release process myself before I automate it.

A typical manual release starts with building the application locally, signing it with the appropriate Developer ID certificate, submitting it to Apple for notarization, stapling the notarization ticket when appropriate, packaging the final artifact, and then uploading it to GitHub Releases along with release notes.

Because every step is performed manually, I like to validate each one before moving to the next. After building the application, I’ll confirm that the binary was signed with the expected identity:

codesign --verify --deep --strict --verbose=2 MyApp.app

codesign -dv --verbose=4 MyApp.app

Once Apple has notarized the application, I’ll verify that Gatekeeper accepts it on a clean Mac:

spctl --assess --type execute --verbose MyApp.app

If the application was distributed as a DMG or installer package, I’ll also confirm that the notarization ticket was stapled successfully:

xcrun stapler validate MyApp.dmg

Before uploading anything, I like to generate a checksum for the exact file I’m about to publish. That gives both me and anyone downloading the release a way to confirm they’re working with the same artifact.

shasum -a 256 MyApp.dmg

Finally, I take one last look at the artifact itself before it ever reaches GitHub. Is this the final notarized build? Did I accidentally package an earlier test version? Is the version number correct? If I downloaded this release six months from now, would I be confident that I could identify exactly how it was produced?

Those questions are easy to answer while the release is still sitting on my workstation. They’re much harder to answer after the files have been uploaded and people have already started downloading them.

The biggest mistake I see isn’t that releases are performed manually. It’s treating a manual release as nothing more than dragging a file into GitHub and assuming that’s enough. The moment someone else downloads and runs your software, they’re placing trust in the release process. Even if every step is performed by hand, that process should be deliberate, repeatable, and easy to validate long after the release has been published.

Notarization Is Not the Same as Trust, but It Matters

When people first start distributing Mac applications, it’s easy to think of notarization as the finish line. Once Apple accepts the submission, the assumption is often that the application is now “trusted.” In reality, notarization is only one part of establishing that trust.

Apple’s notarization service doesn’t review your application’s architecture, evaluate your coding practices, or determine whether the software is appropriate for a particular environment. What it does provide is an automated validation that the application was properly signed, successfully passed Apple’s notarization process, and can be recognized by macOS as software that’s appropriate for distribution.

From a technical perspective, I like to verify each of those steps myself rather than simply assuming they completed successfully. After signing an application, I’ll confirm the code signature is valid:

codesign --verify --deep --strict --verbose=2 MyApp.app

After notarization has completed, I’ll verify that Gatekeeper accepts the application:

spctl --assess --type execute --verbose MyApp.app

If I’m distributing a DMG or installer package, I’ll also confirm that the notarization ticket was successfully stapled to the artifact:

xcrun stapler validate MyApp.dmg

Those commands don’t tell me whether the application is well written or whether it will meet a user’s expectations. They simply confirm that the release satisfies the technical requirements macOS uses when evaluating software from an identified developer.

The reason this matters is because users experience trust long before they experience features. An unsigned application, an “unidentified developer” warning, or a Gatekeeper prompt that doesn’t behave as expected immediately changes a user’s perception of the software. In many organizations, it also creates unnecessary support requests because users are no longer sure whether they’re supposed to proceed.

That’s why I think of signing and notarization as part of the overall release process rather than a checkbox at the end of development. They aren’t a substitute for secure coding, thorough testing, or responsible development practices, but they are an important part of delivering software that integrates cleanly with the macOS security model. If you’re asking other people to download and run your application, taking the time to validate those steps is simply part of the responsibility of shipping software.

Automation Makes the Release Repeatable

Once I’m comfortable with the manual release process, the next step is usually automation. By that point I already know how to build the application, sign it, notarize it, package it, and publish it. Automating those steps isn’t about changing the process—it’s about making the same process repeatable.

A GitHub Actions workflow is a common way to accomplish that. Instead of relying on the state of my local Mac, the workflow can build the application, sign it using credentials stored as GitHub Secrets, submit it to Apple for notarization, package the final artifact, and publish a release automatically whenever I intentionally trigger it.

The benefit isn’t simply that the release becomes faster. It’s that every release follows the same sequence of steps. If I release version 1.0 today and version 1.5 six months from now, I want both releases to be produced in exactly the same way.

That consistency doesn’t happen automatically, though. The workflow deserves the same level of review that I’d apply to any other piece of code.

For example, I want to understand exactly what permissions the workflow has:

permissions:
  contents: write

If a workflow only needs to publish a release, it probably doesn’t need broader repository permissions.

I also want to verify that signing and notarization credentials are being referenced securely instead of being embedded directly into the workflow:

env:
  APPLE_ID: $
  APPLE_TEAM_ID: $
  APPLE_APP_PASSWORD: $

When reviewing the workflow, I also pay attention to how it is triggered. During development I may choose to run it manually:

on:
  workflow_dispatch:

Later, when I’m confident in the process, I might allow releases to be triggered from version tags instead:

on:
  push:
    tags:
      - "v*"

Another area worth reviewing is the use of third-party GitHub Actions. Rather than referencing a moving branch, I prefer to pin actions to a specific release or commit so the workflow behaves predictably over time.

- uses: actions/checkout@v4

Finally, I want to confirm that the artifact uploaded to GitHub Releases is the exact artifact the workflow just built, signed, notarized, and validated—not an earlier build that happened to be sitting in the workspace. If necessary, I’ll even generate and publish a checksum as part of the workflow so anyone downloading the release can verify they’re using the same file that the automation produced.

Automation doesn’t automatically make a release more secure, but it can make it far more consistent. A well-designed workflow reduces manual mistakes, produces repeatable builds, and documents the release process in a way that’s easy to review and improve over time. Like any other automation, though, it earns trust through careful design rather than simply because it’s automated.

Checksums Help Users Confirm the File

A checksum is one of the simplest ways to help users verify that the file they downloaded is the same one you intended to publish. It isn’t a complicated security feature, and it doesn’t require any special tooling beyond what’s already available on most operating systems.

Before publishing a release, I like to generate a SHA-256 checksum for the final artifact:

shasum -a 256 MyApp.dmg

The output will look something like this:

7a7c97d5d52fd3d4e4efc93d4c1f91c0d22a6d3f44d2e9b1d4e2db6b58c8d1f9  MyApp.dmg

I can include that value in the release notes or publish it alongside the release as a separate checksum file. After downloading the application, a user can generate the checksum on their own system:

shasum -a 256 MyApp.dmg

If the calculated value matches the one I published, both of us can answer a very specific question with confidence:

Did I download the same bytes the publisher intended to distribute?

That’s an important distinction because a checksum only verifies file integrity. It doesn’t tell anyone whether the application is well written, whether it contains vulnerabilities, or whether the developer’s account has ever been compromised. Those are completely different questions that require different forms of trust.

What a checksum does provide is an easy way to detect accidental corruption during download, incomplete transfers, or situations where a file has unexpectedly changed. If the values don’t match, users immediately know they’re not working with the same artifact that was originally published.

For projects that use GitHub Actions, generating a checksum can even become part of the release workflow:

shasum -a 256 MyApp.dmg > MyApp.dmg.sha256

That checksum file can then be uploaded alongside the release asset, giving anyone who downloads the application an easy way to verify the integrity of the file before installing it.

It’s a small step that takes only a few seconds, but it’s an easy way to make a release a little more transparent and give users one more piece of information they can validate for themselves.

Artifact attestations answer a different question

GitHub artifact attestations become useful when the release is built in GitHub Actions.

They do not replace signing. They do not replace notarization. They do not replace checksums. They do not mean “this app is safe.”

They answer a provenance question:

Did this artifact come from the GitHub repository and workflow I expected?

That is especially useful when a project publishes binaries, packages, archives, containers, or generated release assets. If the project creates an attestation during the release workflow, a downloader can verify that the artifact is tied back to the expected GitHub build.

The workflow shape looks like this:

permissions:
  contents: read
  id-token: write
  attestations: write

steps:
  - uses: actions/checkout@v4

  - name: Build artifact
    run: ./build.sh

  - name: Attest build provenance
    uses: actions/attest-build-provenance@v2
    with:
      subject-path: dist/example.tar.gz

That is the provenance part of the release. The workflow still needs a real build, signing, notarization, packaging, and release upload process around it.

As a Publisher, Make Verification Obvious

Publishing a trust signal is only useful if people know it exists and understand how to verify it. That’s something I think is easy to overlook when setting up an automated release pipeline.

For example, if a GitHub Actions workflow generates an artifact attestation, I don’t want that to be a hidden implementation detail buried somewhere in the workflow definition. Most users are never going to read the YAML file, so they won’t even know the attestation exists.

Instead, I prefer to include clear verification instructions wherever people are likely to download the release, whether that’s the release notes, the project README, or the installation documentation.

A release note might include something as simple as:

This release includes a GitHub artifact attestation.

After downloading MyApp.dmg, verify its provenance with:

gh attestation verify MyApp.dmg --repo owner/repository

That gives someone a concrete next step instead of expecting them to discover the process on their own. They can download the release, run the verification command, and confirm that the artifact’s provenance is associated with the expected GitHub repository.

If you’re documenting the release process, it’s also helpful to show how the verification works from a terminal:

gh attestation verify MyApp.dmg \
  --repo owner/repository

Assuming the attestation validates successfully, the GitHub CLI will report that the artifact’s provenance matches what was published by the repository.

Of course, that only works if an attestation was actually generated and published as part of the release process. If the project doesn’t publish one, there isn’t anything meaningful for a downloader to verify using this command. That doesn’t automatically mean the release is untrustworthy—it simply means artifact attestations aren’t part of that project’s release process.

The broader lesson is that security features shouldn’t be hidden. If you’ve taken the time to sign your application, notarize it, publish checksums, or generate artifact attestations, make those trust signals visible. The easier you make verification for your users, the more likely they are to take advantage of it.

The Release Trust Ladder I Would Use

If I were starting with a brand-new application—even one that began as a weekend project or something I built with the help of AI—I wouldn’t try to implement every release security feature at once. Instead, I’d build confidence in the release process one layer at a time, making sure I understand each step before adding the next.

The first goal is simply producing consistent builds. If I can’t build the same application twice and understand exactly what changed between releases, everything that follows becomes more difficult to explain and troubleshoot. A repeatable build process becomes the foundation for every other trust signal.

Once the build is predictable, the next step is code signing. Before I distribute anything, I want to confirm that the application or installer is signed with the expected Developer ID certificate.

codesign --verify --deep --strict --verbose=2 MyApp.app

codesign -dv --verbose=4 MyApp.app

After signing, I move on to notarization. If I’m distributing software outside the Mac App Store, I want macOS to recognize that the application successfully completed Apple’s notarization process. Once notarization is complete, I’ll verify that Gatekeeper accepts the application before I publish it.

spctl --assess --type execute --verbose MyApp.app

xcrun stapler validate MyApp.dmg

The next layer is publishing a checksum with every release. It only takes a few seconds to generate a SHA-256 checksum, but it gives anyone downloading the application a way to verify that they’re working with the exact file I intended to distribute.

shasum -a 256 MyApp.dmg > MyApp.dmg.sha256

Only after I’m comfortable performing those steps manually do I automate them. By then I already understand the release process, so the workflow isn’t hiding complexity—it is repeating a process I’ve already validated.

For example, I might manually trigger the release workflow while I’m gaining confidence in it:

gh workflow run release.yml

Eventually, I may decide to have releases created automatically whenever I push a version tag, but I want to earn that level of automation rather than adopting it immediately.

If GitHub Actions is producing the release artifacts, the next layer I would add is artifact attestations. That gives users a way to verify the provenance of the file produced by the CI pipeline instead of simply trusting that it came from the correct repository.

gh attestation verify MyApp.dmg \
  --repo owner/repository

Not every project needs every layer on the first day. A personal utility with a handful of users doesn’t need the same release process as a widely distributed commercial application. As more people begin relying on your software, though, each layer helps answer another question about where the application came from and why users should trust the file they’re about to run.

Why This Matters

One thing I’ve learned over the years is that releasing software is about much more than producing a successful build. Building an application is only the beginning. The real responsibility starts when someone else downloads that application and decides whether they’re willing to run it.

That applies whether you’re publishing a small utility on GitHub, distributing an internal application through your MDM, or maintaining an open-source project with thousands of users. Every release asks someone else to place a certain amount of trust in your work. The more transparent you can make the release process, the easier it becomes for users to understand why that trust is justified.

That’s why I don’t think of code signing, notarization, checksums, and artifact attestations as competing technologies. Each one answers a different question, and together they create a more complete picture of the release.

For example, I can verify that an application was signed by the expected developer:

codesign --verify --deep --strict --verbose=2 MyApp.app

I can confirm that macOS recognizes the application as successfully notarized:

spctl --assess --type execute --verbose MyApp.app

I can compare the published SHA-256 checksum against the file I downloaded:

shasum -a 256 MyApp.dmg

And if the project publishes artifact attestations, I can verify that the release artifact originated from the expected GitHub repository and workflow:

gh attestation verify MyApp.dmg \
  --repo owner/repository

None of those commands prove that an application is free of bugs, immune from vulnerabilities, or deserving of unconditional trust. Software quality, secure development practices, and responsible maintenance all remain essential parts of the equation.

What these technologies do provide is evidence. Code signing helps identify who produced the software. Notarization allows macOS to recognize software that has successfully completed Apple’s distribution process. Checksums allow users to confirm that the file they downloaded hasn’t changed. Artifact attestations add another layer by providing provenance for artifacts produced through a CI workflow.

Viewed individually, each trust signal answers only part of the story. Together, they make the release process significantly more transparent and much easier to validate.

As more developers adopt GitHub Actions and AI-assisted development tools, I think that transparency becomes increasingly important. Automation allows us to ship software faster than ever before, but it also makes it easier to lose sight of how a release was actually produced. The more information we can provide about that process—and the easier we make it for users to verify it themselves—the more confidence they’ll have in the software they’re choosing to install.

Sources

AI Usage Transparency Report

AI Era · Written during widespread use of AI tools

AI Signal Composition

Rep Tone Struct List Instr
Repetition: 65%
Tone: 65%
Structure: 65%
List: 7%
Instructional: 20%
Emoji: 0%

Score: 0.3 · Moderate AI Influence

Summary

A guide to building trust in software releases through signing, notarization, checksums, and attestations.

Related Posts

Setting up Ollama on macOS

Recently, after some bad experiences with OpenAI's ChatGPT and CODEX, I decided to look into and learn more about running local AI models. On its face it was intimidating, but I had seen a lot of people in the MacAdmins community posting examples of macOS setups, which really helped lower the bar for me both in terms of approachability and just making me more aware of the local AI community that exists out there today.

Read more

AI Agent Constraints and Security

I really feel like in this era of AI it's essential to write about and share experiences for others who are leveraging AI, especially now that AI usage seems almost ubiquitous. Specifically, when it comes to AI in development and the rapid growth of AI-driven automations in the IT landscape, I believe there's a need for open discussion and exploration.

Read more

Vibe Coding with Codex: From Fun to Frustration

So there I was, a typically day, a typical weekend. As a ChatGPT customer, I had heard good things about Codex and had not yet tried the platform. To date my experience with agentic coding was simply snippit based support with ChatGPT and Gemeni where I would ask questions, get explanations and support with squashing bugs in a few apps that I work on, for fun, on the side. There were a few core features in one of the apps I built that I wanted to try implementing but the...

Read more