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
7 changes: 6 additions & 1 deletion .github/workflows/initializr-cn1-version-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,15 @@ jobs:
Automated update of website Codename One versions after release `${{ github.event.release.tag_name || github.ref_name }}`.

Updated:
- All `scripts/initializr/**/pom.xml` files containing `cn1.plugin.version`
- All `scripts/initializr/**/*pom.xml` files containing `cn1.plugin.version`, including the
`backend-only-pom.xml` resource
- `scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java`
- `scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java`
- Embedded root `pom.xml` in `scripts/initializr/common/src/main/resources/common.zip`

`initializr-e2e.yml` runs on this PR: it builds every download the site will serve at this
release, from an empty Maven repository, and runs each sample's end-to-end suite against its
server.
branch: automation/website-cn1-version-${{ github.event.release.tag_name || github.ref_name }}
delete-branch: true
labels: |
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/initializr-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Initializr downloads (released framework)

# What start.codenameone.com hands a developer, built and run the way they would:
# against the release the Initializr pins, from an empty Maven repository, with
# nothing from this checkout installed. Every other check of the templates builds
# them against 8.0-SNAPSHOT, which is how a download that only worked on the
# SNAPSHOT could pass CI. See scripts/initializr/e2e-shipped.sh.
#
# The version bump opened after each release (initializr-cn1-version-pr.yml)
# touches scripts/initializr, so it runs this against the new release before it
# merges. The nightly run adds --require-latest: a release the bump never reached
# fails it, rather than the site quietly offering the previous one.

on:
pull_request:
paths:
- '.github/workflows/initializr-e2e.yml'
- 'scripts/initializr/**'
- 'scripts/tests/GenerateInitializr.java'
- 'scripts/tests/generate-initializr-fixtures.py'
- 'scripts/sync-initializr-template.py'
- 'scripts/wayline/common/src/test/**'
- 'scripts/savor/common/src/test/**'
- 'scripts/skirmish/common/src/test/**'
- 'scripts/parley/common/src/test/**'
schedule:
- cron: '17 3 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: initializr-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
download:
name: ${{ matrix.template }}
runs-on: ubuntu-24.04
timeout-minutes: 75
strategy:
fail-fast: false
matrix:
template: [BAREBONES, KOTLIN, WAYLINE, SAVOR, SKIRMISH, PARLEY]
steps:
- uses: actions/checkout@v6

# No cache: the point is a developer's first build, from an empty repository.
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'

- name: Ensure a virtual display (xvfb) for the simulator
run: |
if ! command -v xvfb-run >/dev/null 2>&1; then
(timeout 300 sudo apt-get update && timeout 300 sudo apt-get install -y xvfb) || true
fi

- name: Build and run the download
run: |
args=()
if [ "${{ github.event_name }}" = "schedule" ]; then args+=(--require-latest); fi
scripts/initializr/e2e-shipped.sh "${args[@]}" "${{ matrix.template }}"

- name: Upload the logs and captures
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: initializr-e2e-${{ matrix.template }}
path: |
scripts/initializr/target/e2e-shipped/*.log
scripts/initializr/target/e2e-shipped/*-shots/**
if-no-files-found: warn
retention-days: 14
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
import com.codename1.ui.layouts.GridLayout;
import com.codename1.ui.plaf.Style;
import com.codename1.ui.util.UITimer;
import com.codename1.util.StringUtil;

public class Initializr extends Lifecycle {
private boolean darkMode;
Expand Down Expand Up @@ -140,7 +139,7 @@ public void runApp() {
final TemplatePreviewPanel previewPanel = new TemplatePreviewPanel(selectedTemplate[0]);

// Mutable subtitle labels so panel headers reflect the current selection.
final Label ideSubtitle = panelSubtitle(IDE.INTELLIJ.name());
final Label ideSubtitle = panelSubtitle(ideLabel(IDE.INTELLIJ));
final Label localeSubtitle = panelSubtitle("No bundles");
final Label javaSubtitle = panelSubtitle(ProjectOptions.JavaVersion.JAVA_17.label);
final Label buildSubtitle = panelSubtitle(ProjectOptions.BuildTool.MAVEN.label);
Expand Down Expand Up @@ -195,7 +194,7 @@ public void run() {
previewPanel.setTemplate(selectedTemplate[0]);
previewPanel.setOptions(options);

ideSubtitle.setText(formatEnumLabel(selectedIde[0].name()));
ideSubtitle.setText(ideLabel(selectedIde[0]));
localeSubtitle.setText(includeLocalizationBundles[0]
? "Bundles . " + previewLanguage[0].label
: "No bundles");
Expand Down Expand Up @@ -421,8 +420,9 @@ private Container createEssentialsPanel(TextField appNameField, TextField packag
+ "unique. Unique package identifiers are critical for app store submissions because "
+ "they distinguish your app from others and prevent install/update conflicts."));
fields.add(packageError);
fields.add(labeledField(GeneratorModel.isTemplateOffered(Template.WAYLINE) ? "Start from" : "Language",
languageSelector));
// The selector labels its own groups when it offers the complete apps.
fields.add(GeneratorModel.isTemplateOffered(Template.WAYLINE) ? languageSelector
: labeledField("Language", languageSelector));

Container bodyWrap = new Container(new BorderLayout());
bodyWrap.setUIID("InitializrPanelBody");
Expand All @@ -433,41 +433,75 @@ private Container createEssentialsPanel(TextField appNameField, TextField packag
return panel;
}

/// One of the page's toggle choices, which paints as chosen while it has focus.
///
/// A toggle button that has focus paints its focus (`.selected`) style when the
/// display renders selection, and a desktop browser always does: it is not a
/// pure-touch display. A click gives the button focus, so the option just chosen
/// was painted as if it were not chosen until something else took focus. The
/// simulator is pure touch and never showed it. ChoiceSelectionTest clicks every
/// choice that way.
static RadioButton choice(String label) {
RadioButton button = new RadioButton(label) {
@Override
public Style getStyle() {
Style style = super.getStyle();
return isSelected() && hasFocus() ? getPressedStyle() : style;
}
};
button.setToggle(true);
button.setUIID("InitializrChoice");
return button;
}

/// What the project starts as: an empty app in Java or in Kotlin, or -- once the
/// plugin the Initializr generates against can build it -- the ride-hailing,
/// the food-delivery or the messaging app with its server.
/// plugin the Initializr generates against can build it -- one of the complete
/// apps with its server. Two groups, so the list says what each choice is in a
/// word or two instead of six bars that all ended in "with its server, Java".
private Container createLanguageSelector(Template[] selectedTemplate, Runnable onSelectionChanged) {
boolean fullStack = GeneratorModel.isTemplateOffered(Template.WAYLINE);
Container selector = new Container(new GridLayout(fullStack ? 5 : 1, fullStack ? 1 : 2));
selector.setUIID("InitializrChoicesGrid");
ButtonGroup group = new ButtonGroup();
Template[] languages = fullStack
? new Template[] {Template.BAREBONES, Template.KOTLIN, Template.WAYLINE, Template.SAVOR,
Template.SKIRMISH, Template.PARLEY}
: new Template[] {Template.BAREBONES, Template.KOTLIN};
String[] labels = fullStack
? new String[] {"Empty app, Java", "Empty app, Kotlin", "Ride-hailing app with its server, Java",
"Food delivery app with its server, Java",
"Card battle game with its server, Java", "Messaging app with its server, Java"}
: new String[] {"Java", "Kotlin"};
for (int i = 0; i < languages.length; i++) {
final Template template = languages[i];
RadioButton button = new RadioButton(labels[i]);
button.setToggle(true);
button.setUIID("InitializrChoice");
group.add(button);
selector.add(button);
if (template == selectedTemplate[0]) {
button.setSelected(true);
}
button.addActionListener(evt -> {
if (button.isSelected()) {
selectedTemplate[0] = template;
onSelectionChanged.run();
}
});
Container empty = new Container(new GridLayout(1, 2));
empty.setUIID("InitializrChoicesGrid");
addTemplateChoice(empty, group, Template.BAREBONES, "Java", selectedTemplate, onSelectionChanged);
addTemplateChoice(empty, group, Template.KOTLIN, "Kotlin", selectedTemplate, onSelectionChanged);
if (!GeneratorModel.isTemplateOffered(Template.WAYLINE)) {
return empty;
}
return selector;
Template[] apps = {Template.WAYLINE, Template.SAVOR, Template.SKIRMISH, Template.PARLEY};
Container complete = new Container(new GridLayout(2, 2));
complete.setUIID("InitializrChoicesGrid");
for (Template app : apps) {
addTemplateChoice(complete, group, app, templateTitle(app), selectedTemplate, onSelectionChanged);
}
SpanLabel hint = new SpanLabel("A complete app is the app, its server and the API module they share, "
+ "with demo accounts. Start the server with run-server, then the app with run.");
hint.setUIID("InitializrTip");
hint.setTextUIID("InitializrTip");
return BoxLayout.encloseY(labeledField("Empty app", empty),
labeledField("Complete app with its server (Java)", complete), hint);
}

private void addTemplateChoice(Container parent, ButtonGroup group, final Template template, String label,
Template[] selectedTemplate, Runnable onSelectionChanged) {
RadioButton button = choice(label);
group.add(button);
parent.add(button);
if (template == selectedTemplate[0]) {
button.setSelected(true);
}
button.addActionListener(evt -> {
if (button.isSelected()) {
selectedTemplate[0] = template;
onSelectionChanged.run();
}
});
}

/// What a full-stack template is, in a few words.
static String templateTitle(Template template) {
return template == Template.SAVOR ? "Food delivery" : template == Template.SKIRMISH ? "Card battle game"
: template == Template.PARLEY ? "Messaging" : template == Template.WAYLINE ? "Ride hailing"
: template.IS_KOTLIN ? "Empty app, Kotlin" : "Empty app, Java";
}

/// The colour schemes a full-stack template is offered in. The first is the
Expand Down Expand Up @@ -512,9 +546,7 @@ private Container createLookPanel(int[] brandColor, boolean[] roundedCorners, La
ButtonGroup group = new ButtonGroup();
for (int i = 0; i < SCHEME_COLORS.length; i++) {
final int color = SCHEME_COLORS[i];
RadioButton button = new RadioButton(SCHEME_LABELS[i]);
button.setToggle(true);
button.setUIID("InitializrChoice");
RadioButton button = choice(SCHEME_LABELS[i]);
// The template's own colour is painted when the template is known.
button.setIcon(swatch(color < 0 ? 0 : color));
if (color < 0) {
Expand Down Expand Up @@ -552,9 +584,7 @@ private Container createIdeSelectorPanel(IDE[] selectedIde, Runnable onSelection
selector.setUIID("InitializrChoicesGrid");
ButtonGroup group = new ButtonGroup();
for (IDE ide : IDE.values()) {
RadioButton button = new RadioButton(formatEnumLabel(ide.name()));
button.setToggle(true);
button.setUIID("InitializrChoice");
RadioButton button = choice(ideLabel(ide));
group.add(button);
selector.add(button);
if (ide == selectedIde[0]) {
Expand Down Expand Up @@ -609,10 +639,8 @@ private Container createJavaOptionsPanel(ProjectOptions.JavaVersion[] javaVersio
ProjectOptions.JavaVersion[] versions = {ProjectOptions.JavaVersion.JAVA_17, ProjectOptions.JavaVersion.JAVA_8};
for (int i = 0; i < versions.length; i++) {
final ProjectOptions.JavaVersion version = versions[i];
RadioButton button = new RadioButton(labels[i]);
RadioButton button = choice(labels[i]);
buttons[i] = button;
button.setToggle(true);
button.setUIID("InitializrChoice");
group.add(button);
selector.add(button);
if (version == javaVersion[0]) {
Expand Down Expand Up @@ -669,10 +697,8 @@ private Container createBuildToolPanel(ProjectOptions.BuildTool[] buildTool,
ButtonGroup typeGroup = new ButtonGroup();
final RadioButton[] typeButtons = new RadioButton[ProjectOptions.ProjectType.values().length];
for (ProjectOptions.ProjectType type : ProjectOptions.ProjectType.values()) {
RadioButton button = new RadioButton(type.label);
RadioButton button = choice(type.label);
typeButtons[type.ordinal()] = button;
button.setToggle(true);
button.setUIID("InitializrChoice");
typeGroup.add(button);
types.add(button);
if (type == projectType[0]) {
Expand Down Expand Up @@ -703,9 +729,7 @@ private Container createBuildToolPanel(ProjectOptions.BuildTool[] buildTool,
if (tool == ProjectOptions.BuildTool.GRADLE && !GeneratorModel.isGradleOffered()) {
continue;
}
RadioButton button = new RadioButton(tool.label);
button.setToggle(true);
button.setUIID("InitializrChoice");
RadioButton button = choice(tool.label);
toolGroup.add(button);
tools.add(button);
if (tool == buildTool[0]) {
Expand Down Expand Up @@ -1047,11 +1071,10 @@ private ProjectOptions.PreviewLanguage findLanguageByLabel(String label) {
return ProjectOptions.PreviewLanguage.ENGLISH;
}

private String formatEnumLabel(String text) {
if ("DEFAULT".equals(text)) {
return "Clean";
}
return StringUtil.replaceAll(text, "_", " ");
/// The name an IDE goes by, not its constant.
static String ideLabel(IDE ide) {
return ide == IDE.INTELLIJ ? "IntelliJ IDEA" : ide == IDE.NETBEANS ? "NetBeans"
: ide == IDE.VS_CODE ? "VS Code" : "Eclipse";
}

// ---------- theme sync ----------
Expand Down Expand Up @@ -1178,12 +1201,9 @@ private String createSummary(String appName, String packageName, Template templa
String safePackage = packageName == null ? "" : packageName.trim();
return "App " + safeApp + "\n"
+ "Package " + safePackage + "\n"
+ (template == Template.SKIRMISH ? "Template CARD-BATTLE GAME + SERVER\n"
: template == Template.PARLEY ? "Template MESSAGING APP + SERVER\n"
: template == Template.SAVOR ? "Template FOOD-DELIVERY APP + SERVER\n"
: template.isFullStack() ? "Template RIDE-HAILING APP + SERVER\n" : "")
+ "Language " + (template.IS_KOTLIN ? "KOTLIN" : "JAVA") + "\n"
+ "IDE " + ide.name() + "\n"
+ (template.isFullStack() ? "Template " + templateTitle(template) + " and its server\n" : "")
+ "Language " + (template.IS_KOTLIN ? "Kotlin" : "Java") + "\n"
+ "IDE " + ideLabel(ide) + "\n"
+ "Build " + options.buildTool.label
+ (options.isGradle() || GeneratorModel.isMavenLayoutChoiceOffered()
? " (" + options.projectType.label
Expand All @@ -1192,7 +1212,7 @@ private String createSummary(String appName, String packageName, Template templa
? ", all modules" : "") + ")"
: "") + "\n"
+ "Java " + options.javaVersion.label + "\n"
+ "Bundles " + (options.includeLocalizationBundles ? "INCLUDED" : "NONE") + "\n"
+ "Bundles " + (options.includeLocalizationBundles ? "Included" : "None") + "\n"
+ "Preview " + options.previewLanguage.label;
}

Expand Down
Loading
Loading