ci: release secrets from the main-only release environment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 12:01:16 -04:00
1 parent 1dc4c27dc1
commit 7a9cb2cb22
5 files changed
+65 -7

No files matched your search

+11
View File
@@ -1,2 +1,13 @@
# Every change to FieldCraft needs the maintainer's review before it merges. # Every change to FieldCraft needs the maintainer's review before it merges.
* @jyviko * @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
+4
View File
@@ -33,6 +33,10 @@ env:
jobs: jobs:
build: 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 runs-on: ubuntu-24.04
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
+12
View File
@@ -73,6 +73,9 @@ jobs:
macos: macos:
name: macOS universal name: macOS universal
needs: version 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 runs-on: macos-14
timeout-minutes: 120 timeout-minutes: 120
outputs: outputs:
@@ -118,6 +121,9 @@ jobs:
name: Linux x86_64 name: Linux x86_64
needs: version needs: version
# The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35. # 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 runs-on: ubuntu-22.04
timeout-minutes: 90 timeout-minutes: 90
env: env:
@@ -201,6 +207,9 @@ jobs:
windows: windows:
name: Windows x64 name: Windows x64
needs: version 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 runs-on: windows-latest
timeout-minutes: 90 timeout-minutes: 90
env: env:
@@ -230,6 +239,9 @@ jobs:
release: release:
name: Draft release (or dry-run artifacts) name: Draft release (or dry-run artifacts)
needs: [version, macos, linux, windows] 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 runs-on: ubuntu-24.04
permissions: permissions:
contents: write contents: write
+1 -1
View File
@@ -86,5 +86,5 @@ A `command_run` as sent:
The events and the policy are `fieldcraft_engine::usage`; the consent question and the settings 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 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 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. `POSTHOG_KEY`; it is an ingest-only key. A new event or property is added here first.
+37 -6
View File
@@ -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_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 | | `APPLE_TEAM_ID` | The 10-character team id of the Developer ID |
Add them with `gh secret set <NAME> -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 <NAME> --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 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 `packaging/macos/package.sh` is ad-hoc signed unless `MACOS_SIGN_IDENTITY` names an identity
in the keychain. in the keychain.
@@ -66,7 +67,7 @@ untrusted comment: minisign public key: FEF410EFEF5BAB21
RWQhq1vv7xD0/uAOt8fqERP1hP+1UCM9U9QHTDYTYytWKiuedTkT7Lgr 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 (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 [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 against the public key in the source, and uploads `SHA256SUMS.txt.minisig`. Without it the
@@ -79,7 +80,7 @@ Setting it up, once:
```sh ```sh
cargo install --locked rsign2 --version 0.6.7 cargo install --locked rsign2 --version 0.6.7
rsign generate -W -p fieldcraft-release.pub -s fieldcraft-release.key # -W: no password 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 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 ## Usage analytics key
The release builds (macOS, Linux and Windows jobs) build in the PostHog project key from the 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 `release` environment secret `POSTHOG_KEY` as `FIELDCRAFT_POSTHOG_KEY` ([privacy.md](privacy.md)).
secret the builds ask nothing and send nothing, as local and fork builds do. The web build on Without the secret the builds ask nothing and send nothing, as local, fork and dry-run builds do.
Pages has no key. 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 ## Choose and prepare the version