Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/writes.go
T
jschoubben 24b244bc88
mesh/delivery merged: source.main moved to novox/mesh-catalog@27a7757b970e
mesh/check pass 24b244bc88dc on 27a7757b970e | carried from 6b6d5cba7195 | unit 2046 run, 2327 kept, 6 skipped | integration 2066 run, 2346 kept, 10 s…
mesh-delivery-gate: ask no Approve of a merge the forge account cannot carry out, and say why (issue 1945)
car-hunter #2 was approved and its commit status then refused 403. Before the ask the
gate reads merge.writes for each repository it covers; where the merging account cannot
write, nothing is asked, the delivery waits on 'the forge account cannot write <repo>:
gitea_grant_write' and a condition is raised. gitea.repo.writable reads it again and
asks an approved, refused merge of it again.
2026-10-26 16:39:56 +01:00

116 lines
4.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// **The operator is asked only for a merge the forge account can carry out** (novox/hq issue 1945). car-hunter #2, a
// repository of a user, was approved, and its first act after the Approve, the commit status, was refused 403: the mesh's
// merging account (the gitea module's tools, mesh-tools) only read that repository. Before the gate asks the operator's
// Approve it reads, through the forge holder's gitea_commit_statuses, whether that account writes each repository the
// ask covers (merge.writes), as the ask is about to be written. Where it does not, nothing is asked: the delivery waits,
// saying "the forge account cannot write <repo>: gitea_grant_write", and the gate raises it as a condition at once.
//
// A repository not written is kept so, and the ask is not composed again, until the gitea module's event
// gitea.repo.writable (its grant, read back from the forge) or, as a safety net, writeReadLasts after the read. A forge
// that does not say merge.writes (an older gitea module) is taken as writing: the merge says its own refusal, as before.
// writeReadLasts is how long a read of whether the forge account writes a repository holds.
const writeReadLasts = 10 * time.Minute
// writableEvent is the gitea module's event of an account given write on a repository.
const writableEvent = "gitea.repo.writable"
// writeRead is what the forge last said of the merging account on one repository it cannot write.
type writeRead struct {
says string
at time.Time
}
// cannotWriteSays is what a delivery waits on when the forge account cannot write its repository.
func cannotWriteSays(repository string) string {
return "the forge account cannot write " + repository + ": gitea_grant_write"
}
// writeWaits is what an ask about to be made waits on: a repository it covers that the forge account was read, within
// writeReadLasts, not to write. Under the lock.
func (h *Holder) writeWaits(d *Delivery, members []*Delivery, now time.Time) string {
r := d.Ready
for _, m := range members {
repo := strings.ToLower(m.Repository)
w := h.writeReads[repo]
if w == nil {
continue
}
if !now.Before(w.at.Add(writeReadLasts)) {
delete(h.writeReads, repo) // read again at the ask
continue
}
if r.CannotWrite != w.says {
r.CannotWrite, r.CannotWriteSince = w.says, &now
h.keep(d)
}
h.lookAt(w.at.Add(writeReadLasts), "a repository the forge account could not write, read again")
return w.says
}
if r.CannotWrite != "" {
r.CannotWrite, r.CannotWriteSince = "", nil
h.keep(d)
}
return ""
}
// cannotWrite reads, at the ask, whether the forge account writes each repository the ask covers, and answers what the
// first one it cannot write says; a read that fails says nothing (the merge says its own refusal, as before). Outside the
// lock; a repository not written is kept, under the lock.
func (h *Holder) cannotWrite(reads []memberRead) string {
for _, m := range reads {
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
if err != nil || fc == nil || fc.Merge.Writes == nil || *fc.Merge.Writes {
continue
}
repo := strings.ToLower(m.owner + "/" + m.repo)
says := fc.Merge.CannotWrite
if says == "" {
says = cannotWriteSays(repo)
}
h.mu.Lock()
if h.writeReads == nil {
h.writeReads = map[string]*writeRead{}
}
h.writeReads[repo] = &writeRead{says: says, at: h.Now()}
h.mu.Unlock()
return says
}
return ""
}
// WriteGranted takes the gitea module's gitea.repo.writable: the repository's read is dropped, so the next look reads it
// again, and an approved merge of it that the forge refused is asked again, its cause gone.
func (h *Holder) WriteGranted(body json.RawMessage) error {
var e struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
}
if err := json.Unmarshal(body, &e); err != nil || e.Owner == "" || e.Repo == "" {
return fmt.Errorf("%s names no repository: %s", writableEvent, string(body))
}
repo := strings.ToLower(e.Owner + "/" + e.Repo)
h.mu.Lock()
defer h.mu.Unlock()
delete(h.writeReads, repo)
for _, d := range h.deliveries {
if strings.ToLower(d.Repository) != repo || d.Ready == nil || d.Ready.Approved == nil || d.Ready.Merged != nil {
continue
}
d.Ready.Repeats = 0
h.mergeAgainNow(d)
h.keep(d)
}
h.Logf("[mesh-delivery] the forge account was given write on %s: it is read again before an ask, and an approved merge of it is asked again", repo)
return nil
}