feat: support immutable releases - #38673
Open
silverwind wants to merge 9 commits into
Open
Conversation
Publishing a release in a repository with the setting enabled locks its tag, target and assets, and reserves the tag name permanently so it can never back another release or be pushed again. Existing releases are not affected, and title, notes and prerelease stay editable. The tag name is reserved in its own table because it has to outlive both the release row and the tag itself: deleting the release is allowed and frees the tag for deletion, but the name must stay claimed. Without that, deleting the release and re-pushing the tag would defeat the feature. Closes go-gitea#37102 Assisted-by: Claude Code:Opus 5
ServeAttachment resolves the repository itself when the request carries no repo context, so reading ctx.Repo.Repository.ID panicked on the UUID based attachment route. Assisted-by: Claude Code:Opus 5
The repo scoped lookup only belongs in the deletion path, ServeAttachment resolves its own repository and was never part of this change. Assisted-by: Claude Code:Opus 5
Replacing the asset name input with plain text left it looking like stray text in the form, which then needed a section label to explain it. Keeping it an input without a name renders consistently and submits nothing. Assisted-by: Claude Code:Opus 5
silverwind
commented
Jul 27, 2026
Signed-off-by: silverwind <me@silverwind.io>
A tag name locked by an immutable release now survives the repository it was published in. The lock records the repository twice: by id so it follows renames and transfers, and by owner and name so a repository recreated under the same path inherits the locks of its predecessor. Matching either one is enough, which closes both the rename escape and the resurrection attack. Verified against GitHub, whose locks are keyed by path alone: renaming a repository there frees its locked tag names. Assisted-by: Claude Code:Opus 5
A tag name locked by an immutable release now survives the repository it was published in, so deleting a repository and recreating it under the same path no longer frees its locked names. The lock records the repository twice: by id so it follows renames and transfers, and by owner and name so a repository recreated at the same path inherits it. Matching either one is enough. GitHub keys its locks by path alone, which is why renaming a repository there frees the names it locked. The locks are deliberately not removed when a repository is deleted, so the table only ever grows. Assisted-by: Claude Code:Opus 5
ErrImmutableTag carried no data once its tag name was dropped, so it is a sentinel now, which also survives error wrapping that the type assertion did not. The pre-receive hook asks one question per ref instead of two, and both branches of the tag lookup are exact index seeks. Locked tag names outliving their repository is the point of the feature but was held up only by a bean missing from a list, so it now has a test. Assisted-by: Claude Code:Opus 5
Member
Author
|
Added ressurection protection too so I consider #37102 fixed with this. Attestation is a much larger topic and imho out of scope here, Gitea does not have infrastructure for attestation. |
…thing ErrImmutableRelease becomes a sentinel like ErrImmutableTag, so the package has one error shape instead of two and callers match with errors.Is, which survives wrapping. The pre-receive check is inlined next to the protected tag check it sits beside, matching the rest of that file. The tests drop what they proved twice: the repository deletion case joins the table that already lists what deletion purges, and the integration test keeps only what is specific to HTTP and git. It gains the two branches that were never exercised, publishing a draft and deleting a tag once its release is gone. Assisted-by: Claude Code:Opus 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements points 1, 2 and 4 of #37102.
Publishing a release in a repository with the setting enabled locks its tag, target and assets. Existing releases are unaffected, and title, notes and prerelease stay editable.
The tag name is locked in its own table so it outlives the release, the tag and the repository itself: deleting the release frees the tag for deletion, but the name can never back another release or be pushed again, including from a repository later recreated at the same path. Those rows are deliberately never removed, so the table only grows.
Verified against GitHub, with three deliberate differences: the setting lives on
EditRepoOptionlike every other repo toggle rather than a dedicated endpoint, locked names are matched case-insensitively to match Gitea's tag handling, and the lock also survives a rename — GitHub keys its locks by path alone, so renaming a repository there frees the names it locked.Repo settings:
Create release banner:
Update release banner:
Release view:
Readonly assets on edit: