diff --git a/.github/workflows/build-push.yaml b/.github/workflows/build-push.yaml new file mode 100644 index 0000000..7a51ac2 --- /dev/null +++ b/.github/workflows/build-push.yaml @@ -0,0 +1,132 @@ +name: Build (amd64 and arm64) and push to quay registries + +on: + push: + branches: ["main"] + tags: ["v*.*.*"] + pull_request: + branches: ["main"] + + workflow_dispatch: + +permissions: + contents: read + +env: + REGISTRY: localhost + NAME: patternizer + TAG: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || (github.ref_name == 'main' && 'latest' || github.ref_name) }} + +jobs: + build-container: + strategy: + matrix: + include: + - targetarch: amd64 + runner: ubuntu-latest + - targetarch: arm64 + runner: ubuntu-24.04-arm + + runs-on: ${{ matrix.runner }} + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Build container and save tarball + env: + CONTAINER: ${{ env.NAME }}:${{ env.TAG }} + TARGETARCH: ${{ matrix.targetarch }} + run: | + make "${TARGETARCH}" + buildah push "${CONTAINER}-${TARGETARCH}" "docker-archive:/tmp/image-${TARGETARCH}.tar:${CONTAINER}-${TARGETARCH}" + + - name: Upload image artifact + uses: actions/upload-artifact@v4 + with: + name: image-${{ matrix.targetarch }}-${{ github.run_id }} + path: /tmp/image-${{ matrix.targetarch }}.tar + retention-days: 1 + + push-multiarch-manifest: + needs: [build-container] + if: github.event_name != 'pull_request' + strategy: + matrix: + include: + - upload_registry: quay.io/validatedpatterns + legacy: false + - upload_registry: quay.io/hybridcloudpatterns + legacy: true + + runs-on: ubuntu-latest + permissions: + contents: read + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Download AMD64 image + uses: actions/download-artifact@v5 + with: + name: image-amd64-${{ github.run_id }} + path: /tmp + + - name: Download ARM64 image + uses: actions/download-artifact@v5 + with: + name: image-arm64-${{ github.run_id }} + path: /tmp + + - name: Load tarballs into local containers-storage + run: | + buildah pull docker-archive:/tmp/image-amd64.tar + buildah pull docker-archive:/tmp/image-arm64.tar + + - name: Log into Quay + env: + USERNAME: ${{ matrix.legacy && secrets.LEGACY_QUAY_USERNAME || secrets.QUAY_USERNAME }} + PASSWORD: ${{ matrix.legacy && secrets.LEGACY_QUAY_PASSWORD || secrets.QUAY_PASSWORD }} + run: | + buildah login -u "${USERNAME}" -p "${PASSWORD}" quay.io + + # The compressed manifest in Quay has a different digest than the local so we + # need to use skopeo to retrieve the correct digest for signing + - name: Create manifest and push to Quay + id: manifest-push + env: + UPLOADREGISTRY: ${{ matrix.upload_registry }} + CONTAINER: ${{ env.NAME }}:${{ env.TAG }} + run: | + make manifest + buildah manifest add --arch=amd64 "${REGISTRY}/${CONTAINER}" "${REGISTRY}/${CONTAINER}-amd64" + buildah manifest add --arch=arm64 "${REGISTRY}/${CONTAINER}" "${REGISTRY}/${CONTAINER}-arm64" + make upload + DIGEST=$(skopeo inspect --format "{{.Digest}}" "docker://${UPLOADREGISTRY}/${CONTAINER}") + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + + - name: Install cosign + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + with: + cosign-release: "v2.2.4" + + # Cosign expects the docker config.json for registry authentication so we must + # copy it from buildah + - name: Sign the published Docker image + env: + CONTAINER: ${{ env.NAME }}:${{ env.TAG }} + DIGEST: ${{ steps.manifest-push.outputs.digest }} + UPLOADREGISTRY: ${{ matrix.upload_registry }} + run: | + cat "${XDG_RUNTIME_DIR}/containers/auth.json" > ~/.docker/config.json + cosign sign --yes "${UPLOADREGISTRY}/${CONTAINER}@${DIGEST}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index cbf5756..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: CI Pipeline - -on: - push: - branches: - - main - tags: - - 'v*' - pull_request: - branches: - - main - -env: - IMAGE_NAME: quay.io/validatedpatterns/patternizer - GO_VERSION: '1.24' - -jobs: - lint-and-format: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache-dependency-path: src/go.sum - - - name: Run linting checks - run: make lint - - build-and-test: - runs-on: ubuntu-latest - needs: lint-and-format - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache-dependency-path: src/go.sum - - - name: Build binary - run: make build - - - name: Run unit tests - run: make test-unit - - - name: Generate test coverage report - run: make test-coverage - - - name: Run integration tests - run: make test-integration - - build-container: - runs-on: ubuntu-latest - needs: build-and-test - if: github.event_name == 'push' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Quay.io - uses: docker/login-action@v3 - with: - registry: quay.io - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_PASSWORD }} - - - name: Determine tags - id: meta - run: | - echo "sha_tag=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - if [[ $GITHUB_REF == refs/tags/* ]]; then - echo "is_tag=true" >> $GITHUB_OUTPUT - echo "git_tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - else - echo "is_tag=false" >> $GITHUB_OUTPUT - fi - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Containerfile - push: true - tags: | - ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.sha_tag }} - ${{ steps.meta.outputs.is_tag == 'true' && format('{0}:{1}', env.IMAGE_NAME, steps.meta.outputs.git_tag) || '' }} - ${{ steps.meta.outputs.is_tag == 'false' && format('{0}:latest', env.IMAGE_NAME) || '' }} diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml new file mode 100644 index 0000000..4762908 --- /dev/null +++ b/.github/workflows/lint-test.yaml @@ -0,0 +1,50 @@ +name: Lint and Test on PRs + +on: + pull_request: + branches: ["main"] + +env: + GO_VERSION: '1.24' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: src/go.sum + + - name: Run linting checks + run: make lint + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: src/go.sum + + - name: Build binary + run: make build + + - name: Run unit tests + run: make test-unit + + - name: Generate test coverage report + run: make test-coverage + + - name: Run integration tests + run: make test-integration diff --git a/Containerfile b/Containerfile index a5b62b3..dfc6916 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,6 @@ ARG GO_VERSION=1.24-alpine ARG ALPINE_VERSION=latest +ARG GOARCH=amd64 # Build stage FROM docker.io/library/golang:${GO_VERSION} AS builder @@ -10,7 +11,7 @@ COPY src/go.mod src/go.sum . RUN go mod download COPY src/ . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o patternizer . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build -a -installsuffix cgo -o patternizer . # Runtime stage FROM docker.io/library/alpine:${ALPINE_VERSION} diff --git a/Makefile b/Makefile index 80c3af3..6eca0e0 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,9 @@ -# Makefile for patternizer - -# Variables -BINARY_NAME := patternizer -GO_VERSION := 1.24 -GOLANGCI_LINT_VERSION := v2.1.6 -IMAGE_NAME := patternizer -LOCAL_TAG := local -SRC_DIR := src -CONTAINER_ENGINE := podman +# Container-related variables +NAME := patternizer +TAG := local +CONTAINER ?= $(NAME):$(TAG) +REGISTRY ?= localhost +UPLOADREGISTRY ?= quay.io/validatedpatterns # Go-related variables GO_CMD := go @@ -16,33 +12,33 @@ GO_TEST := $(GO_CMD) test GO_CLEAN := $(GO_CMD) clean GO_VET := $(GO_CMD) vet GO_FMT := gofmt +GO_VERSION := 1.24 +GOLANGCI_LINT_VERSION := v2.1.6 +SRC_DIR := src # Default target .DEFAULT_GOAL := help -# Help target +##@ Help-related tasks .PHONY: help -help: ## Show this help message - @echo "Available targets:" - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) +help: ## Help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^(\s|[a-zA-Z_0-9-])+:.*?##/ { printf " \033[36m%-35s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) -# Build target +##@ Go-related tasks .PHONY: build build: ## Build the patternizer binary @echo "Building patternizer..." - cd $(SRC_DIR) && $(GO_BUILD) -v -o $(BINARY_NAME) . - @echo "Build complete: $(SRC_DIR)/$(BINARY_NAME)" + cd $(SRC_DIR) && $(GO_BUILD) -v -o $(NAME) . + @echo "Build complete: $(SRC_DIR)/$(NAME)" -# Clean target .PHONY: clean clean: ## Clean build artifacts @echo "Cleaning build artifacts..." cd $(SRC_DIR) && $(GO_CLEAN) - rm -f $(SRC_DIR)/$(BINARY_NAME) + rm -f $(SRC_DIR)/$(NAME) rm -f $(SRC_DIR)/coverage.out @echo "Clean complete" -# Install dependencies .PHONY: deps deps: ## Download and install Go dependencies @echo "Installing dependencies..." @@ -50,41 +46,34 @@ deps: ## Download and install Go dependencies cd $(SRC_DIR) && $(GO_CMD) mod tidy @echo "Dependencies installed" -# Unit tests .PHONY: test-unit test-unit: ## Run unit tests @echo "Running unit tests..." cd $(SRC_DIR) && $(GO_TEST) -v ./... -# Test with coverage .PHONY: test-coverage test-coverage: ## Run unit tests with coverage report @echo "Running unit tests with coverage..." cd $(SRC_DIR) && $(GO_TEST) ./... -coverprofile=coverage.out cd $(SRC_DIR) && $(GO_CMD) tool cover -func=coverage.out -# Shellcheck for integration test script .PHONY: shellcheck shellcheck: ## Run shellcheck on integration test script @echo "Running shellcheck on integration test script..." @podman run --pull always -v "$(PWD):/mnt:z" docker.io/koalaman/shellcheck:stable test/integration_test.sh @echo "Shellcheck passed" -# Integration tests .PHONY: test-integration test-integration: build shellcheck ## Run integration tests @echo "Running integration tests..." - PATTERNIZER_BINARY=./$(SRC_DIR)/$(BINARY_NAME) ./test/integration_test.sh + PATTERNIZER_BINARY=./$(SRC_DIR)/$(NAME) ./test/integration_test.sh -# All tests .PHONY: test test: test-unit test-integration ## Run all tests (unit + integration) -# Lint target .PHONY: lint lint: lint-fmt lint-vet lint-golangci ## Run all linting checks -# Format check .PHONY: lint-fmt lint-fmt: ## Check Go formatting @echo "Checking Go formatting..." @@ -95,14 +84,12 @@ lint-fmt: ## Check Go formatting fi @echo "Go formatting check passed" -# Vet check .PHONY: lint-vet lint-vet: ## Run go vet @echo "Running go vet..." cd $(SRC_DIR) && $(GO_VET) ./... @echo "Go vet passed" -# golangci-lint check .PHONY: lint-golangci lint-golangci: ## Run golangci-lint @echo "Running golangci-lint..." @@ -113,25 +100,15 @@ lint-golangci: ## Run golangci-lint cd $(SRC_DIR) && $$(go env GOPATH)/bin/golangci-lint run @echo "golangci-lint passed" -# Format code .PHONY: fmt fmt: ## Format Go code @echo "Formatting Go code..." cd $(SRC_DIR) && $(GO_FMT) -s -w . @echo "Go code formatted" -# Local container build -.PHONY: local-container-build -local-container-build: ## Build container image locally - @echo "Building container image..." - $(CONTAINER_ENGINE) build -t $(IMAGE_NAME):$(LOCAL_TAG) -f Containerfile . - @echo "Container image built: $(IMAGE_NAME):$(LOCAL_TAG)" - -# Full CI pipeline locally .PHONY: ci ci: lint build test ## Run the full CI pipeline locally -# Development setup .PHONY: dev-setup dev-setup: deps ## Set up development environment @echo "Setting up development environment..." @@ -141,22 +118,52 @@ dev-setup: deps ## Set up development environment fi @echo "Development environment ready" -# Version info .PHONY: version version: ## Show version information @echo "Go version: $$(go version)" @echo "Git commit: $$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" @echo "Build date: $$(date -u +%Y-%m-%dT%H:%M:%SZ)" -# Generate documentation .PHONY: docs docs: ## Generate Go documentation @echo "Generating documentation..." cd $(SRC_DIR) && $(GO_CMD) doc -all ./... -# Quick check (fast feedback loop) .PHONY: check check: lint-fmt lint-vet build test-unit ## Quick check (format, vet, build, unit tests) .PHONY: all all: clean deps lint build test local-container-build ## Run everything + +##@ Conatiner-related tasks +.PHONY: manifest +manifest: ## creates the buildah manifest for multi-arch images + # The rm is needed due to bug https://www.github.com/containers/podman/issues/19757 + buildah manifest rm "${REGISTRY}/${CONTAINER}" || /bin/true + buildah manifest create "${REGISTRY}/${CONTAINER}" + +.PHONY: amd64 +amd64: manifest podman-build-amd64 ## Build the container on amd64 + +.PHONY: arm64 +arm64: manifest podman-build-arm64 ## Build the container on arm64 + +.PHONY: podman-build +podman-build: podman-build-amd64 podman-build-arm64 ## Build both amd64 and arm64 + +.PHONY: podman-build-amd64 +podman-build-amd64: ## build the container in amd64 + @echo "Building the patternizer amd64" + buildah bud --platform linux/amd64 --format docker -f Containerfile -t "${CONTAINER}-amd64" + buildah manifest add --arch=amd64 "${REGISTRY}/${CONTAINER}" "${REGISTRY}/${CONTAINER}-amd64" + +.PHONY: podman-build-arm64 +podman-build-arm64: ## build the container in arm64 + @echo "Building the patternizer arm64" + buildah bud --platform linux/arm64 --build-arg GOARCH="arm64" --format docker -f Containerfile -t "${CONTAINER}-arm64" + buildah manifest add --arch=arm64 "${REGISTRY}/${CONTAINER}" "${REGISTRY}/${CONTAINER}-arm64" + +.PHONY: upload +upload: ## Uploads the container to quay.io/validatedpatterns/${CONTAINER} + @echo "Uploading the ${REGISTRY}/${CONTAINER} container to ${UPLOADREGISTRY}/${CONTAINER}" + buildah manifest push --all "${REGISTRY}/${CONTAINER}" "docker://${UPLOADREGISTRY}/${CONTAINER}" diff --git a/README.md b/README.md index b584af2..ae75ec5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Patternizer +![Version: 0.1.1](https://img.shields.io/badge/Version-0.1.1-informational?style=flat-square) [![Quay Repository](https://img.shields.io/badge/Quay.io-patternizer-blue?logo=quay)](https://quay.io/repository/validatedpatterns/patternizer) -[![CI Pipeline](https://github.com/validatedpatterns/patternizer/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/validatedpatterns/patternizer/actions/workflows/ci.yaml) +[![CI Pipeline](https://github.com/validatedpatterns/patternizer/actions/workflows/lint-test.yaml/badge.svg?branch=main)](https://github.com/validatedpatterns/patternizer/actions/workflows/lint-test.yaml) **Patternizer** is a command-line tool that bootstraps a Git repository containing Helm charts into a ready-to-use Validated Pattern. It automatically generates the necessary scaffolding, configuration files, and utility scripts, so you can get your pattern up and running in minutes. @@ -29,12 +30,12 @@ ## Features - - 🚀 **CLI-first design** with intuitive commands and help system - - 📦 **Container-native** for consistent execution across all environments - - 🔍 **Auto-discovery** of Helm charts and Git repository metadata - - 🔐 **Optional secrets integration** with Vault and External Secrets - - 🏗️ **Makefile-driven** utility scripts for easy pattern management - - ♻️ **Upgrade command** to refresh existing pattern repositories to the latest common structure +- 🚀 **CLI-first design** with intuitive commands and help system +- 📦 **Container-native** for consistent execution across all environments +- 🔍 **Auto-discovery** of Helm charts and Git repository metadata +- 🔐 **Optional secrets integration** with Vault and External Secrets +- 🏗️ **Makefile-driven** utility scripts for easy pattern management +- ♻️ **Upgrade command** to refresh existing pattern repositories to the latest common structure ## Quick Start @@ -42,8 +43,8 @@ This guide assumes you have a Git repository containing one or more Helm charts. **Prerequisites:** - * Podman or Docker - * A Git repository you want to convert into a pattern +- Podman or Docker +- A Git repository you want to convert into a pattern Navigate to your repository's root directory and run the initialization command: @@ -131,9 +132,9 @@ What upgrade does: You can start simple and add secrets management later. - * By default, `patternizer init` disables secret loading. - * To add secrets scaffolding, run `patternizer init --with-secrets` at any time. This will update your configuration to enable secrets. - * **Important:** This action is not easily reversible. We recommend committing your work to Git *before* adding secrets support. +- By default, `patternizer init` disables secret loading. +- To add secrets scaffolding, run `patternizer init --with-secrets` at any time. This will update your configuration to enable secrets. +- **Important:** This action is not easily reversible. We recommend committing your work to Git _before_ adding secrets support. For more details on how secrets work in the framework, see the [Secrets Management Documentation](https://validatedpatterns.io/learn/secrets-management-in-the-validated-patterns-framework/). @@ -141,16 +142,16 @@ For more details on how secrets work in the framework, see the [Secrets Manageme Running `patternizer init` creates the following: - * `values-global.yaml`: Global pattern configuration. - * `values-.yaml`: Cluster group-specific values. - * `pattern.sh`: A utility script for common pattern operations (`install`, `upgrade`, etc.). - * `Makefile`: A simple Makefile that includes `Makefile-common`. - * `Makefile-common`: The core Makefile with all pattern-related targets. +- `values-global.yaml`: Global pattern configuration. +- `values-.yaml`: Cluster group-specific values. +- `pattern.sh`: A utility script for common pattern operations (`install`, `upgrade`, etc.). +- `Makefile`: A simple Makefile that includes `Makefile-common`. +- `Makefile-common`: The core Makefile with all pattern-related targets. Using the `--with-secrets` flag additionally creates: - * `values-secret.yaml.template`: A template for defining your secrets. - * It also updates `values-global.yaml` to set `global.secretLoader.disabled: false` and adds Vault and External Secrets Operator to the cluster group values. +- `values-secret.yaml.template`: A template for defining your secrets. +- It also updates `values-global.yaml` to set `global.secretLoader.disabled: false` and adds Vault and External Secrets Operator to the cluster group values. ## Development & Contributing @@ -158,10 +159,10 @@ This section is for developers who want to contribute to the Patternizer project ### Prerequisites - * Go (see `go.mod` for version) - * Podman or Docker - * Git - * Make +- Go (see `go.mod` for version) +- Podman or Docker +- Git +- Make ### Local Development Workflow @@ -183,31 +184,32 @@ make ci The `Makefile` is the single source of truth for all development and CI tasks. - * `make help`: Show all available targets. - * `make check`: Quick feedback loop (format, vet, build, unit tests). - * `make build`: Build the `patternizer` binary. - * `make test`: Run all tests (unit and integration). - * `make test-unit`: Run unit tests only. - * `make test-integration`: Run integration tests only. - * `make lint`: Run all code quality checks. - * `make local-container-build`: Build the container image locally. +- `make help`: Show all available targets. +- `make check`: Quick feedback loop (format, vet, build, unit tests). +- `make build`: Build the `patternizer` binary. +- `make test`: Run all tests (unit and integration). +- `make test-unit`: Run unit tests only. +- `make test-integration`: Run integration tests only. +- `make lint`: Run all code quality checks. +- `make amd64`: Build the amd64 container image locally. +- `make arm64`: Build the arm64 container image locally. ### Testing Strategy Patternizer has a comprehensive test suite to ensure stability and correctness. - * **Unit Tests:** Located alongside the code they test (e.g., `src/internal/helm/helm_test.go`), these tests cover individual functions and packages in isolation. They validate Helm chart detection, Git URL parsing, and YAML processing logic. - * **Integration Tests:** The integration test suite (`test/integration_test.sh`) validates the end-to-end CLI workflow against a real Git repository. Key scenarios include: - 1. **Basic Init:** Validates default file generation without secrets. - 2. **Init with Secrets:** Ensures secrets-related applications and files are correctly added. - 3. **Configuration Preservation:** Verifies that existing custom values are preserved when the tool is re-run. - 4. **Sequential Execution:** Tests running `init` and then `init --with-secrets` to ensure a clean upgrade. - 5. **Selective File Overwriting:** Confirms that running `init` on a repository with pre-existing custom files correctly **merges YAML configurations**, preserves user-modified files (like `Makefile` and `values-secret.yaml.template`), and only overwrites essential, generated scripts (`pattern.sh`, `Makefile-common`). - 6. **Mixed State Handling:** Validates that the tool correctly initializes a partially-configured repository, **creating files that are missing** while leaving existing ones untouched. - 7. **Upgrade (no replace):** Removes legacy `common/` and `pattern.sh` symlink, copies `Makefile-common`/`pattern.sh`, and injects `include Makefile-common` at the top of `Makefile` when missing. - 8. **Upgrade (include present):** Leaves the existing `Makefile` unchanged when it already contains `include Makefile-common` anywhere. - 9. **Upgrade with `--replace-makefile`:** Replaces `Makefile` with the default and refreshes common assets. - 10. **Upgrade (no Makefile present):** Creates the default `Makefile` and refreshes common assets when a `Makefile` does not exist. +- **Unit Tests:** Located alongside the code they test (e.g., `src/internal/helm/helm_test.go`), these tests cover individual functions and packages in isolation. They validate Helm chart detection, Git URL parsing, and YAML processing logic. +- **Integration Tests:** The integration test suite (`test/integration_test.sh`) validates the end-to-end CLI workflow against a real Git repository. Key scenarios include: + 1. **Basic Init:** Validates default file generation without secrets. + 2. **Init with Secrets:** Ensures secrets-related applications and files are correctly added. + 3. **Configuration Preservation:** Verifies that existing custom values are preserved when the tool is re-run. + 4. **Sequential Execution:** Tests running `init` and then `init --with-secrets` to ensure a clean upgrade. + 5. **Selective File Overwriting:** Confirms that running `init` on a repository with pre-existing custom files correctly **merges YAML configurations**, preserves user-modified files (like `Makefile` and `values-secret.yaml.template`), and only overwrites essential, generated scripts (`pattern.sh`, `Makefile-common`). + 6. **Mixed State Handling:** Validates that the tool correctly initializes a partially-configured repository, **creating files that are missing** while leaving existing ones untouched. + 7. **Upgrade (no replace):** Removes legacy `common/` and `pattern.sh` symlink, copies `Makefile-common`/`pattern.sh`, and injects `include Makefile-common` at the top of `Makefile` when missing. + 8. **Upgrade (include present):** Leaves the existing `Makefile` unchanged when it already contains `include Makefile-common` anywhere. + 9. **Upgrade with `--replace-makefile`:** Replaces `Makefile` with the default and refreshes common assets. + 10. **Upgrade (no Makefile present):** Creates the default `Makefile` and refreshes common assets when a `Makefile` does not exist. ### Architecture