You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chore: second-pass review feedback across validator, skills, and workflows (#25)
* chore: second-pass review feedback across validator, skills, and workflows
Validator (scripts/validate_skill.py):
- Required-section emptiness check now uses H2-only positions so an
'## Workflow' with '### Step 1' subheadings is no longer flagged empty.
- New _strip_code_fences masks fenced regions to whitespace of equal
length, preserving byte offsets, so '## something' inside a 'bash'
fence cannot break section detection.
- validate_file and _load_skill_entry open files as utf-8-sig to tolerate
a BOM that some Windows editors insert.
- changed_modules warns to stderr and returns [] when both 'git diff'
invocations fail, instead of raising.
- main() reconfigures stdout/stderr to UTF-8 (errors=replace) so the
check/cross glyphs no longer crash Python 3.14 on a cp1252 Windows
console.
- Tests: four new cases cover fence stripping, subheadings inside
required sections, headings inside fences, and BOM-prefixed input.
58 pass.
github-actions skill:
- OIDC debug snippet decodes the JWT instead of dumping the request
wrapper (jq .value | cut -d. -f2 | base64 -d | jq .sub).
- REFERENCE.md 'packages: read|write' rows distinguish GitHub Packages
npm from GHCR; example dependabot.yml and dependabot-auto-merge.yml
now match the chassis this repo actually ships (deny-all workflow
scope, per-job opt-ins, 'set -euo pipefail' in inline shell).
openshift-deployment skill:
- Replaced the invalid 'oc get pods --field-selector=status.phase=
Pending,status.phase=Terminating' (duplicate key, and Terminating is
not a Pod phase) with the metadata.deletionTimestamp jq filter.
- Probe table now says '(150 s budget)' to match the YAML math
(periodSeconds: 5 * failureThreshold: 30).
- Patroni image bumped from patroni-postgres:12.4-2.0 (PG 12 EOL Nov
2024) to patroni-postgres:16.4-3.0.
azure-networking skill:
- Rewrote the privateEndpointNetworkPolicies = 'Disabled' rationale -
the previous wording had the semantics backwards. Documented all four
values (Disabled, NetworkSecurityGroupEnabled, RouteTableEnabled,
Enabled) and the consumer-side threat-model justification for the
BC Gov default.
- App Gateway NSG GatewayManager row labelled 'v2 SKU; v1 uses
65503-65534'.
- Tagged the untagged Terraform pseudocode fence as 'text'.
Workflows:
- pages.yml: workflow-scope 'permissions: {}' with per-job opt-ins. The
build job only reads the repo; pages:write + id-token:write are scoped
to the deploy job.
- pr.yml: added concurrency 'pr-${{ github.head_ref || github.ref }}'
with cancel-in-progress: true so a force-push supersedes stale runs.
Housekeeping:
- .gitignore: removed the duplicate Python block - every entry was
already covered earlier in the file.
- Internal meta-skills (skill-author, skill-validator) gained
owner: bcgov and tags in frontmatter to model best-practice frontmatter
that the spec recommends.
* style: ruff format new tests (blank-line + double-quoted string)
5.**Create the subnet with its NSG attached in the same API call** — declare `networkSecurityGroup` on the subnet body itself, not as a separate association. Tool specifics: Terraform — use `azapi_resource` with `networkSecurityGroup.id` inside `body.properties` (the `azurerm_subnet` + `azurerm_subnet_network_security_group_association` pair is two PUTs and leaves an unprotected gap); Bicep/ARM — set the `networkSecurityGroup` property on the subnet resource; az CLI — pass `--network-security-group` to `az network vnet subnet create`.
39
39
6.**Serialize sibling-subnet writes** — ARM serializes writes against the same VNet; parallel writes fail with `AnotherOperationInProgress`. Terraform — list every preceding sibling subnet in `depends_on`. Bicep — rely on the implicit resource graph, or add an explicit `dependsOn` array when siblings live in separate modules. az CLI / scripts — issue creates sequentially.
40
40
7.**(Terraform only) Lock the VNet** — add `locks = [data.azurerm_virtual_network.target.id]` to every subnet's `azapi_resource` so Terraform's own parallelism doesn't race ARM in the gap between dependent operations. Bicep/ARM deployments don't need this — the deployment engine doesn't parallelize within one scope.
41
-
8.**For PE subnets** — set `privateEndpointNetworkPolicies = "Disabled"` and no delegation. Size the CIDR for the worst-case service that will land on it (some services consume more than one IP per PE — see references).
41
+
8.**For PE subnets** — set `privateEndpointNetworkPolicies = "Disabled"`(the long-standing default, which means NSGs and UDRs on the subnet are **not** evaluated for the PE NIC) and no delegation. If you do need to override the PE's `/32` default route through a firewall, set `"RouteTableEnabled"` (or `"Enabled"` for both NSG and UDR); for defence-in-depth NSG enforcement on a PE NIC itself, use `"NetworkSecurityGroupEnabled"`. Size the CIDR for the worst-case service that will land on it (some services consume more than one IP per PE — see references).
42
42
9.**For App Gateway subnets** — no delegation. If asymmetric routing is observed and your platform allows custom Route Tables, attach one with `0.0.0.0/0 → Internet` (plus any internal corp ranges that must not egress via ExpressRoute / VWAN hub). **BC Gov Landing Zone**: Route Tables are platform-protected (see Step 1) — raise a Public Cloud Service Request for any required UDR.
43
43
10.**Expose subnet ID and CIDR as outputs** (Terraform `output`, Bicep `output`, deployment script return value) so downstream stacks can read them via `terraform_remote_state`, Bicep `existing` references, or another state-sharing mechanism.
44
44
11.**Validate before apply** — run the tool's validate + dry-run path: Terraform `fmt -recursive`, `validate`, `plan`; Bicep `az bicep build` then `az deployment ... what-if`; ARM `az deployment ... what-if`. Catch CIDR overlap and delegation errors before apply.
- Never share a delegated subnet between services. (Why: each subnet allows exactly one `serviceDelegations` entry; the second service errors at associate time.)
50
50
- Never set a delegation on a Private Endpoint or App Gateway subnet. (Why: Azure rejects PE creation in delegated subnets, and App Gateway refuses to launch in one.)
51
51
- Never move an existing Private Endpoint to a different subnet without a maintenance window. (Why: moving a PE destroys and recreates it, breaking the customer-facing DNS record until propagation completes.)
52
-
- Always set `privateEndpointNetworkPolicies = "Disabled"` on PE subnets. (Why: NSG enforcement on PE NICs requires the subnet flag to be off; otherwise PE traffic is silently dropped.)
52
+
- Always set `privateEndpointNetworkPolicies` explicitly on PE subnets — typically `"Disabled"`, which is the long-standing default and is what most BC Gov PE designs use. (Why: this property controls whether the subnet's NSG and UDR are evaluated for PE NIC traffic; `"Disabled"` skips both, `"NetworkSecurityGroupEnabled"` applies NSGs only, `"RouteTableEnabled"` applies UDRs only, `"Enabled"` applies both. Most threat models for private endpoints only need to govern the *consumer* side of the connection — the workload subnet talking to the PE — because the PE itself only exposes the upstream Azure service and has no arbitrary egress to control. Pin the value in IaC so behaviour is reproducible across regions and subscriptions where the platform default may have drifted. Switch to `"RouteTableEnabled"` (or `"Enabled"`) when you need a UDR to override the PE's `/32` default route through a firewall or NVA.)
53
53
- Always size PE subnets for the **worst-case** service that will land on them. (Why: Cosmos DB PE consumes 2 IPs — one global + one regional endpoint; Azure AI Services "AIServices" kind exposes up to 3 sub-resources = 3 IPs per PE. A `/27` fills surprisingly fast.)
54
54
- Always expose subnet IDs as outputs (Terraform `output`, Bicep `output`) even if no downstream stack reads them yet. (Why: adding outputs later is cheap, but a downstream stack that needs the ID can't read state it isn't exposed to.)
55
55
- Never modify VNet address space or create a Private DNS Zone — both are managed centrally by the platform team (see Step 1's platform-protected list; raise a [Public Cloud Service Request](https://citz-do.atlassian.net/servicedesk/customer/portal/3) instead).
- "Add a Container Apps Environment subnet" → declare the subnet with a `Microsoft.App/environments` delegation (Terraform `azapi_resource` body, Bicep `delegations` array on the subnet, or `az network vnet subnet create --delegations Microsoft.App/environments`); attach an NSG that allows outbound 443 to AzureContainerRegistry, AzureMonitor, AzureActiveDirectory, and VirtualNetwork; serialize against every other subnet in the VNet.
59
59
- "Deployment keeps failing intermittently with `AnotherOperationInProgress`" → sibling subnets are being written in parallel against the same VNet. Terraform — add `depends_on = [azapi_resource.first_subnet]` to the second, and `[azapi_resource.first_subnet, azapi_resource.second_subnet]` to a third. Bicep — add `dependsOn: [first, second]` when siblings span separate modules. CLI — stop backgrounding the `az network vnet subnet create` calls.
60
60
- "App Gateway deployment fails with `SubnetMustNotHaveDelegation`" → remove the delegation from the AppGW subnet (Terraform: drop the `delegations` block from the `azapi_resource` body; Bicep: remove the `delegations` array from the subnet; CLI: omit `--delegations`).
61
-
- "PE creation fails with `PrivateEndpointCannotBeCreatedInSubnet`" → subnet has a delegation, or `privateEndpointNetworkPolicies` is still `"Enabled"`. Fix both.
61
+
- "PE creation fails with `PrivateEndpointCannotBeCreatedInSubnet`" → subnet has a delegation incompatible with PEs, or `privateEndpointNetworkPolicies` is set to a value the PE provider's automation refuses. Set the property to `"Disabled"` and remove any delegation.
62
62
63
63
## Edge Cases
64
64
-**VNet owned by a separate IaC stack** — reference it as an existing resource in the current root (Terraform `data.azurerm_virtual_network`, Bicep `resource ... existing`); pin to its ID for the parent and (Terraform only) for `locks`. Do **not** try to import the VNet.
| 120 | Inbound |`GatewayManager` service tag | 65200–65535 | AGW infrastructure control |
226
+
| 120 | Inbound |`GatewayManager` service tag | 65200–65535 (v2 SKU; v1 uses 65503–65534) | AGW infrastructure control |
227
227
| 130 | Inbound |`AzureLoadBalancer` service tag | * | LB health probes |
228
228
229
229
Port 80 is intentionally not allowed — let App Gateway terminate HTTPS and skip an HTTP-to-HTTPS hop at the NSG layer.
@@ -273,4 +273,4 @@ App Gateway is one of the few subnets that often needs an explicit route table
273
273
|`NetworkSecurityGroupNotAssociated`| Subnet created without an NSG in its body under an NSG-at-creation policy | Attach the NSG inline at creation (Terraform `azapi_resource` with `networkSecurityGroup.id` in `body.properties`; Bicep `networkSecurityGroup` property on the subnet; CLI `--network-security-group`) |
274
274
|`SubnetConflictWithOtherSubnet`| CIDR overlap within the same address space | Re-check the allocation map; pick non-overlapping CIDRs |
275
275
|`AnotherOperationInProgress`| Concurrent subnet PUTs to the same VNet | Serialize subnet writes (Terraform `depends_on` chain + `locks`; Bicep `dependsOn` across modules; CLI — sequential calls) |
276
-
|`PrivateEndpointCannotBeCreatedInSubnet`| Subnet has a delegation, or `privateEndpointNetworkPolicies` is not `"Disabled"`| Fix both on the subnet |
276
+
|`PrivateEndpointCannotBeCreatedInSubnet`| Subnet has a delegation incompatible with PEs, or `privateEndpointNetworkPolicies` is set to a value the PE provider's automation refuses | Set the property to `"Disabled"` and remove any delegation|
0 commit comments