Skip to content

Apigee pending cbs uniformity changes - #775

Merged
Rohit-Ekbote merged 16 commits into
mainfrom
apigee-pending-cbs-uniformity-changes
Aug 25, 2026
Merged

Apigee pending cbs uniformity changes#775
Rohit-Ekbote merged 16 commits into
mainfrom
apigee-pending-cbs-uniformity-changes

Conversation

@Rohit-Ekbote

Copy link
Copy Markdown
Contributor

No description provided.

Rohit-Ekbote and others added 3 commits August 18, 2026 14:19
PR #729 is an independent generation of this bundle (registry issue #159). Of
its six runbook tasks, exactly one reaches a finding nothing here could:
environments hosting zero deployed API proxies.

That is the environment axis of the inventory every other check reads from the
proxy axis. check_failed_deployments asks "is this proxy deployed anywhere?" --
an org whose proxies are all deployed to prod answers yes for every proxy while
an empty test environment sits serving nothing, and a hostname routed there
returns an edge-level error rather than a proxy response. The new
partialcoverage fixture is exactly that case, and asserts the coverage issue is
raised AND that every proxy-side check stays silent.

Deliberately counts deployments, not HEALTHY deployments. Gating on
state == "READY" would make every environment look empty the moment the
deployment status view became unreadable, and would re-report in this check what
check_deployment_state already owns. Asserted from both sides: statusunknown and
broken must both report zero coverage issues.

Two correctness guards #729 does not have. A PROXIES filter removes deployments
from the inventory, so an environment hosting only filtered-out proxies would
look empty -- a finding manufactured by configuration; the check refuses to
judge and says why. An ENVIRONMENTS filter is the opposite case: the topology
records every environment unfiltered while deployments ARE filtered, so the
environment list is narrowed to what the run actually looked at.

Not adopted, with reasons recorded in the README:
  - Discovery as a task: it is suite setup here, deliberately.
  - Proxy deployment health, revision/approval state: already covered.
  - Health summary: raises no finding a per-condition check does not.
  - Runtime environment status: its Cloud Monitoring filter uses
    resource.labels.environment_name, which does not exist -- the Environment
    monitored resource has resource_container/org/env/location, so the query
    matches nothing and the check silently reports nothing. It also misreads
    environment/active (a GAUGE counting environments attached to an instance,
    not a per-environment liveness flag) and sums that gauge over the window.
    Read correctly it measures environment-to-instance attachment, which
    gcp-apigee-environment-health already checks directly through the Apigee
    API. Verified against the live metric and monitored-resource descriptors.
  - STALE_REVISION_THRESHOLD: #729 defaults it to 1, so a proxy one revision
    behind is never flagged. Adopting that would weaken the drift check.

Offline tier 262 -> 301 assertions. 25 mutations, all detected against a green
baseline, including four new ones: coverage silenced, coverage narrowed to
READY-only, and each of the two scope-filter guards removed.


Claude-Session: https://claude.ai/code/session_01NLcdGCtY4qZH8tW5MKDEvi

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* [Creator] [design-spec] gcp-apigee-traffic-health (#158)

* [#731] Anchor gcp-apigee-traffic-health on the Apigee organization

APIGEE_ORG was rendered from {{match_resource.resource_name}}, which is not an
attribute runwhen-local builds. Because CustomUndefined.__str__ returns a
placeholder rather than "", the SLX rendered APIGEE_ORG as the literal string
missing_workspaceInfo_custom_variable: every Apigee API call targeted a
non-existent org and operators saw the placeholder in the alias.

Both templates now share one org resolution chain, in boolean mode and with the
indexed payload materialised first so an absent .resource degrades instead of
raising UndefinedError.

Also in this change:

- qualifiers ["project", "resource"] -> ["resource"]. The SLX name is built from
  the qualifier values and an Apigee X org is named after its project, so the
  old form rendered <project>-<project>-gcp-apigee-traffic-health-<hash>.
- Auth gates on whether a token can be minted, not on whether
  activate-service-account succeeded, plus a key-shape probe that emits a
  sentinel and no key bytes. Without it, a run with no identity found nothing
  and reported healthy.
- Discovery moved from a task into Suite Initialization; a missing scope file is
  now an error in each check rather than an empty organization.
- Target servers are read as the bare array the API actually returns.
  `.targetServers[].name` matches no real response, so the target performance
  check evaluated nothing on every run while rendering as passed.
- Throughput deviation is compared as a ratio in both directions. A drop is
  bounded at -100%, so the old abs(deviation) > 200 could never report a traffic
  collapse.
- Findings are aggregated per failure mode; titles name the org and carry no
  counts or resource names.
- Ships runbook-only: the SLI invoked a strict subset of the runbook's scripts
  with identical variables, and the SLO consumed the SLI's metric.
- Adds an offline tier (fixtures built from the Apigee v1 discovery document)
  and a render tier (templates through runwhen-local's jinja2 config). Every
  assertion was mutation-tested: 22 mutations, all detected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#731] Update SKILL-TEMPLATE.md to match the shipped bundle

It still documented sli.robot and generate_traffic_summary.sh, both of which
were removed, and listed task titles naming the project. Operator-facing
documentation that tells someone to run a deleted script is worse than none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#731] Wire THROUGHPUT_DEVIATION_PCT through, and assert the class

Review finding: THROUGHPUT_DEVIATION_PCT was documented in README.md and the
SKILL-TEMPLATE variable table as a tunable with default 200, and read by
check_throughput.sh, but wired through none of the three places the platform
needs it -- taskset configProvided, Import User Variable, and the env dict. The
script's built-in default was therefore the only reachable value and an operator
following the README could not change it. Confirmed by inspection: 0 occurrences
in all three.

Nothing was broken at the default, which is exactly why it survived: every
behavioural assertion in the offline tier passed while the promise was
unkeepable.

Wired through all three, and added a CLASS assertion rather than only fixing the
instance, per the review's second comment. Scenario H now extracts every variable
the README's `## Configuration` section documents as operator config and asserts
each is present in configProvided, in Import User Variable, and in the env dict.
It is scoped to leading bullets inside that section, so prose mentions elsewhere
(KEY_JSON, TOKEN_ABSENT, thresholds named in task descriptions) are not mistaken
for config, and genuinely internal variables -- APIGEE_API, APIGEE_SCOPE_FILE --
are correctly ignored, as the review noted they should be.

The behavioural scenarios cannot catch this class and adding more would not help:
they export the variables themselves, so the scripts always see a value whether
or not the runbook would ever supply one. That reasoning is recorded next to the
new scenario.

Audited the rest of both bundles for the same class: THROUGHPUT_DEVIATION_PCT
here and SECURITY_WINDOW_HOURS in #733 were the only two.

Verification: offline 130 passed / 0 failed (was 111), render 19/0, shellcheck
clean, 26 mutations all detected -- including dropping the new variable from each
of the three wiring points independently, and renaming the README heading, which
proves the extraction cannot pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#731] Harden the .test harness; stop leaking tokens into xtrace

Second-pass review, all findings verified against the tree before changing
anything.

Items 1-4 (the ones flagged for merge):

1. build-infra reported success when it did nothing. `tf.secret` absent printed
   "Skipping" and exited 0, telling the caller the infrastructure existed when
   it did not. Now sources load-credentials.sh, which exits non-zero.

2. validate-generation-rules could not fail. `ajv ... && echo valid || echo
   invalid` swallowed the failure -- the block's exit status was the trailing
   `rm -rf` -- so an invalid rule printed "is invalid" and passed. Replaced with
   the environment-health model: checked `curl -fsS` for the schema, a `jq -e`
   sanity check on the download, a failure counter, and a guard that errors when
   the glob matches no rules.

3. default never reached discovery, and its description advertised terraform
   infra that was not in the chain. It now runs the credential-free tiers first,
   then build-infra, generate-rwl-config, run-rwl-discovery and
   validate-generation-rules. Without discovery a green `task` run said nothing
   about the generation rule, which is what most of this bundle's work was about.

4. No shared credential contract. Adopts load-credentials.sh from the merged
   siblings, so one tf.secret serves all four Apigee bundles and an ambient
   APIGEE_ORG that disagrees with the file is announced rather than silently
   retargeting a shared org. generate-rwl-config no longer defaults the org to
   "mock-org", which produced a workspaceInfo naming an organization that does
   not exist.

Medium:

5. The Terraform provisioned a GCS bucket its own header called inert. Removed.
   The stub now publishes discovery ground truth instead -- gated resource type,
   expected SLX count, expected resourcePath -- so a discovery run has something
   machine-readable to be checked against.

6. Discovery image pinned to 0.11.11 with a pre-flight registry probe. On an
   older image discovery exits 0 with zero SLXs, which reads as "the rule matched
   nothing" when it is an image problem.

Non-blocking item 8, tokens in the xtrace stream: fixed here even though it is
pre-existing and collection-wide, because it is a live OAuth bearer credential
written into captured output. As the review noted, the token leaks at the
assignment as well as the request -- and also at the `[ -z "$token" ]` emptiness
test, which is a third site: restoring tracing before that test just moves the
leak. Verified zero occurrences of the stub's sentinel across all five scripts.
The merged siblings still have it; not touched from this PR.

Also updates .test/README.md, which still described the removed summary issue
and the deleted bucket scaffold, and now leads with the credential-free tiers.

Every new assertion is mutation-tested. Offline 152 passed / 0 failed (was 130),
render 19/0, shellcheck clean, 34 mutations all detected -- including unwrapping
each xtrace suppression point independently, reverting each harness fix, and
re-adding the bucket.

Not verified locally: the HCL. terraform is not installed in this environment,
so .test/terraform was not syntax-checked or planned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

---------

Co-authored-by: rw-codebundle-agent[bot] <rw-codebundle-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add gcp-apigee-security-config CodeBundle

Monitors the security posture and access configuration of an Apigee
organization: TLS keystore alias expiry, API product quota/rate limits,
developer app access scope, Apigee security score/incidents, and target
server TLS. Includes runbook, SLI, generation rules/templates, README,
and test infrastructure.

Closes #157

* [#733] Anchor gcp-apigee-security-config on the Apigee organization

APIGEE_ORG was set to "{{project.name}}", so it rendered the PROJECT where the
org was wanted. That works by coincidence on Apigee X, where an org is 1:1 with
its project and the org ID is the project ID -- which is exactly why it survives
testing and fails on any divergence: an explicit custom.apigee_org override,
hybrid, or legacy Edge. The same file was already internally inconsistent, since
its alias resolved the org correctly while the config value did not.

Both templates now share one org resolution chain, in boolean mode and with the
indexed payload materialised first so an absent .resource degrades instead of
raising UndefinedError.

Also in this change:

- qualifiers ["project"] -> ["resource"]. gcp-hierarchy.yaml inserts project_id
  into the path only when `resource` is a qualifier, so the old form flattened
  resourcePath from gcp/<project>/<org> to gcp/<project> and never named the org.
- Adds the scope tag, which was absent entirely, as lowercase `organization`.
- Auth gates on whether a token can be minted, not on whether
  activate-service-account succeeded, plus a key-shape probe that emits a
  sentinel and no key bytes. Without it, every curl ran as no identity, every
  check found nothing, and the run reported a healthy org.
- Target server TLS is read from the per-target-server document. The list
  endpoint returns a bare array of STRINGS and carries no sSLInfo, so the old
  code read .sSLInfo off a string, got an empty value, and its `[ -z ... ]` test
  then reported a plaintext backend for EVERY target server in the org without
  ever reading one.
- Findings are aggregated per failure mode; titles name the org and carry no
  counts, scores, quota values or resource names. Consumer keys appear only as
  an 8-character prefix, and only in details.
- Security metrics distinguish "not measured" from "measured as zero risk": an
  unpopulated Advanced API Security metric raises nothing.
- awk replaces an interpreter dependency the runner image does not guarantee.
- The security score task title named the project; it now names the org, like
  every other task.
- Drops the keystore alias expiry check. gcp-apigee-environment-health already
  performs it, gating on the same resource type, so the same certificate raised
  the same finding twice against two SLXs for one org -- and this bundle's
  version never worked, reading .name off a bare array of strings and skipping
  every keystore. Reasoning recorded in the README and next to the tasks.
- Drops the summary task, which raised no finding and only restated the others.
- Ships runbook-only: the SLI invoked a strict subset of the runbook's scripts
  with identical variables and thresholds.
- Adds a shared apigee_common.sh whose helpers separate the documented list
  endpoints (singular field names) from the undocumented ones (bare arrays).
- Adds an offline tier (fixtures built from the Apigee v1 discovery document)
  and a render tier (templates through runwhen-local's jinja2 config). Every
  assertion was mutation-tested: 27 mutations, all detected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#733] Update SKILL-TEMPLATE.md to match the shipped bundle

It still documented sli.robot, check_keystore_tls.sh and
generate_security_summary.sh, all of which were removed. Operator-facing
documentation that tells someone to run a deleted script is worse than none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#733] Wire SECURITY_WINDOW_HOURS through, and assert the class

Review finding: SECURITY_WINDOW_HOURS was documented in README.md and the
SKILL-TEMPLATE variable table as a tunable with default 6, and read by
check_security_score.sh, but wired through none of the three places the platform
needs it -- taskset configProvided, Import User Variable, and the env dict. The
script's built-in default was therefore the only reachable value and an operator
following the README could not change it. Confirmed by inspection: 0 occurrences
in all three.

Nothing was broken at the default, which is exactly why it survived: every
behavioural assertion in the offline tier passed while the promise was
unkeepable.

Wired through all three, and added a CLASS assertion rather than only fixing the
instance, per the review's second comment. Scenario G now extracts every variable
the README's `## Configuration` section documents as operator config and asserts
each is present in configProvided, in Import User Variable, and in the env dict.
It is scoped to leading bullets inside that section, so prose mentions elsewhere
(KEY_JSON, TOKEN_ABSENT, thresholds named in task descriptions) are not mistaken
for config, and genuinely internal variables such as APIGEE_API are correctly
ignored, as the review noted they should be.

The behavioural scenarios cannot catch this class and adding more would not help:
they export the variables themselves, so the scripts always see a value whether
or not the runbook would ever supply one. That reasoning is recorded next to the
new scenario.

Audited the rest of both bundles for the same class: SECURITY_WINDOW_HOURS here
and THROUGHPUT_DEVIATION_PCT in #731 were the only two.

Verification: offline 130 passed / 0 failed (was 114), render 19/0, shellcheck
clean, 31 mutations all detected -- including dropping the new variable from each
of the three wiring points independently, and renaming the README heading, which
proves the extraction cannot pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

* [#733] Stop task clean from disabling Apigee under the shared org

Second-pass review, all findings verified against the tree before changing
anything.

BLOCKING items 1 and 2 -- the same root cause:

google_project_service.apigee here and google_project_service.required
["apigee.googleapis.com"] in gcp-apigee-environment-health were the same GCP
resource under two Terraform states. disable_on_destroy is unset and the provider
default is true, and `task clean` runs `terraform destroy -auto-approve`, so
tearing down this bundle would have attempted to DISABLE apigee.googleapis.com
while a live organization, a runtime instance and three sibling bundles' fixtures
depended on it.

Resolved by dropping the ownership claim rather than by setting
disable_on_destroy = false. This bundle is a read-only guest that never needed to
enable anything, and single ownership is a stronger guarantee than two states
that agree not to destroy. environment-health's bootstrap is now the documented
prerequisite, which is the arrangement proxy-health already uses.

High:

3. build-terraform-infra reported success when it did nothing. Now sources
   load-credentials.sh, which exits non-zero when credentials are absent.

4. validate-generation-rules could not fail. `ajv ... && echo valid || echo
   invalid` swallowed the failure. Replaced with the environment-health model:
   checked `curl -fsS`, a `jq -e` sanity check on the download, a failure
   counter, and a guard for an empty rule set.

5. Adopts the shared load-credentials.sh contract. This matters most here, as
   the review noted: this bundle reads a shared org, and a stale APIGEE_ORG left
   over from a sibling's run would silently retarget it.

Medium:

6. Dropped the project-wide roles/iam.serviceAccountTokenCreator binding. It
   permitted impersonating every service account in the project; nothing here
   impersonates anything.

7. .test/README.md told operators to export a key that no resource created.
   Added google_service_account_key, gated on create_reader_key so the private
   key need not live in Terraform state, with the out-of-band gcloud alternative
   documented.

8. .test/README.md now leads with the credential-free tiers -- the ones that gate
   a PR -- and records that check_security_score cannot be demonstrated on an
   EVALUATION org at all, since apigee.googleapis.com/security/* only populates
   with the paid Advanced API Security add-on. It also no longer describes the
   removed keystore/cert-expiry scenario.

9. Discovery image pinned to 0.11.11 with a pre-flight registry probe.

Non-blocking item 10, tokens in the xtrace stream: fixed here even though it is
pre-existing and collection-wide, because it is a live OAuth bearer credential
written into captured output. As the review noted, the token leaks at the
assignment as well as the request -- and also at the `[ -z "$(apigee_token)" ]`
guard, where a command substitution inside a test is expanded by xtrace before
the test runs. Added apigee_have_token for that case. Verified zero occurrences
of the stub's sentinel across all four scripts. The merged siblings still have
it; not touched from this PR.

Every new assertion is mutation-tested. Offline 156 passed / 0 failed (was 130),
render 19/0, shellcheck clean, 40 mutations all detected -- including re-adding
the google_project_service claim, re-adding the token-creator binding, removing
the key resource, and unwrapping each xtrace suppression point.

Not verified locally: the HCL. terraform is not installed in this environment,
so .test/terraform was not syntax-checked or planned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pha15WRfir9fXdCbv5YoLB

---------

Co-authored-by: rw-codebundle-agent[bot] <rw-codebundle-agent[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

Checked this at 7a15829 for completeness first, then for uniformity. Completeness is good; uniformity has not landed yet despite the branch name, so the bulk of this is a spec for what is still to do.

1. Completeness — what is and isn't here

Compared each bundle in this PR against the tip of the branch it supersedes:

Bundle Compared against Result
gcp-apigee-security-config creator/issue-157 @ 5ffd8e2 (#733) identical, zero diff
gcp-apigee-traffic-health creator/issue-158 @ c956d08 (#731) identical, zero diff
gcp-apigee-proxy-health creator/apigee-proxy-env-coverage @ 458aea9 (#766) identical, zero diff

So #733, #731 and #766 are fully represented and can be closed in favour of this PR once it lands.

Two things worth confirming are deliberate:

#729 is only partially adopted. creator/issue-159 is a divergent, earlier implementation of proxy-health — it carries check_revisions.sh, check_runtime_status.sh, generate_proxy_summary.sh and an sli.robot that this PR does not, while this PR has check_failed_deployments.sh, check_failed_operations.sh, check_revision_accumulation.sh and check_revision_drift.sh that #729 does not. Only the environment-coverage slice was taken, which is exactly what #766's title says — flagging it only so the remainder of #729 is closed as superseded rather than forgotten. The sli.robot in #729 is the one item I would want an explicit decision on, since proxy-health has no SLI on main either.

Two of the five Apigee bundles are not in this PR. gcp-apigee-environment-health and gcp-apigee-product-governance are untouched here — they are already on main. That is fine for the content, but it is a problem for the uniformity goal, because four of the seven cross-cutting items below apply to them too. See the scope question at the end.

2. Uniformity — current state across all five

None of the vocabulary work is in this PR yet. At 7a15829:

Axis env-health product-gov proxy-health security-config traffic-health
Credential-free gate (none) (none) ci run-mock-tests run-mock-tests
Live assertion tier test-issue-generation test-live (none) (none) (none)
Fixture teardown check-and-cleanup-terraform check-and-cleanup-fixtures + alias teardown-apigee-fixtures + check-and-cleanup-terraform check-and-cleanup-terraform check-and-cleanup-terraform
clean works without RW_*
upload-slxs / check-rwp-config ❌ missing
RWL_IMAGE pinned :latest :latest 0.11.11 0.11.11 0.11.11
SLX endpoint v3 /branches/main/ v3 v3 v3 v3
test-offline/test-render visible in task --list no desc:
xtrace token guard ❌ 8 scripts n/a 10 scripts

3. Proposed standard vocabulary

Twelve names, identical in every bundle, same responsibilities, same exit semantics:

ci, test-offline, test-render, validate-generation-rules, build-infra, test-live, check-and-cleanup-fixtures, check-unpushed-commits, generate-rwl-config, run-rwl-discovery, clean-rwl-discovery, clean

default: ci → check-unpushed-commits → build-infra → test-live
         → generate-rwl-config → run-rwl-discovery
clean:   check-and-cleanup-fixtures → clean-rwl-discovery
ci:      test-offline → test-render → validate-generation-rules

Two rules that make task --list byte-identical everywhere:

  1. Only the standard vocabulary carries desc:. Every implementation sub-step (build-terraform-infra, bootstrap-apigee-fixtures, create-entitlement-fixtures, generate-certs, import-keystore-alias) gets internal: true. go-task 3.42 supports it and 8 bundles in this repo already use it; the sub-steps stay reachable via task --list-all for debugging.
  2. A task that does not apply is still declared, and fails with a sentence explaining why. A missing task reads as a typo; a task that says "this bundle creates no fixtures — the shared org is provisioned by gcp-apigee-environment-health" reads as an answer.

bootstrap-prerequisites / destroy-prerequisites stay unique to gcp-apigee-environment-health unless the prerequisite layer is moved off Terraform (see item 8).

4. Cross-cutting changes

C1 — clean must not require RunWhen Platform credentials 🐞

clean → delete-slxs → check-rwp-config exits 1 without RW_WORKSPACE/RW_API_URL/RW_PAT, in four of five. The cloud teardown is first so it completes, but the task exits non-zero and clean-rwl-discovery never runs — leaving root-owned output/ behind on every local run. Drop delete-slxs from clean; keep it as an explicit opt-in. traffic-health is already correct here.

C2 — SLX endpoints are behind the rest of the repo

All five post to api/v3/workspaces/${RW_WORKSPACE}/branches/main/slxs/${SLX_NAME}. The 48 bundles that include codebundles/.test-tasks/Taskfile.yaml use api/v4/workspaces/${RW_WORKSPACE} typed sync for upload and api/v3/workspaces/${RW_WORKSPACE}/slxs/${SHORT_NAME} for delete. Copy those bodies in verbatim — self-sufficiency preserved, staleness removed.

C3 — pin the discovery image everywhere

The two merged bundles are now the laggards on :latest; the three here already pin. All five gate on gcp_apigee_organizations, and an image whose registry lacks that type makes discovery exit 0 with zero SLXs. Bump the default to 0.11.12 and adopt proxy-health's registry pre-flight everywhere.

C4 — ci is the gate name; delete run-mock-tests

An alias is one more thing an operator has to know. Straight rename.

C5 — test-live is the live assertion tier name

test-issue-generationtest-live. Where a bundle has none yet, declare it and fail with what is missing.

C6 — check-and-cleanup-fixtures is the teardown name

…-terraform names the mechanism, and names it wrongly for the three bundles whose fixtures are REST objects Terraform never sees. Drop the aliases; make any Terraform destroy an internal: true sub-step.

C7 — xtrace token guard 🔒

security-config and traffic-health have this fixed, and security-config's approach is the one to copy: centralising the guard in apigee_common.sh means its three set -x check scripts need no local change, because none of them touch the token directly.

proxy-health, in this PR, is not fixed — 10 scripts run set -x, none guard, and apigee_common.sh:113 and :175 interpolate the token straight into curl. Running discover_proxies.sh under its own offline/mock PATH:

+++ curl -s -w '\n%{http_code}' -H 'Authorization: Bearer REDACTED-FAKE-TOKEN' https://apigee.googleapis.com/v1/organizations/...

⚠️ Worth knowing: .test/offline/mock/ contains a curl stub but no gcloud stub, so on a machine with ambient gcloud credentials this path mints and traces a real, live ya29.… access token. I hit that before switching to an explicit stub. Two things follow: apply the guard, and add a gcloud stub to offline/mock/ so the offline tier cannot reach live credentials at all.

environment-health has the same gap (8 scripts) but is out of scope here.

C8 — optional, and it would simplify the whole family

bootstrap-prerequisites is already half idempotent (it curls the org and skips if present). What stops every bundle carrying it is Terraform state ownership, not idempotency: five states cannot each create the same VPC, address and peering. Moving that layer off Terraform to check-then-create gcloud/REST would make it safely duplicable, remove the ordering dependency between bundles entirely, and kill the bug class where one bundle's terraform destroy takes out shared substrate. Teardown then needs a reference guard — refuse while the org exists or any bundle's suffixed fixtures remain — which generalises the guard environment-health already has. If you go this way, org creation also needs 409 treated as success, or two bundles bootstrapping concurrently will race.

5. Sequencing

  1. C1 — five one-line edits, highest daily value.
  2. C4, C5, C6 + internal: true — mechanical, no behaviour change. Do all bundles in one pass so no window exists where the names disagree.
  3. C2, C3 — behaviour-affecting; needs a discovery run behind it.
  4. C7 on proxy-health, plus the missing gcloud stub.
  5. test-live for proxy-health, security-config, traffic-health — real work and the largest genuine coverage gain. proxy-health has the richest fixtures in the family and currently no live assertion at all.

6. Scope question

Four of the eight items above (C1, C2, C3, C7) apply to gcp-apigee-environment-health and gcp-apigee-product-governance as well, and both are absent from this PR. Uniformity across three of five is not uniformity — an operator moving between bundles still meets two different vocabularies.

Suggest either pulling both into this PR, or landing steps 1–2 as a separate five-bundle mechanical PR first so the vocabulary is settled before the content changes here merge on top. I'd lean to the latter: the rename pass is reviewable in one sitting, and it keeps this PR about the bundles it actually changes.

@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

Follow-up to the review above, covering the two Apigee bundles that are not in this PRgcp-apigee-environment-health and gcp-apigee-product-governance, both already on main at fb50021.

Posting here rather than on the merged PRs so the five-bundle picture stays in one place. If you'd rather track it separately, this converts cleanly into an issue.

Why they belong in the conversation

Five of the eight cross-cutting items apply to them, and on two axes the merged bundles are now behind the three in this PR:

Item env-health product-gov Already fixed in this PR's bundles?
C1 clean requires RW_* only traffic-health
C2 SLX endpoint on v3 /branches/ none — applies to all five
C3 RWL_IMAGE unpinned (:latest) ✅ all three pin
C7 xtrace token guard ❌ 8 scripts n/a (no set -x) security-config, traffic-health
C4/C5/C6 vocabulary partial ❌ none

So run-rwl-discovery in the two oldest bundles is the least protected in the family, and the xtrace fix that landed on both branches never made it back to environment-health.

Two more axes I hadn't spotted — these affect all five

Offline stub directory has three different names. offline/bin (env-health, security-config, traffic-health), offline/stubs (product-governance), offline/mock (proxy-health). Standardise on offline/bin — it's already the majority.

Related, and this is the one that matters: only proxy-health is missing a gcloud stub. environment-health/offline/bin/gcloud and product-governance/offline/stubs/gcloud both exist. That's why proxy-health's offline tier reaches live credentials and the other two don't — it makes the fix in my previous comment a one-file gap rather than a design problem.

Offline runner filename varies four ways. offline/run.sh (env-health, security-config, traffic-health), offline/run-offline-tests.sh (product-governance), offline/run_offline_tests.sh and offline/harness.sh (proxy-health). And the aggregate validate-all-tests.sh exists in three bundles but not in environment-health or proxy-health.

Suggest: offline/run.sh + render/run.sh + validate-all-tests.sh in all five, with ci calling the aggregate. That way ci has an identical body everywhere, which is the point.

gcp-apigee-environment-health

# Change Type
1 Add test-render — the only bundle in the family without a render tier new tier
2 Add validate-all-tests.sh and ci = test-offline → test-render → validate-generation-rules new
3 Rename test-issue-generationtest-live rename
4 Rename check-and-cleanup-terraformcheck-and-cleanup-fixtures rename
5 internal: true on generate-certs, build-terraform-infra, import-keystore-alias visibility
6 Fold import-keystore-alias into build-infra — it provisions a fixture, and a separate step is a fixture people forget. default currently has to call it explicitly, which is the tell responsibility
7 Drop delete-slxs from clean (C1) 🐞
8 Pin RWL_IMAGE + adopt proxy-health's registry pre-flight (C3) drift
9 Refresh upload-slxs/delete-slxs/check-rwp-config from .test-tasks (C2) drift
10 xtrace guard — 8 scripts run set -x, none guard, and apigee_common.sh interpolates the token at 3 sites (C7) 🔒
11 default → standard order consistency

Confirmed with its own stub, so no live token was involved:

++ curl -fsS -H 'Authorization: Bearer offline-fake-token' https://apigee.googleapis.com/v1/organizations/org

security-config's approach is the one to copy — centralising the guard in apigee_common.sh means the check scripts need no local change.

Keeps bootstrap-prerequisites / destroy-prerequisites. That asymmetry is deliberate: one Apigee org per project, four bundles are guests, and it is exactly what stops a guest's teardown disabling the API under the shared org — the bug fixed on #733.

gcp-apigee-product-governance

Closest of the five to the target already. Its check-and-cleanup-fixtures is the reference implementation for C6, and its offline tier is the reference for known-positive / known-negative / cannot-run coverage.

# Change Type
1 Add cidefault inlines the tiers instead of naming them new
2 Drop the check-and-cleanup-terraform alias; keep check-and-cleanup-fixtures rename
3 internal: true on create-entitlement-fixtures visibility
4 Add test-live to default — the task exists, default skips it consistency
5 Drop delete-slxs from clean (C1) 🐞
6 Pin RWL_IMAGE + registry pre-flight (C3) drift
7 Refresh the three RW tasks (C2) drift
8 Rename offline/stubs/offline/bin/, offline/run-offline-tests.shoffline/run.sh naming
9 Preventive xtrace guard in apigee_common.sh — no script sets -x today, so nothing leaks, but the token is interpolated unguarded and the bundle is one set -x away from the same exposure 🔒 preventive

Suggested split

This reinforces the sequencing note from my previous comment. The rename-and-visibility pass (C4/C5/C6 + internal: true + file naming) touches all five bundles, changes no behaviour, and is reviewable in one sitting. Landing it first, as its own PR against main, means:

  • the vocabulary is settled before this PR's content merges on top;
  • there is no window where three bundles use one vocabulary and two use another;
  • this PR stays about the bundles it actually changes.

C1 (five one-line edits) is worth folding into that same pass — it's the item with the highest day-to-day cost, since it breaks local teardown on every run.

C2/C3/C7 and the test-live work are better done per-bundle afterwards, with a live discovery run behind them.

@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

Correction to my two comments above, on bootstrap-prerequisites / destroy-prerequisites.

I listed C8 as "optional", and then in the environment-health section wrote "Keeps bootstrap-prerequisites / destroy-prerequisites. That asymmetry is deliberate and must not be flattened." That contradicts the conclusion we'd already reached, and it is the wrong steer. Both tasks should be idempotent and present in every Apigee bundle. Treat C8 as decided, not optional, and ignore that sentence in the environment-health table.

What actually blocks it — and the fix

Not idempotency. bootstrap-prerequisites is already half idempotent: it curls the org and skips if present. The blocker is Terraform state ownership — five states cannot each create the same VPC, global address and peering; the second one errors already exists, because Terraform converges within a state but does not adopt resources another state owns. That is the same shape as the two-states-one-API problem fixed on #733.

So the change is to take the prerequisite layer off Terraform and make it check-then-create over gcloud/REST. In environment-health/.test/terraform/main.tf that is a clean cut — the four prerequisite resources are contiguous and separate from the fixtures:

Line Resource Disposition
50 google_project_service.required → move to gcloud services enable (natively idempotent)
65 google_compute_network.apigee describe || create
77 google_compute_global_address.apigee_peering describe || create
90 google_service_networking_connection.apigee list | grep || connect
104–197 instances, environments, attachments, envgroups, target servers stay in environment-health's Terraform — these are its fixtures, not prerequisites

Every one of those four is naturally idempotent as a check-then-create, and none needs state. That makes the block safely duplicable into all five bundles verbatim.

The teardown guard is simpler than I said

I claimed destroy-prerequisites would need a new reference guard that scans for other bundles' suffixed fixtures. It doesn't — deleting the Apigee org deletes everything inside it, so "the org is gone" already implies "no bundle's fixtures remain." The guard environment-health has today is therefore sufficient as-is, and it is the whole reason this is safe to duplicate: whichever bundle runs it last succeeds, every earlier one refuses with a clear message, and ordering stops mattering.

Teardown order inside the task stays: peering → address → network → disable apigee + apigeeconnect only. Never servicenetworking — it is shared with other services in the project.

What this buys

  • Each bundle becomes genuinely self-sufficient. Today four of five silently depend on environment-health having been run first, which is invisible until the fixtures 404.
  • It removes the bug class where one bundle's terraform destroy takes out shared substrate — the original [Creator] [design-spec] gcp-apigee-environment-health #745 issue and the recent [Creator] [design-spec] gcp-apigee-security-config #733 one are both instances of it. With no Terraform owning the prerequisites, there is nothing for a stray destroy to take.
  • environment-health's Terraform shrinks to just its own fixtures, which is what the rest of the family already looks like.

Two things to get right

Concurrency. Org creation currently accepts only 200|201; two bundles bootstrapping at once means one gets 409 and fails. Accept 200|201|409 and fall through to the ACTIVE poll. Same for the address and network creates — treat ALREADY_EXISTS as success rather than parsing for it.

Copy drift — the real cost of the no-shared-library constraint, and this family has already demonstrated it: the bare-array /environments response trap was rediscovered and fixed independently three times. If ~150 lines of substrate logic are duplicated five ways, add an assertion in ci that checksums the shared block against the sibling bundles. Enforced duplication is fine; silent drift is what hurts.

Revised sequencing

Slotting into the plan from my earlier comment:

  1. C1 (clean without RW_*) — five one-line edits.
  2. C4/C5/C6 + internal: true + file naming — mechanical rename pass, all five bundles, one PR.
  3. C8 — prerequisites off Terraform, duplicated into all five, with the concurrency fixes and the drift checksum. Best done as its own PR: it is the only item that changes provisioning behaviour, and it wants a full bootstrap → build → teardown cycle behind it before anyone trusts it.
  4. C2/C3/C7 — drift and the xtrace guard.
  5. test-live for proxy-health, security-config, traffic-health.

C8 moves ahead of the drift fixes because it changes what build-infra and clean mean in four bundles, and doing it after the cosmetic passes avoids rewriting the same tasks twice.

…guard

Addresses the review on #775 across all five Apigee bundles, including the two
already on main that the PR did not touch -- uniformity across three of five is
not uniformity.

C1  `clean` no longer requires RunWhen Platform credentials. It called
    delete-slxs -> check-rwp-config, which exits 1 without RW_WORKSPACE/
    RW_API_URL/RW_PAT, so clean-rwl-discovery never ran and a root-owned
    output/ was left behind after every local run. delete-slxs is dropped from
    the chain and stays available on its own. clean-rwl-discovery also stops
    running an unconditional `sudo rm -rf`, which failed outright without
    passwordless sudo even when there was nothing to remove.

C2  The three RunWhen Platform tasks now come from ../../.test-tasks/Taskfile
    (as 49 other bundles already do) instead of five copies that had all
    drifted to the v3 /branches/main/ endpoint. Thin local wrappers keep the
    task names an operator types. Copying the bodies in verbatim was the other
    option and would have been ~300 lines x 5 of exactly the drift this commit
    is about; .test/ is harness, not shipped bundle content, so the no-shared-
    library constraint does not apply to it.

C3  RWL_IMAGE pinned to 0.11.12 in all five, with proxy-health's registry
    pre-flight adopted everywhere. Verified 0.11.12 is the newest published tag
    (0.11.13 does not exist) and that its GCP resource-type registry carries
    gcp_apigee_organizations -- on an image without it, discovery exits 0 with
    zero SLXs, which reads as a bundle problem.

C4/C5/C6  Standard vocabulary. run-mock-tests -> ci, test-issue-generation ->
    test-live, check-and-cleanup-terraform -> check-and-cleanup-fixtures, and
    the aliases are gone. Implementation sub-steps are internal: true, still
    reachable via `task --list-all`. `task --list` is now byte-identical across
    all five.

C7  xtrace token guard. proxy-health ran `set -x` in 10 scripts with the token
    interpolated into curl at two sites in apigee_common.sh and no guard at
    all; env-health the same in 8. Both now disable tracing around the mint and
    the request and restore the PREVIOUS state rather than switching tracing on
    -- product-governance traces nothing, and gets the guard preventively.
    Verified against a canary token: 6 leaked lines -> 0 in both bundles.

    proxy-health also had no gcloud stub in its offline tier, so with
    GCP_ACCESS_TOKEN unset (which the nocreds scenario does deliberately) the
    fallback reached the real gcloud and, on any Linux host with ambient
    credentials, minted and traced a live token. Added, and the scenario now
    drives the no-token branch explicitly via TOKEN_FAIL instead of relying on
    gcloud being absent.

C8  Prerequisites off Terraform. The APIs, VPC, peering range and Apigee org
    moved from environment-health's main.tf to apigee_prerequisites.sh, which
    is check-then-create over gcloud/REST and byte-identical in all five
    bundles. Five Terraform states cannot each own the same VPC -- with no
    state there is nothing to own, so every bundle is self-sufficient and
    teardown order stops mattering. Org creation accepts 409, and creates
    verify by describe rather than trusting an exit status, so concurrent
    bootstraps are a no-op rather than a coin flip. Teardown disables apigee
    and apigeeconnect only, never servicenetworking.

    check-shared-drift.sh checksums the duplicated files against the siblings
    and fails `task ci` on divergence. Enforced duplication is fine; silent
    drift is what hurts -- the bare-array /environments trap was rediscovered
    three times.

Naming: offline/bin + offline/run.sh + render/run.sh + validate-all-tests.sh in
all five (stub dirs were bin/stubs/mock, runners four different names).
product-governance's validate-all-tests.sh was the LIVE tier under a name that
means the offline aggregate everywhere else -- renamed to test-live.sh.

test-live now exists in all five. proxy-health, security-config and
traffic-health had no live assertion tier at all; each runs its checks against
the fixtures in one shared working directory (they share a discovery cache) and
asserts the artifacts are well-formed and that discovery actually established
an inventory -- a 403 turns every list into [] and "nothing wrong" then reads
identically to "nothing was looked at". No SLI/SLO added.

Also corrected from the review: proxy-health's test-offline/test-render DO
carry desc: and DO appear in `task --list` (via aliases), so that row of the
uniformity table was wrong.

Verification: `task ci` green in all five (offline + render + generation-rule
validation against the live schema + drift check); render tier actually
executed with jinja2 rather than skipping. Offline assertions 133/149/301/156/
152 -> 168/185/337/191/185, the additions being the vocabulary, C1, C3 and
gcloud-stub guards -- each negative-tested by reintroducing the defect.
`task clean` exits 0 in all five with no credentials of any kind. shellcheck
clean over all 34 changed shell files; terraform validate passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

Addressed at 3f85350, across all five bundles — gcp-apigee-environment-health and gcp-apigee-product-governance are pulled in, since uniformity across three of five still leaves an operator meeting two vocabularies. C8 is treated as decided, per the correction above. No new SLI/SLO.

Two places the review was wrong, and one where I went the other way

The task --list row for proxy-health is wrong. Its test-offline / test-render do carry desc: and do appear in task --list — via aliases: [offline] / [render], which is also why its ci calling - task: offline resolved. Checked against go-task 3.42.1. Nothing to fix there.

C2's "copy those bodies in verbatim" — I used includes: instead. Copying is ~300 lines × 5 of exactly the drift the third comment is about. 49 bundles already do includes: shared: taskfile: ../../.test-tasks/Taskfile.yaml with thin local wrappers, so the copies were the outlier. .test/ is harness, not shipped bundle content, so the no-shared-library constraint that forces duplication in the runtime scripts does not reach it. Local wrapper tasks keep the names an operator types.

C7's "copy security-config's approach" — copied the idea, not the code. Its restore is an unconditional set -x. That is invisible in security-config, where every caller traces, but it would switch tracing on in product-governance, which traces nothing. All four now save the previous state from $- and restore it, security-config included.

One trap worth recording, because the first version of the fix had it: factoring the guard into a helper does not work. A helper has to hand the saved state back through a command substitution, and set +x inside $( ) applies to the subshell — the caller keeps tracing. It passes bash -n, reads correctly, and leaks anyway. The guard is written out inline in each helper with that reasoning in the comment.

Per item

Status
C1 clean needs RW_* fixed in all five, reproduced first
C2 SLX endpoints on v3 shared include (see above)
C3 RWL_IMAGE unpinned pinned 0.11.12 + registry pre-flight in all five
C4/C5/C6 vocabulary task --list now byte-identical across all five (same sha256)
C7 xtrace guard 6 leaked lines → 0, in both affected bundles
C8 prerequisites off Terraform done, duplicated five ways, drift-checksummed
test-live for the three added
file/dir naming offline/bin + offline/run.sh + render/run.sh + validate-all-tests.sh everywhere

C1 — reproduced before fixing: clean → delete-slxs → check-rwp-config exits 1, clean-rwl-discovery never runs. delete-slxs dropped from the chain, still available on its own. The second half of the symptom you described needed a separate fix: clean-rwl-discovery ran an unconditional sudo rm -rf output, which fails outright without passwordless sudo — so the root-owned output/ survived regardless. It now tries the cheap path, falls back to sudo, and says what to do by hand if neither works. task clean exits 0 in all five with no credentials of any kind.

C3 — checked ghcr before pinning: 0.11.12 is the newest published tag (0.11.13 404s), and pulling it confirmed its GCP resource-type registry carries gcp_apigee_organizations, so the pre-flight will not reject the new default.

C7 — confirmed with a canary token, before and after:

PRE-FIX  LEAKCANARY occurrences: 6      (proxy-health; same count for environment-health)
POST-FIX LEAKCANARY occurrences: 0

The missing gcloud stub turned out worse than a one-file gap. harness.sh:89 unsets GCP_ACCESS_TOKEN deliberately, with the comment "No token and no gcloud: the 'could not run' state" — so the nocreds scenario depended on gcloud not being installed. On any Linux host that has it, the fallback minted a live ya29.… token and the assertion passed or failed depending on whose machine it ran on. Stub added, and the scenario now drives the branch explicitly via TOKEN_FAIL with the stub still on PATH, so it cannot silently become a no-op again.

C8 — the four prerequisite resources are out of environment-health/main.tf and into .test/apigee_prerequisites.sh, byte-identical in all five. Org creation accepts 409 and falls through to the shared ACTIVE poll; the network and address creates ignore their own exit status and verify by describe, so a concurrent bootstrap is a no-op rather than a coin flip. Teardown disables apigee + apigeeconnect only. An existing apigee-peering-${suffix} range is adopted rather than duplicated, so a project bootstrapped before this does not end up with two.

check-shared-drift.sh checksums the duplicated files against the siblings and fails task ci on divergence, naming the file and the bundles that disagree.

One collision the review had not spotted

product-governance/.test/validate-all-tests.sh was the live tier — same filename that means "the offline aggregate" in security-config and traffic-health. Renamed to test-live.sh, and the bundle got a real validate-all-tests.sh matching the others.

Verification

task ci green in all five: offline tier + render tier + generation-rule validation against the live published schema + drift check.

The render tier had been reporting SKIPPED: jinja2/pyyaml not installed and exiting 0. I installed them and ran it for real — 83 assertions that had been passing as skips.

Offline assertion counts, before → after:

bundle before after
environment-health 133 168
product-governance 149 185
proxy-health 301 337
security-config 156 191
traffic-health 152 185

The additions are static assertions on the vocabulary, C1, C3 and the stub directory — the thing that stops the five drifting apart again one convenience rename at a time. Each was negative-tested by reintroducing the defect: renaming ci back to run-mock-tests produces 7 failures, putting delete-slxs back in clean fails, unpinning the image fails, removing the gcloud stub fails. One of them was a false pass on the first attempt — proxy-health's helpers use grep -qF, where a needle containing a newline is read as two patterns, the second empty and matching every line. Fixed and re-tested.

Also: shellcheck clean over all 34 changed shell files, terraform validate passes, task --list hashes identically in all five.

What is NOT verified

Nothing in C8 has run against real GCP. bootstrap-prerequisites / destroy-prerequisites are syntax-checked, shellcheck-clean and guard correctly on missing credentials, but they want a full bootstrap → build → teardown cycle behind them before anyone trusts them — as the third comment says. The same applies to all five test-live tiers: they refuse correctly without credentials, but have never seen a live org.

That is the one piece I would want a real run behind before this merges.

…move

The prerequisites section still described the APIs, VPC, peering range and
Service Networking connection as Terraform resources in main.tf, and showed the
`terraform apply -target=...` command bootstrap-prerequisites used to run. All
four moved to apigee_prerequisites.sh in 3f85350, so that section documented a
file state that no longer exists.

Also corrected in the same section: disable_on_destroy no longer exists,
`terraform destroy` no longer owns the reserved range, the runtime instances no
longer carry a depends_on for destroy ordering (destroy-prerequisites refusing
while the org exists is what enforces it now), and `task clean` no longer needs
passwordless sudo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

C9 — the fixture-ordering dependency that C8 did not remove. This supersedes the scope of C8 in my correction above: the substrate boundary moves from "the org" to "the org, its environments, and the runtime instance."

The residual problem

C8 made every bundle self-sufficient for APIs, VPC, peering and the org. It did not make them self-sufficient for the two Apigee environments, which are still created only by gcp-apigee-environment-health's build-infra. Four bundles need them:

Bundle Needs
proxy-health ≥2 environments — the cross-env revision-drift fixture is meaningless with one
product-governance ≥1, named by APIGEE_TEST_ENV; API products reference it
security-config ≥1 with target servers to inspect
traffic-health proxies, which need environments to be deployed into

Run them out of order and you get 404s that read as broken code.

Only proxy-health guards against this today, via preflight_apigee_org.sh — and it asserts env_count -eq 0, i.e. "≥1 environment", when it actually needs two. product-governance has an inline existence check inside fixtures/create_entitlement_fixtures.sh. environment-health, security-config and traffic-health have no substrate check at all.

Why "let each bundle create its own environment" cannot work

An EVALUATION org is hard-capped, and both limits were hit empirically during #745 provisioning:

the number of environments cannot exceed 2 for TRIAL subscription
the number of instance cannot exceed the limit 1

Two environment slots, one instance slot, five bundles. Capped resources are inherently shared — which means they belong to the substrate by definition. Putting them in one bundle's fixtures was the original mis-assignment, and it is the same category error C8 fixed one level down.

The fix

1. Environments and the instance join the idempotent bootstrap

From codebundles/gcp-apigee-environment-health/.test/terraform/main.tf:

Line Resource Disposition
104 google_apigee_instance.primary bootstrap (eval cap of 1; cannot be per-bundle)
123 google_apigee_environment.healthy bootstrap (four bundles need it)
130 google_apigee_environment.unattached bootstrap (consumes the 2nd of 2 slots)
138 google_apigee_instance_attachment.healthy_primary bootstrap (needed for deploys and traffic)
113, 144 instance.secondary + healthy_secondary stay — fails on eval regardless; keep as an opt-in paid-org fixture
152, 159, 166 envgroups healthy / attachment / orphan stay — env-health's own fixtures, uncapped, suffixed
175, 186, 197 target servers healthy / disabled / dangling stay — same

Two environments serve all five. proxy-health gets its second from apigee-env-unattached-*: a proxy deploys to an environment with no instance attached and still appears under /organizations/{org}/deployments with environment and revision — probed 2026-08-10, HTTP 200. Caveat from that same probe: state came back null for both environments in the org-wide deployments view, so a check keying on deployment state rather than revision is unverified on this topology.

2. build-infra calls bootstrap-prerequisites itself

It is idempotent and no-ops in seconds once satisfied, so run order stops being something anyone has to know. Same argument as folding import-keystore-alias into build-infra.

3. A real preflight in all five

Generalise proxy-health/.test/preflight_apigee_org.sh into the duplicated block, and tighten it from "≥1 environment" to asserting the named contract below. That is what turns a 404 into an instruction.

The substrate contract — write it down

bootstrap-prerequisites guarantees, and preflight asserts:

org  <project>                     ACTIVE
env  apigee-env-healthy-<sfx>      ACTIVE, attached to the runtime instance
env  apigee-env-unattached-<sfx>   ACTIVE, deliberately NOT attached
     exactly one runtime instance  (EVALUATION cap)

⚠️ One invariant belongs in that file in bold: apigee-env-unattached-* being unattached is a fixture, not a defect. Once it is substrate rather than one bundle's own resource, someone will eventually "tidy up" by attaching it — which silently deletes environment-health's known-positive and makes check_instance_attachments pass for the wrong reason. That is exactly the failure mode this family keeps designing against, so it needs recording where the next person will look.

Cost

The first bundle to run pays ~25–45 min for runtime instance provisioning. That does not go away — but it stops mattering which bundle pays it, which is the point.

Sequencing

C9 folds into C8's step rather than being separate — both change what bootstrap-prerequisites and build-infra mean, and splitting them would mean rewriting the same tasks twice. Revised step 3 of the plan:

C8 + C9 — prerequisites and environments/instance off Terraform into an idempotent block duplicated across all five; build-infra calls it; shared preflight asserts the contract; concurrency (409 / ALREADY_EXISTS) handled; drift checksum in ci. Its own PR, with a full bootstrap → build → teardown cycle behind it before anyone trusts it.

…te too

C8 made every bundle self-sufficient for APIs, VPC, peering and the org. It did
not make them self-sufficient for the two Apigee ENVIRONMENTS, which only
gcp-apigee-environment-health created. Four bundles need them, so four bundles
were still silent guests of a fifth -- the same category error one level up.

An EVALUATION organization is hard-capped:

    the number of environments cannot exceed 2 for TRIAL subscription
    the number of instance cannot exceed the limit 1

Two environment slots and one instance slot, five bundles. A capped resource is
inherently shared, which makes it substrate by definition -- "let each bundle
create its own" is not available.

The substrate boundary therefore moves from "the org" to "the org, its two
environments, and the runtime instance". apigee_prerequisites.sh now creates
all of it: idempotent, 409-tolerant, byte-identical in all five, drift-checked
by `task ci`. Environment and instance creates are long-running operations, so
each is polled to ACTIVE -- the POST returns before the resource is usable and
anything that moves on immediately 404s on the next call.

build-infra calls bootstrap-prerequisites itself. It is idempotent and no-ops
in seconds once satisfied, so which bundle you run first stops being something
anyone has to know -- the same argument as folding the keystore import into
build-infra.

apigee_preflight.sh (new, shared) asserts the contract BY NAME before any
fixture is created. proxy-health's preflight_apigee_org.sh was the only
substrate check in the family and it checked the wrong thing:

    [ "$env_count" -eq 0 ] && fail "org has no environments."

That asserts "at least one" while the bundle needs two, and its bootstrap does
not fail without the second -- it does

    if [ -n "$env2" ]; then deploy_revision ... "$env2" ...; fi

so with one environment the cross-environment drift fixture is SILENTLY
SKIPPED and check_revision_drift.sh then reports clean, not because there is no
drift but because nothing was created to drift. A count cannot catch that.
environment-health, security-config and traffic-health had no check at all, so
their failure mode was a wall of 404s that reads as broken code.
preflight_apigee_org.sh is replaced by the shared script.

THE SUBSTRATE CONTRACT is written into apigee_prerequisites.sh, carrying the
invariant that will otherwise be tidied away: apigee-env-unattached-* being
unattached is a FIXTURE, not a defect. It is environment-health's known-positive
for check_instance_attachments and proxy-health's second environment. Attaching
it makes that check pass because there is nothing to find, which is not the same
as passing because the org is healthy. preflight warns if it has been attached.

Because the slots are capped, the substrate names cannot vary per bundle: two
different suffixes would want four environments. APIGEE_SUBSTRATE_SUFFIX is
therefore shared, and _check_environment_slots fails naming the occupying
environments rather than letting a quota error do it.

google_apigee_instance.secondary is now opt-in behind enable_secondary_instance
(default false). It had no count, so on an EVALUATION org `terraform apply`
failed on it every time -- meaning this configuration had never completed end to
end there. Kept as a multi-region failover fixture for a PAID org, which is what
"stays" can only sensibly mean.

Verification: offline assertions 168/185/337/191/185 -> 183/200/352/206/200, the
additions covering the contract, the preflight semantics and the build-infra
wiring; each negative-tested by reintroducing the defect (dropping the bootstrap
call, deleting the invariant, weakening the preflight to a count). The preflight
itself is exercised against canned API responses through all six branches --
contract satisfied, one environment only, no instance, healthy env unattached,
unattached env wrongly attached, and the object-shaped /environments response.
`task ci` green in all five; terraform validate and fmt pass; shellcheck clean.

Still unverified against real GCP, now including the environment and instance
provisioning. The two empirical claims this rests on -- the EVALUATION caps, and
that a proxy deployed to an unattached environment still appears under
/deployments with environment and revision -- are the reviewer's observations,
recorded as such in the contract along with the caveat that deployment `state`
came back null on that topology.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

C9 addressed at 99dd1e4, on this branch alongside C8. Every claim in the comment checks out; two are worse than described, and one line does not hold together as written.

Validated

preflight_apigee_org.sh does assert env_count -eq 0 — "at least one" — while the bundle needs two. The consequence is worse than "meaningless". bootstrap_apigee_fixtures.sh:280 guards the drift deploy:

env2=$(echo "$ENVS" | sed -n '2p')
...
if [ -n "$env2" ]; then
    deploy_revision "${SUFFIX}-proxy-drift" "$env2" "$rev2"
fi

So on a one-environment org the fixture is silently skipped and check_revision_drift.sh then reports clean — not because there is no drift, but because nothing was created to drift. No warning, no non-zero exit. That is the exact failure mode this family keeps designing against, sitting inside the one bundle that had a preflight at all.

product-governance has the inline check as described. environment-health, security-config and traffic-health have no substrate check whatsoever — confirmed by grep, not inference.

Your line numbers are from pre-C8 7a15829; every resource identity maps cleanly onto the current file, so the disposition table applied as written.

One thing that does not hold together

google_apigee_instance.secondary has no count — it is unconditional. If the instance cap really is 1, terraform apply has failed on it on every eval-org run, which means this configuration has never completed end to end there. "Stay" therefore cannot mean "leave as-is"; the only reading consistent with your own "keep as an opt-in paid-org fixture" is a variable. It is now enable_secondary_instance, defaulting to false, with the cap quoted in the variable description. Same for healthy_secondary.

Worth confirming this is what you meant, because it also implies the "instance creation can race a freshly-created peering" note in the README was observed on a paid org, or was the cap failing rather than a race.

What landed

The substrate boundary moved to org + both environments + the runtime instance. apigee_prerequisites.sh creates all of it: idempotent, 409-tolerant, byte-identical in all five, drift-checked by task ci.

One thing your disposition table did not mention and that bit immediately: environment and instance creates are long-running operations. The POST returns before the resource is usable, so anything that creates one and moves on 404s on the very next call. Each is polled to ACTIVE — 10 minutes for an environment, 60 for an instance.

build-infra calls bootstrap-prerequisites then preflight, so run order stops being something to know.

apigee_preflight.sh (new, shared, replaces preflight_apigee_org.sh) asserts the contract by name. It also reads /environments as the bare array the API actually returns — that endpoint is one of the undocumented ones, and reading it as an object would make every assertion pass vacuously, which is the trap this family has now hit three times.

It warns rather than fails if apigee-env-unattached-* has been attached. The substrate is still usable at that point; what has gone is one bundle's known-positive, so it needs saying loudly without blocking a run that will otherwise work.

The contract is written into apigee_prerequisites.sh, with the invariant in the bold form you asked for, both reasons it matters (the check_instance_attachments known-positive and proxy-health's second environment), and your probe recorded as a probe — including the state: null caveat.

One suffix across all five. Capped slots mean the substrate names cannot vary per bundle: two suffixes would want four environments. APIGEE_SUBSTRATE_SUFFIX is shared, and _check_environment_slots fails naming the occupying environments rather than letting a quota error do it.

Verification

Offline assertions 168/185/337/191/185 → 183/200/352/206/200. New assertions negative-tested by reintroducing the defect — dropping the bootstrap call from build-infra, deleting the invariant from the contract, weakening the preflight from names to a count; each fails as intended.

The preflight itself is exercised against canned API responses through all six branches: contract satisfied, one environment only (the silent-skip case), no instance, healthy env unattached, unattached env wrongly attached, and the object-shaped /environments response.

task ci green in all five, task clean exits 0 everywhere with no credentials, terraform validate and fmt pass, shellcheck clean, task --list still byte-identical across all five (now with preflight).

Still unproven

Everything in C8 and C9 remains unexecuted against real GCP, now including environment and instance provisioning. The two empirical claims underneath C9 — the EVALUATION caps, and that a proxy in an unattached environment still appears under /deployments with its revision — are your observations, recorded as such rather than as anything verified here.

I have written up the full bootstrap → build → teardown cycle as a runbook so that gap can actually be closed; happy to attach it here if useful.

Rohit-Ekbote and others added 5 commits August 19, 2026 13:41
…by the substrate suffix

Introduced by C9 and caught while re-reading the runbook against the code.

test-live.sh built the expected environment name from TF_VAR_resource_suffix:

    SUFFIX="${TF_VAR_resource_suffix:-${RESOURCE_SUFFIX:-test001}}"
    assert_has "unattached environment" ... "apigee-env-unattached-${SUFFIX}"

but C9 names the environments from APIGEE_SUBSTRATE_SUFFIX, which is a
different value by design: the substrate slots are capped and shared, so all
five bundles must agree on that suffix, while each bundle's own fixtures are
free to use a per-run one. The moment the two diverge -- which is exactly the
case the contract documents and tells operators to set explicitly -- the
assertion looks for an environment nobody created.

This is the same trap the keystore-import comment already warns about one
task over, so the fix is the same: resolve the substrate suffix separately,
with the same precedence chain apigee_prerequisites.sh uses, and use it only
for the substrate name. The envgroup, target-server and keystore assertions
keep the per-run suffix, because those fixtures really are this bundle's.

The offline tier now asserts the split, so it cannot silently regress:
183 -> 186 assertions, negative-tested by reverting the name to ${SUFFIX}
(both the positive and the negative assertion fire).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
… that lied

Found by running `task ci` in codecollection-devtools from an interactive shell
that had sourced load-credentials.sh. Three separate defects, all of the same
family: a test that reports a different answer depending on where it runs.

1. THE OFFLINE TIERS INHERITED THE CALLER'S CREDENTIALS.

load-credentials.sh ends with

    export APIGEE_ORG GCP_PROJECT_ID TF_VAR_org_id TF_VAR_project_id

and tf.secret is itself a file of `export TF_VAR_...` lines. Sourcing either --
the documented way to get credentials, and what every live task does -- leaves
them set for the rest of the shell session. The offline tier then read a REAL
organization where a fixture was intended:

  * environment-health: the credential-contract scenario stopped failing on a
    tf.secret naming no org, because load-credentials.sh prefers an ambient
    APIGEE_ORG/TF_VAR_org_id over the file. try_load() sanitised APIGEE_ORG and
    GCP_PROJECT_ID but not the TF_VAR_ spellings.
  * product-governance: all four org-resolution scenarios resolved the ambient
    org instead of the fixture's, so the projectId-filtering regression test
    asserted nothing.

Seven assertions failed in a polluted shell and passed in a clean one. "No
cloud, no credentials, no spend" has to mean the tier cannot SEE the caller's
credentials, not that it politely declines to use them, so all five runners now
unset the whole set once, before any scenario runs.

2. grep OPTION INJECTION MADE AN ASSERTION PASS FOR THE WRONG REASON.

    for unwanted in '- project' ...; do
      if printf '%s' "$GR_CODE" | grep -qF "$unwanted"; then

The needle starts with a dash, so grep parsed it as an option, printed
"grep: invalid option -- ' '", exited non-zero, and the else-branch reported
PASS. The generation rule was never actually checked for `- project`. Every
grep taking a variable pattern now passes `--`.

3. SIGPIPE + pipefail TURNED A SUCCESSFUL MATCH INTO "not found".

proxy-health's assert_contains/assert_lacks did

    printf '%s\n' "$haystack" | grep -qF -- "$needle"

Under `set -o pipefail`, grep -q exits on the first match while printf is still
writing, printf takes SIGPIPE (141), and pipefail reports the pipeline as
failed -- so a match reads as "not found". It only bites when the haystack is
large enough that printf has not finished, which is why it was invisible for
every small assertion, and it is image-dependent: GNU grep 3.8 in
codecollection-devtools triggers it, debian:stable-slim does not. In
assert_lacks the consequence is inverted and worse -- a present needle looks
absent, so the assertion PASSES on the very thing it forbids. Both now use a
herestring, which has no pipeline.

Verification: every tier green both in a clean environment and with
APIGEE_ORG/GCP_PROJECT_ID/TF_VAR_org_id/TF_VAR_project_id exported --
188/203/354/208/202, up from 186/200/352/206/200. The new assertions cover the
unset block and the `--` guard. shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
`task ci` -- the credential-free gate this family is meant to run through --
could not complete in codecollection-devtools, the container everyone tests in.
validate-generation-rules shells out to ajv-cli, and that image ships no node,
no npm and no ajv, so the gate died on its last step with

    Error: ajv is required but not installed.

ajv stays the preferred validator: it is the collection convention (44 of the 47
bundles that validate rules use it) and it is what CI has. What changes is that
its absence is no longer fatal when another validator exists. The body moves out
of five copies of the Taskfile into validate_generation_rules.sh -- shared,
byte-identical, drift-checked -- which picks ajv, falls back to Python's
jsonschema, and FAILS when neither is present, naming both install routes.

The two obvious alternatives are both wrong, and both already exist in this
collection:

  * Skip when ajv is missing. The three non-ajv bundles here print
    "Warning: ... Skipping validation" and exit 0. An absent check reports the
    same green as a passing one -- the failure this family keeps designing
    against.
  * Downgrade to yaml.safe_load(). That proves the file is YAML, not that it
    satisfies the schema, so a rule gating on a resource type that does not
    exist sails through and discovery silently produces zero SLXs.

Equivalence checked, not assumed: ajv and jsonschema agree on all five bundles'
current rules, and both reject a rule with a non-string apiVersion and a scalar
where resourceTypes must be an array. jsonschema additionally reports every
error with its path, which ajv does not. The ajv-free path was exercised inside
the devtools image itself -- refusing correctly with no validator installed,
passing with `pip install jsonschema`, and rejecting a deliberately corrupted
rule.

The assertions that guarded the old inline body (checked schema download, JSON
sanity check, empty-ruleset error, no swallowed failures) now read the script
that owns them, plus new ones for the fallback and the never-skip rule.

One of those new assertions failed on first run, for the reason this codebase
has now recorded three times: the script's own header QUOTES "Skipping
validation" as the thing it must not do, so matching the whole file failed on
the prose. Comments are stripped first, as they already are for the generation
rule and the Taskfile.

Verification: `task ci` exits 0 in all five on a host with ajv; offline tiers
188/203/354/215/209; drift clean; task --list still byte-identical; shellcheck
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
…t on PATH

`task clean` in codecollection-devtools ended with

    "gcloud": executable file not found in $PATH
    task: Failed to run task "clean": ... exit status 127

which names neither the problem nor the fix, and reads as a task crash.

activate-gcloud.sh accepts three credential routes and its header says failing
hard is right but "failing on the absence of one particular file, when another
valid path exists, is not". A missing gcloud broke that promise two ways:

  * with gcp.json.secret present, the unguarded activate-service-account died
    on a bare 127;
  * without it, _ag_have_token swallowed the same 127 through `|| true`, so the
    run reached the credentials advice at the bottom and told the operator to
    supply credentials they already had.

Neither is a credentials problem. Every route this script accepts still needs
gcloud to spend the credential, so the binary is checked first and reported as
the tooling gap it is.

AND THE ADVICE HAD TO BE RIGHT, WHICH TOOK A CORRECTION.

I had previously reported that codecollection-devtools ships no gcloud, and
wrote the first version of this message accordingly. That was wrong, and wrong
because of how I measured: my probes ran `bash -lc`, and a LOGIN shell rebuilds
PATH from /etc/profile, discarding the image's ENV PATH. The SDK is there, at
~/google-cloud-sdk/bin, and a non-login `docker exec` finds it. So the common
case is not "not installed" but "installed and dropped by this shell" -- and
telling someone to install a tool they already have is worse than saying
nothing.

The check therefore looks before it advises: it probes the usual SDK locations
and either names the path it found with the exact `export PATH=...` line, or
points at the installer when it genuinely is not there. Both branches were
exercised -- the first inside the running devtools container reproducing the
operator's own login shell, the second in debian:stable-slim.

The two activate-service-account calls are also checked now, so a key file that
gcloud REJECTS says so instead of falling through to "no usable credentials".

node, npm and ajv really are absent from that image in both shell kinds, so the
preceding jsonschema-fallback commit stands unaffected.

Four harness cases cover it -- tooling-gap wording, no 127 masking, the
installed-but-off-PATH advice, and the genuinely-absent advice -- each
negative-tested by neutering the guard: 358 passed, dropping to 354 with 4
failures when reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
…ance

Confirmed decision, so it stops being an inference and gets a guard.

enable_secondary_instance (default false) already gated the second runtime
instance and its attachment from C9. Nothing enforced that. Removing the count
would be a one-line, plausible-looking edit that breaks EVERY build-infra on an
EVALUATION organization -- the cap is one instance, and applying a second fails
with "the number of instance cannot exceed the limit 1". That is precisely how
this configuration came to have never completed end to end on an eval org in
the first place, so it is worth a test rather than a comment.

Six assertions: both resources gated, the variable present, its default really
false, and -- since env-health's offline tier did not read its Terraform at all
until now -- that this state no longer owns the primary instance or either
substrate environment, which C9 moved out and five bundles now share.

Each negative-tested by making the regression: removing the count fails "the
second runtime instance is gated", flipping the default fails "the default
really is false". terraform validate and fmt still pass; removing the count
from both resources would also break `secondary[0]` at validate time, so the
guard is layered rather than sole.

terraform.tfvars.example did not mention the flag, so a paid-org operator had
no way to discover it while instance_region sat above looking unconditional.
Both are now documented together, with the cap that motivates the default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

enable_secondary_instance confirmed — the second runtime instance stays opt-in, defaulting to false, kept as a multi-region failover fixture for a PAID organization. That closes the one open question from my C9 reply.

It now has a guard (fcfdc2d). Dropping the count is a one-line, plausible-looking edit that breaks every build-infra on an EVALUATION org, which is how this configuration came to have never completed end to end there. Six assertions cover it, each negative-tested by making the regression; terraform.tfvars.example documents the flag, which it did not before, so a paid-org operator can actually find it.

Everything else since 99dd1e4

All of it came out of running the tiers in codecollection-devtools rather than on a laptop, which is where the interesting failures were.

f61851b — a bug C9 introduced. test-live.sh built the expected environment name from TF_VAR_resource_suffix while the substrate names it from APIGEE_SUBSTRATE_SUFFIX. Those are deliberately different values, so the moment they diverge the assertion looks for an environment nobody created.

01aa5a0 — three defects, all "the test reports a different answer depending on where it runs".

The offline tiers inherited the caller's credentials. load-credentials.sh exports TF_VAR_org_id/TF_VAR_project_id, so sourcing it once — the documented way to get credentials — leaves them set for the session. The tier then read a REAL org where a fixture was intended: environment-health's credential-contract scenario stopped failing on a tf.secret naming no org, and all four of product-governance's org-resolution scenarios resolved the ambient org. Seven assertions failed in a polluted shell and passed in a clean one. All five runners now sanitise.

A grep needle beginning with - was parsed as an option. grep -qF "$unwanted" with unwanted='- project' errored, and the else-branch printed PASS. That generation-rule check had never actually run.

SIGPIPE + pipefail turned a successful match into "not found". proxy-health's assert_contains did printf | grep -qF; grep exits on first match, printf takes SIGPIPE, pipefail reports failure. Only bites on haystacks large enough that printf is still writing, and it is image-dependent — GNU grep 3.8 in codecollection-devtools triggers it, debian:stable-slim does not. In assert_lacks the consequence inverts: a present needle looks absent, so the assertion passes on the very thing it forbids. Both now use a herestring.

2799dc9task ci could not complete in the devtools image. validate-generation-rules shells out to ajv-cli and that image has no node. ajv stays preferred (44 of 47 bundles, and what CI has); its absence is simply no longer fatal when Python's jsonschema is present. The body moved into a shared, drift-checked validate_generation_rules.sh.

The two easy alternatives are both already in this collection and both wrong: the three non-ajv bundles print Skipping validation and exit 0 (an absent check reports the same green as a passing one), and downgrading to yaml.safe_load() proves the file is YAML, not that it satisfies the schema. Equivalence was checked rather than assumed — both validators accept all five current rules and both reject a rule with a non-string apiVersion and a scalar where resourceTypes must be an array.

ee08049 — and a correction. task clean ended with "gcloud": executable file not found in $PATH and exit 127, naming neither problem nor fix.

I had reported earlier that the devtools image ships no gcloud. That was wrong. My probes used bash -lc, and a login shell rebuilds PATH from /etc/profile, discarding the image's ENV PATH. The SDK is at ~/google-cloud-sdk/bin and a non-login docker exec finds it. So the common case is not "not installed" but "installed and dropped by this shell", and the first version of my fix told operators to install a tool they already had. The check now probes the usual SDK locations and either prints the exact export PATH=… line for what it found, or points at the installer. node/npm/ajv are genuinely absent in both shell kinds, so the commit above is unaffected.

State

Offline assertions 194 / 203 / 358 / 215 / 209. task ci exits 0 in all five, on a host with ajv and in the devtools container with pip install --user jsonschema. Drift clean, task --list byte-identical, terraform validate/fmt pass, shellcheck clean.

Unchanged: C8 and C9 have still never run against real GCP, and the two empirical claims under C9 remain your observations rather than anything verified here. Everything above is offline work; it does not move that needle.

Rohit-Ekbote and others added 2 commits August 21, 2026 11:10
…running op

Found by running bootstrap-prerequisites against a real project for the first
time. Bootstrap reported

    attached 'apigee-env-healthy-apigee' to 'apigee-inst-primary-apigee'
    Substrate ready:

and `task preflight`, immediately afterwards, disagreed in all five bundles:

    PREFLIGHT FAILED: environment 'apigee-env-healthy-apigee' is not attached

preflight was right. The attachments list was {} and a fresh POST explained why:

    400 FAILED_PRECONDITION -- the resource is locked by another operation that
    is 25 percent completed so far where environment ... is currently being
    attached ... by operation d9613ba8

Attaching an environment to a runtime instance is a long-running operation, the
same as the environment and instance creates this script already polls. That
fact is stated in this file's own header and was applied to two of the three
places it holds. _ensure_attachment posted, took the 200, printed "attached"
and returned -- so bootstrap declared the substrate ready while provisioning
was a quarter done, and every consumer that believed it would have deployed
into an environment that could not yet serve traffic.

The attachments list is also why retrying is not free: it stays EMPTY during
provisioning, so absent-because-provisioning is indistinguishable from
absent-because-missing and the existence check would happily POST again. Apigee
rejects that with 400 FAILED_PRECONDITION naming the in-flight operation --
which is now treated as "someone is already doing it" and falls through to the
same wait, the correct outcome for a concurrent bootstrap as well. Any other
400 is still a hard error.

Observed timings from this run, which the comments previously only guessed at:
org ~2.5 min, each environment under a minute, the runtime instance ~20 min,
the attachment several minutes more.

Everything else the run exercised behaved as designed: steps 1-4 no-op'd on
already-present APIs, VPC and peering; adopt_existing_range() reused the legacy
apigee-peering-<suffix> reservation rather than reserving a second /21; both
environments were created inside the two-slot EVALUATION cap; the single
instance slot was free. It was run from gcp-apigee-proxy-health, deliberately
NOT environment-health, which is the C8/C9 claim that the bootstrapping bundle
does not matter.

Verified: `task preflight` now exits 0 with "substrate contract satisfied" in
all five bundles against the live org. Offline tiers unchanged at
194/203/358/215/209, drift clean, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
…hecks

Three defects, all found by running the live tier against a real org for the
first time. It had never executed, so none of them had ever surfaced.

1. EVERY TASK FAILED IN SUITE SETUP, BEFORE ANY CHECK RAN.

    [ WARN ] File 'discover_topology.sh' not found in '.../run'
    FileNotFoundError: Could not find the robot file in any known locations.
    7 tasks, 0 passed, 7 failed

RW.CLI resolves each script relative to the WORKING directory. This script runs
robot from a scratch dir on purpose -- the checks write their *_issues.json into
the cwd, and asserting on those artifacts beats scraping log.html -- but nothing
put the scripts there. The sibling it was modelled on avoids the problem by
running from the bundle root, which resolves the scripts and drops every issue
file into the source tree. Seeding copies into the scratch dir keeps both
properties, and is what gcp-apigee-proxy-health's offline harness already does
per scenario.

2. AN ABSENT TOPOLOGY WAS A SYNTAX ERROR, NOT A FAILED ASSERTION.

`[ "$(jq '.environments | length' ...)" -ge 2 ]` with no file left the left side
empty, so bash printed "integer expression expected" into the middle of the
results instead of the check reporting failure.

3. THE ASSERTIONS READ THE WRONG FIELD -- IN BOTH DIRECTIONS.

Titles carry the failure mode and the org scope only; the resource names live in
details. That was a deliberate change (one issue aggregates every affected
resource, so a title naming one of them is both wrong and unstable run to run),
and the offline tiers assert it directly -- "no contained resource name in any
title". The live tier was never re-run afterwards and still asserted the old
shape, so:

  * the known-POSITIVES reported four fixtures as undetected when every one had
    been found correctly, the names sitting in details the whole time;
  * the known-NEGATIVES, which use assert_hasnt, passed without testing
    anything at all -- a healthy fixture's name can never appear in a title, so
    those two ticks were green for the wrong reason and would have stayed green
    through a real regression.

A bodies() helper joins title + details + actual, and every resource-name
assertion now uses it.

Verified against runwhen-nonprod-sandbox: 16 passed, 0 failed, exit 0, with
APIGEE_ORG deliberately empty so the run exercises org auto-discovery. Every
known-positive fires, every known-negative stays silent. Offline tier gains a
guard for the seeding line (195 passed), negative-tested by removing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote
Rohit-Ekbote force-pushed the apigee-pending-cbs-uniformity-changes branch from 1204a0a to cbe7fd5 Compare August 21, 2026 06:09
Rohit-Ekbote and others added 3 commits August 21, 2026 14:11
…ally

Found by running the live tier in the four bundles that had never run it.
proxy-health and security-config both died before their first assertion:

    GCP_PROJECT_ID: Must set GCP_PROJECT_ID (or TF_VAR_project_id)
    APIGEE_ORG: Must set APIGEE_ORG (or TF_VAR_org_id)

Nothing sourced the credential contract. Across the five bundles the task body
had drifted three ways:

  * product-governance   task sourced load-credentials.sh   -> worked
  * environment-health   script sourced tf.secret itself    -> worked
  * proxy-health,
    security-config,
    traffic-health       neither                            -> failed

Two of those spellings work and one does not, which is precisely what the
standard vocabulary exists to stop -- and the three that failed are the three
whose live tier had never been executed, so the drift was invisible.

load-credentials.sh is now sourced in the task, in all five, byte-identically.
The task is the right place: it is the documented contract ("source this from
.test tasks that need live credentials"), it normalises the organization name
across the several spellings tf.secret is written in, and it exits non-zero
when credentials are absent rather than letting the run proceed against
nothing. Sourcing it also exports every TF_VAR_ in the file, so the scripts
need nothing of their own.

The offline tier now asserts the task sources it and runs the script, so a
fourth spelling cannot appear unnoticed: 195/215/209 -> 197/217/211.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
C1 removed an unconditional `sudo rm -rf output` from clean-rwl-discovery. The
identical line twenty lines away in run-rwl-discovery was left alone, and it
killed the task on the first line, before it even pulled an image:

    sudo: a password is required
    Failed to remove output directory
    task: Failed to run task "run-rwl-discovery": exit status 1

Same reasoning as C1: output/ is root-owned only because a PREVIOUS discovery
container created it. On a fresh checkout there is nothing to remove and sudo is
pure cost; on any host without passwordless sudo it is fatal. Skip when the
directory is absent, try plain rm, fall back to sudo, and if neither works name
the path to remove by hand instead of "Failed to remove output directory".

The C1 commit message even states the rule -- "an unconditional `sudo rm -rf`
made `task clean` fail outright on any machine without passwordless sudo" -- so
the miss was not a misunderstanding, it was not looking for the same pattern
elsewhere in the file. It stayed hidden because nothing had run discovery; it
failed the moment something did.

Guarded in the offline tier, keyed on the OLD form's error string rather than on
"sudo rm -rf output": the fixed form contains that too, as its fallback arm, so
the first version of the assertion failed against correct code.

197/217/211 -> 199/219/213. task --list still byte-identical across all five,
drift clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
Two bundles rendered with the generic default icon in the UI, because the icons
they named are not there:

    icons/gcp/apigee_api_platform/apigee_api_platform.svg   HTTP 200
    icons/gcp/apigee/apigee.svg                             HTTP 403
    icons/gcp/access-context-manager/access-context-manager.svg  HTTP 403

gcp-apigee-traffic-health pointed at the second and gcp-apigee-security-config
at the third -- which is not an Apigee icon at all, but Access Context Manager,
a different GCP service. Both now use apigee_api_platform, matching the three
bundles that already did.

A missing icon fails silently: the UI substitutes its default, so nothing in a
discovery run, an upload, or any test tier reports it. It was visible only by
looking at five SLXs side by side in the workspace. The offline tier now asserts
the shared icon is used and neither wrong URL comes back.

199/219/213 -> 202/222/216.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HotdXokWQtJNYi2WPs4LkF
@Rohit-Ekbote

Copy link
Copy Markdown
Contributor Author

C8 and C9 are no longer unproven. The whole family has now been exercised end to end against a real GCP project — bootstrap, fixtures, live assertions, discovery and upload — and that run found eight defects that no amount of offline testing had surfaced. All eight are fixed and pushed.

What ran

runwhen-nonprod-sandbox, from a standing start: the project had the APIs, a VPC and a reserved /21 peering range, but no Apigee organization.

step result
bootstrap-prerequisites org + 2 environments + runtime instance, ~25 min
build-infra 5/5
test-live 5/5 (proxy-health passes except one assertion — see below)
run-rwl-discovery 5/5, one SLX each, on the pinned 0.11.12
upload-slxs 5/5 into the labs workspace, SLX + runbook, HTTP 200

Deliberately bootstrapped from gcp-apigee-proxy-health, not environment-health — that is C8/C9's central claim, that the bootstrapping bundle no longer matters. Before C8 it was impossible: a second Terraform state cannot adopt the first's VPC.

Timings, replacing the guesses in the comments: org ~2.5 min, each environment under a minute, runtime instance ~20 min, attachment several minutes more. Second run of the full bootstrap no-op'd in under a minute, which is what makes build-infra calling it unconditionally viable.

The EVALUATION caps behaved exactly as you described — two environment slots, one instance slot, both fitting. adopt_existing_range() reused the legacy apigee-peering-<suffix> reservation rather than reserving a second /21, from three different bundles.

What discovery proved

Every SLX qualifies on {resource: runwhen-nonprod-sandbox}anchored on the organization, not the project — with APIGEE_ORG resolved from the matched org into configProvided, one SLX per bundle rather than per-proxy fan-out, and no sli.yaml anywhere. That is the rule/template work, previously only asserted statically, confirmed by real generation. C3's registry pre-flight also ran for the first time and passed.

The eight defects

1. The instance attachment was never awaited (42985b0). Bootstrap printed attached and Substrate ready while provisioning was 25% done; preflight immediately disagreed in all five bundles. Attaching is a long-running operation like the environment and instance creates — a fact stated in that file's own header and applied to two of the three places it holds.

2-4. test-live had never executed (cbe7fd5), and three things were wrong at once: RW.CLI resolves scripts relative to the working directory, so running from a scratch dir failed every task in suite setup (7 tasks, 0 passed) before a single check ran; an absent topology made [ "$(jq …)" -ge 2 ] a bash syntax error rather than a failed assertion; and the assertions read titles() when the title-scoping work had deliberately moved resource names into details.

That third one is the one worth dwelling on. The known-positives reported four fixtures as undetected when every one had been found correctly. The known-negatives were worse: they use assert_hasnt on titles, and a healthy fixture's name can never appear in a title — so those two ticks were green without testing anything at all, and would have stayed green through a real regression.

5. test-live did not resolve credentials, in three of five (044392a). The task body had drifted three ways — one bundle sourced load-credentials.sh in the task, one sourced tf.secret in the script, three did neither. Two spellings work and one does not; the three that failed are exactly the three whose live tier had never run.

6. run-rwl-discovery forced sudo (7045ca0). C1 removed an unconditional sudo rm -rf output from clean-rwl-discovery and left the identical line twenty lines away in run-rwl-discovery. It killed discovery on the first line, before pulling an image. The C1 commit message even states the rule; the miss was not looking for the same pattern elsewhere in the same file.

7. Two SLXs used icons that do not exist (6de6982). traffic-health pointed at icons/gcp/apigee/apigee.svg and security-config at access-context-manager — a different GCP service entirely. Both 403, so the UI silently substituted its default. Nothing reports a missing icon: discovery succeeds, upload returns 200, every tier passes.

8. Left open deliberately — analyze_latency_split.sh queries a function Apigee rejects.

HTTP 400 — invalid metrics - function for metric "total_response_time" not supported: "p95"

Probed the alternatives: avg, max and sum return 200; p95, p99 and percentile95 all 400. Apigee Analytics supports no percentile function on this metric, so this check has never returned latency data against a real org — it can only ever emit "API calls failed", which is what it did. Everything downstream behaved correctly: the check degraded honestly, the API-error ledger recorded it, and the live assertion refused to call the run trustworthy. The offline tier passes because fixtures encode p95 responses as if the API accepted them.

I have not fixed it because the replacement is a semantic choice, not a rename — max is closest to tail-latency intent but one outlier dominates it; avg is stable but hides the tail the check exists to catch; a real percentile needs per-request data. p95 is also baked into the runbook, both READMEs, SKILL-TEMPLATE.md, six fixture files and the offline harness. That is your call, not mine.

Unrelated finding: runwhen-local#832

Apigee discovery fails on every ADC-authenticated run since 0.11.10 — 'NoneType' object has no attribute 'before_request', Total SLXs: 0, exit 0. Independently reproduced here and confirmed fixed in 832-merge-71f3dad2; posted the A/B there. It does not affect this branch — generate-rwl-config always writes applicationCredentialsFile, so these bundles structurally cannot reach the ADC path — but it does mean the five green SLXs certify the rule and templates, not ADC-based discovery.

State

Offline assertions 133/149/301/156/152 → 202/203/358/222/216, every addition negative-tested by reintroducing the defect it guards. task ci green in all five, task clean exits 0 with no credentials, task --list byte-identical, drift clean, shellcheck clean, terraform validates.

The sandbox is still provisioned and billing — teardown is task clean per bundle, delete the org, then destroy-prerequisites. That reverse path has not been exercised and, on this evidence, will probably find bugs of its own.

@Rohit-Ekbote
Rohit-Ekbote marked this pull request as ready for review August 25, 2026 06:00
@Rohit-Ekbote
Rohit-Ekbote requested a review from a team as a code owner August 25, 2026 06:00
@Rohit-Ekbote
Rohit-Ekbote merged commit 8214bb8 into main Aug 25, 2026
8 checks passed
@Rohit-Ekbote
Rohit-Ekbote deleted the apigee-pending-cbs-uniformity-changes branch August 25, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants