Release Process
This project publishes packages to PyPI and Windows Package Manager (winget), plus standalone Windows and macOS binaries, via GitHub Actions. Pushing a git tag that follows the pattern vX.Y.Z triggers the automated release workflow.
Release Workflow
The release process is mostly automated. Pushing the tag runs the PyPI, EXE, macOS CLI, and changelog workflows; the winget submission is a separate manual step.
1. PyPI Release (release-pypi.yml)
- Build the wheel and source distribution using
python -m build - Import a GPG key from the
GPG_PRIVATE_KEYrepository secret - Sign all files in
dist/withgpg --detach-sign - Upload the package to PyPI via the Trusted Publishers flow (OIDC)
- Attach the artefacts and their signatures to the GitHub release page
- Generate
SHA256SUMS.txtfor verification
2. Windows EXE Build (release-exe.yml)
Three jobs: build → sign → publish.
1. Build a standalone dji-embed.exe using PyInstaller and download the
just-published wheel from PyPI (windows runner)
2. Authenticode-sign the exe via the reusable certum-sign.yml workflow
(see "Code signing" below)
3. Generate SHA256SUMS.txt, attest build provenance (Sigstore), and attach
everything to the GitHub release — both happen after signing because
signing changes the file hashes
2b. Windows Installer (release-installer.yml)
Five jobs: build → sign-app → assemble → sign-installer → publish.
1. Build dji-embed.exe, publish the Avalonia GUI, stage pinned
FFmpeg/ExifTool + licenses (windows runner)
2. Sign dji-embed.exe and DjiEmbed.Gui.exe
3. Compile the Inno Setup installer over the signed binaries and smoke-test
a silent install (also asserts both signatures are valid); the
Inno-generated uninstaller is knowingly unsigned (compile-time-only)
4. Sign the setup EXE
5. Checksums + attestation + release upload
2c. macOS CLI + App Build (release-macos.yml)
Three jobs: build (arm64 macOS runner) → app → publish.
1. Build a standalone dji-embed binary with PyInstaller (embedded dylibs
signed with the Developer ID identity so hardened-runtime library
validation passes at runtime), codesign the binary with hardened runtime
+ secure timestamp, smoke-test --version, then notarize the zip with
notarytool (the submit-and-poll loop is scripts/macos-notarize.sh)
2. Assemble DJI Metadata Embedder.app: dotnet publish the Avalonia GUI
(self-contained osx-arm64), bundle it with the build job's already
signed CLI (byte-identical to the zip asset, never re-signed) via
tools/macos_bundle.py, sign inside-out with the allow-jit-only
entitlements (installer/macos/entitlements.plist), then notarize and
staple the app and again the DMG — two real notary submissions
3. Generate SHA256SUMS-macos.txt covering both assets (a separate file —
this leg races the Windows leg on the same release, and same-named
assets clobber), attest build provenance (Sigstore), and attach
dji-embed-macos-arm64.zip +
dji-metadata-embedder-<version>-macos-arm64.dmg to the GitHub release
A bare binary has nowhere to staple the notarization ticket, so a user's
first run needs network for Gatekeeper's online check. Apple credentials
live in the APPLE_* repository secrets; after rotating any of them, run
the manual Mac sign test workflow (mac-sign-test.yml) — it proves the
whole chain without cutting a release. Same-repo PRs touching this leg run
the full build + sign + notarize as a check, skipping only the publish job.
Code signing (Certum SimplySign)
Signing runs headlessly on Linux inside the pinned
ghcr.io/callmarcus/dji-drone-metadata-embedder/certum-signer container
(built by signer-image.yml, sources in docker/certum-signer/), driven by
the .github/actions/certum-sign composite action through the reusable
certum-sign.yml workflow. Repository secrets: CERTUM_USER_ID,
CERTUM_OTP_URI, and optionally CERTUM_CERT_FINGERPRINT (SHA-256 pin).
After changing any signing piece or bumping the pinned SimplySign version,
run the manual Sign test workflow (sign-test.yml) — it signs an
existing release exe end-to-end without cutting a release.
3. Winget Submission (release-winget.yml) — manual
This workflow is not triggered by the release tag. It only runs via
workflow_dispatch and must be started by a maintainer once the PyPI/EXE
release has completed:
gh workflow run release-winget.yml -f version=X.Y.Z # portable CLI
gh workflow run release-winget.yml -f version=X.Y.Z -f package=desktop # desktop installer
The desktop run submits the separate CallMarcus.DJIMetadataEmbedder.Desktop
package (manifests in winget/desktop/). Only submit it for releases whose
installer is code-signed (v1.23.0+) — see winget/README.md.
When run it will:
1. Verify version sync using python tools/sync_version.py --check
2. Install wingetcreate tool for manifest submission
3. Submit the updated manifest to microsoft/winget-pkgs
4. Create a pull request for community review
4. Auto-Changelog (auto-changelog.yml)
- Generate changelog entries from conventional commits
- Update
CHANGELOG.mdwith new version section - Commit changes back to the repository
Creating a Release
1. Prepare the Release
# Update to new version across all files
python tools/sync_version.py 1.2.0
# Update unreleased changelog section (optional)
python scripts/update-unreleased.py
# Review all changes
git diff
2. Create and Push Tag
# Commit version updates
git add .
git commit -m "release: prepare version 1.2.0"
# Create and push tag
git tag v1.2.0
git push origin v1.2.0
3. Monitor Workflows
The following workflows will run automatically:
- ✅ PyPI Release - Package uploaded to PyPI
- ✅ Windows EXE - Standalone executable built and attached (triggered by the PyPI workflow completing)
- ✅ Windows Installer - Signed GUI installer built and attached (also triggered by the PyPI workflow completing)
- ✅ macOS CLI + App - Notarized arm64 binary + app DMG built and attached (also triggered by the PyPI workflow completing)
- ✅ Auto-Changelog - Changelog updated from commits
Winget Submission is a separate manual step — see Winget Integration below.
Version Synchronization
The tools/sync_version.py script keeps version numbers consistent across:
src/dji_metadata_embedder/__init__.py(source of truth)README.md(version badge)tools/bootstrap.ps1(fallback version)dji-embed.spec(PyInstaller spec)winget/*.yaml(Windows Package Manager manifests)
Winget Integration
The project maintains local winget manifests in the /winget directory —
two package sets (see winget/README.md):
- Package IDs:
CallMarcus.DJIMetadataEmbedder(portable CLI, monikerdji-embed) andCallMarcus.DJIMetadataEmbedder.Desktop(desktop installer,winget/desktop/, v1.23.0+ signed releases only) - Submission: Manual via
gh workflow run release-winget.yml -f version=X.Y.Z(add-f package=desktopfor the installer; not auto-triggered by the release tag) - Community review: PRs created in microsoft/winget-pkgs
Users can install via:
winget install CallMarcus.DJIMetadataEmbedder # portable CLI
winget install CallMarcus.DJIMetadataEmbedder.Desktop # desktop app + tools