Skip to content

Turn on automatic production releases

Think of GitHub as a robot holding a locked toolbox. A merged change is its work order; the tests are its checklist; production secrets are the tools. The repository can stay public because the toolbox is stored in a protected GitHub environment. Code running with those tools is trusted, so protect changes to deployment code.

1. Prepare the branch

Review the integration branch, then push it and open a pull request when ready. Ordinary checks use synthetic configuration and should pass before any live setup. Do not merge failing checks. Missing production secrets prevent a live deployment, not pull request testing. Keep the old private infrastructure checkout and its private recovery files while completing the cutover.

2. Create the production environment

In off-key → Settings → Environments, create production. Select deployment branches and allow main only. Leave required reviewers off for the agreed automatic deployment model. Under environment variables add AUTO_DEPLOY_ENABLED with value false. This is the off switch; automatic releases stay disabled until the first manual release succeeds.

3. Give the robot its private toolbox

Under that environment's Secrets, create these exact names:

Secret What to paste
OFFKEY_PROD_VAULT The complete existing encrypted production vault.yml, starting with $ANSIBLE_VAULT;. Paste raw text, without base64.
OFFKEY_VAULT_PASSWORD The password that unlocks that vault.
OFFKEY_PROD_INVENTORY A portable production hosts.yml, based on the example in infra/ansible/inventories/prod/.
OFFKEY_DEPLOY_SSH_KEY A dedicated deployment private SSH key.
OFFKEY_DEPLOY_KNOWN_HOSTS Verified SSH host keys for every inventory address and port.
TS_OAUTH_CLIENT_ID The management Tailscale federated identity client ID from step 5.
TS_AUDIENCE Its audience from step 5.

The inventory names must match docker node ls. Use management-tailnet addresses for the manager and workers, their existing SSH ports and their deployment users. Remove laptop-specific ansible_ssh_private_key_file, ProxyCommand, ProxyJump and UserKnownHostsFile settings. The workflow supplies its own key and verified host list. Worker SSH forwarding on port 2222 remains necessary where currently used; the retired Windows guide is unnecessary for a GitHub runner.

GitHub limits each secret to 48 KiB. The existing encrypted vault fits; base64 would unnecessarily enlarge it. If it grows past the limit, split it into reviewed smaller secrets and adjust materialization before enabling releases. Keep the encrypted vault and password in your independent password manager/recovery storage too: GitHub is not a system for downloading secrets back to a new laptop.

The workflow uses its temporary GITHUB_TOKEN for GHCR; it does not need a new PAT. For existing private packages, check Package settings → Manage Actions access and grant off-key read access. Keep the existing vault registry credential only for direct local recovery until you deliberately replace it.

4. Authorize SSH and verify the locks on the servers

Generate a dedicated Ed25519 key, for example with ssh-keygen -t ed25519 -f ~/.ssh/offkey-deploy. Store the private key in the environment secret. Add its public key to each existing deployment user's authorized_keys. These users need Docker access and the existing noninteractive sudo privileges used by Ansible. Keep your personal recovery key; remove neither working access nor vendor identity.

Obtain each server's Ed25519 host fingerprint from its trusted console or an already verified SSH session. Use infra/scripts/trust_ssh_host.py to enroll that exact fingerprint into a separate known-hosts file, then upload that file as the secret. A bare ssh-keyscan is insufficient verification. Nondefault SSH ports use [address]:2222 entries. The CI connection enforces host-key checking.

5. Let the robot reach the servers through Tailscale

In the management tailnet, create tag:offkey-ci. Create a federated identity using GitHub's OIDC issuer https://token.actions.githubusercontent.com, the auth_keys scope, and permission to create ephemeral devices with that tag. Restrict trust to this repository's production environment and the deployment workflow on main. Use GitHub's numeric repository/owner IDs as additional immutable claims where supported. Copy the client ID and audience into step 3.

A GitHub environment changes the default subject to repo:DataDrivenSustainabilitySolutions/off-key:environment:production. Also constrain the repository, ref (refs/heads/main) and workflow_ref (DataDrivenSustainabilitySolutions/off-key/.github/workflows/deploy-production.yml@refs/heads/main) claims in the federated identity. Do not configure trust for every repository or PR.

Give that tag ordinary network access to only the actual deployment addresses: manager TCP 22 and worker TCP 2222 (adjust to the real inventory). For example, merge this into your existing policy using your real destination addresses:

{
  "tagOwners": {"tag:offkey-ci": ["autogroup:admin"]},
  "grants": [
    {"src": ["tag:offkey-ci"], "dst": ["100.100.10.1"], "ip": ["tcp:22"]},
    {"src": ["tag:offkey-ci"], "dst": ["100.100.10.2", "100.100.10.3"], "ip": ["tcp:2222"]}
  ]
}

This uses the existing OpenSSH service; it does not require Tailscale SSH rules. The runner leaves the tailnet after its job. Do not enroll it into the separate vendor tailnet or give it vendor identity, NFS, database or MQTT network access. Review broader existing tailnet grants too; a narrow new rule cannot undo an existing allow-all rule.

6. Keep main protected

In Settings → Rules → Rulesets, or the existing branch protection rule, require the existing review policy and code-owner review for infrastructure/workflows/SQL. The new CODEOWNERS file names the existing owner. Preserve the current owner and administrator policy while adding the pull-request checks listed in Contributing and CI after they have appeared. Bind RADAR production integration to GitHub Actions.

Docker Publish runs after a main push, so it is a release gate rather than a PR merge check. Even an administrator merge cannot bypass the deployment workflow's same-commit success checks. Do not require Deploy Production as a pre-merge check.

7. Merge, then do the first controlled release

After the PR is green and reviewed, merge it. Wait for every release-gate workflow in Contributing and CI to succeed on main. Under Actions → Deploy Production → Run workflow, select main, paste its complete 40-character commit SHA, and check bootstrap.

Choose a quiet maintenance window. The robot stops old writers, validates a private database backup on the database server, upgrades the schema, captures existing monitor settings, stops their old workers, deploys the exact images and restarts the monitors with fresh calibration. Original telemetry and evidence stay in the database.

Watch the workflow result, then check the website, login, Sources and monitor status yourself. Quiet chargers may show waiting for data. On the manager, /opt/stacks/off-key/release.json must name this commit, and release-maintenance.json must be gone. The runtime database operation must be complete. A failure leaves the last verified release unchanged; use the recovery runbook and retry the same SHA before attempting another release.

8. Flip the switch

Only after step 7 succeeds, change AUTO_DEPLOY_ENABLED to true in the production environment. Future runtime changes merged into main deploy automatically after all release checks pass. Documentation changes skip deployment; the comparison starts at the last verified release, so an earlier runtime change cannot be skipped.

After this cutover is verified and your independent recovery material is safe, archive the old private off-key-infra repository. Its history and private university/vendor notes stay private; they are not imported into public Git history.

For another laptop, clone off-key and use make deploy-prod REVISION=<tested SHA> to dispatch GitHub. No local production environment files are needed for normal CD. Changing live configuration means updating the encrypted vault/inventory environment secrets and manually dispatching a tested release; secret edits alone do not trigger CD.

References: GitHub environments, workflow_run semantics, Tailscale GitHub Action, workload identity federation.