diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index d41ba25..a8dcd8e 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.4.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:a18f15d00b467f9585fecf7ee3230f00025f69726e17135f14eb99f3036b5827" - digest: "sha256:7d370fff3455208269852d3bd0f7e4eff1e61b3eb8e80df9c967245bf464cda5" + source_tree_digest: "sha256:66532e92cee2ba8351911491ea4d8267fe853db10ebe36559551cb21aad5f209" + digest: "sha256:02032875c4b1e479d953ba4e9b766cf260bc469a81c38856ab6a6c6044b1c147" projection: - input_digest: "sha256:88f5073a543617f2b37990db826b8777ef8c396eb658de403f60e2e480e15f62" - output_digest: "sha256:d76ab0b017214dce592c2ae8b23e30b89caabf5a0197be3b25dae6d948cd00aa" + input_digest: "sha256:02a7ff040caee7de0f5de1b2402485a7e6972b68e867413ad7a1d71add5d0336" + output_digest: "sha256:8fd7fc9e1adc79eec4876daaa1d9711882d3959584c92fc5c427c64ee6ecebce" files: - path: ".gds/compiled-policy.json" digest: "sha256:78d09606bb4168d74bce1f50ab62b46a7ded34652c6b23af1badfd26dd060e94" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:1508f13759d4a238114e35d7c6ee6e0f6d2b3b09e18d8fb7eff46b534143cb73" + digest: "sha256:184bc5f09a65158b07fe4d6948c4d805869ddb35f9dcfaf528c5f5b4720abebb" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index b25364c..5aafd3a 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.4.0-dev -# source-tree-digest: sha256:a18f15d00b467f9585fecf7ee3230f00025f69726e17135f14eb99f3036b5827 -# input-digest: sha256:88f5073a543617f2b37990db826b8777ef8c396eb658de403f60e2e480e15f62 +# source-tree-digest: sha256:66532e92cee2ba8351911491ea4d8267fe853db10ebe36559551cb21aad5f209 +# input-digest: sha256:02a7ff040caee7de0f5de1b2402485a7e6972b68e867413ad7a1d71add5d0336 # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml diff --git a/core/app/module_coverage.go b/core/app/module_coverage.go index 4f64246..419fa5a 100644 --- a/core/app/module_coverage.go +++ b/core/app/module_coverage.go @@ -127,6 +127,23 @@ func (services *Services) CoverModules( Evidence: map[string]any{"module": selected}, }) } + // Coverage over nothing is not coverage. Without this the command reports + // success having compared no module against any gate, which is + // indistinguishable from every module being covered -- and the estates most + // likely to hit it are the ones whose repositories were never declared as + // modules, which is exactly when someone is asking whether their gates are + // watched. + if len(data.Modules) == 0 { + findings = append(findings, domain.Finding{ + Code: "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN", Severity: domain.SeverityHigh, + Message: "No declared git-submodule-consumer relationship was covered, " + + "so no gate was compared.", + Evidence: map[string]any{ + "declared_relationships": len(consumer.Relationships), + "repository_id": consumer.Repository.ID, + }, + }) + } return domain.NewEnvelope(command, classifyFindings(findings), data, findings...) } diff --git a/core/app/module_coverage_test.go b/core/app/module_coverage_test.go new file mode 100644 index 0000000..75f75a5 --- /dev/null +++ b/core/app/module_coverage_test.go @@ -0,0 +1,44 @@ +package app + +import ( + "context" + "testing" + + "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" +) + +// TestModuleCoverageOverNothingIsNotProven covers the case the command was +// silent about. An estate whose repositories are not declared as +// git-submodule-consumer relationships gets an empty module set, and reporting +// success there is indistinguishable from every module being covered -- while +// the estates most likely to hit it are exactly the ones asking whether their +// gates are watched at all. +// +// This runs against the external estate fixture rather than the developer's own +// machine. A first version of this test lived in core/cli and passed locally +// only because a registered estate happened to be configured there; in CI the +// command failed earlier at GDS_POLICY_ESTATE_NOT_PROVEN and never reached this +// path, so it was green over the wrong refusal. +func TestModuleCoverageOverNothingIsNotProven(t *testing.T) { + root := appTestRepositoryRoot(t) + runtimePath := appTestRuntimeConfig(t, root) + services, err := NewServices(DefaultClock) + if err != nil { + t.Fatal(err) + } + envelope := services.CoverModules(context.Background(), root, ModuleCoverageOptions{ + GitHubReadOptions: GitHubReadOptions{RuntimeConfig: runtimePath}, + }) + if envelope.ExitClass == domain.ExitSuccess { + t.Fatalf("coverage over an empty module set reported success: %#v", envelope) + } + found := false + for _, finding := range envelope.Findings { + if finding.Code == "GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN" { + found = true + } + } + if !found { + t.Fatalf("no GDS_MODULE_COVERAGE_SCOPE_NOT_PROVEN finding: %#v", envelope) + } +}