Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f4b88a54 | ||
|
|
d84a5783bf | ||
|
|
deafa64779 | ||
|
|
b5086fbd63 | ||
|
|
25927df071 | ||
|
|
44ecc8793d | ||
|
|
67478867d9 |
No files matched your search
@@ -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:
|
||||
- "*"
|
||||
+16
-304
@@ -1,313 +1,25 @@
|
||||
# Generated by qqq-orb generate_qctl_workflow; edit the central worker, not local jobs.
|
||||
# GitHub-native macOS/Windows runners are required for the release binaries.
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
full_native:
|
||||
description: Qualify all five native platforms without publishing
|
||||
type: boolean
|
||||
default: false
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'graalvm'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-maven-
|
||||
|
||||
- name: Run tests
|
||||
run: mvn clean verify -q
|
||||
|
||||
build-native:
|
||||
needs: test
|
||||
if: github.event_name == 'push'
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact: qctl-linux-amd64
|
||||
- os: ubuntu-24.04-arm
|
||||
artifact: qctl-linux-arm64
|
||||
- os: macos-15
|
||||
artifact: qctl-macos-amd64
|
||||
- os: macos-14
|
||||
artifact: qctl-macos-arm64
|
||||
- os: windows-latest
|
||||
artifact: qctl-windows-amd64.exe
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM
|
||||
uses: graalvm/setup-graalvm@v1
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'graalvm'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cache Maven packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: ${{ runner.os }}-maven-
|
||||
|
||||
- name: Build native image
|
||||
run: mvn clean package -DskipTests -Pnative -q
|
||||
|
||||
- name: Rename artifact (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: mv qctl-cli/target/qctl qctl-cli/target/${{ matrix.artifact }}
|
||||
|
||||
- name: Rename artifact (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: mv qctl-cli/target/qctl.exe qctl-cli/target/${{ matrix.artifact }}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: qctl-cli/target/${{ matrix.artifact }}
|
||||
|
||||
release:
|
||||
needs: build-native
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Flatten artifacts
|
||||
run: |
|
||||
mkdir -p release
|
||||
find artifacts -type f -exec mv {} release/ \;
|
||||
ls -la release/
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/*
|
||||
generate_release_notes: true
|
||||
|
||||
docker:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Linux AMD64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qctl-linux-amd64
|
||||
path: .
|
||||
|
||||
- name: Download Linux ARM64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qctl-linux-arm64
|
||||
path: .
|
||||
|
||||
- name: Prepare binaries
|
||||
run: |
|
||||
chmod +x qctl-linux-amd64 qctl-linux-arm64
|
||||
# Dockerfile expects qctl-linux-amd64 and qctl-linux-arm64
|
||||
ls -la qctl-linux-*
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: packaging/docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/qrun-io/qctl:${{ github.ref_name }}
|
||||
ghcr.io/qrun-io/qctl:latest
|
||||
|
||||
update-homebrew:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Checkout homebrew-qctl tap
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: QRun-IO/homebrew-qctl
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
path: homebrew-tap
|
||||
|
||||
- name: Update Homebrew formula
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${VERSION#v}"
|
||||
|
||||
SHA_MACOS_ARM64=$(shasum -a 256 artifacts/qctl-macos-arm64/qctl-macos-arm64 | cut -d' ' -f1)
|
||||
SHA_MACOS_AMD64=$(shasum -a 256 artifacts/qctl-macos-amd64/qctl-macos-amd64 | cut -d' ' -f1)
|
||||
SHA_LINUX_ARM64=$(shasum -a 256 artifacts/qctl-linux-arm64/qctl-linux-arm64 | cut -d' ' -f1)
|
||||
SHA_LINUX_AMD64=$(shasum -a 256 artifacts/qctl-linux-amd64/qctl-linux-amd64 | cut -d' ' -f1)
|
||||
|
||||
FORMULA="homebrew-tap/Formula/qctl.rb"
|
||||
|
||||
# Update version
|
||||
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "$FORMULA"
|
||||
|
||||
# Update SHA256 hashes in order: macos-arm64, macos-amd64, linux-arm64, linux-amd64
|
||||
awk -v v1="$SHA_MACOS_ARM64" -v v2="$SHA_MACOS_AMD64" -v v3="$SHA_LINUX_ARM64" -v v4="$SHA_LINUX_AMD64" '
|
||||
BEGIN { n = 0 }
|
||||
/sha256/ {
|
||||
n++
|
||||
if (n == 1) sub(/sha256 "[^"]*"/, "sha256 \"" v1 "\"")
|
||||
else if (n == 2) sub(/sha256 "[^"]*"/, "sha256 \"" v2 "\"")
|
||||
else if (n == 3) sub(/sha256 "[^"]*"/, "sha256 \"" v3 "\"")
|
||||
else if (n == 4) sub(/sha256 "[^"]*"/, "sha256 \"" v4 "\"")
|
||||
}
|
||||
{ print }
|
||||
' "$FORMULA" > "$FORMULA.tmp" && mv "$FORMULA.tmp" "$FORMULA"
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
cd homebrew-tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Formula/qctl.rb
|
||||
git diff --staged --quiet || git commit -m "Update qctl to ${GITHUB_REF_NAME}"
|
||||
git push
|
||||
|
||||
update-scoop:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qctl-windows-amd64.exe
|
||||
path: artifacts
|
||||
|
||||
- name: Update Scoop manifest
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${VERSION#v}"
|
||||
SHA_WINDOWS=$(shasum -a 256 artifacts/qctl-windows-amd64.exe | cut -d' ' -f1)
|
||||
|
||||
MANIFEST="packaging/scoop/qctl.json"
|
||||
|
||||
# Update version
|
||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" "$MANIFEST"
|
||||
|
||||
# Update download URL version
|
||||
sed -i "s|/download/v[^/]*/|/download/v${VERSION}/|g" "$MANIFEST"
|
||||
|
||||
# Update hash
|
||||
sed -i "s/\"hash\": \"[^\"]*\"/\"hash\": \"${SHA_WINDOWS}\"/" "$MANIFEST"
|
||||
|
||||
- name: Clean up artifacts
|
||||
run: rm -rf artifacts
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(scoop): update manifest to ${{ github.ref_name }}"
|
||||
title: "chore(scoop): update manifest to ${{ github.ref_name }}"
|
||||
body: "Automated Scoop manifest update for release ${{ github.ref_name }}"
|
||||
branch: chore/scoop-${{ github.ref_name }}
|
||||
base: develop
|
||||
add-paths: packaging/scoop/qctl.json
|
||||
|
||||
update-aur:
|
||||
needs: release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Download Linux AMD64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qctl-linux-amd64
|
||||
path: artifacts
|
||||
|
||||
- name: Update AUR PKGBUILD
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
VERSION="${VERSION#v}"
|
||||
SHA_LINUX=$(shasum -a 256 artifacts/qctl-linux-amd64 | cut -d' ' -f1)
|
||||
|
||||
PKGBUILD="packaging/aur/PKGBUILD"
|
||||
|
||||
# Update version
|
||||
sed -i "s/pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
|
||||
|
||||
# Reset pkgrel for new version
|
||||
sed -i "s/pkgrel=.*/pkgrel=1/" "$PKGBUILD"
|
||||
|
||||
# Update hash
|
||||
sed -i "s/sha256sums_x86_64=('[^']*')/sha256sums_x86_64=('${SHA_LINUX}')/" "$PKGBUILD"
|
||||
|
||||
- name: Clean up artifacts
|
||||
run: rm -rf artifacts
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "chore(aur): update PKGBUILD to ${{ github.ref_name }}"
|
||||
title: "chore(aur): update PKGBUILD to ${{ github.ref_name }}"
|
||||
body: "Automated AUR PKGBUILD update for release ${{ github.ref_name }}"
|
||||
branch: chore/aur-${{ github.ref_name }}
|
||||
base: develop
|
||||
add-paths: packaging/aur/PKGBUILD
|
||||
native:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qctl-native.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
|
||||
with:
|
||||
full_native: ${{ github.event_name == 'workflow_dispatch' && inputs.full_native }}
|
||||
secrets: inherit
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
***************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user