Dentolize · Foreground update-docs Walkthrough
On this pageBusiness viewTechnical view

In Depth: The Sandbox Docs Pipeline

Business view

Every pull request in this repository automatically gets two things:

  1. A sandbox — a private, fully-seeded, running copy of Dentolize at

https://<branch>.sandbox.anastawfik.com.

  1. A docs site — an AI-written walkthrough of that change (a site exactly

like the one you're reading) at https://<slug>.docs.anastawfik.com.

The docs site is produced by a docs agent — headless Claude Code — that reads the PR, drives the sandbox with a browser to capture real screenshots, and writes an audience-first walkthrough. There are two ways that agent runs:

  • Automatically, once, when the PR is opened. This happens quietly in the

background so the build system stays responsive.

  • On demand, whenever someone wants a fresh version. This is the

update-docs action — and it's the one this PR changes.

PR #354 only touches the on-demand path. It makes that refresh run in the open — you watch the agent work and know exactly when the site is live — instead of firing it off invisibly. The automatic path is deliberately left as fire-and-forget, because no human is sitting there watching a PR-open job.

Think of it as the difference between:

  • Old: handing a task to someone and immediately marking it "done" on your

board because you handed it off.

  • New: keeping the task "in progress" until it's genuinely finished, and being

able to watch it happen.

Technical view

The three workflows

The pipeline is three GitHub Actions workflows, all pinned to the homelab self-hosted runner (runs-on: [self-hosted, homelab, dentolize]):

WorkflowTriggerRole
sandbox.ymlpull_request (opened/sync/reopened/closed)Deploy/destroy the sandbox; auto-generate docs on open (detached)
sandbox-ops.ymlworkflow_dispatchManual ops incl. update-docs (the file this PR changes)
sandbox-qa.ymlworkflow_dispatchAI QA agent (review/plan/test/fix), timeout-minutes: 300

The docs CLI: two verbs

Both docs paths shell out to the same on-runner CLI, /opt/homelab/sandbox/bin/sandbox-docs (which is not in this repo — its internals aren't verifiable from source):

  • generate — used by auto-generation on PR open:

``yaml # .github/workflows/sandbox.yml:66 /opt/homelab/sandbox/bin/sandbox-docs dentolize "${{ github.head_ref }}" generate --detach || true ` Runs **detached**; the || true means even a launch failure won't fail the PR's deploy job. Guarded by if: github.event.action == 'opened' (.github/workflows/sandbox.yml:59`), so it fires only on first open, not on every push.

  • refresh — used by the manual update-docs action:

``yaml # .github/workflows/sandbox-ops.yml:65 (AFTER this PR) update-docs) /opt/homelab/sandbox/bin/sandbox-docs dentolize "${{ github.ref_name }}" refresh ;; ` Now runs in the **foreground** (the --detach flag was removed). Per the code comment (:45–46), refresh` may also deploy a missing sandbox first, which is why the timeout was raised to 90 minutes.

Where the finished site is announced

The docs URL is surfaced once, in the PR-open comment builder:

// .github/workflows/sandbox.yml:95
`**Docs:** https://${slug}.docs.anastawfik.com _(AI walkthrough — generates after PR open, refresh via Sandbox Ops → update-docs)_`,

Note what this implies: the only PR-thread mention of docs comes from sandbox.yml at open time. The update-docs action itself posts no comment (see below), so a refresh updates the site silently.

Why update-docs is comment-silent

In sandbox-ops.yml, two guards keep update-docs from touching the PR comment that the other ops (reseed, reset-data, redeploy, destroy) maintain:

# .github/workflows/sandbox-ops.yml:67 — info capture skipped for update-docs
if [ "${{ inputs.action }}" != "destroy" ] && [ "${{ inputs.action }}" != "update-docs" ]; then
  /opt/homelab/sandbox/bin/sandbox dentolize "${{ github.ref_name }}" info >> "$GITHUB_OUTPUT"
fi
# .github/workflows/sandbox-ops.yml:72 — comment step skipped for update-docs
- name: Refresh sandbox comment on the branch PR
  if: inputs.action != 'update-docs'

So the docs refresh has exactly two observable outputs: the job log and the updated site.

The end-to-end flow

                         ┌───────────────────────── sandbox.yml (pull_request) ─────────────────────────┐
 PR opened ─────────────►│ deploy sandbox ──► comment URLs/creds ──► generate --detach (docs, PR-open)   │
 new commit ────────────►│ deploy sandbox (cancel-in-progress: true)                                     │
 PR closed ─────────────►│ destroy sandbox                                                               │
                         └───────────────────────────────────────────────────────────────────────────── ┘

                         ┌───────────────────── sandbox-ops.yml (workflow_dispatch) ────────────────────┐
 pick branch + action ──►│ reseed / reset-data / redeploy / destroy  ──► refresh PR comment              │
                         │ update-docs ──► sandbox-docs … refresh  (FOREGROUND) ──► site published        │  ◄── THIS PR
                         └───────────────────────────────────────────────────────────────────────────── ┘

Cross-reference

The mechanics of why foreground vs. detached matters — runner slots, concurrency, timeouts — are covered in Foreground Execution & Runner Slots.