From 9b9a85d71fdc8edd628054f93135bf6769bf3e66 Mon Sep 17 00:00:00 2001 From: Joaquin Franco Date: Fri, 20 Mar 2026 13:11:57 +0900 Subject: [PATCH 1/5] feat: add integration tests --- .dockerignore | 28 +++++++++++++++++ .github/workflows/integration-test.yaml | 41 ++++++++++++++++++++++++ integration/Dockerfile | 35 +++++++++++++++++++++ integration/docker-compose.yaml | 8 +++++ integration/justfile | 42 +++++++++++++++++++++++++ integration/sample.yaml | 17 ++++++++++ 6 files changed, 171 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/integration-test.yaml create mode 100644 integration/Dockerfile create mode 100644 integration/docker-compose.yaml create mode 100644 integration/justfile create mode 100644 integration/sample.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c3553cb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env* +**/.git +**/.github +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/*Dockerfile +**/docs +LICENSE +README.md diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml new file mode 100644 index 0000000..c3a6736 --- /dev/null +++ b/.github/workflows/integration-test.yaml @@ -0,0 +1,41 @@ +--- +name: Integration Test +"on": + workflow_dispatch: + pull_request: + branches: + - main + paths: + - "integration/**" + - "**/*.go" + push: + branches: + - main + +permissions: + id-token: write + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration-test: + name: Run integration tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Just + uses: ./.github/actions/setup-workspace + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Run integration tests + working-directory: ./integration + run: | + just build + just test-all diff --git a/integration/Dockerfile b/integration/Dockerfile new file mode 100644 index 0000000..09a08de --- /dev/null +++ b/integration/Dockerfile @@ -0,0 +1,35 @@ +# ========== CLI BUILD STAGE ========== +ARG GO_VERSION=1.25 +FROM golang:${GO_VERSION}-alpine AS build + +WORKDIR /src +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x +COPY . . +RUN --mount=type=cache,target=/go/pkg/mod/ \ + CGO_ENABLED=0 go build -o /bin/hackstack . + +# ========== CLI RUN STAGE ========== +FROM ubuntu:22.04 AS app + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates xz-utils && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=build /bin/hackstack /bin/hackstack + +# Tests should use non-root to simulate a real user. +ARG USERNAME=testuser +ARG USER_ID=1000 +ARG GROUP_ID=1000 +RUN addgroup --gid $GROUP_ID ${USERNAME} && \ + adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ${USERNAME} + +USER $USERNAME +ENV HOME=/home/${USERNAME} +WORKDIR ${HOME}/projects + +ENTRYPOINT [ "/bin/hackstack" ] +CMD ["--help"] diff --git a/integration/docker-compose.yaml b/integration/docker-compose.yaml new file mode 100644 index 0000000..57e08fc --- /dev/null +++ b/integration/docker-compose.yaml @@ -0,0 +1,8 @@ +# Integration test Docker suite +services: + hackstack: + container_name: hackstack + image: hackstack:latest + build: + context: .. + dockerfile: integration/Dockerfile diff --git a/integration/justfile b/integration/justfile new file mode 100644 index 0000000..d85ae21 --- /dev/null +++ b/integration/justfile @@ -0,0 +1,42 @@ +# INTEGRATION TEST SCRIPTS + +CAPTURE_OUTPUT_FILE := "output.txt" + +# Default target to list available commands +_default: + @just --list --unsorted + +########################################################## +# TEST WORKSPACE SETUP +########################################################## + +# Build the services in Docker +build: + docker compose build + +# Workspace teardown +clean: + -docker compose down + -rm {{ CAPTURE_OUTPUT_FILE }} + +########################################################## +# INTERACTIVE COMMANDS +########################################################## + +# Run the CLI image with envs +hackstack *args: + @docker compose run \ + --remove-orphans --rm \ + hackstack {{ args }} + +########################################################## +# TEST EXECUTION +########################################################## + +# Run the full test suite +test-all: test-base + @echo "hackstack CLI integration test completed!" + +test-base: + @just hackstack --version | grep -n "hackstack" + @just hackstack --help 2>&1 | grep -n "hackstack" diff --git a/integration/sample.yaml b/integration/sample.yaml new file mode 100644 index 0000000..6dd2a4e --- /dev/null +++ b/integration/sample.yaml @@ -0,0 +1,17 @@ +name: devops-common +description: DevOps CLI - Simplifying your CI/CD pipelines +version: 0.0.2 +repo_url: https://github.com/jgfranco17/devops + +codebase: + language: bash + test: + fail_fast: true + steps: + - echo "Tests completed successfully" + build: + fail_fast: true + env: + FOO: BAR + steps: + - echo "Build completed successfully with $FOO" From 0c001a5a7ad6e2ffc7fce1ae5b46b63091df1930 Mon Sep 17 00:00:00 2001 From: Joaquin Franco Date: Fri, 20 Mar 2026 13:47:39 +0900 Subject: [PATCH 2/5] fix: restructure build --- examples/{cli.yaml => datasource.yaml} | 0 integration/Dockerfile | 6 ++++-- integration/sample.yaml | 17 ----------------- 3 files changed, 4 insertions(+), 19 deletions(-) rename examples/{cli.yaml => datasource.yaml} (100%) delete mode 100644 integration/sample.yaml diff --git a/examples/cli.yaml b/examples/datasource.yaml similarity index 100% rename from examples/cli.yaml rename to examples/datasource.yaml diff --git a/integration/Dockerfile b/integration/Dockerfile index 09a08de..37fea79 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -11,7 +11,7 @@ RUN --mount=type=cache,target=/go/pkg/mod/ \ CGO_ENABLED=0 go build -o /bin/hackstack . # ========== CLI RUN STAGE ========== -FROM ubuntu:22.04 AS app +FROM ubuntu:22.04 AS setup ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ @@ -27,9 +27,11 @@ ARG GROUP_ID=1000 RUN addgroup --gid $GROUP_ID ${USERNAME} && \ adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ${USERNAME} -USER $USERNAME +USER ${USERNAME} ENV HOME=/home/${USERNAME} WORKDIR ${HOME}/projects +FROM setup AS app + ENTRYPOINT [ "/bin/hackstack" ] CMD ["--help"] diff --git a/integration/sample.yaml b/integration/sample.yaml deleted file mode 100644 index 6dd2a4e..0000000 --- a/integration/sample.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: devops-common -description: DevOps CLI - Simplifying your CI/CD pipelines -version: 0.0.2 -repo_url: https://github.com/jgfranco17/devops - -codebase: - language: bash - test: - fail_fast: true - steps: - - echo "Tests completed successfully" - build: - fail_fast: true - env: - FOO: BAR - steps: - - echo "Build completed successfully with $FOO" From 49ba8588b45cd063bbeae283d005933d857d9229 Mon Sep 17 00:00:00 2001 From: Joaquin Franco Date: Fri, 20 Mar 2026 13:56:05 +0900 Subject: [PATCH 3/5] test: run build in container --- integration/Dockerfile | 9 +++++++-- integration/justfile | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/integration/Dockerfile b/integration/Dockerfile index 37fea79..e4ca205 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -10,7 +10,7 @@ COPY . . RUN --mount=type=cache,target=/go/pkg/mod/ \ CGO_ENABLED=0 go build -o /bin/hackstack . -# ========== CLI RUN STAGE ========== +# ========== CLI SETUP STAGE ========== FROM ubuntu:22.04 AS setup ENV DEBIAN_FRONTEND=noninteractive @@ -29,8 +29,13 @@ RUN addgroup --gid $GROUP_ID ${USERNAME} && \ USER ${USERNAME} ENV HOME=/home/${USERNAME} -WORKDIR ${HOME}/projects +ARG PROJECT_DIR="${HOME}/projects" +WORKDIR ${PROJECT_DIR} +COPY examples/ ./examples/ +RUN mkdir -p "${PROJECT_DIR}/backend" "${PROJECT_DIR}/cli" + +# ========== APP RUN STAGE ========== FROM setup AS app ENTRYPOINT [ "/bin/hackstack" ] diff --git a/integration/justfile b/integration/justfile index d85ae21..7e85e5e 100644 --- a/integration/justfile +++ b/integration/justfile @@ -34,9 +34,20 @@ hackstack *args: ########################################################## # Run the full test suite -test-all: test-base +test-all: test-base test-build @echo "hackstack CLI integration test completed!" test-base: - @just hackstack --version | grep -n "hackstack" - @just hackstack --help 2>&1 | grep -n "hackstack" + #!/usr/bin/env bash + just hackstack --version | grep -n "hackstack" + just hackstack --help 2>&1 | grep -n "hackstack" + +test-build: + #!/usr/bin/env bash + just hackstack build --help + just hackstack -vv build backend \ + --output "/home/testuser/projects/backend" \ + --source examples/datasource.yaml + just hackstack -vv build cli \ + --output "/home/testuser/projects/cli" \ + --source examples/datasource.yaml From af428c7c4ed456b62a04acd9780886aa79760598 Mon Sep 17 00:00:00 2001 From: Joaquin Franco Date: Fri, 20 Mar 2026 14:01:01 +0900 Subject: [PATCH 4/5] fix: shell setup --- integration/Dockerfile | 7 +++++-- integration/justfile | 8 +++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/integration/Dockerfile b/integration/Dockerfile index e4ca205..a693233 100644 --- a/integration/Dockerfile +++ b/integration/Dockerfile @@ -11,7 +11,8 @@ RUN --mount=type=cache,target=/go/pkg/mod/ \ CGO_ENABLED=0 go build -o /bin/hackstack . # ========== CLI SETUP STAGE ========== -FROM ubuntu:22.04 AS setup +FROM ubuntu:22.04 AS env-setup +SHELL ["/bin/bash", "-c"] ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ @@ -27,6 +28,7 @@ ARG GROUP_ID=1000 RUN addgroup --gid $GROUP_ID ${USERNAME} && \ adduser --disabled-password --gecos '' --uid ${USER_ID} --gid ${GROUP_ID} ${USERNAME} +FROM env-setup AS workspace-setup USER ${USERNAME} ENV HOME=/home/${USERNAME} ARG PROJECT_DIR="${HOME}/projects" @@ -36,7 +38,8 @@ COPY examples/ ./examples/ RUN mkdir -p "${PROJECT_DIR}/backend" "${PROJECT_DIR}/cli" # ========== APP RUN STAGE ========== -FROM setup AS app +FROM workspace-setup AS app +SHELL ["/usr/bin/env", "bash", "-c"] ENTRYPOINT [ "/bin/hackstack" ] CMD ["--help"] diff --git a/integration/justfile b/integration/justfile index 7e85e5e..d430115 100644 --- a/integration/justfile +++ b/integration/justfile @@ -35,19 +35,21 @@ hackstack *args: # Run the full test suite test-all: test-base test-build - @echo "hackstack CLI integration test completed!" + @echo "Hackstack CLI integration test completed!" +# Base tests to verify the CLI is working and accessible test-base: #!/usr/bin/env bash just hackstack --version | grep -n "hackstack" just hackstack --help 2>&1 | grep -n "hackstack" +# Test the build command with a sample datasource test-build: #!/usr/bin/env bash just hackstack build --help - just hackstack -vv build backend \ + just hackstack -vvv build backend \ --output "/home/testuser/projects/backend" \ --source examples/datasource.yaml - just hackstack -vv build cli \ + just hackstack -vvv build cli \ --output "/home/testuser/projects/cli" \ --source examples/datasource.yaml From 6e050ed582dfb47ab38b9ee25fd2ef2f3e2b2f42 Mon Sep 17 00:00:00 2001 From: Joaquin Franco Date: Fri, 20 Mar 2026 14:10:25 +0900 Subject: [PATCH 5/5] fix: improve package logging --- cli/internal/templating/engine.go | 33 ++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/cli/internal/templating/engine.go b/cli/internal/templating/engine.go index 4558798..b4ca0fd 100644 --- a/cli/internal/templating/engine.go +++ b/cli/internal/templating/engine.go @@ -14,13 +14,23 @@ import ( "github.com/jgfranco17/hackstack/cli/internal/fileutils" "github.com/jgfranco17/hackstack/cli/internal/logging" + "github.com/sirupsen/logrus" ) +const ( + extensionTemplate = ".j2" + extensionRawCopy = ".copy" +) + +// Engine is the entity responsible for rendering embedded template +// files with provided source data. type Engine struct { Files fs.FS Data CLIProject } +// NewEngine creates a new templating engine instance with the provided +// embedded files and source data. func NewEngine(files fs.FS, data CLIProject) *Engine { return &Engine{ Files: files, @@ -28,6 +38,9 @@ func NewEngine(files fs.FS, data CLIProject) *Engine { } } +// Render processes the embedded template files and writes the output to the specified directory. +// It walks through all files in the embedded FS, rendering templates and copying raw files as +// needed. The function returns an error if any step of the rendering process fails. func (e *Engine) Render(ctx context.Context, outputPath string) error { logger := logging.FromContext(ctx).WithField("module", "templating") @@ -36,7 +49,7 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error { walker := func(path string, d fs.DirEntry, err error) error { if err != nil { - return fmt.Errorf("walk error at %q: %w", path, err) + return fmt.Errorf("walk error at %s: %w", path, err) } if d.IsDir() { return nil @@ -46,16 +59,22 @@ func (e *Engine) Render(ctx context.Context, outputPath string) error { var work func() error switch { - case strings.HasSuffix(path, ".j2"): - destPath = strings.TrimSuffix(destPath, ".j2") + case strings.HasSuffix(path, extensionTemplate): + destPath = strings.TrimSuffix(destPath, extensionTemplate) work = func() error { - logger.WithField("file", path).Trace("Rendering from template") + logger.WithFields(logrus.Fields{ + "source": path, + "destination": destPath, + }).Trace("Rendering from template") return renderTemplate(e.Files, path, destPath, e.Data) } - case strings.HasSuffix(path, ".copy"): - destPath = strings.TrimSuffix(destPath, ".copy") + case strings.HasSuffix(path, extensionRawCopy): + destPath = strings.TrimSuffix(destPath, extensionRawCopy) work = func() error { - logger.WithField("file", path).Trace("Copying file") + logger.WithFields(logrus.Fields{ + "source": path, + "destination": destPath, + }).Trace("Copying raw file") return fileutils.CopyFile(e.Files, path, destPath) } default: