extension_auto_bump.rs

  1use gh_workflow::{
  2    Event, Expression, Input, Job, Level, Permissions, Push, Strategy, UsesJob, Workflow,
  3};
  4use indoc::indoc;
  5use serde_json::json;
  6
  7use crate::tasks::workflows::{
  8    extensions::WithAppSecrets,
  9    run_tests::DETECT_CHANGED_EXTENSIONS_SCRIPT,
 10    runners,
 11    steps::{self, CommonJobConditions, NamedJob, named},
 12    vars::{StepOutput, one_workflow_per_non_main_branch},
 13};
 14
 15/// Generates a workflow that triggers on push to main, detects changed extensions
 16/// in the `extensions/` directory, and invokes the `extension_bump` reusable workflow
 17/// for each changed extension via a matrix strategy.
 18pub(crate) fn extension_auto_bump() -> Workflow {
 19    let detect = detect_changed_extensions();
 20    let bump = bump_extension_versions(&detect);
 21
 22    named::workflow()
 23        .add_event(
 24            Event::default().push(
 25                Push::default()
 26                    .add_branch("main")
 27                    .add_path("extensions/**")
 28                    .add_path("!extensions/slash-commands-example/**")
 29                    .add_path("!extensions/test-extension/**")
 30                    .add_path("!extensions/workflows/**")
 31                    .add_path("!extensions/*.md"),
 32            ),
 33        )
 34        .concurrency(one_workflow_per_non_main_branch())
 35        .add_job(detect.name, detect.job)
 36        .add_job(bump.name, bump.job)
 37}
 38
 39fn detect_changed_extensions() -> NamedJob {
 40    let preamble = indoc! {r#"
 41        COMPARE_REV="$(git rev-parse HEAD~1)"
 42        CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA")"
 43    "#};
 44
 45    let filter_new_and_removed = indoc! {r#"
 46        # Filter out newly added or entirely removed extensions
 47        FILTERED="[]"
 48        for ext in $(echo "$EXTENSIONS_JSON" | jq -r '.[]'); do
 49            if git show HEAD~1:"$ext/extension.toml" >/dev/null 2>&1 && \
 50               [ -f "$ext/extension.toml" ]; then
 51                FILTERED=$(echo "$FILTERED" | jq -c --arg e "$ext" '. + [$e]')
 52            fi
 53        done
 54        echo "changed_extensions=$FILTERED" >> "$GITHUB_OUTPUT"
 55    "#};
 56
 57    let script = format!(
 58        "{preamble}{detect}{filter}",
 59        preamble = preamble,
 60        detect = DETECT_CHANGED_EXTENSIONS_SCRIPT,
 61        filter = filter_new_and_removed,
 62    );
 63
 64    let step = named::bash(script).id("detect");
 65
 66    let output = StepOutput::new(&step, "changed_extensions");
 67
 68    let job = Job::default()
 69        .with_repository_owner_guard()
 70        .runs_on(runners::LINUX_SMALL)
 71        .timeout_minutes(5u32)
 72        .add_step(steps::checkout_repo().with_custom_fetch_depth(2))
 73        .add_step(step)
 74        .outputs([("changed_extensions".to_owned(), output.to_string())]);
 75
 76    named::job(job)
 77}
 78
 79fn bump_extension_versions(detect_job: &NamedJob) -> NamedJob<UsesJob> {
 80    let job = Job::default()
 81        .needs(vec![detect_job.name.clone()])
 82        .cond(Expression::new(format!(
 83            "needs.{}.outputs.changed_extensions != '[]'",
 84            detect_job.name
 85        )))
 86        .permissions(
 87            Permissions::default()
 88                .contents(Level::Write)
 89                .issues(Level::Write)
 90                .pull_requests(Level::Write)
 91                .actions(Level::Write),
 92        )
 93        .strategy(
 94            Strategy::default()
 95                .fail_fast(false)
 96                // TODO: Remove the limit. We currently need this to workaround the concurrency group issue
 97                // where different matrix jobs would be placed in the same concurrency group and thus cancelled.
 98                .max_parallel(1u32)
 99                .matrix(json!({
100                    "extension": format!(
101                        "${{{{ fromJson(needs.{}.outputs.changed_extensions) }}}}",
102                        detect_job.name
103                    )
104                })),
105        )
106        .uses_local(".github/workflows/extension_bump.yml")
107        .with(
108            Input::default()
109                .add("working-directory", "${{ matrix.extension }}")
110                .add("force-bump", false),
111        )
112        .with_app_secrets();
113
114    named::job(job)
115}