Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:
name: Build and push to GHCR
needs: verify
runs-on: ubuntu-latest
# A normal build takes ~4 minutes. Fail loudly on a stall instead of
# hanging for GitHub's 6-hour default with `:latest` silently stale.
timeout-minutes: 25
permissions:
contents: read
packages: write
Expand Down Expand Up @@ -113,8 +116,16 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Separate cache scopes for branch and tag builds. A release pushes
# `main` and the `vX.Y.Z` tag seconds apart, so both builds run at
# once (different refs, different concurrency groups); writing the
# same gha cache scope concurrently stalled the `:latest` build for
# 12+ minutes on v1.8.0. Tag builds still READ main's cache, so they
# stay fast, but each writes only its own scope.
cache-from: |
type=gha,scope=main
type=gha,scope=${{ github.ref_type == 'tag' && 'release' || 'main' }}
cache-to: type=gha,mode=max,scope=${{ github.ref_type == 'tag' && 'release' || 'main' }}

# NOTE: the GitHub Release itself is created by release-please.yml (with a
# changelog grouped by feat/fix/deps), not here — it creates the git tag as a
Expand Down
47 changes: 43 additions & 4 deletions API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,49 @@ All request/response bodies are JSON unless noted otherwise.
- Auth: cookie.
- Path param: `name` — container name.
- Body: none.
- Response: `200 { "streamId": "string" }` — starts a pull + recreate
operation for that container; use the returned `streamId` to subscribe to
progress via the SSE endpoint below.
- Response: `200 { "streamId": "string", "job": {...} }` — queues a pull +
recreate for that container on the server (see `GET /api/jobs`); subscribe to
progress via the SSE endpoint below. The stream replays the whole log on every
(re)connect and is kept for 15 minutes after the job finishes.
- Errors: `404` if no such container; `409` if an update is already in
progress for that container.
- Note: after `up -d`, the container is health-checked; if it doesn't come up
healthy the result is reported as `success:false` with an actionable message
(and a rollback point is recorded).

### `POST /api/update-all`

- Auth: cookie. Body: `{ "names": [string] }` (container names, ≤ 500).
- Queues an update for each, as one batch, run on the server (one at a time
within a compose project, up to 3 at once overall) — nothing needs to stay
connected. When the last finishes, one summary notification is sent (if
notifications and `notifyBatchSummary` are on, and the batch had ≥ 2).
- Response: `200 { "batchId", "queued": [name], "skipped": [{ "name", "reason" }] }`;
`400 invalid_payload`.

### `GET /api/jobs`

- Auth: cookie. The server's update queue: queued/running jobs plus those
finished in the last 15 minutes, and the latest batch.
- Response: `200 { "jobs": [{ "id", "name", "kind": "update"|"revert"|"switch", "project", "batchId", "status": "queued"|"running"|"done", "waitingOn": "stack"|"slot"|null, "queuedAt", "startedAt", "finishedAt", "success", "message", "tag"? }], "batch": { "id", "total", "done", "succeeded": [name], "failed": [{ "name", "message" }], "startedAt", "finishedAt" }|null }`.
- `GET /api/events` emits `{"type":"jobs-changed"}` whenever this changes.

### `PUT /api/containers/:name/auto-revert`

- Auth: cookie. Body: `{ "enabled": boolean }`. When on, an update or tag switch
of this container that comes up unhealthy is reverted automatically in the
same job. Container items report it as `autoRevert`.

### `GET /api/containers/:name/logs`

- Auth: cookie. Query: `tail` (default 200, max 1000). The container's own
recent stdout/stderr. Response: `200 { "text", "state", "restartCount" }`.

### `GET /api/history/entry/:id/log`

- Auth: cookie. The saved live log of one update (history rows carry
`has_log`). Response: `200 { "log" }`; `404 not_found`.

### `POST /api/update/:name/revert`

- Auth: cookie.
Expand All @@ -128,7 +162,7 @@ All request/response bodies are JSON unless noted otherwise.
pull/up fails before the container changed, the file is restored. Standalone:
pulls the new tag and recreates the container on it. The rollback point records
the compose edit, so a revert puts the old tag back in the file.
- Response: `200 { "streamId": "string" }` (stream as for updates).
- Response: `200 { "streamId": "string", "job": {...} }` (stream as for updates).
- Errors: `400 invalid_tag`; `409 tag_not_offered`; `409 update_in_progress`;
`404 not_found`.

Expand Down Expand Up @@ -399,6 +433,11 @@ Field notes:
tag of a higher major version, only when the `tagUpdates` setting is `major`.
Both respect a skipped tag (`POST /api/skip-tag`) and are `null` when
`tagUpdates` is `off`.
- `downloadSize` — `{ "bytes": number, "exact": boolean }` for a pending (or
skipped) update: the compressed size of the new image's layers the running
image doesn't have; `exact: false` means the running image's manifest
couldn't be read, so it's the whole image (an upper bound). `null` if unknown.
- `autoRevert` — per-container auto-revert (see `PUT .../auto-revert`).
- `skipped` — `true` when an update exists but the user skipped that exact
build (`POST /api/skip`); `updateAvailable` is then `false`, while
`availableDigest`/`availableVersion` still describe the skipped build.
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,17 @@ If the paths don't match you'll get `compose file not found` and broken bind mou
- **Updates tab** — containers grouped by stack, update-available ones on top.
Defaults to showing only what needs updating; flip to **All** to see everything.
Tap **Update** to pull + recreate that service (watch live logs), or **Update all**
to run them (one at a time within each stack). After an update DockPull verifies the
to run them (one at a time within each stack, up to 3 stacks at once). Updates run
**on the server**: leaving the page, locking your phone or closing the app doesn't
stop them, and any page (or another device) shows progress — the header reads
"Updating · 2/5 done", and cards pick their live log back up when you return.
Each update shows its **download size** (only the layers you don't already have). After an update DockPull verifies the
container actually comes up healthy — waiting as long as the container's own
healthcheck needs, and catching apps that crash a few seconds after starting — and
offers a one-click **Revert** to the previous image if it doesn't. **Pin Version**
offers a one-click **Revert** to the previous image if it doesn't. Tick
**Auto-revert if unhealthy** on a card to have that happen automatically for that
container. **Container logs** shows the container's own recent output (why it
crash-loops, say). **Pin Version**
holds a container at its current version; **Skip** dismisses just the update on
offer, and the card returns when a newer build is published. An update marked
**(rebuilt)** has the same version number but a new image — the publisher re-pushed
Expand All @@ -91,12 +98,13 @@ If the paths don't match you'll get `compose file not found` and broken bind mou
then updates it with the usual health check. If the pull fails the file is put back,
and **Revert** restores the old tag in the file too. Images set via a variable
(`${TAG}`), an anchor, or a digest are left alone. "Update all" never switches tags.
- **History tab** — a log of past updates. **Clear history** wipes it (with a confirm).
- **History tab** — a log of past updates; expand an entry for **Show update log**
(the pull/recreate output, kept with the entry). **Clear history** wipes it.
- **Settings tab** — theme, default view, auto-check on open, newer-tag policy, the
**background scan** (daily at a time, or every N hours; a scan missed while the
server was off runs shortly after it starts), **notifications** (Discord, ntfy,
Gotify, or a generic webhook — with a "send test" button, and optional alerts when
an update fails), pinned versions, **prune**, **sign out everywhere**, and
Gotify, or a generic webhook — with a "send test" button, optional alerts when
an update fails, and a one-message summary when an Update all finishes), pinned versions, **prune**, **sign out everywhere**, and
**backup / restore** (settings, pins and history as a JSON file — it contains your
notification URL, so keep it private).
- **DockPull updates** — when a new DockPull release is out, a banner shows what
Expand Down
3 changes: 3 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Dashboard from './Dashboard.jsx';
import Header from './components/Header.jsx';
import BottomNav from './components/BottomNav.jsx';
import SelfUpdateBanner from './components/SelfUpdateBanner.jsx';
import { LiveProvider } from './hooks/useLive.jsx';
import HistoryPage from './pages/HistoryPage.jsx';
import SettingsPage from './pages/SettingsPage.jsx';

Expand Down Expand Up @@ -85,6 +86,7 @@ export default function App() {
}

return (
<LiveProvider>
<div className="app-shell">
<Header pendingCount={pendingCount} needsPruning={needsPruning} onLoggedOut={handleLoggedOut} />
<main className="app-main">
Expand All @@ -101,5 +103,6 @@ export default function App() {
</main>
<BottomNav needsPruning={needsPruning} />
</div>
</LiveProvider>
);
}
Loading
Loading