airvzxf / airvzxf/ftp-deployment-action
chore(release): runbook needs updating for protect-main ruleset
- 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.