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
34 lines
1.9 KiB
CSS
34 lines
1.9 KiB
CSS
:root { --bg: #111418; --fg: #e6e8eb; --muted: #8a929c; --line: #262b31; --ok: #3fb950; --warn: #d29922; --bad: #f85149; --accent: #58a6ff; }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg: #ffffff; --fg: #1f2328; --muted: #59636e; --line: #d1d9e0; --ok: #1a7f37; --warn: #9a6700; --bad: #cf222e; --accent: #0969da; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, sans-serif; }
|
|
nav { display: flex; gap: 16px; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--line); }
|
|
nav form { margin-left: auto; }
|
|
main { padding: 16px; max-width: 1200px; margin: 0 auto; }
|
|
a { color: var(--accent); }
|
|
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
|
|
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
th { color: var(--muted); font-weight: 500; }
|
|
code, pre { font: 13px ui-monospace, monospace; }
|
|
pre { padding: 12px; border: 1px solid var(--line); overflow-x: auto; }
|
|
.badge { font-size: 12px; color: var(--muted); border: 1px solid var(--line); padding: 0 6px; border-radius: 8px; }
|
|
.status-running { color: var(--ok); }
|
|
.status-degraded, .status-unknown { color: var(--warn); }
|
|
.status-stopped { color: var(--bad); }
|
|
.error { color: var(--bad); }
|
|
.warning { color: var(--warn); }
|
|
form.inline { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
@media (max-width: 600px) { table { display: block; overflow-x: auto; } }
|
|
.muted { color: var(--muted); }
|
|
.status-job-succeeded { color: var(--ok); }
|
|
.status-job-queued, .status-job-running { color: var(--warn); }
|
|
.status-job-failed { color: var(--bad); }
|
|
table.details { width: auto; }
|
|
table.details th { padding-right: 24px; }
|
|
pre.log { white-space: pre-wrap; }
|
|
.badge-update { color: var(--warn); border-color: var(--warn); }
|
|
form.mode { display: inline; margin-left: 8px; }
|
|
form.mode button { font-size: 12px; padding: 0 6px; }
|