Merge pull request 'fix(worker): commit as the token's account and check it can push' (#11) from fix/worker-bot-identity into main
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
commit
425c242ae7
3 files changed
+100
-3
No files matched your search
+1
-1
@@ -42,7 +42,7 @@ All through environment variables; nothing about where it runs lives in this rep
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `PANEL_URL` | The panel the worker takes jobs from |
|
| `PANEL_URL` | The panel the worker takes jobs from |
|
||||||
| `BUILDER_TOKEN` | Shared secret the panel checks on `/api/v1/builder/**` (same value in the panel's env) |
|
| `BUILDER_TOKEN` | Shared secret the panel checks on `/api/v1/builder/**` (same value in the panel's env) |
|
||||||
| `GITEA_TOKEN` | The bot account's token (repository read/write on this repo): pushes draft branches, opens PRs |
|
| `GITEA_TOKEN` | The bot account's token (repository read/write; the account needs Write on this repo): pushes draft branches, opens PRs as that account. Checked when the worker starts |
|
||||||
| `OLLAMA_URL` | Ollama for the model (or `MODEL_PROVIDER=gemini` with `GEMINI_API_KEY`) |
|
| `OLLAMA_URL` | Ollama for the model (or `MODEL_PROVIDER=gemini` with `GEMINI_API_KEY`) |
|
||||||
| `SEARXNG_URL`, `READER_URL` | A SearXNG instance (JSON API) and a page-to-text reader (`READER_URL/<page url>`) |
|
| `SEARXNG_URL`, `READER_URL` | A SearXNG instance (JSON API) and a page-to-text reader (`READER_URL/<page url>`) |
|
||||||
| `MODEL`, `POLL_INTERVAL`, `WORKER_NAME`, `WORK_DIR` | Optional: model name, poll interval (20s), name shown in the panel, work folder |
|
| `MODEL`, `POLL_INTERVAL`, `WORKER_NAME`, `WORK_DIR` | Optional: model name, poll interval (20s), name shown in the panel, work folder |
|
||||||
|
|||||||
+52
-2
@@ -49,6 +49,53 @@ type workerConfig struct {
|
|||||||
workDir, name string
|
workDir, name string
|
||||||
poll time.Duration
|
poll time.Duration
|
||||||
provider, model string
|
provider, model string
|
||||||
|
botLogin, botEmail string // the token's account, used as the commit author
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkBot reads which account GITEA_TOKEN belongs to and makes sure it can push to the games repo, so a
|
||||||
|
// wrong token or missing permission shows when the worker starts, not at the end of a 30-minute build.
|
||||||
|
func (c *workerConfig) checkBot() error {
|
||||||
|
var user struct {
|
||||||
|
Login string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
if err := c.giteaGet("/user", &user); err != nil {
|
||||||
|
return fmt.Errorf("GITEA_TOKEN: %w", err)
|
||||||
|
}
|
||||||
|
var repo struct {
|
||||||
|
Permissions struct {
|
||||||
|
Push bool `json:"push"`
|
||||||
|
} `json:"permissions"`
|
||||||
|
}
|
||||||
|
if err := c.giteaGet("/repos/"+c.repo, &repo); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", c.repo, err)
|
||||||
|
}
|
||||||
|
if !repo.Permissions.Push {
|
||||||
|
return fmt.Errorf("the account %s can't push to %s: give it Write access there", user.Login, c.repo)
|
||||||
|
}
|
||||||
|
c.botLogin, c.botEmail = user.Login, user.Email
|
||||||
|
if c.botEmail == "" {
|
||||||
|
c.botEmail = user.Login + "@users.noreply"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *workerConfig) giteaGet(p string, out any) error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, c.gitea+"/api/v1"+p, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "token "+c.botToken)
|
||||||
|
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("HTTP %d: %.200s", resp.StatusCode, raw)
|
||||||
|
}
|
||||||
|
return json.NewDecoder(resp.Body).Decode(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runWorker(args []string) error {
|
func runWorker(args []string) error {
|
||||||
@@ -71,7 +118,10 @@ func runWorker(args []string) error {
|
|||||||
return fmt.Errorf("set %s", v.name)
|
return fmt.Errorf("set %s", v.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("[worker] %s polling %s every %s\n", c.name, c.panel, c.poll)
|
if err := c.checkBot(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("[worker] %s polling %s every %s; pull requests as %s\n", c.name, c.panel, c.poll, c.botLogin)
|
||||||
for {
|
for {
|
||||||
job, err := c.claim()
|
job, err := c.claim()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -313,7 +363,7 @@ func (c *workerConfig) openPR(dir string, job *builderJob, res *buildResult) (st
|
|||||||
}{
|
}{
|
||||||
{"checkout", []string{"checkout", "-q", "-b", branch}},
|
{"checkout", []string{"checkout", "-q", "-b", branch}},
|
||||||
{"add", []string{"add", filepath.ToSlash(res.Dir)}},
|
{"add", []string{"add", filepath.ToSlash(res.Dir)}},
|
||||||
{"commit", []string{"-c", "user.name=gbr-bot", "-c", "user.email=gbr-bot@users.noreply.gitea.gbreed.com", "commit", "-q", "-m",
|
{"commit", []string{"-c", "user.name=" + c.botLogin, "-c", "user.email=" + c.botEmail, "commit", "-q", "-m",
|
||||||
fmt.Sprintf("feat(games): draft %s\n\nDrafted and boot-tested by the GBR builder (job %s).", job.GameName, job.ID)}},
|
fmt.Sprintf("feat(games): draft %s\n\nDrafted and boot-tested by the GBR builder (job %s).", job.GameName, job.ID)}},
|
||||||
{"push", []string{"-c", auth, "push", "-q", "origin", branch}},
|
{"push", []string{"-c", auth, "push", "-q", "origin", branch}},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckBotNeedsPushAccess(t *testing.T) {
|
||||||
|
push := false
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("Authorization") != "token secret" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/user":
|
||||||
|
w.Write([]byte(`{"login":"git_ai_bot","email":"bot@example.com"}`))
|
||||||
|
case "/api/v1/repos/org/games":
|
||||||
|
if push {
|
||||||
|
w.Write([]byte(`{"permissions":{"push":true}}`))
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(`{"permissions":{"push":false}}`))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := &workerConfig{gitea: srv.URL, repo: "org/games", botToken: "secret"}
|
||||||
|
if err := c.checkBot(); err == nil || !strings.Contains(err.Error(), "git_ai_bot can't push to org/games") {
|
||||||
|
t.Errorf("read-only bot: got %v", err)
|
||||||
|
}
|
||||||
|
push = true
|
||||||
|
if err := c.checkBot(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.botLogin != "git_ai_bot" || c.botEmail != "bot@example.com" {
|
||||||
|
t.Errorf("identity = %s <%s>", c.botLogin, c.botEmail)
|
||||||
|
}
|
||||||
|
c.botToken = "wrong"
|
||||||
|
if err := c.checkBot(); err == nil || !strings.Contains(err.Error(), "GITEA_TOKEN") {
|
||||||
|
t.Errorf("bad token: got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user