mandrel-platform 0.20.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # ACTIONS_RUNNER_HOOK_JOB_STARTED hook — generalized, runner-scoped hygiene
4
+ # for PERSISTENT self-hosted runners (mandrel-platform runner kit).
5
+ #
6
+ # A persistent runner (launchd/systemd service) leaks state from a prior job
7
+ # into the next one: the runner does not always reap the job's child process
8
+ # tree, and some actions leave files in shared locations. Observed breakage
9
+ # classes this hook guards against:
10
+ #
11
+ # - an orphaned `pnpm`/`node` process (e.g. a hung install or lint) still
12
+ # mutating the pnpm shim install, corrupting the pnpm CLI for the next job;
13
+ # - leftover `gitleaks.tmp` / `gitleaks-*` artifacts in the shared $TMPDIR
14
+ # blocking the next gitleaks download.
15
+ #
16
+ # Running this before every job gives each job a clean slate ("fresh per job"
17
+ # without the cost of re-registering an ephemeral runner).
18
+ #
19
+ # ── CONCURRENCY SAFETY (the load-bearing design constraint) ─────────────────
20
+ #
21
+ # Multiple runners on one host typically run as the SAME OS user, so anything
22
+ # under $HOME (notably `~/setup-pnpm`, the pnpm/action-setup DEFAULT install
23
+ # destination) is SHARED across all co-resident runners. A hook that reaps
24
+ # processes matching `~/setup-pnpm` or `rm -rf`s it will destroy a pnpm
25
+ # install a CONCURRENT runner is mid-flight on. This hook therefore:
26
+ #
27
+ # 1. NEVER touches `~/setup-pnpm` or any other $HOME-shared pnpm path.
28
+ # The pnpm shim MUST instead be runner-scoped at install time: the
29
+ # platform's `setup-toolchain` composite action already defaults
30
+ # pnpm/action-setup's `dest` to `${{ runner.temp }}/pnpm` (i.e.
31
+ # `<RUNNER_DIR>/_work/_temp/pnpm`, unique per runner), and
32
+ # `pr-quality.yml` exposes a `pnpm-dest` input for explicit overrides.
33
+ # See templates/runbooks/runner-provisioning.md § "pnpm scoping".
34
+ # 2. Reaps ONLY processes whose command line resolves inside THIS runner's
35
+ # own work tree (`<RUNNER_DIR>/_work/...`). Every path below derives
36
+ # from RUNNER_DIR, which is unique per runner, so a co-resident
37
+ # runner's processes and files are never matched.
38
+ # 3. Age-gates cleanup of the genuinely shared $TMPDIR gitleaks artifacts,
39
+ # so a fresh (in-flight) download owned by a concurrent job is never
40
+ # deleted — only stale leftovers are.
41
+ #
42
+ # ── PARAMETERIZATION ────────────────────────────────────────────────────────
43
+ #
44
+ # No hardcoded usernames, repo names, or runner names. All paths derive from:
45
+ #
46
+ # RUNNER_DIR — the runner's root directory. Defaults to the directory
47
+ # containing this script (the kit installs the hook into the
48
+ # runner root, next to config.sh / run.sh). Override via env
49
+ # only if you install the hook elsewhere.
50
+ # RUNNER_TMP — the runner's per-runner job temp (`runner.temp`), always
51
+ # `${RUNNER_DIR}/_work/_temp`.
52
+ # JOB_CLEANUP_STALE_MINUTES
53
+ # — age threshold (minutes) for the shared-$TMPDIR gitleaks
54
+ # sweep. Default 60. Artifacts younger than this are assumed
55
+ # in-flight and left alone.
56
+ #
57
+ # Configured via `ACTIONS_RUNNER_HOOK_JOB_STARTED=<RUNNER_DIR>/job-cleanup.sh`
58
+ # in the runner's `.env` (see .env.example in this directory).
59
+ #
60
+ # NEVER fails the job — best-effort cleanup, always exits 0.
61
+
62
+ set +e
63
+
64
+ RUNNER_DIR="${RUNNER_DIR:-$(cd "$(dirname "$0")" && pwd)}"
65
+ RUNNER_WORK="${RUNNER_DIR}/_work"
66
+ RUNNER_TMP="${RUNNER_WORK}/_temp"
67
+ TMP="${TMPDIR:-/tmp}"
68
+ STALE_MINUTES="${JOB_CLEANUP_STALE_MINUTES:-60}"
69
+
70
+ # 1) Reap orphaned pnpm/node processes from prior jobs — scoped to THIS
71
+ # runner's work tree only. The patterns target executable paths INSIDE the
72
+ # runner-scoped install locations (`.../node_modules`), so they match the
73
+ # actual pnpm/node binaries that ran from these dirs — not a shell that
74
+ # merely references the path. RUNNER_TMP and RUNNER_WORK are unique per
75
+ # runner, so co-resident runners and unrelated user processes are never
76
+ # hit. The shared `~/setup-pnpm` is deliberately NOT a reap target (see
77
+ # the concurrency-safety header).
78
+ pkill -9 -f "${RUNNER_TMP}/pnpm/node_modules" 2>/dev/null
79
+ pkill -9 -f "${RUNNER_TMP}/setup-pnpm/node_modules" 2>/dev/null
80
+ pkill -9 -f "${RUNNER_WORK}/_tool/[^ ]*node_modules" 2>/dev/null
81
+
82
+ # 2) Remove stale runner-scoped pnpm shim installs so the next job's
83
+ # pnpm/action-setup starts from a clean slate. Only paths under THIS
84
+ # runner's `_work/_temp` are deleted — never `~/setup-pnpm`.
85
+ rm -rf "${RUNNER_TMP}/pnpm" 2>/dev/null
86
+ rm -rf "${RUNNER_TMP}/setup-pnpm" 2>/dev/null
87
+
88
+ # 3) Sweep stale gitleaks artifacts from the SHARED $TMPDIR. Because this
89
+ # location is shared by every runner on the host, deletion is age-gated:
90
+ # only artifacts older than STALE_MINUTES are removed, so a concurrent
91
+ # runner's in-flight download is never deleted mid-job.
92
+ find "${TMP}" -maxdepth 1 -name 'gitleaks.tmp' -mmin "+${STALE_MINUTES}" \
93
+ -exec rm -f {} + 2>/dev/null
94
+ find "${TMP}" -maxdepth 1 -name 'gitleaks-*' -mmin "+${STALE_MINUTES}" \
95
+ -exec rm -rf {} + 2>/dev/null
96
+
97
+ exit 0
@@ -0,0 +1,77 @@
1
+ name: deploy-staging-run
2
+
3
+ # Canonical staging-deploy caller template — DEPLOY half (Story #272).
4
+ #
5
+ # > **Why this file runs on `workflow_dispatch`.** Its sibling
6
+ # > `deploy-staging.yml` fires on CI-green (`workflow_run`) and DISPATCHES this
7
+ # > workflow. The deploy lives here, on `workflow_dispatch`, because the shared
8
+ # > `deploy-cloudflare.yml`'s `environment:`-gated jobs (`check-env` /
9
+ # > `migration` / `deploy` / `boot-smoke`) are silently SKIPPED on a
10
+ # > `workflow_run` event but run normally on `workflow_dispatch` (Story #272).
11
+ # > Adopt this file together with `deploy-staging.yml`; `platform-sync`
12
+ # > materializes both.
13
+ #
14
+ # > **Thin local caller.** The defence-in-depth deploy core lives in the shared
15
+ # > `dsj1984/mandrel-platform` `deploy-cloudflare.yml` reusable workflow — see
16
+ # > https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml.
17
+ # > This file only holds <PROJECT_NAME>-specific values (worker names, build
18
+ # > step, secret mapping). When the deploy PROCESS changes, that change lands
19
+ # > upstream in mandrel-platform — not here.
20
+ #
21
+ # Replace every <PLACEHOLDER> with your project's real values:
22
+ # <MANDREL_PLATFORM_SHA> the pinned mandrel-platform commit SHA (resolve via
23
+ # `node scripts/platform-sync.mjs --ref <release-tag>`
24
+ # from the consumer repo root, or `git ls-remote`).
25
+ # <MANDREL_PLATFORM_TAG> the human-readable release tag matching the SHA
26
+ # above (trailing `# <tag>` comment).
27
+ # <WORKERS_CSV> comma-separated Worker names for this env, e.g.
28
+ # "api,web".
29
+ # <BUILD_COMMAND> optional build command (omit build-command /
30
+ # build-artifact entirely if the deploy job's default
31
+ # checkout is build-ready).
32
+ #
33
+ # See the full input/secret contract:
34
+ # https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml
35
+
36
+ on:
37
+ # Dispatched by deploy-staging.yml on CI-green, and available for manual
38
+ # on-demand deploys (UI "Run workflow" + `gh workflow run`). The optional sha
39
+ # input records the CI-verified commit; the deploy itself runs against the
40
+ # main tip (`--ref main`).
41
+ workflow_dispatch:
42
+ inputs:
43
+ sha:
44
+ description: >
45
+ Commit SHA that passed CI (informational — the deploy runs against
46
+ the current main tip). Populated automatically when dispatched by
47
+ deploy-staging.yml.
48
+ required: false
49
+ type: string
50
+
51
+ permissions:
52
+ contents: read
53
+
54
+ # Serialize staging deploys: only the freshest dispatch should reach the
55
+ # staging surfaces. The shared deploy-cloudflare.yml additionally serializes
56
+ # per-environment.
57
+ concurrency:
58
+ group: deploy-staging-run
59
+ cancel-in-progress: true
60
+
61
+ jobs:
62
+ deploy:
63
+ name: Staging deploy (shared deploy-cloudflare.yml)
64
+ uses: dsj1984/mandrel-platform/.github/workflows/deploy-cloudflare.yml@<MANDREL_PLATFORM_SHA> # <MANDREL_PLATFORM_TAG>
65
+ with:
66
+ environment: staging
67
+ gh-environment: staging
68
+ workers: <WORKERS_CSV>
69
+ migrate: true
70
+ # db-engine defaults to 'd1'. Set db-engine + migrate-command +
71
+ # snapshot-command for a non-D1 engine (e.g. Turso) — see the contract
72
+ # doc's "command seams" section.
73
+ # Frozen secret allowlist: only {CLOUDFLARE_*, TURSO_*} cross into the
74
+ # shared workflow. Map your project's secret NAMES onto these slots.
75
+ secrets:
76
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
77
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -1,86 +1,91 @@
1
1
  name: deploy-staging
2
2
 
3
- # Canonical staging-deploy caller template (Story #175).
3
+ # Canonical staging-deploy caller template — DISPATCHER half (Story #175,
4
+ # reworked for Story #272).
5
+ #
6
+ # > **Why two files (Story #272).** A reusable workflow's `environment:`-gated
7
+ # > jobs (`deploy-cloudflare.yml`'s `check-env` / `migration` / `deploy` /
8
+ # > `boot-smoke`) are **silently skipped on a `workflow_run` event** — a
9
+ # > documented GitHub limitation. The previous single-file template called
10
+ # > `deploy-cloudflare.yml` DIRECTLY from `on: workflow_run`, so those jobs
11
+ # > skipped and **0 workers deployed while the run reported green** (all-skipped,
12
+ # > none failed). One consumer hid 40+ consecutive non-deploys this way.
13
+ # >
14
+ # > The fix: this file is now a thin **dispatcher** that fires on CI-green and
15
+ # > re-launches the deploy via **`workflow_dispatch`** — the event on which the
16
+ # > `environment:` jobs DO run — against its sibling `deploy-staging-run.yml`.
17
+ # > `workflow_dispatch` and `repository_dispatch` are the two events that
18
+ # > "always create workflow runs" even when triggered with the built-in
19
+ # > `GITHUB_TOKEN`, so **no PAT is required** for this same-repo dispatch — just
20
+ # > `permissions: actions: write` below. (Cross-repo dispatch, like
21
+ # > `smoke-dispatch.yml`, still needs a PAT; same repo does not.)
4
22
  #
5
23
  # > **Thin local caller.** The defence-in-depth deploy core (secret-isolation
6
- # > audit -> CF env gate -> pre-migration snapshot -> migrate -> deploy ->
7
- # > boot-smoke + auto-rollback) AND the CI-green guard both live in the shared
24
+ # > audit -> CF env gate -> migration (snapshot + apply) -> deploy ->
25
+ # > boot-smoke + auto-rollback) lives in the shared
8
26
  # > `dsj1984/mandrel-platform` `deploy-cloudflare.yml` reusable workflow — see
9
27
  # > https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml.
10
- # > This file only holds <PROJECT_NAME>-specific values (worker names, build
11
- # > step, secret mapping). When the deploy PROCESS changes, that change lands
12
- # > upstream in mandrel-platform — not here.
13
- #
14
- # One paved road (operator decision 2026-07-01, D4): every consumer triggers
15
- # staging deploy via `workflow_run` on its own CI workflow, gated on
16
- # `conclusion == 'success'`. `workflow_run` fires on BOTH a successful AND a
17
- # failed upstream run, so a caller-side guard against a red run used to be
18
- # REQUIRED here — every consumer hand-copied the same `preflight` job (see
19
- # mandrel-platform Story #175 context). That guard is now a `require-ci-green`
20
- # job INSIDE `deploy-cloudflare.yml` itself (`github.event` inside a reusable
21
- # workflow is the CALLER's event, so the shared workflow can see and gate on
22
- # the `workflow_run` conclusion even though it cannot own this file's `on:`
23
- # block). This template needs NO caller-side preflight guard as a result —
24
- # copy it as-is and fill in the placeholders below.
28
+ # > The <PROJECT_NAME>-specific values (worker names, build step, secret
29
+ # > mapping) live in the sibling `deploy-staging-run.yml`. When the deploy
30
+ # > PROCESS changes, that change lands upstream in mandrel-platform — not here.
25
31
  #
26
- # Replace every <PLACEHOLDER> with your project's real values:
27
- # <CI_WORKFLOW_NAME> the `name:` of the workflow this deploy should
28
- # gate on (e.g. "quality", "CI", "PR Quality").
29
- # Must match EXACTLY GitHub matches
30
- # `workflow_run.workflows` by workflow name, not
31
- # file path.
32
- # <MANDREL_PLATFORM_SHA> the pinned mandrel-platform commit SHA (resolve
33
- # via `node scripts/platform-sync.mjs --ref
34
- # <release-tag>` from the consumer repo root, or
35
- # hand-resolve via `git ls-remote`).
36
- # <MANDREL_PLATFORM_TAG> the human-readable release tag matching the SHA
37
- # above (trailing `# <tag>` comment).
38
- # <WORKERS_CSV> comma-separated Worker names for this env, e.g.
39
- # "api,web".
40
- # <BUILD_COMMAND> optional build command (omit build-command /
41
- # build-artifact entirely if the deploy job's
42
- # default checkout is build-ready).
32
+ # Adopt BOTH files together: this `deploy-staging.yml` (dispatcher) and
33
+ # `deploy-staging-run.yml` (the actual deploy). `platform-sync` materializes
34
+ # both. Replace every <PLACEHOLDER>:
35
+ # <CI_WORKFLOW_NAME> the `name:` of the workflow this deploy gates on (e.g.
36
+ # "quality", "CI"). Must match EXACTLY — GitHub matches
37
+ # `workflow_run.workflows` by workflow name, not path.
43
38
  #
44
39
  # See the full input/secret contract:
45
40
  # https://github.com/dsj1984/mandrel-platform/blob/main/docs/reusable-workflows.md#deploy-cloudflareyml
46
41
 
47
42
  on:
48
- # CI-green gate: fires when <CI_WORKFLOW_NAME> finishes on main. The shared
49
- # deploy-cloudflare.yml's require-ci-green job skips-with-notice unless the
50
- # upstream conclusion was 'success' no caller-side guard needed.
43
+ # CI-green gate: fires when <CI_WORKFLOW_NAME> completes on main. Unlike the
44
+ # old shape, this does NOT call the deploy directly a `workflow_run` deploy
45
+ # would skip every `environment:` job. It only DISPATCHES the deploy (below)
46
+ # on success, so the actual deploy runs on `workflow_dispatch` where those
47
+ # jobs execute. A red upstream run simply does not dispatch — there is no
48
+ # green-but-didn't-deploy run at all.
51
49
  workflow_run:
52
50
  workflows: [<CI_WORKFLOW_NAME>]
53
51
  branches: [main]
54
52
  types: [completed]
55
- # Manual on-demand trigger (UI "Run workflow" + `gh workflow run`).
56
- # workflow_dispatch always passes the shared workflow's CI-green guard
57
- # (operator-intentional, no upstream conclusion to gate on).
58
- workflow_dispatch:
59
53
 
54
+ # actions:write lets the built-in GITHUB_TOKEN dispatch deploy-staging-run.yml
55
+ # via the workflow_dispatch API. No PAT needed for a same-repo dispatch —
56
+ # workflow_dispatch always creates a run even from GITHUB_TOKEN.
60
57
  permissions:
61
58
  contents: read
59
+ actions: write
62
60
 
63
- # Cancel an in-flight staging deploy when a newer commit lands on main — only
64
- # the freshest tip of main should reach the staging surfaces. The shared
65
- # deploy-cloudflare.yml additionally serializes per-environment.
61
+ # Only the freshest tip of main should reach staging: cancel an in-flight
62
+ # DISPATCH when a newer CI run completes. (The deploy itself is serialized
63
+ # separately in deploy-staging-run.yml and per-environment inside the shared
64
+ # deploy-cloudflare.yml.)
66
65
  concurrency:
67
- group: deploy-staging
66
+ group: deploy-staging-dispatch
68
67
  cancel-in-progress: true
69
68
 
70
69
  jobs:
71
- deploy:
72
- name: Staging deploy (shared deploy-cloudflare.yml)
73
- uses: dsj1984/mandrel-platform/.github/workflows/deploy-cloudflare.yml@<MANDREL_PLATFORM_SHA> # <MANDREL_PLATFORM_TAG>
74
- with:
75
- environment: staging
76
- gh-environment: staging
77
- workers: <WORKERS_CSV>
78
- migrate: true
79
- # db-engine defaults to 'd1'. Set db-engine + migrate-command +
80
- # snapshot-command for a non-D1 engine (e.g. Turso) — see the contract
81
- # doc's "command seams" section.
82
- # Frozen secret allowlist: only {CLOUDFLARE_*, TURSO_*} cross into the
83
- # shared workflow. Map your project's secret NAMES onto these slots.
84
- secrets:
85
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
86
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
70
+ dispatch:
71
+ name: Dispatch staging deploy on CI-green
72
+ runs-on: ubuntu-latest
73
+ timeout-minutes: 5
74
+ # CI-green gate: dispatch the deploy ONLY when the upstream CI run
75
+ # concluded 'success'. `workflow_run` fires on both success and failure, so
76
+ # this guard is load-bearing — without it a red main would still deploy.
77
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
78
+ steps:
79
+ - name: Dispatch deploy-staging-run.yml (workflow_dispatch)
80
+ env:
81
+ GH_TOKEN: ${{ github.token }}
82
+ REPO: ${{ github.repository }}
83
+ SHA: ${{ github.event.workflow_run.head_sha }}
84
+ shell: bash
85
+ run: |
86
+ set -euo pipefail
87
+ gh workflow run deploy-staging-run.yml \
88
+ --repo "${REPO}" \
89
+ --ref main \
90
+ -f sha="${SHA}"
91
+ echo "Dispatched staging deploy for ${SHA} (deploy runs on workflow_dispatch so environment: jobs execute)."