Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/allowed_signers
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
#
# you@example.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
#
# Empty until the release manager adds their key (docs/stores.md, "Signing keys"); until then
# every release tag is refused.

# Ayla Croft (@HackTuah), release manager: SHA256:HqIaFq/thUm844NjYC3a+4Br40cqD2yJHEq1L2iiw4E
dev@scriptkittyos.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAiWNJRQYwtCxb7uEMB5i1XdR6k9Xd+hF6+kMQa4nQ3j dev@scriptkittyos.com
2 changes: 1 addition & 1 deletion MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ step.

| Name | GitHub | Roles | Areas | Tag-signing key | Since |
|---|---|---|---|---|---|
| Ayla Croft | [@HackTuah](https://github.com/HackTuah) | Lead maintainer, maintainer, release manager, security response lead | Server, extension, docs | Not yet: to be added to `.github/allowed_signers` before the first release tag | 2026-10 |
| Ayla Croft | [@HackTuah](https://github.com/HackTuah) | Lead maintainer, maintainer, release manager, security response lead | Server, extension, docs | SSH ed25519 `SHA256:HqIaFq/thUm844NjYC3a+4Br40cqD2yJHEq1L2iiw4E` (`.github/allowed_signers`) | 2026-10 |
| minitru | [@minitru](https://github.com/minitru) | Maintainer | Reviews, triage | None (not a release manager) | 2026-10 |

## Not yet filled
Expand Down
17 changes: 11 additions & 6 deletions docs/stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Tag it on the release PR's head, whose version is already bumped:
```sh
git fetch origin release-please--branches--main
git switch --detach origin/release-please--branches--main
scripts/tag-release.sh --rc 1
scripts/tag-release.sh --rc 1 --ssh-key ~/.ssh/kotiko_release.pub
```

### Trying the pipeline without releasing
Expand Down Expand Up @@ -152,12 +152,17 @@ store credentials.

Each release manager signs tags with their own key; no signing key exists on GitHub.

- **SSH** (simplest): `git config --global gpg.format ssh` and
`git config --global user.signingkey ~/.ssh/id_ed25519.pub`, then add a line to
`.github/allowed_signers` in a pull request:
`you@example.com namespaces="git" ssh-ed25519 AAAA鈥.
- **SSH** (simplest): make a key just for release tags, with the project's address and
not a personal one, since the public key and its comment are published:
`ssh-keygen -t ed25519 -C "dev@scriptkittyos.com" -f ~/.ssh/kotiko_release`. Add a line
to `.github/allowed_signers` in a pull request:
`you@example.com namespaces="git" ssh-ed25519 AAAA鈥. Sign with
`scripts/tag-release.sh --ssh-key ~/.ssh/kotiko_release.pub`, which uses the key for that
tag only; how git signs your commits stays as it is. (Setting `gpg.format ssh` globally
would switch commit signing to that key too.)
- **GPG**: `git config --global user.signingkey <fingerprint>`, then
`gpg --armor --export <fingerprint> >> .github/release-keys.asc` in a pull request.
`gpg --armor --export <fingerprint> >> .github/release-keys.asc` in a pull request. The
exported key includes every user ID on it, email addresses too, and they become public.

List the same key's fingerprint in MAINTAINERS.md. Until a key is added, every release tag
is refused.
Expand Down
20 changes: 16 additions & 4 deletions scripts/tag-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# scripts/tag-release.sh # v0.3.0, from the merged release commit
# scripts/tag-release.sh --rc 1 # v0.3.0-rc.1, a release candidate (no store upload)
# scripts/tag-release.sh --dry-run # every check, no tag
# scripts/tag-release.sh --ssh-key ~/.ssh/kotiko_release.pub # sign this tag with that SSH
# # key, whatever git signs commits with
#
# Checks first: a clean tree; HEAD is origin/main (a candidate may instead be the head of
# release-please's branch, whose version is already bumped); for a release, HEAD is the
Expand All @@ -28,12 +30,14 @@ die() {
rc=""
dry_run=false
remote="origin"
ssh_key=""
while [ $# -gt 0 ]; do
case "$1" in
--rc) rc="$2"; shift 2 ;;
--dry-run) dry_run=true; shift ;;
--remote) remote="$2"; shift 2 ;;
*) die "unknown option $1 (use --rc N, --dry-run, --remote NAME)" ;;
--ssh-key) ssh_key="$2"; shift 2 ;;
*) die "unknown option $1 (use --rc N, --dry-run, --remote NAME, --ssh-key FILE)" ;;
esac
done
[ -z "$rc" ] || [[ "$rc" =~ ^[1-9][0-9]*$ ]] || die "--rc takes a number: --rc 1"
Expand Down Expand Up @@ -78,10 +82,18 @@ if [ "$dry_run" = true ]; then
exit 0
fi

[ -n "$(git config user.signingkey || true)" ] ||
die "no signing key: set git config user.signingkey (and gpg.format ssh for an SSH key); see docs/stores.md"
# --ssh-key signs this one tag with an SSH key, leaving how git signs commits (often a GPG
# key) as it is.
sign=(git)
if [ -n "$ssh_key" ]; then
[ -f "$ssh_key" ] || die "--ssh-key: $ssh_key isn't a file"
sign=(git -c gpg.format=ssh -c "user.signingkey=$ssh_key")
else
[ -n "$(git config user.signingkey || true)" ] ||
die "no signing key: pass --ssh-key FILE, or set git config user.signingkey (and gpg.format ssh for an SSH key); see docs/stores.md"
fi

git tag -s "$tag" -m "Kotiko $version"
"${sign[@]}" tag -s "$tag" -m "Kotiko $version"
if ! bash scripts/verify-tag.sh "$tag" --main "$remote/main" --rc-branch "$remote/$rc_branch"; then
git tag -d "$tag" >/dev/null
die "the new tag didn't verify (is your key in .github/allowed_signers?); it was deleted, nothing was pushed"
Expand Down
12 changes: 12 additions & 0 deletions test/unit/release-tags.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ test("tag-release refuses a commit that isn't the release, a dirty tree, leftove
assert.equal(run(work, "git", ["rev-parse", "--verify", "--quiet", "refs/tags/v0.3.0"]).status, 1, "the bad tag is deleted");
});

test("--ssh-key signs the tag with that key, whatever git signs commits with", () => {
const { work } = setup();
git(work, "config", "--unset", "user.signingkey");
git(work, "config", "gpg.format", "openpgp");
assert.match(run(work, "bash", ["scripts/tag-release.sh"]).stderr, /no signing key: pass --ssh-key FILE/);
assert.match(run(work, "bash", ["scripts/tag-release.sh", "--ssh-key", join(base, "missing.pub")]).stderr, /isn't a file/);
const r = run(work, "bash", ["scripts/tag-release.sh", "--ssh-key", join(base, "release.pub")]);
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /pushed v0\.3\.0/);
assert.equal(git(work, "config", "gpg.format"), "openpgp", "the repository's own signing settings are untouched");
});

test("a release candidate is tagged on release-please's branch and verifies as a pre-release", () => {
const { work } = setup();
git(work, "switch", "-q", "-c", "release-please--branches--main");
Expand Down