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