extension_workflow_rollout.rs

  1use gh_workflow::{
  2    Event, Expression, Job, Level, Run, Step, Strategy, Use, Workflow, WorkflowDispatch,
  3};
  4use indoc::indoc;
  5use serde_json::json;
  6
  7use crate::tasks::workflows::{
  8    extension_bump::{RepositoryTarget, generate_token},
  9    runners,
 10    steps::{self, NamedJob, named},
 11    vars::{self, StepOutput},
 12};
 13
 14pub(crate) fn extension_workflow_rollout() -> Workflow {
 15    let fetch_repos = fetch_extension_repos();
 16    let rollout_workflows = rollout_workflows_to_extension(&fetch_repos);
 17
 18    named::workflow()
 19        .on(Event::default().workflow_dispatch(WorkflowDispatch::default()))
 20        .add_env(("CARGO_TERM_COLOR", "always"))
 21        .add_job(fetch_repos.name, fetch_repos.job)
 22        .add_job(rollout_workflows.name, rollout_workflows.job)
 23}
 24
 25fn fetch_extension_repos() -> NamedJob {
 26    fn get_repositories() -> (Step<Use>, StepOutput) {
 27        let step = named::uses("actions", "github-script", "v7")
 28            .id("list-repos")
 29            .add_with((
 30                "script",
 31                indoc! {r#"
 32                    const repos = await github.paginate(github.rest.repos.listForOrg, {
 33                        org: 'zed-extensions',
 34                        type: 'public',
 35                        per_page: 100,
 36                    });
 37
 38                    const filteredRepos = repos
 39                        .filter(repo => !repo.archived)
 40                        .map(repo => repo.name);
 41
 42                    console.log(`Found ${filteredRepos.length} extension repos`);
 43                    return filteredRepos;
 44                "#},
 45            ))
 46            .add_with(("result-encoding", "json"));
 47
 48        let filtered_repos = StepOutput::new(&step, "result");
 49
 50        (step, filtered_repos)
 51    }
 52
 53    let (get_org_repositories, list_repos_output) = get_repositories();
 54
 55    let job = Job::default()
 56        .runs_on(runners::LINUX_SMALL)
 57        .timeout_minutes(5u32)
 58        .outputs([("repos".to_owned(), list_repos_output.to_string())])
 59        .add_step(get_org_repositories);
 60
 61    named::job(job)
 62}
 63
 64fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob {
 65    fn checkout_zed_repo() -> Step<Use> {
 66        steps::checkout_repo()
 67            .name("checkout_zed_repo")
 68            .add_with(("path", "zed"))
 69    }
 70
 71    fn checkout_extension_repo(token: &StepOutput) -> Step<Use> {
 72        steps::checkout_repo_with_token(token)
 73            .add_with(("repository", "zed-extensions/${{ matrix.repo }}"))
 74            .add_with(("path", "extension"))
 75    }
 76
 77    fn copy_workflow_files() -> Step<Run> {
 78        named::bash(indoc! {r#"
 79            mkdir -p extension/.github/workflows
 80            if [ "${{ matrix.repo }}" = "workflows" ]; then
 81                cp zed/extensions/workflows/*.yml extension/.github/workflows/
 82            else
 83                cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/
 84            fi
 85        "#})
 86    }
 87
 88    fn get_short_sha() -> (Step<Run>, StepOutput) {
 89        let step = named::bash(indoc! {r#"
 90            echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
 91        "#})
 92        .id("short-sha")
 93        .working_directory("zed");
 94
 95        let step_output = StepOutput::new(&step, "sha_short");
 96
 97        (step, step_output)
 98    }
 99
100    fn create_pull_request(token: &StepOutput, short_sha: StepOutput) -> Step<Use> {
101        let title = format!("Update CI workflows to `zed@{}`", short_sha);
102
103        named::uses("peter-evans", "create-pull-request", "v7")
104            .add_with(("path", "extension"))
105            .add_with(("title", title.clone()))
106            .add_with((
107                "body",
108                indoc! {r#"
109                    This PR updates the CI workflow files from the main Zed repository
110                    based on the commit zed-industries/zed@${{ github.sha }}
111                "#},
112            ))
113            .add_with(("commit-message", title))
114            .add_with(("branch", "update-workflows"))
115            .add_with((
116                "committer",
117                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
118            ))
119            .add_with((
120                "author",
121                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
122            ))
123            .add_with(("base", "main"))
124            .add_with(("delete-branch", true))
125            .add_with(("token", token.to_string()))
126            .add_with(("sign-commits", true))
127            .id("create-pr")
128    }
129
130    fn enable_auto_merge(token: &StepOutput) -> Step<gh_workflow::Run> {
131        named::bash(indoc! {r#"
132            PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
133            if [ -n "$PR_NUMBER" ]; then
134                cd extension
135                gh pr merge "$PR_NUMBER" --auto --squash
136            fi
137        "#})
138        .add_env(("GH_TOKEN", token.to_string()))
139    }
140
141    let (authenticate, token) = generate_token(
142        vars::ZED_ZIPPY_APP_ID,
143        vars::ZED_ZIPPY_APP_PRIVATE_KEY,
144        Some(
145            RepositoryTarget::new("zed-extensions", &["${{ matrix.repo }}"]).permissions([
146                ("permission-pull-requests".to_owned(), Level::Write),
147                ("permission-contents".to_owned(), Level::Write),
148                ("permission-workflows".to_owned(), Level::Write),
149            ]),
150        ),
151    );
152    let (calculate_short_sha, short_sha) = get_short_sha();
153
154    let job = Job::default()
155        .needs([fetch_repos_job.name.clone()])
156        .cond(Expression::new(format!(
157            "needs.{}.outputs.repos != '[]'",
158            fetch_repos_job.name
159        )))
160        .runs_on(runners::LINUX_SMALL)
161        .timeout_minutes(10u32)
162        .strategy(
163            Strategy::default()
164                .fail_fast(false)
165                .max_parallel(5u32)
166                .matrix(json!({
167                    "repo": format!("${{{{ fromJson(needs.{}.outputs.repos) }}}}", fetch_repos_job.name)
168                })),
169        )
170        .add_step(authenticate)
171        .add_step(checkout_zed_repo())
172        .add_step(checkout_extension_repo(&token))
173        .add_step(copy_workflow_files())
174        .add_step(calculate_short_sha)
175        .add_step(create_pull_request(&token, short_sha))
176        .add_step(enable_auto_merge(&token));
177
178    named::job(job)
179}