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