# GitLab CI

`.gitlab-ci.yml` is the source of truth. The pipeline has four stages: `test`,
`build`, `checksum`, and `release`.

## Test jobs

Merge requests, the default branch, and tags run:

| Job | Responsibility |
|---|---|
| `test:linux` | tidy diff, race tests, coverage, Cobertura report, test-only browser fixture artifact |
| `vet` | `go vet` and gofmt freshness |
| `version-identity` | sentinel-linked binary and exact JSON identity |
| `frontend` | `npm ci`, embedded htmx/Alpine freshness, Vitest 5, native Chromium jobs/SSE integration, npm audit, Tailwind freshness |
| `govulncheck` | reachable Go vulnerability scan with a pinned scanner |

Go uses the repository's 1.27.0 floor. Node uses the Node 22 image. Go and npm
caches are scoped from lock/module inputs and the Git ref.

Every CI image, including the release CLI, is pinned to an immutable manifest
digest. Updating a base image is a reviewed dependency change: resolve the new
tag's amd64 manifest digest, update the human-readable tag and digest together,
then run the CI-contract and full test suites. Do not restore floating image
tags.

## Tag-only build jobs

A tag starts six CGO-free application builds:

- `build:linux-amd64`
- `build:linux-arm64`
- `build:windows-x86_64`
- `build:windows-arm64`
- `build:macos-intel`
- `build:macos-arm`

The leading `v` is stripped before linking `pkg/version.CurrentVersion`. The
Linux amd64 job executes the artifact and verifies exact product, component,
version, OS, and architecture. Native Windows/macOS execution remains a
separate release gate.

All tag-only jobs accept only canonical `vMAJOR.MINOR.PATCH` SemVer tags,
including valid prerelease/build suffixes, and require
`CI_COMMIT_REF_PROTECTED=true`. The tag policy job runs for every tag so an
invalid or unprotected tag fails visibly instead of merely omitting release
jobs. Configure `v*` as a protected-tag pattern in GitLab and restrict tag
creation to release maintainers.

Windows build jobs run the pinned `go-winres` v0.3.3 generator for the target
architecture, with the numeric release version, and fail if the resource
object is absent. After linking, CI extracts each PE's resources and verifies
the icon, Windows 10 minimum, and exact file/product versions. Both amd64 and
arm64 resource objects are committed so ordinary cross-builds remain usable.

Stable tag builds require `MANIFEST_PUBLIC_KEYS` and `MANIFEST_KEY_VALIDITY`;
prerelease tag builds require `RC_MANIFEST_PUBLIC_KEYS` and
`RC_MANIFEST_KEY_VALIDITY`. CI selects the matching channel before validation
and injection. RC builds never inherit the stable ring. An empty key ring blocks release builds; local
development builds may leave it empty and will reject network release metadata.
The tag pipeline also builds `generate-info` and `gen-checksums` for internal
CI use. `generate-info` produces an unsigned candidate; application-controlled
code never receives the manifest private key.

## Packaging and publication

Packaging produces:

| Platform | User archive | Raw updater artifact |
|---|---|---|
| Linux amd64 | `mpv-manager-<tag>-linux-amd64.tar.gz` | `mpv-manager-linux-amd64` |
| Linux arm64 | `mpv-manager-<tag>-linux-arm64.tar.gz` | `mpv-manager-linux-arm64` |
| Windows amd64 | `mpv-manager-<tag>-win-x86_64.zip` | `mpv-manager-win-x86_64.exe` |
| Windows arm64 | `mpv-manager-<tag>-win-arm64.zip` | `mpv-manager-win-arm64.exe` |
| macOS Intel | `mpv-manager-<tag>-macos-intel.zip` | `mpv-manager-macos-intel` |
| macOS arm64 | `mpv-manager-<tag>-macos-arm.zip` | `mpv-manager-macos-arm` |

Every archive includes `LICENSE` and `THIRD_PARTY_NOTICES.md`; packaging jobs
open the completed archives and assert both entries. The `checksums` job
produces GNU-style `BLAKE3SUMS.txt` and runs the documented
`b3sum -c BLAKE3SUMS.txt` command before publication. The
`publish:package-registry` job uploads raw binaries, archives, and checksums to
the GitLab generic package registry with bounded parallelism and retries.

Publication is serialized by the `stable-release-publication` resource group.
Before the first upload, the registry job requires HTTP 404 for every intended
raw artifact, archive, checksum, and `releases.json` path. Any existing object,
authorization error, or unexpected server response stops the release; a
partially published version must be investigated and explicitly recovered,
not overwritten. The manifest job repeats the 404 check immediately before its
own upload. GitLab's generic-package duplicate-file setting must also reject
overwrites as the server-side backstop.

The manual `native:evidence` job blocks registry publication. Configure its
`release/native-evidence` environment as protected and require release-owner
approval. The approver supplies an immutable HTTPS evidence URL and exact
SHA-256 digests for the four pipeline-local Windows/macOS binaries; the job
also requires matching tag and commit variables, recomputes all four digests,
and retains a one-year JSON binding record. This is a release approval control,
not a substitute for executing the artifacts on native systems.

`publish:release-manifest` then:

1. requires `release/provenance-<tag>.json`, whose reviewed versions, URLs,
   digests, verification methods, and evidence are bound to the tag version;
2. receives all six raw manager binaries directly from their build jobs and
   byte-compares the uploaded registry objects against them, then computes
   manifest sizes/hashes from those pipeline-local bytes;
3. runs `generate-info` without a signing key; unattended generation cannot
   query upstream “latest” APIs and rejects missing, extra, or mismatched pins.
   Fresh downloads use a provenance entry's reviewed `download_url` when present,
   so a replaced upstream asset does not break a clean runner. Original URLs remain
   the provenance/cache identity and every downloaded or cached file must still
   match the reviewed digest;
4. submits the unsigned candidate and lock to the isolated signer using a
   short-lived GitLab OIDC ID token scoped to `https://signing.mpv.rocks`;
5. independently verifies the returned Ed25519-signed document;
6. uploads it to the immutable tag-scoped package path and sends it to the
   stable-channel publication endpoint;
7. fetches the public document and re-verifies its signature and exact version.

Only then can the final `release` job create release notes. The publication
endpoint is responsible for validating authorization/signature/version and
atomically replacing the stable document; it must never expose a partially
written JSON file.

The v1.3 stable document also carries the legacy `manager` URL/BLAKE3 entries
and emits both `mpv-version` and the exact v1.1/v1.2 `MpvVersion` spelling.
Producer tests decode the signed JSON through a historical-client-shaped
structure. Do not remove those bootstrap fields until the supported upgrade
window explicitly excludes v1.1/v1.2.

## Isolated signer and protected release variables

The application project must not define `MANIFEST_SIGNING_KEY` or
`MANIFEST_SIGNING_KEY_ID`; the release job deliberately fails if either raw-key
variable is present. Configure these protected values before creating a tag:

| Variable | Value |
|---|---|
| `RELEASE_SIGNING_SERVICE_URL` | Isolated signer's candidate-submission endpoint; non-secret |
| `MANIFEST_PUBLIC_KEYS` | Stable `key-id=base64-public-key` trust ring |
| `RC_MANIFEST_PUBLIC_KEYS` | Separate RC public trust ring |
| `RC_MANIFEST_KEY_VALIDITY` | RC key validity epochs (`key-id=not-before:not-after`) |
| `RC_MANIFEST_REVOKED_KEY_IDS` | Optional revoked RC key IDs |
| `MANIFEST_KEY_VALIDITY` | Comma-separated `key-id=not-before-unix:not-after-unix` publication epochs; required by tag builds |
| `MANIFEST_REVOKED_KEY_IDS` | Optional comma-separated denylist embedded in new builds for emergency revocation |
| `RELEASE_MANIFEST_PUBLISH_URL` | Authenticated endpoint that atomically advances stable `releases/stable.json` |
| `RELEASE_MANIFEST_PUBLISH_TOKEN` | Masked/protected bearer token for that endpoint |
| `RELEASE_MANIFEST_VERIFY_URL` | Public stable document, normally `https://mpv.rocks/api/releases/stable.json` |

The protected native-evidence approval also requires:

| Variable | Value |
|---|---|
| `NATIVE_EVIDENCE_URL` | Immutable HTTPS record containing the native execution/signing evidence |
| `NATIVE_EVIDENCE_COMMIT_SHA` | Exact 40-hex commit SHA under review; must equal `CI_COMMIT_SHA` |
| `NATIVE_EVIDENCE_TAG` | Exact release tag; must equal `CI_COMMIT_TAG` |
| `NATIVE_WINDOWS_X86_64_SHA256` | SHA-256 of this pipeline's raw amd64 Windows artifact |
| `NATIVE_WINDOWS_ARM64_SHA256` | SHA-256 of this pipeline's raw arm64 Windows artifact |
| `NATIVE_MACOS_INTEL_SHA256` | SHA-256 of this pipeline's raw Intel macOS artifact |
| `NATIVE_MACOS_ARM_SHA256` | SHA-256 of this pipeline's raw arm64 macOS artifact |

The signing-service owner can generate a key offline from a separately reviewed
checkout/tool:

```bash
go run ./cmd/manifest-keygen \
  -key-id stable-2026-01 \
  -private-key-file /secure/offline/mpv-manager-stable-2026-01.seed
```

The command never prints the private seed, refuses to overwrite a file, writes
mode `0600`, and prints the key ID/public key. Import the private material into
the signing service's non-exportable key store; never transfer it into this
application project or leave it on a general-purpose runner.

The signer must validate the OIDC issuer/audience/project/protected-tag claims,
commit and pipeline IDs, expiry, and one-time `jti`; independently enforce the
provenance-lock schema/review policy and candidate-to-lock mapping; and return
only the signed manifest. It should have no general outbound network access.
See [Release Signing Service Contract](RELEASE_SIGNING_SERVICE.md).

Protected refs route every job through the project-only
`mpv-manager-protected` runner. Configure it as locked, protected, and unable
to accept untagged jobs. Its Docker executor runs inside a dedicated VM with
no host Docker socket, host checkout, SSH credentials, or signing keys mounted
into jobs. Ordinary unprotected validation retains the `docker` runner.
Before installing an offline signing approval, independently check the pipeline
API for the exact commit and runner identity of every prerequisite job, then
bind the signing job ID and runner ID in the approval. Job-provided headers
are not evidence of runner identity.

GitLab's generic-package duplicate-file policy must reject overwrites for the
release package/version. The preflight checks and serialized publication are
repository controls; server-side immutability prevents a later writer from
replacing an existing filename after it has been signed. Administrators can
still delete packages; deletion is a separate privileged recovery operation
and must never be used to republish a released version. The manifest job's post-upload
`cmp` remains a required same-pipeline provenance check.

For planned rotation, ship an application release whose `MANIFEST_PUBLIC_KEYS`
and `MANIFEST_KEY_VALIDITY` contain both old and new keys with non-overbroad
epochs, verify adoption, then sign a later manifest with the new key. Remove
the old key in a subsequent application release. If a signing key is
compromised, stop publication, revoke its publication credential, add its ID
to `MANIFEST_REVOKED_KEY_IDS` in the clean recovery build, distribute that
build through an independently controlled channel, and do not sign another
manifest with the compromised key. Clients also persist the highest accepted
stable publication/version and reject replay, rollback, and same-time
equivocation; release builds reject manifests predating their build time.

## Release candidates

See [release channels](RELEASE_CHANNELS.md) for signed stable/RC endpoints,
legacy v1.2 compatibility, separate RC publication credentials, final promotion,
and the Linode timer migration. The legacy timer must not overwrite signed
stable metadata after migration.

## Remaining release gates

The owner moved Windows Authenticode and Apple Developer ID/notarization to
v1.4.0 on 2026-09-06 while identity verification completes. v1.3.0 is an
unsigned platform release; manual QA must qualify its actual unsigned launch
and permission behavior. The separate Ed25519 updater manifest signer remains
a v1.3 prerequisite. See [v1.3 release readiness](RELEASE_READINESS_v1.3.0.md)
for the current scope and Atlas task mapping.

The current CI file does not perform Windows Authenticode or macOS
signing/notarization, nor does a Docker runner itself execute updater/relaunch/
rollback tests on `win-dev` and `macos-dev`. It now blocks publication on a
protected approval tied to exact artifact digests and immutable native
evidence. The external isolated signer, protected tag/environment policy,
duplicate-file rejection, a real reviewed tag lock, and the native-evidence
workflow must still be provisioned and exercised. The repeatable
current-protocol native matrix is complete; final artifact manual acceptance
and publication rehearsal remain v1.3 gates under Atlas task `b7ef5143`.
The protected-environments API returned 404 in the September 6 check; configure
and review an approval/attestation equivalent supported by this deployment if
the documented protected-environment feature is unavailable.

## Creating a release

After the local and native gates in [Testing](TESTING.md) pass:

1. copy `release/provenance.example.json` to
   `release/provenance-vX.Y.Z.json`;
2. replace every placeholder with exact independently verified values and
   immutable evidence, then merge that file through release approval;
3. create the matching protected tag only after the reviewed lock is on the
   tagged commit;
4. complete native Windows/macOS execution and signature-policy checks for the
   exact build-job bytes, publish the immutable evidence record, and approve
   `native:evidence` with the matching protected variables;
5. allow the serialized publication chain to continue only after that job
   emits its digest-binding artifact.

```bash
git tag -a v1.3.0 -m "Release v1.3.0"
git push origin v1.3.0
```

Do not manually rerun only the release job after a partial pipeline. Resume
or rerun from the failed prerequisite so artifacts, checksums, signatures, and
metadata all describe the same commit and tag.

---

See also: [Build and Deployment](BUILD_DEPLOYMENT.md) · [Self-update and Wails Review](SELF_UPDATE_AND_WAILS_REVIEW_2026-08-28.md) · [Project README](../README.md)

### Release descriptions

The release job uses `scripts/prepare-release-description.sh`. A reviewed
`release/notes-vX.Y.Z.md` becomes the complete GitGud release description;
otherwise `release/description-template.md` provides the tagged download and
verification instructions. The v1.3.0 notes include the full change list and
commit history since v1.2.0, upgrade guidance and accepted platform coverage.
