Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `PackageLoader.$normalizeWheelsVersion()` now detects unstamped dev builds by the placeholder's structural shape (`@build.` prefix + `@` suffix) instead of a literal `@build.version@` comparison, mirroring `BuildInfo.cfc::isDev()`. The release pipeline's global `sed` pass over every `.cfc` previously rewrote that literal into the release version on shipped artifacts, so the real runtime version normalised to `0.0.0` and `wheelsVersion` constraint enforcement was silently disabled for every package on every released build (4.0.0–4.0.3). `prepare-core.sh` now also fails the build if a stamped self-version sentinel reappears in `PackageLoader.cfc` (#3178)
20 changes: 20 additions & 0 deletions tools/build/scripts/prepare-core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,25 @@ if [ -f "${BUILDINFO_CFC}" ]; then
fi
fi

# Sanity check: PackageLoader.cfc must detect dev builds STRUCTURALLY (prefix
# `@build.` + suffix `@`), never via a literal `@build.version@` comparison.
# The global `sed s/@build.version@/<version>/g` above (line ~59) rewrites every
# literal version placeholder in this artifact. If such a literal sat in
# $normalizeWheelsVersion()'s guard it would now read `local.raw == "<version>"`,
# normalising the real runtime version to "0.0.0" and silently disabling
# wheelsVersion constraint enforcement for every package on every released build
# (issue #3178). After substitution the only place "${VERSION}" should appear in
# a `local.raw ==` comparison is nowhere — the guard is structural. Fail loud if
# the fragile literal has crept back in.
PACKAGELOADER_CFC="${BUILD_DIR}/wheels/PackageLoader.cfc"
if [ -f "${PACKAGELOADER_CFC}" ]; then
if grep -qF "local.raw == \"${VERSION}\"" "${PACKAGELOADER_CFC}"; then
echo "ERROR: PackageLoader.cfc contains a stamped self-version sentinel (local.raw == \"${VERSION}\")." >&2
echo " Release stamping clobbered the dev-build guard — wheelsVersion enforcement would be disabled on every released build (issue #3178)." >&2
echo " Use the structural Left/Right placeholder check that BuildInfo.cfc::isDev() uses instead of a literal comparison." >&2
exit 1
fi
fi

echo "Wheels Core prepared for ForgeBox publishing!"
echo "Directory: ${BUILD_DIR}/wheels/"
25 changes: 20 additions & 5 deletions vendor/wheels/PackageLoader.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1359,14 +1359,29 @@ component output="false" {

/**
* Returns the runtime Wheels version normalised for semver comparison.
* Dev builds surface as "0.0.0-dev" via BuildInfo. The legacy
* "@build.version@" check is kept as a defensive guard for any path that
* still feeds the raw placeholder in. Both normalise to "0.0.0" so strict
* version constraints don't falsely reject packages during development.
* Dev builds surface as "0.0.0-dev" via BuildInfo, or as the unsubstituted
* build placeholder when the raw value is fed in directly. Both normalise
* to "0.0.0" so strict version constraints don't falsely reject packages
* during development.
*
* The placeholder is detected by its STRUCTURAL shape (prefix + suffix),
* NOT by literal equality. tools/build/scripts/prepare-core.sh does a
* GLOBAL line-oriented sed over every .cfc at artifact-construction time
* that rewrites the literal version placeholder to the release version. A
* literal comparison here would be rewritten too — turning the guard into
* `local.raw == "4.0.3"` on a shipped 4.0.3 build, normalising the real
* runtime version to "0.0.0" and silently disabling wheelsVersion
* enforcement for every package (issue #3178). Mirrors BuildInfo.cfc's
* isDev() detection, which is structural for exactly this reason. The
* prefix/suffix fragments are not full placeholder tokens, so the sed pass
* leaves them untouched.
*/
private string function $normalizeWheelsVersion() {
local.raw = SpanExcluding(variables.wheelsVersion, " ");
if (local.raw == "@build.version@" || local.raw == "0.0.0-dev") {
local.isPlaceholder = Len(local.raw) >= 8
&& Left(local.raw, 7) == "@build."
&& Right(local.raw, 1) == "@";
if (local.isPlaceholder || local.raw == "0.0.0-dev") {
return "0.0.0";
}
return local.raw;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
component extends="wheels.WheelsTest" {

// Regression coverage for issue #3178: release stamping clobbers
// PackageLoader's "@build.version@" dev-build sentinel.
//
// tools/build/scripts/prepare-core.sh does a GLOBAL
// `sed s/@build.version@/<version>/g` over every .cfc in the artifact. If a
// literal "@build.version@" sits inside $normalizeWheelsVersion()'s guard,
// that occurrence is rewritten too — on a shipped 4.0.3 build the guard
// becomes `local.raw == "4.0.3"`, so the real runtime version normalises to
// "0.0.0" and $isCompatibleVersion() skips enforcement for EVERY package.
// Net effect: wheelsVersion constraints are silently disabled on every
// released build. Detection must be STRUCTURAL (prefix `@build.` + suffix
// `@`), mirroring BuildInfo.cfc::isDev(), so global stamping can't break it.

function run() {

describe("PackageLoader release-stamp safety", () => {

describe("dev-build detection is sed-safe (structural, not literal)", () => {

it("source contains zero '@build.version@' sentinels (regression guard)", () => {
// prepare-core.sh's line-oriented sed does not respect CFML
// syntax: a "@build.version@" literal anywhere in this file —
// in a comparison OR a comment — is rewritten at build time.
// $normalizeWheelsVersion() must therefore detect dev builds
// by the prefix/suffix shape (Left(raw, 7) == "@build." &&
// Right(raw, 1) == "@"), never by literal equality. Unlike
// BuildInfo.cfc (which legitimately seeds the placeholder into
// its `version:` field), PackageLoader receives the runtime
// version as a constructor arg, so the sentinel should appear
// nowhere in this source at all.
var src = FileRead(ExpandPath("/wheels/PackageLoader.cfc"));
var token = "@" & "build.version" & "@"; // split so this file isn't itself a sentinel
var occurrences = (Len(src) - Len(Replace(src, token, "", "all"))) / Len(token);
expect(occurrences).toBe(
0,
"PackageLoader.cfc must contain zero '" & token & "' literals (found "
& occurrences & "). Any occurrence is rewritten by prepare-core.sh's "
& "global sed and disables wheelsVersion enforcement on every released build. "
& "Use the structural Left/Right check that BuildInfo.cfc::isDev() uses."
);
});

});

describe("a stamped release runtime still enforces wheelsVersion constraints", () => {

it("rejects an incompatible package when the runtime reports a concrete release version", () => {
// This is exactly the state a shipped artifact is in: a
// concrete version string like "4.0.3". A package pinned to
// ">=99.0" must NOT load. (Before the fix this passes on the
// unstamped source tree but breaks once sed turns the guard
// into `local.raw == "4.0.3"`; the structural check keeps it
// honest in both worlds — and the source-scan guard above is
// what fails on the unpatched tree.)
var loader = new wheels.PackageLoader(
vendorPath = ExpandPath("/wheels/tests/_assets/packages"),
componentPrefix = "wheels.tests._assets.packages",
wheelsVersion = "4.0.3"
);
var pkgs = loader.getPackages();
expect(pkgs).notToHaveKey("incompatversion");
expect(pkgs).toHaveKey("compatversion");
});

});

describe("a placeholder-shaped runtime stays permissive in local dev", () => {

it("loads strictly-pinned packages when the version has the unstamped placeholder shape", () => {
var loader = new wheels.PackageLoader(
vendorPath = ExpandPath("/wheels/tests/_assets/packages"),
componentPrefix = "wheels.tests._assets.packages",
wheelsVersion = "@build.version@"
);
var pkgs = loader.getPackages();
// Even the ">=99.0" fixture loads on an unstamped dev build.
expect(pkgs).toHaveKey("incompatversion");
expect(pkgs).toHaveKey("compatversion");
});

});

});

}

}
Loading