Skip to content

Commit e594e8a

Browse files
committed
chore: feedback fixes
1 parent 9f49ea7 commit e594e8a

7 files changed

Lines changed: 166 additions & 99 deletions

File tree

Lines changed: 64 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
///usr/bin/env jbang "$0" "$@" ; exit $?
22
//JAVA 21+
3+
//DEPS info.picocli:picocli:4.7.7
4+
5+
import picocli.CommandLine;
6+
import picocli.CommandLine.Command;
7+
import picocli.CommandLine.Option;
38

49
import java.io.IOException;
510
import java.nio.file.Files;
611
import java.nio.file.Path;
712
import java.nio.file.StandardCopyOption;
8-
import java.util.ArrayList;
913
import java.util.List;
14+
import java.util.concurrent.Callable;
1015
import java.util.regex.Matcher;
1116
import java.util.regex.Pattern;
1217

@@ -15,85 +20,79 @@
1520
* pom.xml, runs the deploy, then restores the original pom.xml (even if the build fails or
1621
* the script is interrupted). Keeps deploy credentials/config out of the committed pom.xml.
1722
*
18-
* Usage:
19-
* jbang ./.github/scripts/DeployToPlatform.java --url <platformUrl> --key <key> --tenants <t1,t2,...> [options]
20-
*
21-
* Required:
22-
* --url Timefold platform URL, e.g. https://sandbox.timefold.dev
23-
* --key Model key, e.g. maintenance-scheduling-template
24-
* --tenants Comma-separated tenant id(s)
25-
*
26-
* Optional:
27-
* --module <path> Path to the module containing pom.xml (default: current directory)
28-
* --overwrite <bool> Value for <overwrite> (default: true)
29-
* --handle-subscription <bool> Value for <handleSubscription> (default: true)
30-
*
3123
* Example:
32-
* jbang ./github/scripts/DeployToPlatform.java --module use-cases/maintenance-scheduling \
24+
* jbang .github/scripts/DeployToPlatform.java --module use-cases/maintenance-scheduling \
3325
* --url https://sandbox.timefold.dev \
3426
* --key maintenance-scheduling-template \
3527
* --tenants ae226da1-5aea-4aba-93fc-8f911f37aa23
3628
*/
37-
public final class DeployToPlatform {
29+
@Command(name = "DeployToPlatform",
30+
mixinStandardHelpOptions = true,
31+
description = "Deploys a quickstart to the Timefold Platform by temporarily injecting the "
32+
+ "timefold-maven-plugin deploy configuration into its pom.xml.")
33+
public final class DeployToPlatform implements Callable<Integer> {
34+
35+
private static final String NL = System.lineSeparator();
3836

3937
// Matches a top-level <build> (2-space indent) only, not one nested in a <profile>.
38+
// \R matches whatever line break the pom.xml actually uses.
4039
private static final Pattern TOP_LEVEL_BUILD =
41-
Pattern.compile("\\n {2}<build>\\n.*?\\n {2}</build>\\n", Pattern.DOTALL);
42-
private static final Pattern PLUGINS_OPEN = Pattern.compile("\\n {4}<plugins>\\n");
40+
Pattern.compile("\\R {2}<build>\\R.*?\\R {2}</build>\\R", Pattern.DOTALL);
41+
private static final Pattern PLUGINS_OPEN = Pattern.compile("\\R {4}<plugins>\\R");
4342
private static final Pattern PARENT_BLOCK = Pattern.compile("<parent>(.*?)</parent>", Pattern.DOTALL);
4443
private static final Pattern VERSION_TAG = Pattern.compile("<version>\\s*(.*?)\\s*</version>");
4544
private static final Pattern MAJOR_MINOR_PATCH = Pattern.compile("^\\d+\\.\\d+\\.\\d+$");
4645

47-
public static void main(String[] args) throws Exception {
48-
Path module = Path.of(".");
49-
String platformUrl = null;
50-
String key = null;
51-
String tenants = null;
52-
String overwrite = "true";
53-
String handleSubscription = "true";
54-
55-
for (int i = 0; i < args.length; i++) {
56-
switch (args[i]) {
57-
case "--module" -> module = Path.of(args[++i]);
58-
case "--url" -> platformUrl = args[++i];
59-
case "--key" -> key = args[++i];
60-
case "--tenants" -> tenants = args[++i];
61-
case "--overwrite" -> overwrite = args[++i];
62-
case "--handle-subscription" -> handleSubscription = args[++i];
63-
case "-h", "--help" -> usage(0);
64-
default -> {
65-
System.err.println("Unknown argument: " + args[i]);
66-
usage(1);
67-
}
68-
}
69-
}
46+
@Option(names = {"-m", "--module"}, defaultValue = ".",
47+
description = "Path to the module containing pom.xml (default: ${DEFAULT-VALUE}).")
48+
Path module;
7049

71-
if (platformUrl == null || key == null || tenants == null) {
72-
usage(1);
73-
}
50+
@Option(names = {"-u", "--url"}, required = true,
51+
description = "Timefold platform URL, e.g. https://sandbox.timefold.dev")
52+
String platformUrl;
7453

75-
List<String> tenantList = new ArrayList<>();
76-
for (String tenant : tenants.split(",")) {
77-
if (!tenant.isBlank()) {
78-
tenantList.add(tenant.strip());
79-
}
80-
}
54+
@Option(names = {"-k", "--key"}, required = true,
55+
description = "Model key, e.g. maintenance-scheduling-template")
56+
String key;
57+
58+
@Option(names = {"-t", "--tenants"}, required = true, split = ",", paramLabel = "<tenantId>",
59+
description = "Comma-separated tenant id(s).")
60+
List<String> tenants;
61+
62+
@Option(names = {"--overwrite"}, defaultValue = "true", arity = "0..1", fallbackValue = "true",
63+
paramLabel = "<bool>", description = "Value for <overwrite> (default: ${DEFAULT-VALUE}).")
64+
boolean overwrite;
65+
66+
@Option(names = {"--handle-subscription"}, defaultValue = "true", arity = "0..1", fallbackValue = "true",
67+
paramLabel = "<bool>", description = "Value for <handleSubscription> (default: ${DEFAULT-VALUE}).")
68+
boolean handleSubscription;
69+
70+
public static void main(String[] args) {
71+
System.exit(new CommandLine(new DeployToPlatform()).execute(args));
72+
}
73+
74+
@Override
75+
public Integer call() throws Exception {
76+
List<String> tenantList = tenants.stream()
77+
.map(String::strip)
78+
.filter(tenant -> !tenant.isBlank())
79+
.toList();
8180
if (tenantList.isEmpty()) {
8281
System.err.println("No tenants given");
83-
System.exit(1);
82+
return 1;
8483
}
8584

8685
Path pom = module.resolve("pom.xml");
8786
if (!Files.isRegularFile(pom)) {
8887
System.err.println("No pom.xml found at " + pom);
89-
System.exit(1);
88+
return 1;
9089
}
9190

9291
try {
9392
validateParentVersion(pom);
9493
} catch (IllegalStateException e) {
9594
System.err.println(e.getMessage());
96-
System.exit(1);
95+
return 1;
9796
}
9897

9998
Path backup = Files.createTempFile("deploy-to-platform-pom", ".xml");
@@ -107,15 +106,15 @@ public static void main(String[] args) throws Exception {
107106
}
108107
}));
109108

110-
injectDeployPlugin(pom, platformUrl, key, tenantList, overwrite, handleSubscription);
109+
injectDeployPlugin(pom, tenantList);
111110

112111
System.out.println("Deploying '" + module + "' to '" + platformUrl + "' (key=" + key
113-
+ ", tenants=" + tenants + ")...");
112+
+ ", tenants=" + String.join(",", tenantList) + ")...");
114113
Process process = new ProcessBuilder("mvn", "clean", "package", "-Denterprise=true", "timefold:deploy")
115114
.directory(module.toFile())
116115
.inheritIO()
117116
.start();
118-
System.exit(process.waitFor());
117+
return process.waitFor();
119118
}
120119

121120
/**
@@ -142,13 +141,13 @@ private static void validateParentVersion(Path pom) throws IOException {
142141
}
143142
}
144143

145-
private static void injectDeployPlugin(Path pom, String platformUrl, String key, List<String> tenants,
146-
String overwrite, String handleSubscription) throws IOException {
147-
String tenantXml = tenants.stream()
144+
private void injectDeployPlugin(Path pom, List<String> tenantList) throws IOException {
145+
String tenantXml = tenantList.stream()
148146
.map(tenant -> " <tenant>" + tenant + "</tenant>")
149147
.reduce((a, b) -> a + "\n" + b)
150148
.orElseThrow();
151149

150+
// Text blocks always separate their lines with \n, so translate to the platform's separator.
152151
String pluginXml = """
153152
<plugin>
154153
<groupId>ai.timefold.solver</groupId>
@@ -157,9 +156,7 @@ private static void injectDeployPlugin(Path pom, String platformUrl, String key,
157156
<configuration>
158157
<platformUrl>%s</platformUrl>
159158
<key>%s</key>
160-
<tenants>
161-
%s
162-
</tenants>
159+
<tenants>%s</tenants>
163160
<overwrite>%s</overwrite>
164161
<handleSubscription>%s</handleSubscription>
165162
</configuration>
@@ -171,7 +168,8 @@ private static void injectDeployPlugin(Path pom, String platformUrl, String key,
171168
</execution>
172169
</executions>
173170
</plugin>"""
174-
.formatted(platformUrl, key, tenantXml, overwrite, handleSubscription);
171+
.formatted(platformUrl, key, tenantXml, overwrite, handleSubscription)
172+
.replace("\n", NL);
175173

176174
String content = Files.readString(pom);
177175
Matcher buildMatcher = TOP_LEVEL_BUILD.matcher(content);
@@ -182,35 +180,14 @@ private static void injectDeployPlugin(Path pom, String platformUrl, String key,
182180
if (!pluginsMatcher.find()) {
183181
throw new IllegalStateException("Found a top-level <build> without <plugins>; cannot insert automatically");
184182
}
185-
String newBlock = block.substring(0, pluginsMatcher.end()) + pluginXml + "\n"
183+
String newBlock = block.substring(0, pluginsMatcher.end()) + pluginXml + NL
186184
+ block.substring(pluginsMatcher.end());
187185
newContent = content.substring(0, buildMatcher.start()) + newBlock + content.substring(buildMatcher.end());
188186
} else {
189-
String newBlock = " <build>\n <plugins>\n" + pluginXml + "\n </plugins>\n </build>\n";
187+
String newBlock = " <build>" + NL + " <plugins>" + NL + pluginXml + NL
188+
+ " </plugins>" + NL + " </build>" + NL;
190189
newContent = content.replaceFirst(Pattern.quote("</project>"), Matcher.quoteReplacement(newBlock) + "</project>");
191190
}
192191
Files.writeString(pom, newContent);
193192
}
194-
195-
private static void usage(int exitCode) {
196-
System.err.println("""
197-
Usage: jbang scripts/DeployToPlatform.java --url <platformUrl> --key <key> --tenants <t1,t2,...> [options]
198-
199-
Required:
200-
--url Timefold platform URL, e.g. https://sandbox.timefold.dev
201-
--key Model key, e.g. maintenance-scheduling-template
202-
--tenants Comma-separated tenant id(s)
203-
204-
Optional:
205-
--module <path> Path to the module containing pom.xml (default: current directory)
206-
--overwrite <bool> Value for <overwrite> (default: true)
207-
--handle-subscription <bool> Value for <handleSubscription> (default: true)
208-
209-
Example:
210-
jbang scripts/DeployToPlatform.java --module use-cases/maintenance-scheduling \\
211-
--url https://app.timefold.ai \\
212-
--key maintenance-scheduling-template \\
213-
--tenants ae226da1-5aea-4aba-93fc-8f23411f37aa23""");
214-
System.exit(exitCode);
215-
}
216193
}

‎use-cases/bed-allocation/src/main/resources/META-INF/resources/shared/platform-integration.js‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
const MESSAGE_SOURCE = "timefold-visualization";
99

10+
// How long to wait for the platform's "init" message before giving up.
11+
const INIT_TIMEOUT_MS = 15000;
12+
1013
// Origins the platform may embed this ui from.
1114
const ALLOWED_PARENT_ORIGINS = [];
1215

@@ -29,7 +32,16 @@
2932

3033
// Callers (quickstart-page.js) must wait on it before touching SETUP.apiUrl/apiKey/runId.
3134
SETUP.ready = new Promise((resolve, reject) => {
32-
window.addEventListener("message", function handleInit(event) {
35+
const timeoutId = setTimeout(() => {
36+
window.removeEventListener("message", handleInit);
37+
reject({
38+
status: 0,
39+
statusText: "platform did not send an init message within "
40+
+ (INIT_TIMEOUT_MS / 1000) + "s"
41+
});
42+
}, INIT_TIMEOUT_MS);
43+
44+
function handleInit(event) {
3345
if (!isTrustedOrigin(event)) return;
3446
const msg = event.data;
3547
if (!msg || msg.source !== MESSAGE_SOURCE || msg.type !== "init") return;
@@ -43,6 +55,7 @@
4355
SETUP.apiKey = apiKey;
4456
}
4557

58+
clearTimeout(timeoutId);
4659
window.removeEventListener("message", handleInit);
4760
parentOrigin = event.origin;
4861
window.parent.postMessage({source: MESSAGE_SOURCE, type: "init-response", success: ok}, parentOrigin);
@@ -53,7 +66,9 @@
5366
} else {
5467
reject({status: 0, statusText: "platform did not send tenantId/runId/apiUrl/apiKey"});
5568
}
56-
});
69+
}
70+
71+
window.addEventListener("message", handleInit);
5772
});
5873

5974
// ── Iframe resize reporting ──

‎use-cases/conference-scheduling/src/main/resources/META-INF/resources/shared/platform-integration.js‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
const MESSAGE_SOURCE = "timefold-visualization";
99

10+
// How long to wait for the platform's "init" message before giving up.
11+
const INIT_TIMEOUT_MS = 15000;
12+
1013
// Origins the platform may embed this ui from.
1114
const ALLOWED_PARENT_ORIGINS = [];
1215

@@ -29,7 +32,16 @@
2932

3033
// Callers (quickstart-page.js) must wait on it before touching SETUP.apiUrl/apiKey/runId.
3134
SETUP.ready = new Promise((resolve, reject) => {
32-
window.addEventListener("message", function handleInit(event) {
35+
const timeoutId = setTimeout(() => {
36+
window.removeEventListener("message", handleInit);
37+
reject({
38+
status: 0,
39+
statusText: "platform did not send an init message within "
40+
+ (INIT_TIMEOUT_MS / 1000) + "s"
41+
});
42+
}, INIT_TIMEOUT_MS);
43+
44+
function handleInit(event) {
3345
if (!isTrustedOrigin(event)) return;
3446
const msg = event.data;
3547
if (!msg || msg.source !== MESSAGE_SOURCE || msg.type !== "init") return;
@@ -43,6 +55,7 @@
4355
SETUP.apiKey = apiKey;
4456
}
4557

58+
clearTimeout(timeoutId);
4659
window.removeEventListener("message", handleInit);
4760
parentOrigin = event.origin;
4861
window.parent.postMessage({source: MESSAGE_SOURCE, type: "init-response", success: ok}, parentOrigin);
@@ -53,7 +66,9 @@
5366
} else {
5467
reject({status: 0, statusText: "platform did not send tenantId/runId/apiUrl/apiKey"});
5568
}
56-
});
69+
}
70+
71+
window.addEventListener("message", handleInit);
5772
});
5873

5974
// ── Iframe resize reporting ──

‎use-cases/flight-crew-scheduling/src/main/resources/META-INF/resources/shared/platform-integration.js‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
const MESSAGE_SOURCE = "timefold-visualization";
99

10+
// How long to wait for the platform's "init" message before giving up.
11+
const INIT_TIMEOUT_MS = 15000;
12+
1013
// Origins the platform may embed this ui from.
1114
const ALLOWED_PARENT_ORIGINS = [];
1215

@@ -29,7 +32,16 @@
2932

3033
// Callers (quickstart-page.js) must wait on it before touching SETUP.apiUrl/apiKey/runId.
3134
SETUP.ready = new Promise((resolve, reject) => {
32-
window.addEventListener("message", function handleInit(event) {
35+
const timeoutId = setTimeout(() => {
36+
window.removeEventListener("message", handleInit);
37+
reject({
38+
status: 0,
39+
statusText: "platform did not send an init message within "
40+
+ (INIT_TIMEOUT_MS / 1000) + "s"
41+
});
42+
}, INIT_TIMEOUT_MS);
43+
44+
function handleInit(event) {
3345
if (!isTrustedOrigin(event)) return;
3446
const msg = event.data;
3547
if (!msg || msg.source !== MESSAGE_SOURCE || msg.type !== "init") return;
@@ -43,6 +55,7 @@
4355
SETUP.apiKey = apiKey;
4456
}
4557

58+
clearTimeout(timeoutId);
4659
window.removeEventListener("message", handleInit);
4760
parentOrigin = event.origin;
4861
window.parent.postMessage({source: MESSAGE_SOURCE, type: "init-response", success: ok}, parentOrigin);
@@ -53,7 +66,9 @@
5366
} else {
5467
reject({status: 0, statusText: "platform did not send tenantId/runId/apiUrl/apiKey"});
5568
}
56-
});
69+
}
70+
71+
window.addEventListener("message", handleInit);
5772
});
5873

5974
// ── Iframe resize reporting ──

0 commit comments

Comments
 (0)