Attach ./bin and ./assets to a release
`release` now uploads every file in the project's ./bin and ./assets when
those directories exist. Nothing to configure, and nothing happens for a
project that has neither.
This is the part deliberately left out when `release` was written,
because it is where the three providers stop resembling each other:
Gitea multipart POST to .../releases/<id>/assets?name=<name>
GitHub raw POST to the separate upload host the release object names
in upload_url, whose RFC 6570 template suffix has to go first
GitLab a release stores links, not files: the file goes into the
project's generic package registry and the release gets a
package link pointing at it
So findRelease and createRelease now return a releaseRef carrying the id
and, for GitHub, that upload host -- the id alone cannot address an
upload. Uploads stream from disk rather than buffering: these are whole
binaries, and the Gitea multipart body is assembled through a pipe.
Only regular files directly in those directories are taken. Symlinks are
skipped, which matters here: build.sh leaves bin/mgsh pointing at one of
its siblings, and uploading the same 9M twice under two names helps
nobody. A name present in both directories is used from bin and reported
for assets. Re-releasing a tag replaces same-named assets rather than
failing on them, since rebuilding and publishing again is the normal
reason to do it, and a file that fails does not stop the rest.
Each provider's request shape is pinned down against the recording
stand-in, and the whole chain was run once end to end -- real repository,
real binaries, a fake Gitea that also serves git-http-backend so the tag
push is real too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+65
-25
@@ -56,46 +56,68 @@ func (r *remoteAPI) releaseByTagPath(owner, repo, tag string) string {
|
||||
return r.releasePath(owner, repo) + "/tags/" + url.PathEscape(tag)
|
||||
}
|
||||
|
||||
// findRelease returns the provider's id for the release of tag and whether it
|
||||
// exists at all. GitLab addresses releases by tag, so there the tag is the id.
|
||||
func (r *remoteAPI) findRelease(owner, repo, tag string) (id string, found bool, err error) {
|
||||
// releaseRef identifies a release for the calls that follow creating it.
|
||||
type releaseRef struct {
|
||||
tag string
|
||||
id string // provider id; the tag itself on GitLab
|
||||
// GitHub uploads assets to a different host than its API, and names it in
|
||||
// the release object as an RFC 6570 template
|
||||
uploadURL string
|
||||
}
|
||||
|
||||
// parseReleaseRef reads the identifying fields out of a release object.
|
||||
func (r *remoteAPI) parseReleaseRef(tag string, data []byte) releaseRef {
|
||||
ref := releaseRef{tag: tag, id: tag}
|
||||
if r.kind == kindGitLab {
|
||||
return ref // GitLab addresses a release by its tag throughout
|
||||
}
|
||||
var res struct {
|
||||
ID int64 `json:"id"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
}
|
||||
json.Unmarshal(data, &res)
|
||||
ref.id = strconv.FormatInt(res.ID, 10)
|
||||
// ".../assets{?name,label}" -> ".../assets"
|
||||
if i := strings.IndexByte(res.UploadURL, '{'); i >= 0 {
|
||||
ref.uploadURL = res.UploadURL[:i]
|
||||
} else {
|
||||
ref.uploadURL = res.UploadURL
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
// findRelease returns the release of tag and whether it exists at all.
|
||||
func (r *remoteAPI) findRelease(owner, repo, tag string) (ref releaseRef, found bool, err error) {
|
||||
code, data, err := r.do("GET", r.releaseByTagPath(owner, repo, tag), nil)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
return releaseRef{}, false, err
|
||||
}
|
||||
switch code {
|
||||
case 404:
|
||||
return "", false, nil
|
||||
return releaseRef{}, false, nil
|
||||
case 200:
|
||||
if r.kind == kindGitLab {
|
||||
return tag, true, nil
|
||||
}
|
||||
var res struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
json.Unmarshal(data, &res)
|
||||
return strconv.FormatInt(res.ID, 10), true, nil
|
||||
return r.parseReleaseRef(tag, data), true, nil
|
||||
default:
|
||||
return "", false, fmt.Errorf("checking release failed (HTTP %d): %s", code, firstLine(data))
|
||||
return releaseRef{}, false, fmt.Errorf("checking release failed (HTTP %d): %s", code, firstLine(data))
|
||||
}
|
||||
}
|
||||
|
||||
// createRelease publishes a new release for an already pushed tag.
|
||||
func (r *remoteAPI) createRelease(owner, repo string, rel release) error {
|
||||
func (r *remoteAPI) createRelease(owner, repo string, rel release) (releaseRef, error) {
|
||||
code, data, err := r.do("POST", r.releasePath(owner, repo), r.releaseBody(rel, true))
|
||||
if err != nil {
|
||||
return err
|
||||
return releaseRef{}, err
|
||||
}
|
||||
if code != 200 && code != 201 {
|
||||
return fmt.Errorf("creating release failed (HTTP %d): %s", code, firstLine(data))
|
||||
return releaseRef{}, fmt.Errorf("creating release failed (HTTP %d): %s", code, firstLine(data))
|
||||
}
|
||||
return nil
|
||||
return r.parseReleaseRef(rel.Tag, data), nil
|
||||
}
|
||||
|
||||
// updateRelease rewrites the name and notes of an existing release. Gitea and
|
||||
// GitHub patch it by numeric id, GitLab puts it by tag.
|
||||
func (r *remoteAPI) updateRelease(owner, repo, id string, rel release) error {
|
||||
method, ep := "PATCH", r.releasePath(owner, repo)+"/"+id
|
||||
func (r *remoteAPI) updateRelease(owner, repo string, ref releaseRef, rel release) error {
|
||||
method, ep := "PATCH", r.releasePath(owner, repo)+"/"+ref.id
|
||||
if r.kind == kindGitLab {
|
||||
method, ep = "PUT", r.releaseByTagPath(owner, repo, rel.Tag)
|
||||
}
|
||||
@@ -164,9 +186,20 @@ func handleRelease(args string) {
|
||||
body = releaseNotes(DIR, tag)
|
||||
}
|
||||
|
||||
// ./bin and ./assets ride along when they are there; nothing to configure,
|
||||
// and nothing happens for a project that has neither
|
||||
assets, skipped := collectAssets(DIR)
|
||||
for _, s := range skipped {
|
||||
errorln("skipping " + s + ": another directory already contributes that name")
|
||||
}
|
||||
if len(assets) > 0 {
|
||||
fmt.Printf("%s %s %s\n", col(cGray, "attaching"), col(cYellow, assetSummary(assets)),
|
||||
col(cGray, "from "+strings.Join(assetDirList(DIR), " and ")))
|
||||
}
|
||||
|
||||
done := 0
|
||||
for _, t := range targets {
|
||||
if publishRelease(t, PRJ, tag, body) {
|
||||
if publishRelease(t, PRJ, tag, body, assets) {
|
||||
done++
|
||||
}
|
||||
}
|
||||
@@ -223,7 +256,7 @@ func releaseNotes(dir, tag string) string {
|
||||
// publishRelease pushes the tag to one mirror target and turns it into a
|
||||
// release there. It returns success, so one unreachable server does not stop
|
||||
// the remaining ones.
|
||||
func publishRelease(t RemoteTarget, repo, tag, body string) bool {
|
||||
func publishRelease(t RemoteTarget, repo, tag, body string, assets []releaseAsset) bool {
|
||||
api := newRemoteAPI(t.URL, t.Key, t.Type)
|
||||
|
||||
owner, err := api.authUser()
|
||||
@@ -248,17 +281,17 @@ func publishRelease(t RemoteTarget, repo, tag, body string) bool {
|
||||
}
|
||||
|
||||
rel := release{Tag: tag, Name: tag, Body: body, Prerelease: preReleaseRe.MatchString(tag)}
|
||||
id, found, err := api.findRelease(owner, repo, tag)
|
||||
ref, found, err := api.findRelease(owner, repo, tag)
|
||||
if err != nil {
|
||||
errorln(t.Name + ": " + err.Error())
|
||||
return false
|
||||
}
|
||||
if found {
|
||||
if err := api.updateRelease(owner, repo, id, rel); err != nil {
|
||||
if err := api.updateRelease(owner, repo, ref, rel); err != nil {
|
||||
errorln(t.Name + ": " + err.Error())
|
||||
return false
|
||||
}
|
||||
} else if err := api.createRelease(owner, repo, rel); err != nil {
|
||||
} else if ref, err = api.createRelease(owner, repo, rel); err != nil {
|
||||
errorln(t.Name + ": " + err.Error())
|
||||
return false
|
||||
}
|
||||
@@ -272,5 +305,12 @@ func publishRelease(t RemoteTarget, repo, tag, body string) bool {
|
||||
}
|
||||
fmt.Printf("%s %s %s %s\n", col(cGray, "remote"), col(cYellow, t.Name),
|
||||
col(cGreen, what), col(cCyan, api.repoWebURL(owner, repo)))
|
||||
|
||||
if len(assets) > 0 {
|
||||
if err := api.uploadAssets(owner, repo, ref, assets); err != nil {
|
||||
errorln(t.Name + ": " + err.Error())
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user