1use gh_workflow::{ctx::Context, *};
2use indoc::{formatdoc, indoc};
3
4use crate::tasks::workflows::{
5 extension_tests::{self},
6 runners,
7 steps::{
8 self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob,
9 checkout_repo, dependant_job, named,
10 },
11 vars::{
12 JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch,
13 },
14};
15
16const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#;
17
18// This is used by various extensions repos in the zed-extensions org to bump extension versions.
19pub(crate) fn extension_bump() -> Workflow {
20 let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned()));
21 // TODO: Ideally, this would have a default of `false`, but this is currently not
22 // supported in gh-workflows
23 let force_bump = WorkflowInput::bool("force-bump", None);
24
25 let (app_id, app_secret) = extension_workflow_secrets();
26 let (check_version_changed, version_changed, current_version) = check_version_changed();
27
28 let version_changed = version_changed.as_job_output(&check_version_changed);
29 let current_version = current_version.as_job_output(&check_version_changed);
30
31 let dependencies = [&check_version_changed];
32 let bump_version = bump_extension_version(
33 &dependencies,
34 ¤t_version,
35 &bump_type,
36 &version_changed,
37 &force_bump,
38 &app_id,
39 &app_secret,
40 );
41 let create_label = create_version_label(
42 &dependencies,
43 &version_changed,
44 ¤t_version,
45 &app_id,
46 &app_secret,
47 );
48 let trigger_release = trigger_release(
49 &[&check_version_changed, &create_label],
50 current_version,
51 &app_id,
52 &app_secret,
53 );
54
55 named::workflow()
56 .add_event(
57 Event::default().workflow_call(
58 WorkflowCall::default()
59 .add_input(bump_type.name, bump_type.call_input())
60 .add_input(force_bump.name, force_bump.call_input())
61 .secrets([
62 (app_id.name.to_owned(), app_id.secret_configuration()),
63 (
64 app_secret.name.to_owned(),
65 app_secret.secret_configuration(),
66 ),
67 ]),
68 ),
69 )
70 .concurrency(one_workflow_per_non_main_branch())
71 .add_env(("CARGO_TERM_COLOR", "always"))
72 .add_env(("RUST_BACKTRACE", 1))
73 .add_env(("CARGO_INCREMENTAL", 0))
74 .add_env((
75 "ZED_EXTENSION_CLI_SHA",
76 extension_tests::ZED_EXTENSION_CLI_SHA,
77 ))
78 .add_job(check_version_changed.name, check_version_changed.job)
79 .add_job(bump_version.name, bump_version.job)
80 .add_job(create_label.name, create_label.job)
81 .add_job(trigger_release.name, trigger_release.job)
82}
83
84fn check_version_changed() -> (NamedJob, StepOutput, StepOutput) {
85 let (compare_versions, version_changed, current_version) = compare_versions();
86
87 let job = Job::default()
88 .with_repository_owner_guard()
89 .outputs([
90 (version_changed.name.to_owned(), version_changed.to_string()),
91 (
92 current_version.name.to_string(),
93 current_version.to_string(),
94 ),
95 ])
96 .runs_on(runners::LINUX_SMALL)
97 .timeout_minutes(1u32)
98 .add_step(steps::checkout_repo().add_with(("fetch-depth", 0)))
99 .add_step(compare_versions);
100
101 (named::job(job), version_changed, current_version)
102}
103
104fn create_version_label(
105 dependencies: &[&NamedJob],
106 version_changed_output: &JobOutput,
107 current_version: &JobOutput,
108 app_id: &WorkflowSecret,
109 app_secret: &WorkflowSecret,
110) -> NamedJob {
111 let (generate_token, generated_token) =
112 generate_token(&app_id.to_string(), &app_secret.to_string(), None);
113 let job = steps::dependant_job(dependencies)
114 .cond(Expression::new(format!(
115 "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && \
116 github.ref == 'refs/heads/main' && {version_changed} == 'true'",
117 version_changed = version_changed_output.expr(),
118 )))
119 .runs_on(runners::LINUX_SMALL)
120 .timeout_minutes(1u32)
121 .add_step(generate_token)
122 .add_step(steps::checkout_repo())
123 .add_step(create_version_tag(current_version, generated_token));
124
125 named::job(job)
126}
127
128fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step<Use> {
129 named::uses("actions", "github-script", "v7").with(
130 Input::default()
131 .add(
132 "script",
133 formatdoc! {r#"
134 github.rest.git.createRef({{
135 owner: context.repo.owner,
136 repo: context.repo.repo,
137 ref: 'refs/tags/v{current_version}',
138 sha: context.sha
139 }})"#
140 },
141 )
142 .add("github-token", generated_token.to_string()),
143 )
144}
145
146/// Compares the current and previous commit and checks whether versions changed inbetween.
147pub(crate) fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
148 let check_needs_bump = named::bash(formatdoc! {
149 r#"
150 CURRENT_VERSION="$({VERSION_CHECK})"
151 PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}"
152
153 if [[ -n "$PR_PARENT_SHA" ]]; then
154 git checkout "$PR_PARENT_SHA"
155 elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then
156 git checkout "$BRANCH_PARENT_SHA"
157 else
158 git checkout "$(git log -1 --format=%H)"~1
159 fi
160
161 PARENT_COMMIT_VERSION="$({VERSION_CHECK})"
162
163 [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \
164 echo "version_changed=false" >> "$GITHUB_OUTPUT" || \
165 echo "version_changed=true" >> "$GITHUB_OUTPUT"
166
167 echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT"
168 "#
169 })
170 .id("compare-versions-check");
171
172 let version_changed = StepOutput::new(&check_needs_bump, "version_changed");
173 let current_version = StepOutput::new(&check_needs_bump, "current_version");
174
175 (check_needs_bump, version_changed, current_version)
176}
177
178fn bump_extension_version(
179 dependencies: &[&NamedJob],
180 current_version: &JobOutput,
181 bump_type: &WorkflowInput,
182 version_changed_output: &JobOutput,
183 force_bump_output: &WorkflowInput,
184 app_id: &WorkflowSecret,
185 app_secret: &WorkflowSecret,
186) -> NamedJob {
187 let (generate_token, generated_token) =
188 generate_token(&app_id.to_string(), &app_secret.to_string(), None);
189 let (bump_version, new_version) = bump_version(current_version, bump_type);
190
191 let job = steps::dependant_job(dependencies)
192 .cond(Expression::new(format!(
193 "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({force_bump} == 'true' || {version_changed} == 'false')",
194 force_bump = force_bump_output.expr(),
195 version_changed = version_changed_output.expr(),
196 )))
197 .runs_on(runners::LINUX_SMALL)
198 .timeout_minutes(3u32)
199 .add_step(generate_token)
200 .add_step(steps::checkout_repo())
201 .add_step(install_bump_2_version())
202 .add_step(bump_version)
203 .add_step(create_pull_request(new_version, generated_token));
204
205 named::job(job)
206}
207
208pub(crate) fn generate_token(
209 app_id_source: &str,
210 app_secret_source: &str,
211 repository_target: Option<RepositoryTarget>,
212) -> (Step<Use>, StepOutput) {
213 let step = named::uses("actions", "create-github-app-token", "v2")
214 .id("generate-token")
215 .add_with(
216 Input::default()
217 .add("app-id", app_id_source)
218 .add("private-key", app_secret_source)
219 .when_some(
220 repository_target,
221 |input,
222 RepositoryTarget {
223 owner,
224 repositories,
225 permissions,
226 }| {
227 input
228 .when_some(owner, |input, owner| input.add("owner", owner))
229 .when_some(repositories, |input, repositories| {
230 input.add("repositories", repositories)
231 })
232 .when_some(permissions, |input, permissions| {
233 permissions
234 .into_iter()
235 .fold(input, |input, (permission, level)| {
236 input.add(
237 permission,
238 serde_json::to_value(&level).unwrap_or_default(),
239 )
240 })
241 })
242 },
243 ),
244 );
245
246 let generated_token = StepOutput::new(&step, "token");
247
248 (step, generated_token)
249}
250
251fn install_bump_2_version() -> Step<Run> {
252 named::run(
253 runners::Platform::Linux,
254 "pip install bump2version --break-system-packages",
255 )
256}
257
258fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step<Run>, StepOutput) {
259 let step = named::bash(formatdoc! {r#"
260 OLD_VERSION="{current_version}"
261
262 BUMP_FILES=("extension.toml")
263 if [[ -f "Cargo.toml" ]]; then
264 BUMP_FILES+=("Cargo.toml")
265 fi
266
267 bump2version \
268 --search "version = \"{{current_version}}"\" \
269 --replace "version = \"{{new_version}}"\" \
270 --current-version "$OLD_VERSION" \
271 --no-configured-files {bump_type} "${{BUMP_FILES[@]}}"
272
273 if [[ -f "Cargo.toml" ]]; then
274 cargo update --workspace
275 fi
276
277 NEW_VERSION="$({VERSION_CHECK})"
278
279 echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT"
280 "#
281 })
282 .id("bump-version");
283
284 let new_version = StepOutput::new(&step, "new_version");
285 (step, new_version)
286}
287
288fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step<Use> {
289 let formatted_version = format!("v{new_version}");
290
291 named::uses("peter-evans", "create-pull-request", "v7").with(
292 Input::default()
293 .add("title", format!("Bump version to {new_version}"))
294 .add(
295 "body",
296 format!("This PR bumps the version of this extension to {formatted_version}",),
297 )
298 .add(
299 "commit-message",
300 format!("Bump version to {formatted_version}"),
301 )
302 .add("branch", "zed-zippy-autobump")
303 .add(
304 "committer",
305 "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
306 )
307 .add("base", "main")
308 .add("delete-branch", true)
309 .add("token", generated_token.to_string())
310 .add("sign-commits", true)
311 .add("assignees", Context::github().actor().to_string()),
312 )
313}
314
315fn trigger_release(
316 dependencies: &[&NamedJob],
317 version: JobOutput,
318 app_id: &WorkflowSecret,
319 app_secret: &WorkflowSecret,
320) -> NamedJob {
321 let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]);
322 let (generate_token, generated_token) = generate_token(
323 &app_id.to_string(),
324 &app_secret.to_string(),
325 Some(extension_registry),
326 );
327 let (get_extension_id, extension_id) = get_extension_id();
328
329 let job = dependant_job(dependencies)
330 .with_repository_owner_guard()
331 .runs_on(runners::LINUX_SMALL)
332 .add_step(generate_token)
333 .add_step(checkout_repo())
334 .add_step(get_extension_id)
335 .add_step(release_action(extension_id, version, generated_token));
336
337 named::job(job)
338}
339
340fn get_extension_id() -> (Step<Run>, StepOutput) {
341 let step = named::bash(indoc! {
342 r#"
343 EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)"
344
345 echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
346 "#})
347 .id("get-extension-id");
348
349 let extension_id = StepOutput::new(&step, "extension_id");
350
351 (step, extension_id)
352}
353
354fn release_action(
355 extension_id: StepOutput,
356 version: JobOutput,
357 generated_token: StepOutput,
358) -> Step<Use> {
359 named::uses("huacnlee", "zed-extension-action", "v2")
360 .add_with(("extension-name", extension_id.to_string()))
361 .add_with(("push-to", "zed-industries/extensions"))
362 .add_with(("tag", format!("v{version}")))
363 .add_env(("COMMITTER_TOKEN", generated_token.to_string()))
364}
365
366fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) {
367 let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR");
368 let app_secret =
369 WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID");
370
371 (app_id, app_secret)
372}
373
374pub(crate) struct RepositoryTarget {
375 owner: Option<String>,
376 repositories: Option<String>,
377 permissions: Option<Vec<(String, Level)>>,
378}
379
380impl RepositoryTarget {
381 pub fn new<T: ToString>(owner: T, repositories: &[&str]) -> Self {
382 Self {
383 owner: Some(owner.to_string()),
384 repositories: Some(repositories.join("\n")),
385 permissions: None,
386 }
387 }
388
389 pub fn current() -> Self {
390 Self {
391 owner: None,
392 repositories: None,
393 permissions: None,
394 }
395 }
396
397 pub fn permissions(self, permissions: impl Into<Vec<(String, Level)>>) -> Self {
398 Self {
399 permissions: Some(permissions.into()),
400 ..self
401 }
402 }
403}