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