ci: Set explicit permissions for Zed Zippy (#52895)

Finn Evers created

This hopefully fixes the permission issues we are seeing with the Zed
Zippy cherry picks

Release Notes:

- N/A

Change summary

.github/workflows/autofix_pr.yml                                |  2 
.github/workflows/cherry_pick.yml                               |  2 
tooling/xtask/src/tasks/workflows/autofix_pr.rs                 | 10 
tooling/xtask/src/tasks/workflows/bump_patch_version.rs         |  2 
tooling/xtask/src/tasks/workflows/cherry_pick.rs                | 10 
tooling/xtask/src/tasks/workflows/extension_bump.rs             |  3 
tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs | 27 
tooling/xtask/src/tasks/workflows/publish_extension_cli.rs      |  3 
tooling/xtask/src/tasks/workflows/release.rs                    |  2 
tooling/xtask/src/tasks/workflows/steps.rs                      | 73 +-
10 files changed, 88 insertions(+), 46 deletions(-)

Detailed changes

.github/workflows/autofix_pr.yml 🔗

@@ -97,6 +97,8 @@ jobs:
       with:
         app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
         private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+        permission-contents: write
+        permission-workflows: write
     - name: steps::checkout_repo
       uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
       with:

.github/workflows/cherry_pick.yml 🔗

@@ -35,6 +35,8 @@ jobs:
       with:
         app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
         private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+        permission-contents: write
+        permission-workflows: write
     - name: cherry_pick::run_cherry_pick::cherry_pick
       run: ./script/cherry-pick "$BRANCH" "$COMMIT" "$CHANNEL"
       env:

tooling/xtask/src/tasks/workflows/autofix_pr.rs 🔗

@@ -2,7 +2,7 @@ use gh_workflow::*;
 
 use crate::tasks::workflows::{
     runners,
-    steps::{self, FluentBuilder, NamedJob, named},
+    steps::{self, FluentBuilder, NamedJob, RepositoryTarget, TokenPermissions, named},
     vars::{self, StepOutput, WorkflowInput},
 };
 
@@ -161,7 +161,13 @@ fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob
         .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = steps::authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy()
+        .for_repository(RepositoryTarget::current())
+        .with_permissions([
+            (TokenPermissions::Contents, Level::Write),
+            (TokenPermissions::Workflows, Level::Write),
+        ])
+        .into();
 
     named::job(
         Job::default()

tooling/xtask/src/tasks/workflows/bump_patch_version.rs 🔗

@@ -63,7 +63,7 @@ fn run_bump_patch_version(branch: &WorkflowInput) -> steps::NamedJob {
         .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = steps::authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy().into();
 
     named::job(
         Job::default()

tooling/xtask/src/tasks/workflows/cherry_pick.rs 🔗

@@ -2,7 +2,7 @@ use gh_workflow::*;
 
 use crate::tasks::workflows::{
     runners,
-    steps::{self, NamedJob, named},
+    steps::{self, NamedJob, RepositoryTarget, TokenPermissions, named},
     vars::{StepOutput, WorkflowInput},
 };
 
@@ -44,7 +44,13 @@ fn run_cherry_pick(
             .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = steps::authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy()
+        .for_repository(RepositoryTarget::current())
+        .with_permissions([
+            (TokenPermissions::Contents, Level::Write),
+            (TokenPermissions::Workflows, Level::Write),
+        ])
+        .into();
 
     named::job(
         Job::default()

tooling/xtask/src/tasks/workflows/extension_bump.rs 🔗

@@ -359,7 +359,8 @@ fn trigger_release(
     let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]);
     let (generate_token, generated_token) =
         generate_token(&app_id.to_string(), &app_secret.to_string())
-            .for_repository(extension_registry);
+            .for_repository(extension_registry)
+            .into();
     let (get_extension_id, extension_id) = get_extension_id();
     let (release_action, pull_request_number) = release_action(extension_id, tag, &generated_token);
 

tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs 🔗

@@ -6,6 +6,7 @@ use indoc::indoc;
 use serde_json::json;
 
 use crate::tasks::workflows::steps::CheckoutStep;
+use crate::tasks::workflows::steps::TokenPermissions;
 use crate::tasks::workflows::steps::cache_rust_dependencies_namespace;
 use crate::tasks::workflows::vars::JobOutput;
 use crate::tasks::workflows::{
@@ -309,13 +310,17 @@ fn rollout_workflows_to_extension(
     }
 
     let (authenticate, token) =
-        generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY).for_repository(
-            RepositoryTarget::new("zed-extensions", &["${{ matrix.repo }}"]).permissions([
-                ("permission-pull-requests".to_owned(), Level::Write),
-                ("permission-contents".to_owned(), Level::Write),
-                ("permission-workflows".to_owned(), Level::Write),
-            ]),
-        );
+        generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
+            .for_repository(RepositoryTarget::new(
+                "zed-extensions",
+                &["${{ matrix.repo }}"],
+            ))
+            .with_permissions([
+                (TokenPermissions::PullRequests, Level::Write),
+                (TokenPermissions::Contents, Level::Write),
+                (TokenPermissions::Workflows, Level::Write),
+            ])
+            .into();
 
     let (calculate_short_sha, short_sha) = get_short_sha();
 
@@ -372,10 +377,10 @@ fn create_rollout_tag(rollout_job: &NamedJob, filter_repos_input: &WorkflowInput
     }
 
     let (authenticate, token) =
-        generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY).for_repository(
-            RepositoryTarget::current()
-                .permissions([("permission-contents".to_owned(), Level::Write)]),
-        );
+        generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
+            .for_repository(RepositoryTarget::current())
+            .with_permissions([(TokenPermissions::Contents, Level::Write)])
+            .into();
 
     let job = Job::default()
         .needs([rollout_job.name.clone()])

tooling/xtask/src/tasks/workflows/publish_extension_cli.rs 🔗

@@ -119,7 +119,8 @@ fn update_sha_in_extensions(publish_job: &NamedJob) -> NamedJob {
     let extensions_repo = RepositoryTarget::new("zed-industries", &["extensions"]);
     let (generate_token, generated_token) =
         generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
-            .for_repository(extensions_repo);
+            .for_repository(extensions_repo)
+            .into();
 
     fn checkout_extensions_repo(token: &StepOutput) -> Step<Use> {
         named::uses(

tooling/xtask/src/tasks/workflows/release.rs 🔗

@@ -179,7 +179,7 @@ fn validate_release_assets(deps: &[&NamedJob]) -> NamedJob {
 }
 
 fn auto_release_preview(deps: &[&NamedJob]) -> NamedJob {
-    let (authenticate, token) = steps::authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy().into();
 
     named::job(
         dependant_job(deps)

tooling/xtask/src/tasks/workflows/steps.rs 🔗

@@ -513,20 +513,50 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
         .add_env(("REF_NAME", ref_name.to_string()))
 }
 
+/// Non-exhaustive list of the permissions to be set for a GitHub app token.
+///
+/// See https://github.com/actions/create-github-app-token?tab=readme-ov-file#permission-permission-name
+/// and beyond for a full list of available permissions.
+#[allow(unused)]
+pub(crate) enum TokenPermissions {
+    Contents,
+    Issues,
+    PullRequests,
+    Workflows,
+}
+
+impl TokenPermissions {
+    pub fn environment_name(&self) -> &'static str {
+        match self {
+            TokenPermissions::Contents => "permission-contents",
+            TokenPermissions::Issues => "permission-issues",
+            TokenPermissions::PullRequests => "permission-pull-requests",
+            TokenPermissions::Workflows => "permission-workflows",
+        }
+    }
+}
+
 pub(crate) struct GenerateAppToken<'a> {
     job_name: String,
     app_id: &'a str,
     app_secret: &'a str,
     repository_target: Option<RepositoryTarget>,
+    permissions: Option<Vec<(TokenPermissions, Level)>>,
 }
 
 impl<'a> GenerateAppToken<'a> {
-    pub fn for_repository(self, repository_target: RepositoryTarget) -> (Step<Use>, StepOutput) {
+    pub fn for_repository(self, repository_target: RepositoryTarget) -> Self {
         Self {
             repository_target: Some(repository_target),
             ..self
         }
-        .into()
+    }
+
+    pub fn with_permissions(self, permissions: impl Into<Vec<(TokenPermissions, Level)>>) -> Self {
+        Self {
+            permissions: Some(permissions.into()),
+            ..self
+        }
     }
 }
 
@@ -549,26 +579,24 @@ impl<'a> From<GenerateAppToken<'a>> for (Step<Use>, StepOutput) {
                          RepositoryTarget {
                              owner,
                              repositories,
-                             permissions,
                          }| {
                             input
                                 .when_some(owner, |input, owner| input.add("owner", owner))
                                 .when_some(repositories, |input, repositories| {
                                     input.add("repositories", repositories)
                                 })
-                                .when_some(permissions, |input, permissions| {
-                                    permissions.into_iter().fold(
-                                        input,
-                                        |input, (permission, level)| {
-                                            input.add(
-                                                permission,
-                                                serde_json::to_value(&level).unwrap_or_default(),
-                                            )
-                                        },
-                                    )
-                                })
                         },
-                    ),
+                    )
+                    .when_some(token.permissions, |input, permissions| {
+                        permissions
+                            .into_iter()
+                            .fold(input, |input, (permission, level)| {
+                                input.add(
+                                    permission.environment_name(),
+                                    serde_json::to_value(&level).unwrap_or_default(),
+                                )
+                            })
+                    }),
             );
 
         let generated_token = StepOutput::new(&step, "token");
@@ -579,7 +607,6 @@ impl<'a> From<GenerateAppToken<'a>> for (Step<Use>, StepOutput) {
 pub(crate) struct RepositoryTarget {
     owner: Option<String>,
     repositories: Option<String>,
-    permissions: Option<Vec<(String, Level)>>,
 }
 
 impl RepositoryTarget {
@@ -587,7 +614,6 @@ impl RepositoryTarget {
         Self {
             owner: Some(owner.to_string()),
             repositories: Some(repositories.join("\n")),
-            permissions: None,
         }
     }
 
@@ -595,14 +621,6 @@ impl RepositoryTarget {
         Self {
             owner: None,
             repositories: None,
-            permissions: None,
-        }
-    }
-
-    pub fn permissions(self, permissions: impl Into<Vec<(String, Level)>>) -> Self {
-        Self {
-            permissions: Some(permissions.into()),
-            ..self
         }
     }
 }
@@ -614,8 +632,8 @@ pub(crate) fn generate_token<'a>(
     generate_token_with_job_name(app_id_source, app_secret_source)
 }
 
-pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
-    generate_token_with_job_name(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY).into()
+pub fn authenticate_as_zippy() -> GenerateAppToken<'static> {
+    generate_token_with_job_name(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
 }
 
 fn generate_token_with_job_name<'a>(
@@ -627,5 +645,6 @@ fn generate_token_with_job_name<'a>(
         app_id: app_id_source,
         app_secret: app_secret_source,
         repository_target: None,
+        permissions: None,
     }
 }