diff --git a/fixtures/api/codeowners-escaped-multiple.json b/fixtures/api/codeowners-escaped-multiple.json new file mode 100644 index 0000000..5959387 --- /dev/null +++ b/fixtures/api/codeowners-escaped-multiple.json @@ -0,0 +1,762 @@ +{ + "caseId": "github-codeowners-escaped-multiple", + "description": "CODEOWNERS: parses escaped paths and accepts one qualified owner-team reviewer for a multiple-owner rule.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "docs/Design Notes/guide.md", + "status": "modified" + }, + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 113 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "docs/Design Notes/guide.md", + "status": "modified" + }, + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 113 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 73, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "ZG9jcy9EZXNpZ25cIE5vdGVzLyoqIEBvcmcvcmV2aWV3ZXJzCnNyYy8qKiBAb3JnL3Jldmlld2VycyBAb3JnL3NlY3VyaXR5Cg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 234 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 73, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "ZG9jcy9EZXNpZ25cIE5vdGVzLyoqIEBvcmcvcmV2aWV3ZXJzCnNyYy8qKiBAb3JnL3Jldmlld2VycyBAb3JnL3NlY3VyaXR5Cg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 234 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "id": 201, + "user": { + "id": 11, + "login": "alice", + "type": "User" + }, + "state": "APPROVED", + "commit_id": "head-sha", + "submitted_at": "2026-08-23T00:00:00.000Z", + "author_association": "MEMBER" + } + ], + "bytes": 173 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "id": 201, + "user": { + "id": 11, + "login": "alice", + "type": "User" + }, + "state": "APPROVED", + "commit_id": "head-sha", + "submitted_at": "2026-08-23T00:00:00.000Z", + "author_association": "MEMBER" + } + ], + "bytes": 173 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/collaborators/alice/permission" + }, + "response": { + "status": 200, + "body": { + "permission": "write", + "user": { + "id": 11, + "login": "alice" + } + }, + "headers": {}, + "bytes": 55 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/reviewers" + }, + "response": { + "status": 200, + "body": { + "id": 700, + "slug": "reviewers", + "organization": { + "login": "org" + } + }, + "headers": {}, + "bytes": 60 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/reviewers/memberships/alice" + }, + "response": { + "status": 200, + "body": { + "state": "active", + "role": "member" + }, + "headers": {}, + "bytes": 34 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/security" + }, + "response": { + "status": 200, + "body": { + "id": 701, + "slug": "security", + "organization": { + "login": "org" + } + }, + "headers": {}, + "bytes": 59 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/security/memberships/alice" + }, + "response": { + "status": 404, + "body": { + "message": "Not Found" + }, + "headers": {}, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/collaborators/alice/permission" + }, + "response": { + "status": 200, + "body": { + "permission": "write", + "user": { + "id": 11, + "login": "alice" + } + }, + "headers": {}, + "bytes": 55 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/reviewers" + }, + "response": { + "status": 200, + "body": { + "id": 700, + "slug": "reviewers", + "organization": { + "login": "org" + } + }, + "headers": {}, + "bytes": 60 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/reviewers/memberships/alice" + }, + "response": { + "status": 200, + "body": { + "state": "active", + "role": "member" + }, + "headers": {}, + "bytes": 34 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/security" + }, + "response": { + "status": 200, + "body": { + "id": 701, + "slug": "security", + "organization": { + "login": "org" + } + }, + "headers": {}, + "bytes": 59 + } + }, + { + "request": { + "method": "GET", + "path": "/orgs/org/teams/security/memberships/alice" + }, + "response": { + "status": 404, + "body": { + "message": "Not Found" + }, + "headers": {}, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-incomplete-changed-paths.json b/fixtures/api/codeowners-incomplete-changed-paths.json new file mode 100644 index 0000000..de90b8d --- /dev/null +++ b/fixtures/api/codeowners-incomplete-changed-paths.json @@ -0,0 +1,556 @@ +{ + "caseId": "github-codeowners-incomplete-changed-paths", + "description": "CODEOWNERS: keeps incomplete changed-path retrieval from becoming a proven non-match.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 403, + "headers": {}, + "body": { + "message": "Forbidden" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 403, + "headers": {}, + "body": { + "message": "Forbidden" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 166 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 166 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-permission-boundary.json b/fixtures/api/codeowners-permission-boundary.json new file mode 100644 index 0000000..4570eaa --- /dev/null +++ b/fixtures/api/codeowners-permission-boundary.json @@ -0,0 +1,550 @@ +{ + "caseId": "github-codeowners-permission-boundary", + "description": "CODEOWNERS: keeps a permission denial as incomplete ownership evidence rather than treating it as no matching owners.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 403, + "headers": {}, + "body": { + "message": "Forbidden" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 403, + "headers": {}, + "body": { + "message": "Forbidden" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-precedence-docs.json b/fixtures/api/codeowners-precedence-docs.json new file mode 100644 index 0000000..ae47c38 --- /dev/null +++ b/fixtures/api/codeowners-precedence-docs.json @@ -0,0 +1,630 @@ +{ + "caseId": "github-codeowners-precedence-docs", + "description": "CODEOWNERS: selects docs/CODEOWNERS only after higher-precedence candidates are absent at the trusted base revision.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/docs/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": "docs/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 163 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/docs/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": "docs/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 163 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-precedence-github.json b/fixtures/api/codeowners-precedence-github.json new file mode 100644 index 0000000..9df0579 --- /dev/null +++ b/fixtures/api/codeowners-precedence-github.json @@ -0,0 +1,562 @@ +{ + "caseId": "github-codeowners-precedence-github", + "description": "CODEOWNERS: selects .github/CODEOWNERS before repository-root and docs candidates at the trusted base revision.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 166 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 166 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-precedence-root.json b/fixtures/api/codeowners-precedence-root.json new file mode 100644 index 0000000..140e9f1 --- /dev/null +++ b/fixtures/api/codeowners-precedence-root.json @@ -0,0 +1,596 @@ +{ + "caseId": "github-codeowners-precedence-root", + "description": "CODEOWNERS: selects repository-root CODEOWNERS after the .github candidate is absent and before docs/CODEOWNERS.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 23 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": "CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 158 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 22, + "name": "CODEOWNERS", + "path": "CODEOWNERS", + "content": "c3JjLyoqIEBvcmcvcmV2aWV3ZXJzCg==", + "sha": "codeowners-blob-sha" + }, + "bytes": 158 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/codeowners-unsupported.json b/fixtures/api/codeowners-unsupported.json new file mode 100644 index 0000000..b12aa25 --- /dev/null +++ b/fixtures/api/codeowners-unsupported.json @@ -0,0 +1,562 @@ +{ + "caseId": "github-codeowners-unsupported", + "description": "CODEOWNERS: keeps unsupported syntax bounded and returns policy_ambiguous instead of guessing ownership.", + "request": { + "owner": "example", + "name": "service", + "pullNumber": 7, + "eventKind": "pull_request", + "targetKind": "head", + "allowConfirmedAbsence": true + }, + "exchanges": [ + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 1, + "name": "service", + "full_name": "example/service" + }, + "bytes": 65 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7" + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "id": 70, + "number": 7, + "base": { + "ref": "main", + "sha": "base-sha", + "repo": { + "id": 1, + "full_name": "example/service" + } + }, + "head": { + "ref": "feature", + "sha": "head-sha", + "repo": { + "id": 2, + "full_name": "fork/service" + } + }, + "merge_commit_sha": null, + "user": { + "id": 99, + "login": "author", + "type": "User" + } + }, + "bytes": 330 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/patchgate.yml", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 362, + "name": "patchgate.yml", + "path": "patchgate.yml", + "content": "dmVyc2lvbjogMQppc3N1ZV9saW5rYWdlOgogIHJlcXVpcmVkOiB0cnVlCnJlcXVpcmVkX2NoZWNrczoKICAtIGlkOiB1bml0CiAgICBuYW1lOiB1bml0CiAgICB0YXJnZXQ6IGhlYWQKICAgIGFjY2VwdGFibGVfY29uY2x1c2lvbnM6IFtzdWNjZXNzXQogICAgZXhwZWN0ZWRfc291cmNlOgogICAgICBraW5kOiBnaXRodWJfYWN0aW9uc193b3JrZmxvdwogICAgICBhcHBfaWQ6IDE1MzY4CiAgICAgIHdvcmtmbG93X2lkOiAxMjMKICAgICAgd29ya2Zsb3dfcGF0aDogLmdpdGh1Yi93b3JrZmxvd3MvY2kueW1sCiAgICAgIGV2ZW50OiBwdWxsX3JlcXVlc3QKb3duZXJzaGlwOgogIHJlcXVpcmVfY29kZV9vd25lcl9hcHByb3ZhbDogdHJ1ZQo=", + "sha": "blob-sha" + }, + "bytes": 606 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/files", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [ + { + "filename": "src/service.ts", + "status": "modified" + } + ], + "bytes": 51 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 138, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "IyB1bnN1cHBvcnRlZCBzdWJzZXQgY2FzZXMKIXNyYy9wcml2YXRlLyoqIEBvcmcvc2VjdXJpdHkKc3JjL1thYl0udHMgQG9yZy9zZWN1cml0eQpzcmMvPy50cyBAb3JnL3NlY3VyaXR5Cm1hbGZvcm1lZC1vbmx5CnNyYy8qKiBAb3JnL3NhZmUK", + "sha": "codeowners-blob-sha" + }, + "bytes": 319 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/contents/.github/CODEOWNERS", + "query": { + "ref": "base-sha" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "type": "file", + "encoding": "base64", + "size": 138, + "name": "CODEOWNERS", + "path": ".github/CODEOWNERS", + "content": "IyB1bnN1cHBvcnRlZCBzdWJzZXQgY2FzZXMKIXNyYy9wcml2YXRlLyoqIEBvcmcvc2VjdXJpdHkKc3JjL1thYl0udHMgQG9yZy9zZWN1cml0eQpzcmMvPy50cyBAb3JnL3NlY3VyaXR5Cm1hbGZvcm1lZC1vbmx5CnNyYy8qKiBAb3JnL3NhZmUK", + "sha": "codeowners-blob-sha" + }, + "bytes": 319 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "POST", + "path": "/graphql", + "operation": "pullRequestClosingIssues", + "variables": { + "owner": "example", + "name": "service", + "number": 7, + "first": 100, + "after": null + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "data": { + "repository": { + "pullRequest": { + "closingIssuesReferences": { + "nodes": [ + { + "issue": { + "id": "issue-12", + "repository": { + "nameWithOwner": "example/service", + "id": "issue-repo" + }, + "number": 12 + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": null + } + } + } + } + } + }, + "bytes": 260 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/commits/head-sha/check-runs", + "query": { + "per_page": 100, + "filter": "all" + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "check_runs": [ + { + "id": 50, + "name": "unit", + "status": "completed", + "conclusion": "success", + "head_sha": "head-sha", + "app": { + "id": 15368, + "slug": "github-actions" + }, + "check_suite": { + "id": 77 + } + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/actions/runs", + "query": { + "head_sha": "head-sha", + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": { + "total_count": 1, + "workflow_runs": [ + { + "id": 101, + "run_attempt": 1, + "head_sha": "head-sha", + "event": "pull_request", + "workflow_id": 123, + "path": ".github/workflows/ci.yml", + "check_suite_id": 77, + "status": "completed", + "conclusion": "success" + } + ] + }, + "bytes": 250 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/pulls/7/reviews", + "query": { + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/rulesets", + "query": { + "includes_parents": true, + "per_page": 100 + } + }, + "response": { + "status": 200, + "headers": {}, + "body": [], + "bytes": 2 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + }, + { + "request": { + "method": "GET", + "path": "/repos/example/service/branches/main/protection" + }, + "response": { + "status": 404, + "headers": {}, + "body": { + "message": "Not Found" + }, + "bytes": 24 + } + } + ] +} diff --git a/fixtures/api/manifest.json b/fixtures/api/manifest.json index 1fad6b8..5401317 100644 --- a/fixtures/api/manifest.json +++ b/fixtures/api/manifest.json @@ -5,6 +5,13 @@ { "id": "github-pr-happy-path", "path": "fixtures/api/happy-path.json", "kind": "evaluate", "expected": { "build": "built", "status": "ready_for_review", "exit": 0, "complete": true, "requestCount": 26, "remainingResponses": 0, "minAttemptedRequests": 26, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, { "id": "github-pr-merge-target-derived", "path": "fixtures/api/happy-path.json", "variant": "merge-target", "kind": "evaluate", "expected": { "build": "built", "status": "ready_for_review", "exit": 0, "complete": true, "requestCount": 26, "remainingResponses": 0, "minAttemptedRequests": 26, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/merge-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/merge-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, { "id": "github-merge-group-unsupported", "path": "fixtures/api/merge-group-unsupported.json", "kind": "reject", "expected": { "diagnosticId": "GITHUB_API_UNSUPPORTED", "exit": 2, "requestCount": 0, "remainingResponses": 0, "requestSequence": [] } }, - { "id": "github-repository-permission-denied", "path": "fixtures/api/permission-denied.json", "kind": "reject", "expected": { "diagnosticId": "GITHUB_PERMISSION_INSUFFICIENT", "exit": 2, "requestCount": 1, "remainingResponses": 0, "minAttemptedRequests": 1, "requestSequence": ["GET /repos/example/private-service"] } } + { "id": "github-repository-permission-denied", "path": "fixtures/api/permission-denied.json", "kind": "reject", "expected": { "diagnosticId": "GITHUB_PERMISSION_INSUFFICIENT", "exit": 2, "requestCount": 1, "remainingResponses": 0, "minAttemptedRequests": 1, "requestSequence": ["GET /repos/example/private-service"] } }, + { "id": "github-codeowners-precedence-github", "path": "fixtures/api/codeowners-precedence-github.json", "kind": "evaluate", "expected": { "build": "built", "status": "human_review_required", "exit": 0, "complete": true, "requestCount": 22, "remainingResponses": 0, "minAttemptedRequests": 22, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-precedence-root", "path": "fixtures/api/codeowners-precedence-root.json", "kind": "evaluate", "expected": { "build": "built", "status": "human_review_required", "exit": 0, "complete": true, "requestCount": 24, "remainingResponses": 0, "minAttemptedRequests": 24, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-precedence-docs", "path": "fixtures/api/codeowners-precedence-docs.json", "kind": "evaluate", "expected": { "build": "built", "status": "human_review_required", "exit": 0, "complete": true, "requestCount": 26, "remainingResponses": 0, "minAttemptedRequests": 26, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "GET /repos/example/service/contents/CODEOWNERS", "GET /repos/example/service/contents/docs/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-escaped-multiple", "path": "fixtures/api/codeowners-escaped-multiple.json", "kind": "evaluate", "expected": { "build": "built", "status": "ready_for_review", "exit": 0, "complete": true, "requestCount": 32, "remainingResponses": 0, "minAttemptedRequests": 32, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/collaborators/alice/permission", "GET /orgs/org/teams/reviewers", "GET /orgs/org/teams/reviewers/memberships/alice", "GET /orgs/org/teams/security", "GET /orgs/org/teams/security/memberships/alice", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/collaborators/alice/permission", "GET /orgs/org/teams/reviewers", "GET /orgs/org/teams/reviewers/memberships/alice", "GET /orgs/org/teams/security", "GET /orgs/org/teams/security/memberships/alice", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-unsupported", "path": "fixtures/api/codeowners-unsupported.json", "kind": "evaluate", "expected": { "build": "built", "status": "policy_ambiguous", "exit": 0, "complete": false, "requestCount": 22, "remainingResponses": 0, "minAttemptedRequests": 22, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-permission-boundary", "path": "fixtures/api/codeowners-permission-boundary.json", "kind": "evaluate", "expected": { "build": "built", "status": "evidence_missing", "exit": 0, "complete": false, "requestCount": 22, "remainingResponses": 0, "minAttemptedRequests": 22, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } }, + { "id": "github-codeowners-incomplete-changed-paths", "path": "fixtures/api/codeowners-incomplete-changed-paths.json", "kind": "evaluate", "expected": { "build": "built", "status": "evidence_missing", "exit": 0, "complete": false, "requestCount": 22, "remainingResponses": 0, "minAttemptedRequests": 22, "maxResponseBytes": 2097152, "caps": [], "redacted": true, "requestSequence": ["GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection", "GET /repos/example/service", "GET /repos/example/service/pulls/7", "GET /repos/example/service/contents/patchgate.yml", "GET /repos/example/service/pulls/7/files", "GET /repos/example/service/contents/.github/CODEOWNERS", "POST /graphql", "GET /repos/example/service/commits/head-sha/check-runs", "GET /repos/example/service/actions/runs", "GET /repos/example/service/pulls/7/reviews", "GET /repos/example/service/rulesets", "GET /repos/example/service/branches/main/protection"] } } ] } diff --git a/src/github/permissions.ts b/src/github/permissions.ts index 7fd7063..e9b92f5 100644 --- a/src/github/permissions.ts +++ b/src/github/permissions.ts @@ -63,25 +63,25 @@ export async function qualifyReviews(client: GitHubClient, reviews: readonly Rev const bindings: QualificationPrincipal[] = []; const teamIds: number[] = []; const teams: string[] = []; - let qualified = permission.state === "sufficient"; - let complete = permission.state !== "unknown"; - let permissionState: PermissionState = permission.state; + let hasSufficientPrincipal = principals.length === 0 && permission.state === "sufficient"; + let hasUnknownPrincipal = false; for (const principal of principals) { const team = configuredTeamPrincipal(principal); if (team !== undefined) { const membership = await teamMembership(client, team.org, team.slug, review.login, phase); teamStates.push(membership.state); if (membership.principal !== undefined) { bindings.push(membership.principal); teamIds.push(membership.principal.immutableId); teams.push(`@${team.org}/${team.slug}`); } - if (membership.state !== "sufficient") { complete = false; permissionState = membership.state; } - qualified = qualified && membership.state === "sufficient" && permission.state === "sufficient"; + if (membership.state === "sufficient" && permission.state === "sufficient") hasSufficientPrincipal = true; + if (membership.state !== "sufficient") hasUnknownPrincipal = true; } else if (configuredUserPrincipal(principal, review.login)) { const binding: QualificationPrincipal = { configuredPrincipal: principal, kind: "user", immutableId: permission.id ?? review.actorId, membershipState: permission.state === "sufficient" ? "active" : "unknown" }; bindings.push(binding); - qualified = qualified && permission.state === "sufficient"; + if (permission.state === "sufficient") hasSufficientPrincipal = true; } } - if (principals.length > 0 && bindings.length === 0) qualified = false; - if (permission.state === "unknown") complete = false; + const qualified = permission.state === "sufficient" && hasSufficientPrincipal; + const complete = permission.state !== "unknown" && (qualified || !hasUnknownPrincipal); + const permissionState: PermissionState = qualified ? "sufficient" : permission.state === "unknown" || hasUnknownPrincipal ? "unknown" : "insufficient"; output.push({ ...review, qualified, teams: [...new Set(teams)], teamIds: [...new Set(teamIds)], qualification: { ...review.qualification, complete, permissionState, principalBindings: bindings } }); } const aggregate = (states: readonly PermissionState[], identity: string, revision?: string): ObservationMeta => { diff --git a/test/integration/github-adapter.test.ts b/test/integration/github-adapter.test.ts index a8d872c..e34729d 100644 --- a/test/integration/github-adapter.test.ts +++ b/test/integration/github-adapter.test.ts @@ -11,6 +11,8 @@ import { buildGitHubSnapshot } from "../../src/github/snapshot-builder.js"; import type { GitHubSnapshotRequest } from "../../src/github/identity.js"; interface Fixture { + caseId: string; + description: string; request: GitHubSnapshotRequest & { allowConfirmedAbsence?: boolean }; exchanges: RecordedExchange[]; } @@ -25,7 +27,12 @@ async function fixture(name: string): Promise { function client(exchanges: readonly RecordedExchange[]): { client: GitHubClient; transport: RecordedGitHubTransport } { const transport = new RecordedGitHubTransport(exchanges); - return { transport, client: new GitHubClient(transport) }; + return { + transport, + client: new GitHubClient(transport, undefined, { + clock: { now: () => Date.parse("2026-08-24T00:00:00.000Z"), sleep: async () => {} }, + }), + }; } function branchProtectionBody(requiredApprovingReviewCount: number): Record { @@ -238,15 +245,142 @@ describe("authenticated GitHub adapter fixtures", () => { expect(evaluateContribution(result.input, "2026-08-13T00:00:00.000Z").final.status).toBe("ready_for_review"); }); + for (const scenario of [ + { file: "codeowners-precedence-github.json", selectedPath: ".github/CODEOWNERS", skippedPaths: ["CODEOWNERS", "docs/CODEOWNERS"] }, + { file: "codeowners-precedence-root.json", selectedPath: "CODEOWNERS", skippedPaths: ["docs/CODEOWNERS"] }, + { file: "codeowners-precedence-docs.json", selectedPath: "docs/CODEOWNERS", skippedPaths: [] }, + ]) { + it(`resolves CODEOWNERS precedence to ${scenario.selectedPath}`, async () => { + const recorded = await fixture(scenario.file); + const { client: github, transport } = client(recorded.exchanges); + const result = await buildGitHubSnapshot(recorded.request, github, { allowConfirmedAbsence: true }); + + expect(result.kind).toBe("built"); + if (result.kind !== "built") return; + expect(result.input.policySources.find((source) => source.kind === "codeowners")?.identity).toBe(scenario.selectedPath); + expect(result.input.ownershipRequirements).toEqual([{ id: "codeowners.1", owners: ["@org/reviewers"], requiredCount: 1 }]); + if (scenario.skippedPaths.length > 0) { + expect(transport.requested().map((request) => request.path)).not.toEqual(expect.arrayContaining(scenario.skippedPaths.map((path) => `/repos/example/service/contents/${path}`))); + } + expect(evaluateContribution(result.input, "2026-08-13T00:00:00.000Z").final.status).toBe("human_review_required"); + expect(transport.remainingResponses()).toBe(0); + }); + } + + it("replays escaped patterns and treats multiple owners as one required approval", async () => { + const first = await fixture("codeowners-escaped-multiple.json"); + const second = await fixture("codeowners-escaped-multiple.json"); + const firstClient = client(first.exchanges); + const secondClient = client(second.exchanges); + const firstResult = await buildGitHubSnapshot(first.request, firstClient.client, { allowConfirmedAbsence: true }); + const secondResult = await buildGitHubSnapshot(second.request, secondClient.client, { allowConfirmedAbsence: true }); + + expect(firstResult.kind).toBe("built"); + expect(secondResult.kind).toBe("built"); + if (firstResult.kind !== "built" || secondResult.kind !== "built") return; + expect(firstResult.diagnostics).toEqual([]); + expect(firstResult.input.ownershipRequirements).toEqual([ + { id: "codeowners.1", owners: ["@org/reviewers", "@org/security"], requiredCount: 1 }, + { id: "codeowners.2", owners: ["@org/reviewers"], requiredCount: 1 }, + ]); + expect(firstResult.input.reviews).toEqual(expect.arrayContaining([ + expect.objectContaining({ + login: "alice", + qualified: true, + teams: ["@org/reviewers"], + teamIds: [700], + qualification: expect.objectContaining({ + complete: true, + permissionState: "sufficient", + principalBindings: [expect.objectContaining({ configuredPrincipal: "@org/reviewers", membershipState: "active" })], + }), + }), + ])); + expect(canonicalJson(firstResult.input)).toBe(canonicalJson(secondResult.input)); + const receipt = evaluateContribution(firstResult.input, "2026-08-13T00:00:00.000Z"); + expect(receipt.final.status).toBe("ready_for_review"); + expect(receipt.requirements).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: "ownership.codeowners.1", result: "passed", observed: { requiredCount: 1, approvedCount: 1 } }), + expect.objectContaining({ id: "ownership.codeowners.2", result: "passed", observed: { requiredCount: 1, approvedCount: 1 } }), + ])); + expect(firstClient.transport.remainingResponses()).toBe(0); + expect(secondClient.transport.remainingResponses()).toBe(0); + }); + + it("keeps unsupported CODEOWNERS syntax diagnostic and policy-ambiguous", async () => { + const recorded = await fixture("codeowners-unsupported.json"); + const { client: github, transport } = client(recorded.exchanges); + const result = await buildGitHubSnapshot(recorded.request, github, { allowConfirmedAbsence: true }); + + expect(result.kind).toBe("built"); + if (result.kind !== "built") return; + const diagnostics = result.diagnostics.filter((diagnostic) => diagnostic.id === "GITHUB_API_UNSUPPORTED"); + expect(diagnostics).toHaveLength(4); + expect(diagnostics.map((diagnostic) => diagnostic.message)).toEqual(expect.arrayContaining([ + expect.stringContaining("CODEOWNERS line 2 uses syntax outside"), + expect.stringContaining("CODEOWNERS line 3 uses syntax outside"), + expect.stringContaining("CODEOWNERS line 4 uses syntax outside"), + expect.stringContaining("CODEOWNERS line 5 is invalid"), + ])); + expect(diagnostics.every((diagnostic) => diagnostic.remediation.length > 0 && diagnostic.observation === "ownership")).toBe(true); + expect(result.input.observations.ownership).toMatchObject({ complete: false, permissionState: "unknown" }); + expect(evaluateContribution(result.input, "2026-08-13T00:00:00.000Z").final.status).toBe("policy_ambiguous"); + expect(transport.remainingResponses()).toBe(0); + }); + + it("does not turn a CODEOWNERS permission denial into empty ownership", async () => { + const recorded = await fixture("codeowners-permission-boundary.json"); + const { client: github, transport } = client(recorded.exchanges); + const result = await buildGitHubSnapshot(recorded.request, github, { allowConfirmedAbsence: true }); + + expect(result.kind).toBe("built"); + if (result.kind !== "built") return; + expect(result.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ id: "GITHUB_PERMISSION_INSUFFICIENT", observation: "ownership" })])); + expect(result.input.policySources.some((source) => source.kind === "codeowners")).toBe(false); + expect(result.input.ownershipRequirements).toEqual([]); + expect(result.input.observations.ownership).toMatchObject({ complete: false, permissionState: "unknown" }); + expect(result.capability.observations).toContainEqual(expect.objectContaining({ endpoint: "contents/CODEOWNERS@base", state: "insufficient" })); + const receipt = evaluateContribution(result.input, "2026-08-13T00:00:00.000Z"); + expect(receipt.final.status).toBe("evidence_missing"); + expect(receipt.requirements).toEqual(expect.arrayContaining([expect.objectContaining({ id: "ownership.observation", result: "unknown" })])); + expect(transport.remainingResponses()).toBe(0); + }); + + it("does not treat incomplete changed paths as a proven CODEOWNERS non-match", async () => { + const recorded = await fixture("codeowners-incomplete-changed-paths.json"); + const { client: github, transport } = client(recorded.exchanges); + const result = await buildGitHubSnapshot(recorded.request, github, { allowConfirmedAbsence: true }); + + expect(result.kind).toBe("built"); + if (result.kind !== "built") return; + expect(result.diagnostics).toEqual(expect.arrayContaining([expect.objectContaining({ id: "GITHUB_PERMISSION_INSUFFICIENT", observation: "changedPaths" })])); + expect(result.input.observations.changedPaths).toMatchObject({ complete: false, permissionState: "insufficient" }); + expect(result.input.observations.ownership).toMatchObject({ complete: false, permissionState: "unknown" }); + expect(result.input.policySources.some((source) => source.kind === "codeowners")).toBe(true); + expect(result.input.ownershipRequirements).toEqual([]); + const receipt = evaluateContribution(result.input, "2026-08-13T00:00:00.000Z"); + expect(receipt.final.status).toBe("evidence_missing"); + expect(receipt.requirements).toEqual(expect.arrayContaining([expect.objectContaining({ id: "ownership.observation", result: "unknown" })])); + expect(receipt.requirements.some((requirement) => requirement.id === "ownership.no_match")).toBe(false); + expect(transport.remainingResponses()).toBe(0); + }); + it("keeps the API fixture manifest bijective with owned JSON fixtures", async () => { const manifest = JSON.parse(await readFile(resolve("fixtures/api/manifest.json"), "utf8")) as FixtureManifest; const files = (await readdir(resolve("fixtures/api"))).filter((file) => file.endsWith(".json") && file !== "manifest.json").map((file) => `fixtures/api/${file}`).sort(); const references = [...new Set(manifest.cases.map((item) => item.path))].sort(); expect(references).toEqual(files); expect(new Set(manifest.cases.map((item) => item.id)).size).toBe(manifest.cases.length); + const codeownersDescriptions = new Set(); for (const item of manifest.cases) { expect(files).toContain(item.path); const recorded = await fixture(item.path.slice("fixtures/api/".length)); + if (item.path.includes("/codeowners-")) { + expect(recorded.caseId).toBe(item.id); + expect(recorded.description).toMatch(/^CODEOWNERS: /); + expect(recorded.request).toMatchObject({ eventKind: "pull_request", targetKind: "head", allowConfirmedAbsence: true }); + codeownersDescriptions.add(recorded.description); + } let exchanges = structuredClone(recorded.exchanges); const request = item.variant === "merge-target" ? { ...recorded.request, targetKind: "merge" as const } : recorded.request; if (item.variant === "merge-target") { @@ -291,7 +425,8 @@ describe("authenticated GitHub adapter fixtures", () => { expect(Object.values(result.input.observations).flatMap((value) => Array.isArray(value) ? value : [value]).every((meta) => meta.complete && meta.permissionState === "sufficient")).toBe(item.expected.complete); expect(result.input.policySources.every((source) => /^sha256:[0-9a-f]{64}$/.test(source.digest))).toBe(true); const observationMetas = Object.values(result.input.observations).flatMap((value) => Array.isArray(value) ? value : [value]); - expect(observationMetas.every((meta) => /^sha256:[0-9a-f]{64}$/.test(meta.normalizedDigest ?? ""))).toBe(true); + expect(observationMetas.filter((meta) => meta.complete).every((meta) => /^sha256:[0-9a-f]{64}$/.test(meta.normalizedDigest ?? ""))).toBe(true); + expect(observationMetas.filter((meta) => !meta.complete).every((meta) => meta.normalizedDigest === undefined)).toBe(true); expect(result.metrics.caps).toEqual(item.expected.caps ?? []); const report = redactForReport({ input: result.input, diagnostics: result.diagnostics, metrics: result.metrics, capability: result.capability }); assertRedacted(report); @@ -307,6 +442,7 @@ describe("authenticated GitHub adapter fixtures", () => { expect(transport.requested().map((request) => `${request.method} ${request.path}`)).toEqual(item.expected.requestSequence); expect(transport.requested()).toHaveLength(Number(item.expected.requestCount)); } + expect(codeownersDescriptions).toHaveLength(7); expect(manifest.cases.map((item) => item.id)).toEqual(expect.arrayContaining(["github-pr-happy-path", "github-pr-merge-target-derived", "github-merge-group-unsupported", "github-repository-permission-denied"])); }); });