extension_bump.rs

  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        &current_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        &current_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}