Skip to content

Commit aa6ad7f

Browse files
committed
feat(attestation): expand PR reviewers with requested status and review state
Add Requested and ReviewStatus fields to prinfo.Reviewer to track which reviewers were explicitly requested vs who submitted reviews. For GitHub PRs, merge requested reviewer lists from both event payload and API endpoint, then overlay review states from /reviews API. For GitLab, mark all API-retrieved reviewers as requested. Bump PR info schema to v1.2. Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent e93bf92 commit aa6ad7f

9 files changed

Lines changed: 656 additions & 21 deletions

File tree

internal/prinfo/prinfo.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ const (
1919
// EvidenceID is the identifier for the PR/MR info material type
2020
EvidenceID = "CHAINLOOP_PR_INFO"
2121
// EvidenceSchemaURL is the URL to the JSON schema for PR/MR info
22-
EvidenceSchemaURL = "https://schemas.chainloop.dev/prinfo/1.1/pr-info.schema.json"
22+
EvidenceSchemaURL = "https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json"
2323
)
2424

2525
// Reviewer represents a reviewer of the PR/MR
2626
type Reviewer struct {
27-
Login string `json:"login" jsonschema:"required,description=Username of the reviewer"`
28-
Type string `json:"type" jsonschema:"required,enum=User,enum=Bot,enum=unknown,description=Account type of the reviewer"`
27+
Login string `json:"login" jsonschema:"required,description=Username of the reviewer"`
28+
Type string `json:"type" jsonschema:"required,enum=User,enum=Bot,enum=unknown,description=Account type of the reviewer"`
29+
Requested bool `json:"requested" jsonschema:"description=Whether the reviewer was explicitly requested to review"`
30+
ReviewStatus string `json:"review_status,omitempty" jsonschema:"enum=APPROVED,enum=CHANGES_REQUESTED,enum=COMMENTED,enum=DISMISSED,enum=PENDING,description=The reviewer's current review state if they have submitted a review"`
2931
}
3032

3133
// Data represents the data payload of the PR/MR info evidence

internal/prinfo/prinfo_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,84 @@ func TestValidatePRInfo(t *testing.T) {
185185
}
186186
}
187187

188+
func TestValidatePRInfoV1_2(t *testing.T) {
189+
testCases := []struct {
190+
name string
191+
data string
192+
wantErr bool
193+
}{
194+
{
195+
name: "valid reviewer with requested and review_status",
196+
data: `{
197+
"platform": "github",
198+
"type": "pull_request",
199+
"number": "789",
200+
"url": "https://github.com/owner/repo/pull/789",
201+
"reviewers": [
202+
{"login": "reviewer1", "type": "User", "requested": true, "review_status": "COMMENTED"},
203+
{"login": "reviewer2", "type": "User", "requested": false, "review_status": "APPROVED"},
204+
{"login": "coderabbitai", "type": "Bot", "requested": true}
205+
]
206+
}`,
207+
wantErr: false,
208+
},
209+
{
210+
name: "backward compat: reviewer without new fields is still valid",
211+
data: `{
212+
"platform": "github",
213+
"type": "pull_request",
214+
"number": "789",
215+
"url": "https://github.com/owner/repo/pull/789",
216+
"reviewers": [
217+
{"login": "reviewer1", "type": "User"}
218+
]
219+
}`,
220+
wantErr: false,
221+
},
222+
{
223+
name: "invalid review_status value",
224+
data: `{
225+
"platform": "github",
226+
"type": "pull_request",
227+
"number": "789",
228+
"url": "https://github.com/owner/repo/pull/789",
229+
"reviewers": [
230+
{"login": "reviewer1", "type": "User", "review_status": "UNKNOWN_STATE"}
231+
]
232+
}`,
233+
wantErr: true,
234+
},
235+
{
236+
name: "additional property in reviewer not allowed",
237+
data: `{
238+
"platform": "github",
239+
"type": "pull_request",
240+
"number": "789",
241+
"url": "https://github.com/owner/repo/pull/789",
242+
"reviewers": [
243+
{"login": "reviewer1", "type": "User", "extra": "not allowed"}
244+
]
245+
}`,
246+
wantErr: true,
247+
},
248+
}
249+
250+
for _, tc := range testCases {
251+
t.Run(tc.name, func(t *testing.T) {
252+
var data interface{}
253+
err := json.Unmarshal([]byte(tc.data), &data)
254+
require.NoError(t, err)
255+
256+
err = schemavalidators.ValidatePRInfo(data, schemavalidators.PRInfoVersion1_2)
257+
if tc.wantErr {
258+
assert.Error(t, err)
259+
} else {
260+
assert.NoError(t, err)
261+
}
262+
})
263+
}
264+
}
265+
188266
func TestValidatePRInfoV1_0BackwardCompat(t *testing.T) {
189267
testCases := []struct {
190268
name string

internal/prinfo/schemas/generate.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2025-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -23,13 +23,13 @@ import (
2323
"github.com/chainloop-dev/chainloop/internal/prinfo"
2424
)
2525

26-
//go:generate go run ./generate.go --output-dir ../../../internal/schemavalidators/internal_schemas/prinfo --version 1.1
26+
//go:generate go run ./generate.go --output-dir ../../../internal/schemavalidators/internal_schemas/prinfo --version 1.2
2727
func main() {
2828
var outputDir string
2929
var version string
3030

3131
flag.StringVar(&outputDir, "output-dir", "../../../internal/schemavalidators/internal_schemas/prinfo", "Directory to output the schema files")
32-
flag.StringVar(&version, "version", "1.1", "Schema version")
32+
flag.StringVar(&version, "version", "1.2", "Schema version")
3333
flag.Parse()
3434

3535
generator := prinfo.NewGenerator()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json",
4+
"properties": {
5+
"platform": {
6+
"type": "string",
7+
"enum": [
8+
"github",
9+
"gitlab"
10+
],
11+
"description": "The CI/CD platform"
12+
},
13+
"type": {
14+
"type": "string",
15+
"enum": [
16+
"pull_request",
17+
"merge_request"
18+
],
19+
"description": "The type of change request"
20+
},
21+
"number": {
22+
"type": "string",
23+
"description": "The PR/MR number or identifier"
24+
},
25+
"title": {
26+
"type": "string",
27+
"description": "The PR/MR title"
28+
},
29+
"description": {
30+
"type": "string",
31+
"description": "The PR/MR description or body"
32+
},
33+
"source_branch": {
34+
"type": "string",
35+
"description": "The source branch name"
36+
},
37+
"target_branch": {
38+
"type": "string",
39+
"description": "The target branch name"
40+
},
41+
"url": {
42+
"type": "string",
43+
"format": "uri",
44+
"description": "Direct URL to the PR/MR"
45+
},
46+
"author": {
47+
"type": "string",
48+
"description": "Username of the PR/MR author"
49+
},
50+
"reviewers": {
51+
"items": {
52+
"properties": {
53+
"login": {
54+
"type": "string",
55+
"description": "Username of the reviewer"
56+
},
57+
"type": {
58+
"type": "string",
59+
"enum": [
60+
"User",
61+
"Bot",
62+
"unknown"
63+
],
64+
"description": "Account type of the reviewer"
65+
},
66+
"requested": {
67+
"type": "boolean",
68+
"description": "Whether the reviewer was explicitly requested to review"
69+
},
70+
"review_status": {
71+
"type": "string",
72+
"enum": [
73+
"APPROVED",
74+
"CHANGES_REQUESTED",
75+
"COMMENTED",
76+
"DISMISSED",
77+
"PENDING"
78+
],
79+
"description": "The reviewer's current review state if they have submitted a review"
80+
}
81+
},
82+
"additionalProperties": false,
83+
"type": "object",
84+
"required": [
85+
"login",
86+
"type"
87+
]
88+
},
89+
"type": "array",
90+
"description": "List of reviewers who reviewed or were requested to review"
91+
}
92+
},
93+
"additionalProperties": false,
94+
"type": "object",
95+
"required": [
96+
"platform",
97+
"type",
98+
"number",
99+
"url"
100+
],
101+
"title": "Pull Request / Merge Request Information",
102+
"description": "Schema for Pull Request or Merge Request metadata collected during attestation"
103+
}

internal/schemavalidators/schemavalidators.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const (
5050
PRInfoVersion1_0 PRInfoVersion = "1.0"
5151
// PRInfoVersion1_1 represents PR/MR Info version 1.1 schema (adds reviewers).
5252
PRInfoVersion1_1 PRInfoVersion = "1.1"
53+
// PRInfoVersion1_2 represents PR/MR Info version 1.2 schema (adds requested and review_status to reviewers).
54+
PRInfoVersion1_2 PRInfoVersion = "1.2"
5355
// CycloneDXVersion1_5 represents CycloneDX version 1.5 schema.
5456
CycloneDXVersion1_5 CycloneDXVersion = "1.5"
5557
// CycloneDXVersion1_6 represents CycloneDX version 1.6 schema.
@@ -96,6 +98,8 @@ var (
9698
prInfoSpecVersion1_0 string
9799
//go:embed internal_schemas/prinfo/pr-info-1.1.schema.json
98100
prInfoSpecVersion1_1 string
101+
//go:embed internal_schemas/prinfo/pr-info-1.2.schema.json
102+
prInfoSpecVersion1_2 string
99103

100104
// AI Agent Config schemas
101105
//go:embed internal_schemas/aiagentconfig/ai-agent-config-0.1.schema.json
@@ -120,6 +124,7 @@ var schemaURLMapping = map[string]string{
120124
"https://chainloop.dev/schemas/runner-context-response-0.1.schema.json": runnerContextSpecVersion0_1,
121125
"https://schemas.chainloop.dev/prinfo/1.0/pr-info.schema.json": prInfoSpecVersion1_0,
122126
"https://schemas.chainloop.dev/prinfo/1.1/pr-info.schema.json": prInfoSpecVersion1_1,
127+
"https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json": prInfoSpecVersion1_2,
123128
"https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json": aiAgentConfigSpecVersion0_1,
124129
}
125130

@@ -149,6 +154,7 @@ func init() {
149154
compiledPRInfoSchemas = make(map[PRInfoVersion]*jsonschema.Schema)
150155
compiledPRInfoSchemas[PRInfoVersion1_0] = compiler.MustCompile("https://schemas.chainloop.dev/prinfo/1.0/pr-info.schema.json")
151156
compiledPRInfoSchemas[PRInfoVersion1_1] = compiler.MustCompile("https://schemas.chainloop.dev/prinfo/1.1/pr-info.schema.json")
157+
compiledPRInfoSchemas[PRInfoVersion1_2] = compiler.MustCompile("https://schemas.chainloop.dev/prinfo/1.2/pr-info.schema.json")
152158

153159
compiledAIAgentConfigSchemas = make(map[AIAgentConfigVersion]*jsonschema.Schema)
154160
compiledAIAgentConfigSchemas[AIAgentConfigVersion0_1] = compiler.MustCompile("https://schemas.chainloop.dev/aiagentconfig/0.1/ai-agent-config.schema.json")
@@ -246,7 +252,7 @@ func ValidateChainloopRunnerContext(data interface{}, version RunnerContextVersi
246252
// ValidatePRInfo validates the PR/MR info schema.
247253
func ValidatePRInfo(data interface{}, version PRInfoVersion) error {
248254
if version == "" {
249-
version = PRInfoVersion1_1
255+
version = PRInfoVersion1_2
250256
}
251257

252258
schema, ok := compiledPRInfoSchemas[version]

pkg/attestation/crafter/materials/chainloop_pr_info.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2025-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -75,7 +75,7 @@ func (i *ChainloopPRInfoCrafter) Craft(ctx context.Context, artifactPath string)
7575
}
7676

7777
// Validate the data against JSON schema
78-
if err := schemavalidators.ValidatePRInfo(rawData, schemavalidators.PRInfoVersion1_1); err != nil {
78+
if err := schemavalidators.ValidatePRInfo(rawData, schemavalidators.PRInfoVersion1_2); err != nil {
7979
i.logger.Debug().Err(err).Msg("schema validation failed")
8080
return nil, fmt.Errorf("PR info validation failed: %w", err)
8181
}

pkg/attestation/crafter/materials/chainloop_pr_info_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func TestChainloopPRInfoCrafter_Validation(t *testing.T) {
118118
require.NoError(t, err)
119119

120120
// Validate the data against JSON schema
121-
err = schemavalidators.ValidatePRInfo(rawData, schemavalidators.PRInfoVersion1_1)
121+
err = schemavalidators.ValidatePRInfo(rawData, schemavalidators.PRInfoVersion1_2)
122122

123123
if tc.wantErr {
124124
require.Error(t, err)

0 commit comments

Comments
 (0)