Azure / Azure/unbounded

releng: replace the user-owned release tag deploy key with a GitHub App so release runs are not attributed to a person

Open
#698 0 comments 0 reactions 0 assignees View on GitHub
enhancement github_actions releng
Dominant language
Go
Stars
28
Forks
11
Avg merge
1d 8h
Merged PRs (30d)
55

Description

## Symptom

Every `unbounded release` (`release.yaml`) run reports the same `actor` regardless
of who cut the release. Two runs from 2026-09-02:

| Release build | `actor` | `triggering_actor` | Who actually cut it |
| --- | --- | --- | --- |
| [33655638847](https://github.com/Azure/unbounded/actions/runs/33655638847) (`v0.5.0`) | `plombardi89` | `plombardi89` on attempt 1 | `cchildress`, via prepare run [33655577293](https://github.com/Azure/unbounded/actions/runs/33655577293) dispatched 36s earlier |
| [33667392561](https://github.com/Azure/unbounded/actions/runs/33667392561) (`v0.6.0`) | `plombardi89` | `plombardi89` | `cchildress`, via prepare run [33667308864](https://github.com/Azure/unbounded/actions/runs/33667308864) dispatched 49s earlier |

The `v0.6.0` case is the clean one: cchildress dispatched, and both `actor` and
`triggering_actor` came back `plombardi89` on attempt 1. cchildress appears
nowhere on the run.

This holds for every release since 2026-06-23. Correlating all 80 `release.yaml`
runs against all 52 `release-prepare` runs: every release build with a matching
prepare run reports `plombardi89`, including ones cut by `bcho`, `vpatelsj`,
`jwilder`, `jveski` and `cchildress`. The only builds with a truthful actor are
the handful with no prepare run at all (`v0.1.24-rc.11` through `rc.18`,
`v0.2.0-beta.4`, `v0.2.3-alpha.0` through `alpha.2`), which were tags pushed by
hand.

## Cause

`release-prepare` pushes the tag over SSH with `RELEASE_TAG_DEPLOY_KEY` rather
than `GITHUB_TOKEN`, because GitHub suppresses workflow triggers for tags pushed
with the default token:

https://github.com/Azure/unbounded/blob/main/.github/workflows/release-prepare.yaml#L110-L116

GitHub attributes a deploy-key push to the account that **registered the key**.
That key was added by an individual maintainer when 6c8d83d (#328) introduced
this mechanism, so the actor on every workflow-cut release is that person,
permanently.

`triggering_actor` cannot substitute: on a push it equals `actor`, and on a
re-run it names whoever pressed re-run. Neither field can name the cutter.

## Why this is worth fixing rather than just documenting

- **Audit trail.** The release history names one person for releases they had
nothing to do with. For a release pipeline that signs artifacts, that is the
wrong property.
- **Bus factor.** The push credential is owned by an individual account. If that
account is offboarded or the key is removed, `release-prepare` starts failing
at the push, and the failure mode is the ugly one: a tag can land with no
build behind it (`RELEASING.md`, "A tag was pushed but no build started").
- **Ownership.** It is a repository write credential held by a person rather
than by the org.

## Fix

Replace the user-owned deploy key with an org-owned identity. Either:

1. **GitHub App installation token**, minted per run with
`actions/create-github-app-token`, scoped to this repo with `contents: write`.
Pushes made with an App token do trigger workflows, so the reason the deploy
key exists at all is preserved. Runs would then be attributed to the App's bot
identity.
2. **Deploy key re-issued from a machine account**, which is cheaper but keeps a
long-lived key and moves the ownership problem rather than removing it.

Option 1 is preferred.

## Notes

- Needs a repo admin. `GET /repos/Azure/unbounded/keys` returns 404 for a
non-admin token, so confirming the current key's owner and doing the swap
cannot be done from outside admin.
- `relctl` now works around the symptom: `relctl watch ` reports a `Cut by:`
line and `relctl status` a `BY` column, both derived by correlating the build
against the `release-prepare` run whose execution window contains the tag push.
That is a mitigation, not a fix, and it can only report `unknown` for builds
older than its correlation window. It should be simplified once the underlying
attribution is correct.

Contributor guide

Open the contributing guide

Research direction

Start with .github/workflows/release-prepare.yaml around lines 110-116 and review how the release tag is pushed. Check the release.yaml workflow and the relctl watch/status attribution workaround before choosing the GitHub App token approach. Done means release pushes use the org-owned identity, workflow actor attribution is no longer tied to a person, and the workaround is simplified as appropriate.

Written by the indexing model from the issue text.

Assessment

Tech stack
git, github-actions
Domain
ci-cd, release, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.