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