diff --git a/module-ci-action/README.md b/module-ci-action/README.md index 01698b8..fdb72f4 100644 --- a/module-ci-action/README.md +++ b/module-ci-action/README.md @@ -79,6 +79,7 @@ modules live at `infra/modules/...`). | `all-modules` | no | `false` | When `true`, operate on **every** module under `modules/` instead of only the ones the event changed. | | `mode` | no | `auto` | `auto` \| `preview` \| `publish` \| `cleanup`. `auto` derives the mode from the event (see the table above). Set explicitly to override. | | `path-prefix` | no | `""` | Sub-path to the `modules/` tree relative to the repo root (e.g. `infra/`). Empty means `modules/` is at the root. | +| `auto-create-intents` | no | `true` | Pass `--auto-create` to raptor, so a module whose **intent** the Control Plane has never seen registers it. Without this the upload fails `404 Intent not found`. See **Intents** below before turning it off. | ### Secrets @@ -92,6 +93,42 @@ modules repo. They are exported as the environment variables `raptor` reads dire | `FACETS_USERNAME` | `FACETS_USERNAME` | Username | | `FACETS_TOKEN` | `FACETS_TOKEN` | API token | + +## Intents + +A module's `intent` (its `kind`) is a **shared** object on the Control Plane: one +intent, many flavors. `postgres` is an intent; `gcp-cloudsql` and `aws-rds` are +flavors of it. Nothing in a modules repo creates an intent implicitly, and +`raptor create resource-type-mapping` rejects one it does not know — so the first +module of a new intent fails the upload outright: + +``` +Error: upload failed with status 404: {"message":"Intent gcs not found","code":"404"} +``` + +`auto-create-intents` (default `true`) passes raptor's `--auto-create` on the two +steps that upload — preview registration and publish. It is deliberately **not** +passed to the `--dry-run` validation, which never touches the intent. + +**The flag does two things, and the second one is shared state.** Besides creating +a missing intent, `--auto-create` rewrites an existing intent's metadata from the +uploading module's `intentDetails`. Because every flavor of an intent writes the +same object, two flavors that disagree will fight, and the last module published +wins: + +```yaml +# modules/cloud_account/gcp_provider/1.0/facets.yaml +intentDetails: { displayName: Cloud Account, ... } + +# modules/cloud_account/gcp_org_provider/1.0/facets.yaml +intentDetails: { displayName: GCP Org Cloud Account, ... } # same intent! +``` + +Keep `intentDetails` identical across the flavors of one intent, and treat a +difference as a repo bug. Set `auto-create-intents: 'false'` to freeze intent +metadata instead — at the cost of failing on any intent the Control Plane has not +seen before. + ## Example workflow A single workflow wiring up all three triggers: diff --git a/module-ci-action/action.yml b/module-ci-action/action.yml index beb80ed..6dff594 100644 --- a/module-ci-action/action.yml +++ b/module-ci-action/action.yml @@ -44,6 +44,10 @@ inputs: description: "Sub-path to the modules/ tree, relative to the repo root (e.g. 'infra/' when modules live at infra/modules/...). Empty means modules/ is at the repo root." required: false default: "" + auto-create-intents: + description: "When 'true' (default), pass --auto-create to raptor so a module whose intent does not yet exist on the Control Plane creates it. Without this, the first module of a new intent fails with 404 'Intent not found' and a modules repo cannot bootstrap a fresh Control Plane. NOTE: --auto-create also rewrites an EXISTING intent's metadata from this module's intentDetails, and an intent is shared by all its flavors — so when two flavors disagree, the last one published wins. Set to 'false' to keep intent metadata frozen, at the cost of failing on any new intent." + required: false + default: "true" runs: using: "composite" @@ -256,6 +260,7 @@ runs: FACETS_USERNAME: ${{ inputs.username }} FACETS_TOKEN: ${{ inputs.token }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + AUTO_CREATE_INTENTS: ${{ inputs.auto-create-intents }} # Trivy runs inside raptor's module validation, AFTER terraform init has # vendored remote deps into .terraform/. Trivy reads flags from TRIVY_* # env vars; skip the vendored tree so findings in dependency code the @@ -284,6 +289,17 @@ runs: echo "|---|---|---|---|---|" } > "$COMMENT" + # --auto-create makes raptor register the module's intent when the Control + # Plane has never seen that kind. Without it the upload 404s ("Intent + # not found"), which is what a modules repo hits the first time it + # carries a new intent — nothing else creates one: resource-type-mapping + # rejects unknown intents outright. The flag is NOT passed to --dry-run, + # which never touches the intent. + AUTO_CREATE="" + if [ "${AUTO_CREATE_INTENTS:-true}" = "true" ]; then + AUTO_CREATE="--auto-create" + fi + # shellcheck disable=SC2086,SC2154 # $dirs is an intentional space-separated list injected via $GITHUB_ENV for dir in $dirs; do echo "::group::Preview ${dir}" @@ -309,7 +325,8 @@ runs: if [ "$OK" = "1" ]; then echo "Registering feature-branch preview for ${REF} at ${PR_HEAD_SHA}..." - if ! raptor create iac-module -f "$dir" --feature-branch --git-ref "$PR_HEAD_SHA"; then + # shellcheck disable=SC2086 # $AUTO_CREATE is a deliberate empty-or-one-flag word + if ! raptor create iac-module -f "$dir" $AUTO_CREATE --feature-branch --git-ref "$PR_HEAD_SHA"; then echo "::error::Preview registration failed for ${REF}" echo "${REF} (preview)" >> "$FAILURES" RESULT="preview failed"; OK=0 @@ -357,6 +374,7 @@ runs: CONTROL_PLANE_URL: ${{ inputs.control_plane_url }} FACETS_USERNAME: ${{ inputs.username }} FACETS_TOKEN: ${{ inputs.token }} + AUTO_CREATE_INTENTS: ${{ inputs.auto-create-intents }} # Same rationale as the preview step: keep raptor's Trivy scan out of # terraform-init'd dependencies under .terraform/ (issue #13). TRIVY_SKIP_DIRS: "**/.terraform,**/.terraform/**" @@ -369,6 +387,17 @@ runs: | sed -E "s/[[:space:]]*#.*$//; s/^[\"']//; s/[\"'][[:space:]]*$//; s/[[:space:]]*$//" } + # --auto-create makes raptor register the module's intent when the Control + # Plane has never seen that kind. Without it the upload 404s ("Intent + # not found"), which is what a modules repo hits the first time it + # carries a new intent — nothing else creates one: resource-type-mapping + # rejects unknown intents outright. The flag is NOT passed to --dry-run, + # which never touches the intent. + AUTO_CREATE="" + if [ "${AUTO_CREATE_INTENTS:-true}" = "true" ]; then + AUTO_CREATE="--auto-create" + fi + # shellcheck disable=SC2086,SC2154 # $dirs is an intentional space-separated list injected via $GITHUB_ENV for dir in $dirs; do echo "::group::Publish ${dir}" @@ -383,7 +412,8 @@ runs: # Upload as PREVIEW. Git provenance is auto-detected from the work tree # (HEAD == the pushed commit on a push build). echo "Uploading ${REF}..." - if ! raptor create iac-module -f "$dir"; then + # shellcheck disable=SC2086 # $AUTO_CREATE is a deliberate empty-or-one-flag word + if ! raptor create iac-module -f "$dir" $AUTO_CREATE; then echo "::error::Upload failed for ${REF}" echo "${REF} (upload)" >> "$FAILURES" echo "::endgroup::"; continue