Releasing
Versioning, release notes, the GitHub workflows, what each release carries, and the secrets they need.
Workflows
| Workflow | Runs on | Does |
|---|---|---|
CI (ci.yml) | every push to main and every pull request | Lints (ruff) and tests (pytest) the server, builds the client and checks that every manifest carries the version from VERSION, builds and validates the wheel. |
Pre-release (prerelease.yml) | every push to main | Builds everything, including the mobile apps, and publishes a GitHub pre-release named after the short commit SHA with all the artifacts attached. Nothing is uploaded to a store. |
Release (release.yml) | by hand from the Actions tab, or every pushed tag | Refuses to run unless the tag matches VERSION and release-notes/<VERSION>.md exists; then builds everything, uploads the mobile apps to TestFlight and the Google Play internal track, and publishes a stable GitHub release with the notes as its body. |
Mobile (mobile.yml) | called by the two above, or by hand from the Actions tab | Builds the signed IPA, APK, and AAB. Uploads to the stores only when the upload input is set, which the Release workflow does and a manual run can. |
Docs (docs.yml) | every push to main and every pull request | Builds this site, checks every link and anchor, verifies the Settings Reference is current, and (on main) deploys it to GitHub Pages. |
Cutting a release
- Set the new version in
VERSION(for example0.4.0). - Write
release-notes/0.4.0.md. It becomes the body of the GitHub release, so it is user-facing: what changed, what to do when upgrading. - Regenerate the Settings Reference if settings changed, and update the documentation for anything user-visible.
- Merge to
mainand let CI and the pre-release run. The pre-release is the release candidate: its extension zips, APK, and IPA are what you test. - Cut the release as described below, with the version as the tag (
0.4.0).
The Release workflow checks the tag against VERSION and the notes file
first, so a mismatch fails before anything is tagged, built, or uploaded.
Releases are cut by running the
Release
workflow from the Actions tab, Run workflow with Use workflow from on
main and the tag, or by pushing the tag. A run releases the commit it
started from: a new tag is created on it once the checks pass, and a tag that
already exists has to be on it, so an earlier commit's tag is released by a
run started from that tag. The latest input decides the "Latest" badge:
auto gives it to a release and not to a pre-release, never holds it back
for a fix to an older series, and always gives it to a pre-release as well.
The "Latest" badge is also the release LinuxServer.io's image builds, since
that image takes releases/latest, so a fix to an older series released with
never reaches no image. The stores refuse a build number they already have,
so a release the stores already received fails at the upload.
What a release carries
| Asset | Built by |
|---|---|
sealskin-chrome-v<VERSION>.zip, sealskin-firefox-v<VERSION>.zip | browser_extension/build.sh |
sealskin-v<VERSION>.apk, sealskin-v<VERSION>.ipa | mobile/build.sh and the iOS job |
sealskin_server-<VERSION>-py3-none-any.whl | python -m build --wheel server/ with the UI copied into the package |
sealskin-ui-v<VERSION>.tar.gz | tar of client/dist/ui |
Stores
- Chrome Web Store and Firefox Add-ons submissions are manual: upload the zips from the release. A store release is only needed when the shell itself changed (manifest, bridge protocol, background script), because the UI ships with the server.
- TestFlight and Google Play receive every stable release automatically
from the Mobile workflow, and any manual run with
uploadticked. Promotion from TestFlight and from the internal track to production is done in App Store Connect and the Play Console.
The store build number is the commit count on the branch plus an offset, so
it only ever goes up across workflows; the iOS marketing version is
2.<build> and is deliberately independent of VERSION. Both are derived in
the Mobile workflow's first job.
Secrets
The Mobile workflow needs these repository secrets:
| Secret | Used for |
|---|---|
ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD | Signing the APK and AAB. Without them build.sh produces a debug-signed APK. |
PLAY_SERVICE_ACCOUNT_JSON | Uploading the AAB to the Google Play internal track. |
IOS_DIST_CERT_P12, IOS_DIST_CERT_PASSWORD, IOS_PROVISIONING_PROFILE | The Apple Distribution certificate and the App Store provisioning profile for the bundle id. The workflow verifies that the profile was issued against that certificate. |
APPSTORE_KEY_ID, APPSTORE_ISSUER_ID, APPSTORE_PRIVATE_KEY | The App Store Connect API key used by altool to upload the IPA. |
The Docs workflow needs GitHub Pages enabled with GitHub Actions as the
source; it uses the built-in GITHUB_TOKEN. CI, Pre-release, and Release need
nothing beyond contents: write for creating releases.
Release notes
release-notes/<VERSION>.md is required for a stable release and is written
by hand. Keep the tone of the existing notes: a short lead on what the
release means for someone running SealSkin, then Highlights, per-component
sections where useful, and an Upgrading section whenever an operator or
a user has to do something.