Releasing Codex
Every push to the release branch runs
.github/workflows/release.yml. It builds installers for
macOS, Windows, Linux, FreeBSD and the web, signs the ones it has
certificates for, and creates or updates a draft GitHub
Release named Codex v<version>. Nobody sees a draft
until a maintainer publishes it.
The pipeline was ported from Picto's. User-facing names say
Codex; files, binaries and ids stay lowercase
(codex-<version>-<platform>-<arch>.<ext>,
com.ncdev.codex).
Cutting a release
Bump the version on
main. It lives only in[workspace.package] versionin the rootCargo.toml:cargo xtask version # prints the current version cargo xtask version set 0.3.0 # or 0.3.0-rc.1; updates Cargo.toml and Cargo.lockCommit the change through the normal review flow (see the
Release: Codex v0.2.1PR).Merge
mainintorelease(or fast-forward it) and push. The workflow starts by itself.Wait for the draft. When every job has finished (notarization is the slow part), the Releases page has a draft
Codex v0.3.0targeting the pushed commit, with every artifact andSHA256SUMS.txt. The notes are generated from the merged PRs.Check it. Download an installer or two and read the job summaries. A
::warning::there means a signing secret was missing and that artifact is unsigned.Publish the draft in the GitHub UI. Publishing creates the
v0.3.0tag. Versions with a pre-release suffix (-rc.1) are marked as pre-releases.
Pushing to release again before you publish rebuilds the
same draft and replaces its assets. Once the draft is published, the
workflow refuses to touch that version again, so bump it first.
Test runs: Actions ▸ Release ▸ Run workflow
runs the whole pipeline by hand. The optional version input
(such as 0.3.0-rc.1) overrides Cargo.toml for
that run only; each job applies it with
cargo xtask version set before building, so the binaries
report it too. The signing jobs (macOS, Windows) and the draft-release
job use the release environment, so pick the
release branch in the dialog for a full run. On any other
branch the same dispatch is a dry run: the Linux,
Flatpak, FreeBSD and web jobs build and check everything, the signing
jobs are refused by the environment's branch rule, and the draft-release
job (which needs them) is skipped, so nothing is published
(gh workflow run release.yml --ref <branch>).
What gets built
| Platform | Artifacts | Built on |
|---|---|---|
| macOS 11+ (universal: Apple silicon + Intel) | codex-<v>-macos-universal.dmg,
codex-cli-<v>-macos-universal.zip |
macos-15 |
| Windows 10+ x64 | codex-<v>-windows-x64.msi,
codex-<v>-windows-x64-portable.zip |
windows-latest |
| Windows 10+ x86 (32-bit) | codex-<v>-windows-x86.msi,
codex-<v>-windows-x86-portable.zip |
windows-latest |
| Windows 11 on ARM64 | codex-<v>-windows-arm64.msi,
codex-<v>-windows-arm64-portable.zip |
windows-latest (cross-compiled) |
| Linux x86_64 | codex-<v>-linux-x86_64.{AppImage,AppImage.zsync,deb,rpm,tar.gz},
codex-cli-<v>-linux-x86_64.tar.gz |
ubuntu-22.04 |
| Linux aarch64 | codex-<v>-linux-aarch64.{AppImage,AppImage.zsync,deb,rpm,tar.gz},
codex-cli-<v>-linux-aarch64.tar.gz |
ubuntu-22.04-arm |
| Linux riscv64 | codex-<v>-linux-riscv64.tar.gz,
codex-cli-<v>-linux-riscv64.tar.gz (glibc 2.39+) |
ubuntu-24.04 (cross-compiled; CLI smoke-tested under
QEMU) |
| Flatpak x86_64 | codex-<v>-linux-x86_64.flatpak |
ubuntu-24.04 (repackages the Linux tarball) |
| Flatpak aarch64 | codex-<v>-linux-aarch64.flatpak |
ubuntu-24.04-arm (repackages the Linux tarball) |
| FreeBSD 14 x86_64 | codex-<v>-freebsd-x86_64.tar.gz |
FreeBSD VM on ubuntu-latest |
| Web | codex-web-<v>.zip (a static site; see packaging/web/README.md) |
ubuntu-latest |
The ARM64 Windows build is cross-compiled on the x64 runner, so
signing and WiX work as for the other Windows builds;
.github/workflows/windows-arm64.yml installs and runs it on
ARM64 hardware.
Every binary reports its version (codex --version,
codex-cli --version, Help ▸ About). The workflow
sets CODEX_BUILD_SHA and CODEX_BUILD_DATE
(packaging/env.sh fills them in for local builds): the
commit is recorded in the macOS Info.plist
(CodexBuildCommit) and the date in the AppStream metadata.
The binaries don't embed the commit yet.
Fonts: every job checks out craft-fonts at the
commit pinned in release.yml and builds with
CRAFT_FONTS_DIR and CRAFT_FONTS_REQUIRED=1, so
releases embed its Japanese fonts and fail rather than ship without them
(AGENTS.md §1.4). Bump the pin deliberately.
OCR models: every desktop package ships the Scan
& OCR models (#103). The packaging scripts call
stage_models (packaging/env.sh;
package.ps1 on Windows), which runs
cargo xtask models to fetch every
kind = "model" file in ATTRIBUTION.toml
(verified by SHA-256) with its licence text and an
ATTRIBUTION.txt, straight into the package, so packaging
needs the network. They go where
codex_ocr::Models::dirs_beside_exe looks:
models\ beside codex.exe (MSI, portable zip),
Codex.app/Contents/Resources/models (macOS), and
share/codex/models beside bin/ (deb, rpm,
tar.gz, AppImage, Flatpak, FreeBSD). The web build doesn't include them
yet.
macOS
packaging/macos/package.sh builds both architectures,
joins them with lipo, and assembles Codex.app
from Info.plist.in (bundle id com.ncdev.codex,
macOS 11+, PDF declared as a document type with rank Alternate, so Codex
is offered under Open With without taking over from Preview). Files
opened from Finder arrive as Apple events, which
apps/codex/src/apple_events.rs receives.
- Signing uses the hardened runtime and a secure
timestamp, executable first, then the bundle.
packaging/macos/import-cert.shputs the certificate in a temporary keychain, deleted at the end of the job. - Notarization: the app is zipped and sent with
xcrun notarytool submit --wait, the ticket is stapled, and the result is checked withcodesign --verify,stapler validateandspctl. The app ships on a drag-to-Applications DMG, which is signed and notarized too. Its Finder window (background, icon size and positions) comes frompackaging/macos/dmg/, and its volume is namedCodexwithout the version, which the window's background needs; the DMG file name keeps the version. - CLI:
codex-cliis signed and notarized as a zip. A bare executable can't hold a stapled ticket, so Gatekeeper looks it up online the first time a downloaded copy runs.
Without certificates (locally) the script signs ad-hoc and skips notarization:
packaging/macos/package.sh --arch aarch64 # quicker, host-only; --arch universal needs both targets
Windows
packaging/windows/package.ps1 -Arch x64|x86|arm64 builds
with -C target-feature=+crt-static, so neither the MSI nor
the portable zip needs the Visual C++ redistributable.
- Before packaging, it reads both executables' PE headers: the machine
type must match
-Arch, andcodex.exemust be a GUI-subsystem program (no console window, #57) whilecodex-cli.exestays a console program. codex.wxs(WiX v5) installs per machine into Program Files with a Start Menu shortcut, a desktop shortcut (on by default) and an App Paths entry. Both shortcuts are plain links tocodex.exe, not advertised MSI shortcuts (#107, #143). The MSI version is the numericX.Y.Z(MSI has no pre-release field), and same-version upgrades are allowed so release candidates replace each other. Icon ids end in.icoor.exe(Windows Installer requires it); packaging-lint checks this.installer-ui.wxssupplies native welcome, maintenance, progress, files-in-use and outcome dialogs. Full UI confirms success with Finish; failures and cancellations have distinct messages. The welcome dialog has a "Create a desktop shortcut" checkbox and an "Edit with Codex" right-click menu checkbox, both ticked by default./qnand/qbstay unattended and create both unlessINSTALLDESKTOPSHORTCUT=0orINSTALLPDFCONTEXTMENU=0is passed;INSTALLIMAGECONTEXTMENU=0(no checkbox) leaves out the image menu. An update (the launcher runsmsiexec /passivewith no properties) keeps the desktop shortcut only ifCodex.lnkis still on the all-users desktop, so deleting it once keeps it away (#1058), and keeps each menu only if its key path is still in HKLM (#305). Both are AppSearch results read before the old version is removed. An update from a release that didn't have a menu yet (before 0.7.1 for the PDF menu, before 0.4.0 for the image menu, found byOnlyDetectUpgrade rows) adds it like a first install. A value given on the command line wins.- The PDF right-click entry (#682) is a classic verb under
HKLM\Software\Classes\SystemFileAssociations\.pdf\shell\Codex.Edit, so it shows whichever app is the default for.pdfand never changes that default. Several selected PDFs (up to 15, the limit for aDocumentverb) start onecodex.exe "<file>"each, and the single-instance hand-off opens them as tabs. On Windows 11 it is under "Show more options"; a first-level entry needs anIExplorerCommandhandler and package identity (a sparse MSIX package), which the MSI doesn't have. - The image right-click entry (#190) is the same kind of verb,
Codex.CreatePdf, for ten image types, in its own component (CodexImageContextMenu) since it became optional. Its rows moved out ofCodexApp; that's safe becauseMajorUpgraderemoves the old version before installing the new one (afterInstallValidate), whichtest-msi.ps1checks. test-msi.ps1 <file.msi>checks the compiled shortcut, context-menu and dialog tables, and evaluates the first-install and update conditions of the desktop shortcut and both menus. Packaging runs it in a child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install smoke test checks that both all-users shortcuts point at the installedcodex.exeand are removed on uninstall, and thatINSTALLDESKTOPSHORTCUT=0skips the desktop one.- The portable zip holds both executables, the README, the licences,
the OFL licence of each embedded craft-fonts family, and
portable.txt. That marker besidecodex.exekeeps the settings, logs, crash recovery and new digital IDs inCodexData\next to the exe instead of%APPDATA%and%LOCALAPPDATA%(crates/ui-egui/src/portable.rs). - Signing:
packaging/windows/sign.ps1signs both executables and the MSI withsigntool(SHA-256, RFC 3161 timestamp), from a.pfx(WINDOWS_CERTIFICATE) or Azure Trusted Signing (AZURE_*), whichever is configured. It is the one place to change when signing changes.
Locally: dotnet tool install -g wix --version 5.0.2,
then pwsh packaging/windows/package.ps1 -Arch x64.
Linux
packaging/linux/package.sh stages one FHS tree (both
binaries, the desktop entry, hicolor icons, AppStream metainfo) and
makes every format from it: an AppImage (any
distribution, nothing to install), a .deb and an
.rpm (built with nfpm from nfpm.yaml;
they integrate with the menu, MIME and icon caches), a
.tar.gz, and a CLI-only
codex-cli-<v>-linux-<arch>.tar.gz
(the stripped codex-cli plus the licences and README, for
servers, CI and agent sandboxes). --formats picks a subset
(appimage deb rpm tar cli).
The binaries are built on Ubuntu 22.04, the oldest GitHub-hosted
image, so they need only glibc ≥ 2.35: Ubuntu 22.04+,
Debian 12+, Fedora 36+, RHEL 10. Windowing (X11, Wayland, xkbcommon) and
the GPU (Vulkan, EGL) are loaded at runtime from the system; the .deb
and .rpm declare them as dependencies (see nfpm.yaml).
Each AppImage embeds update information
(gh-releases-zsync|storytold|codex|latest|codex-*-linux-<arch>.AppImage.zsync),
and its .zsync is published beside it, so AppImageUpdate
and AppImageLauncher can update it in place, downloading only the
changed blocks. latest is the newest published,
non-pre-release version. package.sh writes the
.zsync when zsyncmake (the zsync
package) is installed and warns otherwise; the release job checks
both.
Flatpak: the flatpak job turns each
arch's tarball into a single-file bundle with
packaging/linux/flatpak-bundle.sh and
flatpak/com.ncdev.codex.bundle.yml (no Rust build; the same
binaries), then installs it and runs codex-cli --version in
the sandbox. flatpak/com.ncdev.codex.yml is the from-source
manifest for a later Flathub submission; packaging-lint keeps the
runtime and sandbox permissions (finish-args) of the two
identical. Printing is not available in the Flatpak yet: it runs
lp, which the freedesktop runtime lacks (the job logs a
note); it needs the print portal. Users install the bundle with
flatpak install --user codex-<v>-linux-<arch>.flatpak;
the freedesktop runtime comes from Flathub.
Locally (on Linux, with nfpm):
packaging/linux/package.sh or
--formats "deb tar"; then
packaging/linux/flatpak-bundle.sh for the Flatpak.
FreeBSD
GitHub has no FreeBSD runners, so the freebsd job runs
packaging/freebsd/package.sh in a FreeBSD VM with the
packages from .github/workflows/freebsd.yml. The tarball is
a /usr/local-style tree with the same desktop entry,
metainfo and icons as Linux. FreeBSD has no code signing for loose
binaries; check the tarball against SHA256SUMS.txt.
Web
packaging/web/package.sh runs
trunk build --release in apps/codex-web and
zips the site with sample _headers and
.htaccess files. Hosting (MIME types, compression, caching,
iframes) is covered in packaging/web/README.md.
Secrets
The signing secrets live in the repository's
release environment (Settings ▸
Environments ▸ release), restricted to the release
branch; every job in release.yml declares
environment: release. Each secret is optional: if one is
missing, that platform's artifacts are unsigned and the run shows a
warning.
| Secret | Used for |
|---|---|
APPLE_CERTIFICATE,
APPLE_CERTIFICATE_PASSWORD |
base64 Developer ID Application .p12 and its
password |
KEYCHAIN_PASSWORD |
the temporary CI keychain (random if unset) |
APPLE_ID, APPLE_PASSWORD,
APPLE_TEAM_ID |
notarytool (an app-specific password) |
WINDOWS_CERTIFICATE,
WINDOWS_CERTIFICATE_PASSWORD |
base64 .pfx code-signing certificate and its
password |
AZURE_TENANT_ID, AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET |
service principal for Azure Trusted Signing (instead of a
.pfx) |
AZURE_SIGNING_ENDPOINT,
AZURE_SIGNING_ACCOUNT, AZURE_CERT_PROFILE |
Trusted Signing endpoint, account and certificate profile |
GITHUB_TOKEN creates the release; only the final job
gets contents: write.
Checks
.github/workflows/packaging-lint.yml runs on changes to
packaging/ and the workflows: actionlint, shellcheck, a
PowerShell parse, xmllint (WiX, plist, MIME),
desktop-file-validate, appstreamcli validate,
and the MSI icon-id check. The FreeBSD and Windows ARM64 workflows also
exercise their packaging before a release does.
Scanner detections
Antivirus and reputation scanners sometimes flag a released file.
Check the facts before you change code or answer publicly. The first
report was issue #747,
a match of the CobaltStrikeBeacon family rule.
Confirm the file is the release. Compare its SHA-256 with
SHA256SUMS.txton the GitHub Release. Only a file whose hash matches the release says anything about that release.Record the signature.
packaging/windows/security-audit.ps1prints the Authenticode status:Valid: the signature verifies.NotSigned: no signature, as in developer and test builds. Not evidence of compromise by itself.HashMismatch: the file changed after it was signed. Investigate.NotTrustedorUnknownError: a signature exists but did not validate on this machine. Compare the signer and the hash with the release.
Extract the payloads without running them. Scan the executables inside the MSI and the portable zip, not only the outer installer.
7z x -oDIR codex-<version>-windows-x64.msiextracts them. The payload names are WiX-internal, so tell the executables apart by their file type. Don't install or run the MSI or the extracted files.Scan with YARA and keep every match. Use your own copies of YARA and the rule. Codex bundles neither, and the helper never runs in the app, the installer or CI:
pwsh packaging/windows/security-audit.ps1 -Path .\codex.exe, .\codex-cli.exe -YaraExe C:\tools\yara64.exe -YaraRule C:\rules\CobaltStrikeBeacon.yarThe output lists each matched identifier with its offset, and the SHA-256 of the rule file. Record where the rule came from and its revision. For CAPEv2's
CobaltStrikeBeaconrule, that is the commit that last changeddata/yara/CAPE/CobaltStrikeBeacon.yar.Find the condition branch. Family rules usually have alternative branches, such as
all of ($x*) or 2 of ($a*) or .... Copy the rule, make one rule per branch with only the strings it uses, and scan again to see which branch fired.Interpret with care. A match on generic repeated-byte patterns says much less than a match on distinctive code or strings, and it does not show that the software belongs to the family. Rules change too: CAPEv2 commit
9b599185(2024-12-05) lengthened this rule's two repeated-byte patterns from 4 to 16 bytes to reduce false positives, so an older rule revision can match files that the current one does not. Don't alter Codex's binaries to change a scanner's result. That changes nothing about the software.Report. To the scanner, send the file's SHA-256, the release URL, the rule name and revision, and the matched identifiers and offsets. Report Codex problems at https://github.com/storytold/pdfcraft/issues with the same facts.