From d1b650f8c71ab12403d68d73cbc795e8458bc581 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Mon, 2 Nov 2026 21:28:38 -0500 Subject: [PATCH 1/7] chore(deps): update platformdirs --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index a73ac38..d3e9de7 100644 --- a/uv.lock +++ b/uv.lock @@ -687,11 +687,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.14.2" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/4e/2684728d9f2c8c4110738dcadca7e0bed4b60c8dc68c40c2a9e0cf017900/platformdirs-4.14.2.tar.gz", hash = "sha256:6d13b4c1d66b7548dcec9e3db13c1ba9d9adb65041b5d07869313afc07e6e41b", size = 70792, upload-time = "2026-10-31T15:10:34.029Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/886dc7c9d21e2d95aceb696f1e31271742033aeef3661f6e9c8921493592/platformdirs-4.14.3.tar.gz", hash = "sha256:021add1ab113483edbcd4694f5fbb2d688b893ea30d35e1155bb371cf4be0be8", size = 71124, upload-time = "2026-11-01T16:27:32.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2d/98af31fdcd7c95c7114e2eb7c2eb0fa55d2a012c37ba4eb3722b7272abcb/platformdirs-4.14.2-py3-none-any.whl", hash = "sha256:dc1f93390f2a9265e44e5afc8667ec7faf737739604e39bc538fec889720c36b", size = 35291, upload-time = "2026-10-31T15:10:32.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/f0aa003909ee77a9e7e55f8659abb635ca23ab0d022a449bbb18da87a1ef/platformdirs-4.14.3-py3-none-any.whl", hash = "sha256:cba2d05fe5e18b26b1fa6f376b8e6234c012abe1f8696c486fface7d0054e931", size = 35276, upload-time = "2026-11-01T16:27:31.498Z" }, ] [[package]] From d9f516984f5fffc388d31c0950a41b521602dec2 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Wed, 4 Nov 2026 19:27:39 -0500 Subject: [PATCH 2/7] refactor(layouts): simplify discovery and replace repository tutorials --- ...stacksmith-gitops-opinionated-reusable.yml | 2 +- docs/guides/advanced-authoring.md | 8 +- docs/guides/execution.md | 12 +- docs/guides/repository-layouts.md | 15 + docs/integrations/ci.md | 8 +- docs/reference/cli-usage.md | 14 +- docs/reference/cli.md | 20 +- docs/reference/python-api.md | 4 +- examples/README.md | 377 +----------------- examples/environment-directories/README.md | 13 + .../common/stacksmith.yaml | 8 + examples/environment-directories/config.yaml | 11 + .../environments/dev/stacksmith.yaml | 4 + .../environments/dev/values.yaml | 2 + .../environments/prod/stacksmith.yaml | 4 + .../environments/prod/values.yaml | 2 + examples/environment-directories/stack.yaml | 8 + examples/environment-files/README.md | 13 + .../environment-files/common/stacksmith.yaml | 8 + examples/environment-files/config.yaml | 11 + .../environment-files/environments/dev.yaml | 4 + .../environment-files/environments/prod.yaml | 4 + examples/environment-files/stack.yaml | 8 + examples/environment-files/vars/dev.yaml | 2 + examples/environment-files/vars/prod.yaml | 2 + examples/github-actions/stacksmith-apply.yml | 2 +- .../github-actions/stacksmith-destroy.yml | 2 +- .../github-actions/stacksmith-operation.yml | 2 +- .../stacksmith-plan-operation.yml | 2 +- examples/github-actions/stacksmith-plan.yml | 2 +- examples/monorepo/README.md | 3 + examples/monorepo/apps/catalog/README.md | 12 + examples/monorepo/apps/catalog/config.yaml | 11 + examples/monorepo/apps/catalog/stack.yaml | 8 + .../monorepo/apps/catalog/stacksmith.yaml | 12 + examples/monorepo/apps/catalog/values.yaml | 2 + examples/monorepo/apps/payments/README.md | 12 + examples/monorepo/apps/payments/config.yaml | 11 + examples/monorepo/apps/payments/stack.yaml | 8 + .../monorepo/apps/payments/stacksmith.yaml | 12 + examples/monorepo/apps/payments/values.yaml | 2 + examples/operations/README.md | 15 + examples/operations/config.yaml | 50 +++ examples/operations/stack.yaml | 17 + examples/operations/stacksmith.yaml | 12 + examples/operations/values.yaml | 6 + examples/quickstart/README.md | 12 + examples/quickstart/config.yaml | 11 + examples/quickstart/stack.yaml | 8 + examples/quickstart/stacksmith.yaml | 12 + examples/quickstart/values.yaml | 2 + examples/scripts/run-act-workflow.sh | 240 ----------- src/stacksmith/ci/selection.py | 71 +--- src/stacksmith/cli/args.py | 7 +- src/stacksmith/enums.py | 1 - .../authoring-examples}/gitops-repo/README.md | 0 .../gitops-repo/common/stacksmith.yaml | 0 .../gitops-repo/environments/dev.yaml | 0 .../gitops-repo/environments/prod.yaml | 0 .../manifests/common/frontend-values.yaml | 0 .../manifests/common/platform.stack.yaml | 2 +- .../manifests/common/service.stack.yaml | 4 +- .../environments/dev/app-config.yaml | 0 .../environments/dev/frontend-values.yaml | 0 .../environments/prod/app-config.yaml | 0 .../environments/prod/frontend-values.yaml | 0 .../gitops-repo/vars/vars.dev.yaml | 0 .../gitops-repo/vars/vars.prod.yaml | 0 .../gitops-simple-repo/README.md | 0 .../gitops-simple-repo/common/stacksmith.yaml | 0 .../gitops-simple-repo/environments/dev.yaml | 0 .../gitops-simple-repo/environments/prod.yaml | 0 .../common/terraform-data.stack.yaml | 0 .../gitops-simple-repo/vars/vars.common.yaml | 0 .../gitops-simple-repo/vars/vars.dev.yaml | 0 .../gitops-simple-repo/vars/vars.prod.yaml | 0 .../authoring-examples/modules/README.md | 56 +++ .../modules/helm_app/README.md | 41 ++ .../modules/helm_app/main.tf | 17 + .../modules/helm_app/outputs.tf | 4 + .../modules/helm_app/variables.tf | 49 +++ .../modules/helm_app/versions.tf | 8 + .../modules/kubernetes_app/README.md | 36 ++ .../modules/kubernetes_app/main.tf | 33 ++ .../modules/kubernetes_app/variables.tf | 21 + .../modules/kubernetes_app/versions.tf | 8 + .../modules/terraform_data/README.md | 49 +++ .../modules/terraform_data/main.tf | 3 + .../modules/terraform_data/outputs.tf | 4 + .../modules/terraform_data/variables.tf | 4 + .../modules/terraform_data/versions.tf | 3 + .../shared-config-repo/README.md | 0 .../scripts/providers/aws_identity.py | 0 .../configure_aws_secondary_provider.py | 0 .../transforms/transform_bucket_name.py | 0 .../transforms/transform_lifecycle_rules.py | 0 .../transforms/transform_s3_write_policy.py | 0 .../transform_security_group_ingress_rules.py | 0 .../scripts/transforms/transform_tags.py | 0 .../validations/validate_aws_region.py | 0 .../validations/validate_ec2_instance_type.py | 0 .../validate_ec2_requires_imdsv2.py | 0 .../validate_ec2_t3_micro_warning.py | 0 .../validate_s3_no_public_access.py | 0 .../validate_s3_write_policy_scope.py | 0 .../stacksmith-base-config.yaml | 0 .../shared-config-repo/stacksmith-config.yaml | 0 .../shared-config-repo/tests.yaml | 0 .../authoring-examples}/stack-repo/stack.yaml | 0 .../stack-repo/vars.dev.yaml | 0 .../stack-simple-repo/stack.yaml | 0 .../stack-simple-repo/vars.dev.yaml | 0 .../stack-simple-repo/vars.prod.yaml | 0 tests/test_ci_api.py | 24 +- tests/test_cli.py | 28 +- tests/test_example_scripts.py | 17 +- tests/test_example_validations.py | 4 +- tests/test_execution_plan_integration.py | 2 +- tests/test_formatters.py | 19 +- tests/test_generator.py | 32 +- tests/test_gitops.py | 74 ++-- tests/test_introspection.py | 11 +- tests/test_merging.py | 13 +- tests/test_release_pins.py | 8 +- tests/test_schema_sync.py | 28 +- zensical.toml | 1 + 126 files changed, 917 insertions(+), 817 deletions(-) create mode 100644 docs/guides/repository-layouts.md create mode 100644 examples/environment-directories/README.md create mode 100644 examples/environment-directories/common/stacksmith.yaml create mode 100644 examples/environment-directories/config.yaml create mode 100644 examples/environment-directories/environments/dev/stacksmith.yaml create mode 100644 examples/environment-directories/environments/dev/values.yaml create mode 100644 examples/environment-directories/environments/prod/stacksmith.yaml create mode 100644 examples/environment-directories/environments/prod/values.yaml create mode 100644 examples/environment-directories/stack.yaml create mode 100644 examples/environment-files/README.md create mode 100644 examples/environment-files/common/stacksmith.yaml create mode 100644 examples/environment-files/config.yaml create mode 100644 examples/environment-files/environments/dev.yaml create mode 100644 examples/environment-files/environments/prod.yaml create mode 100644 examples/environment-files/stack.yaml create mode 100644 examples/environment-files/vars/dev.yaml create mode 100644 examples/environment-files/vars/prod.yaml create mode 100644 examples/monorepo/README.md create mode 100644 examples/monorepo/apps/catalog/README.md create mode 100644 examples/monorepo/apps/catalog/config.yaml create mode 100644 examples/monorepo/apps/catalog/stack.yaml create mode 100644 examples/monorepo/apps/catalog/stacksmith.yaml create mode 100644 examples/monorepo/apps/catalog/values.yaml create mode 100644 examples/monorepo/apps/payments/README.md create mode 100644 examples/monorepo/apps/payments/config.yaml create mode 100644 examples/monorepo/apps/payments/stack.yaml create mode 100644 examples/monorepo/apps/payments/stacksmith.yaml create mode 100644 examples/monorepo/apps/payments/values.yaml create mode 100644 examples/operations/README.md create mode 100644 examples/operations/config.yaml create mode 100644 examples/operations/stack.yaml create mode 100644 examples/operations/stacksmith.yaml create mode 100644 examples/operations/values.yaml create mode 100644 examples/quickstart/README.md create mode 100644 examples/quickstart/config.yaml create mode 100644 examples/quickstart/stack.yaml create mode 100644 examples/quickstart/stacksmith.yaml create mode 100644 examples/quickstart/values.yaml delete mode 100755 examples/scripts/run-act-workflow.sh rename {examples => tests/fixtures/authoring-examples}/gitops-repo/README.md (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/common/stacksmith.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/environments/dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/environments/prod.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/common/frontend-values.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/common/platform.stack.yaml (95%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/common/service.stack.yaml (85%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/environments/dev/app-config.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/environments/dev/frontend-values.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/environments/prod/app-config.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/manifests/environments/prod/frontend-values.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/vars/vars.dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-repo/vars/vars.prod.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/README.md (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/common/stacksmith.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/environments/dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/environments/prod.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/manifests/common/terraform-data.stack.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/vars/vars.common.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/vars/vars.dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/gitops-simple-repo/vars/vars.prod.yaml (100%) create mode 100644 tests/fixtures/authoring-examples/modules/README.md create mode 100644 tests/fixtures/authoring-examples/modules/helm_app/README.md create mode 100644 tests/fixtures/authoring-examples/modules/helm_app/main.tf create mode 100644 tests/fixtures/authoring-examples/modules/helm_app/outputs.tf create mode 100644 tests/fixtures/authoring-examples/modules/helm_app/variables.tf create mode 100644 tests/fixtures/authoring-examples/modules/helm_app/versions.tf create mode 100644 tests/fixtures/authoring-examples/modules/kubernetes_app/README.md create mode 100644 tests/fixtures/authoring-examples/modules/kubernetes_app/main.tf create mode 100644 tests/fixtures/authoring-examples/modules/kubernetes_app/variables.tf create mode 100644 tests/fixtures/authoring-examples/modules/kubernetes_app/versions.tf create mode 100644 tests/fixtures/authoring-examples/modules/terraform_data/README.md create mode 100644 tests/fixtures/authoring-examples/modules/terraform_data/main.tf create mode 100644 tests/fixtures/authoring-examples/modules/terraform_data/outputs.tf create mode 100644 tests/fixtures/authoring-examples/modules/terraform_data/variables.tf create mode 100644 tests/fixtures/authoring-examples/modules/terraform_data/versions.tf rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/README.md (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/providers/aws_identity.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/providers/configure_aws_secondary_provider.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/transforms/transform_bucket_name.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/transforms/transform_lifecycle_rules.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/transforms/transform_s3_write_policy.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/transforms/transform_security_group_ingress_rules.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/transforms/transform_tags.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_aws_region.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_ec2_instance_type.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_ec2_requires_imdsv2.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_ec2_t3_micro_warning.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_s3_no_public_access.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/scripts/validations/validate_s3_write_policy_scope.py (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/stacksmith-base-config.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/stacksmith-config.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/shared-config-repo/tests.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/stack-repo/stack.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/stack-repo/vars.dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/stack-simple-repo/stack.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/stack-simple-repo/vars.dev.yaml (100%) rename {examples => tests/fixtures/authoring-examples}/stack-simple-repo/vars.prod.yaml (100%) diff --git a/.github/workflows/stacksmith-gitops-opinionated-reusable.yml b/.github/workflows/stacksmith-gitops-opinionated-reusable.yml index 34e5003..ef70674 100644 --- a/.github/workflows/stacksmith-gitops-opinionated-reusable.yml +++ b/.github/workflows/stacksmith-gitops-opinionated-reusable.yml @@ -23,7 +23,7 @@ on: type: string default: "" discovery_mode: - description: "Discovery mode. Supported values: auto (default), folders, flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string default: auto diff --git a/docs/guides/advanced-authoring.md b/docs/guides/advanced-authoring.md index eeeb7a1..633bb70 100644 --- a/docs/guides/advanced-authoring.md +++ b/docs/guides/advanced-authoring.md @@ -410,8 +410,8 @@ When `--config` points at one or more managed config layers, Stacksmith discover ```shell stacksmith test \ - --config examples/shared-config-repo/stacksmith-base-config.yaml \ - --config examples/shared-config-repo/stacksmith-config.yaml + --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml \ + --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml ``` ```shell @@ -426,8 +426,8 @@ Use `--dump-tests` when you want to inspect the generated pytest code. ```shell stacksmith test \ - --config examples/shared-config-repo/stacksmith-base-config.yaml \ - --config examples/shared-config-repo/stacksmith-config.yaml \ + --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml \ + --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml \ --dump-tests /tmp/stacksmith-generated-tests.py ``` diff --git a/docs/guides/execution.md b/docs/guides/execution.md index aaf5002..a402954 100644 --- a/docs/guides/execution.md +++ b/docs/guides/execution.md @@ -220,9 +220,9 @@ Use `info graph` to inspect the discovered dependency graph and the execution th ```bash stacksmith info graph \ - --root examples/gitops-simple-repo \ - --runfile examples/gitops-simple-repo/common/stacksmith.yaml \ - --runfile examples/gitops-simple-repo/environments/dev.yaml + --root tests/fixtures/authoring-examples/gitops-simple-repo \ + --runfile tests/fixtures/authoring-examples/gitops-simple-repo/common/stacksmith.yaml \ + --runfile tests/fixtures/authoring-examples/gitops-simple-repo/environments/dev.yaml ``` The default table view includes stack paths, dependency edges, state keys, selected components, mock-output usage, build directories, logical Terragrunt commands, and the computed order. Use `--action destroy` to preview the reversed destruction order. Stack filters (`--include-tag` and `--exclude-tag`) and component selectors (`--tag` and `--tag-expr`) use the same semantics as `run-all`. @@ -233,9 +233,9 @@ Use `run-all --dry-run` to perform the same discovery, input resolution, static ```bash stacksmith run-all plan \ - --root examples/gitops-simple-repo \ - --runfile examples/gitops-simple-repo/common/stacksmith.yaml \ - --runfile examples/gitops-simple-repo/environments/dev.yaml \ + --root tests/fixtures/authoring-examples/gitops-simple-repo \ + --runfile tests/fixtures/authoring-examples/gitops-simple-repo/common/stacksmith.yaml \ + --runfile tests/fixtures/authoring-examples/gitops-simple-repo/environments/dev.yaml \ --tag-expr "component_name == 'first'" \ --dry-run \ --format json diff --git a/docs/guides/repository-layouts.md b/docs/guides/repository-layouts.md new file mode 100644 index 0000000..504140f --- /dev/null +++ b/docs/guides/repository-layouts.md @@ -0,0 +1,15 @@ +# Repository layouts + +Repository ownership and deployment layout are separate choices. Application teams can own stacks while platform teams own managed configuration and modules in another repository. + +| Layout | Identifier | Choose it when | +| --- | --- | --- | +| Single deployment | `single` | One deployment lives at the root with `stacksmith.yaml` or `stack.yaml`. | +| Environment files | `env-files` | Shared inputs live in `common/stacksmith.yaml` and small overrides in `environments/dev.yaml`. | +| Environment directories | `folders` | Each environment needs its own runfile and assets under `environments/dev/`. | + +Use `auto` to detect the layout. Mixing environment files and environment runfile directories requires an explicit mode or reorganizing the root. Shared runfiles do not count as a competing single deployment. Single deployments use `default` as their target identity. + +Start with the local examples in `examples/quickstart`, then compare `examples/environment-files` and `examples/environment-directories`. They use the same workload. Shared changes affect all environments; environment-specific changes select their target. Unknown changed paths conservatively select every environment. + +A monorepo can contain several ordinary deployment roots; this does not require another layout. Platform currently configures one root per repository. Local examples use local state; configure durable remote state before CI execution. diff --git a/docs/integrations/ci.md b/docs/integrations/ci.md index 2fd2b61..f8abe2b 100644 --- a/docs/integrations/ci.md +++ b/docs/integrations/ci.md @@ -25,7 +25,7 @@ The deployment CI selector is named `command` in GitHub Actions and `COMMAND` in - GitHub Actions `workflow_dispatch` can run all environments, or a comma-delimited subset with `environments`. - `plan-operation` previews native operations without approval or execution; `apply-operation` previews, requests approval, and executes them. Supply a comma-delimited `operation_names` input such as `publish_image,deploy_app,smoke_test`, or leave it empty to select all operations in each stack. Set the repository, organization, job, or folder variable `STACKSMITH_MAX_PARALLEL_OPERATIONS` to cap concurrency and `STACKSMITH_FORCE_RERUN=1` to force replacement of selected operation runner resources when their execution identities have not changed. -- `discovery_mode` selects how environments are discovered. Use `folders` for `environments//` directories, `flat-files` for root-level `stacksmith..yaml|yml|json` files, or `env-files` for the hybrid `environments/.yaml` layout. The aliases `env` and `env-files` both map to the hybrid env-file discovery path. +- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). - In GitHub Actions, `STACKSMITH_GITOPS_ROOT` defaults to `.` and can be overridden per run with `gitops_root`. - Changes under `/common` and `/manifests/common` fan out to all environments. - Changes under `/environments/` and `/manifests/environments/` target only that environment. @@ -76,7 +76,7 @@ In your own repository, you can do either of the following. The wrappers pass reusable workflow inputs from repository variables when available. - `STACKSMITH_GITOPS_ROOT` (default `.`) -- `STACKSMITH_DISCOVERY_MODE` (default `auto`; set to `flat-files` for root-level env files, or `env-files` for the hybrid `environments/.yaml` layout) +- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). - `STACKSMITH_WORKDIR` (default `.`) - `STACKSMITH_ENV_FILE` (default `/dev/null`) - `STACKSMITH_IMAGE_VERSION` (default `latest`) @@ -205,7 +205,7 @@ jobs: > ℹ️ **Tip:** Pin the `uses:` reference to a release tag for stable downstream usage. -The reusable workflow also supports the `folders` and `flat-files` discovery modes for repositories that prefer those layouts. +- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). ## Jenkins @@ -254,7 +254,7 @@ Configure these values as Jenkins folder properties or job environment variables - `STACKSMITH_IMAGE_VERSION`: Image tag when `STACKSMITH_IMAGE` is unset. Defaults to `latest`. - `STACKSMITH_TEST_PIPELINE`: Set to a truthy value to make the job a test-only pipeline. The `COMMAND` parameter is not exposed in this mode and cannot override the test command. - `STACKSMITH_GITOPS_ROOT`: GitOps root for discovery. Defaults to `WORKDIR` in Jenkins. -- `STACKSMITH_DISCOVERY_MODE`: `auto`, `folders`, `flat-files`, or `env-files`. Defaults to `auto`. +- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). - `STACKSMITH_MAX_PARALLEL_OPERATIONS`: Maximum independent operations planned or run concurrently within each environment. Defaults to `10`. - `STACKSMITH_ENV_FILE`: Env file passed to Stacksmith. Defaults to `/dev/null` to prevent implicit `.env` loading. - `STACKSMITH_CONFIG_REF`: Required platform-managed Stacksmith config reference. diff --git a/docs/reference/cli-usage.md b/docs/reference/cli-usage.md index 59b0601..edf6d8a 100644 --- a/docs/reference/cli-usage.md +++ b/docs/reference/cli-usage.md @@ -82,7 +82,7 @@ Use `info associations` to see how organization-managed association rules resolv `info associations --format json` writes the same details as machine-readable output to stdout. ```shell -stacksmith info associations examples/stack-repo/stack.yaml --config examples/shared-config-repo/stacksmith-base-config.yaml --config examples/shared-config-repo/stacksmith-config.yaml --vars examples/stack-repo/vars.dev.yaml +stacksmith info associations tests/fixtures/authoring-examples/stack-repo/stack.yaml --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml --vars tests/fixtures/authoring-examples/stack-repo/vars.dev.yaml ``` Use `info modules-and-policies` to review configured modules, mappings, metadata, and plan validations. @@ -92,7 +92,7 @@ Use `info modules-and-policies` to review configured modules, mappings, metadata `info modules-and-policies --format table` writes human-readable output to stderr. ```shell -stacksmith info modules-and-policies --config examples/shared-config-repo/stacksmith-base-config.yaml --config examples/shared-config-repo/stacksmith-config.yaml +stacksmith info modules-and-policies --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml ``` Use `info diagnose` to inspect cache and module-resolution diagnostics for a stack. @@ -100,7 +100,7 @@ Use `info diagnose` to inspect cache and module-resolution diagnostics for a sta `info diagnose` writes diagnostics to stderr. ```shell -stacksmith info diagnose examples/stack-simple-repo/stack.yaml --config examples/shared-config-repo/stacksmith-base-config.yaml --config examples/shared-config-repo/stacksmith-config.yaml +stacksmith info diagnose tests/fixtures/authoring-examples/stack-simple-repo/stack.yaml --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml --config tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml ``` ## CI commands @@ -117,10 +117,10 @@ Run it without `--gitops-root` from a repository checkout to find every plausibl stacksmith ci environments stacksmith ci environments \ - --gitops-root examples/gitops-repo \ + --gitops-root tests/fixtures/authoring-examples/gitops-repo \ --discovery-mode env-files \ --event-name push \ - --changed-path examples/gitops-repo/environments/dev.yaml + --changed-path tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml ``` Use `ci validate` to run CI-oriented preflight checks with a stable check-result contract. @@ -129,9 +129,9 @@ The first release focuses on static checks such as discovery mode validity, runf ```shell stacksmith ci validate \ - --gitops-root examples/gitops-repo \ + --gitops-root tests/fixtures/authoring-examples/gitops-repo \ --discovery-mode env-files \ - --workflow-runfile examples/gitops-repo/common/stacksmith.yaml \ + --workflow-runfile tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml \ --workflow-env-file /dev/null \ --workflow-validation-report-format json ``` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 097cca7..2a3512d 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -687,7 +687,7 @@ stacksmith info graph [-h] [--action {plan,apply,destroy}] [--root ROOT] [--stac ```text stacksmith ci environments [-h] [--gitops-root GITOPS_ROOT] - [--discovery-mode {folders,flat-files,env-files,single,env,auto}] + [--discovery-mode {folders,env-files,single,auto}] [--environments ENVIRONMENTS] [--event-name EVENT_NAME] [--changed-path CHANGED_PATH] [--base-ref BASE_REF] [--before BEFORE] [--after AFTER] [--format {table,json}] @@ -696,7 +696,7 @@ stacksmith ci environments [-h] [--gitops-root GITOPS_ROOT] | Argument | Description | | - | - | | `--gitops-root` | Relative path to one GitOps root folder. When omitted from `ci environments`, Stacksmith searches the repository. | -| `--discovery-mode` | Environment discovery mode. Use single, folders, flat-files, or env-files (env is an alias for env-files). Choices: `folders`, `flat-files`, `env-files`, `single`, `env`, `auto`. | +| `--discovery-mode` | Environment discovery mode. Use single, folders, or env-files . Choices: `folders`, `env-files`, `single`, `auto`. | | `--environments` | Optional comma-separated environment names to target manually. | | `--event-name` | Optional caller event name used for event-aware selection. | | `--changed-path` | Changed repository path used for selection simulation. Repeatable. | @@ -709,9 +709,8 @@ stacksmith ci environments [-h] [--gitops-root GITOPS_ROOT] ```text stacksmith ci validate [-h] [--gitops-root GITOPS_ROOT] - [--discovery-mode {folders,flat-files,env-files,single,env,auto}] - [--environments ENVIRONMENTS] [--workflow-runfile WORKFLOW_RUNFILE] - [--workflow-env-file WORKFLOW_ENV_FILE] + [--discovery-mode {folders,env-files,single,auto}] [--environments ENVIRONMENTS] + [--workflow-runfile WORKFLOW_RUNFILE] [--workflow-env-file WORKFLOW_ENV_FILE] [--workflow-validation-report-format WORKFLOW_VALIDATION_REPORT_FORMAT] [--format {table,json}] ``` @@ -719,7 +718,7 @@ stacksmith ci validate [-h] [--gitops-root GITOPS_ROOT] | Argument | Description | | - | - | | `--gitops-root` | Relative path to the GitOps root folder. | -| `--discovery-mode` | Environment discovery mode. Use single, folders, flat-files, or env-files (env is an alias for env-files). Choices: `folders`, `flat-files`, `env-files`, `single`, `env`, `auto`. | +| `--discovery-mode` | Environment discovery mode. Use single, folders, or env-files . Choices: `folders`, `env-files`, `single`, `auto`. | | `--environments` | Optional comma-separated environment names to target manually. | | `--workflow-runfile` | Optional runfile path to validate for CI invocations. | | `--workflow-env-file` | Env file path to validate for CI invocations. Use /dev/null to represent deterministic no-env-file mode. | @@ -730,10 +729,9 @@ stacksmith ci validate [-h] [--gitops-root GITOPS_ROOT] ```text stacksmith ci prepare [-h] [--gitops-root GITOPS_ROOT] - [--discovery-mode {folders,flat-files,env-files,single,env,auto}] - [--environments ENVIRONMENTS] [--event-name EVENT_NAME] - [--changed-path CHANGED_PATH] [--base-ref BASE_REF] [--before BEFORE] - [--after AFTER] + [--discovery-mode {folders,env-files,single,auto}] [--environments ENVIRONMENTS] + [--event-name EVENT_NAME] [--changed-path CHANGED_PATH] [--base-ref BASE_REF] + [--before BEFORE] [--after AFTER] --command {test,plan,apply,destroy,plan-operation,apply-operation} [--operation-names OPERATION_NAMES] [--tags TAGS] --config-ref CONFIG_REF [--workdir WORKDIR] [--env-file ENV_FILE] @@ -748,7 +746,7 @@ stacksmith ci prepare [-h] [--gitops-root GITOPS_ROOT] | Argument | Description | | - | - | | `--gitops-root` | Relative path to the GitOps root folder. | -| `--discovery-mode` | Environment discovery mode. Use single, folders, flat-files, or env-files (env is an alias for env-files). Choices: `folders`, `flat-files`, `env-files`, `single`, `env`, `auto`. | +| `--discovery-mode` | Environment discovery mode. Use single, folders, or env-files . Choices: `folders`, `env-files`, `single`, `auto`. | | `--environments` | Optional comma-separated environment names to target manually. | | `--event-name` | Optional caller event name used for event-aware selection. | | `--changed-path` | Changed repository path used for selection simulation. Repeatable. | diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index e090b05..f6657db 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -87,11 +87,11 @@ merge_mode: deep configs: - source: local data: - path: examples/gitops-repo/common/stacksmith.yaml + path: tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml vars: - source: local data: - path: examples/gitops-repo/vars/vars.dev.yaml + path: tests/fixtures/authoring-examples/gitops-repo/vars/vars.dev.yaml ``` For production use, add GitHub Environment protections and secrets per environment. The reusable workflow completes the unprotected plan phase first, then maps each apply phase to the matching GitHub Environment so approvals and scoped credentials gate deployment after the plan is available. diff --git a/examples/README.md b/examples/README.md index 7332215..95422b3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,372 +1,17 @@ # Examples -This folder contains a realistic multi-repo-style example for Stacksmith. +Start with [Quickstart](quickstart/README.md), then compare the same workload in [Environment files](environment-files/README.md) and [Environment directories](environment-directories/README.md). -The example provisions a small EC2 writer workload and S3 buckets, then wires IAM and bucket policy so the EC2 role can write objects securely. +| Tutorial | Purpose | +| --- | --- | +| Quickstart | One deployment, local state, no cloud credentials. | +| Environment files | Shared inputs with compact dev/prod overrides. | +| Environment directories | Environment-owned runfiles and assets. | +| Operations | Deferred secrets and automatic operation reconciliation. | +| Multi-application monorepo | Independent application deployment roots. | -The `gitops-repo` example is the canonical GitOps sample. It uses the hybrid `env-files` discovery mode with `environments/.yaml` files and a shared `common/stacksmith.yaml` runfile. +These tutorials use the built-in `terraform_data` resource. Local-state examples are not ready for CI; configure durable remote state first. GitHub Actions wrapper templates live in `github-actions`. -The `gitops-simple-repo` example uses the same discovery layout to plan two cloud-credential-free Terraform `terraform_data` instances across `dev` and `prod`. It demonstrates that operations can use their own credentials even when the infrastructure layer requires no cloud authentication. +The supported discovery identifiers are `single`, `env-files`, and `folders`, with `auto` for detection. Platform currently configures one deployment root per repository. -The same workflow also supports the other two discovery styles. - -- `folders` for `environments//` directories -- `flat-files` for root-level `stacksmith..yaml` files - -All GitOps examples use the shared module implementations under `examples/modules` via module mappings in `examples/shared-config-repo/stacksmith-config.yaml`. - -## GitOps discovery styles - -```text -folders: - environments/ - dev/ - stacksmith.yaml - prod/ - stacksmith.yaml - -flat-files: - stacksmith.dev.yaml - stacksmith.prod.yaml - -env-files: - common/ - stacksmith.yaml - environments/ - dev.yaml - prod.yaml -``` - -## Example GitHub Actions wrappers - -This example includes GitHub Actions wrapper workflow templates under `examples/github-actions`. The wrappers call reusable workflows from this repository using `uses`. - -- [`github-actions/stacksmith-plan.yml`](github-actions/stacksmith-plan.yml) triggers on pull requests to `main` and manual dispatch. -- [`github-actions/stacksmith-apply.yml`](github-actions/stacksmith-apply.yml) observes every push, reconciles pushes to the repository's default branch, and supports manual dispatch. -- [`github-actions/stacksmith-destroy.yml`](github-actions/stacksmith-destroy.yml) is manual-only, requires explicit environment names, destroys isolated operation state, and then destroys infrastructure. -- [`github-actions/stacksmith-operation.yml`](github-actions/stacksmith-operation.yml) manually runs a stack-local native operation in the selected environments. - -All templates call `ci-sourcerer/stacksmith/.github/workflows/stacksmith-gitops-opinionated-reusable.yml@main`. You can call that workflow directly and keep the trigger policy in your repository. These example files do not run here because they are intentionally stored outside `.github/workflows`. - -The apply wrapper intentionally has no `paths` filter. This prevents a new manifest layout, variable source, lockfile, or other declarative input from silently failing to start default-branch reconciliation. Stacksmith still uses changed-path discovery after the workflow starts to target affected environments. If a push contains any path that cannot be mapped safely, Stacksmith reconciles every environment. - -When adapting the plan template, update its defaults and choose path filters that match your discovery layout. - -- For `folders`, include `**/stacksmith.yaml` and `**/manifests/environments/**`. -- For `flat-files`, include `**/stacksmith.*.yaml`, `**/stacksmith.*.yml`, and `**/stacksmith.*.json`. -- For `env-files`, include `**/environments/*.yaml`, `**/environments/*.yml`, and `**/environments/*.json`. -- For shared changes that should fan out to all environments, include `**/common/**` and `**/manifests/common/**`. -- Include stack file edits when your env/runfiles reference stack manifests directly, for example `**/*.stack.yaml` and `**/*.stack.yml`. - -## Jenkins shared library - -A reusable Jenkins pipeline is available as the `stacksmith()` entrypoint in `jenkins/vars/stacksmith.groovy`. Configure this repository as the trusted global Jenkins library `stacksmith`, using `jenkins` as its library path. It exposes the same GitOps selection behavior as the GitHub Actions wrappers. - -For consuming repositories, use the Jenkins folder properties or job environment described in the main README to select an agent and pass Stacksmith settings. - -Each consumer's protected `Jenkinsfile` imports `stacksmith` and calls `stacksmith()`. The function mirrors the GitHub Actions workflow's discovery, execution flags, approval, and artifact behavior using Jenkins-native features. Set `STACKSMITH_TEST_PIPELINE` to a truthy value on a job or folder to make it a test-only pipeline that runs managed-configuration tests through `stacksmith test`. Without that setting, plan requests run a `Plan` stage, apply requests apply infrastructure before reconciling operations, and destroy requests remove operation state before infrastructure after approval. - -## Example stack - -The stack file is [`stack-repo/stack.yaml`](stack-repo/stack.yaml). - -The vars file is [`stack-repo/vars.dev.yaml`](stack-repo/vars.dev.yaml). - -The stack has the following tags. - -- `compute` -- `storage` -- `example` -- `prod` - -The `app` component is tagged with `web`, so you can combine stack-level and component-level targeting with expressions like `contains(stack_tags, 'prod') && tag.web`. - -The shared managed config also defines `web-instance-production-bucket-writer`. It selects the `web` EC2 component as a producer and each `prod` S3 bucket as a consumer, then supplies the EC2 role's public `role_arn` output to the bucket's `writer_principal_arn` property. The stack therefore receives its scoped bucket policy without declaring that component reference itself. The untagged logs bucket is not associated. - -## Shared config repo - -The effective managed config combines [`shared-config-repo/stacksmith-base-config.yaml`](shared-config-repo/stacksmith-base-config.yaml) and [`shared-config-repo/stacksmith-config.yaml`](shared-config-repo/stacksmith-config.yaml), in that order. - -The config demonstrates `default_module_mapping` with the convention `https://github.com/my-org/{{ component.type | replace("-", "_") }}`. Its explicit mappings keep every bundled component runnable and take precedence over this placeholder fallback. - -The transform script directory is [`shared-config-repo/scripts/transforms`](shared-config-repo/scripts/transforms). - -The validation script directory is [`shared-config-repo/scripts/validations`](shared-config-repo/scripts/validations). - -The provider script directory is [`shared-config-repo/scripts/providers`](shared-config-repo/scripts/providers). - -### Environment variables - -The example scripts support these optional environment variables. - -- `STACKSMITH_SSE` -- `STACKSMITH_FETCH_AWS_ACCOUNT_ID` -- `STACKSMITH_EXAMPLE_AWS_ACCOUNT` -- `AWS_PROFILE` -- `AWS_DEFAULT_PROFILE` - -Useful CLI environment variables for this example. - -- `STACKSMITH_CONFIG` -- `STACKSMITH_ROOT` -- `STACKSMITH_DEBUG` -- `STACKSMITH_REQUIRE_LOCKFILE` -- `STACKSMITH_OFFLINE` -- `STACKSMITH_LOCKFILE` -- `STACKSMITH_ONLY_USE_LOCAL_MODULES` - -Implementation details are in [`shared-config-repo/scripts/transforms/transform_s3_write_policy.py`](shared-config-repo/scripts/transforms/transform_s3_write_policy.py). - -## Prerequisites - -Before running `plan`, `apply`, or `destroy`, make sure the following are set up. - -- AWS credentials are available for your shell session. -- The secondary provider only skips `assume_role` when STS caller identity resolves to a root ARN. If identity lookup fails or is inconclusive, the example keeps `assume_role` enabled. -- Set `STACKSMITH_VAR_AWS_PROFILE`, `AWS_PROFILE`, or `AWS_DEFAULT_PROFILE` if you want deterministic profile selection for the root identity check. -- The secondary provider config lives in [`shared-config-repo/scripts/providers/configure_aws_secondary_provider.py`](shared-config-repo/scripts/providers/configure_aws_secondary_provider.py) and reuses [`shared-config-repo/scripts/providers/aws_identity.py`](shared-config-repo/scripts/providers/aws_identity.py) for root detection. -- Vars in [`stack-repo/vars.dev.yaml`](stack-repo/vars.dev.yaml) are adapted for your account, especially `subnet_id`. -- Account and region assumptions in [`shared-config-repo/stacksmith-config.yaml`](shared-config-repo/stacksmith-config.yaml) match your target environment. - -## Common commands - -Run these commands from the repository root. - -```bash -STACK_FILE="examples/stack-repo/stack.yaml" -VARS_FILE="examples/stack-repo/vars.dev.yaml" -BASE_CONFIG_FILE="examples/shared-config-repo/stacksmith-base-config.yaml" -CONFIG_FILE="examples/shared-config-repo/stacksmith-config.yaml" -SHARED_VARS_FILE= -``` - -Validate one stack. - -```bash -stacksmith validate "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Generate OpenTofu and Terragrunt JSON. - -```bash -stacksmith generate "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Run a plan with plan validations enabled and read its validation summary. - -```shell -stacksmith plan "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" \ - | jq '.summary' -``` - -This example includes a warning policy for `t3.micro` EC2 plans, so plan output can contain a warning outcome while still exiting successfully by default. - -Treat warnings as failures in strict mode. - -```bash -stacksmith plan "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" \ - --strict-validation-warnings -``` - -Write the machine-readable JSON report to a file. - -```bash -stacksmith plan "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" \ - --validation-report-format json \ - > plan-validation-report.json -``` - -Layer shared defaults from another repo ahead of stack-local values. - -```bash -stacksmith plan "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$SHARED_VARS_FILE" \ - --vars "$VARS_FILE" -``` - -Run `run-all` using the stack repo root. - -```bash -stacksmith run-all plan \ - --root examples/stack-repo \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Run a targeted plan for web-tagged components only when the stack has the `prod` tag. - -```bash -stacksmith run-all plan \ - --root examples/stack-repo \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" \ - --tag-expr "contains(stack_tags, 'prod') && tag.web" -``` - -## Lockfile workflow for examples/stack-repo - -The current example workflow supports a reproducible lockfile, but the generated lockfile is intentionally ignored in this repo until the output becomes fully machine-portable. `generate` and `init` create the file automatically when it is missing and enforce it during that command. - -Generate or intentionally update a lockfile for the example stack. - -```bash -cd examples/stack-repo -stacksmith lock stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml -``` - -Verify the existing lockfile matches resolved inputs: - -```bash -stacksmith lock stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml \ - --check -``` - -Use it for generation and plan/apply runs: - -```bash -stacksmith generate stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml - -stacksmith plan stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml \ - --locked - -stacksmith apply stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml \ - --locked -``` - -If the artifact cache is already warmed, add `--offline` to require local-only resolution: - -```bash -stacksmith generate stack.yaml \ - --config ../shared-config-repo/stacksmith-base-config.yaml \ - --config ../shared-config-repo/stacksmith-config.yaml \ - --vars vars.dev.yaml \ - --locked --offline -``` - -Unlocked runtime calls warn by default. Suppress that warning when the calling environment manages reproducibility another way. - -```bash -export STACKSMITH_WARN_ON_UNLOCKED=0 -stacksmith plan stack.yaml --config ../shared-config-repo/stacksmith-base-config.yaml --config ../shared-config-repo/stacksmith-config.yaml --vars vars.dev.yaml -``` - -Enable strict lockfile requirement for CI or gated environments: - -```bash -export STACKSMITH_REQUIRE_LOCKFILE=1 -stacksmith plan stack.yaml --config ../shared-config-repo/stacksmith-base-config.yaml --config ../shared-config-repo/stacksmith-config.yaml --vars vars.dev.yaml --locked -``` - -## Additional commands and options - -Initialize providers and backend for one stack. - -```bash -stacksmith init "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Apply one stack. - -```bash -stacksmith apply "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Destroy one stack. - -```bash -stacksmith destroy "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" \ - --vars "$VARS_FILE" -``` - -Inspect configured module mappings. - -```bash -stacksmith info modules-and-policies \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" -``` - -Show cache and module diagnostics for one stack. - -```bash -stacksmith info diagnose "$STACK_FILE" \ - --config "$BASE_CONFIG_FILE" \ - --config "$CONFIG_FILE" -``` - -High-value flags for this example workflow. - -- `--env-file path/to/.env` -- `--var key=value` -- `--build-dir path/to/build-dir` -- `--destroy` (for `stacksmith plan` and `stacksmith run-all plan`) -- `--strict-validation-warnings` -- `--use-local-modules` - -## Generated output - -For single-stack operations, generated files are written under `examples/stack-repo/.stacksmith`. - -When using `run-all` commands, generated files are organized as `examples/stack-repo/.stacksmith//`. - -You can override the default build directory location with `--build-dir`. - -Notable generated files include `stacksmith.tf.json` and `terragrunt.hcl.json`. - -## Security posture demonstrated - -This example includes plan validations for these controls. - -- IMDSv2 required on EC2 -- Warning when EC2 plans include `t3.micro` -- S3 public access settings checked in generated plans -- Bucket write permissions scoped to role principal and object paths - -The stack inputs also configure the following controls on S3 components. - -- S3 ownership controls -- Insecure transport deny policy attachment - -It also generates restrictive SSH ingress rules from explicit CIDR input. +For real repository ownership boundaries, see [the managed platform example](https://github.com/ci-sourcerer/stacksmith-example-platform) and [the application example](https://github.com/ci-sourcerer/stacksmith-example-app). diff --git a/examples/environment-directories/README.md b/examples/environment-directories/README.md new file mode 100644 index 0000000..6ddfbb2 --- /dev/null +++ b/examples/environment-directories/README.md @@ -0,0 +1,13 @@ +# Environment Directories + +A local-state tutorial using the built-in `terraform_data` resource. No AWS credentials or operation secrets are required. Run commands from this directory. + +```sh +stacksmith ci environments --gitops-root . --discovery-mode auto +stacksmith validate --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev/stacksmith.yaml +stacksmith plan --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev/stacksmith.yaml +stacksmith apply --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev/stacksmith.yaml +stacksmith destroy --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev/stacksmith.yaml +``` + +Repeat with `prod` in place of `dev`. Discovery lists both targets. Each deployment contains one resource with its environment and greeting as inputs. These examples use local state and require a remote backend for CI. diff --git a/examples/environment-directories/common/stacksmith.yaml b/examples/environment-directories/common/stacksmith.yaml new file mode 100644 index 0000000..4941436 --- /dev/null +++ b/examples/environment-directories/common/stacksmith.yaml @@ -0,0 +1,8 @@ +configs: + - source: local + data: + path: ../config.yaml +stacks: + - source: local + data: + path: ../stack.yaml diff --git a/examples/environment-directories/config.yaml b/examples/environment-directories/config.yaml new file mode 100644 index 0000000..ca1c4f6 --- /dev/null +++ b/examples/environment-directories/config.yaml @@ -0,0 +1,11 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../modules/terraform_data + auto_inject_inputs: true diff --git a/examples/environment-directories/environments/dev/stacksmith.yaml b/examples/environment-directories/environments/dev/stacksmith.yaml new file mode 100644 index 0000000..491333a --- /dev/null +++ b/examples/environment-directories/environments/dev/stacksmith.yaml @@ -0,0 +1,4 @@ +vars: + - source: local + data: + path: values.yaml diff --git a/examples/environment-directories/environments/dev/values.yaml b/examples/environment-directories/environments/dev/values.yaml new file mode 100644 index 0000000..d53f449 --- /dev/null +++ b/examples/environment-directories/environments/dev/values.yaml @@ -0,0 +1,2 @@ +environment: dev +message: Hello dev diff --git a/examples/environment-directories/environments/prod/stacksmith.yaml b/examples/environment-directories/environments/prod/stacksmith.yaml new file mode 100644 index 0000000..491333a --- /dev/null +++ b/examples/environment-directories/environments/prod/stacksmith.yaml @@ -0,0 +1,4 @@ +vars: + - source: local + data: + path: values.yaml diff --git a/examples/environment-directories/environments/prod/values.yaml b/examples/environment-directories/environments/prod/values.yaml new file mode 100644 index 0000000..d63ce28 --- /dev/null +++ b/examples/environment-directories/environments/prod/values.yaml @@ -0,0 +1,2 @@ +environment: prod +message: Hello prod diff --git a/examples/environment-directories/stack.yaml b/examples/environment-directories/stack.yaml new file mode 100644 index 0000000..58a0d6f --- /dev/null +++ b/examples/environment-directories/stack.yaml @@ -0,0 +1,8 @@ +name: "tutorial-{{ inputs.environment }}" +components: + message: + type: terraform_data + properties: + input: + environment: "{{ inputs.environment }}" + message: "{{ inputs.message }}" diff --git a/examples/environment-files/README.md b/examples/environment-files/README.md new file mode 100644 index 0000000..35767e4 --- /dev/null +++ b/examples/environment-files/README.md @@ -0,0 +1,13 @@ +# Environment Files + +A local-state tutorial using the built-in `terraform_data` resource. No AWS credentials or operation secrets are required. Run commands from this directory. + +```sh +stacksmith ci environments --gitops-root . --discovery-mode auto +stacksmith validate --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev.yaml +stacksmith plan --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev.yaml +stacksmith apply --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev.yaml +stacksmith destroy --build-dir .stacksmith/dev --runfile common/stacksmith.yaml --runfile environments/dev.yaml +``` + +Repeat with `prod` in place of `dev`. Discovery lists both targets. Each deployment contains one resource with its environment and greeting as inputs. These examples use local state and require a remote backend for CI. diff --git a/examples/environment-files/common/stacksmith.yaml b/examples/environment-files/common/stacksmith.yaml new file mode 100644 index 0000000..4941436 --- /dev/null +++ b/examples/environment-files/common/stacksmith.yaml @@ -0,0 +1,8 @@ +configs: + - source: local + data: + path: ../config.yaml +stacks: + - source: local + data: + path: ../stack.yaml diff --git a/examples/environment-files/config.yaml b/examples/environment-files/config.yaml new file mode 100644 index 0000000..ca1c4f6 --- /dev/null +++ b/examples/environment-files/config.yaml @@ -0,0 +1,11 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../modules/terraform_data + auto_inject_inputs: true diff --git a/examples/environment-files/environments/dev.yaml b/examples/environment-files/environments/dev.yaml new file mode 100644 index 0000000..dfdd4d9 --- /dev/null +++ b/examples/environment-files/environments/dev.yaml @@ -0,0 +1,4 @@ +vars: + - source: local + data: + path: ../vars/dev.yaml diff --git a/examples/environment-files/environments/prod.yaml b/examples/environment-files/environments/prod.yaml new file mode 100644 index 0000000..fb7f564 --- /dev/null +++ b/examples/environment-files/environments/prod.yaml @@ -0,0 +1,4 @@ +vars: + - source: local + data: + path: ../vars/prod.yaml diff --git a/examples/environment-files/stack.yaml b/examples/environment-files/stack.yaml new file mode 100644 index 0000000..58a0d6f --- /dev/null +++ b/examples/environment-files/stack.yaml @@ -0,0 +1,8 @@ +name: "tutorial-{{ inputs.environment }}" +components: + message: + type: terraform_data + properties: + input: + environment: "{{ inputs.environment }}" + message: "{{ inputs.message }}" diff --git a/examples/environment-files/vars/dev.yaml b/examples/environment-files/vars/dev.yaml new file mode 100644 index 0000000..d53f449 --- /dev/null +++ b/examples/environment-files/vars/dev.yaml @@ -0,0 +1,2 @@ +environment: dev +message: Hello dev diff --git a/examples/environment-files/vars/prod.yaml b/examples/environment-files/vars/prod.yaml new file mode 100644 index 0000000..d63ce28 --- /dev/null +++ b/examples/environment-files/vars/prod.yaml @@ -0,0 +1,2 @@ +environment: prod +message: Hello prod diff --git a/examples/github-actions/stacksmith-apply.yml b/examples/github-actions/stacksmith-apply.yml index 08926de..8ec0f77 100644 --- a/examples/github-actions/stacksmith-apply.yml +++ b/examples/github-actions/stacksmith-apply.yml @@ -13,7 +13,7 @@ on: required: false type: string discovery_mode: - description: "Discovery mode. Supported values: folders (default), flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string debug: diff --git a/examples/github-actions/stacksmith-destroy.yml b/examples/github-actions/stacksmith-destroy.yml index 4bb5a11..ec52fbf 100644 --- a/examples/github-actions/stacksmith-destroy.yml +++ b/examples/github-actions/stacksmith-destroy.yml @@ -12,7 +12,7 @@ on: required: false type: string discovery_mode: - description: "Discovery mode. Supported values: folders (default), flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string debug: diff --git a/examples/github-actions/stacksmith-operation.yml b/examples/github-actions/stacksmith-operation.yml index 04eeb98..096d315 100644 --- a/examples/github-actions/stacksmith-operation.yml +++ b/examples/github-actions/stacksmith-operation.yml @@ -16,7 +16,7 @@ on: required: false type: string discovery_mode: - description: "Discovery mode. Supported values: folders (default), flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string debug: diff --git a/examples/github-actions/stacksmith-plan-operation.yml b/examples/github-actions/stacksmith-plan-operation.yml index 6962f64..6b68d45 100644 --- a/examples/github-actions/stacksmith-plan-operation.yml +++ b/examples/github-actions/stacksmith-plan-operation.yml @@ -16,7 +16,7 @@ on: required: false type: string discovery_mode: - description: "Discovery mode. Supported values: folders (default), flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string debug: diff --git a/examples/github-actions/stacksmith-plan.yml b/examples/github-actions/stacksmith-plan.yml index 0c2362e..b23fcc0 100644 --- a/examples/github-actions/stacksmith-plan.yml +++ b/examples/github-actions/stacksmith-plan.yml @@ -13,7 +13,7 @@ on: required: false type: string discovery_mode: - description: "Discovery mode. Supported values: folders (default), flat-files, or env-files." + description: "Discovery mode. Supported values: auto (default), single, folders, or env-files." required: false type: string debug: diff --git a/examples/monorepo/README.md b/examples/monorepo/README.md new file mode 100644 index 0000000..62d474d --- /dev/null +++ b/examples/monorepo/README.md @@ -0,0 +1,3 @@ +# Multi-application monorepo + +`apps/payments` and `apps/catalog` are independent single-deployment roots. Run each tutorial from its own directory. Select each root separately in CLI/CI. Platform currently configures only one deployment root per repository; multiple-root support is deferred. diff --git a/examples/monorepo/apps/catalog/README.md b/examples/monorepo/apps/catalog/README.md new file mode 100644 index 0000000..d2f7e9a --- /dev/null +++ b/examples/monorepo/apps/catalog/README.md @@ -0,0 +1,12 @@ +# Catalog + +A local-state tutorial using the built-in `terraform_data` resource. No AWS credentials or operation secrets are required. Run commands from this directory. + +```sh +stacksmith validate --runfile stacksmith.yaml +stacksmith plan --runfile stacksmith.yaml +stacksmith apply --runfile stacksmith.yaml +stacksmith destroy --runfile stacksmith.yaml +``` + +Expect one resource on the first apply and no changes on a repeated plan. Destroy removes it from local state. Keep `.stacksmith-state` until cleanup completes. CI requires a durable remote backend. diff --git a/examples/monorepo/apps/catalog/config.yaml b/examples/monorepo/apps/catalog/config.yaml new file mode 100644 index 0000000..c19772b --- /dev/null +++ b/examples/monorepo/apps/catalog/config.yaml @@ -0,0 +1,11 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../../../modules/terraform_data + auto_inject_inputs: true diff --git a/examples/monorepo/apps/catalog/stack.yaml b/examples/monorepo/apps/catalog/stack.yaml new file mode 100644 index 0000000..2793361 --- /dev/null +++ b/examples/monorepo/apps/catalog/stack.yaml @@ -0,0 +1,8 @@ +name: tutorial +components: + message: + type: terraform_data + properties: + input: + environment: "{{ inputs.environment }}" + message: "{{ inputs.message }}" diff --git a/examples/monorepo/apps/catalog/stacksmith.yaml b/examples/monorepo/apps/catalog/stacksmith.yaml new file mode 100644 index 0000000..6551eb8 --- /dev/null +++ b/examples/monorepo/apps/catalog/stacksmith.yaml @@ -0,0 +1,12 @@ +configs: + - source: local + data: + path: config.yaml +stacks: + - source: local + data: + path: stack.yaml +vars: + - source: local + data: + path: values.yaml diff --git a/examples/monorepo/apps/catalog/values.yaml b/examples/monorepo/apps/catalog/values.yaml new file mode 100644 index 0000000..7a10fad --- /dev/null +++ b/examples/monorepo/apps/catalog/values.yaml @@ -0,0 +1,2 @@ +environment: default +message: Hello default diff --git a/examples/monorepo/apps/payments/README.md b/examples/monorepo/apps/payments/README.md new file mode 100644 index 0000000..fdfeee6 --- /dev/null +++ b/examples/monorepo/apps/payments/README.md @@ -0,0 +1,12 @@ +# Payments + +A local-state tutorial using the built-in `terraform_data` resource. No AWS credentials or operation secrets are required. Run commands from this directory. + +```sh +stacksmith validate --runfile stacksmith.yaml +stacksmith plan --runfile stacksmith.yaml +stacksmith apply --runfile stacksmith.yaml +stacksmith destroy --runfile stacksmith.yaml +``` + +Expect one resource on the first apply and no changes on a repeated plan. Destroy removes it from local state. Keep `.stacksmith-state` until cleanup completes. CI requires a durable remote backend. diff --git a/examples/monorepo/apps/payments/config.yaml b/examples/monorepo/apps/payments/config.yaml new file mode 100644 index 0000000..c19772b --- /dev/null +++ b/examples/monorepo/apps/payments/config.yaml @@ -0,0 +1,11 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../../../modules/terraform_data + auto_inject_inputs: true diff --git a/examples/monorepo/apps/payments/stack.yaml b/examples/monorepo/apps/payments/stack.yaml new file mode 100644 index 0000000..2793361 --- /dev/null +++ b/examples/monorepo/apps/payments/stack.yaml @@ -0,0 +1,8 @@ +name: tutorial +components: + message: + type: terraform_data + properties: + input: + environment: "{{ inputs.environment }}" + message: "{{ inputs.message }}" diff --git a/examples/monorepo/apps/payments/stacksmith.yaml b/examples/monorepo/apps/payments/stacksmith.yaml new file mode 100644 index 0000000..6551eb8 --- /dev/null +++ b/examples/monorepo/apps/payments/stacksmith.yaml @@ -0,0 +1,12 @@ +configs: + - source: local + data: + path: config.yaml +stacks: + - source: local + data: + path: stack.yaml +vars: + - source: local + data: + path: values.yaml diff --git a/examples/monorepo/apps/payments/values.yaml b/examples/monorepo/apps/payments/values.yaml new file mode 100644 index 0000000..7a10fad --- /dev/null +++ b/examples/monorepo/apps/payments/values.yaml @@ -0,0 +1,2 @@ +environment: default +message: Hello default diff --git a/examples/operations/README.md b/examples/operations/README.md new file mode 100644 index 0000000..d5a2154 --- /dev/null +++ b/examples/operations/README.md @@ -0,0 +1,15 @@ +# Operations + +This local example adds an automatic reconciliation operation to the quickstart workload. Infrastructure requires no cloud credentials. The operation uses a deferred secret independently of infrastructure. + +Set `STACKSMITH_EXAMPLE_SECRET` to a disposable test token and `STACKSMITH_SECRET_FINGERPRINT_KEY` to a stable, high-entropy value in your shell. Do not commit either value. Run from this directory. + +```sh +stacksmith plan --runfile stacksmith.yaml +stacksmith apply --runfile stacksmith.yaml +stacksmith apply --runfile stacksmith.yaml +stacksmith operation destroy --runfile stacksmith.yaml +stacksmith destroy --runfile stacksmith.yaml +``` + +The first apply reconciles the operation, masking the token. The second unchanged apply should not rerun it. Rotating the token changes its fingerprint. Unsetting the token makes secret resolution fail before execution. Keep the fingerprint key stable between runs. This example uses local state and is not a CI deployment. diff --git a/examples/operations/config.yaml b/examples/operations/config.yaml new file mode 100644 index 0000000..bbbcdaf --- /dev/null +++ b/examples/operations/config.yaml @@ -0,0 +1,50 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../modules/terraform_data + auto_inject_inputs: true +operations: + echo_reconciliation: + description: Echo completion of a simple GitOps reconciliation. + runner: local + trigger: after_apply + stream_output: true + output_masking: + inputs: + - secret_token + literals: + - DO-NOT-LEAK + command: + - sh + - -c + - 'echo "Stacksmith simple GitOps reconciliation completed: environment=$STACKSMITH_OPERATION_ENVIRONMENT + message=$STACKSMITH_OPERATION_MESSAGE project=$STACKSMITH_OPERATION_PROJECT + token=$STACKSMITH_OPERATION_SECRET_TOKEN"' + environment: + mode: auto + inputs: + overrides: + STACKSMITH_OPERATION_ENVIRONMENT: environment + STACKSMITH_OPERATION_MESSAGE: message + STACKSMITH_OPERATION_PROJECT: project + STACKSMITH_OPERATION_SECRET_TOKEN: secret_token + inputs: + environment: + description: Deployment environment being reconciled. + required: true + message: + description: Message declared by the environment manifest. + required: true + project: + description: Project declared by the common manifest. + required: true + secret_token: + description: Sensitive token shown in streamed output for masking demonstration. + required: true + secret: true diff --git a/examples/operations/stack.yaml b/examples/operations/stack.yaml new file mode 100644 index 0000000..810ae83 --- /dev/null +++ b/examples/operations/stack.yaml @@ -0,0 +1,17 @@ +name: tutorial +components: + message: + type: terraform_data + properties: + input: + environment: '{{ inputs.environment }}' + message: '{{ inputs.message }}' +operations: + announce_reconciliation: + description: Emit a harmless completion message after the stack applies. + use: echo_reconciliation + with: + environment: '{{ inputs.environment }}' + message: '{{ inputs.message }}' + project: '{{ inputs.project }}' + secret_token: '{{ inputs.reconciliation_secret_token }}' diff --git a/examples/operations/stacksmith.yaml b/examples/operations/stacksmith.yaml new file mode 100644 index 0000000..6551eb8 --- /dev/null +++ b/examples/operations/stacksmith.yaml @@ -0,0 +1,12 @@ +configs: + - source: local + data: + path: config.yaml +stacks: + - source: local + data: + path: stack.yaml +vars: + - source: local + data: + path: values.yaml diff --git a/examples/operations/values.yaml b/examples/operations/values.yaml new file mode 100644 index 0000000..5bf4883 --- /dev/null +++ b/examples/operations/values.yaml @@ -0,0 +1,6 @@ +environment: default +message: Hello default +project: stacksmith +reconciliation_secret_token: + $secret: + env: STACKSMITH_EXAMPLE_SECRET diff --git a/examples/quickstart/README.md b/examples/quickstart/README.md new file mode 100644 index 0000000..3cd4749 --- /dev/null +++ b/examples/quickstart/README.md @@ -0,0 +1,12 @@ +# Quickstart + +A local-state tutorial using the built-in `terraform_data` resource. No AWS credentials or operation secrets are required. Run commands from this directory. + +```sh +stacksmith validate --runfile stacksmith.yaml +stacksmith plan --runfile stacksmith.yaml +stacksmith apply --runfile stacksmith.yaml +stacksmith destroy --runfile stacksmith.yaml +``` + +Expect one resource on the first apply and no changes on a repeated plan. Destroy removes it from local state. Keep `.stacksmith-state` until cleanup completes. CI requires a durable remote backend. diff --git a/examples/quickstart/config.yaml b/examples/quickstart/config.yaml new file mode 100644 index 0000000..ca1c4f6 --- /dev/null +++ b/examples/quickstart/config.yaml @@ -0,0 +1,11 @@ +backend: + data: + type: local + path: .stacksmith-state +module_mappings: + terraform_data: + source: + source: local + data: + path: ../modules/terraform_data + auto_inject_inputs: true diff --git a/examples/quickstart/stack.yaml b/examples/quickstart/stack.yaml new file mode 100644 index 0000000..2793361 --- /dev/null +++ b/examples/quickstart/stack.yaml @@ -0,0 +1,8 @@ +name: tutorial +components: + message: + type: terraform_data + properties: + input: + environment: "{{ inputs.environment }}" + message: "{{ inputs.message }}" diff --git a/examples/quickstart/stacksmith.yaml b/examples/quickstart/stacksmith.yaml new file mode 100644 index 0000000..6551eb8 --- /dev/null +++ b/examples/quickstart/stacksmith.yaml @@ -0,0 +1,12 @@ +configs: + - source: local + data: + path: config.yaml +stacks: + - source: local + data: + path: stack.yaml +vars: + - source: local + data: + path: values.yaml diff --git a/examples/quickstart/values.yaml b/examples/quickstart/values.yaml new file mode 100644 index 0000000..7a10fad --- /dev/null +++ b/examples/quickstart/values.yaml @@ -0,0 +1,2 @@ +environment: default +message: Hello default diff --git a/examples/scripts/run-act-workflow.sh b/examples/scripts/run-act-workflow.sh deleted file mode 100755 index 3f35a0a..0000000 --- a/examples/scripts/run-act-workflow.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/sh - -set -eu - -####################################### -# Print usage instructions and exit. -# Globals: -# None -# Arguments: -# None -####################################### -usage() { - cat < -Examples: - $0 gitops-repo plan dev - $0 gitops-simple-repo apply dev - -Environment variables: - IMAGE_VERSION Optional image version (default: latest) - STACKSMITH_EXAMPLE_SECRET Required for gitops-simple-repo - STACKSMITH_SECRET_FINGERPRINT_KEY Required for gitops-simple-repo - STACKSMITH_JENKINS_USERNAME Required for gitops-repo apply - STACKSMITH_JENKINS_API_TOKEN Required for gitops-repo apply -EOF - exit 1 -} - -if [ "$#" -ne 3 ]; then - usage -fi - -case ${1%/} in -gitops-repo | examples/gitops-repo) - gitops_root=examples/gitops-repo - config_ref=examples/shared-config-repo/stacksmith-config.yaml - ;; -gitops-simple-repo | examples/gitops-simple-repo) - gitops_root=examples/gitops-simple-repo - config_ref=examples/shared-config-repo/stacksmith-config.yaml - ;; -*) - echo "Invalid example: $1" >&2 - usage - ;; -esac - -stacksmith_command=$2 -environment=$3 -image_version=${IMAGE_VERSION:-latest} -image_ref=docker.io/cisourcerer/stacksmith:"$image_version" - -if [ "$stacksmith_command" != "plan" ] && - [ "$stacksmith_command" != "apply" ]; then - echo "Invalid command: $stacksmith_command" >&2 - usage -fi - -if [ "$gitops_root" = "examples/gitops-repo" ]; then - : "${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID must be set}" - : "${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY must be set}" - - if [ "$stacksmith_command" = "apply" ]; then - : "${STACKSMITH_JENKINS_USERNAME:?must be set}" - : "${STACKSMITH_JENKINS_API_TOKEN:?must be set}" - fi - - aws_region=${AWS_REGION:-${AWS_DEFAULT_REGION:-}} - if [ -z "$aws_region" ]; then - aws_region=$(aws configure get region) - fi - if [ -z "$aws_region" ]; then - echo "AWS_REGION or AWS_DEFAULT_REGION must be set" >&2 - exit 1 - fi - - AWS_REGION=$aws_region - AWS_DEFAULT_REGION=$aws_region - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN:-} - export AWS_DEFAULT_REGION AWS_REGION AWS_SESSION_TOKEN -fi - -if [ "$gitops_root" = "examples/gitops-simple-repo" ]; then - umask 077 - mkdir -p examples/gitops-simple-repo/.stacksmith/act-secrets - if [ -z "${STACKSMITH_EXAMPLE_SECRET:-}" ]; then - if [ ! -s examples/gitops-simple-repo/.stacksmith/act-secrets/example ]; then - openssl rand -hex 32 >examples/gitops-simple-repo/.stacksmith/act-secrets/example - fi - STACKSMITH_EXAMPLE_SECRET=$(cat examples/gitops-simple-repo/.stacksmith/act-secrets/example) - fi - if [ -z "${STACKSMITH_SECRET_FINGERPRINT_KEY:-}" ]; then - if [ ! -s examples/gitops-simple-repo/.stacksmith/act-secrets/fingerprint ]; then - openssl rand -hex 32 >examples/gitops-simple-repo/.stacksmith/act-secrets/fingerprint - fi - STACKSMITH_SECRET_FINGERPRINT_KEY=$(cat examples/gitops-simple-repo/.stacksmith/act-secrets/fingerprint) - fi -fi - -if ! docker pull "$image_ref"; then - poe build-image --single-arch --plain - if [ "$image_version" != "latest" ]; then - docker image tag docker.io/cisourcerer/stacksmith:latest "$image_ref" - fi -fi - -tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/stacksmith-act.XXXXXX") -manifest_file=$tmpdir/ci-manifest.json -event_file=$tmpdir/workflow-call.json -secret_file=$tmpdir/secrets -secret_file_ready=false -node_container= -trap 'if [ -n "$node_container" ]; then docker rm -f "$node_container" >/dev/null; fi; rm -f "$manifest_file" "$event_file" "$secret_file" "$tmpdir/node" "$tmpdir/workflow.yml"; rmdir "$tmpdir"' EXIT - -docker pull --platform linux/amd64 node:24-bookworm-slim -node_container=$(docker create --platform linux/amd64 node:24-bookworm-slim) -docker cp "$node_container:/usr/local/bin/node" "$tmpdir/node" -docker rm "$node_container" >/dev/null -node_container= -awk -v node="$tmpdir/node" ' - /^ env:$/ && !env_added { - print - print " STACKSMITH_EXAMPLE_SECRET: ${{ secrets.STACKSMITH_EXAMPLE_SECRET }}" - print " STACKSMITH_SECRET_FINGERPRINT_KEY: ${{ secrets.STACKSMITH_SECRET_FINGERPRINT_KEY }}" - env_added = 1 - next - } - /^ image: docker.io\/cisourcerer\/stacksmith:/ { - print - print " volumes:" - print " - " node ":/usr/local/bin/node:ro" - next - } - { print } -' .github/workflows/stacksmith-gitops-reusable.yml >"$tmpdir/workflow.yml" - -if [ "$gitops_root" = "examples/gitops-simple-repo" ]; then - umask 077 - printf 'STACKSMITH_EXAMPLE_SECRET=%s\n' "$STACKSMITH_EXAMPLE_SECRET" \ - >"$secret_file" - printf 'STACKSMITH_SECRET_FINGERPRINT_KEY=%s\n' \ - "$STACKSMITH_SECRET_FINGERPRINT_KEY" >>"$secret_file" - secret_file_ready=true -fi - -# The GitOps example runfiles already layer the shared platform config -# through their own `configs:` list. The CI wrapper should pass only the -# managed config reference here and let the runfile resolve the full -# shared configuration stack. -if [ "$gitops_root" = "examples/gitops-simple-repo" ]; then - docker run --rm \ - --volume "$PWD:/workspace:ro" \ - --workdir /workspace \ - --env "STACKSMITH_EXAMPLE_SECRET=$STACKSMITH_EXAMPLE_SECRET" \ - --env "INPUT_COMMAND=$stacksmith_command" \ - --env "INPUT_CONFIG_REF=$config_ref" \ - --env "INPUT_GITOPS_ROOT=$gitops_root" \ - --env INPUT_DISCOVERY_MODE=env-files \ - --env "INPUT_ENVIRONMENTS=$environment" \ - --env INPUT_WORKDIR=. \ - --env INPUT_ENV_FILE=/dev/null \ - --env 'INPUT_STACKSMITH_ARGS_JSON=[]' \ - --env 'STACKSMITH_CI_SKIP_BACKEND_VALIDATION=true' \ - --env SKIP_BRANCH_VALIDATION=true \ - "$image_ref" ci prepare-from-env --provider generic >"$manifest_file" -else - docker run --rm \ - --volume "$PWD:/workspace:ro" \ - --workdir /workspace \ - --env "INPUT_COMMAND=$stacksmith_command" \ - --env "INPUT_CONFIG_REF=$config_ref" \ - --env "INPUT_GITOPS_ROOT=$gitops_root" \ - --env INPUT_DISCOVERY_MODE=env-files \ - --env "INPUT_ENVIRONMENTS=$environment" \ - --env INPUT_WORKDIR=. \ - --env INPUT_ENV_FILE=/dev/null \ - --env 'INPUT_STACKSMITH_ARGS_JSON=[]' \ - --env "STACKSMITH_CI_SKIP_BACKEND_VALIDATION=${STACKSMITH_CI_SKIP_BACKEND_VALIDATION:-false}" \ - --env SKIP_BRANCH_VALIDATION=true \ - "$image_ref" ci prepare-from-env --provider generic >"$manifest_file" -fi - -jq -e \ - --arg command "$stacksmith_command" \ - --arg config_ref "$config_ref" \ - --arg environment "$environment" \ - ' - .command == $command - and .config_ref == $config_ref - and any(.matrix[]; .environment == $environment) - ' \ - "$manifest_file" >/dev/null - -jq -n \ - --rawfile ci_manifest "$manifest_file" \ - --arg environment "$environment" \ - --arg image_version "$image_version" \ - '{ - inputs: { - ci_manifest: $ci_manifest, - environment: $environment, - image_version: $image_version, - upload_artifacts: false - } - }' >"$event_file" - -set -- workflow_call \ - -W "$tmpdir/workflow.yml" \ - --container-architecture linux/amd64 \ - -P ubuntu-latest=catthehacker/ubuntu:full-latest \ - -e "$event_file" \ - --pull=false - -if [ "$gitops_root" = "examples/gitops-repo" ]; then - set -- "$@" \ - --env AWS_ACCESS_KEY_ID \ - --env AWS_SECRET_ACCESS_KEY \ - --env AWS_SESSION_TOKEN \ - --env AWS_REGION \ - --env AWS_DEFAULT_REGION -fi - -if [ "$gitops_root" = "examples/gitops-repo" ] && - [ "$stacksmith_command" = "apply" ]; then - if [ "$secret_file_ready" = false ]; then - umask 077 - : >"$secret_file" - secret_file_ready=true - fi - printf 'STACKSMITH_JENKINS_USERNAME=%s\n' \ - "$STACKSMITH_JENKINS_USERNAME" >>"$secret_file" - printf 'STACKSMITH_JENKINS_API_TOKEN=%s\n' \ - "$STACKSMITH_JENKINS_API_TOKEN" >>"$secret_file" -fi - -if [ "$secret_file_ready" = true ]; then - set -- "$@" --secret-file "$secret_file" -fi - -act "$@" diff --git a/src/stacksmith/ci/selection.py b/src/stacksmith/ci/selection.py index f97e7d0..ff344ab 100644 --- a/src/stacksmith/ci/selection.py +++ b/src/stacksmith/ci/selection.py @@ -101,14 +101,7 @@ def normalize_discovery_mode(value: str) -> DiscoveryMode: Canonical discovery mode. """ mode = value.strip().lower() or DiscoveryMode.FOLDERS.value - if mode in {DiscoveryMode.AUTO.value, ""}: - normalized = DiscoveryMode.AUTO - else: - normalized = ( - DiscoveryMode.ENV_FILES - if mode in {"env", DiscoveryMode.ENV_FILES.value} - else DiscoveryMode(mode) - ) + normalized = DiscoveryMode(mode) LOGGER.debug("Normalized discovery mode %r -> %r", value, normalized) return normalized @@ -140,18 +133,24 @@ def resolve_discovery_mode(mode: DiscoveryMode, root: Path) -> DiscoveryMode: return mode env_root = root / "environments" - if env_root.exists() and any(env_root.iterdir()): + candidates = set() + if env_root.is_dir(): for path in env_root.iterdir(): - if path.is_file() and path.suffix in { - f".{ext}" for ext in DISCOVERY_FILE_EXTENSIONS - }: - return DiscoveryMode.ENV_FILES - return DiscoveryMode.FOLDERS - if any( - _flat_file_environment_name(path) not in _NON_ENVIRONMENT_NAMES - for path in _flat_environment_files(root) - ): - return DiscoveryMode.FLAT_FILES + if path.is_file() and path.suffix.lstrip(".") in DISCOVERY_FILE_EXTENSIONS: + if path.stem not in _NON_ENVIRONMENT_NAMES: + candidates.add(DiscoveryMode.ENV_FILES) + elif path.is_dir() and any( + (path / f"stacksmith.{extension}").is_file() + for extension in DISCOVERY_FILE_EXTENSIONS + ): + candidates.add(DiscoveryMode.FOLDERS) + if len(candidates) > 1: + raise ValueError( + "Ambiguous deployment layout: environment files and directories coexist. " + "Choose an explicit discovery mode or keep only one layout." + ) + if candidates: + return candidates.pop() if any((root / candidate).is_file() for candidate in COMMON_CANDIDATES) or any( (root / f"stack.{extension}").is_file() for extension in DISCOVERY_FILE_EXTENSIONS @@ -160,18 +159,6 @@ def resolve_discovery_mode(mode: DiscoveryMode, root: Path) -> DiscoveryMode: return DiscoveryMode.FOLDERS -def _flat_environment_files(root: Path) -> list[Path]: - return ( - sorted(root.glob("stacksmith.*.yaml")) - + sorted(root.glob("stacksmith.*.yml")) - + sorted(root.glob("stacksmith.*.json")) - ) - - -def _flat_file_environment_name(path: Path) -> str: - return path.stem.replace("stacksmith.", "", 1) - - def project_root(gitops_root: str) -> Path: """Return filesystem path for a normalized GitOps root value. @@ -212,6 +199,7 @@ def discover_environments(mode: DiscoveryMode, root: Path) -> tuple[list[str], s Returns: Tuple of discovered environment names and common runfile path. """ + mode = validate_discovery_mode(mode) LOGGER.info("Discovering environments with mode=%s root=%s", mode, root) all_envs = [] if mode == DiscoveryMode.SINGLE: @@ -227,17 +215,6 @@ def discover_environments(mode: DiscoveryMode, root: Path) -> tuple[list[str], s ) env_root = root / "environments" - if mode == DiscoveryMode.FLAT_FILES: - common_file = common_run_file_for(root) - LOGGER.debug("Scanning flat-files under %s", root) - for path in _flat_environment_files(root): - name = _flat_file_environment_name(path) - if name in _NON_ENVIRONMENT_NAMES: - continue - all_envs.append(name) - LOGGER.debug("Discovered flat-file environments: %s", all_envs) - return all_envs, common_file - if mode == DiscoveryMode.ENV_FILES: common_file = common_run_file_for(root) LOGGER.debug("Scanning env-files under %s", env_root) @@ -428,16 +405,6 @@ def _changed_path_environment( if relative_path.parts else None ) - if mode == DiscoveryMode.FLAT_FILES: - return ( - _known_environment( - candidate.removeprefix("stacksmith.").rsplit(".", 1)[0], - all_envs, - ) - if candidate.startswith("stacksmith.") - and candidate.endswith((".yaml", ".yml", ".json")) - else None - ) if mode == DiscoveryMode.ENV_FILES: if not candidate.startswith("environments/"): return None diff --git a/src/stacksmith/cli/args.py b/src/stacksmith/cli/args.py index f525263..7164db1 100644 --- a/src/stacksmith/cli/args.py +++ b/src/stacksmith/cli/args.py @@ -657,10 +657,8 @@ def _add_gitops_discovery_args( ) -> None: discovery_mode_choices = [ DiscoveryMode.FOLDERS.value, - DiscoveryMode.FLAT_FILES.value, DiscoveryMode.ENV_FILES.value, DiscoveryMode.SINGLE.value, - "env", ] if include_auto_mode: discovery_mode_choices.append(DiscoveryMode.AUTO.value) @@ -679,10 +677,7 @@ def _add_gitops_discovery_args( "--discovery-mode", default=discovery_mode_default, choices=discovery_mode_choices, - help=( - "Environment discovery mode. Use single, folders, flat-files, or env-files " - "(env is an alias for env-files)." - ), + help=("Environment discovery mode. Use single, folders, or env-files "), ) parser.add_argument( "--environments", diff --git a/src/stacksmith/enums.py b/src/stacksmith/enums.py index 5f40de2..723b85e 100644 --- a/src/stacksmith/enums.py +++ b/src/stacksmith/enums.py @@ -21,7 +21,6 @@ class DiscoveryMode(StrEnum): """Supported GitOps environment discovery modes.""" FOLDERS = "folders" - FLAT_FILES = "flat-files" ENV_FILES = "env-files" SINGLE = "single" AUTO = "auto" diff --git a/examples/gitops-repo/README.md b/tests/fixtures/authoring-examples/gitops-repo/README.md similarity index 100% rename from examples/gitops-repo/README.md rename to tests/fixtures/authoring-examples/gitops-repo/README.md diff --git a/examples/gitops-repo/common/stacksmith.yaml b/tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml similarity index 100% rename from examples/gitops-repo/common/stacksmith.yaml rename to tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml diff --git a/examples/gitops-repo/environments/dev.yaml b/tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml similarity index 100% rename from examples/gitops-repo/environments/dev.yaml rename to tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml diff --git a/examples/gitops-repo/environments/prod.yaml b/tests/fixtures/authoring-examples/gitops-repo/environments/prod.yaml similarity index 100% rename from examples/gitops-repo/environments/prod.yaml rename to tests/fixtures/authoring-examples/gitops-repo/environments/prod.yaml diff --git a/examples/gitops-repo/manifests/common/frontend-values.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/common/frontend-values.yaml similarity index 100% rename from examples/gitops-repo/manifests/common/frontend-values.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/common/frontend-values.yaml diff --git a/examples/gitops-repo/manifests/common/platform.stack.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/common/platform.stack.yaml similarity index 95% rename from examples/gitops-repo/manifests/common/platform.stack.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/common/platform.stack.yaml index 3e1e9b9..b9f6ee1 100644 --- a/examples/gitops-repo/manifests/common/platform.stack.yaml +++ b/tests/fixtures/authoring-examples/gitops-repo/manifests/common/platform.stack.yaml @@ -40,7 +40,7 @@ components: repository: https://kubernetes.github.io/ingress-nginx version: "4.11.3" values_files: - - examples/gitops-repo/manifests/common/frontend-values.yaml + - tests/fixtures/authoring-examples/gitops-repo/manifests/common/frontend-values.yaml app_config: type: k8s_app description: Kubernetes configuration for the application. diff --git a/examples/gitops-repo/manifests/common/service.stack.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/common/service.stack.yaml similarity index 85% rename from examples/gitops-repo/manifests/common/service.stack.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/common/service.stack.yaml index 89fa843..ffd409a 100644 --- a/examples/gitops-repo/manifests/common/service.stack.yaml +++ b/tests/fixtures/authoring-examples/gitops-repo/manifests/common/service.stack.yaml @@ -22,7 +22,7 @@ components: version: "4.11.3" namespace: "{{ inputs.environment }}" values_files: - - "examples/gitops-repo/manifests/environments/{{ inputs.environment }}/frontend-values.yaml" + - "tests/fixtures/authoring-examples/gitops-repo/manifests/environments/{{ inputs.environment }}/frontend-values.yaml" app_config: type: k8s_app tags: @@ -31,7 +31,7 @@ components: properties: namespace: "{{ inputs.environment }}" manifest_files: - - "examples/gitops-repo/manifests/environments/{{ inputs.environment }}/app-config.yaml" + - "tests/fixtures/authoring-examples/gitops-repo/manifests/environments/{{ inputs.environment }}/app-config.yaml" logs_bucket: type: aws_s3_bucket tags: diff --git a/examples/gitops-repo/manifests/environments/dev/app-config.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/app-config.yaml similarity index 100% rename from examples/gitops-repo/manifests/environments/dev/app-config.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/app-config.yaml diff --git a/examples/gitops-repo/manifests/environments/dev/frontend-values.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/frontend-values.yaml similarity index 100% rename from examples/gitops-repo/manifests/environments/dev/frontend-values.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/frontend-values.yaml diff --git a/examples/gitops-repo/manifests/environments/prod/app-config.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/environments/prod/app-config.yaml similarity index 100% rename from examples/gitops-repo/manifests/environments/prod/app-config.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/environments/prod/app-config.yaml diff --git a/examples/gitops-repo/manifests/environments/prod/frontend-values.yaml b/tests/fixtures/authoring-examples/gitops-repo/manifests/environments/prod/frontend-values.yaml similarity index 100% rename from examples/gitops-repo/manifests/environments/prod/frontend-values.yaml rename to tests/fixtures/authoring-examples/gitops-repo/manifests/environments/prod/frontend-values.yaml diff --git a/examples/gitops-repo/vars/vars.dev.yaml b/tests/fixtures/authoring-examples/gitops-repo/vars/vars.dev.yaml similarity index 100% rename from examples/gitops-repo/vars/vars.dev.yaml rename to tests/fixtures/authoring-examples/gitops-repo/vars/vars.dev.yaml diff --git a/examples/gitops-repo/vars/vars.prod.yaml b/tests/fixtures/authoring-examples/gitops-repo/vars/vars.prod.yaml similarity index 100% rename from examples/gitops-repo/vars/vars.prod.yaml rename to tests/fixtures/authoring-examples/gitops-repo/vars/vars.prod.yaml diff --git a/examples/gitops-simple-repo/README.md b/tests/fixtures/authoring-examples/gitops-simple-repo/README.md similarity index 100% rename from examples/gitops-simple-repo/README.md rename to tests/fixtures/authoring-examples/gitops-simple-repo/README.md diff --git a/examples/gitops-simple-repo/common/stacksmith.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/common/stacksmith.yaml similarity index 100% rename from examples/gitops-simple-repo/common/stacksmith.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/common/stacksmith.yaml diff --git a/examples/gitops-simple-repo/environments/dev.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/environments/dev.yaml similarity index 100% rename from examples/gitops-simple-repo/environments/dev.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/environments/dev.yaml diff --git a/examples/gitops-simple-repo/environments/prod.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/environments/prod.yaml similarity index 100% rename from examples/gitops-simple-repo/environments/prod.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/environments/prod.yaml diff --git a/examples/gitops-simple-repo/manifests/common/terraform-data.stack.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/manifests/common/terraform-data.stack.yaml similarity index 100% rename from examples/gitops-simple-repo/manifests/common/terraform-data.stack.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/manifests/common/terraform-data.stack.yaml diff --git a/examples/gitops-simple-repo/vars/vars.common.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.common.yaml similarity index 100% rename from examples/gitops-simple-repo/vars/vars.common.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.common.yaml diff --git a/examples/gitops-simple-repo/vars/vars.dev.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.dev.yaml similarity index 100% rename from examples/gitops-simple-repo/vars/vars.dev.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.dev.yaml diff --git a/examples/gitops-simple-repo/vars/vars.prod.yaml b/tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.prod.yaml similarity index 100% rename from examples/gitops-simple-repo/vars/vars.prod.yaml rename to tests/fixtures/authoring-examples/gitops-simple-repo/vars/vars.prod.yaml diff --git a/tests/fixtures/authoring-examples/modules/README.md b/tests/fixtures/authoring-examples/modules/README.md new file mode 100644 index 0000000..98fcdf3 --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/README.md @@ -0,0 +1,56 @@ +# Shared GitOps example modules + +This directory contains shared Stacksmith module implementations for app deployment types that a platform team can expose through a managed config. + +These modules are intentionally simplified examples. + +- `helm_app` models a Helm-style app deployment path using `helm_release`. +- `k8s_app` models a Kubernetes manifest delivery path using `kubernetes_manifest`. +- `terraform_data` creates a credential-free placeholder for the simple GitOps example. + +They demonstrate how Stacksmith component types can be mapped to platform-approved modules. Note that `helm_app` and `kubernetes_app` are shown here as modules, but they could also be implemented as [native operations](../../README.md#native-operations) instead; it is purely a platform team preference. Modules are declarative and benefit from OpenTofu state management and dependency tracking. Operations are imperative actions that are useful for commands or workflows that don't map cleanly to infrastructure code. For app deployments like these, modules are typically the better choice, because they have first-class OpenTofu provider support, but the architecture is flexible. + +These modules are referenced by the canonical [GitOps example](../gitops-repo) through its [managed config](../shared-config-repo/stacksmith-config.yaml). + +## Usage + +Use these module sources in `stacksmith-config.yaml` module mappings and then refer to them from stack components by `type`. + +Example module mapping. + +```yaml +module_mappings: + helm_app: + source: + source: local + data: + path: examples/modules/helm_app +``` + +Example component. + +```yaml +components: + frontend: + type: helm_app + properties: + name: frontend-release + chart: ingress-nginx + repository: https://kubernetes.github.io/ingress-nginx + version: "4.11.3" + namespace: web + values_files: + - frontend-values.dev.yaml +``` + +Example Kubernetes manifest component. + +```yaml +components: + app_config: + type: k8s_app + properties: + namespace: default + manifest_files: + - app-config.dev.yaml +``` diff --git a/tests/fixtures/authoring-examples/modules/helm_app/README.md b/tests/fixtures/authoring-examples/modules/helm_app/README.md new file mode 100644 index 0000000..ad7b7ea --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/helm_app/README.md @@ -0,0 +1,41 @@ + +## Requirements + +| Name | Version | +| - | - | +| [helm](#requirement\_helm) | ~> 3.1 | + +## Providers + +| Name | Version | +| - | - | +| [helm](#provider\_helm) | ~> 3.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +| - | - | +| [helm_release.app](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| - | - | - | - | :------: | +| [atomic](#input\_atomic) | Whether rollbacks are attempted if the Helm release deployment fails. | `bool` | `true` | no | +| [chart](#input\_chart) | Helm chart name to deploy. | `string` | n/a | yes | +| [chart\_version](#input\_chart\_version) | Helm chart version. | `string` | n/a | yes | +| [create\_namespace](#input\_create\_namespace) | Whether to create the target namespace if it does not already exist. | `bool` | `false` | no | +| [name](#input\_name) | Optional Helm release name. When omitted, a default name is derived from the chart and namespace. | `string` | `null` | no | +| [namespace](#input\_namespace) | Target Kubernetes namespace for the Helm release. | `string` | n/a | yes | +| [repository](#input\_repository) | Helm chart repository URL. | `string` | n/a | yes | +| [values\_files](#input\_values\_files) | List of Helm values YAML files to render into the chart deployment. | `list(string)` | `[]` | no | +| [wait](#input\_wait) | Whether Terraform waits for the Helm release to become ready. | `bool` | `true` | no | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/tests/fixtures/authoring-examples/modules/helm_app/main.tf b/tests/fixtures/authoring-examples/modules/helm_app/main.tf new file mode 100644 index 0000000..58e10d7 --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/helm_app/main.tf @@ -0,0 +1,17 @@ +locals { + derived_release_name = replace(var.chart, "/", "-") + release_name = coalesce(var.name, "${var.namespace}-${local.derived_release_name}") + values = [for values_file in var.values_files : file(abspath(values_file))] +} + +resource "helm_release" "app" { + name = local.release_name + chart = var.chart + repository = var.repository + version = var.chart_version + namespace = var.namespace + create_namespace = var.create_namespace + wait = var.wait + atomic = var.atomic + values = local.values +} diff --git a/tests/fixtures/authoring-examples/modules/helm_app/outputs.tf b/tests/fixtures/authoring-examples/modules/helm_app/outputs.tf new file mode 100644 index 0000000..afbe47b --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/helm_app/outputs.tf @@ -0,0 +1,4 @@ +output "release_name" { + description = "Deployed Helm release name." + value = helm_release.app.name +} diff --git a/tests/fixtures/authoring-examples/modules/helm_app/variables.tf b/tests/fixtures/authoring-examples/modules/helm_app/variables.tf new file mode 100644 index 0000000..05a527c --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/helm_app/variables.tf @@ -0,0 +1,49 @@ +variable "name" { + type = string + default = null + description = "Optional Helm release name. When omitted, a default name is derived from the chart and namespace." +} + +variable "chart" { + type = string + description = "Helm chart name to deploy." +} + +variable "repository" { + type = string + description = "Helm chart repository URL." +} + +variable "chart_version" { + type = string + description = "Helm chart version." +} + +variable "namespace" { + type = string + description = "Target Kubernetes namespace for the Helm release." +} + +variable "create_namespace" { + type = bool + default = false + description = "Whether to create the target namespace if it does not already exist." +} + +variable "wait" { + type = bool + default = true + description = "Whether Terraform waits for the Helm release to become ready." +} + +variable "atomic" { + type = bool + default = true + description = "Whether rollbacks are attempted if the Helm release deployment fails." +} + +variable "values_files" { + type = list(string) + default = [] + description = "List of Helm values YAML files to render into the chart deployment." +} diff --git a/tests/fixtures/authoring-examples/modules/helm_app/versions.tf b/tests/fixtures/authoring-examples/modules/helm_app/versions.tf new file mode 100644 index 0000000..6523d8a --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/helm_app/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + helm = { + source = "hashicorp/helm" + version = "~> 3.1" + } + } +} diff --git a/tests/fixtures/authoring-examples/modules/kubernetes_app/README.md b/tests/fixtures/authoring-examples/modules/kubernetes_app/README.md new file mode 100644 index 0000000..cb1165c --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/kubernetes_app/README.md @@ -0,0 +1,36 @@ + +## Requirements + +| Name | Version | +| - | - | +| [kubernetes](#requirement\_kubernetes) | ~> 3.2 | + +## Providers + +| Name | Version | +| - | - | +| [kubernetes](#provider\_kubernetes) | ~> 3.2 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +| - | - | +| [kubernetes_manifest.app](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| - | - | - | - | :------: | +| [field\_manager](#input\_field\_manager) | Field manager name used when applying managed fields to Kubernetes resources. | `string` | `"stacksmith"` | no | +| [manifest\_files](#input\_manifest\_files) | Paths to Kubernetes manifest YAML files to apply. | `list(string)` | n/a | yes | +| [namespace](#input\_namespace) | Kubernetes namespace for the manifests. | `string` | n/a | yes | +| [wait](#input\_wait) | Whether Terraform waits for each manifest apply operation to complete. | `bool` | `true` | no | + +## Outputs + +No outputs. + \ No newline at end of file diff --git a/tests/fixtures/authoring-examples/modules/kubernetes_app/main.tf b/tests/fixtures/authoring-examples/modules/kubernetes_app/main.tf new file mode 100644 index 0000000..2ddd93a --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/kubernetes_app/main.tf @@ -0,0 +1,33 @@ +locals { + manifests = [for path in var.manifest_files : yamldecode(file(abspath(path)))] + manifests_with_namespace = [ + for manifest in local.manifests : + merge( + manifest, + { + metadata = merge( + manifest.metadata == null ? {} : manifest.metadata, + { namespace = var.namespace } + ) + } + ) + ] +} + +resource "kubernetes_manifest" "app" { + for_each = { for idx, manifest in local.manifests_with_namespace : tostring(idx) => manifest } + + manifest = each.value + + dynamic "field_manager" { + for_each = var.field_manager != "" ? [1] : [] + content { + name = var.field_manager + } + } + + dynamic "wait" { + for_each = var.wait ? [1] : [] + content {} + } +} diff --git a/tests/fixtures/authoring-examples/modules/kubernetes_app/variables.tf b/tests/fixtures/authoring-examples/modules/kubernetes_app/variables.tf new file mode 100644 index 0000000..35fbd0b --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/kubernetes_app/variables.tf @@ -0,0 +1,21 @@ +variable "manifest_files" { + type = list(string) + description = "Paths to Kubernetes manifest YAML files to apply." +} + +variable "namespace" { + type = string + description = "Kubernetes namespace for the manifests." +} + +variable "field_manager" { + type = string + default = "stacksmith" + description = "Field manager name used when applying managed fields to Kubernetes resources." +} + +variable "wait" { + type = bool + default = true + description = "Whether Terraform waits for each manifest apply operation to complete." +} diff --git a/tests/fixtures/authoring-examples/modules/kubernetes_app/versions.tf b/tests/fixtures/authoring-examples/modules/kubernetes_app/versions.tf new file mode 100644 index 0000000..e0f1046 --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/kubernetes_app/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 3.2" + } + } +} diff --git a/tests/fixtures/authoring-examples/modules/terraform_data/README.md b/tests/fixtures/authoring-examples/modules/terraform_data/README.md new file mode 100644 index 0000000..6cd27a5 --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/terraform_data/README.md @@ -0,0 +1,49 @@ +# terraform_data component + +This module creates one credential-free Terraform `terraform_data` using the supplied input values. + +## Usage + +```hcl +module "example" { + source = "./modules/terraform_data" + + input = { + message = "hello" + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.0 | + +## Providers + +No providers. + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [terraform_data.this](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [input](#input\_input) | Values that cause the resource to be replaced or computed values to state when they change. | `map(string)` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [id](#output\_id) | ID of the terraform_data resource. | + diff --git a/tests/fixtures/authoring-examples/modules/terraform_data/main.tf b/tests/fixtures/authoring-examples/modules/terraform_data/main.tf new file mode 100644 index 0000000..c7f5e1d --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/terraform_data/main.tf @@ -0,0 +1,3 @@ +resource "terraform_data" "this" { + input = var.input +} diff --git a/tests/fixtures/authoring-examples/modules/terraform_data/outputs.tf b/tests/fixtures/authoring-examples/modules/terraform_data/outputs.tf new file mode 100644 index 0000000..35ab09e --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/terraform_data/outputs.tf @@ -0,0 +1,4 @@ +output "id" { + description = "ID of the terraform_data resource." + value = terraform_data.this.id +} diff --git a/tests/fixtures/authoring-examples/modules/terraform_data/variables.tf b/tests/fixtures/authoring-examples/modules/terraform_data/variables.tf new file mode 100644 index 0000000..dd84cbd --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/terraform_data/variables.tf @@ -0,0 +1,4 @@ +variable "input" { + description = "Values that cause the resource to be replaced or computed values to state when they change." + type = map(string) +} diff --git a/tests/fixtures/authoring-examples/modules/terraform_data/versions.tf b/tests/fixtures/authoring-examples/modules/terraform_data/versions.tf new file mode 100644 index 0000000..fd126d1 --- /dev/null +++ b/tests/fixtures/authoring-examples/modules/terraform_data/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 1.5.0" +} diff --git a/examples/shared-config-repo/README.md b/tests/fixtures/authoring-examples/shared-config-repo/README.md similarity index 100% rename from examples/shared-config-repo/README.md rename to tests/fixtures/authoring-examples/shared-config-repo/README.md diff --git a/examples/shared-config-repo/scripts/providers/aws_identity.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/providers/aws_identity.py similarity index 100% rename from examples/shared-config-repo/scripts/providers/aws_identity.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/providers/aws_identity.py diff --git a/examples/shared-config-repo/scripts/providers/configure_aws_secondary_provider.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/providers/configure_aws_secondary_provider.py similarity index 100% rename from examples/shared-config-repo/scripts/providers/configure_aws_secondary_provider.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/providers/configure_aws_secondary_provider.py diff --git a/examples/shared-config-repo/scripts/transforms/transform_bucket_name.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_bucket_name.py similarity index 100% rename from examples/shared-config-repo/scripts/transforms/transform_bucket_name.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_bucket_name.py diff --git a/examples/shared-config-repo/scripts/transforms/transform_lifecycle_rules.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_lifecycle_rules.py similarity index 100% rename from examples/shared-config-repo/scripts/transforms/transform_lifecycle_rules.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_lifecycle_rules.py diff --git a/examples/shared-config-repo/scripts/transforms/transform_s3_write_policy.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_s3_write_policy.py similarity index 100% rename from examples/shared-config-repo/scripts/transforms/transform_s3_write_policy.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_s3_write_policy.py diff --git a/examples/shared-config-repo/scripts/transforms/transform_security_group_ingress_rules.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_security_group_ingress_rules.py similarity index 100% rename from examples/shared-config-repo/scripts/transforms/transform_security_group_ingress_rules.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_security_group_ingress_rules.py diff --git a/examples/shared-config-repo/scripts/transforms/transform_tags.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_tags.py similarity index 100% rename from examples/shared-config-repo/scripts/transforms/transform_tags.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/transforms/transform_tags.py diff --git a/examples/shared-config-repo/scripts/validations/validate_aws_region.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_aws_region.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_aws_region.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_aws_region.py diff --git a/examples/shared-config-repo/scripts/validations/validate_ec2_instance_type.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_instance_type.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_ec2_instance_type.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_instance_type.py diff --git a/examples/shared-config-repo/scripts/validations/validate_ec2_requires_imdsv2.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_requires_imdsv2.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_ec2_requires_imdsv2.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_requires_imdsv2.py diff --git a/examples/shared-config-repo/scripts/validations/validate_ec2_t3_micro_warning.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_t3_micro_warning.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_ec2_t3_micro_warning.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_ec2_t3_micro_warning.py diff --git a/examples/shared-config-repo/scripts/validations/validate_s3_no_public_access.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_s3_no_public_access.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_s3_no_public_access.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_s3_no_public_access.py diff --git a/examples/shared-config-repo/scripts/validations/validate_s3_write_policy_scope.py b/tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_s3_write_policy_scope.py similarity index 100% rename from examples/shared-config-repo/scripts/validations/validate_s3_write_policy_scope.py rename to tests/fixtures/authoring-examples/shared-config-repo/scripts/validations/validate_s3_write_policy_scope.py diff --git a/examples/shared-config-repo/stacksmith-base-config.yaml b/tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml similarity index 100% rename from examples/shared-config-repo/stacksmith-base-config.yaml rename to tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml diff --git a/examples/shared-config-repo/stacksmith-config.yaml b/tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml similarity index 100% rename from examples/shared-config-repo/stacksmith-config.yaml rename to tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml diff --git a/examples/shared-config-repo/tests.yaml b/tests/fixtures/authoring-examples/shared-config-repo/tests.yaml similarity index 100% rename from examples/shared-config-repo/tests.yaml rename to tests/fixtures/authoring-examples/shared-config-repo/tests.yaml diff --git a/examples/stack-repo/stack.yaml b/tests/fixtures/authoring-examples/stack-repo/stack.yaml similarity index 100% rename from examples/stack-repo/stack.yaml rename to tests/fixtures/authoring-examples/stack-repo/stack.yaml diff --git a/examples/stack-repo/vars.dev.yaml b/tests/fixtures/authoring-examples/stack-repo/vars.dev.yaml similarity index 100% rename from examples/stack-repo/vars.dev.yaml rename to tests/fixtures/authoring-examples/stack-repo/vars.dev.yaml diff --git a/examples/stack-simple-repo/stack.yaml b/tests/fixtures/authoring-examples/stack-simple-repo/stack.yaml similarity index 100% rename from examples/stack-simple-repo/stack.yaml rename to tests/fixtures/authoring-examples/stack-simple-repo/stack.yaml diff --git a/examples/stack-simple-repo/vars.dev.yaml b/tests/fixtures/authoring-examples/stack-simple-repo/vars.dev.yaml similarity index 100% rename from examples/stack-simple-repo/vars.dev.yaml rename to tests/fixtures/authoring-examples/stack-simple-repo/vars.dev.yaml diff --git a/examples/stack-simple-repo/vars.prod.yaml b/tests/fixtures/authoring-examples/stack-simple-repo/vars.prod.yaml similarity index 100% rename from examples/stack-simple-repo/vars.prod.yaml rename to tests/fixtures/authoring-examples/stack-simple-repo/vars.prod.yaml diff --git a/tests/test_ci_api.py b/tests/test_ci_api.py index fb080e0..b1541ea 100644 --- a/tests/test_ci_api.py +++ b/tests/test_ci_api.py @@ -145,19 +145,6 @@ def test_inspect_environments_auto_discovers_env_files_layout( assert not any(record.levelno >= logging.ERROR for record in caplog.records) -def test_inspect_environments_auto_discovers_flat_files_layout(tmp_path: Path): - (tmp_path / "stacksmith.dev.yaml").write_text("stacks: []\n") - (tmp_path / "stacksmith.prod.yaml").write_text("stacks: []\n") - - payload = inspect_environments( - gitops_root=str(tmp_path), - discovery_mode="auto", - ) - - assert payload["discovery_mode"] == "flat-files" - assert payload["selected_environments"] == ["dev", "prod"] - - def test_discover_deployments_finds_root_and_nested_layouts( tmp_path: Path, monkeypatch ): @@ -181,7 +168,7 @@ def test_discover_deployments_finds_root_and_nested_layouts( @pytest.mark.parametrize( ("layout", "expected_mode"), - [("folders", "folders"), ("flat-files", "flat-files")], + [("folders", "folders")], ) def test_discover_deployments_supports_other_repository_layouts( tmp_path: Path, monkeypatch, layout: str, expected_mode: str @@ -190,8 +177,9 @@ def test_discover_deployments_supports_other_repository_layouts( (tmp_path / "common" / "stacksmith.yaml").write_text("stacks: []\n") if layout == "folders": (tmp_path / "environments" / "dev").mkdir(parents=True) - else: - (tmp_path / "stacksmith.dev.yaml").write_text("stacks: []\n") + (tmp_path / "environments" / "dev" / "stacksmith.yaml").write_text( + "stacks: []\n" + ) monkeypatch.chdir(tmp_path) payload = discover_deployments() @@ -209,9 +197,7 @@ def test_discover_deployments_ignores_generated_and_duplicate_roots( "name: ignored\ncomponents: {}\n" ) (tmp_path / "environments" / "generated").mkdir() - (tmp_path / "environments" / "generated" / "stacksmith.yaml").write_text( - "stacks: []\n" - ) + (tmp_path / "environments" / "generated" / "stack.yaml").write_text("stacks: []\n") (tmp_path / ".stacksmith" / "generated").mkdir(parents=True) (tmp_path / ".stacksmith" / "generated" / "stack.yaml").write_text( "name: ignored\ncomponents: {}\n" diff --git a/tests/test_cli.py b/tests/test_cli.py index 5a3e781..3d78c33 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -444,13 +444,13 @@ def test_ci_environments_has_gitops_flags(parser): "ci", "environments", "--gitops-root", - "./examples/gitops-repo", + "./tests/fixtures/authoring-examples/gitops-repo", "--discovery-mode", "env-files", "--event-name", "push", "--changed-path", - "examples/gitops-repo/common/stacksmith.yaml", + "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml", "--format", "json", ] @@ -458,10 +458,12 @@ def test_ci_environments_has_gitops_flags(parser): assert args.command == "ci" assert args.ci_command == "environments" - assert args.gitops_root == "./examples/gitops-repo" + assert args.gitops_root == "./tests/fixtures/authoring-examples/gitops-repo" assert args.discovery_mode == "env-files" assert args.event_name == "push" - assert args.changed_path == ["examples/gitops-repo/common/stacksmith.yaml"] + assert args.changed_path == [ + "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml" + ] assert args.format == "json" @@ -490,7 +492,7 @@ def test_ci_validate_has_workflow_flags(parser): "ci", "validate", "--gitops-root", - "examples/gitops-repo", + "tests/fixtures/authoring-examples/gitops-repo", "--discovery-mode", "folders", "--workflow-runfile", @@ -2038,24 +2040,28 @@ def test_cmd_ci_environments_emits_json(monkeypatch, parser, capsys): lambda **kwargs: { "deployments": [ { - "gitops_root": "examples/gitops-repo", + "gitops_root": "tests/fixtures/authoring-examples/gitops-repo", "requested_discovery_mode": "auto", "discovery_mode": "env-files", - "common_runfile": "examples/gitops-repo/common/stacksmith.yaml", + "common_runfile": "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml", "all_environments": ["dev", "prod"], "selected_environments": ["dev"], - "changed_paths": ["examples/gitops-repo/environments/dev.yaml"], + "changed_paths": [ + "tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml" + ], "matrix": [ { "environment": "dev", - "runfile": "examples/gitops-repo/common/stacksmith.yaml", - "environment_runfile": "examples/gitops-repo/environments/dev.yaml", + "runfile": "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml", + "environment_runfile": "tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml", } ], "source_lockfiles": { "status": "missing", "present": [], - "missing": ["examples/gitops-repo/common/stacksmith.lock.yaml"], + "missing": [ + "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.lock.yaml" + ], "missing_environments": ["dev"], }, } diff --git a/tests/test_example_scripts.py b/tests/test_example_scripts.py index 0015b01..967f76e 100644 --- a/tests/test_example_scripts.py +++ b/tests/test_example_scripts.py @@ -10,7 +10,11 @@ def test_shared_config_demonstrates_convention_based_module_mapping() -> None: config_root = ( - Path(__file__).resolve().parents[1] / "examples" / "shared-config-repo" + Path(__file__).resolve().parents[1] + / "tests" + / "fixtures" + / "authoring-examples" + / "shared-config-repo" ) config = load_config( [ @@ -27,7 +31,12 @@ def test_shared_config_demonstrates_convention_based_module_mapping() -> None: def test_shared_config_associates_web_instance_role_with_production_bucket() -> None: - examples_root = Path(__file__).resolve().parents[1] / "examples" + examples_root = ( + Path(__file__).resolve().parents[1] + / "tests" + / "fixtures" + / "authoring-examples" + ) config_root = examples_root / "shared-config-repo" config = load_config( [ @@ -53,7 +62,9 @@ def test_shared_config_associates_web_instance_role_with_production_bucket() -> def test_transform_s3_write_policy_uses_actual_bucket_arn_from_inputs() -> None: config_root = Path(__file__).resolve().parents[1] - base_path = config_root / "examples" / "shared-config-repo" + base_path = ( + config_root / "tests" / "fixtures" / "authoring-examples" / "shared-config-repo" + ) spec = TransformSpec( script={ "source": "local", diff --git a/tests/test_example_validations.py b/tests/test_example_validations.py index 6bee111..9a11aa1 100644 --- a/tests/test_example_validations.py +++ b/tests/test_example_validations.py @@ -6,7 +6,9 @@ VALIDATIONS_PATH = ( Path(__file__).resolve().parents[1] - / "examples" + / "tests" + / "fixtures" + / "authoring-examples" / "shared-config-repo" / "scripts" / "validations" diff --git a/tests/test_execution_plan_integration.py b/tests/test_execution_plan_integration.py index bcd7212..b31e365 100644 --- a/tests/test_execution_plan_integration.py +++ b/tests/test_execution_plan_integration.py @@ -163,7 +163,7 @@ def test_reviewed_apply_and_destroy_across_workspaces(tmp_path: Path, monkeypatc "data": { "path": str( Path(__file__).parents[1] - / "examples/modules/terraform_data" + / "tests/fixtures/authoring-examples/modules/terraform_data" ), }, }, diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 9d6df6f..6ba2a2d 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -102,12 +102,16 @@ def test_render_module_source_for_terraform_local_source_with_base_path(): "terraform", LocalModuleSourceReference( source="local", - data={"path": "examples/modules/helm_app"}, + data={"path": "tests/fixtures/authoring-examples/modules/helm_app"}, ), options={"base_path": "."}, ) - assert rendered == {"source": str(Path("examples/modules/helm_app").resolve())} + assert rendered == { + "source": str( + Path("tests/fixtures/authoring-examples/modules/helm_app").resolve() + ) + } assert "version" not in rendered @@ -118,11 +122,11 @@ def test_render_module_source_for_terraform_local_package_subdir_with_base_path( source="local", data={"path": "../modules//cert-manager"}, ), - options={"base_path": "examples/envs/prod"}, + options={"base_path": "tests/fixtures/authoring-examples/envs/prod"}, ) assert rendered == { - "source": f"{Path('examples/envs/modules').resolve()}//cert-manager" + "source": f"{Path('tests/fixtures/authoring-examples/envs/modules').resolve()}//cert-manager" } assert "version" not in rendered @@ -133,10 +137,13 @@ def test_render_module_source_identity_preserves_local_package_subdir(): source="local", data={"path": "../modules//cert-manager"}, ), - options={"base_path": "examples/envs/prod"}, + options={"base_path": "tests/fixtures/authoring-examples/envs/prod"}, ) - assert source == f"{Path('examples/envs/modules').resolve()}//cert-manager" + assert ( + source + == f"{Path('tests/fixtures/authoring-examples/envs/modules').resolve()}//cert-manager" + ) assert version == "local" diff --git a/tests/test_generator.py b/tests/test_generator.py index c06a282..3aefb9d 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -81,7 +81,11 @@ def _disable_auto_inject_inputs(config) -> None: def _load_shared_example_config(): config_root = ( - Path(__file__).resolve().parents[1] / "examples" / "shared-config-repo" + Path(__file__).resolve().parents[1] + / "tests" + / "fixtures" + / "authoring-examples" + / "shared-config-repo" ) return load_config( [ @@ -1671,7 +1675,9 @@ def test_local_module_source_uses_direct_path_without_version( mapping_data = config.module_mappings["aws_s3_bucket"].model_dump() mapping_data["source"] = { "source": "local", - "data": {"path": "../../examples/modules/helm_app"}, + "data": { + "path": "../../tests/fixtures/authoring-examples/modules/helm_app" + }, } config.module_mappings["aws_s3_bucket"] = ModuleMapping.model_validate( mapping_data @@ -1696,7 +1702,7 @@ def test_local_module_source_uses_direct_path_without_version( ( Path(__file__).resolve().parent / "fixtures" - / "../../examples/modules/helm_app" + / "../../tests/fixtures/authoring-examples/modules/helm_app" ).resolve() ) assert "version" not in bucket_mod @@ -1712,8 +1718,12 @@ def test_local_module_version_property_is_mapped_to_chart_version( config = load_config( [ - Path("examples/shared-config-repo/stacksmith-base-config.yaml"), - Path("examples/shared-config-repo/stacksmith-config.yaml"), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml" + ), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml" + ), ] ) config.module_mappings["helm_app"].properties = { @@ -1744,13 +1754,13 @@ def test_module_path_inputs_are_normalized_to_absolute_paths(self, tmp_path: Pat ' version: "4.11.3"\n' " namespace: default\n" " values_files:\n" - " - examples/gitops-repo/manifests/environments/dev/frontend-values.yaml\n" + " - tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/frontend-values.yaml\n" " app_config:\n" " type: k8s_app\n" " properties:\n" " namespace: default\n" " manifest_files:\n" - " - examples/gitops-repo/manifests/environments/dev/app-config.yaml\n" + " - tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/app-config.yaml\n" ), encoding="utf-8", ) @@ -1758,8 +1768,12 @@ def test_module_path_inputs_are_normalized_to_absolute_paths(self, tmp_path: Pat stack = load_stack(stack_path) config = load_config( [ - Path("examples/shared-config-repo/stacksmith-base-config.yaml"), - Path("examples/shared-config-repo/stacksmith-config.yaml"), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml" + ), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml" + ), ] ) diff --git a/tests/test_gitops.py b/tests/test_gitops.py index 219fd31..0f9468a 100644 --- a/tests/test_gitops.py +++ b/tests/test_gitops.py @@ -3,7 +3,6 @@ import pytest from stacksmith.ci.selection import ( - discover_environments, evaluate_environment_selection, normalize_discovery_mode, select_changed_environments, @@ -19,8 +18,10 @@ def _create_env_files_layout(tmp_path: Path) -> None: (tmp_path / "environments" / "prod.yaml").write_text("stacks: []\n") -def test_normalize_discovery_mode_env_alias(): - assert normalize_discovery_mode("env") == "env-files" +@pytest.mark.parametrize("mode", ["env", "flat-files"]) +def test_normalize_discovery_mode_rejects_removed_modes(mode): + with pytest.raises(ValueError): + normalize_discovery_mode(mode) def test_validate_discovery_mode_rejects_unsupported_value(): @@ -41,27 +42,14 @@ def test_evaluate_environment_selection_auto_detects_env_files_layout(tmp_path: assert selection.selected_environments == ["dev", "prod"] -def test_discover_environments_flat_files_filters_common_variants(tmp_path: Path): - (tmp_path / "common").mkdir() - (tmp_path / "common" / "stacksmith.yaml").write_text("merge_mode: deep\n") - (tmp_path / "stacksmith.dev.yaml").write_text("stacks: []\n") - (tmp_path / "stacksmith.prod.yaml").write_text("stacks: []\n") - (tmp_path / "stacksmith.common.yaml").write_text("stacks: []\n") - - environments, common_runfile = discover_environments("flat-files", tmp_path) - - assert environments == ["dev", "prod"] - assert common_runfile == f"{tmp_path.as_posix()}/common/stacksmith.yaml" - - def test_select_changed_environments_fan_out_for_common_changes(): selected = select_changed_environments( - ["examples/gitops-repo/common/stacksmith.yaml"], + ["tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml"], "env-files", - "examples/gitops-repo/", + "tests/fixtures/authoring-examples/gitops-repo/", ( - "examples/gitops-repo/common/", - "examples/gitops-repo/manifests/common/", + "tests/fixtures/authoring-examples/gitops-repo/common/", + "tests/fixtures/authoring-examples/gitops-repo/manifests/common/", ), ["dev", "prod"], ) @@ -71,12 +59,12 @@ def test_select_changed_environments_fan_out_for_common_changes(): def test_select_changed_environments_targets_env_file(): selected = select_changed_environments( - ["examples/gitops-repo/environments/dev.yaml"], + ["tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml"], "env-files", - "examples/gitops-repo/", + "tests/fixtures/authoring-examples/gitops-repo/", ( - "examples/gitops-repo/common/", - "examples/gitops-repo/manifests/common/", + "tests/fixtures/authoring-examples/gitops-repo/common/", + "tests/fixtures/authoring-examples/gitops-repo/manifests/common/", ), ["dev", "prod"], ) @@ -84,29 +72,16 @@ def test_select_changed_environments_targets_env_file(): assert selected == {"dev"} -def test_select_changed_environments_targets_flat_file_env(): - selected = select_changed_environments( - ["examples/gitops-repo/stacksmith.prod.yaml"], - "flat-files", - "examples/gitops-repo/", - ( - "examples/gitops-repo/common/", - "examples/gitops-repo/manifests/common/", - ), - ["dev", "prod"], - ) - - assert selected == {"prod"} - - def test_select_changed_environments_targets_environment_manifest_path(): selected = select_changed_environments( - ["examples/gitops-repo/manifests/environments/prod/app-config.yaml"], + [ + "tests/fixtures/authoring-examples/gitops-repo/manifests/environments/prod/app-config.yaml" + ], "env-files", - "examples/gitops-repo/", + "tests/fixtures/authoring-examples/gitops-repo/", ( - "examples/gitops-repo/common/", - "examples/gitops-repo/manifests/common/", + "tests/fixtures/authoring-examples/gitops-repo/common/", + "tests/fixtures/authoring-examples/gitops-repo/manifests/common/", ), ["dev", "prod"], ) @@ -320,3 +295,16 @@ def test_single_deployment_accepts_direct_root_stack( "stack": f"stack.{extension}", } ] + + +def test_auto_rejects_mixed_environment_layouts(tmp_path: Path): + _create_env_files_layout(tmp_path) + (tmp_path / "environments" / "staging").mkdir() + (tmp_path / "environments" / "staging" / "stacksmith.yaml").write_text( + "stacks: []\n" + ) + with pytest.raises(ValueError, match="Ambiguous deployment layout"): + evaluate_environment_selection(str(tmp_path), "auto") + assert evaluate_environment_selection( + str(tmp_path), "env-files", event_name="workflow_dispatch" + ).all_environments == ["dev", "prod"] diff --git a/tests/test_introspection.py b/tests/test_introspection.py index 99d976d..3af9683 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -131,7 +131,14 @@ def test_ignores_nested_module_outputs(self, tmp_path: Path): class TestResolveModuleDir: def test_resolves_local_module_dir_without_git_clone(self, tmp_path: Path): - module_dir = tmp_path / "examples" / "modules" / "helm_app" + module_dir = ( + tmp_path + / "tests" + / "fixtures" + / "authoring-examples" + / "modules" + / "helm_app" + ) module_dir.mkdir(parents=True) (module_dir / "variables.tf").write_text( 'variable "name" {\n type = string\n}\n', @@ -141,7 +148,7 @@ def test_resolves_local_module_dir_without_git_clone(self, tmp_path: Path): source, version = render_module_source_identity( LocalModuleSourceReference( source="local", - data={"path": "examples/modules/helm_app"}, + data={"path": "tests/fixtures/authoring-examples/modules/helm_app"}, ), options={"base_path": tmp_path}, ) diff --git a/tests/test_merging.py b/tests/test_merging.py index 8dbc822..8fb09a6 100644 --- a/tests/test_merging.py +++ b/tests/test_merging.py @@ -262,7 +262,8 @@ def test_external_policy_can_control_runfile_merge(tmp_path: Path): def test_gitops_example_overrides_environment_values_files(): runfile = load_runfile( - Path(__file__).parents[1] / "examples/gitops-repo/common/stacksmith.yaml" + Path(__file__).parents[1] + / "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml" ) assert load_stacks( @@ -279,12 +280,14 @@ def test_gitops_example_overrides_environment_values_files(): } }, ).components["frontend_release"].properties["values_files"] == [ - "examples/gitops-repo/manifests/environments/dev/frontend-values.yaml" + "tests/fixtures/authoring-examples/gitops-repo/manifests/environments/dev/frontend-values.yaml" ] def test_gitops_example_wires_pinned_commit_to_jenkins_operation(): - examples_root = Path(__file__).parents[1] / "examples" + examples_root = ( + Path(__file__).parents[1] / "tests" / "fixtures" / "authoring-examples" + ) runfile = load_runfile(examples_root / "gitops-repo/common/stacksmith.yaml") application_commit = yaml.safe_load( (examples_root / "gitops-repo/vars/vars.dev.yaml").read_text(encoding="utf-8") @@ -323,7 +326,9 @@ def test_gitops_example_wires_pinned_commit_to_jenkins_operation(): def test_simple_gitops_example_wires_after_apply_echo_operation(monkeypatch): monkeypatch.setenv("STACKSMITH_EXAMPLE_SECRET", "example-secret-DO-NOT-LEAK") monkeypatch.setenv("STACKSMITH_SECRET_FINGERPRINT_KEY", "test-key") - examples_root = Path(__file__).parents[1] / "examples" + examples_root = ( + Path(__file__).parents[1] / "tests" / "fixtures" / "authoring-examples" + ) runfile = load_runfile(examples_root / "gitops-simple-repo/common/stacksmith.yaml") stack = load_stacks( [Path(reference.data.path) for reference in runfile.stacks], diff --git a/tests/test_release_pins.py b/tests/test_release_pins.py index 53b1f10..a08e375 100644 --- a/tests/test_release_pins.py +++ b/tests/test_release_pins.py @@ -17,7 +17,9 @@ def _pin_checkout(tmp_path: Path) -> Path: _ROOT / ".github/workflows/stacksmith-gitops-opinionated-reusable.yml", _ROOT / ".github/workflows/stacksmith-gitops-reusable.yml", _ROOT / "jenkins/vars/stacksmith.groovy", - *(_ROOT / "examples/github-actions").glob("stacksmith-*.yml"), + *(_ROOT / "tests/fixtures/authoring-examples/github-actions").glob( + "stacksmith-*.yml" + ), _ROOT / "docs/integrations/ci.md", ): target = tmp_path / path.relative_to(_ROOT) @@ -66,7 +68,9 @@ def test_pin_task_coordinates_workflow_image_and_library_refs( assert ( executor["on"]["workflow_call"]["inputs"]["image_version"]["default"] == version ) - for path in (_pin_checkout / "examples/github-actions").glob("stacksmith-*.yml"): + for path in ( + _pin_checkout / "tests/fixtures/authoring-examples/github-actions" + ).glob("stacksmith-*.yml"): workflow = yaml.safe_load(path.read_text()) assert all( job["uses"].endswith(f"@v{version}") for job in workflow["jobs"].values() diff --git a/tests/test_schema_sync.py b/tests/test_schema_sync.py index eab5c97..29effdc 100644 --- a/tests/test_schema_sync.py +++ b/tests/test_schema_sync.py @@ -503,31 +503,43 @@ def test_generated_layer_schema_matches_runtime_fragment_schema( ( "config.layer.schema.json", [ - Path("examples/shared-config-repo/stacksmith-base-config.yaml"), - Path("examples/shared-config-repo/stacksmith-config.yaml"), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-base-config.yaml" + ), + Path( + "tests/fixtures/authoring-examples/shared-config-repo/stacksmith-config.yaml" + ), ], ), ( "runfile.layer.schema.json", [ - Path("examples/gitops-repo/common/stacksmith.yaml"), - Path("examples/gitops-repo/environments/dev.yaml"), + Path( + "tests/fixtures/authoring-examples/gitops-repo/common/stacksmith.yaml" + ), + Path( + "tests/fixtures/authoring-examples/gitops-repo/environments/dev.yaml" + ), ], ), ( "stack.layer.schema.json", [ - Path("examples/gitops-repo/manifests/common/platform.stack.yaml"), - Path("examples/gitops-repo/manifests/common/service.stack.yaml"), + Path( + "tests/fixtures/authoring-examples/gitops-repo/manifests/common/platform.stack.yaml" + ), + Path( + "tests/fixtures/authoring-examples/gitops-repo/manifests/common/service.stack.yaml" + ), ], ), ( "test_manifest.layer.schema.json", - [Path("examples/shared-config-repo/tests.yaml")], + [Path("tests/fixtures/authoring-examples/shared-config-repo/tests.yaml")], ), ( "vars.layer.schema.json", - [Path("examples/gitops-repo/vars/vars.dev.yaml")], + [Path("tests/fixtures/authoring-examples/gitops-repo/vars/vars.dev.yaml")], ), ], ) diff --git a/zensical.toml b/zensical.toml index ab58a06..a4d04fc 100644 --- a/zensical.toml +++ b/zensical.toml @@ -9,6 +9,7 @@ nav = [ { "Getting started" = "getting-started.md" }, { "Core concepts" = "concepts.md" }, { "Guides" = [ + { "Repository layouts" = "guides/repository-layouts.md" }, { "Stack authoring" = "guides/stack-authoring.md" }, { "Managed configuration" = "guides/managed-configuration.md" }, { "Execution and orchestration" = "guides/execution.md" }, From f709f67abbbc2290ad1a07ba66ad318e3a4ed3d7 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Wed, 4 Nov 2026 19:31:24 -0500 Subject: [PATCH 3/7] docs(ci): clarify retained discovery settings --- docs/integrations/ci.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/ci.md b/docs/integrations/ci.md index f8abe2b..d943f63 100644 --- a/docs/integrations/ci.md +++ b/docs/integrations/ci.md @@ -76,7 +76,7 @@ In your own repository, you can do either of the following. The wrappers pass reusable workflow inputs from repository variables when available. - `STACKSMITH_GITOPS_ROOT` (default `.`) -- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). +- `STACKSMITH_DISCOVERY_MODE` (default `auto`; supported values: `single`, `folders`, and `env-files`) - `STACKSMITH_WORKDIR` (default `.`) - `STACKSMITH_ENV_FILE` (default `/dev/null`) - `STACKSMITH_IMAGE_VERSION` (default `latest`) @@ -205,7 +205,7 @@ jobs: > ℹ️ **Tip:** Pin the `uses:` reference to a release tag for stable downstream usage. -- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). +See the [repository layout guide](../guides/repository-layouts.md) to choose between single deployments, environment files, and environment directories. ## Jenkins @@ -254,7 +254,7 @@ Configure these values as Jenkins folder properties or job environment variables - `STACKSMITH_IMAGE_VERSION`: Image tag when `STACKSMITH_IMAGE` is unset. Defaults to `latest`. - `STACKSMITH_TEST_PIPELINE`: Set to a truthy value to make the job a test-only pipeline. The `COMMAND` parameter is not exposed in this mode and cannot override the test command. - `STACKSMITH_GITOPS_ROOT`: GitOps root for discovery. Defaults to `WORKDIR` in Jenkins. -- `discovery_mode` supports `auto`, `single`, `folders`, and `env-files`. See the [repository layout guide](../guides/repository-layouts.md). +- `STACKSMITH_DISCOVERY_MODE`: `auto`, `single`, `folders`, or `env-files`. Defaults to `auto`. - `STACKSMITH_MAX_PARALLEL_OPERATIONS`: Maximum independent operations planned or run concurrently within each environment. Defaults to `10`. - `STACKSMITH_ENV_FILE`: Env file passed to Stacksmith. Defaults to `/dev/null` to prevent implicit `.env` loading. - `STACKSMITH_CONFIG_REF`: Required platform-managed Stacksmith config reference. From 1fa3cb2159c077dcd1d5f54c17be2f22bfa15361 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Wed, 4 Nov 2026 19:32:00 -0500 Subject: [PATCH 4/7] chore(examples): remove unused showcase module copies --- examples/modules/README.md | 57 +------------------- examples/modules/helm_app/README.md | 41 -------------- examples/modules/helm_app/main.tf | 17 ------ examples/modules/helm_app/outputs.tf | 4 -- examples/modules/helm_app/variables.tf | 49 ----------------- examples/modules/helm_app/versions.tf | 8 --- examples/modules/kubernetes_app/README.md | 36 ------------- examples/modules/kubernetes_app/main.tf | 33 ------------ examples/modules/kubernetes_app/variables.tf | 21 -------- examples/modules/kubernetes_app/versions.tf | 8 --- 10 files changed, 2 insertions(+), 272 deletions(-) delete mode 100644 examples/modules/helm_app/README.md delete mode 100644 examples/modules/helm_app/main.tf delete mode 100644 examples/modules/helm_app/outputs.tf delete mode 100644 examples/modules/helm_app/variables.tf delete mode 100644 examples/modules/helm_app/versions.tf delete mode 100644 examples/modules/kubernetes_app/README.md delete mode 100644 examples/modules/kubernetes_app/main.tf delete mode 100644 examples/modules/kubernetes_app/variables.tf delete mode 100644 examples/modules/kubernetes_app/versions.tf diff --git a/examples/modules/README.md b/examples/modules/README.md index 98fcdf3..59b9888 100644 --- a/examples/modules/README.md +++ b/examples/modules/README.md @@ -1,56 +1,3 @@ -# Shared GitOps example modules +# Tutorial modules -This directory contains shared Stacksmith module implementations for app deployment types that a platform team can expose through a managed config. - -These modules are intentionally simplified examples. - -- `helm_app` models a Helm-style app deployment path using `helm_release`. -- `k8s_app` models a Kubernetes manifest delivery path using `kubernetes_manifest`. -- `terraform_data` creates a credential-free placeholder for the simple GitOps example. - -They demonstrate how Stacksmith component types can be mapped to platform-approved modules. Note that `helm_app` and `kubernetes_app` are shown here as modules, but they could also be implemented as [native operations](../../README.md#native-operations) instead; it is purely a platform team preference. Modules are declarative and benefit from OpenTofu state management and dependency tracking. Operations are imperative actions that are useful for commands or workflows that don't map cleanly to infrastructure code. For app deployments like these, modules are typically the better choice, because they have first-class OpenTofu provider support, but the architecture is flexible. - -These modules are referenced by the canonical [GitOps example](../gitops-repo) through its [managed config](../shared-config-repo/stacksmith-config.yaml). - -## Usage - -Use these module sources in `stacksmith-config.yaml` module mappings and then refer to them from stack components by `type`. - -Example module mapping. - -```yaml -module_mappings: - helm_app: - source: - source: local - data: - path: examples/modules/helm_app -``` - -Example component. - -```yaml -components: - frontend: - type: helm_app - properties: - name: frontend-release - chart: ingress-nginx - repository: https://kubernetes.github.io/ingress-nginx - version: "4.11.3" - namespace: web - values_files: - - frontend-values.dev.yaml -``` - -Example Kubernetes manifest component. - -```yaml -components: - app_config: - type: k8s_app - properties: - namespace: default - manifest_files: - - app-config.dev.yaml -``` +`terraform_data` provides the same credential-free workload for the local layout tutorials. Complex module and authoring fixtures live under `tests/fixtures/authoring-examples`. diff --git a/examples/modules/helm_app/README.md b/examples/modules/helm_app/README.md deleted file mode 100644 index ad7b7ea..0000000 --- a/examples/modules/helm_app/README.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Requirements - -| Name | Version | -| - | - | -| [helm](#requirement\_helm) | ~> 3.1 | - -## Providers - -| Name | Version | -| - | - | -| [helm](#provider\_helm) | ~> 3.1 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -| - | - | -| [helm_release.app](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -| - | - | - | - | :------: | -| [atomic](#input\_atomic) | Whether rollbacks are attempted if the Helm release deployment fails. | `bool` | `true` | no | -| [chart](#input\_chart) | Helm chart name to deploy. | `string` | n/a | yes | -| [chart\_version](#input\_chart\_version) | Helm chart version. | `string` | n/a | yes | -| [create\_namespace](#input\_create\_namespace) | Whether to create the target namespace if it does not already exist. | `bool` | `false` | no | -| [name](#input\_name) | Optional Helm release name. When omitted, a default name is derived from the chart and namespace. | `string` | `null` | no | -| [namespace](#input\_namespace) | Target Kubernetes namespace for the Helm release. | `string` | n/a | yes | -| [repository](#input\_repository) | Helm chart repository URL. | `string` | n/a | yes | -| [values\_files](#input\_values\_files) | List of Helm values YAML files to render into the chart deployment. | `list(string)` | `[]` | no | -| [wait](#input\_wait) | Whether Terraform waits for the Helm release to become ready. | `bool` | `true` | no | - -## Outputs - -No outputs. - \ No newline at end of file diff --git a/examples/modules/helm_app/main.tf b/examples/modules/helm_app/main.tf deleted file mode 100644 index 58e10d7..0000000 --- a/examples/modules/helm_app/main.tf +++ /dev/null @@ -1,17 +0,0 @@ -locals { - derived_release_name = replace(var.chart, "/", "-") - release_name = coalesce(var.name, "${var.namespace}-${local.derived_release_name}") - values = [for values_file in var.values_files : file(abspath(values_file))] -} - -resource "helm_release" "app" { - name = local.release_name - chart = var.chart - repository = var.repository - version = var.chart_version - namespace = var.namespace - create_namespace = var.create_namespace - wait = var.wait - atomic = var.atomic - values = local.values -} diff --git a/examples/modules/helm_app/outputs.tf b/examples/modules/helm_app/outputs.tf deleted file mode 100644 index afbe47b..0000000 --- a/examples/modules/helm_app/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "release_name" { - description = "Deployed Helm release name." - value = helm_release.app.name -} diff --git a/examples/modules/helm_app/variables.tf b/examples/modules/helm_app/variables.tf deleted file mode 100644 index 05a527c..0000000 --- a/examples/modules/helm_app/variables.tf +++ /dev/null @@ -1,49 +0,0 @@ -variable "name" { - type = string - default = null - description = "Optional Helm release name. When omitted, a default name is derived from the chart and namespace." -} - -variable "chart" { - type = string - description = "Helm chart name to deploy." -} - -variable "repository" { - type = string - description = "Helm chart repository URL." -} - -variable "chart_version" { - type = string - description = "Helm chart version." -} - -variable "namespace" { - type = string - description = "Target Kubernetes namespace for the Helm release." -} - -variable "create_namespace" { - type = bool - default = false - description = "Whether to create the target namespace if it does not already exist." -} - -variable "wait" { - type = bool - default = true - description = "Whether Terraform waits for the Helm release to become ready." -} - -variable "atomic" { - type = bool - default = true - description = "Whether rollbacks are attempted if the Helm release deployment fails." -} - -variable "values_files" { - type = list(string) - default = [] - description = "List of Helm values YAML files to render into the chart deployment." -} diff --git a/examples/modules/helm_app/versions.tf b/examples/modules/helm_app/versions.tf deleted file mode 100644 index 6523d8a..0000000 --- a/examples/modules/helm_app/versions.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_providers { - helm = { - source = "hashicorp/helm" - version = "~> 3.1" - } - } -} diff --git a/examples/modules/kubernetes_app/README.md b/examples/modules/kubernetes_app/README.md deleted file mode 100644 index cb1165c..0000000 --- a/examples/modules/kubernetes_app/README.md +++ /dev/null @@ -1,36 +0,0 @@ - -## Requirements - -| Name | Version | -| - | - | -| [kubernetes](#requirement\_kubernetes) | ~> 3.2 | - -## Providers - -| Name | Version | -| - | - | -| [kubernetes](#provider\_kubernetes) | ~> 3.2 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -| - | - | -| [kubernetes_manifest.app](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -| - | - | - | - | :------: | -| [field\_manager](#input\_field\_manager) | Field manager name used when applying managed fields to Kubernetes resources. | `string` | `"stacksmith"` | no | -| [manifest\_files](#input\_manifest\_files) | Paths to Kubernetes manifest YAML files to apply. | `list(string)` | n/a | yes | -| [namespace](#input\_namespace) | Kubernetes namespace for the manifests. | `string` | n/a | yes | -| [wait](#input\_wait) | Whether Terraform waits for each manifest apply operation to complete. | `bool` | `true` | no | - -## Outputs - -No outputs. - \ No newline at end of file diff --git a/examples/modules/kubernetes_app/main.tf b/examples/modules/kubernetes_app/main.tf deleted file mode 100644 index 2ddd93a..0000000 --- a/examples/modules/kubernetes_app/main.tf +++ /dev/null @@ -1,33 +0,0 @@ -locals { - manifests = [for path in var.manifest_files : yamldecode(file(abspath(path)))] - manifests_with_namespace = [ - for manifest in local.manifests : - merge( - manifest, - { - metadata = merge( - manifest.metadata == null ? {} : manifest.metadata, - { namespace = var.namespace } - ) - } - ) - ] -} - -resource "kubernetes_manifest" "app" { - for_each = { for idx, manifest in local.manifests_with_namespace : tostring(idx) => manifest } - - manifest = each.value - - dynamic "field_manager" { - for_each = var.field_manager != "" ? [1] : [] - content { - name = var.field_manager - } - } - - dynamic "wait" { - for_each = var.wait ? [1] : [] - content {} - } -} diff --git a/examples/modules/kubernetes_app/variables.tf b/examples/modules/kubernetes_app/variables.tf deleted file mode 100644 index 35fbd0b..0000000 --- a/examples/modules/kubernetes_app/variables.tf +++ /dev/null @@ -1,21 +0,0 @@ -variable "manifest_files" { - type = list(string) - description = "Paths to Kubernetes manifest YAML files to apply." -} - -variable "namespace" { - type = string - description = "Kubernetes namespace for the manifests." -} - -variable "field_manager" { - type = string - default = "stacksmith" - description = "Field manager name used when applying managed fields to Kubernetes resources." -} - -variable "wait" { - type = bool - default = true - description = "Whether Terraform waits for each manifest apply operation to complete." -} diff --git a/examples/modules/kubernetes_app/versions.tf b/examples/modules/kubernetes_app/versions.tf deleted file mode 100644 index e0f1046..0000000 --- a/examples/modules/kubernetes_app/versions.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_providers { - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 3.2" - } - } -} From eed764577a6fb88c93c6e026ea67aebba6e81baf Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Wed, 4 Nov 2026 19:33:50 -0500 Subject: [PATCH 5/7] docs(discovery): describe mode validation and ambiguity errors --- src/stacksmith/ci/selection.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/stacksmith/ci/selection.py b/src/stacksmith/ci/selection.py index ff344ab..8e516a4 100644 --- a/src/stacksmith/ci/selection.py +++ b/src/stacksmith/ci/selection.py @@ -92,13 +92,16 @@ def normalize_gitops_root(value: str) -> str: def normalize_discovery_mode(value: str) -> DiscoveryMode: - """Normalize discovery mode aliases used by GitOps workflows. + """Normalize a supported GitOps discovery mode. Args: value: Raw mode input. Returns: Canonical discovery mode. + + Raises: + ValueError: If the mode is unsupported. """ mode = value.strip().lower() or DiscoveryMode.FOLDERS.value normalized = DiscoveryMode(mode) @@ -128,7 +131,18 @@ def validate_discovery_mode(value: str) -> DiscoveryMode: def resolve_discovery_mode(mode: DiscoveryMode, root: Path) -> DiscoveryMode: - """Resolve discovery mode from the GitOps layout when set to `auto`.""" + """Resolve discovery mode from the GitOps layout when set to `auto`. + + Args: + mode: Requested discovery mode. + root: Deployment root to inspect. + + Returns: + Explicit or automatically detected discovery mode. + + Raises: + ValueError: If automatic discovery finds competing environment layouts. + """ if mode != DiscoveryMode.AUTO: return mode From c8f2d2cfdf0c85216db0644bd5e2d04fc3835a82 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Thu, 5 Nov 2026 04:26:23 -0500 Subject: [PATCH 6/7] refactor(discovery): inline normalized mode input --- src/stacksmith/ci/selection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stacksmith/ci/selection.py b/src/stacksmith/ci/selection.py index 8e516a4..aebcd6e 100644 --- a/src/stacksmith/ci/selection.py +++ b/src/stacksmith/ci/selection.py @@ -103,8 +103,7 @@ def normalize_discovery_mode(value: str) -> DiscoveryMode: Raises: ValueError: If the mode is unsupported. """ - mode = value.strip().lower() or DiscoveryMode.FOLDERS.value - normalized = DiscoveryMode(mode) + normalized = DiscoveryMode(value.strip().lower() or DiscoveryMode.FOLDERS.value) LOGGER.debug("Normalized discovery mode %r -> %r", value, normalized) return normalized From e34e3d26287165e12a3f6653f74c2194a5634462 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Thu, 5 Nov 2026 04:29:37 -0500 Subject: [PATCH 7/7] fix(ci): include directory environment runfiles in execution rows --- src/stacksmith/ci/selection.py | 10 +++++++--- tests/test_gitops.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/stacksmith/ci/selection.py b/src/stacksmith/ci/selection.py index aebcd6e..1f32f2d 100644 --- a/src/stacksmith/ci/selection.py +++ b/src/stacksmith/ci/selection.py @@ -53,10 +53,14 @@ def matrix(self) -> list[dict[str, str]]: for environment in self.selected_environments: runfile = self.common_runfile environment_runfile = "" - if self.discovery_mode == "env-files": + if self.discovery_mode in {"env-files", "folders"}: root = Path(self.gitops_root or ".") for ext in DISCOVERY_FILE_EXTENSIONS: - env_file = root / "environments" / f"{environment}.{ext}" + env_file = ( + root / "environments" / f"{environment}.{ext}" + if self.discovery_mode == "env-files" + else root / "environments" / environment / f"stacksmith.{ext}" + ) if env_file.exists(): environment_runfile = env_file.as_posix() break @@ -249,7 +253,7 @@ def discover_environments(mode: DiscoveryMode, root: Path) -> tuple[list[str], s all_envs.append(env_dir.name) LOGGER.debug("Discovered folder environments: %s", all_envs) - return all_envs, "common/stacksmith.yaml" + return all_envs, common_run_file_for(root) def parse_manual_environments(value: str) -> set[str]: diff --git a/tests/test_gitops.py b/tests/test_gitops.py index 0f9468a..f89fcc8 100644 --- a/tests/test_gitops.py +++ b/tests/test_gitops.py @@ -308,3 +308,17 @@ def test_auto_rejects_mixed_environment_layouts(tmp_path: Path): assert evaluate_environment_selection( str(tmp_path), "env-files", event_name="workflow_dispatch" ).all_environments == ["dev", "prod"] + + +def test_folder_matrix_includes_environment_runfile(tmp_path: Path): + (tmp_path / "common").mkdir() + (tmp_path / "common/stacksmith.yaml").write_text("stacks: []\n") + (tmp_path / "environments/dev").mkdir(parents=True) + (tmp_path / "environments/dev/stacksmith.yaml").write_text("vars: []\n") + selection = evaluate_environment_selection( + str(tmp_path), "auto", event_name="workflow_dispatch" + ) + assert selection.matrix[0]["runfile"] == str(tmp_path / "common/stacksmith.yaml") + assert selection.matrix[0]["environment_runfile"] == str( + tmp_path / "environments/dev/stacksmith.yaml" + )