airvzxf / airvzxf/ftp-deployment-action

chore(release): runbook needs updating for protect-main ruleset

Offen
#141 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
pending-human pending-review
Vorherrschende Sprache
Shell
Sterne
37
Forks
9
Ø Merge
44 Min.
Gemergte PRs (30 T.)
47

Beschreibung

## Problem

The repo's `protect-main` ruleset (id18646266, rule `pull_request` without bypass actors) blocks `git push origin main` even for the repo owner. The v2.11.1 release pipeline encountered this: the signed tag `v2.11.1` (pointing to commit `6c89b83`) pushed successfully, but the bookkeeping commit `chore(release): v2.11.1 — VERSION + CHANGELOG` could not be pushed directly and had to be brought back to `main` via a follow-up PR (#140).

The release commit `6c89b83` is now reachable only via the tag `v2.11.1` (orphan of any branch). git will retain it while the tag exists; not a correctness issue, but unconventional.

## Recommendation

Update the local release runbook (no doc file currently exists; the release flow lives in the head-only notes) so step 7 of any future release flow is:

1. Cut the bookkeeping commit (VERSION + CHANGELOG) on a `chore/release-vX.Y.Z` branch.
2. Open a PR against `main`.
3. Wait for CI to go green.
4. Merge the PR (squash).
5. Pull `main` locally; cut the signed tag pointing at the merge commit on `main`.
6. Push the tag — this still works because tags aren't gated by the `pull_request` rule.

This guarantees the release commit lives on `main` and the tag points at a reachable commit. Eliminates the orphan-commit pattern observed in v2.11.1.

## Related

- PR #139 (FTPS flake fix, pre-baked test server image)
- PR #140 (follow-up bookkeeping sync after the ruleset blocked direct push)
- v2.11.1 release (the one where this was first observed)
- Source of finding: post-#135 release retrospective

---

## Postmortem — v2.11.1 and v2.11.0 orphan tags (resolved)

> Correction to the "Problem" section above: the orphan commit **was**
> a correctness issue, not merely "unconventional". That original
> assessment was wrong and is superseded by this postmortem.

`v2.11.1` was initially published with the tag pointing at commit
`6c89b83`, which was not reachable from any branch. The orphan arose
because:

1. The signed tag pushed successfully — tags are not gated by
`protect-main` (the ruleset targets branches only).
2. The bookkeeping commit (VERSION + CHANGELOG) on a local branch
could not be pushed directly to `main` due to the ruleset.
3. The PR (#140) that brought the bookkeeping to `main` produced a
**different** commit hash (`133502f`) than the tag pointed at
(`6c89b83`), because a squash-merge rewrites the commit.

This was a real correctness problem: `git checkout v2.11.1` yielded the
orphan's commit rather than the on-main commit, and `git log` /
`git diff main v2.11.1` reported misleading history — even though the
file trees were byte-identical.

The same defect pre-existed for `v2.11.0` (tag at `e671157`, orphan;
on-main equivalent `4b54aec`). It had been present since the v2.11.0
release and went unnoticed until the v2.11.1 retrospective.

### Tag remapping

| Tag | Was (orphan commit) | Now (on `main`) | Tree identical |
|---|---|---|---|
| `v2.11.1` | `6c89b83ab134614307ae2b24ac0510228777168a` | `133502f0680bafd44b0a24423f31161715c8c411` | yes — both `1791b6e714196f03620cdd05a7a358474bbab3ca` |
| `v2.11.0` | `e6711573d780c8aeca1a174c79cd528bcfe61a7f` | `4b54aecdfddd1cea5de4532d4dafd7c1acce0004` | yes — both `c63e07fa6902ff0e90a6f19b6f6d1f4b2bc79085` |

The old annotated tag objects were `8e869dd` (`v2.11.1`) and `d1e9104`
(`v2.11.0`); the replacements are `7e3f3f1` and `34f1521`.

### Fixes applied

Both tags were re-created on their on-main equivalents and force-pushed
**in place**, without deleting the remote ref first:

```bash
git tag -d v2.11.1
git tag -s v2.11.1 -m "v2.11.1 — pre-baked FTPS test server image (closes #135)" 133502f
git push --force origin refs/tags/v2.11.1:refs/tags/v2.11.1

git tag -d v2.11.0
git tag -s v2.11.0 -F --cleanup=verbatim 4b54aec
git push --force origin refs/tags/v2.11.0:refs/tags/v2.11.0
```

Two deliberate choices here:

- **In-place force-push, not delete-then-push.** Deleting a remote tag
that a published Release points at can flip that Release into a
draft. An in-place `git push --force` updates the ref atomically and
leaves the Release attached. It is accepted because the repo has no
tag ruleset and no legacy tag protection — `protect-main` targets
branches. Both Releases were confirmed still published afterwards
(`v2.11.1` remains "Latest"), with their bodies intact.
- **The `v2.11.0` annotation was preserved byte-for-byte.** That tag
carried 2,927 bytes of release notes (the #116 EPIC breakdown, known
limitations, published image tags). Re-tagging with a short `-m`
message would have destroyed it. Both re-created tags are signed by
the same key as the originals,
`SHA256:POu2Sr8ILb1IM05Vh1cGU3xivjx05QjWoWYhdLc6YHA`
(`airvzxf@github`), and pass `git verify-tag`.

### Verification that the published images still match the tags

Image **IDs** cannot be compared meaningfully here: the registry images
were built by `docker/build-push-action` (buildx, 7 layers) whereas a
local rebuild used podman (6 layers), and an image ID is a digest over
config + history, which embeds builder metadata and timestamps. A
mismatch there says nothing about source divergence, so the images were
compared by **content** instead — rebuilding each tag's exact tree with
the pipeline's `--build-arg VERSION=` and diffing the exported
root filesystems:

| Version | Files compared | Differing |
|---|---|---|
| 2.11.1 | 292 | 1 — `/var/log/apk.log` |
| 2.11.0 | 292 | 1 — `/var/log/apk.log` |

The sole difference is the timestamp line inside the apk transaction
log (`... at 2026-09-03 06:16:44` vs `06:22:07`); the package list and
every other byte are identical. `/app/entrypoint.sh`, `/app/lib.sh`,
`/app/VERSION`, `/app/LICENSE`, `/app/README.md` and `/usr/bin/lftp`
all hash-match exactly. Rebuilding from the relocated tags therefore
reproduces the published images.

The strongest guarantee is upstream of Docker entirely: the orphan and
on-main commits share the **same git tree object**, so the build
context was provably identical.

### Scope check — no other tag is affected

All 27 tags were audited for reachability from `main`. Only `v2.11.0`
and `v2.11.1` were orphaned; `v1` through `v2.10.0` (including
`v2.7.0`, `v2.8.0`, `v2.9.0`, `v2.10.0`) all point at commits on
`main`. No further remediation is needed.

### Additional runbook gap found

`git tag -s` fails outright on the maintainer's host with
`fatal: either user.signingkey or gpg.ssh.defaultKeyCommand needs to be
configured`. The global config sets `gpg.format=ssh` and
`tag.gpgsign=true` but never sets `user.signingkey`, so tag signing
only works when the key is passed explicitly:

```bash
git -c user.signingkey=~/.ssh/github_airvzxf_ed25519.pub tag -s ...
```

The release runbook should either record this invocation or set
`user.signingkey` permanently, so a future release cannot silently fail
at the signing step.

### Side effect of force-moving a tag: the "Latest" badge moves

Force-updating a tag ref resets the `created_at` of the GitHub Release
attached to it. Both releases came back with
`created_at = 2026-09-03T06:15:21Z` (the push time), replacing
`2026-09-03T04:30:08Z` (`v2.11.1`) and `2026-09-02T23:59:51Z`
(`v2.11.0`). Because GitHub picks the "Latest" release by sorting on
`created_at`, the now-tied timestamps resolved to **`v2.11.0`**, so
`GET /releases/latest` briefly reported the older version, and the
releases page moved the "Latest" badge onto `v2.11.0`.

Fixed by pinning it explicitly:

```bash
gh release edit v2.11.1 --latest --verify-tag
```

`GET /releases/latest` now returns `v2.11.1` again. Release bodies,
names, draft and prerelease flags were verified byte-identical to
pre-change backups (654 and 8,952 bytes).

Anyone relocating a release tag in future must re-assert `--latest`
afterwards, and should check it when relocating more than one tag in
the same minute — that is what created the tie here.

Beitragsleitfaden

Für dieses Repository ist kein Beitragsleitfaden indexiert

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.