Skip to content
Open
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
6 changes: 5 additions & 1 deletion cmd/mock-oci-registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var seedData embed.FS
//go:embed testdata/opa-complypack/*
var opaComplypackData embed.FS

//go:embed testdata/ampel-complypack/*
var ampelComplypackData embed.FS

const defaultPort = "8765"

const (
Expand Down Expand Up @@ -490,10 +493,11 @@ func (s *contentStore) seedDefaults() {
[]string{"v1.0.0", "latest"})

// complypacks/ampel-bp — ComplyPack artifact for AMPEL branch protection evaluator
// Contains granular policy JSON from testdata/ampel-complypack/
s.addComplypackArtifact("complypacks/ampel-bp", []string{"v1.0.0", "latest"}, complypackDef{
evaluatorID: "ampel",
version: "1.0.0",
content: buildDummyTarGz("policy.json", []byte(`{"name":"ampel-branch-protection","version":"1.0.0"}`)),
content: buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack"),
})

// policies/test-opa-policy — OPA container security controls
Expand Down
86 changes: 86 additions & 0 deletions cmd/mock-oci-registry/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -65,6 +66,43 @@ func TestBuildTarGzFromFS_ContentReadable(t *testing.T) {
}
}

func TestBuildTarGzFromFS_AmpelFS(t *testing.T) {
data := buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack")

gr, err := gzip.NewReader(bytes.NewReader(data))
require.NoError(t, err)
defer gr.Close()

tr := tar.NewReader(gr)
files := make(map[string]bool)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
files[hdr.Name] = true
assert.Equal(t, int64(0o644), hdr.Mode)
assert.Greater(t, hdr.Size, int64(0))

// Verify each file is valid JSON with a non-empty "id" field.
content := make([]byte, hdr.Size)
_, readErr := io.ReadFull(tr, content)
require.NoError(t, readErr)

var policy struct {
ID string `json:"id"`
}
require.NoError(t, json.Unmarshal(content, &policy),
"file %s should be valid JSON", hdr.Name)
assert.NotEmpty(t, policy.ID,
"file %s should have a non-empty id field", hdr.Name)
}

assert.Contains(t, files, "block-force-push.json")
assert.Len(t, files, 1, "expected exactly 1 file in tar archive")
}

func TestBuildDummyTarGz_SingleFile(t *testing.T) {
content := []byte(`{"test": true}`)
data := buildDummyTarGz("test.json", content)
Expand Down Expand Up @@ -115,6 +153,54 @@ func TestSeedDefaults_AllReposSeeded(t *testing.T) {
require.NotNil(t, complypackRepo)
_, hasLatest = complypackRepo.tags["latest"]
assert.True(t, hasLatest, "OPA complypack should have 'latest' tag")

// Verify ampel complypack has expected tags and valid content
ampelRepo := store.repos["complypacks/ampel-bp"]
require.NotNil(t, ampelRepo)
_, hasLatest = ampelRepo.tags["latest"]
assert.True(t, hasLatest, "ampel complypack should have 'latest' tag")
_, hasV1 = ampelRepo.tags["v1.0.0"]
assert.True(t, hasV1, "ampel complypack should have 'v1.0.0' tag")

// Verify the ampel complypack content blob contains valid granular policy JSON.
ampelArt := ampelRepo.tags["latest"]
require.NotNil(t, ampelArt)
var ampelManifest ociManifest
require.NoError(t, json.Unmarshal(ampelArt.manifestBytes, &ampelManifest))
require.NotEmpty(t, ampelManifest.Layers, "ampel complypack should have at least one layer")

contentDigest := ampelManifest.Layers[0].Digest
contentBlob, ok := ampelRepo.blobs[contentDigest]
require.True(t, ok, "content blob should exist for digest %s", contentDigest)

gr, err := gzip.NewReader(bytes.NewReader(contentBlob.data))
require.NoError(t, err)
defer gr.Close()

tr := tar.NewReader(gr)
fileCount := 0
for {
hdr, tarErr := tr.Next()
if tarErr == io.EOF {
break
}
require.NoError(t, tarErr)
fileCount++

data := make([]byte, hdr.Size)
_, readErr := io.ReadFull(tr, data)
require.NoError(t, readErr)

var policy struct {
ID string `json:"id"`
}
require.NoError(t, json.Unmarshal(data, &policy),
"ampel complypack file %s should be valid JSON", hdr.Name)
assert.NotEmpty(t, policy.ID,
"ampel complypack file %s should have a non-empty id field", hdr.Name)
}
assert.GreaterOrEqual(t, fileCount, 1,
"ampel complypack should contain at least one file")
}

func TestResolveContentDir_Default(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"id": "block-force-push",
"meta": {
"description": "Validate force push blocking is enabled on protected branches via the GitHub API",
"controls": [
{ "framework": "test-branch-protection", "class": "source-code", "id": "force-push-protection" }
]
},
"tenets": [
{
"id": "01",
"code": "(has(predicates[0].data.values) && type(predicates[0].data.values) == list && predicates[0].data.values.exists(rule, rule.type == \"non_fast_forward\")) || (has(predicates[0].data.values) && type(predicates[0].data.values) != list && has(predicates[0].data.values.allow_force_push) && predicates[0].data.values.allow_force_push == false)",
"predicates": {
"types": [
"http://github.com/carabiner-dev/snappy/specs/github/branch-rules.yaml"
]
},
"assessment": {
"message": "Force pushing is disabled in protected branch."
},
"error": {
"message": "Force pushes can be sent to protected branch",
"guidance": "Create a branch ruleset and enable 'Block force pushes' in the GitHub repository settings."
}
}
]
}
2 changes: 2 additions & 0 deletions openspec/changes/ampel-complypack-content/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-04
93 changes: 93 additions & 0 deletions openspec/changes/ampel-complypack-content/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
## Context

The mock OCI registry (`cmd/mock-oci-registry/main.go`) seeds test
content for integration testing. It currently seeds the ampel complypack
(`complypacks/ampel-bp`) with a dummy payload via `buildDummyTarGz()`:

```go
content: buildDummyTarGz("policy.json",
[]byte(`{"name":"ampel-branch-protection","version":"1.0.0"}`))
```

This dummy JSON lacks the `id` field that `LoadGranularPolicies()`
requires. The OPA complypack was already migrated to the
`buildTarGzFromFS()` pattern in commit `74fbae8`, where real policy
files are embedded from `testdata/opa-complypack/` via `//go:embed`.

The cross-repo integration test script (`cross_repo_integration_test.sh`)
currently works around the dummy content by manually copying
`block-force-push.json` into the workspace default directory. With the
ampel provider now consuming `ComplypackContentPath`, this workaround
is insufficient — the complypack content takes precedence.

## Goals / Non-Goals

**Goals:**
- Seed the ampel complypack with valid granular policy content that the
provider accepts
- Follow the established OPA complypack pattern (embed + `buildTarGzFromFS`)
- Remove the manual granular policy pre-staging from the integration test
- Maintain backward compatibility with the pre-PR#52 ampel provider

**Non-Goals:**
- Adding new ampel policies or expanding test coverage beyond what exists
- Modifying the ampel provider code (that is `complytime-providers` PR #52)
- Changing the complypack OCI artifact format or media types
- Adding OPA-side changes (already handled by `opa-devcontainer-content`)

## Decisions

### D1: Reuse existing `block-force-push.json` content

**Decision**: Copy the content from
`tests/cross-repo/testdata/granular-policies/block-force-push.json`
into `cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json`.

**Rationale**: This is the same fixture the integration test already
validates against. Using identical content ensures the test assertions
(e.g., checking for `block-force-push` policy ID in results) continue
to pass without modification.

**Alternatives considered**:
- Create minimal stub content: Rejected — would require updating test
assertions and diverges from the real policy format the test validates.

### D2: Remove pre-staged granular policies from integration test

**Decision**: Remove the `mkdir -p` and `cp` lines that pre-stage
`block-force-push.json` into `.complytime/ampel/granular-policies/` in
`cross_repo_integration_test.sh`.

**Rationale**: With `ComplypackContentPath` taking precedence in PR #52's
provider, the pre-staged directory is never consulted. Keeping it creates
a false safety net — the test would pass even if complypack delivery
broke, defeating the purpose of the integration test.

**Alternatives considered**:
- Keep pre-staged content as fallback: Rejected — masks complypack
delivery failures and makes the test less meaningful.

### D3: Add `//go:embed` directive alongside the existing OPA one

**Decision**: Add a new `//go:embed testdata/ampel-complypack/*` var
declaration directly below the existing OPA embed directive.

**Rationale**: Follows the established pattern. Each complypack provider
gets its own embedded filesystem variable and testdata subdirectory.

## Risks / Trade-offs

- **[Risk] Merge ordering**: If this PR merges before
`complytime-providers` PR #52, the cross-repo CI will build the old
ampel provider from `main`, which ignores `ComplypackContentPath`
and falls back to the default directory. Since we're removing the
pre-staged content, the test would fail.
→ **Mitigation**: Keep the pre-staged content removal as a separate,
final task. If needed, split the change: merge the complypack content
seeding first (backward-compatible), then remove pre-staging after
PR #52 lands on `complytime-providers` main.

- **[Risk] Content drift**: The embedded `block-force-push.json` could
diverge from the provider's expected format over time.
→ **Mitigation**: This is test content for integration validation —
if the format changes, the integration test will catch it (by design).
54 changes: 54 additions & 0 deletions openspec/changes/ampel-complypack-content/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Why

The mock OCI registry seeds the `complypacks/ampel-bp` artifact with a
dummy `policy.json` containing `{"name":"...","version":"..."}` — a
placeholder that lacks the `id` field required by the ampel provider's
`LoadGranularPolicies()`. Before `complytime-providers` PR #52, the ampel
provider ignored `ComplypackContentPath` and fell back to pre-staged
granular policies, so the dummy content was never parsed. Now that the
provider consumes complypack content, the cross-repo integration test
fails because the dummy payload is rejected. This change replaces the
dummy content with a valid ampel granular policy, mirroring the pattern
already established for the OPA complypack (commit `74fbae8`).

## What Changes

- Replace the dummy `buildDummyTarGz("policy.json", ...)` call in
`seedDefaults()` with `buildTarGzFromFS()` using embedded ampel
complypack test content
- Add `testdata/ampel-complypack/block-force-push.json` containing a
valid `AmpelPolicy` structure (matching the existing cross-repo test
fixture at `tests/cross-repo/testdata/granular-policies/`)
- Add `//go:embed testdata/ampel-complypack/*` directive to embed the
ampel complypack content
- Update `TestSeedDefaults_AllReposSeeded` to verify the ampel
complypack contains the expected file count and content structure
- Remove the now-unused `block-force-push.json` from
`tests/cross-repo/testdata/granular-policies/` and the corresponding
`cp` / `mkdir` lines from `cross_repo_integration_test.sh`, since
the provider will consume complypack content directly instead of the
pre-staged fallback directory

## Capabilities

### New Capabilities
- `ampel-complypack-seed`: Embedded ampel complypack test content in the
mock OCI registry, producing a valid tar.gz payload that the ampel
provider's `LoadGranularPolicies()` accepts

### Modified Capabilities

## Impact

- `cmd/mock-oci-registry/main.go`: New embed directive, updated
`seedDefaults()` call for `complypacks/ampel-bp`
- `cmd/mock-oci-registry/testdata/ampel-complypack/`: New directory with
valid granular policy JSON
- `cmd/mock-oci-registry/main_test.go`: Updated test assertions for
ampel complypack content
- `tests/cross-repo/testdata/granular-policies/`: Removed (content
migrated to embedded complypack)
- `tests/cross-repo/cross_repo_integration_test.sh`: Simplified setup
(no manual granular policy staging)
- Backward-compatible: the old ampel provider ignores
`ComplypackContentPath` and is unaffected by serving valid content
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## ADDED Requirements

### Requirement: Valid ampel complypack content in mock registry
The mock OCI registry SHALL seed the `complypacks/ampel-bp` artifact
with a tar.gz payload containing valid ampel granular policy JSON that
the ampel provider's `LoadGranularPolicies()` accepts without error.
Each embedded JSON file SHALL contain an `id` field, a `meta` object
with `description` and `controls`, and a `tenets` array with at least
one entry.

#### Scenario: Mock registry serves valid ampel complypack
- **WHEN** the mock registry starts with default seed data
- **THEN** the `complypacks/ampel-bp` artifact SHALL contain a tar.gz
layer with at least one `.json` file that conforms to the `AmpelPolicy`
schema (non-empty `id`, `meta.controls`, and `tenets` fields)

#### Scenario: Ampel provider parses complypack content without error
- **WHEN** the ampel provider's `Generate()` receives a
`ComplypackContentPath` pointing to the extracted complypack content
- **THEN** `LoadGranularPolicies()` SHALL parse all JSON files
successfully and return a non-empty policy map

### Requirement: Ampel complypack embedded via filesystem
The ampel complypack content SHALL be embedded using `//go:embed` and
`buildTarGzFromFS()`, matching the pattern established by the OPA
complypack. The content SHALL NOT use `buildDummyTarGz()` with inline
string literals.

#### Scenario: Embedded testdata directory structure
- **WHEN** the mock registry binary is compiled
- **THEN** the `testdata/ampel-complypack/` directory SHALL be embedded
via `//go:embed` and its contents packaged by `buildTarGzFromFS()`

### Requirement: Cross-repo integration test uses complypack flow
The cross-repo integration test SHALL exercise the complypack content
path for the ampel provider. Manual pre-staging of granular policy files
into the workspace default directory SHALL be removed so the test
validates the complypack-delivered content path.

#### Scenario: Integration test passes without pre-staged granular policies
- **WHEN** the cross-repo integration test runs `complyctl generate`
- **THEN** the ampel provider SHALL load granular policies from the
complypack content path (delivered by `complyctl get`) instead of from
a manually pre-staged `.complytime/ampel/granular-policies/` directory

#### Scenario: Backward compatibility with old provider
- **WHEN** the mock registry serves valid ampel complypack content
- **AND** the ampel provider binary does NOT support `ComplypackContentPath`
(pre-PR#52 version)
- **THEN** the provider SHALL ignore the complypack content and fall back
to the pre-staged granular policies directory without error

### Requirement: Unit test coverage for ampel complypack seeding
The mock registry's unit tests SHALL verify that the ampel complypack
artifact is seeded with valid, readable content containing the expected
files.

#### Scenario: TestSeedDefaults verifies ampel complypack content
- **WHEN** `TestSeedDefaults_AllReposSeeded` runs
- **THEN** it SHALL verify that `complypacks/ampel-bp` contains a content
blob that decompresses to at least one `.json` file with a non-empty
`id` field
21 changes: 21 additions & 0 deletions openspec/changes/ampel-complypack-content/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## 1. Ampel Complypack Test Content

- [x] 1.1 [P] Create `cmd/mock-oci-registry/testdata/ampel-complypack/block-force-push.json` with valid `AmpelPolicy` content (copy from `tests/cross-repo/testdata/granular-policies/block-force-push.json`)
- [x] 1.2 Add `//go:embed testdata/ampel-complypack/*` directive and `ampelComplypackData` variable to `cmd/mock-oci-registry/main.go`, below the existing OPA embed directive
- [x] 1.3 Update `seedDefaults()` in `cmd/mock-oci-registry/main.go` to replace `buildDummyTarGz("policy.json", ...)` with `buildTarGzFromFS(ampelComplypackData, "testdata/ampel-complypack")` for the `complypacks/ampel-bp` artifact

## 2. Unit Tests

- [x] 2.1 Add `TestBuildTarGzFromFS_AmpelFS` test in `cmd/mock-oci-registry/main_test.go` to verify the ampel complypack archive contains `block-force-push.json` with valid content
- [x] 2.2 Update `TestSeedDefaults_AllReposSeeded` to verify the ampel complypack content blob decompresses to at least one `.json` file with a non-empty `id` field

## 3. Integration Test Cleanup

- [ ] 3.1 DEFERRED (merge ordering) Remove the `mkdir -p "${WORK_DIR}/.complytime/ampel/granular-policies"` and `cp` lines from `tests/cross-repo/cross_repo_integration_test.sh` (lines 176-179)
- [ ] 3.2 DEFERRED (merge ordering) Remove `tests/cross-repo/testdata/granular-policies/block-force-push.json` and its parent directory (content now embedded in mock registry)

## 4. Validation

- [x] 4.1 Verify `make build` compiles with the new embedded testdata
- [x] 4.2 Verify `make test-unit` passes (mock registry tests)
- [x] 4.3 Verify `make lint` passes with zero issues
Loading