Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:42:23.866Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

extension_workflow_rollout.rs

366 lines · 13.6 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.