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}