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