Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .gds/bundle.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ schema_version: 1
bundle:
version: "0.9.7-dev"
release_sequence: 0
source_tree_digest: "sha256:ff0bdb5a3aecc647e785a571e5b08f4d47afdf4b9865139b63670f7636ed0df0"
digest: "sha256:83833e3c808531d699accaa7bde3e31289a631da93553c4fae5d08d9ffe902cd"
source_tree_digest: "sha256:c8582e4ff6726dc451444553411d81bf681dbcb03db409987de1db35c016f723"
digest: "sha256:f8b44694166c4358ee247362f0d3ec725c421bc681018ad8bea526250440b16b"

projection:
input_digest: "sha256:847d847bf309749351054160ec089ef7f7d22a1969c26a3eb82ca69fa58beb23"
output_digest: "sha256:469c17732e4509ca38c6f2903b590cc0e8670502f965ebf50b4b40833e3f2cb4"
input_digest: "sha256:033f6d802a41abddbad4bb0375ca2ad899c2c5651409c3e9541b229179a4248c"
output_digest: "sha256:3ff85f46591c1c6d1479bb305adf866d64537f94345fac5185acba78b0738a6f"
files:
- path: ".gds/compiled-policy.json"
digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f"
- path: ".github/workflows/gds-ci.yml"
digest: "sha256:75d8ce3e084bc5b3f1b33ce920b245d635915810cb97fb9887db0eba168d5f7a"
digest: "sha256:387375f2ea62d4971ae8051ad1d13aa3c0d42ef9e7695a6df40d636808cc06d6"
4 changes: 2 additions & 2 deletions .github/workflows/gds-ci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# GENERATED FILE - DO NOT EDIT DIRECTLY
# generator: gds
# bundle: 0.9.7-dev
# source-tree-digest: sha256:ff0bdb5a3aecc647e785a571e5b08f4d47afdf4b9865139b63670f7636ed0df0
# input-digest: sha256:847d847bf309749351054160ec089ef7f7d22a1969c26a3eb82ca69fa58beb23
# source-tree-digest: sha256:c8582e4ff6726dc451444553411d81bf681dbcb03db409987de1db35c016f723
# input-digest: sha256:033f6d802a41abddbad4bb0375ca2ad899c2c5651409c3e9541b229179a4248c
# output-digest: sha256:4ef1ee2fcc42927eaedef9c85f7b421f87e5cc75ff7055ef520216a00f7d4b74
# edit-source:
# - .gds/repository.yaml
Expand Down
8 changes: 5 additions & 3 deletions core/app/projection_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,11 @@ func (services *Services) projectionOperationContext(
if len(findings) != 0 {
return projectionContext{}, findings
}
compiled := services.Compiler.CompileDirectory(
root, anchor, version,
)
compilePolicy := services.Compiler.CompileDirectory
if source.released() {
compilePolicy = services.Compiler.CompileReleasedPublicDirectory
}
compiled := compilePolicy(root, anchor, version)
if len(compiled.Findings) != 0 {
return projectionContext{}, compiled.Findings
}
Expand Down
65 changes: 65 additions & 0 deletions core/app/released_project_projection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,78 @@ package app

import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/NDDev-OpenNetwork/github-device-sync/core/bundle"
)

func TestReleasedPublicProjectGeneratesFromVerifiedPortableArtifact(t *testing.T) {
root := releasedProjectFixture(t, "public")
services, err := NewServices(DefaultClock)
if err != nil {
t.Fatal(err)
}
engine := appTestRepositoryRoot(t)
command := exec.Command("git", "-C", engine, "ls-files", "--",
"policies", "schemas/v1", "schemas/migrations", "templates/agents",
"templates/github-actions", "templates/harnesses", "skills/canonical",
"skills/registry.yaml", "harnesses", "plugins/gds-core",
"plugins/gds-estate-admin", "plugins/gds-module")
tracked, err := command.Output()
if err != nil {
t.Fatal(err)
}
options := bundle.BuildOptions{
BundleVersion: "1.0.0", ReleaseSequence: 1,
SourceCommit: strings.Repeat("a", 40), MinimumCLIVersion: "0.1.0",
Workflow: ".github/workflows/release-bundle.yml", SourceRef: "refs/heads/main",
TrackedSources: strings.Fields(string(tracked)),
}
trust := bundle.TrustPolicy{
Source: bundle.TrustSource{Owner: "example-owner", Repository: "example-engine",
AllowedWorkflows: []string{options.Workflow}, AllowedRefs: []string{options.SourceRef}},
Release: bundle.TrustRelease{MinimumReleaseSequence: 1},
}
candidate, findings := bundle.Build(engine, options, trust, services.Schemas)
if len(findings) != 0 {
t.Fatalf("synthetic release build: %#v", findings)
}
directory := t.TempDir()
archive := filepath.Join(directory, "bundle.tar.gz")
envelope := filepath.Join(directory, "release-envelope.json")
raw, err := json.Marshal(candidate.Envelope)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(archive, candidate.Artifact, 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(envelope, raw, 0o600); err != nil {
t.Fatal(err)
}
source := ProjectionSourceOptions{BundleArchive: archive, ReleaseEnvelope: envelope}
t.Run("operation-plan-inputs", func(t *testing.T) {
resolved, findings := services.projectionOperationContext(context.Background(), root, source)
if len(findings) != 0 || len(resolved.candidate.Files) != 2 {
t.Fatalf("public project projection: files=%d findings=%#v", len(resolved.candidate.Files), findings)
}
})
t.Run("read-only-generation", func(t *testing.T) {
envelope := services.GenerateRepository(context.Background(), root, false, source)
if envelope.ExitCode != 0 {
t.Fatalf("public project generation: %#v", envelope.Findings)
}
})
if _, err := os.Stat(filepath.Join(root, ".gds", "compiled-policy.json")); !os.IsNotExist(err) {
t.Fatalf("candidate generation wrote a projection: %v", err)
}
}

func TestReleasedPublicProjectRequiresArtifactWithoutFallingBackToEstate(t *testing.T) {
root := releasedProjectFixture(t, "public")
services, err := NewServices(DefaultClock)
Expand Down
12 changes: 7 additions & 5 deletions core/app/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,10 @@ func (services *Services) GenerateRepository(
return envelopeForError("gds generate repository", path, infoErr)
}
anchor, findings = manifest.NewLoader(services.Schemas).LoadRepository(repositoryInfo.WorktreeRoot)
if len(findings) == 0 && !isPublicModuleProjection(anchor) {
if len(findings) == 0 && anchor.Classification.VisibilityContract != "public" {
findings = []domain.Finding{{
Code: "GDS_PROJECTION_RELEASE_TARGET_INVALID", Severity: domain.SeverityHigh,
Message: "Released projection sources are accepted only for public modules.",
Message: "Released projection sources are accepted only for public repositories.",
}}
}
var releasedManifest bundle.Manifest
Expand All @@ -323,9 +323,11 @@ func (services *Services) GenerateRepository(
"gds generate repository", classifyFindings(findings), nil, findings...,
)
}
compiled := services.Compiler.CompileDirectory(
root, anchor, version,
)
compilePolicy := services.Compiler.CompileDirectory
if options.released() {
compilePolicy = services.Compiler.CompileReleasedPublicDirectory
}
compiled := compilePolicy(root, anchor, version)
if len(compiled.Findings) != 0 {
return domain.NewEnvelope(
"gds generate repository", classifyFindings(compiled.Findings), nil,
Expand Down
29 changes: 28 additions & 1 deletion core/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ func (compiler *Compiler) CompileDirectory(
root string,
anchor domain.RepositoryAnchor,
bundleVersion string,
) CompileResult {
return compiler.compileDirectory(root, anchor, bundleVersion, isPublicModuleAnchor(anchor))
}

// CompileReleasedPublicDirectory compiles the portable policy inputs extracted
// from a verified public release. Those inputs deliberately omit an estate's
// owner register. Explicit owner selectors remain unresolvable and are rejected;
// this does not permit a development estate to lose its owner register.
func (compiler *Compiler) CompileReleasedPublicDirectory(
root string,
anchor domain.RepositoryAnchor,
bundleVersion string,
) CompileResult {
if anchor.Classification.VisibilityContract != "public" {
return CompileResult{Findings: []domain.Finding{{
Code: "GDS_POLICY_RELEASE_TARGET_INVALID", Severity: domain.SeverityHigh,
Message: "Portable released policy requires a public target repository.",
}}}
}
return compiler.compileDirectory(root, anchor, bundleVersion, true)
}

func (compiler *Compiler) compileDirectory(
root string,
anchor domain.RepositoryAnchor,
bundleVersion string,
allowMissingOwnerRegister bool,
) CompileResult {
sources, findings := compiler.loader.Load(root)
if len(findings) != 0 {
Expand All @@ -63,7 +90,7 @@ func (compiler *Compiler) CompileDirectory(
if len(ownerFindings) != 0 {
ownerDirectory := filepath.Join(root, "estate", "owners")
_, ownerErr := os.Stat(ownerDirectory)
if !(isPublicModuleAnchor(anchor) && os.IsNotExist(ownerErr) &&
if !(allowMissingOwnerRegister && os.IsNotExist(ownerErr) &&
allFindingCodes(ownerFindings, "GDS_POLICY_OWNER_REGISTER_UNAVAILABLE")) {
return CompileResult{Findings: ownerFindings}
}
Expand Down
86 changes: 86 additions & 0 deletions core/compiler/released_public_directory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package compiler

import (
"os"
"path/filepath"
"testing"
)

func releasedPolicyDirectory(t *testing.T) string {
t.Helper()
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "policies", "base"), 0o755); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(filepath.Join(testRepositoryRoot(t), "policies", "base", "repository-default.yaml"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "policies", "base", "repository-default.yaml"), raw, 0o644); err != nil {
t.Fatal(err)
}
return root
}

func TestReleasedPublicProjectCompilesWithoutEstateOwners(t *testing.T) {
anchor := testAnchor("repository-default")
anchor.Classification.VisibilityContract = "public"
anchor.Repository.Roles = []string{"project"}
result := New(testSchemas(t)).CompileReleasedPublicDirectory(releasedPolicyDirectory(t), anchor, "1.0.0")
if len(result.Findings) != 0 {
t.Fatalf("released public project: %#v", result.Findings)
}
}

func TestDevelopmentPublicProjectStillRequiresEstateOwners(t *testing.T) {
anchor := testAnchor("repository-default")
anchor.Classification.VisibilityContract = "public"
anchor.Repository.Roles = []string{"project"}
result := New(testSchemas(t)).CompileDirectory(releasedPolicyDirectory(t), anchor, DevelopmentBundleVersion)
assertFinding(t, result.Findings, "GDS_POLICY_OWNER_REGISTER_UNAVAILABLE")
}

func TestReleasedDirectoryRejectsPrivateTarget(t *testing.T) {
anchor := testAnchor("repository-default")
anchor.Classification.VisibilityContract = "private"
result := New(testSchemas(t)).CompileReleasedPublicDirectory(releasedPolicyDirectory(t), anchor, "1.0.0")
assertFinding(t, result.Findings, "GDS_POLICY_RELEASE_TARGET_INVALID")
}

func TestReleasedDirectoryRejectsCorruptExistingOwnerRegister(t *testing.T) {
root := releasedPolicyDirectory(t)
directory := filepath.Join(root, "estate", "owners")
if err := os.MkdirAll(directory, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(directory, "invalid.yaml"), []byte("owner: ["), 0o644); err != nil {
t.Fatal(err)
}
anchor := testAnchor("repository-default")
anchor.Classification.VisibilityContract = "public"
result := New(testSchemas(t)).CompileReleasedPublicDirectory(root, anchor, "1.0.0")
assertFinding(t, result.Findings, "GDS_POLICY_OWNER_REGISTER_UNAVAILABLE")
}

func TestReleasedDirectoryDoesNotInventAnOwnerIdentity(t *testing.T) {
root := releasedPolicyDirectory(t)
policy := `schema_version: 1
policy:
id: owner-selected
tier: owner
priority: 100
distribution: public
match:
owner: owner:example-declared
apply:
rollout:
mode: pull-request
`
if err := os.WriteFile(filepath.Join(root, "policies", "owner-selected.yaml"), []byte(policy), 0o644); err != nil {
t.Fatal(err)
}
anchor := testAnchor("repository-default", "owner-selected")
anchor.Classification.VisibilityContract = "public"
result := New(testSchemas(t)).CompileReleasedPublicDirectory(root, anchor, "1.0.0")
assertFinding(t, result.Findings, "GDS_POLICY_PROFILE_NOT_APPLICABLE")
}
6 changes: 6 additions & 0 deletions docs/contracts/projections-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ content-set digest and attestation identity. Plan/apply/verify require the same
two immutable input paths so precondition re-observation cannot change source.
Private targets cannot use this standalone boundary.

Portable released compilation does not require the private estate owner
register. Profiles without an owner selector can apply; an explicit owner
selector remains unresolvable and is rejected rather than deriving an identity
from the GitHub login. A development public project still requires its estate
owner register, and a corrupt existing register is always rejected.

## Manual drift

Verification uses `lstat`, rejects symlinks and non-regular files, confines
Expand Down
Loading