diff --git a/module-ci-action/README.md b/module-ci-action/README.md index d71ce3f..ea5f446 100644 --- a/module-ci-action/README.md +++ b/module-ci-action/README.md @@ -10,7 +10,7 @@ triggering event: | Event | Mode | What it does | |-------|------|--------------| | `pull_request` opened / synchronize / reopened | **preview** | Creates each changed output type the Control Plane does not have yet, then validates each changed module and registers a **feature-branch preview** pinned to the PR head commit. Optionally upserts a PR comment. | -| `push` (to your default branch) | **publish** | Applies each changed output type, then uploads each changed module and **publishes** it (PREVIEW → PUBLISHED). | +| `push` (to your default branch) | **publish** | Applies each changed output type, then uploads each changed module and **publishes** it (PREVIEW → PUBLISHED), declaring compatibility from the registry state — see **Compatibility declarations** below. | | `pull_request` **closed without merge** | **cleanup** | Deletes the preview each changed module owns — **only if** the preview belongs to a commit from this PR. | | `pull_request` **closed by merge** | **no-op** | Skipped: the concurrent `push` (publish) run re-uploads and publishes the module at the merge commit, so it owns the slot. Running cleanup here would be redundant and could race the publish. | @@ -50,6 +50,30 @@ committed. If your trees are not at the repo root, set `path-prefix` (e.g. `infra/` when they live at `infra/modules/...` and `infra/outputs/...`). +## Compatibility declarations + +`raptor publish` refuses to publish over an already-published version without an +explicit `--backward-compatible yes`, and refuses a new contract version without +`--versionupgrade` (raptor ≥ v0.1.106). A bare publish therefore fails for every +module a repository has already shipped. + +In a repo-first flow the author has already declared, in git: + +| What the author did | What CI passes | +|---|---| +| Edited `modules/{intent}/{flavor}/0.2` in place | `--backward-compatible yes` | +| Added a new `modules/{intent}/{flavor}/0.3` directory | `--versionupgrade` | +| Added the first module of a type/flavor | nothing | + +The action reads the published versions of that `intent/flavor` back off the +Control Plane and passes the matching flag. Version **ordering** stays raptor's +job: a "bump" that is not actually greater than every published version is +rejected there, and should be. + +If the listing cannot be read, the module fails rather than publishing with a +guessed declaration — reading a failed count as "nothing is published" would send +a first-publication for a module that already has one. + ## Output types A module names its output type by reference (`@namespace/name`). The Control Plane diff --git a/module-ci-action/action.yml b/module-ci-action/action.yml index 3769d19..6baf181 100644 --- a/module-ci-action/action.yml +++ b/module-ci-action/action.yml @@ -584,8 +584,52 @@ runs: echo "::endgroup::"; continue fi + # raptor refuses to publish over an already-published version without an + # explicit compatibility declaration, and refuses a NEW version without + # --versionupgrade. A bare publish therefore fails for every module a + # repository has already shipped. + # + # In a repo-first flow the author HAS declared, in git: editing + # modules/{intent}/{flavor}/0.2 in place says "compatible"; adding a 0.3 + # directory says "breaking". Read that choice back off the registry and + # pass the matching flag. Version ORDERING stays raptor's job — a bump + # that is not actually greater is rejected there, and must be. + if ! MODULES_JSON="$(raptor get iac-module -o json 2>/dev/null)"; then + echo "::error::Could not list modules to determine the compatibility declaration for ${REF}" + echo "${REF} (publish)" >> "$FAILURES" + echo "::endgroup::"; continue + fi + family() { + printf '%s' "$MODULES_JSON" | jq -r --arg t "$TYPE" --arg f "$FLAVOR" --arg v "$1" \ + '(if type=="array" then . else [] end) + | map(select(.intent==$t and .flavor==$f and .stage=="PUBLISHED" + and ($v=="" or .version==$v))) + | length' 2>/dev/null || echo "" + } + PUBLISHED_ANY="$(family "")" + PUBLISHED_TARGET="$(family "$VERSION")" + if [ -z "$PUBLISHED_ANY" ] || [ -z "$PUBLISHED_TARGET" ]; then + # A failed count is not "nothing is published" — that reading would send + # a first-publication (no flag) for a module that already has one. + echo "::error::Could not read published versions of ${TYPE}/${FLAVOR} for ${REF}" + echo "${REF} (publish)" >> "$FAILURES" + echo "::endgroup::"; continue + fi + + DECLARATION="" + if [ "$PUBLISHED_TARGET" != "0" ]; then + DECLARATION="--backward-compatible yes" + echo "${VERSION} is already published: declaring a compatible update." + elif [ "$PUBLISHED_ANY" != "0" ]; then + DECLARATION="--versionupgrade" + echo "${VERSION} is a new contract version for ${TYPE}/${FLAVOR}: declaring a version upgrade." + else + echo "First publication of ${TYPE}/${FLAVOR}: no declaration needed." + fi + echo "Publishing ${REF}..." - if ! raptor publish iac-module "$REF"; then + # shellcheck disable=SC2086 # $DECLARATION is a deliberate empty-or-flag word list + if ! raptor publish iac-module "$REF" $DECLARATION; then echo "::error::Publish failed for ${REF}" echo "${REF} (publish)" >> "$FAILURES" echo "::endgroup::"; continue