Skip to content

Commit d8d4921

Browse files
author
Peter Amiri
committed
Merge remote-tracking branch 'origin/develop' into HEAD
Signed-off-by: Peter Amiri <petera@pai.com>
2 parents e70d18e + 297fb9b commit d8d4921

59 files changed

Lines changed: 2254 additions & 1161 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
name: CommandBox install smoke (ForgeBox path)
2+
3+
# Durability net for the ForgeBox/CommandBox install path (issues #3173, #3174,
4+
# #3176, #3177). The published ForgeBox channel was repaired across a campaign:
5+
# the base template is now regenerated from cli/lucli/templates/app/ as the
6+
# single source of truth (#3176) with build-time placeholder substitution
7+
# (#3173) and db/ retained (#3174), and wheels-core lands flat (#3177).
8+
#
9+
# Those fixes are easy to silently regress — a placeholder reintroduced into a
10+
# config override, a db/ exclude added back to the base .gitignore, a prepare
11+
# script re-pointed at the demo app — and ForgeBox publishing is stable-only
12+
# and asynchronous, so the breakage only surfaces in production weeks later
13+
# (which is exactly how the channel rotted the first time).
14+
#
15+
# This leg closes that gap by exercising the FIXED build pipeline end to end on
16+
# every PR that touches it:
17+
#
18+
# 1. Build the base + core packages FROM THE TREE
19+
# (tools/build/scripts/prepare-base.sh + prepare-core.sh) — i.e. the
20+
# artifact this PR would publish, NOT the lagging ForgeBox copy.
21+
# 2. Stage them the way `box install wheels-base-template` lands them: the
22+
# base scaffold at the app root, wheels-core at vendor/wheels/ per the
23+
# box.json installPaths.
24+
# 3. Boot the app under CommandBox (the ortussolutions/commandbox image —
25+
# CommandBox is the runtime on this path, not LuCLI) with ZERO manual
26+
# edits and assert the install->serve journey:
27+
# - dev: server starts no-edit, GET / is 200, /wheels/info is 200
28+
# (the dev whitelist), clean URLs (/main/index) work, db/ ships.
29+
# - prod: tools/ci/smoke-env.sh passes its 6 reload-gate / no-trace
30+
# probes (the same script the Smoke workflow drives).
31+
#
32+
# IMPORTANT — ordering within the campaign: this leg tests the FIXED pipeline.
33+
# The template keystone (#3176, build base template from cli/lucli/templates/app)
34+
# is a sibling PR that is NOT yet on develop. Until it merges, develop's
35+
# prepare-base.sh still copies the repo-root demo app and ships
36+
# "cfengine":"|cfmlEngine|" — so `box server start` hard-errors with "Invalid
37+
# slug detected" and the connectivity probe fails (status 000). That is the
38+
# defect this leg is meant to catch; the probes are deliberately NOT weakened to
39+
# pass on the unfixed tree. Once the keystone lands on develop this job goes
40+
# green. The wheels-core fixes (#3177/#3178/#3179) are already on develop.
41+
#
42+
# Verified locally (2026-06-12, CommandBox 6.3.3 / ortussolutions/commandbox:
43+
# latest, keystone composed): all six smoke-env.sh probes PASS in production
44+
# and the dev journey (GET / 200, /wheels/info 200, /main/index 200, db/
45+
# present) passes with no edits. Booting the unfixed develop artifact exits 1
46+
# with "Invalid slug detected" — the gate is real.
47+
48+
on:
49+
pull_request:
50+
branches:
51+
- develop
52+
paths:
53+
# The build pipeline that produces the packages.
54+
- 'tools/build/**'
55+
# The single source of truth the base template is regenerated from.
56+
- 'cli/lucli/templates/app/**'
57+
# The probe harness this leg drives in production.
58+
- 'tools/ci/smoke-env.sh'
59+
# The workflow itself.
60+
- '.github/workflows/commandbox-install-smoke.yml'
61+
62+
permissions:
63+
contents: read
64+
65+
jobs:
66+
commandbox-install-smoke:
67+
name: "box install -> serve (CommandBox)"
68+
runs-on: ubuntu-latest
69+
timeout-minutes: 25
70+
# CommandBox is the runtime on the ForgeBox install path. Run the whole job
71+
# inside the official image so `box` is on PATH exactly as a real CommandBox
72+
# user has it — no host install, mirroring how release.yml gets CommandBox
73+
# for ForgeBox publishing (its "Install CommandBox" step), just containerized
74+
# so the box server boots in the same userland it serves from.
75+
container:
76+
image: ortussolutions/commandbox:latest
77+
env:
78+
# Throwaway values for the CI app; not secrets.
79+
SMOKE_RELOAD_PASSWORD: "wheels-dev"
80+
# Pin the version the prepare scripts stamp into the artifacts.
81+
PKG_VERSION: "0.0.0-cismoke"
82+
steps:
83+
- uses: actions/checkout@v5
84+
85+
# curl + jq are the only host tools the probes need; the CommandBox image
86+
# is Debian-based but minimal. Install defensively (no-op if present).
87+
- name: Ensure curl is available
88+
run: |
89+
command -v curl >/dev/null 2>&1 || {
90+
apt-get update -y && apt-get install -y --no-install-recommends curl ca-certificates
91+
}
92+
curl --version | head -1
93+
94+
# Build BOTH packages from the working tree — this is the output of the
95+
# pipeline this PR would publish, so a regression in either prepare script
96+
# fails here, not silently on ForgeBox weeks later. (Snapshot params:
97+
# branch=develop so the version-number handling matches a snapshot build.)
98+
- name: Build base + core packages from the tree
99+
run: |
100+
set -e
101+
bash tools/build/scripts/prepare-base.sh "${PKG_VERSION}" "develop" "${{ github.run_number }}" "false"
102+
bash tools/build/scripts/prepare-core.sh "${PKG_VERSION}" "develop" "${{ github.run_number }}" "false"
103+
test -d build-wheels-base || { echo "::error::prepare-base.sh produced no build-wheels-base/"; exit 1; }
104+
test -d build-wheels-core/wheels || { echo "::error::prepare-core.sh produced no build-wheels-core/wheels/"; exit 1; }
105+
106+
# Structural guards on the prepared base artifact (cheap, pre-boot). These
107+
# pin the three packaging defects the campaign repaired so a regression is
108+
# named precisely instead of surfacing as an opaque boot failure.
109+
- name: Assert prepared base artifact is publish-clean
110+
run: |
111+
set -e
112+
BASE=build-wheels-base
113+
114+
# #3174: db/ must ship (JDBC needs a parent dir; the base .gitignore
115+
# must not strip it). The keeper marker is enough — only generated
116+
# *.db/*.sqlite files are ignored.
117+
test -d "$BASE/db" || { echo "::error::#3174 regression: base artifact has no db/ directory"; exit 1; }
118+
test -f "$BASE/db/.keep" || { echo "::error::#3174 regression: db/.keep missing — empty dir will not survive packaging"; exit 1; }
119+
120+
# #3173: NO unsubstituted placeholders may remain in the files a raw
121+
# `box install` consumes. |cfmlEngine| in server.json is the one that
122+
# hard-errors `box server start`; the {{...}} app tokens are the ones
123+
# that break boot/config. (app/snippets/*.txt code-gen tokens are
124+
# excluded — those ship verbatim by design.)
125+
if grep -rnE '\|(appName|cfmlEngine|datasourceName|reloadPassword)\|' \
126+
"$BASE/server.json" "$BASE/config" "$BASE/app/views" 2>/dev/null; then
127+
echo "::error::#3173 regression: legacy |placeholder| token survived into the prepared base artifact"
128+
exit 1
129+
fi
130+
if grep -rnE '\{\{(appName|cfmlEngine|datasourceName|reloadPassword|luceeAdminPassword)\}\}' \
131+
"$BASE/server.json" "$BASE/config" "$BASE/app/views" 2>/dev/null; then
132+
echo "::error::#3173 regression: {{placeholder}} token survived into the prepared base artifact"
133+
exit 1
134+
fi
135+
136+
# The root route targets main##index — the default controller/view must
137+
# be generated (the LuCLI template does not carry them statically), else
138+
# GET / is Wheels.ViewNotFound on every install.
139+
test -f "$BASE/app/controllers/Main.cfc" || { echo "::error::default Main.cfc missing — GET / would 404"; exit 1; }
140+
test -f "$BASE/app/views/main/index.cfm" || { echo "::error::default main/index.cfm missing — GET / would 404"; exit 1; }
141+
142+
echo "Prepared base artifact is publish-clean (db/ present, no placeholders, root view present)."
143+
144+
# Stage the app exactly as `box install wheels-base-template` lands it: the
145+
# base scaffold at the app root + wheels-core resolved into vendor/wheels/
146+
# per the box.json installPaths. Installing from the locally built core
147+
# (not ForgeBox) is the whole point — it exercises THIS PR's pipeline
148+
# output rather than the lagging published copy.
149+
- name: Stage the installed app (base + local wheels-core)
150+
run: |
151+
set -e
152+
APP="${RUNNER_TEMP:-/tmp}/cb-app"
153+
rm -rf "$APP"; mkdir -p "$APP"
154+
cp -r build-wheels-base/. "$APP/"
155+
rm -rf "$APP/vendor/wheels"; mkdir -p "$APP/vendor/wheels"
156+
cp -r build-wheels-core/wheels/. "$APP/vendor/wheels/"
157+
# postInstall copies env.example -> .env on a real install; mimic it.
158+
[ -f "$APP/env.example" ] && cp "$APP/env.example" "$APP/.env"
159+
echo "APP_DIR=$APP" >> "$GITHUB_ENV"
160+
echo "Staged app at $APP"
161+
ls -la "$APP"
162+
163+
# --- Development journey: the user's actual first experience. -----------
164+
# Boot with NO edits (dev is the template default) and assert the install
165+
# ->serve journey holds. /wheels/info is 200 in development (the documented
166+
# dev whitelist, #2988), so this leg asserts 200 here and the clean 404 is
167+
# asserted by the production smoke-env.sh leg below.
168+
- name: Start CommandBox server (development)
169+
run: |
170+
set -e
171+
cd "$APP_DIR"
172+
box server start port=8080 host=0.0.0.0 openbrowser=false || true
173+
# Readiness via curl-retry (no sleep): accept the first byte.
174+
curl -s -o /dev/null --retry 40 --retry-delay 3 --retry-all-errors \
175+
--retry-connrefused --max-time 10 "http://localhost:8080/" \
176+
|| { echo "::error::server did not bind in development"; box server log || true; exit 1; }
177+
178+
- name: Assert install->serve journey (development)
179+
run: |
180+
set -e
181+
FAILS=0
182+
probe() { # name url expect must
183+
local name="$1" url="$2" expect="$3" must="$4" body status
184+
body=$(mktemp)
185+
status=$(curl -s -o "$body" -w '%{http_code}' --max-time 30 "$url")
186+
if [ "$status" != "$expect" ]; then
187+
echo "FAIL $name: expected $expect got $status ($url)"
188+
echo " body_head: $(head -c 200 "$body" | tr '\n' ' ')"
189+
FAILS=$((FAILS+1))
190+
elif [ -n "$must" ] && ! grep -qiE "$must" "$body"; then
191+
echo "FAIL $name: status $expect ok but body missing /$must/ ($url)"
192+
echo " body_head: $(head -c 200 "$body" | tr '\n' ' ')"
193+
FAILS=$((FAILS+1))
194+
else
195+
echo "PASS $name ($status)"
196+
fi
197+
rm -f "$body"
198+
}
199+
# GET / renders the welcome page (200, no manual edits).
200+
probe "root-welcome" "http://localhost:8080/" "200" "Wheels"
201+
# /wheels/info is the public component, 200 in development (#2988).
202+
probe "wheels-info-dev-200" "http://localhost:8080/wheels/info" "200" "System Information"
203+
# Clean URLs: controller/action route resolves (rewrites work).
204+
probe "clean-url-main" "http://localhost:8080/main/index" "200" ""
205+
if [ "$FAILS" -ne 0 ]; then
206+
echo "::error::development install->serve journey failed ($FAILS probe(s))"
207+
exit 1
208+
fi
209+
echo "development journey: all probes passed"
210+
211+
- name: Stop CommandBox server (development)
212+
if: always()
213+
run: |
214+
cd "$APP_DIR" 2>/dev/null && box server stop 2>/dev/null || true
215+
216+
# --- Production journey: the non-dev reload-gate / no-trace probes. ------
217+
# Reuse tools/ci/smoke-env.sh (the same harness the Smoke workflow drives)
218+
# against a production boot. environment.cfm hardcodes set(environment=...),
219+
# so flip it to production before boot (the app cold-boots into it). The
220+
# reload password is read from env("WHEELS_RELOAD_PASSWORD") in settings.cfm
221+
# and commandbox-dotenv loads .env at boot, so the password must live in
222+
# .env (a container -e var is overridden by .env on this path) for probe 6
223+
# (authorized reload must 302) to be meaningful.
224+
- name: Switch app to production
225+
run: |
226+
set -e
227+
cd "$APP_DIR"
228+
sed -i 's/set(environment="[a-z]*")/set(environment="production")/' config/environment.cfm
229+
grep -qF 'set(environment="production")' config/environment.cfm || {
230+
echo "::error::environment.cfm substitution failed — pattern drift?"; exit 1; }
231+
# Reload password into .env so settings.cfm's env() read + the smoke
232+
# probe agree. (commandbox-dotenv wins over container env on this path.)
233+
if [ -f .env ]; then
234+
sed -i 's/^WHEELS_RELOAD_PASSWORD=.*/WHEELS_RELOAD_PASSWORD='"${SMOKE_RELOAD_PASSWORD}"'/' .env
235+
grep -q '^WHEELS_RELOAD_PASSWORD=' .env || echo "WHEELS_RELOAD_PASSWORD=${SMOKE_RELOAD_PASSWORD}" >> .env
236+
else
237+
printf 'WHEELS_RELOAD_PASSWORD=%s\n' "${SMOKE_RELOAD_PASSWORD}" > .env
238+
fi
239+
grep -n 'set(environment' config/environment.cfm
240+
grep -n '^WHEELS_RELOAD_PASSWORD' .env
241+
242+
- name: Start CommandBox server (production)
243+
run: |
244+
set -e
245+
cd "$APP_DIR"
246+
box server start port=8080 host=0.0.0.0 openbrowser=false || true
247+
curl -s -o /dev/null --retry 40 --retry-delay 3 --retry-all-errors \
248+
--retry-connrefused --max-time 10 "http://localhost:8080/" \
249+
|| { echo "::error::server did not bind in production"; box server log || true; exit 1; }
250+
251+
- name: Run smoke-env.sh probes (production)
252+
run: |
253+
BASE_URL="http://localhost:8080" SMOKE_ENV="production" \
254+
SMOKE_RELOAD_PASSWORD="${SMOKE_RELOAD_PASSWORD}" \
255+
bash tools/ci/smoke-env.sh
256+
257+
- name: Debug server log
258+
if: failure()
259+
run: |
260+
cd "$APP_DIR" 2>/dev/null && box server log 2>/dev/null | tail -200 || true
261+
262+
- name: Stop CommandBox server (production)
263+
if: always()
264+
run: |
265+
cd "$APP_DIR" 2>/dev/null && box server stop 2>/dev/null || true

.github/workflows/docs-verify.yml

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
- 'web/sites/guides/package.json'
1010
- 'web/sites/guides/src/sidebars/v4-0-0.json'
1111
- '.github/workflows/docs-verify.yml'
12+
# The harness exercises the checkout's CLI module (overlaid onto the
13+
# installed brew CLI below), so CLI changes can flip {test:cli} blocks
14+
# red — run the docs gate when the CLI module changes (#3042).
15+
- 'cli/lucli/**'
1216

1317
jobs:
1418
verify:
@@ -21,6 +25,13 @@ jobs:
2125
runs-on: ubuntu-latest
2226
timeout-minutes: 20
2327

28+
env:
29+
# Attestation mode (#3042): consumed by lib/exec.mjs
30+
# wheelsBinaryAttestation() and echoed by the smoke-test step, so the
31+
# log states exactly whose CLI code the {test:cli}/{test:tutorial}
32+
# blocks dispatched to. github.sha is hex-only (injection-safe).
33+
WHEELS_ATTEST_MODE: "checkout cli/lucli module overlay @ ${{ github.sha }} (released LuCLI runtime via brew)"
34+
2435
steps:
2536
- name: Checkout
2637
uses: actions/checkout@v6
@@ -70,23 +81,45 @@ jobs:
7081
sed -i 's|/openjdk.jdk/Contents/Home||g' "$WRAPPER"
7182
grep JAVA_HOME "$WRAPPER"
7283
84+
- name: Overlay checkout CLI module (branch attestation)
85+
# Binary attestation (#3042): `wheels` is the released LuCLI runtime
86+
# plus a CFML module — and the module IS this repo's cli/lucli/ (the
87+
# brew formula ships it verbatim as wheels-module-<ver>.tar.gz, plus
88+
# vendor/wheels/ staged inside it). The wrapper installed above syncs
89+
# that released module to $HOME/.wheels/modules/wheels the first time
90+
# a non-intercepted command runs. Write that directory from the
91+
# CHECKOUT instead, before anything triggers the first-run sync, so
92+
# every `wheels` invocation in this job dispatches to the branch's
93+
# CLI module. {test:cli} blocks then attest to the tree under review:
94+
# a branch change to a command's output flips its block red.
95+
#
96+
# The .module-version marker is copied from the formula so the
97+
# wrapper's version-diff sync never overwrites the overlay with the
98+
# released module mid-job. (Note: `wheels --version` and `--help`
99+
# are intercepted in the wrapper itself — kept in sync with
100+
# Module.cfc — so those two surfaces still answer with released
101+
# wrapper text; every real subcommand runs branch code.)
102+
#
103+
# Out of scope: the LuCLI runtime stays at the released version the
104+
# formula pins — runtime changes ship via LuCLI releases, not this
105+
# repo's PRs.
106+
run: |
107+
MODULE_DST="$HOME/.wheels/modules/wheels"
108+
rm -rf "$MODULE_DST"
109+
mkdir -p "$MODULE_DST/vendor/wheels"
110+
cp -R "$GITHUB_WORKSPACE/cli/lucli/." "$MODULE_DST/"
111+
cp -R "$GITHUB_WORKSPACE/vendor/wheels/." "$MODULE_DST/vendor/wheels/"
112+
cp "$(brew --prefix wheels)/share/wheels/.module-version" "$MODULE_DST/.module-version"
113+
echo "Overlaid $MODULE_DST from checkout (marker: $(cat "$MODULE_DST/.module-version"))"
114+
73115
- name: Smoke-test the CLI (binary attestation)
74-
# Binary attestation (#3042): this workflow installs the RELEASED
75-
# brew CLI, so {test:cli}/{test:compile}/{test:tutorial} blocks
76-
# attest to that release — not a CLI built from this checkout.
77-
# verify-docs.mjs prints the same "wheels binary: <path> ... " line
78-
# at run start. To attest to a branch-built CLI instead, export
79-
# WHEELS_BIN pointing at it (not wired up yet — issue #3042 tracks
80-
# building the CLI from the checkout here).
116+
# verify-docs.mjs prints the full attestation line (binary path,
117+
# resolution source, version, and WHEELS_ATTEST_MODE) at run start;
118+
# this step is the early, human-greppable copy of the same facts.
81119
run: |
82120
command -v wheels
83121
wheels --version
84-
85-
- name: Warm up wheels module (first-run copy)
86-
# Homebrew wrapper copies the module to $HOME/.wheels/modules/wheels
87-
# on first invocation. Trigger it so the module is in place before
88-
# the harness spawns `wheels`.
89-
run: wheels --version >/dev/null
122+
echo "attest mode: $WHEELS_ATTEST_MODE"
90123
91124
- name: Run harness unit tests
92125
working-directory: web/sites/guides

0 commit comments

Comments
 (0)