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