CI and releases
Five workflows cover testing, security scanning, on-demand builds, automatic daily updates, and PyPI publication. A template or fork can produce signed, verified Android artifacts without local installation or secrets, at cost of relying on public template key and public mirrors. User walkthrough: Build with GitHub Template.
Workflows
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml | push, pull request | Tests, bytecode compilation, lock validation |
security.yml | scheduled, push | Dependency and license scanning |
manual-verified-build.yml | workflow_dispatch, reusable call | Full verified build, optional publication |
daily-verified-build.yml | schedule | Detect upstream changes, build and publish only on change |
publish-pypi.yml | workflow_dispatch, pypi-v* tag push | Bump, tag, verify, publish to PyPI, and create generated GitHub release notes |
Continuous integration
ci.yml runs the standard-library test suite, bytecode compilation, and uv lock --check on Python 3.11 through 3.13 with Java 21. Python 3.11 is the minimum because configuration parsing uses standard-library tomllib.
astral-sh/setup-uv caches Python dependency downloads. CI receives no Google Play credentials and never performs a live download, so a pull request from a fork cannot reach a credential.
Google Play authentication
Configure GOOPDL_ACCOUNT_EMAIL and GOOPDL_AAS_TOKEN together under Settings → Environments → release → Environment secrets. Release jobs use direct account-backed Google Play access when both exist and verified configured fallbacks when anonymous access is unavailable.
A partial configuration fails closed. Pull-request CI receives neither credential and performs no live Google Play download. Release CI is not secret-free when account credentials are configured.
Token generation, local setup, detailed handling rules, and failure diagnosis: Google Play authentication.
Signing key
Manual verified build supports workflow_dispatch and reusable workflow calls, and works in a brand-new template repository with no signing secrets. It uses the committed .github/template-release.p12, alias morphe-builder, password morphe-builder.
Warning:
.github/template-release.p12and its password are public. Anyone can sign an APK that passes this certificate check. The key provides update compatibility, not publisher identity. Trust a release only when GitHub provenance identifies the expected repository and workflow; configure a private replacement keystore for publisher-specific trust.
Optional secrets override the defaults:
| Secret | Purpose |
|---|---|
MORPHE_KEYSTORE_BASE64 | Base64-encoded private replacement keystore. |
MORPHE_KEYSTORE_PASSWORD | Required with a replacement keystore. |
MORPHE_KEYSTORE_ALIAS | Replacement alias; defaults to morphe-builder. |
MORPHE_DISPENSER_URL | Custom anonymous dispenser, used only when no Google account is configured. |
Daily verified update
daily-verified-build.yml resolves each enabled app's effective Morphe CLI and patch source-version tuples from committed configuration, then compares resolved release tags and asset SHA-256 values against the last published APK provenance.
An unchanged toolchain is skipped without building. After a change it runs the full verified build, then still skips publication when the resulting APK version and output hash match the last release. This keeps the release history meaningful: a new release exists only when the artifact actually differs.
Concurrency is limited so only one live Google Play build runs at a time.
Python package publication
publish-pypi.yml publishes through PyPI Trusted Publishing; no API token is stored. Python package tags use the dedicated pypi-vX.Y.Z namespace, so unrelated project tags do not start this workflow. The isolated publish job receives only contents: read and id-token: write, downloads the distribution built by the unprivileged build job, and uses the protected pypi GitHub Environment. Only the preparation and GitHub Release jobs receive contents: write.
Configure the PyPI project or pending publisher once with:
| Setting | Value |
|---|---|
| PyPI project | morphe-builder |
| Owner | Villoh |
| Repository | morphe-builder |
| Workflow | publish-pypi.yml |
| Environment | pypi |
Release sequence:
- Either run Publish Python package from the target branch with bare version
X.Y.Z, or push provisional tagpypi-vX.Y.Zat current default-branch head. - Preparation validates version, updates
[project].versionplusuv.lockwhen needed, and runs tests plus clean wheel-install preflight before changing repository. - Manual runs commit changed version files and create tag. Tag-triggered runs create same bot commit and atomically move provisional tag to it. If metadata already matches, tag remains unchanged.
- Same run checks out resulting tagged commit, confirms tag and package metadata match, rebuilds both distributions from immutable source, and verifies wheel again. Bot pushes made with
GITHUB_TOKENdo not start duplicate workflows. - Publish job downloads exactly that build artifact and publishes it through OIDC.
- After PyPI succeeds, GitHub Release notes explicitly compare against nearest previous
pypi-v*tag, excluding daily and manual Android release tags from Full Changelog. First PyPI release uses short initial-release note instead of unrelated tag range.
Tag-triggered preparation rejects tags not pointing at current default-branch head. Any non-pypi-v* tag is ignored. After preparation moves provisional tag, never move or reuse it again.
Preparation needs permission to push release commit directly to selected/default branch. If branch protection blocks GITHUB_TOKEN pushes, merge version and lock-file bump through required review process before pushing matching tag; do not weaken branch protection merely for this workflow.
Package declares an exact goopdl release from PyPI. Standard pip and uv tool installs therefore resolve the reviewed downloader without a VCS override; version changes require an explicit dependency and lock-file update.
Android artifact publication
A default manual run builds and uploads a short-lived private workflow artifact without creating a GitHub release. Enable publish only after reviewing that artifact.
Release preparation reads the atomic build-summary.json, verifies every declared APK against its provenance, validates module ZIP structure, and writes release-manifest.json with size and SHA-256 for every staged file. The workflow passes a monotonic github.run_number into the module versionCode and the matching update JSON.
Publication then:
- Creates a draft release.
- Uploads APKs, modules, provenance, update JSON,
obtainium.json, Obtainiummanifest.html, manifest, and changelog. - Verifies the uploaded asset count.
- Requests GitHub artifact attestations.
- Makes the release public.
A failure deletes the draft, so an incomplete artifact matrix is never published. The workflow also rejects an existing release tag, a partial build matrix, modified APKs, unsafe module archives, and stale provenance.
The published release body (CHANGELOG.md) links obtainium.json for one-step import into Obtainium of every non-root APK. Its entries use supported HTML parsing against manifest.html to select each app's version and APK links. Obtainium uses <APK version>+morphe.<revision> as feed update identity; revision derives from verified non-root output hashes, so it changes for patch-bundle, patch configuration, and other output changes. It is not Android versionName; patch bundles may come from any configured repository. It also lists every app, version, and architecture with direct download links for the non-root APK, root APK, and module; records exact selected patches; links each resolved patch-bundle release changelog; and records non-root MicroG-RE and rooted zygisk-detach requirements for YouTube and YouTube Music.
Cache policy
Only public GitHub release metadata and tool assets are cached. Trusted APK payloads, dispenser data, authenticated URLs, tokens, and replacement keystore files are never cached and never uploaded as cache entries.
Template repositories may be public. Never store Google Play tokens, custom dispenser credentials, downloaded APKs, or private replacement keystores in Git. See security.md.
