diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c82c52c..84c2822 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,13 @@ # Every change to FieldCraft needs the maintainer's review before it merges. * @jyviko + +# Release and supply-chain paths, named explicitly so they stay owner-reviewed if the rule above +# is ever relaxed: workflows and secrets use, packaging and signing, the updater, dependencies, +# licences. +/.github/ @jyviko +/packaging/ @jyviko +/crates/update/ @jyviko +/Cargo.toml @jyviko +/Cargo.lock @jyviko +/ATTRIBUTION.md @jyviko +/deny.toml @jyviko diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 73a4824..258447f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,6 +33,10 @@ env: jobs: build: + # POSTHOG_KEY lives in the `release` environment (main only, docs/releasing.md). The deploy job + # already uses github-pages and a job has one environment, so the build job reads the key: on + # main it runs in `release`, elsewhere in no environment and with an empty key. + environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }} runs-on: ubuntu-24.04 timeout-minutes: 60 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0debb4..5efb9c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,6 +73,9 @@ jobs: macos: name: macOS universal needs: version + # Signing, notarization and analytics secrets live in the `release` environment, which only + # `main` may use; a dry run on another branch asks for no environment and builds unsigned. + environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }} runs-on: macos-14 timeout-minutes: 120 outputs: @@ -118,6 +121,9 @@ jobs: name: Linux x86_64 needs: version # The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35. + # Signing, notarization and analytics secrets live in the `release` environment, which only + # `main` may use; a dry run on another branch asks for no environment and builds unsigned. + environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }} runs-on: ubuntu-22.04 timeout-minutes: 90 env: @@ -201,6 +207,9 @@ jobs: windows: name: Windows x64 needs: version + # Signing, notarization and analytics secrets live in the `release` environment, which only + # `main` may use; a dry run on another branch asks for no environment and builds unsigned. + environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }} runs-on: windows-latest timeout-minutes: 90 env: @@ -230,6 +239,9 @@ jobs: release: name: Draft release (or dry-run artifacts) needs: [version, macos, linux, windows] + # Signing, notarization and analytics secrets live in the `release` environment, which only + # `main` may use; a dry run on another branch asks for no environment and builds unsigned. + environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }} runs-on: ubuntu-24.04 permissions: contents: write diff --git a/docs/privacy.md b/docs/privacy.md index 21526f6..3725a06 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -86,5 +86,5 @@ A `command_run` as sent: The events and the policy are `fieldcraft_engine::usage`; the consent question and the settings are `crates/ui-egui/src/privacy.rs`; the desktop sender is `apps/fieldcraft/src/usage.rs`, the web one `apps/fieldcraft-web/src/usage.rs`. The PostHog project key is the build-time variable -`FIELDCRAFT_POSTHOG_KEY`, which the release workflow and the Pages build of `main` set from the repository secret +`FIELDCRAFT_POSTHOG_KEY`, which the release workflow and the Pages build of `main` set from the `release` environment secret `POSTHOG_KEY`; it is an ingest-only key. A new event or property is added here first. diff --git a/docs/releasing.md b/docs/releasing.md index 86fe54d..aa8c96a 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -51,7 +51,8 @@ about it, and the release notes say which of the two the draft contains. | `APPLE_PASSWORD` | An app-specific password for that Apple ID (appleid.apple.com › Sign-In and Security) | | `APPLE_TEAM_ID` | The 10-character team id of the Developer ID | -Add them with `gh secret set -R jyviko/fieldcraft`. With the certificate but without the +Add them to the `release` environment ([Where the secrets live](#where-the-secrets-live)) with +`gh secret set --env release -R jyviko/fieldcraft`. With the certificate but without the three notarization secrets the app is signed and not notarized. A dmg made locally with `packaging/macos/package.sh` is ad-hoc signed unless `MACOS_SIGN_IDENTITY` names an identity in the keychain. @@ -66,7 +67,7 @@ untrusted comment: minisign public key: FEF410EFEF5BAB21 RWQhq1vv7xD0/uAOt8fqERP1hP+1UCM9U9QHTDYTYytWKiuedTkT7Lgr ``` -The secret half is the repository secret `FIELDCRAFT_SIGNING_KEY`: the whole secret key file +The secret half is the `release` environment secret `FIELDCRAFT_SIGNING_KEY`: the whole secret key file (two lines), made without a password. With it, the release job signs `SHA256SUMS.txt` with [rsign2](https://github.com/jedisct1/rsign2) (pinned in `release.yml`), checks the signature against the public key in the source, and uploads `SHA256SUMS.txt.minisig`. Without it the @@ -79,7 +80,7 @@ Setting it up, once: ```sh cargo install --locked rsign2 --version 0.6.7 rsign generate -W -p fieldcraft-release.pub -s fieldcraft-release.key # -W: no password -gh secret set FIELDCRAFT_SIGNING_KEY -R jyviko/fieldcraft < fieldcraft-release.key +gh secret set FIELDCRAFT_SIGNING_KEY --env release -R jyviko/fieldcraft < fieldcraft-release.key ``` Keep an offline copy of the secret key file (a password manager, an encrypted backup) and @@ -101,9 +102,39 @@ certificate is added. ## Usage analytics key The release builds (macOS, Linux and Windows jobs) build in the PostHog project key from the -repository secret `POSTHOG_KEY` as `FIELDCRAFT_POSTHOG_KEY` ([privacy.md](privacy.md)). Without the -secret the builds ask nothing and send nothing, as local and fork builds do. The web build on -Pages has no key. +`release` environment secret `POSTHOG_KEY` as `FIELDCRAFT_POSTHOG_KEY` ([privacy.md](privacy.md)). +Without the secret the builds ask nothing and send nothing, as local, fork and dry-run builds do. +The web build on Pages gets the key only when it runs from `main`: its build job then runs in the +`release` environment (the deploy job already uses `github-pages`, and a job has one environment). + +## Where the secrets live + +Every release secret (the `APPLE_*` secrets, `KEYCHAIN_PASSWORD`, `FIELDCRAFT_SIGNING_KEY`, +`POSTHOG_KEY`) is a secret of the GitHub Environment `release`, whose deployment branches allow +`main` only. Repository secrets are readable by any workflow a writer can run on any branch; +environment secrets reach only jobs that name the environment and run from an allowed branch, so a +contributor with write access cannot read the signing keys from a branch of their own. The jobs +that read them (`release.yml`'s macos, linux, windows and release jobs, `pages.yml`'s build job) +ask for `release` only on `main` +(`environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}`); elsewhere the name is +empty, the job runs in no environment, sees no secrets and builds unsigned. `.github/CODEOWNERS` +keeps the workflows, packaging and updater under the owner's review. + +Moving them from repository secrets, once (the owner; each value comes from its own source file, +never the shell history): + +```sh +for n in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD APPLE_ID APPLE_PASSWORD \ + APPLE_TEAM_ID FIELDCRAFT_SIGNING_KEY POSTHOG_KEY; do + gh secret set "$n" --env release -R jyviko/fieldcraft < "$n.txt" +done +gh secret list --env release -R jyviko/fieldcraft # all eight listed +# After a release run on main has signed with them: +for n in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD APPLE_ID APPLE_PASSWORD \ + APPLE_TEAM_ID FIELDCRAFT_SIGNING_KEY POSTHOG_KEY; do + gh secret delete "$n" -R jyviko/fieldcraft +done +``` ## Choose and prepare the version