From 10b02f635e78f32ebe24182c2714f3f34c91eba7 Mon Sep 17 00:00:00 2001 From: Anuj Hydrabadi Date: Thu, 10 Sep 2026 19:39:40 +0530 Subject: [PATCH] fix(module-ci): create a module's intent, or the first one of a kind 404s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A modules repo could not carry a module whose intent the Control Plane had never seen. The upload failed outright: Error: upload failed with status 404: {"message":"Intent gcs not found"} Nothing else creates an intent. raptor create resource-type-mapping rejects one it does not know, so there was no way through from CI at all — a repo bootstrapped against one Control Plane could not be published to another. raptor already has the flag. Pass --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. Gated behind a new auto-create-intents input, default true, because the flag does two things and the second is shared state. Besides creating a missing intent, --auto-create rewrites an existing intent's metadata from the uploading module's intentDetails. An intent is shared by all its flavors, so two flavors that disagree fight, and the last module published wins. A real example from vibe-modules: cloud_account/gcp_provider says displayName "Cloud Account" while cloud_account/gcp_org_provider says "GCP Org Cloud Account", for the same intent. Keeping intentDetails identical across the flavors of one intent is the right answer, and the README now says so. auto-create-intents: 'false' freezes the metadata instead, at the cost of failing on any new intent. Verified against a live Control Plane: with the flag, gcs/standard/1.0 uploaded and registered the gcs intent; re-running without it then succeeded, which proves the intent persisted. Co-Authored-By: Claude Opus 5 --- module-ci-action/README.md | 37 +++++++++++++++++++++++++++++++++++++ module-ci-action/action.yml | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) 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