Skip to content
Closed
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: 9 additions & 1 deletion pkg/prinfo/prinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ const (
// EvidenceID is the identifier for the PR/MR info material type
EvidenceID = "CHAINLOOP_PR_INFO"
// EvidenceSchemaURL is the URL to the JSON schema for PR/MR info
EvidenceSchemaURL = "https://schemas.chainloop.dev/prinfo/1.3/pr-info.schema.json"
EvidenceSchemaURL = "https://schemas.chainloop.dev/prinfo/1.4/pr-info.schema.json"

// StateOpen is the state of a PR/MR that is not closed or merged
StateOpen = "open"
// StateClosed is the state of a PR/MR that was closed without a merge
StateClosed = "closed"
// StateMerged is the state of a merged PR/MR
StateMerged = "merged"

// AuthorTypeUser represents a human user account
AuthorTypeUser = "User"
Expand Down Expand Up @@ -60,6 +67,7 @@ type Data struct {
URL string `json:"url" jsonschema:"required,format=uri,description=Direct URL to the PR/MR"`
Author *Author `json:"author,omitempty" jsonschema:"description=The PR/MR author"`
Reviewers []Reviewer `json:"reviewers,omitempty" jsonschema:"description=List of reviewers who reviewed or were requested to review"`
State string `json:"state,omitempty" jsonschema:"enum=open,enum=closed,enum=merged,description=The PR/MR state when the data was collected"`
}

// UnmarshalJSON implements custom JSON unmarshaling for Data to handle
Expand Down
9 changes: 7 additions & 2 deletions pkg/prinfo/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ const (
Version1_2 Version = "1.2"
// Version1_3 represents PR/MR Info version 1.3 schema (author as object with type).
Version1_3 Version = "1.3"
// Version1_4 represents PR/MR Info version 1.4 schema (adds the PR/MR state).
Version1_4 Version = "1.4"

// LatestVersion is the schema version emitted by this package.
LatestVersion = Version1_3
LatestVersion = Version1_4
)

// ErrInvalidJSONPayload represents an error for an invalid JSON payload.
Expand All @@ -54,6 +56,8 @@ var (
specVersion1_2 string
//go:embed schemas/pr-info-1.3.schema.json
specVersion1_3 string
//go:embed schemas/pr-info-1.4.schema.json
specVersion1_4 string
)

// rawSchemas holds the canonical JSON schema documents indexed by version.
Expand All @@ -62,6 +66,7 @@ var rawSchemas = map[Version]string{
Version1_1: specVersion1_1,
Version1_2: specVersion1_2,
Version1_3: specVersion1_3,
Version1_4: specVersion1_4,
}

var (
Expand All @@ -86,7 +91,7 @@ func Schema(version Version) (string, error) {

// Versions returns the supported schema versions, oldest first.
func Versions() []Version {
return []Version{Version1_0, Version1_1, Version1_2, Version1_3}
return []Version{Version1_0, Version1_1, Version1_2, Version1_3, Version1_4}
}

func initSchemas() {
Expand Down
58 changes: 50 additions & 8 deletions pkg/prinfo/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,24 @@ func TestSchema(t *testing.T) {
func TestPublishedAuthorShapeIsStable(t *testing.T) {
const stringAuthor = `"octocat"`
const objectAuthor = `{"login": "octocat", "type": "User"}`
const stringForm, objectForm = "string", "object"

testCases := []struct {
version prinfo.Version
author string
wantErr bool
authorForm string
}{
{version: prinfo.Version1_0, author: stringAuthor, authorForm: "string"},
{version: prinfo.Version1_0, author: objectAuthor, authorForm: "object", wantErr: true},
{version: prinfo.Version1_1, author: stringAuthor, authorForm: "string"},
{version: prinfo.Version1_1, author: objectAuthor, authorForm: "object", wantErr: true},
{version: prinfo.Version1_2, author: stringAuthor, authorForm: "string"},
{version: prinfo.Version1_2, author: objectAuthor, authorForm: "object", wantErr: true},
{version: prinfo.Version1_3, author: stringAuthor, authorForm: "string"},
{version: prinfo.Version1_3, author: objectAuthor, authorForm: "object"},
{version: prinfo.Version1_0, author: stringAuthor, authorForm: stringForm},
{version: prinfo.Version1_0, author: objectAuthor, authorForm: objectForm, wantErr: true},
{version: prinfo.Version1_1, author: stringAuthor, authorForm: stringForm},
{version: prinfo.Version1_1, author: objectAuthor, authorForm: objectForm, wantErr: true},
{version: prinfo.Version1_2, author: stringAuthor, authorForm: stringForm},
{version: prinfo.Version1_2, author: objectAuthor, authorForm: objectForm, wantErr: true},
{version: prinfo.Version1_3, author: stringAuthor, authorForm: stringForm},
{version: prinfo.Version1_3, author: objectAuthor, authorForm: objectForm},
{version: prinfo.Version1_4, author: stringAuthor, authorForm: stringForm},
{version: prinfo.Version1_4, author: objectAuthor, authorForm: objectForm},
}

for _, tc := range testCases {
Expand All @@ -109,6 +112,45 @@ func TestPublishedAuthorShapeIsStable(t *testing.T) {
}
}

// 1.4 adds the optional PR/MR state. Older versions reject it, because they do
// not allow unknown properties.
func TestValidateState(t *testing.T) {
testCases := []struct {
name string
version prinfo.Version
state string
wantErr bool
}{
{name: "open", version: prinfo.Version1_4, state: prinfo.StateOpen},
{name: "closed", version: prinfo.Version1_4, state: prinfo.StateClosed},
{name: "merged", version: prinfo.Version1_4, state: prinfo.StateMerged},
{name: "unknown state", version: prinfo.Version1_4, state: "draft", wantErr: true},
{name: "state in 1.3", version: prinfo.Version1_3, state: prinfo.StateMerged, wantErr: true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := `{
"platform": "github",
"type": "pull_request",
"number": "123",
"url": "https://github.com/owner/repo/pull/123",
"state": "` + tc.state + `"
}`

var data any
require.NoError(t, json.Unmarshal([]byte(payload), &data))

err := prinfo.Validate(data, tc.version)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

func TestNewEvidence(t *testing.T) {
evidence := prinfo.NewEvidence(prinfo.Data{
Platform: "github",
Expand Down
142 changes: 142 additions & 0 deletions pkg/prinfo/schemas/pr-info-1.4.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://schemas.chainloop.dev/prinfo/1.4/pr-info.schema.json",
"properties": {
"platform": {
"type": "string",
"enum": [
"github",
"gitlab"
],
"description": "The CI/CD platform"
},
"type": {
"type": "string",
"enum": [
"pull_request",
"merge_request"
],
"description": "The type of change request"
},
"number": {
"type": "string",
"description": "The PR/MR number or identifier"
},
"title": {
"type": "string",
"description": "The PR/MR title"
},
"description": {
"type": "string",
"description": "The PR/MR description or body"
},
"source_branch": {
"type": "string",
"description": "The source branch name"
},
"target_branch": {
"type": "string",
"description": "The target branch name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Direct URL to the PR/MR"
},
"author": {
"oneOf": [
{
"type": "string",
"description": "Username of the PR/MR author (deprecated, use object form)"
},
{
"type": "object",
"properties": {
"login": {
"type": "string",
"description": "Username of the PR/MR author"
},
"type": {
"type": "string",
"enum": [
"User",
"Bot",
"unknown"
],
"description": "Account type of the PR/MR author"
}
},
"required": [
"login",
"type"
],
"additionalProperties": false,
"description": "The PR/MR author with account type"
}
],
"description": "The PR/MR author (string for backwards compatibility, or object with login and type)"
},
"reviewers": {
"items": {
"properties": {
"login": {
"type": "string",
"description": "Username of the reviewer"
},
"type": {
"type": "string",
"enum": [
"User",
"Bot",
"unknown"
],
"description": "Account type of the reviewer"
},
"requested": {
"type": "boolean",
"description": "Whether the reviewer was explicitly requested to review"
},
"review_status": {
"type": "string",
"enum": [
"APPROVED",
"CHANGES_REQUESTED",
"COMMENTED",
"DISMISSED",
"PENDING"
],
"description": "The reviewer's current review state if they have submitted a review"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"login",
"type",
"requested"
]
},
"type": "array",
"description": "List of reviewers who reviewed or were requested to review"
},
"state": {
"type": "string",
"enum": [
"open",
"closed",
"merged"
],
"description": "The PR/MR state when the data was collected"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"platform",
"type",
"number",
"url"
],
"title": "Pull Request / Merge Request Information",
"description": "Schema for Pull Request or Merge Request metadata collected during attestation"
}
Loading