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