Skip to repository content366 lines · 13.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:40:29.154Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
extension_workflow_rollout.rs
1use gh_workflow::{
2 Event, Expression, Job, Level, Run, Step, Strategy, Use, Workflow, WorkflowDispatch,
3};
4use indoc::formatdoc;
5use indoc::indoc;
6use serde_json::json;
7
8use crate::tasks::workflows::steps::GitRef;
9use crate::tasks::workflows::steps::RefSha;
10use crate::tasks::workflows::steps::{
11 CheckoutStep, CommonPermissionSets, DownloadArtifactStep, IfNoFilesFound, ResultEncoding,
12 TokenPermissions, UploadArtifactStep, cache_rust_dependencies_namespace,
13};
14use crate::tasks::workflows::vars::JobOutput;
15use crate::tasks::workflows::{
16 runners,
17 steps::{
18 self, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob, RepositoryTarget, generate_token, named,
19 },
20 vars::{self, StepOutput, WorkflowInput},
21};
22
23const ROLLOUT_TAG_NAME: &str = "extension-workflows";
24const WORKFLOW_ARTIFACT_NAME: &str = "extension-workflow-files";
25
26pub(crate) fn extension_workflow_rollout() -> Workflow {
27 let filter_repos_input = WorkflowInput::string("filter-repos", Some(String::new()))
28 .description(
29 "Comma-separated list of repository names to rollout to. Leave empty for all repos.",
30 );
31 let extra_context_input = WorkflowInput::string("change-description", Some(String::new()))
32 .description("Description for the changes to be expected with this rollout");
33
34 let (fetch_repos, removed_ci, removed_shared) = fetch_extension_repos(&filter_repos_input);
35 let rollout_workflows = rollout_workflows_to_extension(
36 &fetch_repos,
37 removed_ci,
38 removed_shared,
39 &extra_context_input,
40 &filter_repos_input,
41 );
42 let create_tag = create_rollout_tag(&rollout_workflows, &filter_repos_input);
43
44 named::workflow()
45 .with_minimal_permissions()
46 .on(Event::default().workflow_dispatch(
47 WorkflowDispatch::default()
48 .add_input(filter_repos_input.name, filter_repos_input.input())
49 .add_input(extra_context_input.name, extra_context_input.input()),
50 ))
51 .add_env(("CARGO_TERM_COLOR", "always"))
52 .add_job(fetch_repos.name, fetch_repos.job)
53 .add_job(rollout_workflows.name, rollout_workflows.job)
54 .add_job(create_tag.name, create_tag.job)
55}
56
57fn fetch_extension_repos(filter_repos_input: &WorkflowInput) -> (NamedJob, JobOutput, JobOutput) {
58 fn get_repositories(filter_repos_input: &WorkflowInput) -> (Step<Use>, StepOutput) {
59 let step: Step<Use> = steps::github_script(formatdoc! {r#"
60 const repos = await github.paginate(github.rest.repos.listForOrg, {{
61 org: 'zed-extensions',
62 type: 'public',
63 per_page: 100,
64 }});
65
66 let filteredRepos = repos
67 .filter(repo => !repo.archived)
68 .map(repo => repo.name);
69
70 const filterInput = process.env.FILTER_REPOS.trim();
71 if (filterInput.length > 0) {{
72 const allowedNames = filterInput.split(',').map(s => s.trim()).filter(s => s.length > 0);
73 filteredRepos = filteredRepos.filter(name => allowedNames.includes(name));
74 console.log(`Filter applied. Matched ${{filteredRepos.length}} repos from ${{allowedNames.length}} requested.`);
75 }}
76
77 console.log(`Found ${{filteredRepos.length}} extension repos`);
78 return filteredRepos;
79 "#})
80 .result_encoding(ResultEncoding::Json)
81 .custom_name("get_repositories")
82 .id("list-repos")
83 .env("FILTER_REPOS", filter_repos_input.to_string())
84 .into();
85
86 let filtered_repos = StepOutput::new(&step, "result");
87
88 (step, filtered_repos)
89 }
90
91 fn checkout_zed_repo() -> CheckoutStep {
92 steps::checkout_repo()
93 .with_full_history()
94 .with_custom_name("checkout_zed_repo")
95 }
96
97 fn get_previous_tag_commit() -> (Step<Run>, StepOutput) {
98 let step = named::bash(formatdoc! {r#"
99 PREV_COMMIT=$(git rev-parse "{ROLLOUT_TAG_NAME}^{{commit}}" 2>/dev/null || echo "")
100 if [ -z "$PREV_COMMIT" ]; then
101 echo "::error::No previous rollout tag '{ROLLOUT_TAG_NAME}' found. Cannot determine file changes."
102 exit 1
103 fi
104 echo "Found previous rollout at commit: $PREV_COMMIT"
105 echo "prev_commit=$PREV_COMMIT" >> "$GITHUB_OUTPUT"
106 "#})
107 .id("prev-tag");
108
109 let step_output = StepOutput::new(&step, "prev_commit");
110
111 (step, step_output)
112 }
113
114 fn get_removed_files(prev_commit: &StepOutput) -> (Step<Run>, StepOutput, StepOutput) {
115 let step = named::bash(indoc! {r#"
116 for workflow_type in "ci" "shared"; do
117 if [ "$workflow_type" = "ci" ]; then
118 WORKFLOW_DIR="extensions/workflows"
119 else
120 WORKFLOW_DIR="extensions/workflows/shared"
121 fi
122
123 REMOVED=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \
124 awk '/^D/ { print $2 } /^R/ { print $2 }' | \
125 xargs -I{} basename {} 2>/dev/null | \
126 tr '\n' ' ' || echo "")
127 REMOVED=$(echo "$REMOVED" | xargs)
128
129 echo "Removed files for $workflow_type: $REMOVED"
130 echo "removed_${workflow_type}=$REMOVED" >> "$GITHUB_OUTPUT"
131 done
132 "#})
133 .id("calc-changes")
134 .add_env(("PREV_COMMIT", prev_commit.to_string()));
135
136 // These are created in the for-loop above and thus do exist
137 let removed_ci = StepOutput::new_unchecked(&step, "removed_ci");
138 let removed_shared = StepOutput::new_unchecked(&step, "removed_shared");
139
140 (step, removed_ci, removed_shared)
141 }
142
143 fn generate_workflow_files() -> Step<Run> {
144 named::bash(indoc! {r#"
145 cargo xtask workflows "$COMMIT_SHA"
146 "#})
147 .add_env(("COMMIT_SHA", "${{ github.sha }}"))
148 }
149
150 fn upload_workflow_files() -> UploadArtifactStep {
151 steps::upload_artifact(WORKFLOW_ARTIFACT_NAME, "extensions/workflows/**/*.yml")
152 .if_no_files_found(IfNoFilesFound::Error)
153 }
154
155 let (get_org_repositories, list_repos_output) = get_repositories(filter_repos_input);
156 let (get_prev_tag, prev_commit) = get_previous_tag_commit();
157 let (calc_changes, removed_ci, removed_shared) = get_removed_files(&prev_commit);
158
159 let job = Job::default()
160 .cond(Expression::new(format!(
161 "{DEFAULT_REPOSITORY_OWNER_GUARD} && (github.ref == 'refs/tags/{ROLLOUT_TAG_NAME}' || github.ref == 'refs/heads/main')")))
162 .runs_on(runners::LINUX_SMALL)
163 .timeout_minutes(10u32)
164 .outputs([
165 ("repos".to_owned(), list_repos_output.to_string()),
166 ("prev_commit".to_owned(), prev_commit.to_string()),
167 ("removed_ci".to_owned(), removed_ci.to_string()),
168 ("removed_shared".to_owned(), removed_shared.to_string()),
169 ])
170 .add_step(checkout_zed_repo())
171 .add_step(get_prev_tag)
172 .add_step(calc_changes)
173 .add_step(get_org_repositories)
174 .add_step(cache_rust_dependencies_namespace())
175 .add_step(generate_workflow_files())
176 .add_step(upload_workflow_files());
177
178 let job = named::job(job);
179 let (removed_ci, removed_shared) = (
180 removed_ci.as_job_output(&job),
181 removed_shared.as_job_output(&job),
182 );
183
184 (job, removed_ci, removed_shared)
185}
186
187fn rollout_workflows_to_extension(
188 fetch_repos_job: &NamedJob,
189 removed_ci: JobOutput,
190 removed_shared: JobOutput,
191 extra_context_input: &WorkflowInput,
192 filter_repos_input: &WorkflowInput,
193) -> NamedJob {
194 fn checkout_extension_repo(token: &StepOutput) -> CheckoutStep {
195 steps::checkout_repo()
196 .with_custom_name("checkout_extension_repo")
197 .with_token(token)
198 .with_repository("zed-extensions/${{ matrix.repo }}")
199 .with_path("extension")
200 }
201
202 fn download_workflow_files() -> DownloadArtifactStep {
203 steps::download_artifact()
204 .artifact_name(WORKFLOW_ARTIFACT_NAME)
205 .path("workflow-files")
206 }
207
208 fn sync_workflow_files(removed_ci: JobOutput, removed_shared: JobOutput) -> Step<Run> {
209 named::bash(indoc! {r#"
210 mkdir -p extension/.github/workflows
211
212 if [ "$MATRIX_REPO" = "workflows" ]; then
213 REMOVED_FILES="$REMOVED_CI"
214 else
215 REMOVED_FILES="$REMOVED_SHARED"
216 fi
217
218 cd extension/.github/workflows
219
220 if [ -n "$REMOVED_FILES" ]; then
221 for file in $REMOVED_FILES; do
222 if [ -f "$file" ]; then
223 rm -f "$file"
224 fi
225 done
226 fi
227
228 cd - > /dev/null
229
230 if [ "$MATRIX_REPO" = "workflows" ]; then
231 cp workflow-files/*.yml extension/.github/workflows/
232 else
233 cp workflow-files/shared/*.yml extension/.github/workflows/
234 fi
235 "#})
236 .add_env(("REMOVED_CI", removed_ci))
237 .add_env(("REMOVED_SHARED", removed_shared))
238 .add_env(("MATRIX_REPO", "${{ matrix.repo }}"))
239 }
240
241 fn get_short_sha() -> (Step<Run>, StepOutput) {
242 let step = named::bash(indoc! {r#"
243 echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT"
244 "#})
245 .id("short-sha");
246
247 let step_output = StepOutput::new(&step, "sha_short");
248
249 (step, step_output)
250 }
251
252 fn create_pull_request(
253 token: &StepOutput,
254 short_sha: &StepOutput,
255 context_input: &WorkflowInput,
256 filter_repos_input: &WorkflowInput,
257 ) -> Step<Use> {
258 let title = format!("Update CI workflows to `{short_sha}`");
259
260 let body = formatdoc! {r#"
261 This PR updates the CI workflow files from the main Zed repository
262 based on the commit zed-industries/zed@${{{{ github.sha }}}}
263
264 {context_input}
265 "#,
266 };
267
268 let pr_step: Step<Use> = steps::CreatePrStep::new(title, "update-workflows", token)
269 .with_body(body)
270 .with_path("extension")
271 // Save my inbox from exploding on rollout
272 .with_assignee(format!(
273 "${{{{ {repos_expr} != '' && github.actor || '' }}}}",
274 repos_expr = filter_repos_input.expr()
275 ))
276 .into();
277 pr_step.id("create-pr")
278 }
279
280 fn enable_auto_merge(token: &StepOutput) -> Step<gh_workflow::Run> {
281 named::bash(indoc! {r#"
282 if [ -n "$PR_NUMBER" ]; then
283 gh pr merge "$PR_NUMBER" --auto --squash
284 fi
285 "#})
286 .working_directory("extension")
287 .add_env(("GH_TOKEN", token.to_string()))
288 .add_env((
289 "PR_NUMBER",
290 "${{ steps.create-pr.outputs.pull-request-number }}",
291 ))
292 }
293
294 let (authenticate, token) =
295 generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
296 .for_repository(RepositoryTarget::new(
297 "zed-extensions",
298 &["${{ matrix.repo }}"],
299 ))
300 .with_permissions([
301 (TokenPermissions::PullRequests, Level::Write),
302 (TokenPermissions::Contents, Level::Write),
303 (TokenPermissions::Workflows, Level::Write),
304 ])
305 .into();
306
307 let (calculate_short_sha, short_sha) = get_short_sha();
308
309 let job = Job::default()
310 .needs([fetch_repos_job.name.clone()])
311 .cond(Expression::new(format!(
312 "needs.{}.outputs.repos != '[]'",
313 fetch_repos_job.name
314 )))
315 .runs_on(runners::LINUX_SMALL)
316 .timeout_minutes(10u32)
317 .strategy(
318 Strategy::default()
319 .fail_fast(false)
320 .max_parallel(10u32)
321 .matrix(json!({
322 "repo": format!("${{{{ fromJson(needs.{}.outputs.repos) }}}}", fetch_repos_job.name)
323 })),
324 )
325 .add_step(authenticate)
326 .add_step(checkout_extension_repo(&token))
327 .add_step(download_workflow_files())
328 .add_step(sync_workflow_files(removed_ci, removed_shared))
329 .add_step(calculate_short_sha)
330 .add_step(create_pull_request(&token, &short_sha, extra_context_input, filter_repos_input))
331 .add_step(enable_auto_merge(&token));
332
333 named::job(job)
334}
335
336fn create_rollout_tag(rollout_job: &NamedJob, filter_repos_input: &WorkflowInput) -> NamedJob {
337 fn checkout_zed_repo(token: &StepOutput) -> CheckoutStep {
338 steps::checkout_repo().with_full_history().with_token(token)
339 }
340
341 let (authenticate, token) =
342 generate_token(vars::ZED_ZIPPY_APP_ID, vars::ZED_ZIPPY_APP_PRIVATE_KEY)
343 .for_repository(RepositoryTarget::current())
344 .with_permissions([(TokenPermissions::Contents, Level::Write)])
345 .into();
346
347 let job = Job::default()
348 .needs([rollout_job.name.clone()])
349 .cond(Expression::new(format!(
350 "{filter_repos} == ''",
351 filter_repos = filter_repos_input.expr(),
352 )))
353 .runs_on(runners::LINUX_SMALL)
354 .timeout_minutes(1u32)
355 .add_step(authenticate)
356 .add_step(checkout_zed_repo(&token))
357 .add_step(steps::update_ref(
358 GitRef::Tag(ROLLOUT_TAG_NAME.to_owned()),
359 RefSha::Context,
360 &token,
361 true,
362 ));
363
364 named::job(job)
365}
366