Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e41534a47e
|
||
|
|
0a35966498
|
||
|
|
6a27f3d9b4 | ||
|
|
f80d1ac9f8 | ||
|
|
e47cca4b81 | ||
|
|
c7cf55a99e |
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
jobs:
|
||||||
|
maven:
|
||||||
|
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
publish: false
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# GitHub Release Configuration
|
|
||||||
# Auto-generates release notes from PR labels
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
exclude:
|
|
||||||
labels:
|
|
||||||
- ignore-for-release
|
|
||||||
- duplicate
|
|
||||||
- invalid
|
|
||||||
authors:
|
|
||||||
- dependabot
|
|
||||||
- dependabot[bot]
|
|
||||||
categories:
|
|
||||||
- title: Breaking Changes
|
|
||||||
labels:
|
|
||||||
- breaking-change
|
|
||||||
- breaking
|
|
||||||
- title: New Features
|
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
- feature
|
|
||||||
- title: Bug Fixes
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
- fix
|
|
||||||
- title: Dependencies
|
|
||||||
labels:
|
|
||||||
- dependencies
|
|
||||||
- title: Other Changes
|
|
||||||
labels:
|
|
||||||
- "*"
|
|
||||||
@@ -189,3 +189,11 @@ mvn test -pl qctl-integration-tests # E2E tests only
|
|||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
To continue from last session, see `docs/internal/session-state.md` for current progress and next steps.
|
To continue from last session, see `docs/internal/session-state.md` for current progress and next steps.
|
||||||
|
|
||||||
|
## Knowledge base
|
||||||
|
|
||||||
|
Reviewed dossier for this repo (commit `c7cf55a99e51`, branch `develop`, 2026-07-04):
|
||||||
|
`R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/repos/qctl.md`
|
||||||
|
|
||||||
|
QQQ platform hub (cross-repo map, v4.0 impact tracking, licensing follow-ups):
|
||||||
|
`R:/Git.Local/KofTwentyTwo/second-brain/knowledge/qqq/qqq-hub.md`
|
||||||
@@ -1,22 +1,6 @@
|
|||||||
# Session State
|
# Session State
|
||||||
|
|
||||||
Last updated: 2026-10-30
|
Last updated: 2026-01-03
|
||||||
|
|
||||||
## Native CI qualification fixes (2026-10-30)
|
|
||||||
|
|
||||||
Local native CI exposed three defects: `--no-prompt` omitted declared defaults,
|
|
||||||
unknown templates still entered interactive selection, and Windows backslashes
|
|
||||||
escaped Velocity path variables. Defaults now apply with validated explicit
|
|
||||||
overrides; unknown templates return NOT_FOUND without reading stdin; path
|
|
||||||
separators are normalized before interpolation. Interactive fallback remains
|
|
||||||
covered separately.
|
|
||||||
|
|
||||||
Four non-interactive regressions and a Windows-path regression failed before the
|
|
||||||
fixes. On macOS27.2 ARM64/GraalVM21.0.12, full Maven verification, native packaging
|
|
||||||
and verification with the built binary passed235 tests across26 reports with
|
|
||||||
zero failures/errors/skips. Native tests must run after building the binary;
|
|
||||||
the earlier build-before-test arrangement silently skipped10 native E2E cases.
|
|
||||||
Exact-commit Gitea macOS/Windows verification and normal PR review remain pending.
|
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
|
|||||||
@@ -298,82 +298,6 @@ class InitCommandE2ETest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* Non-interactive generation applies defaults without consuming stdin.
|
|
||||||
*
|
|
||||||
* @throws IOException if the generated file cannot be read
|
|
||||||
* @since 0.2.1
|
|
||||||
***************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testInit_noPrompt_usesDefaults() throws IOException
|
|
||||||
{
|
|
||||||
Path outputDir = tempDir.resolve("default-values");
|
|
||||||
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
|
|
||||||
"-o", outputDir.toString());
|
|
||||||
|
|
||||||
assertThat(harness.getExitCode()).as(harness.getStderr()).isEqualTo(ExitCodes.SUCCESS);
|
|
||||||
assertThat(Files.readString(outputDir.resolve("README.md")))
|
|
||||||
.contains("my-project", "com.example");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* Explicit variables override defaults while omitted variables retain them.
|
|
||||||
*
|
|
||||||
* @throws IOException if the generated file cannot be read
|
|
||||||
* @since 0.2.1
|
|
||||||
***************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testInit_noPrompt_overridesOneDefault() throws IOException
|
|
||||||
{
|
|
||||||
Path outputDir = tempDir.resolve("override-default");
|
|
||||||
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
|
|
||||||
"--var", "projectName=custom-project", "-o", outputDir.toString());
|
|
||||||
|
|
||||||
assertThat(harness.getExitCode()).as(harness.getStderr()).isEqualTo(ExitCodes.SUCCESS);
|
|
||||||
assertThat(Files.readString(outputDir.resolve("README.md")))
|
|
||||||
.contains("custom-project", "com.example");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* An unknown template must not fall back to an interactive selection.
|
|
||||||
*
|
|
||||||
* @since 0.2.1
|
|
||||||
***************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testInit_noPrompt_unknownTemplateReturnsNotFound()
|
|
||||||
{
|
|
||||||
Path outputDir = tempDir.resolve("unknown-template");
|
|
||||||
harness.execute("qqq", "init", "missing-template", "--no-prompt",
|
|
||||||
"-o", outputDir.toString());
|
|
||||||
|
|
||||||
assertThat(harness.getExitCode()).isEqualTo(ExitCodes.NOT_FOUND);
|
|
||||||
assertThat(outputDir).doesNotExist();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* Invalid required overrides must fail instead of prompting or using defaults.
|
|
||||||
*
|
|
||||||
* @since 0.2.1
|
|
||||||
***************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testInit_noPrompt_rejectsEmptyRequiredOverride()
|
|
||||||
{
|
|
||||||
Path outputDir = tempDir.resolve("invalid-override");
|
|
||||||
harness.execute("qqq", "init", "test-with-prompts", "--no-prompt",
|
|
||||||
"--var", "projectName=", "-o", outputDir.toString());
|
|
||||||
|
|
||||||
assertThat(harness.getExitCode()).isEqualTo(ExitCodes.VALIDATION);
|
|
||||||
assertThat(outputDir).doesNotExist();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Test combined flags work together.
|
* Test combined flags work together.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -613,19 +613,20 @@ class InitCommandFullE2ETest
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Test behavior when requesting a template that doesn't exist.
|
* Test behavior when requesting a template that doesn't exist.
|
||||||
*
|
*
|
||||||
* Interactive selection can accept the first available fallback template.
|
* With TestTemplateRegistry, non-existent templates cause the system
|
||||||
|
* to fall back to the first available template in --no-prompt mode.
|
||||||
*
|
*
|
||||||
* @since 0.2.0
|
* @since 0.2.0
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@Test
|
@Test
|
||||||
void testInit_unknownTemplate_interactiveFallbackToFirst() throws IOException
|
void testInit_unknownTemplate_fallsBackToFirst() throws IOException
|
||||||
{
|
{
|
||||||
Path outputDir = tempDir.resolve("unknown-template-test");
|
Path outputDir = tempDir.resolve("unknown-template-test");
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// The empty input accepts the first interactive selection. //
|
// With --no-prompt, requesting unknown template uses first available //
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
harness.execute("qqq", "init", "non-existent-template",
|
harness.execute("qqq", "init", "non-existent-template", "--no-prompt",
|
||||||
"-o", outputDir.toString());
|
"-o", outputDir.toString());
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import io.qrun.qctl.qqq.template.ConsoleUI;
|
|||||||
import io.qrun.qctl.qqq.template.MergeOptions;
|
import io.qrun.qctl.qqq.template.MergeOptions;
|
||||||
import io.qrun.qctl.qqq.template.PostGenHookRunner;
|
import io.qrun.qctl.qqq.template.PostGenHookRunner;
|
||||||
import io.qrun.qctl.qqq.template.PromptRunner;
|
import io.qrun.qctl.qqq.template.PromptRunner;
|
||||||
import io.qrun.qctl.qqq.template.PromptValidator;
|
|
||||||
import io.qrun.qctl.qqq.template.TemplateEngine;
|
import io.qrun.qctl.qqq.template.TemplateEngine;
|
||||||
import io.qrun.qctl.qqq.template.TemplateManifest;
|
import io.qrun.qctl.qqq.template.TemplateManifest;
|
||||||
import io.qrun.qctl.qqq.template.TemplateRenderException;
|
import io.qrun.qctl.qqq.template.TemplateRenderException;
|
||||||
@@ -173,21 +172,6 @@ public class InitCommand implements Callable<Integer>
|
|||||||
|
|
||||||
// Step 4: Collect variables (prompts + CLI overrides)
|
// Step 4: Collect variables (prompts + CLI overrides)
|
||||||
Map<String, String> allVars = new LinkedHashMap<>();
|
Map<String, String> allVars = new LinkedHashMap<>();
|
||||||
if(noPrompt && manifest.prompts() != null)
|
|
||||||
{
|
|
||||||
PromptValidator validator = new PromptValidator();
|
|
||||||
for(TemplateManifest.Prompt prompt : manifest.prompts())
|
|
||||||
{
|
|
||||||
String value = variables.getOrDefault(prompt.name(), prompt.defaultValue());
|
|
||||||
PromptValidator.ValidationResult result = validator.validate(prompt, value);
|
|
||||||
if(!result.valid())
|
|
||||||
{
|
|
||||||
ui.error(result.errorMessage());
|
|
||||||
return ExitCodes.VALIDATION;
|
|
||||||
}
|
|
||||||
allVars.put(prompt.name(), value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!noPrompt && manifest.prompts() != null && !manifest.prompts().isEmpty())
|
if(!noPrompt && manifest.prompts() != null && !manifest.prompts().isEmpty())
|
||||||
{
|
{
|
||||||
PromptRunner promptRunner = new PromptRunner();
|
PromptRunner promptRunner = new PromptRunner();
|
||||||
@@ -334,12 +318,6 @@ public class InitCommand implements Callable<Integer>
|
|||||||
List<String> templateIds = templates.stream().map(TemplateInfo::id).toList();
|
List<String> templateIds = templates.stream().map(TemplateInfo::id).toList();
|
||||||
SuggestionEngine suggestionEngine = new SuggestionEngine();
|
SuggestionEngine suggestionEngine = new SuggestionEngine();
|
||||||
List<String> suggestions = suggestionEngine.findSimilar(templateName, templateIds);
|
List<String> suggestions = suggestionEngine.findSimilar(templateName, templateIds);
|
||||||
if(noPrompt)
|
|
||||||
{
|
|
||||||
throw new TemplateError(TemplateError.ErrorCategory.TEMPLATE_NOT_FOUND,
|
|
||||||
"Template '" + templateName + "' not found.", suggestions,
|
|
||||||
"Use 'qctl qqq list' to list available templates");
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.error("Template '" + templateName + "' not found.");
|
ui.error("Template '" + templateName + "' not found.");
|
||||||
if(!suggestions.isEmpty())
|
if(!suggestions.isEmpty())
|
||||||
|
|||||||
@@ -409,9 +409,7 @@ public class TemplateEngine
|
|||||||
// This allows templates to use __VARNAME__ in paths, which is safer //
|
// This allows templates to use __VARNAME__ in paths, which is safer //
|
||||||
// for shells and build tools that might interpret $ as a variable. //
|
// for shells and build tools that might interpret $ as a variable. //
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
// Windows separators otherwise escape the following Velocity variable.
|
String normalizedPath = path.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
|
||||||
String normalizedPath = path.replace('\\', '/')
|
|
||||||
.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
|
|
||||||
return renderContent(normalizedPath, variables);
|
return renderContent(normalizedPath, variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ package io.qrun.qctl.qqq.template;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.InvalidPathException;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
|
||||||
@@ -45,19 +44,11 @@ public class TemplateResolver
|
|||||||
public Path resolve(String source, String version) throws IOException
|
public Path resolve(String source, String version) throws IOException
|
||||||
{
|
{
|
||||||
// Local path
|
// Local path
|
||||||
try
|
Path localPath = Path.of(source);
|
||||||
|
if(Files.isDirectory(localPath))
|
||||||
{
|
{
|
||||||
Path localPath = Path.of(source);
|
System.out.println("Using local template: " + localPath.toAbsolutePath());
|
||||||
if(Files.isDirectory(localPath))
|
return localPath.toAbsolutePath();
|
||||||
{
|
|
||||||
System.out.println("Using local template: " + localPath.toAbsolutePath());
|
|
||||||
return localPath.toAbsolutePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(InvalidPathException e)
|
|
||||||
{
|
|
||||||
// Remote URLs are not valid filesystem paths on Windows.
|
|
||||||
// Validate them below using the supported Git URL formats.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git URL or GitHub shorthand
|
// Git URL or GitHub shorthand
|
||||||
|
|||||||
@@ -303,24 +303,6 @@ class TemplateEngineTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* Windows separators must not escape path placeholders in Velocity.
|
|
||||||
*
|
|
||||||
* @throws TemplateRenderException if rendering fails
|
|
||||||
* @since 0.2.1
|
|
||||||
***************************************************************************/
|
|
||||||
@Test
|
|
||||||
void testRenderWindowsPathVariables() throws TemplateRenderException
|
|
||||||
{
|
|
||||||
Map<String, String> variables = Map.of("packagePath", "com/acme/orders");
|
|
||||||
assertThat(engine.renderPath("src\\main\\java\\$packagePath\\App.java", variables))
|
|
||||||
.isEqualTo("src/main/java/com/acme/orders/App.java");
|
|
||||||
assertThat(engine.renderPath("src\\main\\java\\__packagePath__\\App.java", variables))
|
|
||||||
.isEqualTo("src/main/java/com/acme/orders/App.java");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
* Test binary files are copied without modification.
|
* Test binary files are copied without modification.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|||||||
Reference in new issue
Block a user