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}