Add release: publish tagged releases on the mirror servers
`release [@name ...] <tag> [notes]` does the whole chain in one step — create the annotated tag, push it to the internal server, then push it to each selected mirror and turn it into a release object there. Target selection reuses pushremote's @name mechanism, so the two behave alike. Notes are generated when none are given: the tag's own annotation when it carries more than the default, otherwise the commit subjects since the previous tag, capped at 50 lines. `tag add v1.0 "why this exists"` now takes a message, which is what that fallback reads; before, the annotation was always just the tag name. Tags ending in -rc/-alpha/-beta/-pre are marked as pre-releases on Gitea and GitHub. Releasing the same tag twice updates the existing release; a tag that already points at a different commit stops the command, since moving a published tag makes one version mean different things per server. A repository that is not on the mirror yet is reported instead of being created as a side effect. Binary assets are deliberately out of scope: Gitea attaches them to the release, GitHub uses a separate upload host, and GitLab does not host them at all but wants a link into its package registry. The providers differ in path shape and field names -- GitLab addresses projects by URL-encoded path, calls the notes "description" and has no pre-release flag -- so this comes with a recording httptest stand-in that asserts the exact requests for all three. That harness also covers authUser, repoExists and the auth header forms, which had no test at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,15 +162,18 @@ func (r *remoteAPI) authUser() (string, error) {
|
||||
return "", fmt.Errorf("could not determine remote user")
|
||||
}
|
||||
|
||||
// repoPath is the API path of one repository. GitLab addresses a project by its
|
||||
// URL-encoded "owner/repo" path, the others by two path elements.
|
||||
func (r *remoteAPI) repoPath(owner, repo string) string {
|
||||
if r.kind == kindGitLab {
|
||||
return r.apiRoot() + "/projects/" + url.PathEscape(owner+"/"+repo)
|
||||
}
|
||||
return r.apiRoot() + "/repos/" + owner + "/" + repo
|
||||
}
|
||||
|
||||
// repoExists reports whether owner/repo already exists on the server.
|
||||
func (r *remoteAPI) repoExists(owner, repo string) (bool, error) {
|
||||
var ep string
|
||||
if r.kind == kindGitLab {
|
||||
ep = r.apiRoot() + "/projects/" + url.PathEscape(owner+"/"+repo)
|
||||
} else {
|
||||
ep = r.apiRoot() + "/repos/" + owner + "/" + repo
|
||||
}
|
||||
code, data, err := r.do("GET", ep, nil)
|
||||
code, data, err := r.do("GET", r.repoPath(owner, repo), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -338,15 +341,9 @@ func pushToRemote(t RemoteTarget, repo, description string) bool {
|
||||
|
||||
// keep a credential-free git remote named after the target
|
||||
web := api.repoWebURL(owner, repo)
|
||||
if _, err := gitCapture(DIR, "remote", "get-url", t.Name); err == nil {
|
||||
gitOK(DIR, "remote", "set-url", t.Name, web)
|
||||
} else {
|
||||
gitOK(DIR, "remote", "add", t.Name, web)
|
||||
}
|
||||
ensureGitRemote(t.Name, web)
|
||||
|
||||
// authenticate the push with a one-shot Basic auth header, so the token is
|
||||
// neither persisted in the repository's git config nor visible in `ps`
|
||||
header := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(owner+":"+api.key))
|
||||
header := api.pushHeader(owner)
|
||||
if !gitPushHeader(DIR, t.Name, header, "--all") {
|
||||
return false
|
||||
}
|
||||
@@ -355,6 +352,22 @@ func pushToRemote(t RemoteTarget, repo, description string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ensureGitRemote points a credential-free git remote named name at web,
|
||||
// adding it when the repository does not have it yet.
|
||||
func ensureGitRemote(name, web string) bool {
|
||||
if _, err := gitCapture(DIR, "remote", "get-url", name); err == nil {
|
||||
return gitOK(DIR, "remote", "set-url", name, web)
|
||||
}
|
||||
return gitOK(DIR, "remote", "add", name, web)
|
||||
}
|
||||
|
||||
// pushHeader builds the one-shot HTTP Basic auth header used for pushes, so the
|
||||
// token is neither persisted in the repository's git config nor visible in `ps`.
|
||||
func (r *remoteAPI) pushHeader(owner string) string {
|
||||
return "Authorization: Basic " +
|
||||
base64.StdEncoding.EncodeToString([]byte(owner+":"+r.key))
|
||||
}
|
||||
|
||||
// gitPushHeader runs `git push <remote> <args...>` with an extra HTTP auth
|
||||
// header, disabling interactive credential prompts.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user