Dentolize ยท PR Template Sandbox Block Walkthrough
On this pageBusiness viewTechnical view

How an edit triggers a redeploy

Business view

You don't need to remember to "redeploy" after changing the Sandbox settings โ€” it happens automatically. Open a PR, and it deploys. Change the YAML block afterward (say, you realize you need whatsapp: true), and it redeploys with the new settings. Change something else in the description โ€” fix a typo, add more context, answer a reviewer's question โ€” and nothing redeploys; the existing sandbox just keeps running. The system is deliberately lazy: it only pays the ~10-30 minute deploy cost when the actual settings changed.

Technical view

This logic already existed in .github/workflows/sandbox.yml before this PR โ€” the template change doesn't touch it, but understanding it is what makes the block in the template meaningful rather than decorative.

pull_request triggers

sandbox.yml:19-24 fires on opened, synchronize, reopened, edited, closed. synchronize covers ordinary new commits; edited is the one that matters for the settings block, since it fires whenever the PR title or body changes.

The gate job decides whether to deploy

sandbox.yml:44-78, job gate, step Decide (id: decide):

if [ "$ACTION" != "edited" ]; then
  echo "deploy=true"          # opened/synchronize/reopened
elif [ "$BODY_CHANGED" != "true" ]; then
  echo "deploy=false"         # title-only edit
elif [ "$(extract "$NEW_BODY")" != "$(extract "$OLD_BODY")" ]; then
  echo "deploy=true"          # sandbox block changed
else
  echo "deploy=false"         # body edit, block unchanged
fi

(sandbox.yml:69-77)

opened, synchronize, and reopened always deploy. For edited, it only deploys if the description text actually changed (BODY_CHANGED, sourced from github.event.changes.body != null, sandbox.yml:55) and the extracted Sandbox block differs before vs. after (sandbox.yml:73). Editing unrelated text in the description โ€” or retitling the PR โ€” does not trigger a redeploy.

The extract() helper (sandbox.yml:60-68) is an awk state machine: find a ##-level heading whose text contains "sandbox" (case-insensitive), then capture everything between the next fenced-code-block markers. It's duplicated (not shared) in the deploy job's parse step (sandbox.yml:97-103) โ€” same logic, run twice because the gate job only has access to old/new PR body text, while the deploy job needs the parsed block from the current body.

The deploy job parses and applies it

sandbox.yml:80-138, gated by needs.gate.outputs.deploy == 'true' (sandbox.yml:82):

  1. Parse sandbox settings from PR description (sandbox.yml:86-110)

re-extracts the block from github.event.pull_request.body, then:

  • if the block is non-empty and yq is installed, converts YAML โ†’ JSON

into $RUNNER_TEMP/sandbox-settings.json (sandbox.yml:104-105);

  • otherwise writes {} (sandbox.yml:107) โ€” no block means all

defaults.

  • The parsed JSON is echoed into the job log inside a collapsible

::group::๐Ÿงช parsed sandbox settings (sandbox.yml:110), useful for debugging a malformed block.

  1. Deploy sandbox for <branch> (sandbox.yml:112-138) runs

/opt/homelab/sandbox/bin/sandbox dentolize <branch> deploy, reading SANDBOX_SETTINGS_FILE from the environment (inherited from step 1's $GITHUB_ENV write, sandbox.yml:109). The actual interpretation of two_regions / whatsapp / cron / seed / env happens inside that CLI binary, which lives on the self-hosted runner outside this repository โ€” not verifiable from packages/ source.

  1. PR_BODY is passed via env:, never string-interpolated into the shell

script (sandbox.yml:88-90 comment: "avoid script injection from the body") โ€” a PR author fully controls the body text, so treating it as trusted shell input would be a command-injection vector.

The settings summary line

SETTINGS_SUMMARY itself is produced by sandbox โ€ฆ info (sandbox.yml:147), i.e. by the same external CLI, and surfaces in two places:

  • The job summary table, always: | **Settings** | $SETTINGS_SUMMARY |

(sandbox.yml:211).

  • The PR comment, only conditionally

(sandbox.yml:292): settingsSum && settingsSum !== "defaults (no settings block)". So a PR that never touches the Sandbox block gets a comment with no _settings: โ€ฆ_ line at all; only a PR with at least one non-default value shows it. This is the mechanism behind the settings: two_regions=false whatsapp=false cron=true env_overrides=0 line shown in the Walkthrough โ€” reproduced there since a PR using exactly the template's own defaults, like this one's sandbox, would in practice not show that line at all (a subtlety worth knowing if you go looking for it on a default-settings PR and don't find it).

Docs regeneration is a separate, one-shot step

sandbox.yml:149-157 kicks off sandbox-docs โ€ฆ generate --detach โ€” but only if: github.event.action == 'opened'. Editing the Sandbox block on an already-open PR redeploys the sandbox but does not regenerate this docs site; that's a manual "Sandbox Ops โ†’ update-docs" action (referenced in the PR comment text at sandbox.yml:300).