use gh_workflow::{ctx::Context, *};use indoc::indoc; use crate::tasks::workflows::{ extension_release::extension_workflow_secrets, extension_tests::{self}, runners, steps::{ self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, named, }, vars::{ JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch, },}; const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#; // This is used by various extensions repos in the zed-extensions org to bump extension versions.pub(crate) fn extension_bump() -> Workflow { let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned())); // TODO: Ideally, this would have a default of `false`, but this is currently not // supported in gh-workflows let force_bump = WorkflowInput::bool("force-bump", None); let (app_id, app_secret) = extension_workflow_secrets(); let (check_bump_needed, needs_bump, current_version) = check_bump_needed(); let needs_bump = needs_bump.as_job_output(&check_bump_needed); let current_version = current_version.as_job_output(&check_bump_needed); let dependencies = [&check_bump_needed]; let bump_version = bump_extension_version( &dependencies, ¤t_version, &bump_type, &needs_bump, &force_bump, &app_id, &app_secret, ); let create_label = create_version_label( &dependencies, &needs_bump, ¤t_version, &app_id, &app_secret, ); named::workflow() .add_event( Event::default().workflow_call( WorkflowCall::default() .add_input(bump_type.name, bump_type.call_input()) .add_input(force_bump.name, force_bump.call_input()) .secrets([ (app_id.name.to_owned(), app_id.secret_configuration()), ( app_secret.name.to_owned(), app_secret.secret_configuration(), ), ]), ), ) .concurrency(one_workflow_per_non_main_branch()) .add_env(("CARGO_TERM_COLOR", "always")) .add_env(("RUST_BACKTRACE", 1)) .add_env(("CARGO_INCREMENTAL", 0)) .add_env(( "ZED_EXTENSION_CLI_SHA", extension_tests::ZED_EXTENSION_CLI_SHA, )) .add_job(check_bump_needed.name, check_bump_needed.job) .add_job(bump_version.name, bump_version.job) .add_job(create_label.name, create_label.job)} fn check_bump_needed() -> (NamedJob, StepOutput, StepOutput) { let (compare_versions, version_changed, current_version) = compare_versions(); let job = Job::default() .with_repository_owner_guard() .outputs([ (version_changed.name.to_owned(), version_changed.to_string()), ( current_version.name.to_string(), current_version.to_string(), ), ]) .runs_on(runners::LINUX_SMALL) .timeout_minutes(1u32) .add_step(steps::checkout_repo().add_with(("fetch-depth", 0))) .add_step(compare_versions); (named::job(job), version_changed, current_version)} fn create_version_label( dependencies: &[&NamedJob], needs_bump: &JobOutput, current_version: &JobOutput, app_id: &WorkflowSecret, app_secret: &WorkflowSecret,) -> NamedJob { let (generate_token, generated_token) = generate_token(&app_id.to_string(), &app_secret.to_string(), None); let job = steps::dependant_job(dependencies) .cond(Expression::new(format!( "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'", needs_bump.expr(), ))) .runs_on(runners::LINUX_SMALL) .timeout_minutes(1u32) .add_step(generate_token) .add_step(steps::checkout_repo()) .add_step(create_version_tag(current_version, generated_token)); named::job(job)} fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step<Use> { named::uses("actions", "github-script", "v7").with( Input::default() .add( "script", format!( indoc! {r#" github.rest.git.createRef({{ owner: context.repo.owner, repo: context.repo.repo, ref: 'refs/tags/v{}', sha: context.sha }})"# }, current_version ), ) .add("github-token", generated_token.to_string()), )} /// Compares the current and previous commit and checks whether versions changed inbetween.fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) { let check_needs_bump = named::bash(format!( indoc! { r#" CURRENT_VERSION="$({})" PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}" if [[ -n "$PR_PARENT_SHA" ]]; then git checkout "$PR_PARENT_SHA" elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then git checkout "$BRANCH_PARENT_SHA" else git checkout "$(git log -1 --format=%H)"~1 fi PARENT_COMMIT_VERSION="$({})" [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \ echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \ echo "needs_bump=false" >> "$GITHUB_OUTPUT" echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT" "# }, VERSION_CHECK, VERSION_CHECK )) .id("compare-versions-check"); let needs_bump = StepOutput::new(&check_needs_bump, "needs_bump"); let current_version = StepOutput::new(&check_needs_bump, "current_version"); (check_needs_bump, needs_bump, current_version)} fn bump_extension_version( dependencies: &[&NamedJob], current_version: &JobOutput, bump_type: &WorkflowInput, needs_bump: &JobOutput, force_bump: &WorkflowInput, app_id: &WorkflowSecret, app_secret: &WorkflowSecret,) -> NamedJob { let (generate_token, generated_token) = generate_token(&app_id.to_string(), &app_secret.to_string(), None); let (bump_version, new_version) = bump_version(current_version, bump_type); let job = steps::dependant_job(dependencies) .cond(Expression::new(format!( "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({} == 'true' || {} == 'true')", force_bump.expr(), needs_bump.expr(), ))) .runs_on(runners::LINUX_SMALL) .timeout_minutes(1u32) .add_step(generate_token) .add_step(steps::checkout_repo()) .add_step(install_bump_2_version()) .add_step(bump_version) .add_step(create_pull_request(new_version, generated_token)); named::job(job)} pub(crate) fn generate_token( app_id_source: &str, app_secret_source: &str, repository_target: Option<RepositoryTarget>,) -> (Step<Use>, StepOutput) { let step = named::uses("actions", "create-github-app-token", "v2") .id("generate-token") .add_with( Input::default() .add("app-id", app_id_source) .add("private-key", app_secret_source) .when_some( repository_target, |input, RepositoryTarget { owner, repositories, permissions, }| { input .add("owner", owner) .add("repositories", repositories) .when_some(permissions, |input, permissions| { permissions .into_iter() .fold(input, |input, (permission, level)| { input.add( permission, serde_json::to_value(&level).unwrap_or_default(), ) }) }) }, ), ); let generated_token = StepOutput::new(&step, "token"); (step, generated_token)} fn install_bump_2_version() -> Step<Run> { named::run( runners::Platform::Linux, "pip install bump2version --break-system-packages", )} fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step<Run>, StepOutput) { let step = named::bash(format!( indoc! {r#" OLD_VERSION="{}" BUMP_FILES=("extension.toml") if [[ -f "Cargo.toml" ]]; then BUMP_FILES+=("Cargo.toml") fi bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files {} "${{BUMP_FILES[@]}}" if [[ -f "Cargo.toml" ]]; then cargo update --workspace fi NEW_VERSION="$({})" echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT" "# }, current_version, bump_type, VERSION_CHECK )) .id("bump-version"); let new_version = StepOutput::new(&step, "new_version"); (step, new_version)} fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step<Use> { let formatted_version = format!("v{}", new_version); named::uses("peter-evans", "create-pull-request", "v7").with( Input::default() .add("title", format!("Bump version to {}", new_version)) .add( "body", format!( "This PR bumps the version of this extension to {}", formatted_version ), ) .add( "commit-message", format!("Bump version to {}", formatted_version), ) .add("branch", "zed-zippy-autobump") .add( "committer", "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", ) .add("base", "main") .add("delete-branch", true) .add("token", generated_token.to_string()) .add("sign-commits", true) .add("assignees", Context::github().actor().to_string()), )} pub(crate) struct RepositoryTarget { owner: String, repositories: String, permissions: Option<Vec<(String, Level)>>,} impl RepositoryTarget { pub fn new<T: ToString>(owner: T, repositories: &[&str]) -> Self { Self { owner: owner.to_string(), repositories: repositories.join("\n"), permissions: None, } } pub fn permissions(self, permissions: impl Into<Vec<(String, Level)>>) -> Self { Self { permissions: Some(permissions.into()), ..self } }}
use gh_workflow::{ctx::Context, *};
use indoc::indoc;
use crate::tasks::workflows::{
extension_release::extension_workflow_secrets,
extension_tests::{self},
runners,
steps::{
self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, named,
},
vars::{
JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch,
},
};
const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#;
// This is used by various extensions repos in the zed-extensions org to bump extension versions.
pub(crate) fn extension_bump() -> Workflow {
let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned()));
// TODO: Ideally, this would have a default of `false`, but this is currently not
// supported in gh-workflows
let force_bump = WorkflowInput::bool("force-bump", None);
let (app_id, app_secret) = extension_workflow_secrets();
let (check_bump_needed, needs_bump, current_version) = check_bump_needed();
let needs_bump = needs_bump.as_job_output(&check_bump_needed);
let current_version = current_version.as_job_output(&check_bump_needed);
let dependencies = [&check_bump_needed];
let bump_version = bump_extension_version(
&dependencies,
¤t_version,
&bump_type,
&needs_bump,
&force_bump,
&app_id,
&app_secret,
);
let create_label = create_version_label(
&dependencies,
&needs_bump,
¤t_version,
&app_id,
&app_secret,
);
named::workflow()
.add_event(
Event::default().workflow_call(
WorkflowCall::default()
.add_input(bump_type.name, bump_type.call_input())
.add_input(force_bump.name, force_bump.call_input())
.secrets([
(app_id.name.to_owned(), app_id.secret_configuration()),
(
app_secret.name.to_owned(),
app_secret.secret_configuration(),
),
]),
),
)
.concurrency(one_workflow_per_non_main_branch())
.add_env(("CARGO_TERM_COLOR", "always"))
.add_env(("RUST_BACKTRACE", 1))
.add_env(("CARGO_INCREMENTAL", 0))
.add_env((
"ZED_EXTENSION_CLI_SHA",
extension_tests::ZED_EXTENSION_CLI_SHA,
))
.add_job(check_bump_needed.name, check_bump_needed.job)
.add_job(bump_version.name, bump_version.job)
.add_job(create_label.name, create_label.job)
}
fn check_bump_needed() -> (NamedJob, StepOutput, StepOutput) {
let (compare_versions, version_changed, current_version) = compare_versions();
let job = Job::default()
.with_repository_owner_guard()
.outputs([
(version_changed.name.to_owned(), version_changed.to_string()),
(
current_version.name.to_string(),
current_version.to_string(),
),
])
.runs_on(runners::LINUX_SMALL)
.timeout_minutes(1u32)
.add_step(steps::checkout_repo().add_with(("fetch-depth", 0)))
.add_step(compare_versions);
(named::job(job), version_changed, current_version)
}
fn create_version_label(
dependencies: &[&NamedJob],
needs_bump: &JobOutput,
current_version: &JobOutput,
app_id: &WorkflowSecret,
app_secret: &WorkflowSecret,
) -> NamedJob {
let (generate_token, generated_token) =
generate_token(&app_id.to_string(), &app_secret.to_string(), None);
let job = steps::dependant_job(dependencies)
.cond(Expression::new(format!(
"{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'",
needs_bump.expr(),
)))
.runs_on(runners::LINUX_SMALL)
.timeout_minutes(1u32)
.add_step(generate_token)
.add_step(steps::checkout_repo())
.add_step(create_version_tag(current_version, generated_token));
named::job(job)
}
fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step<Use> {
named::uses("actions", "github-script", "v7").with(
Input::default()
.add(
"script",
format!(
indoc! {r#"
github.rest.git.createRef({{
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/tags/v{}',
sha: context.sha
}})"#
},
current_version
),
)
.add("github-token", generated_token.to_string()),
)
}
/// Compares the current and previous commit and checks whether versions changed inbetween.
fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
let check_needs_bump = named::bash(format!(
indoc! {
r#"
CURRENT_VERSION="$({})"
PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}"
if [[ -n "$PR_PARENT_SHA" ]]; then
git checkout "$PR_PARENT_SHA"
elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then
git checkout "$BRANCH_PARENT_SHA"
else
git checkout "$(git log -1 --format=%H)"~1
fi
PARENT_COMMIT_VERSION="$({})"
[[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \
echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \
echo "needs_bump=false" >> "$GITHUB_OUTPUT"
echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT"
"#
},
VERSION_CHECK, VERSION_CHECK
))
.id("compare-versions-check");
let needs_bump = StepOutput::new(&check_needs_bump, "needs_bump");
let current_version = StepOutput::new(&check_needs_bump, "current_version");
(check_needs_bump, needs_bump, current_version)
}
fn bump_extension_version(
dependencies: &[&NamedJob],
current_version: &JobOutput,
bump_type: &WorkflowInput,
needs_bump: &JobOutput,
force_bump: &WorkflowInput,
app_id: &WorkflowSecret,
app_secret: &WorkflowSecret,
) -> NamedJob {
let (generate_token, generated_token) =
generate_token(&app_id.to_string(), &app_secret.to_string(), None);
let (bump_version, new_version) = bump_version(current_version, bump_type);
let job = steps::dependant_job(dependencies)
.cond(Expression::new(format!(
"{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({} == 'true' || {} == 'true')",
force_bump.expr(),
needs_bump.expr(),
)))
.runs_on(runners::LINUX_SMALL)
.timeout_minutes(1u32)
.add_step(generate_token)
.add_step(steps::checkout_repo())
.add_step(install_bump_2_version())
.add_step(bump_version)
.add_step(create_pull_request(new_version, generated_token));
named::job(job)
}
pub(crate) fn generate_token(
app_id_source: &str,
app_secret_source: &str,
repository_target: Option<RepositoryTarget>,
) -> (Step<Use>, StepOutput) {
let step = named::uses("actions", "create-github-app-token", "v2")
.id("generate-token")
.add_with(
Input::default()
.add("app-id", app_id_source)
.add("private-key", app_secret_source)
.when_some(
repository_target,
|input,
RepositoryTarget {
owner,
repositories,
permissions,
}| {
input
.add("owner", owner)
.add("repositories", repositories)
.when_some(permissions, |input, permissions| {
permissions
.into_iter()
.fold(input, |input, (permission, level)| {
input.add(
permission,
serde_json::to_value(&level).unwrap_or_default(),
)
})
})
},
),
);
let generated_token = StepOutput::new(&step, "token");
(step, generated_token)
}
fn install_bump_2_version() -> Step<Run> {
named::run(
runners::Platform::Linux,
"pip install bump2version --break-system-packages",
)
}
fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step<Run>, StepOutput) {
let step = named::bash(format!(
indoc! {r#"
OLD_VERSION="{}"
BUMP_FILES=("extension.toml")
if [[ -f "Cargo.toml" ]]; then
BUMP_FILES+=("Cargo.toml")
fi
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files {} "${{BUMP_FILES[@]}}"
if [[ -f "Cargo.toml" ]]; then
cargo update --workspace
fi
NEW_VERSION="$({})"
echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT"
"#
},
current_version, bump_type, VERSION_CHECK
))
.id("bump-version");
let new_version = StepOutput::new(&step, "new_version");
(step, new_version)
}
fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step<Use> {
let formatted_version = format!("v{}", new_version);
named::uses("peter-evans", "create-pull-request", "v7").with(
Input::default()
.add("title", format!("Bump version to {}", new_version))
.add(
"body",
format!(
"This PR bumps the version of this extension to {}",
formatted_version
),
)
.add(
"commit-message",
format!("Bump version to {}", formatted_version),
)
.add("branch", "zed-zippy-autobump")
.add(
"committer",
"zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
)
.add("base", "main")
.add("delete-branch", true)
.add("token", generated_token.to_string())
.add("sign-commits", true)
.add("assignees", Context::github().actor().to_string()),
)
}
pub(crate) struct RepositoryTarget {
owner: String,
repositories: String,
permissions: Option<Vec<(String, Level)>>,
}
impl RepositoryTarget {
pub fn new<T: ToString>(owner: T, repositories: &[&str]) -> Self {
Self {
owner: owner.to_string(),
repositories: repositories.join("\n"),
permissions: None,
}
}
pub fn permissions(self, permissions: impl Into<Vec<(String, Level)>>) -> Self {
Self {
permissions: Some(permissions.into()),
..self
}
}
}