Docker Build / docker (push) Successful in 38s
## Summary
- `POST /update` on the master (outside OIDC, bearer `UPDATER_API_KEY`; unset → 503). Matches `image` against `image_repo` on all agents, marks `available_*` on every match, starts webhook-triggered jobs for `auto` services targeting `<image_repo>:<tag>` (followed tag when no tag is given), and waits for them up to `UPDATE_TIMEOUT` (default 5m). 404 on no match, 500 if any auto job failed or timed out, otherwise 200.
- Manual/auto toggle per service on the overview and service page (`POST /services/{id}/mode`, CSRF); auto is blocked for self, `build:` and container services with the reason shown. "Update available" badge and an "Available" row.
- `FinishJob` clears `available_*` only when the successful job was created for that exact ref, after it was reported. Migration `0003_webhook.sql`, phase-3 plan doc, README / gitea-action README / CLAUDE.md / deploy example updated. `action.yml` unchanged.
Closes #8
## Behaviour changes for CI
- Mixed results now return 500 (the old 207 is gone); no match returns 404 instead of 200.
- With a `tag`, Compose services get pinned to `<image_repo>:<tag>` via `compose.override.yaml`; the tag must be a pushed tag.
- `busy` (job already active) and `refused` (not updatable) are reported but return 200.
- The reverse proxy read timeout must exceed `UPDATE_TIMEOUT`.
## Test plan
- [x] `go test ./...` and `go test -race ./internal/agent/... ./internal/master/... ./internal/e2e/`
- [x] `go test -tags integration ./internal/master/store/` against throwaway Postgres
- [x] UI verified in browser (mode toggle, disabled reason, badge via SSE, Available row)
- [ ] Manual: webhook with manual service → `marked` + badge
- [ ] Manual: webhook with auto service → waits, `succeeded`, badge cleared, override pinned
- [ ] Manual: wrong token 401, unknown image 404, offline agent 500
- [ ] Gitea action pointed at the master's `/update` in a real CI run
Reviewed-on: #22
39 lines
1.5 KiB
HTML
39 lines
1.5 KiB
HTML
{{define "content"}}
|
|
<h1>Services</h1>
|
|
<div id="services" hx-get="/fragments/services" hx-trigger="sse:services" hx-swap="innerHTML">
|
|
{{template "services" .}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "services"}}
|
|
{{range .Groups}}
|
|
<section>
|
|
<h2>{{.Title}}</h2>
|
|
<table>
|
|
<thead><tr><th>Service</th><th>Image</th><th>Status</th><th>Mode</th></tr></thead>
|
|
<tbody>
|
|
{{range .Services}}
|
|
<tr>
|
|
<td><a href="/services/{{.ID}}">{{.DisplayName}}</a>{{if .Self}} <span class="badge">managed outside</span>{{end}}{{if .AvailableRef}} <span class="badge badge-update" title="{{.AvailableRef}}">update available</span>{{end}}</td>
|
|
<td><code>{{shortRef .ActualRef}}</code></td>
|
|
<td class="status status-{{.Status}}">{{.Status}}</td>
|
|
<td>{{.UpdateMode}}
|
|
<form method="post" action="/services/{{.ID}}/mode" class="mode">
|
|
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
|
<input type="hidden" name="return" value="overview">
|
|
{{if eq .UpdateMode "auto"}}<input type="hidden" name="mode" value="manual">
|
|
<button type="submit">Make manual</button>
|
|
{{else}}<input type="hidden" name="mode" value="auto">
|
|
<button type="submit"{{if .AutoBlocked}} disabled{{end}}>Make auto</button>{{if .AutoBlocked}} <span class="muted">{{.AutoBlocked}}</span>{{end}}{{end}}
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{{else}}
|
|
<p>No services reported yet. <a href="/agents">Add an agent</a> to get started.</p>
|
|
{{end}}
|
|
{{end}}
|