7 Commits
Author SHA1 Message Date
james.maesandCodex 74f4b88a54 fix: honor non-interactive template defaults and Windows paths (#71)
* fix: honor non-interactive template defaults and Windows paths

Return NOT_FOUND for unknown templates without prompting. Preserve interactive fallback and validate non-interactive overrides.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>

* fix: resolve remote template URLs on Windows

Windows rejects HTTPS URLs as filesystem paths before Git can clone them. Fall through to supported Git URL validation when the local path probe is invalid.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-10-30 10:54:31 -05:00
james.maesandCodex d84a5783bf ci: use shared native worker on main (#70)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 17:07:07 -05:00
james.maes deafa64779 chore: migrate license from AGPL-3.0 to Apache-2.0 2026-01-05 10:11:43 -06:00
james.maes b5086fbd63 Merge branch 'develop' 2026-01-04 19:43:39 -06:00
james.maes 25927df071 Merge develop into main for v0.2.0 release 2026-01-04 19:22:05 -06:00
james.maes 44ecc8793d docs: Standardize README 2025-12-24 14:21:42 -06:00
james.maes 67478867d9 ci: Add GitHub release configuration 2025-12-24 12:44:44 -06:00
10 changed files with 184 additions and 28 deletions

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
with:
publish: false
+31
View File
@@ -0,0 +1,31 @@
# 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:
- "*"
-8
View File
@@ -189,11 +189,3 @@ mvn test -pl qctl-integration-tests # E2E tests only
## Session Continuity
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`
+17 -1
View File
@@ -1,6 +1,22 @@
# Session State
Last updated: 2026-01-03
Last updated: 2026-10-30
## 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
@@ -298,6 +298,82 @@ 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.
*
@@ -613,20 +613,19 @@ class InitCommandFullE2ETest
/***************************************************************************
* Test behavior when requesting a template that doesn't exist.
*
* With TestTemplateRegistry, non-existent templates cause the system
* to fall back to the first available template in --no-prompt mode.
* Interactive selection can accept the first available fallback template.
*
* @since 0.2.0
***************************************************************************/
@Test
void testInit_unknownTemplate_fallsBackToFirst() throws IOException
void testInit_unknownTemplate_interactiveFallbackToFirst() throws IOException
{
Path outputDir = tempDir.resolve("unknown-template-test");
//////////////////////////////////////////////////////////////////////////
// With --no-prompt, requesting unknown template uses first available //
// The empty input accepts the first interactive selection. //
//////////////////////////////////////////////////////////////////////////
harness.execute("qqq", "init", "non-existent-template", "--no-prompt",
harness.execute("qqq", "init", "non-existent-template",
"-o", outputDir.toString());
//////////////////////////////////////////////////////////////////////////
@@ -37,6 +37,7 @@ import io.qrun.qctl.qqq.template.ConsoleUI;
import io.qrun.qctl.qqq.template.MergeOptions;
import io.qrun.qctl.qqq.template.PostGenHookRunner;
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.TemplateManifest;
import io.qrun.qctl.qqq.template.TemplateRenderException;
@@ -172,6 +173,21 @@ public class InitCommand implements Callable<Integer>
// Step 4: Collect variables (prompts + CLI overrides)
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())
{
PromptRunner promptRunner = new PromptRunner();
@@ -318,6 +334,12 @@ public class InitCommand implements Callable<Integer>
List<String> templateIds = templates.stream().map(TemplateInfo::id).toList();
SuggestionEngine suggestionEngine = new SuggestionEngine();
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.");
if(!suggestions.isEmpty())
@@ -409,7 +409,9 @@ public class TemplateEngine
// This allows templates to use __VARNAME__ in paths, which is safer //
// for shells and build tools that might interpret $ as a variable. //
/////////////////////////////////////////////////////////////////////////
String normalizedPath = path.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
// Windows separators otherwise escape the following Velocity variable.
String normalizedPath = path.replace('\\', '/')
.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1");
return renderContent(normalizedPath, variables);
}
@@ -15,6 +15,7 @@ package io.qrun.qctl.qqq.template;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@@ -44,11 +45,19 @@ public class TemplateResolver
public Path resolve(String source, String version) throws IOException
{
// Local path
Path localPath = Path.of(source);
if(Files.isDirectory(localPath))
try
{
System.out.println("Using local template: " + localPath.toAbsolutePath());
return localPath.toAbsolutePath();
Path localPath = Path.of(source);
if(Files.isDirectory(localPath))
{
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
@@ -303,6 +303,24 @@ 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.
***************************************************************************/