[Creator] [design-spec] gcp-apigee-traffic-health - #731
[Creator] [design-spec] gcp-apigee-traffic-health#731rw-codebundle-agent[bot] wants to merge 5 commits into
Conversation
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
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
Rohit-Ekbote
left a comment
There was a problem hiding this comment.
Verified this against the design spec — the org anchoring, qualifier fix, auth gate, discovery-in-Suite-Initialization move and runbook-only changes all check out.
Evidence from a local run at 9973fd5:
| Check | Result |
|---|---|
.test/offline/run.sh |
111 passed, 0 failed |
.test/render/run.sh |
19 passed, 0 failed |
shellcheck -S style *.sh .test/**/*.sh |
clean |
| Mutation pass (14 mutations, scratch copies) | 14/14 turned the expected tier red |
The mutation pass includes the ones specific to this PR: reverting qualifiers to ["project", "resource"], re-adding type: sli and type: slo to outputItems, and moving discovery back out of Suite Initialization into a task — each caught.
I also reproduced the original defect before the fix. Rendering the pre-fix template through runwhen-local's jinja2 config (SandboxedEnvironment, trim_blocks, lstrip_blocks, CustomUndefined) gave:
APIGEE_ORG -> 'missing_workspaceInfo_custom_variable'
alias -> 'GCP Apigee Traffic and Performance Health for Organization
missing_workspaceInfo_custom_variable'
since match_resource.resource_name is not an attribute runwhen-local builds and CustomUndefined.__str__ returns a placeholder rather than "". It now resolves correctly, and the render tier catches a revert in seconds — which the template grep alone would not.
Fixture provenance re-checked against the Apigee v1 discovery document rather than taken on trust: apis.list returns GoogleCloudApigeeV1ListApiProxiesResponse with the proxies field (matches the fixture), and environments/targetservers have no list method at all and return bare string arrays (also matches).
One non-blocking finding inline.
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
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
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
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
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
074c55c to
a15e946
Compare
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
|
Second pass, this time on the Now that this branch is rebased onto Re-verified at High1.
|
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
|
All nine items checked against the tree before changing anything; items 1–6 addressed in 1. 2. 3. 4. No shared credential contract — adopted 5. GCS bucket nobody reads — removed. Took your suggestion for what should replace it: the stub now publishes discovery ground truth ( 6. Unpinned image — pinned to 8. Tokens in xtrace — fixed, despite being pre-existing and non-blocking, because it is a live bearer credential in captured output and the fix is contained. Your note that the assignment leaks too was right and incomplete in one way worth recording: there is a third site. 7. 9. The gap the mock tier cannot close — agreed, and I have recorded it as a known gap in Verification at Not verified: the HCL. |
* [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>
* Adopt environment deployment coverage from the parallel PR #729 (#770)
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/issue 158 (#773)
* [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>
* Creator/issue 157 (#774)
* 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>
* gcp-apigee-*: one task vocabulary, self-sufficient substrate, xtrace 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
* gcp-apigee-environment-health: correct the .test README after the C8 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
* gcp-apigee-*: C9 -- environments and the runtime instance are substrate 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
* gcp-apigee-environment-health: test-live must name the substrate env 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
* gcp-apigee-*: make the offline tiers hermetic, and fix two assertions 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
* gcp-apigee-*: generation-rule validation no longer needs node
`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
* gcp-apigee-proxy-health: say what is actually wrong when gcloud is not 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
* gcp-apigee-environment-health: lock in the opt-in second runtime instance
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
* gcp-apigee-*: wait for the instance attachment, which is also a long-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
* gcp-apigee-environment-health: make test-live actually exercise the checks
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
* gcp-apigee-*: test-live must resolve credentials, in all five identically
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
* gcp-apigee-*: run-rwl-discovery must not force sudo either
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
* gcp-apigee-*: all five SLXs use the one Apigee icon that exists
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
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: rw-codebundle-agent[bot] <rw-codebundle-agent[bot]@users.noreply.github.com>
Automated CodeBundle from runwhen-contrib/codecollection-registry#158.
Source: runwhen-contrib/codecollection-registry#158
Author: rw-codebundle-agent