Skip to content
Merged
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,43 @@
const Logger = require("@ui5/logger");
const log = Logger.getLogger("builder:tasks:buildSignatureTask");

const {readFile} = require("node:fs/promises");
const {join} = require("node:path");

// Reads the control file located at the project root (outside the monitored source/dependency
// readers). Both the task body and the determineBuildSignature callback derive their value from it,
// so a test can change the returned value / produced output independently of any source-file change.
async function readControlValue(rootPath) {
const controlFilePath = join(rootPath, "buildSignatureControl.txt");
try {
const content = await readFile(controlFilePath, {encoding: "utf8"});
return content.trim();
} catch (err) {
// Fall back to a constant if the control file is missing
log.verbose(`build-signature-task: control file missing (${err.code})`);
return "no-control-file";
}
}

// Task body: appends the current control value to the application's test.js. This makes the built
// output observably depend on the control file, so a served resource reflects its value.
module.exports = async function ({taskUtil, workspace, options: {projectNamespace}}) {
log.verbose("build-signature-task executed");

const controlValue = await readControlValue(taskUtil.getProject().getRootPath());
const resource = await workspace.byPath(`/resources/${projectNamespace}/test.js`);
if (resource) {
const content = `${await resource.getString()}\n// build-signature-control: ${controlValue}\n`;
resource.setString(content);
await workspace.write(resource);
}
};

// determineBuildSignature is invoked by TaskDefinitions#getBuildSignatures() to contribute a value
// to the project's build signature. We derive it from the same control file so that changing the
// file changes the returned signature (and thus must invalidate the project's build cache).
module.exports.determineBuildSignature = async function ({taskUtil}) {
const controlValue = await readControlValue(taskUtil.getProject().getRootPath());
log.verbose(`build-signature-task determineBuildSignature: ${controlValue}`);
return controlValue;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
specVersion: "5.0"
type: application
metadata:
name: application.a
builder:
customTasks:
- name: build-signature-task
afterTask: minify
---
specVersion: "5.0"
kind: extension
type: task
metadata:
name: build-signature-task
task:
path: task.build-signature.js
48 changes: 48 additions & 0 deletions packages/project/test/lib/build/BuildServer.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,54 @@ test.serial("Serve application.a (test exclusion of generateVersionInfo)", async
});
});

// A custom task's determineBuildSignature callback derives the project's build signature from an
// input that is NOT a watched source resource (here: an on-disk control file at the project root).
// The desired behavior is that changing such an input while the server runs invalidates the served
// build result — otherwise the dev server keeps serving a stale state and the user has no way of
// knowing. This test asserts that desired behavior: request a resource, change the control file
// (which both the task body and determineBuildSignature read), request again, and expect the served
// content to reflect the new value WITHOUT restarting the server.
//
// It is marked test.failing because it currently fails: BuildServer computes each project's build
// signature exactly once (BuildContext memoizes the ProjectBuildContext for the server's lifetime),
// so determineBuildSignature is never re-evaluated for a running server, and the changed control
// file is ignored until the next `serve()`. AVA reports a failing-marked test as a pass while it
// throws and as a hard error once it starts passing, so committing it keeps CI green and flips to a
// signal the moment the behavior is fixed (at which point drop the `.failing`).
test.serial.failing(
"Serve application.a, changing a determineBuildSignature input invalidates served output", async (t) => {
const fixtureTester = t.context.fixtureTester = await FixtureTester.create(t, "application.a");

// The custom task appends the control file's value to test.js and also feeds it into
// determineBuildSignature.
const controlFilePath = `${fixtureTester.fixturePath}/buildSignatureControl.txt`;
await fs.writeFile(controlFilePath, "v1");

await fixtureTester.serveProject({
graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"},
});

// #1 request: served test.js reflects control value "v1"
const first = await fixtureTester.requestResource({resource: "/test.js"});
const firstContent = await first.getString();
t.true(firstContent.includes("// build-signature-control: v1"),
"Initial served resource reflects control value v1");

// Change ONLY the control file — no watched source resource changes. The determineBuildSignature
// input is now different. The control file lives at the project root, outside the watched
// source paths, so no watcher event fires for it (mirroring a real determineBuildSignature
// input that is not a project source resource).
await fs.writeFile(controlFilePath, "v2");

// #2 request: the served resource must reflect the new control value "v2".
const second = await fixtureTester.requestResource({resource: "/test.js"});
const secondContent = await second.getString();
t.true(secondContent.includes("// build-signature-control: v2"),
"Served resource reflects the changed determineBuildSignature input without a server restart");
t.false(secondContent.includes("// build-signature-control: v1"),
"Served resource no longer reflects the stale control value v1");
});

function getFixturePath(fixtureName) {
return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,71 @@ test.serial.skip("Build application.a (dependency content changes)", async (t) =
t.true(builtFileContent2.includes(`console.log('something new');`), "Build dest contains changed file content");
});

test.serial("Build application.a (custom task determineBuildSignature callback)", async (t) => {
const fixtureTester = new FixtureTester(t, "application.a");
const destPath = fixtureTester.destPath;
await fixtureTester._initialize();

// The custom task "build-signature-task" implements a determineBuildSignature callback which
// derives the project's build signature from an on-disk control file at the project root.
// Changing that file's content changes the returned signature (and nothing else), which must
// invalidate application.a's build cache — proving the callback is wired into signature
// computation. A stable content must keep the cache intact.
const controlFilePath = `${fixtureTester.fixturePath}/buildSignatureControl.txt`;
await fs.writeFile(controlFilePath, "v1");

// #1 build (no cache): everything builds
await fixtureTester.buildProject({
graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"},
config: {destPath, cleanDest: true},
assertions: {
projects: {
"library.d": {},
"library.a": {},
"library.b": {},
"library.c": {},
"application.a": {}
}
}
});

// #2 build (with cache, signature unchanged): full cache hit, nothing rebuilt
await fixtureTester.buildProject({
graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"},
config: {destPath, cleanDest: true},
assertions: {
projects: {}
}
});

// Change only the control file → determineBuildSignature returns a different value.
// No source or dependency resource changes.
await fs.writeFile(controlFilePath, "v2");

// #3 build (with cache, changed signature): application.a's build signature changed, so its
// cache is invalidated and it is rebuilt. The dependencies are unaffected.
await fixtureTester.buildProject({
graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"},
config: {destPath, cleanDest: true},
assertions: {
projects: {
// application.a is rebuilt (its build signature changed); none of its tasks can be
// reused from cache, since the changed signature invalidates the whole project cache.
"application.a": {}
}
}
});

// #4 build (with cache, signature unchanged again): full cache hit, nothing rebuilt
await fixtureTester.buildProject({
graphConfig: {rootConfigPath: "ui5-customTask-buildSignature.yaml"},
config: {destPath, cleanDest: true},
assertions: {
projects: {}
}
});
});

test.serial("Build application.a (cross-project tag change)", async (t) => {
const fixtureTester = new FixtureTester(t, "application.a");
const destPath = fixtureTester.destPath;
Expand Down
Loading