Bug 1786866 - Support the 'public_restricted' pullRequest policy, r=releng-reviewers,aki,jcristau
authorAndrew Halberstadt <ahal@mozilla.com>
Tue, 11 Oct 2022 18:22:31 +0000
changeset 1500 264f0a154aad612bbfba163f0d01fa201572e134
parent 1499 7f710a164bd6d9c375fb1cdada89bbbc595d6a1e
child 1501 37568e5e5748d4f75c7ac0a45a8f1aab3c011799
push id1318
push userahalberstadt@mozilla.com
push dateTue, 11 Oct 2022 18:25:03 +0000
treeherderci-configuration@264f0a154aad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersreleng-reviewers, aki, jcristau
bugs1786866
Bug 1786866 - Support the 'public_restricted' pullRequest policy, r=releng-reviewers,aki,jcristau The ci-admin diff for this revision is identical. Differential Revision: https://phabricator.services.mozilla.com/D157493
grants.yml
projects.yml
src/ciadmin/check/check_pull_request_policies.py
src/ciadmin/generate/ciconfig/projects.py
src/ciadmin/generate/grants.py
tests/ciadmin/test_generate_ciconfig_projects.py
tests/ciadmin/test_generate_grants.py
--- a/grants.yml
+++ b/grants.yml
@@ -811,17 +811,17 @@
         feature: trust-domain-scopes
 
 - grant:
     # routes to support indexing by product
     - queue:route:index.{trust_domain}.v2.{alias}-pr.*
     - index:insert-task:{trust_domain}.v2.{alias}-pr.*
   to:
     - project:
-        job: [pull-request]
+        job: [pull-request:*]
         feature: trust-domain-scopes
 
 - grant:
     # routes to support reporting to treeherder
     - queue:route:tc-treeherder-stage.{alias}.*
     - queue:route:tc-treeherder.{alias}.*
     - queue:route:tc-treeherder-stage.v2.{alias}.*
     - queue:route:tc-treeherder.v2.{alias}.*
@@ -831,17 +831,17 @@
         include_pull_requests: false
 
 - grant:
     # routes to support reporting to treeherder
     - queue:route:tc-treeherder-stage.v2.{alias}-pr.*
     - queue:route:tc-treeherder.v2.{alias}-pr.*
   to:
     - project:
-        job: [pull-request]
+        job: [pull-request:*]
         feature: treeherder-reporting
 
 
 - grant:
     - queue:create-task:{priority}:hg-t/*
     - queue:route:notify.irc-channel.*
     - queue:route:tc-treeherder.v2.version-control-tools.*
   to:
@@ -856,16 +856,17 @@
     - project:
         trust_domain: [taskgraph, ci]
 
 - grant:
     - secrets:get:project/releng/taskgraph/ci
   to:
     - project:
         alias: taskgraph
+        job: [action:*, branch:*, pull-request:trusted, release]
 
 - grant:
     - secrets:get:project/releng/shipit/ci
   to:
     - project:
         alias: shipit
 
 ##
@@ -980,25 +981,25 @@
 
 # - scriptworker specific roles
 - grant:
     - secrets:get:repo:github.com/mozilla-releng/scriptworker:coveralls
     - secrets:get:repo:github.com/mozilla-releng/scriptworker:github
   to:
     - project:
         alias: scriptworker
-        job: [pull-request, branch:main]
+        job: [pull-request:trusted, branch:main]
 
 # - balrog specific roles
 - grant:
     - secrets:get:repo:github.com/mozilla-releng/balrog:coveralls
   to:
     - project:
         alias: balrog
-        job: [pull-request, branch:master, branch:main]
+        job: [pull-request:*, branch:master, branch:main]
 
 - grant:
     - queue:route:index.project.balrog.*
     - queue:route:notify.*
     - secrets:get:repo:github.com/mozilla-releng/balrog:dockerhub
   to:
     - project:
         alias: balrog
@@ -1049,17 +1050,17 @@
         job: [branch:master, branch:production]
 
 # - occ specific roles
 - grant:
     - queue:route:index.project.releng.opencloudconfig.v1.revision.*
   to:
     - project:
         alias: occ
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:*]
 
 - grant:
     - secrets:get:repo:github.com/mozilla-releng/OpenCloudConfig:updatetooltoolrepo
     - secrets:get:repo:github.com/mozilla-releng/OpenCloudConfig:updateworkertype
   to:
     - project:
         alias: occ
         job: [branch:alpha, branch:beta, branch:master]
@@ -1164,17 +1165,17 @@
   environment: staging
 
 # - mapper specific roles
 - grant:
     - secrets:get:project/releng/mapper/ci
   to:
     - projects:
         alias: mapper
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:*]
 
 - grant:
     - secrets:get:project/releng/mapper/deploy
   to:
     - projects:
         alias: mapper
         job: [branch:dev, branch:staging, branch:production]
 
@@ -1209,32 +1210,32 @@
         job: [branch:production, branch:dev]
 
 # - tooltool specific roles
 - grant:
     - secrets:get:project/releng/tooltool/ci
   to:
     - projects:
         alias: tooltool
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:*]
 
 - grant:
     - secrets:get:project/releng/tooltool/deploy
   to:
     - projects:
         alias: tooltool
         job: [branch:dev, branch:staging, branch:production]
 
 # - treestatus specific roles
 - grant:
     - secrets:get:project/releng/treestatus/ci
   to:
     - projects:
         alias: treestatus
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:*]
 
 - grant:
     - secrets:get:project/releng/treestatus/deploy
   to:
     - projects:
         alias: treestatus
         job: [branch:dev, branch:staging, branch:production]
 
@@ -1285,17 +1286,17 @@
     - project:
         feature: mobile-public-code-coverage
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:beetmover:action:push-to-maven
   to:
     - project:
         feature: beetmover-maven-phase
-        job: [release, pull-request, action:release-promotion]
+        job: [release, pull-request:*, action:release-promotion]
     - project:
         feature: beetmover-maven-nightly-phase
         job: [cron:nightly]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:beetmover:bucket:maven-production
   to:
     - project:
@@ -1308,33 +1309,33 @@
     - project:{trust_domain}:{trust_project}:releng:beetmover:bucket:maven-nightly-staging
   to:
     - project:
         feature: beetmover-maven-phase
         level: 1
         job: [release, action:release-promotion, cron:nightly]
     - project:
         feature: beetmover-maven-phase
-        job: [pull-request]
+        job: [pull-request:*]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:beetmover:bucket:maven-nightly-production
   to:
     - project:
         feature: beetmover-maven-nightly-phase
         level: 3
         job: [cron:nightly]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:beetmover:bucket:dep
     - project:{trust_domain}:{trust_project}:releng:beetmover:action:direct-push-to-bucket
   to:
     - project:
         feature: beetmover-phase
-        job: [pull-request]
+        job: [pull-request:*]
     - project:
         feature: beetmover-phase
         level: 1
         job: [action:release-promotion]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:beetmover:bucket:nightly
     - project:{trust_domain}:{trust_project}:releng:beetmover:action:direct-push-to-bucket
@@ -1358,17 +1359,17 @@
         level: 3
         job: [release, action:release-promotion]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:github:action:release
   to:
     - project:
         feature: github-publication
-        job: [action:release-promotion, release, pull-request]
+        job: [action:release-promotion, release, pull-request:*]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:github:project:{trust_project}
   to:
     - project:
         feature: github-publication
         level: 3
         job: [action:release-promotion, release]
@@ -1377,17 +1378,17 @@
     - project:{trust_domain}:{trust_project}:releng:github:project:mock
   to:
     - project:
         feature: github-publication
         level: 1
         job: [release, action:release-promotion]
     - project:
         feature: github-publication
-        job: [pull-request]
+        job: [pull-request:*]
 
 - grant:
     - project:{trust_domain}:{trust_project}:releng:github:project:{alias}
   to:
     - project:
         feature: github-publication
         level: 1
         job: [action:release-promotion, release]
@@ -1444,17 +1445,17 @@
         alias: fenix
         job: [cron:nightly-on-google-play]
 
 - grant:
     - project:mobile:{trust_project}:releng:googleplay:product:{trust_project}:dep
   to:
     - project:
         feature: mobile-pushapk-phase
-        job: [pull-request]
+        job: [pull-request:*]
     - project:
         feature: mobile-pushapk-phase
         level: 1
         job: [release, cron:*, action:release-promotion]
 
 - grant:
     - secrets:get:project/mobile/{trust_project}/firebase
   to:
@@ -1465,26 +1466,26 @@
         # Fenix PRs are restricted to collaborators, so exposing firebase is safe-enough for PRs.
         # Fenix also has some Firebase tests on nightly.
         alias: fenix
         job:
           - cron:nightly
           - cron:nightly-on-google-play
           - cron:screenshots
           - cron:legacy-api-ui-tests
-          - pull-request
+          - pull-request:trusted
     - project:
         # Focus PRs are restricted to collaborators, so exposing firebase is safe-enough for PRs.
         # Focus also has some Firebase tests on nightly.
         alias: focus-android
-        job: [cron:nightly, pull-request]
+        job: [cron:nightly, pull-request:trusted]
     - project:
         # TODO - remove once focus/taskcluster work is complete
         alias: staging-focus-android
-        job: [pull-request]
+        job: [pull-request:trusted]
 
 - grant:
     - secrets:get:project/mobile/github
   to:
     - project:
         feature: mobile-bump-github
         level: 3
         job: [cron:bump-*]
@@ -1494,17 +1495,17 @@
     - queue:create-task:highest:proj-autophone/gecko-t-ap-perf-g5
     - queue:create-task:highest:proj-autophone/gecko-t-ap-perf-p2
     - queue:create-task:highest:proj-autophone/gecko-t-bitbar-gw-perf-g5
     - queue:create-task:highest:proj-autophone/gecko-t-bitbar-gw-perf-p2
     - queue:create-task:highest:proj-autophone/gecko-t-bitbar-gw-perf-a51
   to:
     - project:
         feature: autophone
-        job: [pull-request]
+        job: [pull-request:*]
     - project:
         alias: fenix
         job: [cron:nightly, cron:nightly-test]
     - project:
         alias: reference-browser
         job: [branch:*]
 
 - grant:
@@ -1529,24 +1530,24 @@
 
 - grant:
     - queue:route:notify.email.android-components-team@mozilla.com.on-failed
     - queue:route:notify.email.geckoview-core@mozilla.com.on-failed
   to:
     - project:
         alias: android-components
         # Used in order to warn the AC team whenever a GV update cannot be merged
-        job: [pull-request]
+        job: [pull-request:*]
 
 - grant:
     - project:releng:ship-it:action:mark-as-shipped
   to:
     - project:
         feature: shipit
-        job: [release, pull-request]
+        job: [release, pull-request:*]
     - project:
         feature: [shipit, taskgraph-actions]
         job: [action:release-promotion]
 
 - grant:
     - project:releng:ship-it:server:production
   to:
     - project:
@@ -1565,40 +1566,40 @@
     - project:releng:ship-it:server:staging
   to:
     - project:
         # TODO: once pull-request-based staging releases are more stable and
         # available for all mobile projects, we can get rid of this `level=1`
         # section which addresses the RelEngers forks
         level: 1
         feature: shipit
-        job: [release, pull-request]
+        job: [release, pull-request:*]
     - project:
         level: 1
         feature: [shipit, taskgraph-actions]
         job: [action:release-promotion]
     - project:
         level: 3
         feature: [shipit, taskgraph-actions]
-        job: [pull-request]
+        job: [pull-request:*]
 # fenix specific scopes
 - grant:
     - queue:route:index.project.fenix.android.preview-builds
     - github:create-comment:mozilla-mobile/fenix
   to:
     - project:
         alias: fenix
-        job: [pull-request]
+        job: [pull-request:*]
 
 - grant:
     - secrets:get:project/mobile/fenix/public-tokens
   to:
     - project:
         alias: fenix
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:trusted]
 
 - grant:
     - secrets:get:project/mobile/fenix/nightly-simulation
   to:
     - project:
         alias: fenix
         job: [branch:*, cron:nightly-test]
 
@@ -1670,24 +1671,24 @@
 
 # focus (android) scopes
 - grant:
     - queue:scheduler-id:taskcluster-github
     - queue:route:statuses
   to:
     - project:
         alias: focus-android
-        job: [branch:*, cron:*, pull-request, release]
+        job: [branch:*, cron:*, pull-request:*, release]
 
 - grant:
     - queue:route:notify.irc-channel.#android-ci.on-any
   to:
     - project:
         alias: focus-android
-        job: [branch:*, pull-request]
+        job: [branch:*, pull-request:*]
 
 - grant:
     - secrets:get:project/mobile/focus-android/nightly
     - secrets:get:project/mobile/focus-android/beta
     - secrets:get:project/mobile/focus-android/release
   to:
     - project:
         alias: focus-android
@@ -1719,17 +1720,17 @@
 
 # firefox-tv specific scopes
 - grant:
     - secrets:get:project/mobile/firefox-tv/tokens
     - queue:route:index.project.{trust_domain}.{alias}.cache.level-{level}.*
   to:
     - project:
         alias: firefox-tv
-        job: [branch:*, pull-request, release]
+        job: [branch:*, pull-request:*, release]
 
 - grant:
     - queue:route:notify.email.firefox-tv@mozilla.com.on-completed
     - project:mobile:firefox-tv:releng:signing:cert:production-signing
   to:
     - project:
         alias: firefox-tv
         job: [release]
@@ -1762,17 +1763,17 @@
     - queue:create-task:highest:l10n-{level}/*
     - queue:create-task:{priority}:built-in/*
     - queue:route:index.{trust_domain}.{alias}.cache.level-{level}.*
     - queue:route:notify.email.*
   to:
     - project:
         alias:
           - android-l10n-tooling
-        job: [pull-request, branch:*, cron:*]
+        job: [pull-request:*, branch:*, cron:*]
 
 - grant:
     - secrets:get:l10n/level-{level}/*
   to:
     - project:
         alias:
           - android-l10n-tooling
         job: [branch:*, cron:*]
@@ -1785,17 +1786,17 @@
     - queue:scheduler-id:{trust_domain}-level-{level}
     - docker-worker:cache:{trust_domain}-level-{level}-*
     - generic-worker:cache:{trust_domain}-level-{level}-*
     - queue:route:notify.email.*
   to:
     - project:
         alias:
           - elmo-taskcluster
-        job: [pull-request, branch:*, cron:*]
+        job: [pull-request:*, branch:*, cron:*]
 
 - grant:
     - secrets:get:l10n/level-{level}/*
   to:
     - project:
         alias:
           - elmo-taskcluster
         job: [branch:*, cron:*]
@@ -1824,17 +1825,17 @@
     - generic-worker:cache:{trust_domain}-level-1-*
   to:
     - project:
         alias:
           - mozilla-vpn-client
         job:
           - branch:*
           - cron:*
-          - pull-request
+          - pull-request:*
           - release
 
 - grant:
     - project:mozillavpn:releng:beetmover:action:push-to-candidates
     - project:mozillavpn:releng:beetmover:bucket:release
   to:
     - project:
         alias:
@@ -1859,26 +1860,31 @@
     - project:mozillavpn:releng:beetmover:bucket:dep
     - project:mozillavpn:releng:signing:cert:dep-signing
   to:
     - project:
         alias:
           - mozilla-vpn-client
         job:
           - branch:*
-          - pull-request
+          - pull-request:*
           - release
 
 - grant:
     - project:{trust_project}:releng:signing:format:*
     - project:releng:services/tooltool/api/download/public
     - project:releng:services/tooltool/api/download/internal
   to:
     - project:
         alias: mozilla-vpn-client
+        job:
+          - action:*
+          - branch:*
+          - pull-request:trusted
+          - release
 
 - grant:
     - project:mozillavpn:releng:googleplay:product:mozillavpn
     - queue:route:index.{trust_domain}.v2.mozilla-vpn-client.release.*
   to:
     - project:
         alias:
           - mozilla-vpn-client
@@ -1935,17 +1941,17 @@
     - project:releng:services/tooltool/api/download/public
     - project:releng:services/tooltool/api/download/internal
   to:
     - project:
         alias:
           - staging-mozilla-vpn-client
         job:
           - branch:*
-          - pull-request
+          - pull-request:trusted
           - release
           - action:release-promotion
 
 - grant:
     - secrets:get:project/mozillavpn/tokens
   to:
     - project:
         alias:
@@ -2023,17 +2029,17 @@
     - queue:scheduler-id:xpi-level-1
     - docker-worker:cache:xpi-level-1-*
     - secrets:get:project/xpi/xpi-github-clone-ssh
     - project:xpi:releng:signing:cert:dep-signing
     - queue:create-task:low:scriptworker-k8s/xpi-t-*
   to:
     - project:
         feature: xpi-roles
-        job: [pull-request, branch:*, cron:*, action:*, tag:*]
+        job: [pull-request:*, branch:*, cron:*, action:*, tag:*]
     - roles:
         # The mozilla-extensions github organization is designed to allow for
         #
         # easily creating new repos for xpi source. Let's automatically
         # give them level 1 scopes for master, PRs, and other branches.
         - repo:github.com/mozilla-extensions/*
         - repo:github.com/mozilla-releng/staging-xpi-*
 
@@ -2067,17 +2073,17 @@
     - queue:route:index.{trust_domain}.v2.{alias}.*
     - queue:route:index.{trust_domain}.v2.staging-adhoc-manifest.*
     - queue:route:index.adhoc-signing.cache.level-{level}.*
     - queue:get-artifact:releng/adhoc/*
     - queue:route:notify.email.*
   to:
     - project:
         feature: adhoc-roles
-        job: [branch:*, action:*, pull-request, action:*, cron:*]
+        job: [branch:*, action:*, pull-request:trusted, action:*, cron:*]
 
 - grant:
     - project:adhoc:releng:signing:cert:release-signing
     - project:adhoc:releng:signing:cert:nightly-signing
     - project:adhoc:releng:ship-it:server:production
     - queue:route:index.{trust_domain}.v2.adhoc-manifest.*
   to:
     - project:
@@ -2097,17 +2103,17 @@
     # explicitly grant level 1 scopes for PRs
     - queue:scheduler-id:scriptworker-level-1
     - queue:create-task:highest:scriptworker-1/*
     - queue:route:index.scriptworker.cache.level-1.*
   to:
     - project:
         alias:
           - scriptworker-scripts
-        job: [branch:*, action:*, pull-request, action:*, cron:*]
+        job: [branch:*, action:*, pull-request:*, action:*, cron:*]
 
 # - scriptworker-scripts specific roles
 # XXX delete these once we port scriptworker-scripts cloudops deploys to CoT
 #     downloads
 - grant:
     - secrets:get:project/releng/scriptworker-scripts/deploy
   to:
     - projects:
--- a/projects.yml
+++ b/projects.yml
@@ -36,16 +36,17 @@
 #   - `beetmover-maven-nightly-phase` -- this https://github.com/mozilla-mobile repository should have beetmover nightly production related scopes associated
 #   - `mobile-firebase-testing` -- this https://github.com/mozilla-mobile repository has end-to-end testing involving devices hosted at Google's Firebase
 #   - `mobile-bump-github` -- this https://github.com/mozilla-mobile repository gets bumped.
 #   - `mobile-public-code-coverage` -- this https://github.com/mozilla-mobile repository has code coverage enabled on PRs
 #   - `mobile-sign-phase` -- this https://github.com/mozilla-mobile repository should have sign related scopes associated
 #   - `mobile-pushapk-phase` -- this https://github.com/mozilla-mobile repository should have pushapk related scopes associated
 #   - `github-taskgraph` -- this github repository has started using taskgraph
 #   - `github-publication` -- this github repository automates publication to github releases
+#   - `github-pull-request` -- this github repository uses github pull requests; Value should include a 'policy' matching the repo's `pullRequestPolicy`
 #   - `gecko-actions` -- this repository should have gecko-related action hooks defined
 #   - 'scriptworker' -- this repository uses at least one scriptworker instance
 #   - 'shipit' -- this repository interacts with a shipit instance
 #   - 'taskgraph-cron' -- tasks defined in `.cron.yml` can run for this project (non-gecko/comm taskgraph repos only)
 #   - 'taskgraph-actions' -- this repository defines in-tree actions and should have corresponding hooks defined
 #   - `trust-domain-scopes` -- this repository should have generic scopes associated to its trust domain
 #   - `treeherder-reporting` -- this repository reports results to treeherder
 #   - `autophone` -- this repository is allowed to use the Android farm called "Autophone"
@@ -460,16 +461,18 @@ ci-configuration-try:
 taskgraph:
   repo: https://github.com/taskcluster/taskgraph
   repo_type: git
   trust_domain: taskgraph
   default_branch: main
   level: 3
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     taskgraph-actions: true
     trust-domain-scopes: true
     treeherder-reporting: true
 
 version-control-tools:
   repo: https://hg.mozilla.org/hgcustom/version-control-tools
   repo_type: hg
   access: scm_versioncontrol
@@ -484,16 +487,18 @@ firefox-android:
   repo_type: git
   default_branch: main
   level: 3
   features:
     beetmover-maven-phase: true
     beetmover-maven-nightly-phase: true
     github-publication: true
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
     taskgraph-cron: true
@@ -508,16 +513,18 @@ staging-firefox-android:
   trust_project: firefox-android
   repo_type: git
   default_branch: main
   level: 1
   features:
     beetmover-maven-phase: true
     beetmover-maven-nightly-phase: true
     github-publication: true
+    github-pull-request:
+      policy: public
     github-taskgraph: true
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
@@ -536,16 +543,18 @@ android-components:
   repo_type: git
   default_branch: main
   level: 3
   features:
     beetmover-maven-phase: true
     beetmover-maven-nightly-phase: true
     github-publication: true
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
     taskgraph-cron: true
@@ -561,16 +570,18 @@ staging-android-components:
   repo_type: git
   default_branch: main
   level: 1
   features:
     beetmover-maven-phase: true
     beetmover-maven-nightly-phase: true
     github-publication: true
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
     taskgraph-cron: true
@@ -585,16 +596,18 @@ reference-browser:
   trust_domain: mobile
   trust_project: reference-browser
   repo_type: git
   default_branch: master
   level: 3
   features:
     autophone: true
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     mobile-bump-github: true
     mobile-firebase-testing: true
     mobile-sign-phase: true
     mobile-pushapk-phase: true
     scriptworker: true
     taskgraph-actions: true
     taskgraph-cron: true
@@ -613,16 +626,18 @@ fenix:
   trust_project: fenix
   repo_type: git
   default_branch: main
   level: 3
   features:
     autophone: true
     beetmover-phase: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     github-publication: true
     mobile-roles: true
     mobile-bump-github: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-pushapk-phase: true
     mobile-sign-phase: true
     scriptworker: true
@@ -645,16 +660,18 @@ staging-fenix:
   repo_type: git
   default_branch: main
   level: 1
   features:
     autophone: true
     beetmover-phase: true
     github-publication: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-pushapk-phase: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
@@ -671,16 +688,18 @@ focus-android:
   trust_domain: mobile
   trust_project: focus-android
   repo_type: git
   default_branch: main
   level: 3
   features:
     beetmover-phase: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     github-publication: true
     mobile-roles: true
     mobile-bump-github: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-pushapk-phase: true
     mobile-sign-phase: true
     scriptworker: true
@@ -698,16 +717,18 @@ staging-focus-android:
   trust_domain: mobile
   trust_project: focus-android
   repo_type: git
   default_branch: main
   level: 1
   features:
     github-publication: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     mobile-roles: true
     mobile-firebase-testing: true
     mobile-public-code-coverage: true
     mobile-pushapk-phase: true
     mobile-sign-phase: true
     scriptworker: true
     shipit: true
     taskgraph-actions: true
@@ -722,32 +743,36 @@ staging-focus-android:
 firefox-ios:
   repo: https://github.com/mozilla-mobile/firefox-ios
   trust_domain: mobile
   trust_project: firefox-ios
   repo_type: git
   level: 3
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     taskgraph-actions: true
     taskgraph-cron: true
     treeherder-reporting: true
     trust-domain-scopes: true
   cron:
     targets: [l10-screenshots]
 
 focus-ios:
   repo: https://github.com/mozilla-mobile/focus-ios
   trust_domain: mobile
   trust_project: focus-ios
   repo_type: git
   level: 3
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     mobile-roles: true
     taskgraph-actions: true
     taskgraph-cron: true
     treeherder-reporting: true
     trust-domain-scopes: true
   cron:
     targets: [l10-screenshots]
 
@@ -757,161 +782,212 @@ application-services:
   repo_type: git
   level: 3
   trust_domain: app-services
   trust_project: app-services
   features:
     # TODO Bug 1601687 - Enable `beetmover-maven-phase` once scriptworker scopes are predictible
     # across projects.
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     scriptworker: true
     taskgraph-cron: true
     taskgraph-actions: true
 
 glean:
   repo: https://github.com/mozilla/glean
   repo_type: git
   level: 3
   trust_domain: glean
   trust_project: glean
   default_branch: main
   features:
     # TODO Bug 1601687 - Enable `beetmover-maven-phase` once scriptworker scopes are predictible
     # across projects.
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     scriptworker: true
     taskgraph-actions: true
 
 # mozilla-releng Github repositories
 k8s-autoscale:
   repo: https://github.com/mozilla-releng/k8s-autoscale
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 mapper:
   repo: https://github.com/mozilla-releng/mapper
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 product-details:
   repo: https://github.com/mozilla-releng/product-details
   repo_type: git
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: collaborators
 
 shipit:
   repo: https://github.com/mozilla-releng/shipit
   repo_type: git
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     trust-domain-scopes: true
   trust_domain: releng
   level: 3
 
 tooltool:
   repo: https://github.com/mozilla-releng/tooltool
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 treestatus:
   repo: https://github.com/mozilla-releng/treestatus
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 balrog:
   repo: https://github.com/mozilla-releng/balrog
   repo_type: git
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 taskhuddler:
   repo: https://github.com/mozilla-releng/taskhuddler
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 scriptworker:
   repo: https://github.com/mozilla-releng/scriptworker
   repo_type: git
   default_branch: main
   trust_domain: scriptworker
   level: 3
   features:
     trust-domain-scopes: true
     taskgraph-actions: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
 
-# mozilla-releng Github repositories
 build-puppet:
   repo: https://github.com/mozilla-releng/build-puppet
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 1
+  features:
+    github-pull-request:
+      policy: public
 
 occ:
   repo: https://github.com/mozilla-releng/OpenCloudConfig
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 3
+  features:
+    github-pull-request:
+      policy: public
 
 winsign:
   repo: https://github.com/mozilla-releng/winsign
   repo_type: git
   default_branch: main
   trust_domain: releng
   level: 1
+  features:
+    github-pull-request:
+      policy: public
 
 mozilla-version:
   repo: https://github.com/mozilla-releng/mozilla-version
   repo_type: git
   default_branch: main
   trust_domain: releng
   level: 1
+  features:
+    github-pull-request:
+      policy: public
 
 build-mar:
   repo: https://github.com/mozilla-releng/build-mar
   repo_type: git
   default_branch: master
   trust_domain: releng
   level: 1
+  features:
+    github-pull-request:
+      policy: public
 
 cloud-image-builder:
   repo: https://github.com/mozilla-platform-ops/cloud-image-builder
   repo_type: git
   default_branch: main
   trust_domain: relops
   level: 3
+  features:
+    github-pull-request:
+      policy: collaborators
 
 cloud-image-deploy:
   repo: https://github.com/mozilla-platform-ops/cloud-image-deploy
   repo_type: git
   default_branch: main
   trust_domain: relops
   level: 3
+  features:
+    github-pull-request:
+      policy: collaborators
 
 # L10n repositories
 android-l10n-tooling:
   repo: https://github.com/mozilla-l10n/android-l10n-tooling
   repo_type: git
   default_branch: master
   level: 3
   trust_domain: l10n
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: public
     taskgraph-cron: true
     taskgraph-actions: true
   cron:
     targets:
       - target: update-l10n
         bindings:
           - exchange: exchange/taskcluster-github/v1/push
             routing_key_pattern: primary.mozilla-mobile.fenix
@@ -927,142 +1003,166 @@ mozilla-vpn-client:
   repo: https://github.com/mozilla-mobile/mozilla-vpn-client
   repo_type: git
   default_branch: main
   level: 3
   trust_domain: mozillavpn
   trust_project: mozillavpn
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     taskgraph-actions: true
     # Enable this after .taskcluster.yml and .cron.yml exist in the repo
     # taskgraph-cron: true
     treeherder-reporting: true
     trust-domain-scopes: true
 
 staging-mozilla-vpn-client:
   repo: https://github.com/mozilla-releng/staging-mozilla-vpn-client
   repo_type: git
   default_branch: main
   level: 1
   trust_domain: mozillavpn
   trust_project: mozillavpn
   features:
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
     taskgraph-actions: true
     # Enable this after .taskcluster.yml and .cron.yml exist in the repo
     # taskgraph-cron: true
     treeherder-reporting: true
     trust-domain-scopes: true
 
 # Bug 1520281: Add user repositories for experimenting with taskgraph and mobile
 # repo:github.com/mozilla-extensions/* is configured in grants.yml, under the xpi-roles feature
 xpi-manifest:
   repo: https://github.com/mozilla-extensions/xpi-manifest
   repo_type: git
   default_branch: main
   trust_domain: xpi
   level: 3
   features:
+    github-pull-request:
+      policy: collaborators
     trust-domain-scopes: true
     xpi-roles: true
     taskgraph-actions: true
     taskgraph-cron: true
 
 rally-core-addon:
   repo: https://github.com/mozilla-rally/rally-core-addon
   repo_type: git
   default_branch: master
   trust_domain: xpi
   level: 1
   features:
+    github-pull-request:
+      policy: collaborators
     trust-domain-scopes: true
     taskgraph-actions: true
     xpi-roles: true
 
 staging-xpi-manifest:
   repo: https://github.com/mozilla-releng/staging-xpi-manifest
   repo_type: git
   default_branch: main
   trust_domain: xpi
   level: 1
   features:
+    github-pull-request:
+      policy: collaborators
     trust-domain-scopes: true
     xpi-roles: true
     taskgraph-actions: true
     taskgraph-cron: true
 
 staging-xpi-public:
   repo: https://github.com/mozilla-releng/staging-xpi-public
   repo_type: git
   default_branch: main
   trust_domain: xpi
   level: 1
   features:
+    github-pull-request:
+      policy: public
     trust-domain-scopes: true
     xpi-roles: true
 
 staging-xpi-private:
   repo: https://github.com/mozilla-releng/staging-xpi-private
   repo_type: git
   default_branch: master
   trust_domain: xpi
   level: 1
   features:
+    github-pull-request:
+      policy: collaborators
     trust-domain-scopes: true
     xpi-roles: true
 
 # Bug 1594621: Create an adhoc signing repository
 staging-adhoc-signing:
   repo: https://github.com/mozilla-releng/staging-adhoc-signing
   repo_type: git
   default_branch: master
   trust_domain: adhoc
   level: 1
   features:
     trust-domain-scopes: true
     adhoc-roles: true
     taskgraph-actions: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
 
 adhoc-signing:
   repo: https://github.com/mozilla-releng/adhoc-signing
   repo_type: git
   default_branch: main
   trust_domain: adhoc
   level: 3
   features:
     trust-domain-scopes: true
     adhoc-roles: true
     taskgraph-actions: true
     github-taskgraph: true
+    github-pull-request:
+      policy: collaborators
 
 # Bug 1614312: Add code-analysis project for code-review & code-coverage
 code-review:
   repo: https://github.com/mozilla/code-review
   repo_type: git
   default_branch: master
   trust_domain: code-analysis
   level: 3
   features:
     trust-domain-scopes: true
+    github-pull-request:
+      policy: public
 
 # Bug 1597598: Add taskgraph support to scriptworker-scripts
 scriptworker-scripts:
   repo: https://github.com/mozilla-releng/scriptworker-scripts
   repo_type: git
   default_branch: master
   trust_domain: scriptworker
   level: 3
   features:
     trust-domain-scopes: true
     taskgraph-actions: true
     github-taskgraph: true
+    github-pull-request:
+      policy: public
 
 # Bug 1617635: Support building & indexing code-coverage docker image
 code-coverage:
   repo: https://github.com/mozilla/code-coverage
   repo_type: git
   default_branch: master
   trust_domain: code-analysis
   level: 3
   features:
     trust-domain-scopes: true
+    github-pull-request:
+      policy: public
new file mode 100644
--- /dev/null
+++ b/src/ciadmin/check/check_pull_request_policies.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at http://mozilla.org/MPL/2.0/.
+
+import pytest
+import yaml
+from tcadmin.util.sessions import with_aiohttp_session
+
+from ciadmin.generate import tcyml
+from ciadmin.generate.ciconfig.projects import Project
+
+
+async def _get_pull_request_policy(project):
+    config = yaml.safe_load(
+        await tcyml.get(
+            project.repo,
+            repo_type=project.repo_type,
+            revision=None,
+            default_branch=project.default_branch,
+        )
+    )
+    return config.get("policy", {}).get("pullRequests")
+
+
+@pytest.mark.asyncio
+@with_aiohttp_session
+async def check_pull_request_policies_for_git_repos():
+    """Ensures that the pull-request policy defined in projects.yml
+    matches the one in-repo.
+    """
+    tcyml_v0_projects = {"build-puppet", "occ"}
+
+    projects = await Project.fetch_all()
+
+    def filter_project(p):
+        # TODO: find a better flag to filter out private repos
+        return (
+            p.repo_type == "git"
+            and "private" not in p.repo
+            and p.feature("github-pull-request")
+            and p.alias not in tcyml_v0_projects
+        )
+
+    pr_policies = {
+        project.alias: project.feature("github-pull-request", key="policy")
+        for project in projects
+        if filter_project(project)
+    }
+    github_pr_policies = {
+        project.alias: await _get_pull_request_policy(project)
+        for project in projects
+        if filter_project(project)
+    }
+    assert pr_policies == github_pr_policies
--- a/src/ciadmin/generate/ciconfig/projects.py
+++ b/src/ciadmin/generate/ciconfig/projects.py
@@ -111,16 +111,25 @@ class Project:
                     "its `level` value".format(self.alias)
                 )
             if self.access:
                 raise ValueError(
                     "Non-hg repo {} cannot define an `access` "
                     "property".format(self.alias)
                 )
 
+        # Convert boolean features into a dict of the form {"enabled": <val>}
+        for name, val in self.features.items():
+            if isinstance(val, dict):
+                val.setdefault("enabled", True)
+            elif isinstance(val, bool):
+                self.features[name] = {"enabled": val}
+            else:
+                raise ValueError("Feature {} must be a dict or boolean".format(name))
+
     @staticmethod
     async def fetch_all():
         """Load project metadata from projects.yml in ci-configuration"""
         projects = await get_ciconfig_file("projects.yml")
         return [Project(alias, **info) for alias, info in projects.items()]
 
     @staticmethod
     async def get(alias):
@@ -131,24 +140,24 @@ class Project:
                 return project
         else:
             raise KeyError("Project {} is not defined".format(alias))
 
     # The `features` property is designed for ease of use in yaml, with true and false
     # values for each feature; the `feature()` and `enabled_features` attributes provide
     # easier access for Python uses.
 
-    def feature(self, feature):
+    def feature(self, feature, key="enabled"):
         "Return True if this feature is enabled"
-        return feature in self.features and self.features[feature]
+        return feature in self.features and self.features[feature][key]
 
     @property
     def enabled_features(self):
         "The list of enabled features"
-        return [f for f, enabled in self.features.items() if enabled]
+        return [f for f, val in self.features.items() if val["enabled"]]
 
     def get_level(self):
         "Get the level, or None if the access level does not define a level"
         if self.access and self.access.startswith("scm_level_"):
             return int(self.access[-1])
         elif self.access and self.access in SYMBOLIC_GROUP_LEVELS:
             return SYMBOLIC_GROUP_LEVELS[self.access]
         elif self._level:
--- a/src/ciadmin/generate/grants.py
+++ b/src/ciadmin/generate/grants.py
@@ -49,52 +49,91 @@ def add_scopes_for_projects(grant, grant
             continue
         if grantee.is_try is not None:
             if project.is_try != grantee.is_try:
                 continue
         if not match(grantee.trust_domain, project.trust_domain):
             continue
 
         jobs = grantee.job
+
+        # Force being explicit with pull-request policies. Otherwise, the `pull-request`
+        # job would be equivalent to `pull-request:trusted`, which may not be intended.
+        if "pull-request" in jobs:
+            raise RuntimeError(
+                "Invalid job 'pull-request'! Use 'pull-request:*' instead."
+            )
+
+        pr_policy = (project.feature("github-pull-request", key="policy") or "").strip()
+
         if "*" in jobs and project.repo_type == "git" and project.level != 1:
             # Github mixes pull-requests and other tasks under the same prefix
             # Since pull-requests should be level-1, we need to explicitly
             # split based on the job
             jobs = [job for job in jobs if job != "*"]
-            jobs += ["pull-request", "branch:*", "release", "cron:*", "action:*"]
+            jobs += ["pull-request:*", "branch:*", "release", "cron:*", "action:*"]
 
         # Only grant scopes to `cron:` or `action:` jobs if the corresponding features
         # are enabled. This allows having generic grants that don't generate unused
         # roles
         if not project.feature("taskgraph-cron") and not project.feature("gecko-cron"):
             jobs = [job for job in jobs if not job.startswith("cron:")]
         if not project.feature("taskgraph-actions") and not project.feature(
             "gecko-actions"
         ):
             jobs = [job for job in jobs if not job.startswith("action:")]
-        if project.repo_type != "git" or not grantee.include_pull_requests:
-            jobs = [job for job in jobs if job != "pull-request"]
+
+        # Only grant pull-request scopes where it makes sense.
+        if (
+            project.repo_type != "git"
+            or not pr_policy
+            or not grantee.include_pull_requests
+        ):
+            jobs = [job for job in jobs if not job.startswith("pull-request")]
+
+        if "pull-request:*" in jobs:
+            jobs.remove("pull-request:*")
+            jobs.extend(["pull-request:trusted", "pull-request:untrusted"])
+
+        # Remove any 'pull-request:trusted' jobs for projects using the 'public' policy.
+        # Similarly, remove any 'pull-request:untrusted' jobs for projects using the
+        # 'collaborators' policy. Only the 'public_restricted' policy supports both at
+        # the same time.
+        if pr_policy == "public":
+            jobs = [job for job in jobs if job != "pull-request:trusted"]
+        elif pr_policy.startswith("collaborators"):
+            jobs = [job for job in jobs if job != "pull-request:untrusted"]
+
+        def job_to_role_suffix(job):
+            # Normalize any `pull-request:` jobs to their appropriate role
+            # suffix.
+            if job == "pull-request:untrusted" and pr_policy == "public_restricted":
+                return "pull-request-untrusted"
+            elif job.startswith("pull-request"):
+                return "pull-request"
+            return job
 
         # ok, this project matches!
         for job in jobs:
-            roleId = "{}:{}".format(project.role_prefix, job)
+            suffix = job_to_role_suffix(job)
+            roleId = "{}:{}".format(project.role_prefix, suffix)
 
             # perform substitutions as grants.yml describes
             subs = {}
             subs["alias"] = project.alias
             if project.trust_domain:
                 subs["trust_domain"] = project.trust_domain
             if project.trust_project:
                 subs["trust_project"] = project.trust_project
             level = project.get_level()
             if level is not None:
                 subs["level"] = project.level
                 # In order to avoid granting pull-requests graphs
                 # access to the level-3 workers, we overwrite their value here
-                if job == "pull-request":
+                if job.startswith("pull-request"):
                     subs["level"] = 1
                 subs["priority"] = LEVEL_PRIORITIES[project.level]
             try:
                 subs["repo_path"] = project.repo_path
             except AttributeError:
                 pass  # not an known supported repo..
 
             for scope in grant.scopes:
--- a/tests/ciadmin/test_generate_ciconfig_projects.py
+++ b/tests/ciadmin/test_generate_ciconfig_projects.py
@@ -99,17 +99,20 @@ async def test_fetch_defaults(
             {
                 "access": "scm_level_2",
                 "cron": {
                     "email_when_trigger_failure": True,
                     "notify_emails": [],
                     "targets": ["a", "b"],
                 },
                 "default_branch": "default",
-                "features": {"hg-push": True, "gecko-cron": False},
+                "features": {
+                    "hg-push": {"enabled": True},
+                    "gecko-cron": {"enabled": False},
+                },
                 "is_try": True,
                 "parent_repo": "https://hg.mozilla.org/mozilla-unified",
                 "repo_type": "hg",
                 "repo": "https://hg.mozilla.org/projects/ash",
                 "trust_domain": "gecko",
                 "trust_project": None,
             },
             {
@@ -120,17 +123,20 @@ async def test_fetch_defaults(
                     "email_when_trigger_failure": True,
                     "notify_emails": [],
                     "targets": [
                         {"target": "a", "bindings": []},
                         {"target": "b", "bindings": []},
                     ],
                 },
                 "default_branch": "default",
-                "features": {"hg-push": True, "gecko-cron": False},
+                "features": {
+                    "hg-push": {"enabled": True},
+                    "gecko-cron": {"enabled": False},
+                },
                 "is_try": True,
                 "parent_repo": "https://hg.mozilla.org/mozilla-unified",
                 "repo": "https://hg.mozilla.org/projects/ash",
                 "repo_path": "projects/ash",
                 "repo_type": "hg",
                 "role_prefix": "repo:hg.mozilla.org/projects/ash",
                 "taskcluster_yml_project": None,
                 "trust_domain": "gecko",
@@ -141,17 +147,20 @@ async def test_fetch_defaults(
             "beetmoverscript",  # git project but not mobile
             {
                 "cron": {
                     "email_when_trigger_failure": True,
                     "notify_emails": [],
                     "targets": ["a", "b"],
                 },
                 "default_branch": "main",
-                "features": {"hg-push": True, "gecko-cron": False},
+                "features": {
+                    "hg-push": {"enabled": True},
+                    "gecko-cron": {"enabled": False},
+                },
                 "is_try": False,
                 "level": 3,
                 "parent_repo": "https://github.com/mozilla-releng/",
                 "repo_type": "git",
                 "repo": "https://github.com/mozilla-releng/beetmoverscript/",
                 "trust_domain": "beet",
                 "trust_project": None,
             },
@@ -163,17 +172,20 @@ async def test_fetch_defaults(
                     "email_when_trigger_failure": True,
                     "notify_emails": [],
                     "targets": [
                         {"target": "a", "bindings": []},
                         {"target": "b", "bindings": []},
                     ],
                 },
                 "default_branch": "main",
-                "features": {"hg-push": True, "gecko-cron": False},
+                "features": {
+                    "hg-push": {"enabled": True},
+                    "gecko-cron": {"enabled": False},
+                },
                 "is_try": False,
                 "parent_repo": "https://github.com/mozilla-releng/",
                 "repo": "https://github.com/mozilla-releng/beetmoverscript/",
                 "repo_path": "mozilla-releng/beetmoverscript",
                 "repo_type": "git",
                 "role_prefix": "repo:github.com/mozilla-releng/beetmoverscript",
                 "taskcluster_yml_project": None,
                 "trust_domain": "beet",
@@ -197,19 +209,25 @@ async def test_fetch_nodefaults(
 def test_project_feature():
     "Test the feature method"
     prj = Project(
         alias="prj",
         repo="https://hg.mozilla.org/prj",
         repo_type="hg",
         access="scm_level_3",
         trust_domain="gecko",
-        features={"taskcluster-pull": True, "gecko-cron": False},
+        features={
+            "taskcluster-pull": True,
+            "gecko-cron": False,
+            "some-data": {"foo": "bar"},
+        },
     )
     assert prj.feature("taskcluster-pull")
+    assert prj.feature("some-data")
+    assert prj.feature("some-data", key="foo") == "bar"
     assert not prj.feature("gecko-cron")
     assert not prj.feature("gecko-cron")
     assert not prj.feature("buildbot")
 
 
 def test_project_enabled_features():
     "Test enabled_features"
     prj = Project(
--- a/tests/ciadmin/test_generate_grants.py
+++ b/tests/ciadmin/test_generate_grants.py
@@ -1,14 +1,16 @@
 # -*- coding: utf-8 -*-
 
 # This Source Code Form is subject to the terms of the Mozilla Public License,
 # v. 2.0. If a copy of the MPL was not distributed with this file, You can
 # obtain one at http://mozilla.org/MPL/2.0/.
 
+from pprint import pprint
+
 import pytest
 from tcadmin.resources import Resources
 
 from ciadmin.generate import grants
 from ciadmin.generate.ciconfig.grants import Grant, GroupGrantee, ProjectGrantee
 from ciadmin.generate.ciconfig.projects import Project
 
 
@@ -229,16 +231,146 @@ class TestAddScopesForGroups:
         "{..} in scopes is an error for groups"
         grantee = GroupGrantee(groups=["group1", "group2"])
         with pytest.raises(KeyError):
             grants.add_scopes_for_groups(
                 Grant(scopes=["level:{level}"], grantees=[grantee]), grantee, add_scope
             )
 
 
+class TestAddScopesForGithubPullRequest:
+
+    projects = [
+        Project(
+            alias="hg",
+            repo="https://hg.mozilla.org/hg",
+            repo_type="hg",
+            access="scm_level_1",
+            trust_domain="foo",
+        ),
+        Project(
+            alias="no-prs",
+            repo="https://github.com/mozilla/no-prs",
+            repo_type="git",
+            level=3,
+            trust_domain="foo",
+        ),
+        Project(
+            alias="public",
+            repo="https://github.com/mozilla/public",
+            repo_type="git",
+            level=3,
+            trust_domain="foo",
+            features={
+                "github-pull-request": {
+                    "enabled": True,
+                    "policy": "public",
+                }
+            },
+        ),
+        Project(
+            alias="public-restricted",
+            repo="https://github.com/mozilla/public-restricted",
+            repo_type="git",
+            level=3,
+            trust_domain="foo",
+            features={
+                "github-pull-request": {
+                    "enabled": True,
+                    "policy": "public_restricted",
+                }
+            },
+        ),
+        Project(
+            alias="collaborators",
+            repo="https://github.com/mozilla/collaborators",
+            repo_type="git",
+            level=3,
+            trust_domain="foo",
+            features={
+                "github-pull-request": {
+                    "enabled": True,
+                    "policy": "collaborators",
+                }
+            },
+        ),
+    ]
+
+    def test_grant_to_pull_request_trusted(self, add_scope):
+        grantee = ProjectGrantee(job=["pull-request:trusted"])
+        grants.add_scopes_for_projects(
+            Grant(scopes=["sc"], grantees=[grantee]), grantee, add_scope, self.projects
+        )
+        # Dump expected for copy/paste.
+        pprint(add_scope.added)
+        assert add_scope.added == [
+            ("repo:github.com/mozilla/public-restricted:pull-request", "sc"),
+            ("repo:github.com/mozilla/collaborators:pull-request", "sc"),
+        ]
+
+    def test_grant_to_pull_request_untrusted(self, add_scope):
+        grantee = ProjectGrantee(job=["pull-request:untrusted"])
+        grants.add_scopes_for_projects(
+            Grant(scopes=["sc"], grantees=[grantee]), grantee, add_scope, self.projects
+        )
+        # Dump expected for copy/paste.
+        pprint(add_scope.added)
+        assert add_scope.added == [
+            ("repo:github.com/mozilla/public:pull-request", "sc"),
+            ("repo:github.com/mozilla/public-restricted:pull-request-untrusted", "sc"),
+        ]
+
+    def test_grant_to_pull_request_star(self, add_scope):
+        grantee = ProjectGrantee(job=["pull-request:*"])
+        grants.add_scopes_for_projects(
+            Grant(scopes=["sc"], grantees=[grantee]), grantee, add_scope, self.projects
+        )
+        # Dump expected for copy/paste.
+        pprint(add_scope.added)
+        assert add_scope.added == [
+            ("repo:github.com/mozilla/public:pull-request", "sc"),
+            ("repo:github.com/mozilla/public-restricted:pull-request", "sc"),
+            ("repo:github.com/mozilla/public-restricted:pull-request-untrusted", "sc"),
+            ("repo:github.com/mozilla/collaborators:pull-request", "sc"),
+        ]
+
+    def test_grant_to_star(self, add_scope):
+        grantee = ProjectGrantee(job=["*"])
+        grants.add_scopes_for_projects(
+            Grant(scopes=["sc"], grantees=[grantee]), grantee, add_scope, self.projects
+        )
+        pr_grants = [g for g in add_scope.added if "pull-request" in g[0]]
+
+        # Dump expected for copy/paste.
+        pprint(pr_grants)
+        assert pr_grants == [
+            ("repo:github.com/mozilla/public:pull-request", "sc"),
+            ("repo:github.com/mozilla/public-restricted:pull-request", "sc"),
+            ("repo:github.com/mozilla/public-restricted:pull-request-untrusted", "sc"),
+            ("repo:github.com/mozilla/collaborators:pull-request", "sc"),
+        ]
+
+    def test_include_pull_requests_false(self, add_scope):
+        grantee = ProjectGrantee(job=["pull-request:*"], include_pull_requests=False)
+        grants.add_scopes_for_projects(
+            Grant(scopes=["sc"], grantees=[grantee]), grantee, add_scope, self.projects
+        )
+        assert add_scope.added == []
+
+    def test_invalid_grantee(self):
+        grantee = ProjectGrantee(job=["pull-request"])
+        with pytest.raises(RuntimeError):
+            grants.add_scopes_for_projects(
+                Grant(scopes=["sc"], grantees=[grantee]),
+                grantee,
+                add_scope,
+                self.projects,
+            )
+
+
 @pytest.mark.asyncio
 async def test_update_resources(mock_ciconfig_file, set_environment):
     mock_ciconfig_file(
         "projects.yml",
         {
             "proj1": dict(
                 repo="https://hg.mozilla.org/foo/proj1",
                 repo_type="hg",