> ## Documentation Index
> Fetch the complete documentation index at: https://3to1go.docs.thesteau.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Release state and recovery

> The metadata branch's exact contents and recovery after accidental deletion.

The [release flow](/releases) keeps production code on `prod` and release information on `release-state`. Deleting `release-state` does not delete production commits, version tags, GitHub Releases, or GHCR images. It does remove the automation's record of which version and production SHA were last approved.

## Branch shape

`release-state` starts with an **orphan commit**: it has its own history, independent of main and prod. Its entire tree contains exactly these three regular, non-executable files (Git mode `100644`):

```text theme={null}
release-state
├── .release-please-manifest.json
├── CHANGELOG.md
└── release.json
```

There are no subdirectories, application files, workflows, configuration files, symlinks, or generated JavaScript on this branch. The automation and `release-please-config.json` live on the code branches. Never restore release-state from a prod version tag or by copying a code branch: those commits contain the application tree, which fails metadata validation. Never merge release-state into main or prod.

### Before the first release

The bridge creates these exact starting contents:

```json .release-please-manifest.json theme={null}
{}
```

```markdown CHANGELOG.md theme={null}
# Changelog
```

```json release.json theme={null}
null
```

This means **no release has been approved**. An open metadata PR is a proposal; its contents do not become approved state until it is merged into release-state.

### After approval

For example, a first `v1.0.0` approval has this manifest:

```json .release-please-manifest.json theme={null}
{
  ".": "1.0.0"
}
```

Its release record has the following shape. The SHA and notes below are illustrative; recovery must use actual production SHAs and the exact approved notes.

```json release.json theme={null}
{
  "version": "1.0.0",
  "tag": "v1.0.0",
  "prodSha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  "previousTag": null,
  "previousSha": null,
  "notes": "## 1.0.0\n\n### Features\n\n* Initial release"
}
```

| Field | Required meaning |
| - | - |
| `version` | Stable semantic version without `v`, matching the manifest's `.` entry |
| `tag` | Exactly `v` followed by `version` |
| `prodSha` | Full, lowercase, 40-character SHA of the selected application commit in prod history |
| `previousTag` | Immediately preceding published production release tag; `null` for the first release |
| `previousSha` | That preceding tag's production commit SHA; `null` together with `previousTag` for the first release |
| `notes` | Nonempty, exact Release Please release notes, stored as a JSON string with newlines escaped |

Future records use an increasing version, such as `1.0.1`, with `previousTag: "v1.0.0"` and `previousSha` set to the actual commit tagged `v1.0.0`. Both SHAs refer to application history, **not** metadata branch commits. A non-null record with no previous release must be version `1.0.0`.

`CHANGELOG.md` starts with `# Changelog` and accumulates Release Please entries, newest first. It includes the currently approved version. The complete notes contain the generated headings, dates, links, and commit references; retain those when recovering.

The manifest and release record describe the **last approved release**, which may still be awaiting publication. Publication does not add a separate flag or metadata commit. To determine its status, check that the GitHub Release is published, not a prerelease, and its tag points to `prodSha`; its body must equal `notes`. GHCR uploads can finish later or require a separate retry.

## Recover a deleted branch

First stop prod promotions and disable **Stable release** in the Actions workflow menu while restoring. This prevents a prod push from automatically recreating empty state. Leave application deployments, existing tags, releases, and images intact.

After releases have shipped, **do not recover by running plan against empty state**. The bootstrap path assumes no previous release and will propose `v1.0.0` again. Restore the last approved metadata, or reconstruct the last published state, before resuming automation.

### Prefer the original metadata commit

The best recovery preserves all three files and their original history, including any approval whose publication failed. Find the former release-state tip in a local clone/reflog, a backup, or the `merge_commit_sha` of its most recently merged metadata PR. A version tag is not this commit: tags point to prod code.

In a recovery clone, fetch and inspect the known metadata commit. Replace the placeholder with the full SHA:

```sh theme={null}
git fetch origin <METADATA_COMMIT_SHA>
git ls-tree -r <METADATA_COMMIT_SHA>
git show <METADATA_COMMIT_SHA>:release.json
git show <METADATA_COMMIT_SHA>:.release-please-manifest.json
git show <METADATA_COMMIT_SHA>:CHANGELOG.md
```

Confirm that the tree contains only the three `100644` files, and that this is the most recent **merged/approved** state. A temporary automation PR head may contain an unapproved future release; do not restore it as approved state. If GitHub can no longer fetch the commit, use a clone or backup that still has it, or reconstruct published state below.

If the remote branch is absent, recreate it at that commit with an empty expected-ref lease:

```sh theme={null}
git push --force-with-lease=refs/heads/release-state: origin <METADATA_COMMIT_SHA>:refs/heads/release-state
```

The empty lease requires the remote ref to be absent and refuses to replace an existing branch, including one recreated during recovery. Do not replace it with an unconditional force push. A ruleset may require an administrator's narrowly scoped recovery exception to recreate the ref. Restore its normal protections immediately afterward. If automation has already recreated an empty branch, stop and inspect it; an administrator must review and restore the intended metadata ref under a temporary recovery exception.

### Reconstruct from published releases

Use this only when the original metadata commit cannot be recovered. It reconstructs **published** state, not an approved-but-unpublished release. Check merged metadata PRs and failed publication runs first; preserve any outstanding approval separately rather than silently treating it as shipped.

The recipe requires Git, [GitHub CLI](https://cli.github.com/manual/gh_api) authenticated for read access, and Python 3. Use a **new, empty recovery directory**, keeping exported API data outside the metadata checkout:

```sh theme={null}
mkdir release-state-recovery
cd release-state-recovery
gh api --paginate --slurp repos/thesteau/3to1go/releases > releases.json
git clone --no-checkout https://github.com/thesteau/3to1go.git metadata
cd metadata
git fetch origin --tags
git switch --orphan release-state
```

Confirm that `releases.json` includes all published production releases and has not been edited. Save the following Python recipe as `../rebuild-state.py` (outside the metadata checkout), then run `python ../rebuild-state.py` from `metadata/`:

```python theme={null}
import json
import re
import subprocess
from pathlib import Path

def git(*args):
    return subprocess.check_output(["git", *args], text=True).strip()

pages = json.loads(Path("../releases.json").read_text(encoding="utf-8-sig"))
releases = [release for page in pages for release in page]
stable = [release for release in releases
          if not release["draft"] and not release["prerelease"]
          and re.fullmatch(r"v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)",
                           release["tag_name"])]
stable.sort(key=lambda release: tuple(map(int, release["tag_name"][1:].split("."))))
if not stable or stable[0]["tag_name"] != "v1.0.0":
    raise SystemExit("Expected published history starting at v1.0.0; inspect releases before recovery.")

previous_sha = None
for release in stable:
    sha = git("rev-parse", release["tag_name"] + "^{commit}")
    git("merge-base", "--is-ancestor", sha, "origin/prod")
    if previous_sha:
        git("merge-base", "--is-ancestor", previous_sha, sha)
    if release["target_commitish"] not in ("prod", sha):
        raise SystemExit("Release target does not match production tag: " + release["tag_name"])
    if not isinstance(release["body"], str) or not release["body"].strip():
        raise SystemExit("Missing original release notes: " + release["tag_name"])
    release["prodSha"] = sha
    previous_sha = sha

latest = stable[-1]
previous = stable[-2] if len(stable) > 1 else None
state = {
    "version": latest["tag_name"][1:],
    "tag": latest["tag_name"],
    "prodSha": latest["prodSha"],
    "previousTag": previous["tag_name"] if previous else None,
    "previousSha": previous["prodSha"] if previous else None,
    "notes": latest["body"],
}
Path("release.json").write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
Path(".release-please-manifest.json").write_text(
    json.dumps({".": state["version"]}, indent=2) + "\n", encoding="utf-8")
Path("CHANGELOG.md").write_text(
    "# Changelog\n\n" + "\n\n".join(release["body"] for release in reversed(stable)) + "\n",
    encoding="utf-8")
print("Reconstructed", state["tag"], "at", state["prodSha"])
```

This orders stable releases by semantic version, rather than assuming GitHub's latest release or publication date identifies the correct boundary. The reconstructed changelog contains the published notes newest first; it may differ in spacing from the original generated file. Deleted releases, edited notes, or missing tags require recovery from an original metadata commit or backup instead of guessing the missing history.

Review the three files and the selected latest/previous release against GitHub, then commit and inspect the metadata-only tree:

```sh theme={null}
git add .release-please-manifest.json CHANGELOG.md release.json
git commit -m "chore: recover published release state"
git ls-tree -r HEAD
git push --force-with-lease=refs/heads/release-state: origin HEAD:refs/heads/release-state
```

The push recreates an absent branch; it does not replace an existing one. Apply any narrowly scoped administrator recovery exception only for this operation, then restore the branch rules. Keep recovery scripts and API exports outside the branch.

### If nothing has been released or approved

If no production release exists and no merged metadata PR has an outstanding approval, the empty bootstrap shape is sufficient. Re-enable **Stable release** and run **plan** from prod; it recreates the orphan branch and proposes the first releasable `v1.0.0`. Restore the branch rules before approving its PR.

### Resume automation

1. Restore the release-state ruleset: PR required, independent approval, stale approvals dismissed, deletion/force pushes blocked, and **Validate release metadata** required. Verify the metadata Actions event policy is still enabled.
2. Re-enable **Stable release**. If the recovered approved record has not been published, run **publish** from prod to complete that specific approval. If it matches an existing published release, run **plan** from prod to calculate the next release from its recorded SHA.
3. Inspect any old/open metadata PRs before merging. Close stale proposals based on the lost branch; let plan create/update the current proposal and ensure its metadata check passes. A restored pending release is not an invitation to approve a different open proposal.
4. Verify existing version tags still point to their original prod commits. Retry missing stable image uploads using **Stable Docker Images** with the existing release tag. Resume normal main → prod promotions.

For future recovery, retain a backup of the **release-state ref and its Git objects** alongside repository backups. Published tags preserve application code, not this branch's metadata history; GitHub Releases preserve published notes, but not an unpublished approval.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.