From 3e5f10db1b35f9402b6eebc6c8cb5ec281435a5d Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 09:37:42 +0500 Subject: [PATCH 1/2] feat(policy): reconcile explicit advisory CI delivery intent Signed-off-by: rldyourmnd --- CHANGELOG.md | 6 ++ core/app/github_governance_operations.go | 2 + core/app/github_ruleset_delivery_test.go | 31 ++++++ core/app/github_ruleset_operations.go | 25 ++++- core/compiler/delivery.go | 7 ++ core/compiler/delivery_test.go | 30 ++++++ core/githubruleset/ruleset.go | 34 +++++-- core/githubruleset/ruleset_test.go | 98 +++++++++++++++++++ core/projections/generator.go | 2 + core/projections/generator_test.go | 24 +++++ core/providers/github/mutation_ruleset.go | 52 +++++----- core/providers/github/mutation_test.go | 61 +++++++++++- core/providers/github/ruleset_observation.go | 2 +- .../github/ruleset_observation_test.go | 30 ++++++ docs/continuous-development-policy.md | 50 ++++++++++ policies/stacks/continuous-development.yaml | 9 ++ schemas/v1/plan.schema.json | 22 ++++- schemas/v1/policy.schema.json | 8 ++ templates/agents/public-module.md.tmpl | 10 ++ templates/agents/repository.md.tmpl | 10 ++ .../control-plane/.claude/CLAUDE.md | 2 +- .../control-plane/.gds/bundle.lock.yaml | 12 +-- .../.github/workflows/gds-ci.yml | 2 +- .../projections/control-plane/AGENTS.md | 2 +- 24 files changed, 485 insertions(+), 46 deletions(-) create mode 100644 core/app/github_ruleset_delivery_test.go create mode 100644 core/compiler/delivery.go create mode 100644 core/compiler/delivery_test.go create mode 100644 docs/continuous-development-policy.md create mode 100644 policies/stacks/continuous-development.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bb1be..3c53e07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ Versioning. ## [Unreleased] +- Add opt-in `continuous-development` policy with explicit, journaled removal + intent for selected repository ruleset status checks and generated advisory CI + guidance. Preserve omitted rules and unknown status-check parameters across + updates; verify empty ruleset readback and replay without another write. + + ### Changed - Ruleset reconciliation compares the complete owned required-check identities, diff --git a/core/app/github_governance_operations.go b/core/app/github_governance_operations.go index f5cbd33..f16ae51 100644 --- a/core/app/github_governance_operations.go +++ b/core/app/github_governance_operations.go @@ -40,6 +40,7 @@ type GitHubGovernancePlanData struct { } type githubGovernanceOperationContext struct { + policy compiler.CompiledPolicyDocument root string estateRoot string runtime githubRuntime @@ -433,6 +434,7 @@ func (services *Services) githubGovernanceContext( ready, blocker := githubGovernanceMutationGate(runtime.desired, assignment, capability, anchor.Repository.Lifecycle) return githubGovernanceOperationContext{ root: localRoot, estateRoot: estateRoot, runtime: runtime, reader: reader, snapshot: snapshot, + policy: compiled.Document, comparison: comparison, remediation: remediation, assignment: assignment, capability: capability, ready: ready, blocker: blocker, observation: operations.Observation{ diff --git a/core/app/github_ruleset_delivery_test.go b/core/app/github_ruleset_delivery_test.go new file mode 100644 index 0000000..6f7dbd3 --- /dev/null +++ b/core/app/github_ruleset_delivery_test.go @@ -0,0 +1,31 @@ +package app + +import ( + "github.com/NDDev-OpenNetwork/github-device-sync/core/compiler" + githubprovider "github.com/NDDev-OpenNetwork/github-device-sync/core/providers/github" + "testing" +) + +func TestDeliveryPolicyRemovesChecksAndRequiresObservedAbsence(t *testing.T) { + original := githubprovider.RepositoryRuleset{Name: "main", Enforcement: "active", Rules: []githubprovider.RulesetRule{ + {Type: "required_signatures"}, {Type: "required_status_checks", RequiredStatusChecks: []githubprovider.RequiredStatusCheck{{Context: "background"}}}, + }} + for _, profile := range []string{"", "standard", "continuous-development"} { + desired := rulesetForDeliveryPolicy(original, compiler.CompiledPolicyDocument{Effective: map[string]any{"delivery": map[string]any{"profile": profile}}}) + advisory := profile == "continuous-development" + if desired.RemoveRequiredStatusChecks != advisory { + t.Fatal("removal is not opt-in") + } + observed := githubprovider.RepositoryRulesetState{Enforcement: "active", Rules: original.Rules} + if rulesetOwnedStateMatches(observed, desired) == advisory { + t.Fatal("comparison did not account for existing checks") + } + observed.Rules = []githubprovider.RulesetRule{{Type: "required_signatures"}} + if rulesetOwnedStateMatches(observed, desired) != advisory { + t.Fatal("comparison did not account for absent checks") + } + } + if len(original.Rules) != 2 || original.Rules[1].Type != "required_status_checks" { + t.Fatal("projection changed its input") + } +} diff --git a/core/app/github_ruleset_operations.go b/core/app/github_ruleset_operations.go index cfc3da0..7863cb5 100644 --- a/core/app/github_ruleset_operations.go +++ b/core/app/github_ruleset_operations.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/NDDev-OpenNetwork/github-device-sync/core/compiler" "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" "github.com/NDDev-OpenNetwork/github-device-sync/core/githubmutationruntime" "github.com/NDDev-OpenNetwork/github-device-sync/core/githubruleset" @@ -311,6 +312,7 @@ func (services *Services) githubRulesetContext( }) return githubRulesetOperationContext{}, &failure } + desired = rulesetForDeliveryPolicy(desired, governanceContext.policy) capability, found := mutationCapabilityForInstallation( governanceContext.runtime.desired, options.InstallationID, ) @@ -364,7 +366,7 @@ func (services *Services) githubRulesetContext( if rulesetID == 0 { return githubRulesetOperationContext{ governance: governanceContext, privileged: privileged, - desired: desired, exists: false, inSync: false, + desired: desired, exists: false, inSync: desired.RemoveRequiredStatusChecks && len(desired.Rules) == 0, providerRepositoryID: providerRepositoryID, mutationCapabilityID: capability.Mutation.ID, }, nil @@ -400,6 +402,11 @@ func rulesetOwnedStateMatches( for _, rule := range observed.Rules { byType[rule.Type] = rule } + if desired.RemoveRequiredStatusChecks { + if _, present := byType["required_status_checks"]; present { + return false + } + } for _, wanted := range desired.Rules { actual, present := byType[wanted.Type] if !present { @@ -590,3 +597,19 @@ func githubRulesetPlanInvalid(command string) domain.Envelope { Message: "The referenced plan is not a valid stored GitHub ruleset plan.", }) } + +// Policy projection is explicit and bound by the plan's compiled-policy digest. +func rulesetForDeliveryPolicy(desired githubprovider.RepositoryRuleset, policy compiler.CompiledPolicyDocument) githubprovider.RepositoryRuleset { + if !compiler.AdvisoryCI(policy) { + return desired + } + desired.RemoveRequiredStatusChecks = true + rules := make([]githubprovider.RulesetRule, 0, len(desired.Rules)) + for _, rule := range desired.Rules { + if rule.Type != "required_status_checks" { + rules = append(rules, rule) + } + } + desired.Rules = rules + return desired +} diff --git a/core/compiler/delivery.go b/core/compiler/delivery.go new file mode 100644 index 0000000..ab0fcdf --- /dev/null +++ b/core/compiler/delivery.go @@ -0,0 +1,7 @@ +package compiler + +// AdvisoryCI is opt-in. An absent profile retains the existing policy behavior. +func AdvisoryCI(document CompiledPolicyDocument) bool { + delivery, _ := document.Effective["delivery"].(map[string]any) + return delivery["profile"] == "continuous-development" +} diff --git a/core/compiler/delivery_test.go b/core/compiler/delivery_test.go new file mode 100644 index 0000000..fc8c92f --- /dev/null +++ b/core/compiler/delivery_test.go @@ -0,0 +1,30 @@ +package compiler + +import "testing" + +func TestContinuousDevelopmentProfileIsOptIn(t *testing.T) { + for _, enabled := range []bool{false, true} { + anchor := testAnchor("repository-default") + if enabled { + anchor.Policy.Profiles = append(anchor.Policy.Profiles, "continuous-development") + } + result := New(testSchemas(t)).CompileDirectory(testRepositoryRoot(t), anchor, DevelopmentBundleVersion) + if len(result.Findings) != 0 { + t.Fatalf("findings: %#v", result.Findings) + } + if AdvisoryCI(result.Document) != enabled { + t.Fatalf("advisory=%v want %v", AdvisoryCI(result.Document), enabled) + } + if enabled && result.Document.Provenance["/effective/delivery/profile"].Source != "continuous-development" { + t.Fatal("delivery profile lacks provenance") + } + } +} + +func TestDeliveryProfileRejectsUnknownValue(t *testing.T) { + source := testSource("bad-delivery", "stack", 500, map[string]any{"delivery": map[string]any{"profile": "pretend-green"}}) + result := New(testSchemas(t)).Compile(testAnchor("bad-delivery"), map[string]PolicySource{"bad-delivery": source}, DevelopmentBundleVersion) + if len(result.Findings) == 0 { + t.Fatal("invalid delivery profile accepted") + } +} diff --git a/core/githubruleset/ruleset.go b/core/githubruleset/ruleset.go index e322be6..3b9b0ce 100644 --- a/core/githubruleset/ruleset.go +++ b/core/githubruleset/ruleset.go @@ -278,12 +278,12 @@ func normalizeDesired(value githubprovider.RepositoryRuleset) (githubprovider.Re if value.ID < 0 || value.Name == "" || len(value.Name) > 256 || strings.ContainsAny(value.Name, "\x00\r\n") || (value.Target != "" && value.Target != "branch") || (value.Enforcement != "" && value.Enforcement != "active") || - len(value.Rules) == 0 || len(value.Rules) > 32 { + (len(value.Rules) == 0 && !(value.RemoveRequiredStatusChecks && value.ID > 0)) || len(value.Rules) > 32 { return githubprovider.RepositoryRuleset{}, errors.New("desired GitHub ruleset is invalid") } value.Target = "branch" value.Enforcement = "active" - value.Rules = append([]githubprovider.RulesetRule(nil), value.Rules...) + value.Rules = append([]githubprovider.RulesetRule{}, value.Rules...) seen := map[string]struct{}{} for index := range value.Rules { rule := &value.Rules[index] @@ -314,6 +314,9 @@ func normalizeDesired(value githubprovider.RepositoryRuleset) (githubprovider.Re } rule.AllowedMergeMethods = methods case "required_status_checks": + if value.RemoveRequiredStatusChecks { + return githubprovider.RepositoryRuleset{}, errors.New("required status checks conflict with explicit removal") + } if len(rule.RequiredStatusChecks) == 0 || len(rule.RequiredStatusChecks) > 50 || rule.RequiredApprovingReviewCount != 0 { return githubprovider.RepositoryRuleset{}, errors.New("GitHub status-check rule is invalid") @@ -343,7 +346,7 @@ func normalizeDesired(value githubprovider.RepositoryRuleset) (githubprovider.Re func normalizeState(value githubprovider.RepositoryRulesetState) (githubprovider.RepositoryRulesetState, error) { if value.ID <= 0 || value.Name == "" || value.Target != "branch" || value.SourceType != "Repository" || value.Source == "" || (value.Enforcement != "active" && value.Enforcement != "disabled" && value.Enforcement != "evaluate") || len(value.ConditionIncludes) == 0 || - len(value.Rules) == 0 { + value.Rules == nil { return githubprovider.RepositoryRulesetState{}, errors.New("GitHub ruleset state is invalid") } includes := make([]string, len(value.ConditionIncludes)) @@ -377,7 +380,8 @@ func normalizeState(value githubprovider.RepositoryRulesetState) (githubprovider knownRules = append(knownRules, rule) } desired, err := normalizeDesired(githubprovider.RepositoryRuleset{ - ID: value.ID, Name: value.Name, Target: value.Target, + RemoveRequiredStatusChecks: len(knownRules) == 0, + ID: value.ID, Name: value.Name, Target: value.Target, Enforcement: "active", Rules: knownRules, }) if err != nil { @@ -472,7 +476,18 @@ func ownedStateEqual(current githubprovider.RepositoryRulesetState, desired gith if desired.Enforcement == "" && current.Enforcement != "active" { return false } - return reflect.DeepEqual(ownedRules(current.Rules), ownedRules(desired.Rules)) + currentOwned := ownedRulesByType(current.Rules) + if desired.RemoveRequiredStatusChecks { + if _, present := currentOwned["required_status_checks"]; present { + return false + } + } + for kind, wanted := range ownedRulesByType(desired.Rules) { + if actual, present := currentOwned[kind]; !present || !reflect.DeepEqual(actual, wanted) { + return false + } + } + return true } func applyOwnedState(current githubprovider.RepositoryRulesetState, desired githubprovider.RepositoryRuleset) githubprovider.RepositoryRulesetState { @@ -483,12 +498,15 @@ func applyOwnedState(current githubprovider.RepositoryRulesetState, desired gith owned := ownedRulesByType(desired.Rules) result := make([]githubprovider.RulesetRule, 0, len(current.Rules)+len(owned)) for _, rule := range current.Rules { + if desired.RemoveRequiredStatusChecks && rule.Type == "required_status_checks" { + continue + } if rule.Type == "required_status_checks" || rule.Type == "pull_request" { if replacement, exists := owned[rule.Type]; exists { - if rule.Type == "pull_request" { - replacement.ExternalParameters = append(json.RawMessage(nil), rule.ExternalParameters...) - } + replacement.ExternalParameters = append(json.RawMessage(nil), rule.ExternalParameters...) result = append(result, replacement) + } else { + result = append(result, rule) } delete(owned, rule.Type) continue diff --git a/core/githubruleset/ruleset_test.go b/core/githubruleset/ruleset_test.go index 7592c84..2add4c3 100644 --- a/core/githubruleset/ruleset_test.go +++ b/core/githubruleset/ruleset_test.go @@ -3,6 +3,7 @@ package githubruleset import ( "context" "encoding/json" + "fmt" "reflect" "strings" "testing" @@ -268,3 +269,100 @@ func TestRulesetSemanticPostconditionIgnoresOnlyWritableSerialization(t *testing t.Fatal("semantic ruleset drift was hidden with serialization evidence") } } + +func TestRemovalApplyVerifyAndReplay(t *testing.T) { + for _, keepOther := range []bool{false, true} { + t.Run(fmt.Sprint(keepOther), func(t *testing.T) { + fixture := newRulesetFixture() + state := desiredState(Scope{Owner: "example", Name: "repository"}, desiredRuleset(9), 9) + state.WritablePayload = json.RawMessage(`{"fixture":"before"}`) + if keepOther { + state.Rules = append(state.Rules, githubprovider.RulesetRule{Type: "required_signatures"}) + } + fixture.state = &state + expected := visibleState(state) + desired := githubprovider.RepositoryRuleset{ID: 9, Name: state.Name, Target: "branch", Enforcement: "active", Rules: []githubprovider.RulesetRule{}, RemoveRequiredStatusChecks: true} + step := rulesetStep(&expected, desired) + handler := &Handler{Reader: fixture, Writer: fixture} + result, err := handler.Apply(context.Background(), step) + if err != nil { + t.Fatal(err) + } + if fixture.writes != 1 || requiredChecks(fixture.state.Rules) != nil { + t.Fatal("checks were not removed") + } + if (len(fixture.state.Rules) == 1) != keepOther { + t.Fatal("other rules changed") + } + raw, _ := json.Marshal(result.After) + if err := handler.Verify(context.Background(), step, raw); err != nil { + t.Fatal(err) + } + if _, err := handler.Apply(context.Background(), step); err != nil || fixture.writes != 1 { + t.Fatalf("replay wrote again: %v", err) + } + fixture.state.Rules = append(fixture.state.Rules, githubprovider.RulesetRule{Type: "required_status_checks", RequiredStatusChecks: []githubprovider.RequiredStatusCheck{{Context: "returned"}}}) + if err := handler.Verify(context.Background(), step, raw); err == nil { + t.Fatal("verification accepted restored checks") + } + }) + } +} + +func TestOmittedRulePreservedByExpectedPostcondition(t *testing.T) { + state := desiredState(Scope{Owner: "example", Name: "repository"}, desiredRuleset(9), 9) + desired := githubprovider.RepositoryRuleset{ID: 9, Name: state.Name, Enforcement: "active", Rules: []githubprovider.RulesetRule{{Type: "required_signatures"}}} + after := applyOwnedState(state, desired) + if requiredChecks(after.Rules) == nil || !ownedStateEqual(after, desired) { + t.Fatal("omission must preserve existing status checks") + } +} + +func TestRemovalPlanSchemaAndParameterValidation(t *testing.T) { + schemas, err := validation.NewSchemaSet() + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + id int64 + remove, withChecks, valid bool + }{ + {"bound-clear-only", 9, true, false, true}, + {"ordinary-empty", 9, false, false, false}, + {"empty-create", 0, true, false, false}, + {"contradictory", 9, true, true, false}, + {"default", 9, false, true, true}, + } { + t.Run(tc.name, func(t *testing.T) { + desired := desiredRuleset(tc.id) + desired.RemoveRequiredStatusChecks = tc.remove + if !tc.withChecks { + desired.Rules = []githubprovider.RulesetRule{} + } + state := desiredState(Scope{Owner: "example", Name: "repository"}, desiredRuleset(9), 9) + state.WritablePayload = json.RawMessage(`{"fixture":"plan"}`) + expected := visibleState(state) + step := rulesetStep(&expected, desired) + if tc.id == 0 { + step = rulesetStep(nil, desired) + } + now := time.Date(2026, 7, 11, 5, 0, 0, 0, time.UTC) + plan, err := operations.NewPlan("plan_01KX7BV07RHD6KRA4Z4J0KCHGR", now, now.Add(15*time.Minute), operations.PlanInput{ + Operation: "reconcile-github-ruleset", Actor: operations.Actor{Type: "agent-session", SessionID: "test-session"}, + Preconditions: []operations.Precondition{{RepositoryID: step.RepositoryID, HeadOID: strings.Repeat("a", 40), ManifestDigest: "sha256:" + strings.Repeat("a", 64), PolicyDigest: "sha256:" + strings.Repeat("b", 64)}}, + Steps: []operations.Step{step}, ApprovalClass: "github-ruleset-write", + }) + if err != nil { + t.Fatal(err) + } + if got := len(plan.Validate(schemas)) == 0; got != tc.valid { + t.Fatalf("schema valid=%v findings=%#v", got, plan.Validate(schemas)) + } + _, err = StepParameters(step) + if (err == nil) != tc.valid { + t.Fatalf("parameters valid=%v err=%v", err == nil, err) + } + }) + } +} diff --git a/core/projections/generator.go b/core/projections/generator.go index 5679dd5..1bf0ad1 100644 --- a/core/projections/generator.go +++ b/core/projections/generator.go @@ -77,6 +77,7 @@ type Generator struct { } type templateData struct { + AdvisoryCI bool Purpose string Capabilities []string Entrypoints []domain.ProductEntrypoint @@ -606,6 +607,7 @@ func projectionTemplateData( product = &domain.ProductFacts{} } return templateData{ + AdvisoryCI: compiler.AdvisoryCI(policy), Purpose: product.Purpose, Capabilities: product.Capabilities, Entrypoints: product.Entrypoints, diff --git a/core/projections/generator_test.go b/core/projections/generator_test.go index 3ed66c8..62a1f89 100644 --- a/core/projections/generator_test.go +++ b/core/projections/generator_test.go @@ -509,3 +509,27 @@ func projectionRepositoryRoot(t *testing.T) string { } return filepath.Clean(filepath.Join(filepath.Dir(currentFile), "..", "..")) } + +func TestAdvisoryDeliveryGuidanceDoesNotAlterWorkflowResults(t *testing.T) { + generator, anchor, policy, bundle := controlPlaneInputs(t) + baseline, findings := generator.Generate(anchor, policy, bundle) + if len(findings) != 0 { + t.Fatal(findings) + } + policy.Effective["delivery"] = map[string]any{"profile": "continuous-development"} + advisory, findings := generator.Generate(anchor, policy, bundle) + if len(findings) != 0 { + t.Fatal(findings) + } + body := candidateFile(t, advisory, "AGENTS.md").Content + if !bytes.Contains(body, []byte("Broad GitHub CI is asynchronous evidence")) { + t.Fatalf("missing guidance: %s", body) + } + if bytes.Contains(candidateFile(t, baseline, "AGENTS.md").Content, []byte("## Continuous development")) { + t.Fatal("default profile changed") + } + workflow := candidateFile(t, advisory, goCIOutputPath).Content + if bytes.Contains(workflow, []byte("continue-on-error")) || bytes.Contains(workflow, []byte("|| true")) { + t.Fatal("advisory CI must preserve real outcomes") + } +} diff --git a/core/providers/github/mutation_ruleset.go b/core/providers/github/mutation_ruleset.go index 7d7a8d2..e44ff23 100644 --- a/core/providers/github/mutation_ruleset.go +++ b/core/providers/github/mutation_ruleset.go @@ -37,11 +37,13 @@ type RulesetRule struct { } type RepositoryRuleset struct { - ID int64 `json:"id"` - Name string `json:"name"` - Target string `json:"target"` - Enforcement string `json:"enforcement"` - Rules []RulesetRule `json:"rules"` + // RemoveRequiredStatusChecks is explicit plan intent, never a GitHub wire field. + RemoveRequiredStatusChecks bool `json:"remove_required_status_checks,omitempty"` + ID int64 `json:"id"` + Name string `json:"name"` + Target string `json:"target"` + Enforcement string `json:"enforcement"` + Rules []RulesetRule `json:"rules"` } type rulesetMutationResponse struct { @@ -61,6 +63,9 @@ func (mutator *RepositoryMutator) UpsertDefaultBranchRuleset( if err := validateRepositoryRuleset(ruleset); err != nil { return RulesetSummary{}, MutationMeta{}, err } + if ruleset.RemoveRequiredStatusChecks && ruleset.ID > 0 && current == nil { + return RulesetSummary{}, MutationMeta{}, rulesetStageFailure(RulesetStageObservationBinding, "removal-observation-missing") + } var payload map[string]any if current != nil { // Each condition is reported separately. Collapsed into one message they @@ -91,7 +96,7 @@ func (mutator *RepositoryMutator) UpsertDefaultBranchRuleset( if payload["enforcement"] == "" { payload["enforcement"] = "active" } - if err := replaceOwnedRules(payload, ruleset.Rules); err != nil { + if err := replaceOwnedRules(payload, ruleset.Rules, ruleset.RemoveRequiredStatusChecks); err != nil { return RulesetSummary{}, MutationMeta{}, err } } else { @@ -162,7 +167,7 @@ func (mutator *RepositoryMutator) UpsertDefaultBranchRuleset( }, meta, nil } -func replaceOwnedRules(payload map[string]any, desired []RulesetRule) error { +func replaceOwnedRules(payload map[string]any, desired []RulesetRule, removeChecks bool) error { rawRules, ok := payload["rules"].([]any) if !ok { return rulesetStageFieldFailure( @@ -187,12 +192,13 @@ func replaceOwnedRules(payload map[string]any, desired []RulesetRule) error { ) } typeName, _ := rule["type"].(string) + if removeChecks && typeName == "required_status_checks" { + continue + } if typeName == "required_status_checks" || typeName == "pull_request" { if replacement, exists := owned[typeName]; exists { - if typeName == "pull_request" { - if err := preserveExternalPullParameters(rule, replacement); err != nil { - return err - } + if err := preserveExternalRuleParameters(rule, replacement); err != nil { + return err } result = append(result, replacement) delete(owned, typeName) @@ -212,30 +218,23 @@ func replaceOwnedRules(payload map[string]any, desired []RulesetRule) error { return nil } -func preserveExternalPullParameters(observed, desired map[string]any) error { +func preserveExternalRuleParameters(observed, desired map[string]any) error { observedParameters, ok := observed["parameters"].(map[string]any) if !ok { return rulesetStageFieldFailure( - RulesetStageExternalFieldMerge, "preserved-pull-parameters-not-an-object", "rules/pull_request", + RulesetStageExternalFieldMerge, "preserved-rule-parameters-not-an-object", "rules/parameters", ) } desiredParameters, ok := desired["parameters"].(map[string]any) if !ok { return rulesetStageFieldFailure( - RulesetStageExternalFieldMerge, "desired-pull-parameters-not-an-object", "rules/pull_request", + RulesetStageExternalFieldMerge, "desired-rule-parameters-not-an-object", "rules/parameters", ) } - owned := map[string]bool{ - "required_approving_review_count": true, - "dismiss_stale_reviews_on_push": true, - "require_code_owner_review": true, - "required_review_thread_resolution": true, - "require_last_push_approval": true, - "require_extra_approval_for_unattributed_changes": true, - "allowed_merge_methods": true, - } for key, value := range observedParameters { - if !owned[key] { + // The encoder supplies every owned parameter, including false/empty + // values. Preserve only keys outside that closed owned set. + if _, owned := desiredParameters[key]; !owned { desiredParameters[key] = value } } @@ -256,7 +255,7 @@ func validateRepositoryRuleset(ruleset RepositoryRuleset) error { {boundedProviderText(ruleset.Name, 256), "name-unbounded", "name"}, {ruleset.Target == "" || ruleset.Target == "branch", "target-unsupported", "target"}, {ruleset.Enforcement == "" || ruleset.Enforcement == "active", "enforcement-unsupported", "enforcement"}, - {len(ruleset.Rules) > 0, "rules-empty", "rules"}, + {len(ruleset.Rules) > 0 || (ruleset.RemoveRequiredStatusChecks && ruleset.ID > 0), "rules-empty", "rules"}, {len(ruleset.Rules) <= 32, "rules-over-bound", "rules"}, } { if !condition.ok { @@ -284,6 +283,9 @@ func validateRepositoryRuleset(ruleset RepositoryRuleset) error { ) } case "required_status_checks": + if ruleset.RemoveRequiredStatusChecks { + return rulesetStageFieldFailure(RulesetStageContractValidation, "status-check-removal-conflicts", rule.Type) + } if len(rule.RequiredStatusChecks) == 0 || len(rule.RequiredStatusChecks) > 50 || rule.RequiredApprovingReviewCount != 0 { return rulesetStageFieldFailure( diff --git a/core/providers/github/mutation_test.go b/core/providers/github/mutation_test.go index beb56d9..4577f17 100644 --- a/core/providers/github/mutation_test.go +++ b/core/providers/github/mutation_test.go @@ -304,7 +304,7 @@ func TestRepositoryRulesetUpdatePreservesExternallyManagedPayload(t *testing.T) "rules":[ {"type":"pull_request","parameters":{"required_approving_review_count":2,"require_last_push_approval":true,"allowed_merge_methods":["squash"],"required_reviewers":[],"dismissal_restriction":{"enabled":false,"allowed_actors":[]}}}, {"type":"provider_future_rule","parameters":{"opaque":{"keep":true}}}, - {"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"old"}],"strict_required_status_checks_policy":false,"do_not_enforce_on_create":true}} + {"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"old"}],"strict_required_status_checks_policy":false,"do_not_enforce_on_create":true,"future_status_parameter":{"keep":true}}} ]}`)} desired := RepositoryRuleset{ID: 9, Name: "gds-main", Target: "branch", Enforcement: "active", Rules: []RulesetRule{ { @@ -333,6 +333,10 @@ func TestRepositoryRulesetUpdatePreservesExternallyManagedPayload(t *testing.T) rules[2].(map[string]any)["type"] != "required_status_checks" { t.Fatalf("rule order/external rules changed: %#v", rules) } + statusParameters := rules[2].(map[string]any)["parameters"].(map[string]any) + if statusParameters["future_status_parameter"] == nil || statusParameters["do_not_enforce_on_create"] != false { + t.Fatalf("status parameters were not merged losslessly: %#v", statusParameters) + } pullParameters := rules[0].(map[string]any)["parameters"].(map[string]any) if pullParameters["require_extra_approval_for_unattributed_changes"] != false || !reflect.DeepEqual(pullParameters["allowed_merge_methods"], []any{"merge"}) || @@ -381,3 +385,58 @@ func mutationTestMutator( } return mutator } + +func TestExplicitStatusCheckRemovalPreservesOtherRulesAndWireFields(t *testing.T) { + for _, remove := range []bool{false, true} { + t.Run(fmt.Sprint(remove), func(t *testing.T) { + var received map[string]any + writes := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + writes++ + if r.Method != http.MethodPut || r.URL.Path != "/repos/example/repository/rulesets/9" { + t.Errorf("wrong target: %s %s", r.Method, r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&received); err != nil { + t.Error(err) + } + _, _ = w.Write([]byte(`{"id":9,"name":"gds-main","target":"branch","source_type":"Repository","source":"example/repository","enforcement":"active"}`)) + })) + defer server.Close() + mutator := mutationTestMutator(t, server, []string{MutationRepositoryRuleset}, nil, nil) + repository, err := mutator.BindRepository(RepositoryMutationScope{RepositoryID: 42, Owner: "example", Name: "repository", Operations: []string{MutationRepositoryRuleset}}) + if err != nil { + t.Fatal(err) + } + current := RepositoryRulesetState{ID: 9, WritablePayload: json.RawMessage(`{"name":"gds-main","target":"branch","enforcement":"active","bypass_actors":[{"actor_id":7,"actor_type":"Team","bypass_mode":"always"}],"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":["refs/heads/vendor/**"]}},"rules":[{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"background"}],"strict_required_status_checks_policy":true}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"required_approving_review_count":2}},{"type":"provider_future_rule","parameters":{"keep":true}}]}`)} + desired := RepositoryRuleset{ID: 9, Name: "gds-main", Rules: []RulesetRule{{Type: "required_signatures"}}, RemoveRequiredStatusChecks: remove} + if _, _, err := repository.UpsertDefaultBranchRuleset(context.Background(), desired, ¤t); err != nil { + t.Fatal(err) + } + if writes != 1 { + t.Fatalf("writes=%d", writes) + } + if _, present := received["remove_required_status_checks"]; present { + t.Fatal("internal intent leaked to wire") + } + var old map[string]any + _ = json.Unmarshal(current.WritablePayload, &old) + for _, key := range []string{"bypass_actors", "conditions"} { + if !reflect.DeepEqual(received[key], old[key]) { + t.Fatalf("changed %s", key) + } + } + want := old["rules"].([]any) + if remove { + want = want[1:] + } + if !reflect.DeepEqual(received["rules"], want) { + t.Fatalf("rules=%#v want=%#v", received["rules"], want) + } + desired.Rules = []RulesetRule{{Type: "required_status_checks", RequiredStatusChecks: []RequiredStatusCheck{{Context: "conflict"}}}} + desired.RemoveRequiredStatusChecks = true + if _, _, err := repository.UpsertDefaultBranchRuleset(context.Background(), desired, ¤t); err == nil || writes != 1 { + t.Fatal("conflicting intent reached provider") + } + }) + } +} diff --git a/core/providers/github/ruleset_observation.go b/core/providers/github/ruleset_observation.go index 5858a72..584090d 100644 --- a/core/providers/github/ruleset_observation.go +++ b/core/providers/github/ruleset_observation.go @@ -152,7 +152,7 @@ func normalizeRepositoryRuleset( raw.SourceType != "Repository" || !strings.EqualFold(raw.Source, owner+"/"+name) || (raw.Enforcement != "active" && raw.Enforcement != "disabled" && raw.Enforcement != "evaluate") || len(raw.Conditions.RefName.Include) == 0 || len(raw.Conditions.RefName.Include) > 100 || - len(raw.Conditions.RefName.Exclude) > 100 || len(raw.Rules) == 0 || len(raw.Rules) > 32 { + len(raw.Conditions.RefName.Exclude) > 100 || raw.Rules == nil || len(raw.Rules) > 32 { return RepositoryRulesetState{}, fmt.Errorf("GitHub repository ruleset response is invalid") } state := RepositoryRulesetState{ diff --git a/core/providers/github/ruleset_observation_test.go b/core/providers/github/ruleset_observation_test.go index 8cc3fff..e81c666 100644 --- a/core/providers/github/ruleset_observation_test.go +++ b/core/providers/github/ruleset_observation_test.go @@ -3,6 +3,7 @@ package github import ( "bytes" "context" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -63,3 +64,32 @@ func rulesetStateJSON(includeBypass bool) string { } return fmt.Sprintf(`{"id":9,"name":"gds-default-branch","target":"branch","source_type":"Repository","source":"example/repository","enforcement":"active"%s,"conditions":{"ref_name":{"include":["~DEFAULT_BRANCH"],"exclude":[]},"future_condition":{"preserve":true}},"rules":[{"type":"non_fast_forward"},{"type":"pull_request","provider_rule_metadata":{"preserve":true},"parameters":{"required_approving_review_count":1,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"required_review_thread_resolution":true,"require_last_push_approval":false}}]}`, bypass) } + +func TestEmptyRulesetObservationRequiresExplicitArray(t *testing.T) { + for _, rules := range []string{`[]`, `null`, ``} { + t.Run(rules, func(t *testing.T) { + var document map[string]any + if err := json.Unmarshal([]byte(rulesetStateJSON(true)), &document); err != nil { + t.Fatal(err) + } + if rules == "" { + delete(document, "rules") + } else { + var value any + _ = json.Unmarshal([]byte(rules), &value) + document["rules"] = value + } + raw, _ := json.Marshal(document) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(raw) })) + defer server.Close() + client := testClient(t, server, fixedToken("token", time.Now().Add(time.Hour)), nil) + state, _, err := client.GetRepositoryRuleset(context.Background(), "example", "repository", 9) + if (err == nil) != (rules == "[]") { + t.Fatalf("err=%v", err) + } + if err == nil && state.Rules == nil { + t.Fatal("empty rules must remain an array") + } + }) + } +} diff --git a/docs/continuous-development-policy.md b/docs/continuous-development-policy.md new file mode 100644 index 0000000..caa0967 --- /dev/null +++ b/docs/continuous-development-policy.md @@ -0,0 +1,50 @@ +# Continuous development policy + +Add `continuous-development` to the repository anchor's `policy.profiles` to +select the public policy in `policies/stacks/continuous-development.yaml`. +The effective leaf is `delivery.profile: continuous-development`; omission or +`standard` retains existing behavior. A higher-priority repository policy may +set `standard` explicitly. + +This profile makes broad GitHub CI asynchronous evidence. Generated repository +instructions still require relevant checks and project-owned build, migration +and runtime contracts. Workflows keep their real conclusions; the profile does +not insert `continue-on-error`, skips or fabricated successful statuses. + +## Ruleset reconciliation + +The normal `gds github ruleset` workflow reads the tracked +`.github/rulesets/branch-main.json` contract and any declared external required +checks. The compiled profile then removes the `required_status_checks` rule from +the desired rules and sets `remove_required_status_checks: true` in the immutable +GDS plan. The plan also binds the compiled-policy digest and observed ruleset. +The internal removal field is never sent to GitHub. + +An ordinary omitted rule remains preserved. Explicit removal drops status checks +from the selected repository ruleset, including externally declared contexts in +that rule. Pull-request rules, signatures, other existing rules, conditions and +bypass actors remain under their existing ownership contract. Ordinary updates +also retain unknown provider parameters on status-check and pull-request rules. +Conflicting removal plus desired status checks is rejected before writing. + +An existing checks-only ruleset may become an empty ruleset. Readback requires an +explicit empty array, not missing or null rules. When no matching ruleset exists +and removing checks leaves no desired rules, the planner returns in-sync without +creating an empty shell. Apply verifies the resulting state; replay does not +write again, and verification fails if required checks reappear. + +Selection alone changes neither live GitHub settings nor application delivery. +Provider writes still use the normal exact-plan approval and journal contract. +Review the concrete selected ruleset before applying removal. Other repository +or organization rulesets, classic branch protection, required workflows and +environment gates are separate contracts: this profile does not assert they +were removed or that a branch can merge without any other requirements. + +Applications own deployment executors and their affected checks. CI feedback may +publish actionable failures to the owning repository; this profile does not +launch repair agents or configure a deployment service. + +GitHub describes the status-check requirement in +[available rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets) +and the update payload in the +[repository rules REST API](https://docs.github.com/en/rest/repos/rules#update-a-repository-ruleset). diff --git a/policies/stacks/continuous-development.yaml b/policies/stacks/continuous-development.yaml new file mode 100644 index 0000000..7589215 --- /dev/null +++ b/policies/stacks/continuous-development.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +policy: + id: continuous-development + tier: stack + priority: 500 + distribution: public +apply: + delivery: + profile: continuous-development diff --git a/schemas/v1/plan.schema.json b/schemas/v1/plan.schema.json index 79b161e..83c0752 100644 --- a/schemas/v1/plan.schema.json +++ b/schemas/v1/plan.schema.json @@ -1464,7 +1464,27 @@ "type": "object", "additionalProperties": false, "required": ["id", "name", "target", "enforcement", "rules"], + "allOf": [{ + "if": { + "required": ["remove_required_status_checks"], + "properties": {"remove_required_status_checks": {"const": true}} + }, + "then": { + "properties": { + "rules": {"not": {"contains": { + "properties": {"type": {"const": "required_status_checks"}}, + "required": ["type"] + }}} + }, + "anyOf": [ + {"properties": {"rules": {"minItems": 1}}}, + {"properties": {"id": {"minimum": 1}}} + ] + }, + "else": {"properties": {"rules": {"minItems": 1}}} + }], "properties": { + "remove_required_status_checks": {"type": "boolean"}, "id": {"type": "integer", "minimum": 0, "maximum": 9007199254740991}, "name": {"type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^\\u0000\\r\\n]+$"}, "target": {"const": "branch"}, @@ -1480,7 +1500,7 @@ }, "githubRulesetRules": { "type": "array", - "minItems": 1, + "minItems": 0, "maxItems": 32, "items": {"$ref": "#/$defs/githubRulesetRule"} }, diff --git a/schemas/v1/policy.schema.json b/schemas/v1/policy.schema.json index 3441c38..d9ae005 100644 --- a/schemas/v1/policy.schema.json +++ b/schemas/v1/policy.schema.json @@ -94,6 +94,12 @@ } }, "$defs": { + "deliverySettings": { + "type": "object", + "additionalProperties": false, + "required": ["profile"], + "properties": {"profile": {"enum": ["standard", "continuous-development"]}} + }, "gitSettings": { "type": "object", "additionalProperties": false, @@ -431,6 +437,7 @@ "type": "object", "additionalProperties": false, "properties": { + "delivery": {"$ref": "#/$defs/deliverySettings"}, "git": { "$ref": "#/$defs/gitSettings" }, @@ -462,6 +469,7 @@ "type": "object", "additionalProperties": false, "properties": { + "delivery": {"$ref": "#/$defs/deliverySettings"}, "git": { "$ref": "#/$defs/gitSettings" }, diff --git a/templates/agents/public-module.md.tmpl b/templates/agents/public-module.md.tmpl index 96a5a51..16d00ce 100644 --- a/templates/agents/public-module.md.tmpl +++ b/templates/agents/public-module.md.tmpl @@ -16,3 +16,13 @@ - No repository-owned verification command is declared; report it as `NOT_PROVEN`. {{- end }} +{{- if .AdvisoryCI }} + +## Continuous development + +- Broad GitHub CI is asynchronous evidence; it does not block ordinary merge + or deployment. Preserve actual failed, cancelled, skipped and unknown results. +- Run checks relevant to the changed behavior and follow project-owned build, + migration and runtime contracts. Report failures in the owning repository. +- Selecting this profile does not itself apply provider settings or deploy code. +{{- end }} diff --git a/templates/agents/repository.md.tmpl b/templates/agents/repository.md.tmpl index 5ab4eac..3d624a1 100644 --- a/templates/agents/repository.md.tmpl +++ b/templates/agents/repository.md.tmpl @@ -47,3 +47,13 @@ - Repository `{{ .RepositoryID }}`, roles `{{ .Roles }}`, bundle `{{ .BundleVersion }}`. - Canonical inputs: `.gds/repository.yaml`; compiled result: `.gds/compiled-policy.json`. - Visibility `{{ .VisibilityContract }}`, data `{{ .DataClassification }}`. +{{- if .AdvisoryCI }} + +## Continuous development + +- Broad GitHub CI is asynchronous evidence; it does not block ordinary merge + or deployment. Preserve actual failed, cancelled, skipped and unknown results. +- Run checks relevant to the changed behavior and follow project-owned build, + migration and runtime contracts. Report failures in the owning repository. +- Selecting this profile does not itself apply provider settings or deploy code. +{{- end }} diff --git a/tests/golden/projections/control-plane/.claude/CLAUDE.md b/tests/golden/projections/control-plane/.claude/CLAUDE.md index ab48e3c..188c3e6 100644 --- a/tests/golden/projections/control-plane/.claude/CLAUDE.md +++ b/tests/golden/projections/control-plane/.claude/CLAUDE.md @@ -3,7 +3,7 @@ GENERATED FILE - DO NOT EDIT DIRECTLY generator: gds bundle: 0.8.0-dev source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -input-digest: sha256:219ef57969079d002d66fee569234d522948f265bcca42505726642ef93571c1 +input-digest: sha256:3d7c04888af26c66e2c8b7fedc84b08f90a73d1182cd18ee889c3a849151ce25 output-digest: sha256:88cb57297d8d713287872a8afaca8d42f7146ecf7a091e4996e65eee8f962665 edit-source: - .gds/repository.yaml diff --git a/tests/golden/projections/control-plane/.gds/bundle.lock.yaml b/tests/golden/projections/control-plane/.gds/bundle.lock.yaml index 4851797..51df09b 100644 --- a/tests/golden/projections/control-plane/.gds/bundle.lock.yaml +++ b/tests/golden/projections/control-plane/.gds/bundle.lock.yaml @@ -6,17 +6,17 @@ bundle: release_sequence: 0 channel: "development" source_tree_digest: "sha256:0000000000000000000000000000000000000000000000000000000000000001" - digest: "sha256:0226e28bcb10a18e1868f3af7ded0c27de1debd676ef23c64af9c4826436f506" + digest: "sha256:2bf1503b8188a42c0433be96950a6c263a1c715154924ffa682de2b65d955ee3" projection: - input_digest: "sha256:219ef57969079d002d66fee569234d522948f265bcca42505726642ef93571c1" - output_digest: "sha256:a1607491555e1ab5d2d782bce3b1cddf3386b3fa609be3864a9934365265b411" + input_digest: "sha256:3d7c04888af26c66e2c8b7fedc84b08f90a73d1182cd18ee889c3a849151ce25" + output_digest: "sha256:7127d81d7b0656bbaac983d1383d1cde816813b7e975bbc2c486954515f325de" files: - path: ".claude/CLAUDE.md" - digest: "sha256:ba65eecfa28d3e90c8141c115e684920700510e322783371e2e98b9e6d14835e" + digest: "sha256:1eea64f1a7ff5fb20ef5c11db42231217a1a4bb1f9f9904d5daed91da33037a4" - path: ".gds/compiled-policy.json" digest: "sha256:24850d3c840160a5bef16753424da983b1e220984e2457fd22ee03ed278f1f1b" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:f0c853df6a9c564895c11fc7520621c0c59e9f24b86d476fcd4fd4cc2203dc62" + digest: "sha256:19a37bc7b84efca6d34e15d7ce68ef4fa9f886ffc208c606b262882d83640489" - path: "AGENTS.md" - digest: "sha256:730986b8d110ec0e6f26952c4b91a9dda8611f13d1b51bf7fd9e7d5114558e32" + digest: "sha256:840da70bfecd98bf2b674d6aff3be6cbae808609e0d6ce61292d4a84e1d00e2d" diff --git a/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml b/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml index 47963db..17d3488 100644 --- a/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml +++ b/tests/golden/projections/control-plane/.github/workflows/gds-ci.yml @@ -2,7 +2,7 @@ # generator: gds # bundle: 0.8.0-dev # source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -# input-digest: sha256:219ef57969079d002d66fee569234d522948f265bcca42505726642ef93571c1 +# input-digest: sha256:3d7c04888af26c66e2c8b7fedc84b08f90a73d1182cd18ee889c3a849151ce25 # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml diff --git a/tests/golden/projections/control-plane/AGENTS.md b/tests/golden/projections/control-plane/AGENTS.md index 106ac6f..51b28e2 100644 --- a/tests/golden/projections/control-plane/AGENTS.md +++ b/tests/golden/projections/control-plane/AGENTS.md @@ -3,7 +3,7 @@ GENERATED FILE - DO NOT EDIT DIRECTLY generator: gds bundle: 0.8.0-dev source-tree-digest: sha256:0000000000000000000000000000000000000000000000000000000000000001 -input-digest: sha256:219ef57969079d002d66fee569234d522948f265bcca42505726642ef93571c1 +input-digest: sha256:3d7c04888af26c66e2c8b7fedc84b08f90a73d1182cd18ee889c3a849151ce25 output-digest: sha256:ece934738d1ed0bb3532f6db213366f3d60c064e6c0aa79101b3c0b4a9e4e06c edit-source: - .gds/repository.yaml From 681a99e31c65954dc9c9f409e819f006f0ee8e66 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 09:38:53 +0500 Subject: [PATCH 2/2] chore(projections): bind advisory policy source revision Signed-off-by: rldyourmnd --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index c937d31..acf911f 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.8.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:383828ba805b05a5abf42b530c1cd067b23c2559be0026090ae955a2a8bd5c9a" - digest: "sha256:e664eabadb2a55b913c39813c8ef77427ca501ca5f09a4bcdf7328cc53f835c6" + source_tree_digest: "sha256:fc35a6bc483600e82186f5a9f592f6818cd2aca3cb50dbd1e9b9bb1772ead851" + digest: "sha256:f4e6402e5d6729a944595ace0f2b81990cd562fd662cfa1f7adf369d8bfec896" projection: - input_digest: "sha256:7d1612c99b393e2e34ba48c987cf2c87a5f93f82742cabdd49996d8701ac9328" - output_digest: "sha256:2b832569605daaaaec7dd7a477e6b59743b6d087411188df886d0621b34d25e3" + input_digest: "sha256:f7fa43557bb16cb97cbe7b4926a47af83ff32fec4c72a84a3be7dc8251447e2d" + output_digest: "sha256:4bb6f7dd03e8efc9ca96005abb42cb824723bafb8a67afc8c09b1770bc148cd7" files: - path: ".gds/compiled-policy.json" digest: "sha256:b5517ed46f67866220c2b18dbfbda4a40d99f00327611e56742a118d0ac59d0b" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:9b8ae8db225032c36aea608c746eb87a8bd9875b5aea6870efa4053b602a180e" + digest: "sha256:aab129beab492fc47a89d991fca50d9d04fa90a242bea316e77c42e0d92fcb39" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 6b7c570..1fb9e92 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.8.0-dev -# source-tree-digest: sha256:383828ba805b05a5abf42b530c1cd067b23c2559be0026090ae955a2a8bd5c9a -# input-digest: sha256:7d1612c99b393e2e34ba48c987cf2c87a5f93f82742cabdd49996d8701ac9328 +# source-tree-digest: sha256:fc35a6bc483600e82186f5a9f592f6818cd2aca3cb50dbd1e9b9bb1772ead851 +# input-digest: sha256:f7fa43557bb16cb97cbe7b4926a47af83ff32fec4c72a84a3be7dc8251447e2d # output-digest: sha256:01fb4854784be9e4564bcc84e70786484b370879be5e5ab1dd49f8b73ea2dea4 # edit-source: # - .gds/repository.yaml