From 921786b374a1476ef32b284fd439c9a5b212ea03 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 23 Jul 2026 06:44:52 +0000 Subject: [PATCH 1/7] fix(ceres): rebase CL-relative change paths onto Buck monorepo root When a CL under /project/ resolves repo_path to /, keep cl_path and prefix change paths so Orion can discover impacted targets. --- .../api_service/mono/buck/upload.rs | 1 + .../build_trigger/changes_calculator.rs | 146 +++++++++++++++++- ceres/src/application/build_trigger/model.rs | 13 +- .../src/application/build_trigger/service.rs | 11 ++ .../build_trigger/web_edit_handler.rs | 2 + ceres/src/application/code_edit/on_edit.rs | 2 + ceres/src/application/code_edit/on_push.rs | 2 + moon/apps/web/components/Home/HomeSidebar.tsx | 57 +++++-- 8 files changed, 215 insertions(+), 19 deletions(-) diff --git a/ceres/src/application/api_service/mono/buck/upload.rs b/ceres/src/application/api_service/mono/buck/upload.rs index 80580e33d..17e802b0b 100644 --- a/ceres/src/application/api_service/mono/buck/upload.rs +++ b/ceres/src/application/api_service/mono/buck/upload.rs @@ -44,6 +44,7 @@ impl MonoApiService { response.commit_id.clone(), response.cl_link.clone(), Some(response.cl_id), + Some(response.repo_path.clone()), Some(username.to_string()), ); context.ref_name = Some("main".to_string()); diff --git a/ceres/src/application/build_trigger/changes_calculator.rs b/ceres/src/application/build_trigger/changes_calculator.rs index d8aa97a28..19f4d4c6e 100644 --- a/ceres/src/application/build_trigger/changes_calculator.rs +++ b/ceres/src/application/build_trigger/changes_calculator.rs @@ -120,20 +120,63 @@ fn push_change_if_valid( } } +/// Rebase a CL-relative file path onto the Buck repo root when the CL directory +/// is a strict subdirectory of `repo_path`. +/// +/// Example: repo=`/`, cl=`/project/dagrs-derive`, file=`src/lib.rs` +/// → `project/dagrs-derive/src/lib.rs` +fn rebase_cl_relative_path(repo_path: &str, cl_path: &str, file: &Path) -> PathBuf { + let repo = repo_path.trim_matches('/'); + let cl = cl_path.trim_matches('/'); + let file_str = file + .to_string_lossy() + .replace('\\', "/") + .trim_start_matches('/') + .to_string(); + + let cl_rel = if repo.is_empty() { + if cl.is_empty() { + return PathBuf::from(&file_str); + } + cl.to_string() + } else if cl == repo { + return PathBuf::from(&file_str); + } else if let Some(stripped) = cl.strip_prefix(&format!("{repo}/")) { + stripped.to_string() + } else { + return PathBuf::from(&file_str); + }; + + if cl_rel.is_empty() { + return PathBuf::from(&file_str); + } + + if file_str == cl_rel || file_str.starts_with(&format!("{cl_rel}/")) { + return PathBuf::from(&file_str); + } + + PathBuf::from(format!("{cl_rel}/{file_str}")) +} + fn build_changes_for_repo( repo_path: &str, + cl_path: Option<&str>, cl_diff_files: Vec, ) -> Result>, MegaError> { let repo_prefix = repo_path.trim_matches('/'); let repo_prefix_with_slash = (!repo_prefix.is_empty()).then(|| format!("{repo_prefix}/")); let to_project_relative = |path: &Path| { + let rebased = match cl_path { + Some(cl) => rebase_cl_relative_path(repo_path, cl, path), + None => path.to_path_buf(), + }; let normalized = normalize_change_path_for_repo_with_prefix( repo_prefix, repo_prefix_with_slash.as_deref(), - path, + &rebased, ); if let Some(normalized_path) = &normalized { - monitor_possible_repo_prefix_mismatch(repo_path, path, normalized_path.as_str()); + monitor_possible_repo_prefix_mismatch(repo_path, &rebased, normalized_path.as_str()); } normalized }; @@ -201,7 +244,8 @@ impl ChangesCalculator

{ let new_files = self.get_commit_blobs(&context.commit_hash).await?; let diff_files = self.cl_files_list(old_files, new_files).await?; - let changes = build_changes_for_repo(&context.repo_path, diff_files)?; + let changes = + build_changes_for_repo(&context.repo_path, context.cl_path.as_deref(), diff_files)?; Ok(changes) } @@ -280,6 +324,7 @@ mod tests { fn test_build_changes_normalizes_repo_local_paths_and_keeps_external_paths() { let changes = build_changes_for_repo( "/project/buck2_test", + Some("/project/buck2_test"), vec![ ClDiffFile::Modified( PathBuf::from("src/main.rs"), @@ -318,6 +363,7 @@ mod tests { fn test_build_changes_filters_unsafe_paths() { let changes = build_changes_for_repo( "/project/buck2_test", + Some("/project/buck2_test"), vec![ ClDiffFile::Modified( PathBuf::from("src/main.rs"), @@ -342,6 +388,7 @@ mod tests { fn test_build_changes_keeps_added_paths_for_create_entry_variants() { let changes = build_changes_for_repo( "/project/buck2_test", + Some("/project/buck2_test"), vec![ ClDiffFile::New( PathBuf::from("/project/buck2_test/src/new_file_abs.rs"), @@ -373,6 +420,7 @@ mod tests { fn test_build_changes_keeps_added_gitkeep_for_new_directory() { let changes = build_changes_for_repo( "/project/buck2_test", + Some("/project/buck2_test"), vec![ClDiffFile::New( PathBuf::from("project/buck2_test/src/new_dir/.gitkeep"), ObjectHash::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), @@ -388,6 +436,98 @@ mod tests { ); } + #[test] + fn test_rebase_cl_relative_path_prefixes_monorepo_subdir_cl() { + assert_eq!( + rebase_cl_relative_path("/", "/project/dagrs-derive", Path::new("src/lib.rs")), + PathBuf::from("project/dagrs-derive/src/lib.rs") + ); + assert_eq!( + rebase_cl_relative_path( + "/", + "/project/dagrs-derive", + Path::new("project/dagrs-derive/BUCK") + ), + PathBuf::from("project/dagrs-derive/BUCK") + ); + } + + #[test] + fn test_rebase_cl_relative_path_noop_when_cl_equals_repo() { + assert_eq!( + rebase_cl_relative_path( + "/project/buck2_test", + "/project/buck2_test", + Path::new("src/main.rs") + ), + PathBuf::from("src/main.rs") + ); + } + + #[test] + fn test_rebase_cl_relative_path_nested_cl_under_registered_repo() { + assert_eq!( + rebase_cl_relative_path( + "/project/buck2_test", + "/project/buck2_test/src", + Path::new("main.rs") + ), + PathBuf::from("src/main.rs") + ); + } + + #[test] + fn test_build_changes_rebases_dagrs_derive_style_paths_onto_monorepo_root() { + let changes = build_changes_for_repo( + "/", + Some("/project/dagrs-derive"), + vec![ + ClDiffFile::Modified( + PathBuf::from("src/lib.rs"), + ObjectHash::from_str("1111111111111111111111111111111111111111").unwrap(), + ObjectHash::from_str("2222222222222222222222222222222222222222").unwrap(), + ), + ClDiffFile::New( + PathBuf::from("BUCK"), + ObjectHash::from_str("3333333333333333333333333333333333333333").unwrap(), + ), + ClDiffFile::New( + PathBuf::from("project/dagrs-derive/Cargo.toml"), + ObjectHash::from_str("4444444444444444444444444444444444444444").unwrap(), + ), + ], + ) + .unwrap(); + + assert_eq!( + changes, + vec![ + Status::Modified(ProjectRelativePath::new("project/dagrs-derive/src/lib.rs")), + Status::Added(ProjectRelativePath::new("project/dagrs-derive/BUCK")), + Status::Added(ProjectRelativePath::new("project/dagrs-derive/Cargo.toml")), + ] + ); + } + + #[test] + fn test_build_changes_rebases_nested_cl_under_buck2_test() { + let changes = build_changes_for_repo( + "/project/buck2_test", + Some("/project/buck2_test/src"), + vec![ClDiffFile::Modified( + PathBuf::from("main.rs"), + ObjectHash::from_str("1111111111111111111111111111111111111111").unwrap(), + ObjectHash::from_str("2222222222222222222222222222222222222222").unwrap(), + )], + ) + .unwrap(); + + assert_eq!( + changes, + vec![Status::Modified(ProjectRelativePath::new("src/main.rs"))] + ); + } + #[test] fn test_detect_single_level_prefixed_candidate_returns_unique_match() { let tempdir = TempDir::new().expect("create tempdir"); diff --git a/ceres/src/application/build_trigger/model.rs b/ceres/src/application/build_trigger/model.rs index c25de80d5..d70ddceeb 100644 --- a/ceres/src/application/build_trigger/model.rs +++ b/ceres/src/application/build_trigger/model.rs @@ -318,6 +318,9 @@ pub struct TriggerContext { pub trigger_source: TriggerSource, pub triggered_by: Option, pub repo_path: String, + /// Original CL directory path (may be a subdirectory of [`Self::repo_path`]). + /// Used to rebase CL-relative change paths onto the Buck repo root. + pub cl_path: Option, pub from_hash: String, pub commit_hash: String, pub cl_link: Option, @@ -335,6 +338,7 @@ impl TriggerContext { commit_hash: String, cl_link: String, cl_id: Option, + cl_path: Option, triggered_by: Option, ) -> Self { Self { @@ -346,6 +350,7 @@ impl TriggerContext { }, triggered_by, repo_path, + cl_path, from_hash, commit_hash, cl_link: Some(cl_link), @@ -368,6 +373,7 @@ impl TriggerContext { trigger_source: TriggerSource::User, triggered_by: Some(triggered_by), repo_path, + cl_path: None, from_hash: commit_hash.clone(), commit_hash, cl_link: None, @@ -385,6 +391,7 @@ impl TriggerContext { commit_hash: String, cl_link: Option, cl_id: Option, + cl_path: Option, triggered_by: Option, original_trigger_id: i64, ) -> Self { @@ -393,6 +400,7 @@ impl TriggerContext { trigger_source: TriggerSource::User, triggered_by, repo_path, + cl_path, from_hash, commit_hash, cl_link, @@ -412,6 +420,7 @@ impl TriggerContext { commit_hash: String, cl_link: String, cl_id: Option, + cl_path: Option, triggered_by: Option, ) -> Self { Self { @@ -423,6 +432,7 @@ impl TriggerContext { }, triggered_by, repo_path, + cl_path, from_hash, commit_hash, cl_link: Some(cl_link), @@ -441,7 +451,8 @@ impl From for TriggerContext { trigger_type: BuildTriggerType::WebEdit, trigger_source: TriggerSource::User, triggered_by: Some(cl.username), - repo_path: cl.path, + repo_path: cl.path.clone(), + cl_path: Some(cl.path), from_hash: cl.from_hash, commit_hash: cl.to_hash, cl_link: Some(cl.link), diff --git a/ceres/src/application/build_trigger/service.rs b/ceres/src/application/build_trigger/service.rs index 35855e88f..310fb657f 100644 --- a/ceres/src/application/build_trigger/service.rs +++ b/ceres/src/application/build_trigger/service.rs @@ -91,12 +91,14 @@ impl BuildTriggerService { return Ok(None); } + let cl_path = Some(event.repo_path.clone()); let context = TriggerContext::from_git_push( event.repo_path, event.from_hash, event.commit_hash, event.cl_link, event.cl_id, + cl_path, event.triggered_by, ); @@ -211,12 +213,20 @@ impl BuildTriggerService { .parse_payload() .map_err(|e| MegaError::Other(format!("Failed to parse payload: {}", e)))?; + // Historical payloads only store Buck repo_path; reload CL path so retry + // can rebase CL-relative change paths onto the monorepo root. + let cl_path = match self.storage.cl_storage().get_cl(payload.cl_link()).await? { + Some(cl) => Some(cl.path), + None => None, + }; + let context = TriggerContext::from_retry( payload.repo_path().to_string(), payload.from_hash().to_string(), payload.commit_hash().to_string(), Some(payload.cl_link().to_string()), payload.cl_id(), + cl_path, Some(triggered_by), original_trigger_id, ); @@ -322,6 +332,7 @@ mod tests { .expect("resolve cl context"); assert_eq!(context.repo_path, "/project/buck2_test"); + assert_eq!(context.cl_path.as_deref(), Some("/project/buck2_test/src")); assert_eq!(context.cl_link.as_deref(), Some("HVKM7CXI")); assert_eq!(context.trigger_type, BuildTriggerType::WebEdit); } diff --git a/ceres/src/application/build_trigger/web_edit_handler.rs b/ceres/src/application/build_trigger/web_edit_handler.rs index d8e2cfcde..007781de3 100644 --- a/ceres/src/application/build_trigger/web_edit_handler.rs +++ b/ceres/src/application/build_trigger/web_edit_handler.rs @@ -90,6 +90,7 @@ mod tests { trigger_source: TriggerSource::User, triggered_by: Some("jackie".to_string()), repo_path: "/project/buck2_test".to_string(), + cl_path: Some("/project/buck2_test".to_string()), from_hash: "1".repeat(40), commit_hash: "2".repeat(40), cl_link: Some("HVKM7CXI".to_string()), @@ -110,6 +111,7 @@ mod tests { trigger_source: TriggerSource::User, triggered_by: Some("jackie".to_string()), repo_path: "/project/buck2_test".to_string(), + cl_path: Some("/project/buck2_test".to_string()), from_hash: "1".repeat(40), commit_hash: "abcdef1234567890abcdef1234567890abcdef12".to_string(), cl_link: None, diff --git a/ceres/src/application/code_edit/on_edit.rs b/ceres/src/application/code_edit/on_edit.rs index 089624f92..6f4680809 100644 --- a/ceres/src/application/code_edit/on_edit.rs +++ b/ceres/src/application/code_edit/on_edit.rs @@ -78,6 +78,7 @@ impl model::TriggerContextBuilder for OneditTrigerBuilder { cl.to_hash.clone(), cl.link.clone(), Some(cl.id), + Some(cl.path.clone()), Some(username.to_string()), )) } @@ -112,6 +113,7 @@ impl model::TriggerContextBuilder for OneditTrigerBuilder { cl_model.to_hash.clone(), cl_model.link.clone(), Some(cl_model.id), + Some(cl_model.path.clone()), Some(username), ); BuildTriggerService::build_by_context(storage, git_cache, build_dispatch, context).await diff --git a/ceres/src/application/code_edit/on_push.rs b/ceres/src/application/code_edit/on_push.rs index 5d6ef18e6..b6aa873c1 100644 --- a/ceres/src/application/code_edit/on_push.rs +++ b/ceres/src/application/code_edit/on_push.rs @@ -53,6 +53,7 @@ impl model::TriggerContextBuilder for OnpushTrigerBuilder { cl.to_hash.clone(), cl.link.clone(), Some(cl.id), + Some(cl.path.clone()), Some(username.to_string()), )) } @@ -89,6 +90,7 @@ impl model::TriggerContextBuilder for OnpushTrigerBuilder { cl_model.to_hash.clone(), cl_model.link.clone(), Some(cl_model.id), + Some(cl_model.path.clone()), Some(username), ); BuildTriggerService::build_by_context(storage, git_cache, build_dispatch, context).await diff --git a/moon/apps/web/components/Home/HomeSidebar.tsx b/moon/apps/web/components/Home/HomeSidebar.tsx index 06f72b49d..12bc0108e 100644 --- a/moon/apps/web/components/Home/HomeSidebar.tsx +++ b/moon/apps/web/components/Home/HomeSidebar.tsx @@ -349,6 +349,23 @@ function SearchInput({ query, setQuery }: { query: string; setQuery: (query: str ) } +function needsGithubRelogin(user: { github_login?: string | null; integration?: boolean; system?: boolean }) { + if (user.integration || user.system) return false + return !user.github_login +} + +function GithubReloginHint() { + return ( + + + + 需要重新登陆 + + + + ) +} + export function CurrentUserStatus() { const [statusDialogOpen, setStatusDialogOpen] = useState(false) const { data: currentUser } = useGetCurrentUser() @@ -356,6 +373,8 @@ export function CurrentUserStatus() { if (!currentUser || !member) return null + const showRelogin = needsGithubRelogin(currentUser) + return ( <> @@ -372,14 +391,17 @@ export function CurrentUserStatus() {

{currentUser.display_name} - - {!member.status && ( - - Update status - + {showRelogin ? ( + + ) : ( + !member.status && ( + + Update status + + ) )}
@@ -429,13 +451,18 @@ function InnerMember({ member }: { member: OrganizationMember }) {
{member.user.display_name} - {!member.status && memberIsViewer && ( - - Update status - + {needsGithubRelogin(member.user) ? ( + + ) : ( + !member.status && + memberIsViewer && ( + + Update status + + ) )}
From 15aecb866ad29bf6c27805963134970e311165de Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 23 Jul 2026 15:01:30 +0800 Subject: [PATCH 2/7] update action --- .github/workflows/mono-engine-deploy.yml | 49 ++++++++++++++----- .github/workflows/orion-server-deploy.yml | 48 +++++++++++++----- .github/workflows/web-deploy.yml | 42 ++++++++++++---- .github/workflows/web-sync-server-deploy.yml | 17 ++++++- moon/apps/web/components/Home/HomeSidebar.tsx | 2 +- 5 files changed, 121 insertions(+), 37 deletions(-) diff --git a/.github/workflows/mono-engine-deploy.yml b/.github/workflows/mono-engine-deploy.yml index 923ab5c9a..1fbf76bfc 100644 --- a/.github/workflows/mono-engine-deploy.yml +++ b/.github/workflows/mono-engine-deploy.yml @@ -20,6 +20,7 @@ env: REGISTRY_ALIAS: m8q5m4u3 REPOSITORY: mega/mono-engine IMAGE_TAG_BASE: latest + HARBOR_REGISTRY: registry.xuanwu.openatom.cn # Using AWS access key for authentication permissions: @@ -57,10 +58,17 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + # ----------------------------- - # Build & push to ECR + # Build & push to ECR + Harbor # ----------------------------- - - name: Build & push image to ECR (amd64) + - name: Build & push image to ECR and Harbor (amd64) env: ECR_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} IMAGE_TAG_BASE: ${{ env.IMAGE_TAG_BASE }} @@ -71,10 +79,12 @@ jobs: IMAGE_TAG="${IMAGE_TAG_BASE}-${ARCH_SUFFIX}" ECR_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" + HARBOR_IMAGE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$IMAGE_TAG" ECR_CACHE_IMAGE="$ECR_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:buildcache-${ARCH_SUFFIX}" CACHE_SCOPE="mono-engine-${ARCH_SUFFIX}" echo "ECR_IMAGE=$ECR_IMAGE" + echo "HARBOR_IMAGE=$HARBOR_IMAGE" echo "ECR_CACHE_IMAGE=$ECR_CACHE_IMAGE" echo "CACHE_SCOPE=$CACHE_SCOPE" @@ -85,6 +95,7 @@ jobs: --sbom=false \ -f ./mono/Dockerfile \ -t "$ECR_IMAGE" \ + -t "$HARBOR_IMAGE" \ --push . manifest: @@ -106,25 +117,37 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - name: Create & push manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} run: | set -euo pipefail IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" - # latest manifest - docker manifest create "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}" - - # commit-hash manifest (same digests as latest for this commit) - docker manifest create "$IMAGE_BASE:$SHORT_SHA" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$IMAGE_BASE:$SHORT_SHA" + push_manifests() { + local base="$1" + docker manifest create "$base:${{ env.IMAGE_TAG_BASE }}" \ + "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ + "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + docker manifest push "$base:${{ env.IMAGE_TAG_BASE }}" + + docker manifest create "$base:$SHORT_SHA" \ + "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ + "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + docker manifest push "$base:$SHORT_SHA" + } + + push_manifests "$IMAGE_BASE" + push_manifests "$HARBOR_BASE" deploy-aws: needs: manifest diff --git a/.github/workflows/orion-server-deploy.yml b/.github/workflows/orion-server-deploy.yml index c9f2bbb2e..4b04bacd3 100644 --- a/.github/workflows/orion-server-deploy.yml +++ b/.github/workflows/orion-server-deploy.yml @@ -11,6 +11,7 @@ env: REGISTRY_ALIAS: m8q5m4u3 REPOSITORY: mega/orion-server IMAGE_TAG_BASE: latest + HARBOR_REGISTRY: registry.xuanwu.openatom.cn permissions: id-token: write @@ -50,10 +51,17 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + # ----------------------------- - # Build & push to AWS + # Build & push to AWS + Harbor # ----------------------------- - - name: Build & push amd64 image to AWS + - name: Build & push amd64 image to AWS and Harbor env: AWS_REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} run: | @@ -63,6 +71,7 @@ jobs: ARCH_SUFFIX="amd64" AWS_IMAGE_BASE="$AWS_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_IMAGE_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" TAG="${{ env.IMAGE_TAG_BASE }}-$ARCH_SUFFIX" CACHE_SCOPE="orion-server-$ARCH_SUFFIX" @@ -75,6 +84,7 @@ jobs: --sbom=false \ -f orion-server/Dockerfile \ -t "$AWS_IMAGE_BASE:$TAG" \ + -t "$HARBOR_IMAGE_BASE:$TAG" \ --push . manifest: @@ -96,25 +106,37 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - name: Create & push manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} run: | set -euo pipefail IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" - # latest manifest - docker manifest create "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}" - - # commit-hash manifest (same digests as latest for this commit) - docker manifest create "$IMAGE_BASE:$SHORT_SHA" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$IMAGE_BASE:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$IMAGE_BASE:$SHORT_SHA" + push_manifests() { + local base="$1" + docker manifest create "$base:${{ env.IMAGE_TAG_BASE }}" \ + "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ + "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + docker manifest push "$base:${{ env.IMAGE_TAG_BASE }}" + + docker manifest create "$base:$SHORT_SHA" \ + "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ + "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + docker manifest push "$base:$SHORT_SHA" + } + + push_manifests "$IMAGE_BASE" + push_manifests "$HARBOR_BASE" deploy-aws: needs: manifest diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 52c398864..e04166faa 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -13,6 +13,7 @@ env: REGISTRY_ALIAS: m8q5m4u3 REPOSITORY: mega/mega-ui IMAGE_VERSION: latest + HARBOR_REGISTRY: registry.xuanwu.openatom.cn permissions: id-token: write @@ -61,6 +62,13 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - name: Build & push unified image working-directory: moon env: @@ -75,6 +83,8 @@ jobs: PUBLIC_IMAGE="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$IMAGE_TAG" PUBLIC_IMAGE_SHA="$ECR_PUBLIC_REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}:$SHA_TAG" + HARBOR_IMAGE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$IMAGE_TAG" + HARBOR_IMAGE_SHA="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}:$SHA_TAG" docker buildx build \ --platform "$PLATFORM" \ @@ -84,7 +94,9 @@ jobs: --sbom=false \ -f apps/web/Dockerfile \ -t "$PUBLIC_IMAGE" \ - -t "$PUBLIC_IMAGE_SHA" . \ + -t "$PUBLIC_IMAGE_SHA" \ + -t "$HARBOR_IMAGE" \ + -t "$HARBOR_IMAGE_SHA" . \ --push manifest: @@ -106,6 +118,13 @@ jobs: with: registry-type: public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - name: Create & push unified manifest env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} @@ -113,18 +132,23 @@ jobs: set -euo pipefail IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" TAG="${IMAGE_VERSION}" SHORT_SHA="${GITHUB_SHA:0:7}" - # latest manifest - docker manifest create "$IMAGE_BASE:$TAG" \ - "$IMAGE_BASE:${TAG}-amd64" - docker manifest push "$IMAGE_BASE:$TAG" + push_manifests() { + local base="$1" + docker manifest create "$base:$TAG" \ + "$base:${TAG}-amd64" + docker manifest push "$base:$TAG" + + docker manifest create "$base:$SHORT_SHA" \ + "$base:${SHORT_SHA}-amd64" + docker manifest push "$base:$SHORT_SHA" + } - # commit-hash manifest - docker manifest create "$IMAGE_BASE:$SHORT_SHA" \ - "$IMAGE_BASE:${SHORT_SHA}-amd64" - docker manifest push "$IMAGE_BASE:$SHORT_SHA" + push_manifests "$IMAGE_BASE" + push_manifests "$HARBOR_BASE" deploy-aws: needs: manifest diff --git a/.github/workflows/web-sync-server-deploy.yml b/.github/workflows/web-sync-server-deploy.yml index c4c93dad9..02ee94bab 100644 --- a/.github/workflows/web-sync-server-deploy.yml +++ b/.github/workflows/web-sync-server-deploy.yml @@ -19,6 +19,7 @@ env: REGISTRY_ALIAS: m8q5m4u3 REPOSITORY: mega/web-sync-server IMAGE_TAG: latest + HARBOR_REGISTRY: registry.xuanwu.openatom.cn jobs: build-and-push: @@ -45,7 +46,14 @@ jobs: with: registry-type: public - - name: Build, tag, and push docker image to Amazon ECR Public + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.HARBOR_REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Build, tag, and push docker image to ECR Public and Harbor working-directory: moon env: REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} @@ -53,6 +61,7 @@ jobs: set -euo pipefail AWS_IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" + HARBOR_IMAGE_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" IMAGE_TAG="${{ env.IMAGE_TAG }}" SHORT_SHA="${GITHUB_SHA:0:7}" @@ -61,11 +70,17 @@ jobs: -t "$AWS_IMAGE_BASE:$IMAGE_TAG" . docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$AWS_IMAGE_BASE:$SHORT_SHA" + docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$HARBOR_IMAGE_BASE:$IMAGE_TAG" + docker tag "$AWS_IMAGE_BASE:$IMAGE_TAG" "$HARBOR_IMAGE_BASE:$SHORT_SHA" # Push to AWS ECR Public (latest + commit hash) docker push "$AWS_IMAGE_BASE:$IMAGE_TAG" docker push "$AWS_IMAGE_BASE:$SHORT_SHA" + # Push to Harbor (latest + commit hash) + docker push "$HARBOR_IMAGE_BASE:$IMAGE_TAG" + docker push "$HARBOR_IMAGE_BASE:$SHORT_SHA" + deploy-aws: needs: build-and-push if: false # disabled diff --git a/moon/apps/web/components/Home/HomeSidebar.tsx b/moon/apps/web/components/Home/HomeSidebar.tsx index 12bc0108e..2eaa9afbf 100644 --- a/moon/apps/web/components/Home/HomeSidebar.tsx +++ b/moon/apps/web/components/Home/HomeSidebar.tsx @@ -359,7 +359,7 @@ function GithubReloginHint() { - 需要重新登陆 + Re-login required From 692d897f54844ac682416798e486c0b998cf3b87 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 23 Jul 2026 15:59:30 +0800 Subject: [PATCH 3/7] fix(cl): make CLA check advisory and stop blocking merge Keep unsigned CLA visible in the merge box as a reminder, but do not gate Add to Queue or ensure_cl_mergeable. Mark path_check_configs.cla_sign as not required so existing environments pick this up on migrate. --- .github/workflows/mono-engine-deploy.yml | 20 +++--- .github/workflows/orion-server-deploy.yml | 20 +++--- ceres/src/merge_checker/mod.rs | 4 +- ceres/src/model/change_list.rs | 5 +- ...60304_013434_seed_cla_sign_check_config.rs | 2 +- ...0723_080000_cla_sign_check_not_required.rs | 34 ++++++++++ jupiter-migrate/src/migration/mod.rs | 2 + .../web/components/ClBox/ChecksSection.tsx | 68 ++++++++++++------- .../web/components/ClBox/MergeSection.tsx | 16 ++++- 9 files changed, 124 insertions(+), 47 deletions(-) create mode 100644 jupiter-migrate/src/migration/m20260723_080000_cla_sign_check_not_required.rs diff --git a/.github/workflows/mono-engine-deploy.yml b/.github/workflows/mono-engine-deploy.yml index 1fbf76bfc..2a6fb300e 100644 --- a/.github/workflows/mono-engine-deploy.yml +++ b/.github/workflows/mono-engine-deploy.yml @@ -132,17 +132,21 @@ jobs: IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" + TAG_BASE="${{ env.IMAGE_TAG_BASE }}" push_manifests() { local base="$1" - docker manifest create "$base:${{ env.IMAGE_TAG_BASE }}" \ - "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$base:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$base:${{ env.IMAGE_TAG_BASE }}" - - docker manifest create "$base:$SHORT_SHA" \ - "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + local refs=("$base:${TAG_BASE}-amd64") + if docker manifest inspect "$base:${TAG_BASE}-arm64" >/dev/null 2>&1; then + refs+=("$base:${TAG_BASE}-arm64") + else + echo "WARN: $base:${TAG_BASE}-arm64 not found; publishing amd64-only manifests" + fi + + docker manifest create "$base:${TAG_BASE}" "${refs[@]}" + docker manifest push "$base:${TAG_BASE}" + + docker manifest create "$base:$SHORT_SHA" "${refs[@]}" docker manifest push "$base:$SHORT_SHA" } diff --git a/.github/workflows/orion-server-deploy.yml b/.github/workflows/orion-server-deploy.yml index 4b04bacd3..d0d8cf07b 100644 --- a/.github/workflows/orion-server-deploy.yml +++ b/.github/workflows/orion-server-deploy.yml @@ -121,17 +121,21 @@ jobs: IMAGE_BASE="$REGISTRY/${{ env.REGISTRY_ALIAS }}/${{ env.REPOSITORY }}" HARBOR_BASE="${{ env.HARBOR_REGISTRY }}/${{ env.REPOSITORY }}" SHORT_SHA="${GITHUB_SHA:0:7}" + TAG_BASE="${{ env.IMAGE_TAG_BASE }}" push_manifests() { local base="$1" - docker manifest create "$base:${{ env.IMAGE_TAG_BASE }}" \ - "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$base:${{ env.IMAGE_TAG_BASE }}-arm64" - docker manifest push "$base:${{ env.IMAGE_TAG_BASE }}" - - docker manifest create "$base:$SHORT_SHA" \ - "$base:${{ env.IMAGE_TAG_BASE }}-amd64" \ - "$base:${{ env.IMAGE_TAG_BASE }}-arm64" + local refs=("$base:${TAG_BASE}-amd64") + if docker manifest inspect "$base:${TAG_BASE}-arm64" >/dev/null 2>&1; then + refs+=("$base:${TAG_BASE}-arm64") + else + echo "WARN: $base:${TAG_BASE}-arm64 not found; publishing amd64-only manifests" + fi + + docker manifest create "$base:${TAG_BASE}" "${refs[@]}" + docker manifest push "$base:${TAG_BASE}" + + docker manifest create "$base:$SHORT_SHA" "${refs[@]}" docker manifest push "$base:$SHORT_SHA" } diff --git a/ceres/src/merge_checker/mod.rs b/ceres/src/merge_checker/mod.rs index e446248f4..9d5c9d348 100644 --- a/ceres/src/merge_checker/mod.rs +++ b/ceres/src/merge_checker/mod.rs @@ -102,7 +102,9 @@ impl CheckType { CheckType::CodeReview => { "Ensure the required reviewers have approved the merge request" } - CheckType::ClaSign => "Ensure the CL author has signed CLA", + CheckType::ClaSign => { + "Report whether the CL author has signed CLA (advisory; does not block merge)" + } } } } diff --git a/ceres/src/model/change_list.rs b/ceres/src/model/change_list.rs index 729036d78..ecfc87742 100644 --- a/ceres/src/model/change_list.rs +++ b/ceres/src/model/change_list.rs @@ -256,7 +256,10 @@ impl MergeBoxRes { pub fn from_condition(conditions: Vec) -> Self { let mut state = RequirementsState::MERGEABLE; for cond in &conditions { - if cond.result == ConditionResult::FAILED { + // CLA is advisory-only: keep reporting the result, but do not mark the CL unmergeable. + if cond.result == ConditionResult::FAILED + && cond.condition_type != CheckType::ClaSign + { state = RequirementsState::UNMERGEABLE } } diff --git a/jupiter-migrate/src/migration/m20260304_013434_seed_cla_sign_check_config.rs b/jupiter-migrate/src/migration/m20260304_013434_seed_cla_sign_check_config.rs index 15c560a45..b8df4256a 100644 --- a/jupiter-migrate/src/migration/m20260304_013434_seed_cla_sign_check_config.rs +++ b/jupiter-migrate/src/migration/m20260304_013434_seed_cla_sign_check_config.rs @@ -13,7 +13,7 @@ impl MigrationTrait for Migration { db_backend, r#" INSERT INTO path_check_configs (created_at, updated_at, id, path, check_type_code, enabled, required) - SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, COALESCE(MAX(id), 0) + 1, '/', 'cla_sign', true, true + SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, COALESCE(MAX(id), 0) + 1, '/', 'cla_sign', true, false FROM path_check_configs WHERE NOT EXISTS ( SELECT 1 FROM path_check_configs WHERE path = '/' AND check_type_code = 'cla_sign' diff --git a/jupiter-migrate/src/migration/m20260723_080000_cla_sign_check_not_required.rs b/jupiter-migrate/src/migration/m20260723_080000_cla_sign_check_not_required.rs new file mode 100644 index 000000000..e450adc8a --- /dev/null +++ b/jupiter-migrate/src/migration/m20260723_080000_cla_sign_check_not_required.rs @@ -0,0 +1,34 @@ +use sea_orm_migration::{prelude::*, sea_orm::Statement}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db_backend = manager.get_database_backend(); + // CLA remains enabled (still reported) but no longer blocks merge. + manager + .get_connection() + .execute_raw(Statement::from_string( + db_backend, + r#"UPDATE path_check_configs SET required = false, updated_at = CURRENT_TIMESTAMP WHERE check_type_code = 'cla_sign';"#, + )) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db_backend = manager.get_database_backend(); + manager + .get_connection() + .execute_raw(Statement::from_string( + db_backend, + r#"UPDATE path_check_configs SET required = true, updated_at = CURRENT_TIMESTAMP WHERE check_type_code = 'cla_sign';"#, + )) + .await?; + + Ok(()) + } +} diff --git a/jupiter-migrate/src/migration/mod.rs b/jupiter-migrate/src/migration/mod.rs index 3dd5a5fd3..0447cfbdd 100644 --- a/jupiter-migrate/src/migration/mod.rs +++ b/jupiter-migrate/src/migration/mod.rs @@ -100,6 +100,7 @@ mod m20260413_033315_create_artifact_tables; mod m20260612_011232_drop_build_events_log; mod m20260714_021900_create_user_approval_status; mod m20260720_060000_rename_webhook_event_type_underscores; +mod m20260723_080000_cla_sign_check_not_required; mod runner; pub use runner::apply_migrations; @@ -187,6 +188,7 @@ impl MigratorTrait for Migrator { Box::new(m20260612_011232_drop_build_events_log::Migration), Box::new(m20260714_021900_create_user_approval_status::Migration), Box::new(m20260720_060000_rename_webhook_event_type_underscores::Migration), + Box::new(m20260723_080000_cla_sign_check_not_required::Migration), ] } } diff --git a/moon/apps/web/components/ClBox/ChecksSection.tsx b/moon/apps/web/components/ClBox/ChecksSection.tsx index e932d3ce1..10c733461 100644 --- a/moon/apps/web/components/ClBox/ChecksSection.tsx +++ b/moon/apps/web/components/ClBox/ChecksSection.tsx @@ -1,7 +1,8 @@ import { useEffect, useMemo, useState } from 'react' import * as Collapsible from '@radix-ui/react-collapsible' -import { AlertIcon, ArrowDownIcon, ArrowUpIcon, CheckIcon, LoadingSpinner } from '@gitmono/ui' +import { CheckType, ConditionResult } from '@gitmono/types' +import { AlertIcon, ArrowDownIcon, ArrowUpIcon, CheckIcon, LoadingSpinner, WarningTriangleIcon } from '@gitmono/ui' import { useGetClUpdateStatus } from '@/hooks/CL/useGetClUpdateStatus' import { usePostClUpdateBranch } from '@/hooks/CL/usePostClUpdateBranch' @@ -128,7 +129,10 @@ interface AdditionalCheckItemProps { check: AdditionalCheckItem } -const getStatusIcon = (status: AdditionalCheckStatus) => { +const getStatusIcon = (status: AdditionalCheckStatus, advisory = false) => { + if (advisory && status === 'FAILED') { + return + } switch (status) { case 'PASSED': return @@ -139,32 +143,44 @@ const getStatusIcon = (status: AdditionalCheckStatus) => { } } -const AdditionalCheckItemComponent = ({ check }: AdditionalCheckItemProps) => ( -
-
{getStatusIcon(check.result)}
-
-
-
{ADDITIONAL_CHECK_LABELS[check.type]}
- - {check.result.toLowerCase()} - +const AdditionalCheckItemComponent = ({ check }: AdditionalCheckItemProps) => { + const isClaAdvisory = check.type === CheckType.ClaSign && check.result === ConditionResult.FAILED + + return ( +
+
{getStatusIcon(check.result, isClaAdvisory)}
+
+
+
{ADDITIONAL_CHECK_LABELS[check.type]}
+ + {isClaAdvisory ? 'advisory' : check.result.toLowerCase()} + +
+ {check.result === 'FAILED' && ( +
    +
  • + {isClaAdvisory ? `${check.message} (does not block merge)` : check.message} +
  • +
+ )}
- {check.result === 'FAILED' && ( -
    -
  • {check.message}
  • -
- )}
-
-) + ) +} interface AdditionalChecksSectionProps { additionalChecks: AdditionalCheckItem[] diff --git a/moon/apps/web/components/ClBox/MergeSection.tsx b/moon/apps/web/components/ClBox/MergeSection.tsx index 2a17de95a..ed9cb3230 100644 --- a/moon/apps/web/components/ClBox/MergeSection.tsx +++ b/moon/apps/web/components/ClBox/MergeSection.tsx @@ -80,8 +80,10 @@ export const MergeSection = React.memo( let statusNode: React.ReactNode - const isMergeable = isAllReviewerApproved && claCheck !== false + // CLA is advisory-only for now: show a hint but do not block merge. + const isMergeable = isAllReviewerApproved const isDraft = clStatus?.toLowerCase() === 'draft' + const showClaHint = claCheck === false if (isDraft) { statusNode = ( @@ -125,6 +127,16 @@ export const MergeSection = React.memo(
{statusNode} + {showClaHint && ( +
+ + + CLA is not signed yet. This is a reminder only and does not block merging. + {isClAuthor && ' Please sign the CLA when you can.'} + +
+ )} + {/* Queue Status Info */} {inQueue && queueItem && (
@@ -151,7 +163,7 @@ export const MergeSection = React.memo( )} - {claCheck === false && isClAuthor && ( + {showClaHint && isClAuthor && ( + ) + } + ] + : []) ], - [onStatusChange, statusFilter, statusOptions] + [onStatusChange, onViewLogs, showLogsAction, statusFilter, statusOptions] ) if (isLoading) { diff --git a/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts b/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts index 183839337..faf226c57 100644 --- a/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts +++ b/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts @@ -6,6 +6,11 @@ export type RunnerLogsStatus = 'idle' | 'connecting' | 'streaming' | 'error' const MAX_LOG_CHARS = 400_000 +/** Strip CSI / OSC ANSI sequences so terminal-colored scheduler logs render cleanly in HTML. */ +function stripAnsi(text: string): string { + return text.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '') +} + function runnerLogsStreamUrl(vmId: string): string { const base = MONO_API_URL.replace(/\/$/, '') @@ -14,16 +19,17 @@ function runnerLogsStreamUrl(vmId: string): string { /** * Subscribe to mono-proxied Orion runner startup logs (SSE). - * Enabled while `vmId` is set; closes the EventSource when cleared or unmounted. + * `streamKey` is a scheduler VM id or domain host (client hostname is the WS URL). + * Enabled while set; closes the EventSource when cleared or unmounted. */ -export function useRunnerLogsSSE(vmId: string | null) { +export function useRunnerLogsSSE(streamKey: string | null) { const [logs, setLogs] = useState('') const [status, setStatus] = useState('idle') const [error, setError] = useState(null) const esRef = useRef(null) useEffect(() => { - if (!vmId) { + if (!streamKey) { esRef.current?.close() esRef.current = null setLogs('') @@ -36,7 +42,7 @@ export function useRunnerLogsSSE(vmId: string | null) { setError(null) setStatus('connecting') - const es = new EventSource(runnerLogsStreamUrl(vmId), { withCredentials: true }) + const es = new EventSource(runnerLogsStreamUrl(streamKey), { withCredentials: true }) esRef.current = es @@ -45,12 +51,15 @@ export function useRunnerLogsSSE(vmId: string | null) { } es.onmessage = (event) => { - const chunk = event.data + const chunk = stripAnsi(event.data ?? '') if (!chunk) return setLogs((prev) => { - const next = prev ? `${prev}${chunk}` : chunk + // EventSource joins multi-line SSE `data:` fields with `\n` but does not + // guarantee a trailing newline between successive events. + const sep = prev && !prev.endsWith('\n') && !chunk.startsWith('\n') ? '\n' : '' + const next = prev ? `${prev}${sep}${chunk}` : chunk if (next.length <= MAX_LOG_CHARS) return next return next.slice(next.length - MAX_LOG_CHARS) @@ -72,7 +81,7 @@ export function useRunnerLogsSSE(vmId: string | null) { esRef.current = null } } - }, [vmId]) + }, [streamKey]) return { logs, status, error } } diff --git a/moon/apps/web/pages/[org]/oc/index.tsx b/moon/apps/web/pages/[org]/oc/index.tsx index 8db8cc6f0..4d15d58e1 100644 --- a/moon/apps/web/pages/[org]/oc/index.tsx +++ b/moon/apps/web/pages/[org]/oc/index.tsx @@ -14,7 +14,7 @@ import { Button, UIText } from '@gitmono/ui' import { RefreshIcon } from '@gitmono/ui/Icons' import { AppLayout } from '@/components/Layout/AppLayout' -import { ClientsTable, OrionClientStatus } from '@/components/OrionClient' +import { ClientsTable, OrionClient, OrionClientStatus } from '@/components/OrionClient' import AuthAppProviders from '@/components/Providers/AuthAppProviders' import { useAdminCheck } from '@/hooks/admin/useAdminCheck' import { usePostOrionClientsInfo } from '@/hooks/OrionClient/OrionClientsInfo' @@ -23,14 +23,37 @@ import { usePostStartRunner } from '@/hooks/OrionClient/usePostStartRunner' import { useRunnerLogsSSE } from '@/hooks/OrionClient/useRunnerLogsSSE' import { PageWithLayout } from '@/utils/types' +/** Client `hostname` is the WS URL (e.g. wss://orion.example/ws); scheduler keys VMs by that host. */ +function domainFromClientHostname(hostname: string): string | null { + const raw = hostname.trim() + + if (!raw) return null + + try { + const url = new URL(raw.includes('://') ? raw : `ws://${raw}`) + + return url.hostname || null + } catch { + const host = raw.split('/')[0]?.split(':')[0] + + return host || null + } +} + +type LogPanelSource = 'runner' | 'client' + const OrionClientPage: PageWithLayout = () => { const [hostnameInput, setHostnameInput] = React.useState('') const [debouncedHostname, setDebouncedHostname] = React.useState('') const [statusFilter, setStatusFilter] = React.useState('all') const [currentPage, setCurrentPage] = React.useState(1) - const [activeVmId, setActiveVmId] = React.useState(null) + /** Stream key for scheduler logs: VM id (after Start Runner) or domain host (from client list). */ + const [activeLogKey, setActiveLogKey] = React.useState(null) const [activePhase, setActivePhase] = React.useState(null) const [activeDomain, setActiveDomain] = React.useState(null) + const [logSource, setLogSource] = React.useState(null) + const [logClientId, setLogClientId] = React.useState(null) + const logPanelRef = React.useRef(null) const perPage = 8 @@ -38,12 +61,22 @@ const OrionClientPage: PageWithLayout = () => { const isAdmin = adminCheck?.data?.is_admin || false const { mutate: startRunner, isPending: isStartingRunner } = usePostStartRunner() - const { data: runnerStatus } = useGetRunnerStatus(activeVmId, activePhase) - const { logs: runnerLogs, status: runnerLogsStatus, error: runnerLogsError } = useRunnerLogsSSE(activeVmId) + const runnerStatusVmId = logSource === 'runner' ? activeLogKey : null + const { data: runnerStatus } = useGetRunnerStatus(runnerStatusVmId, activePhase) + const { logs: runnerLogs, status: runnerLogsStatus, error: runnerLogsError } = useRunnerLogsSSE(activeLogKey) + const logsPreRef = React.useRef(null) + const logsFollowRef = React.useRef(true) const { mutate, isPending, error } = usePostOrionClientsInfo() const [clientsPage, setClientsPage] = React.useState(null) + React.useEffect(() => { + const el = logsPreRef.current + + if (!el || !logsFollowRef.current) return + el.scrollTop = el.scrollHeight + }, [runnerLogs]) + React.useEffect(() => { const handle = setTimeout(() => { setDebouncedHostname(hostnameInput) @@ -104,20 +137,54 @@ const OrionClientPage: PageWithLayout = () => { } }, [runnerStatus?.phase, handleRefresh]) + const openLogPanel = React.useCallback((key: string, source: LogPanelSource, opts?: { domain?: string | null; clientId?: string | null; phase?: string | null }) => { + setActiveLogKey(key) + setLogSource(source) + setActiveDomain(opts?.domain ?? null) + setLogClientId(opts?.clientId ?? null) + setActivePhase(opts?.phase ?? null) + logsFollowRef.current = true + requestAnimationFrame(() => { + logPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }) + }, []) + + const handleCloseLogs = React.useCallback(() => { + setActiveLogKey(null) + setLogSource(null) + setActiveDomain(null) + setLogClientId(null) + setActivePhase(null) + }, []) + const handleStartRunner = React.useCallback( (replace = false) => { startRunner( { replace }, { onSuccess: (data) => { - setActiveVmId(data.vm_id) - setActivePhase(data.phase) - setActiveDomain(data.domain ?? null) + openLogPanel(data.vm_id, 'runner', { + domain: data.domain ?? null, + phase: data.phase + }) } } ) }, - [startRunner] + [openLogPanel, startRunner] + ) + + const handleViewClientLogs = React.useCallback( + (client: OrionClient) => { + const domain = domainFromClientHostname(client.hostname) + + if (!domain) { + return + } + + openLogPanel(domain, 'client', { domain, clientId: client.client_id }) + }, + [openLogPanel] ) React.useEffect(() => { @@ -159,8 +226,9 @@ const OrionClientPage: PageWithLayout = () => { Orion Client -
-
+ {/* AppLayout main is overflow-hidden; this page must own scrolling. */} +
+

Orion Clients

@@ -189,21 +257,40 @@ const OrionClientPage: PageWithLayout = () => {
- {activeVmId ? ( -
- - Runner {activeVmId} - + {activeLogKey ? ( +
+
+
+ + {logSource === 'client' && logClientId + ? `Client ${logClientId}` + : `Runner ${activeLogKey}`} + + {logSource === 'client' ? ( + + Streaming scheduler logs for domain {activeDomain ?? activeLogKey} + + ) : null} +
+ +
{(runnerStatus?.domain ?? activeDomain) ? ( Domain: {runnerStatus?.domain ?? activeDomain} ) : null} - - Phase:{' '} - {runnerStatus?.phase ?? activePhase ?? 'unknown'} - + {logSource === 'runner' ? ( + + Phase:{' '} + {runnerStatus?.phase ?? activePhase ?? 'unknown'} + + ) : null} {runnerStatus?.vm_ip ? ( VM IP: {runnerStatus.vm_ip} @@ -219,17 +306,17 @@ const OrionClientPage: PageWithLayout = () => { {runnerStatus.error} ) : null} - {runnerStatus?.phase === 'failed' ? ( + {logSource === 'runner' && runnerStatus?.phase === 'failed' ? ( ) : null}
-
+
- Startup logs + {logSource === 'client' ? 'Runner logs' : 'Startup logs'} {runnerLogsStatus === 'connecting' @@ -246,11 +333,20 @@ const OrionClientPage: PageWithLayout = () => { {runnerLogsError} ) : null} -
+                
 {
+                    const el = e.currentTarget
+                    const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
+
+                    logsFollowRef.current = distanceFromBottom < 48
+                  }}
+                  className='h-80 max-h-80 w-full min-w-0 overflow-auto overscroll-contain whitespace-pre-wrap break-words rounded border border-gray-200 bg-black/90 p-3 font-mono text-xs leading-5 text-green-100 dark:border-gray-700'
+                >
                   {runnerLogs ||
                     (runnerLogsStatus === 'connecting'
                       ? 'Waiting for log stream…'
-                      : 'No log lines yet. Logs appear while the runner provisions.')}
+                      : 'No log lines yet. Logs appear while the runner is running.')}
                 
@@ -290,6 +386,8 @@ const OrionClientPage: PageWithLayout = () => { isLoading={isPending} statusFilter={statusFilter} onStatusChange={(value: OrionClientStatus | 'all') => setStatusFilter(value)} + canViewLogs={isAdmin} + onViewLogs={handleViewClientLogs} statusOptions={[ { value: 'all', label: 'All statuses' }, { value: 'idle', label: 'Idle' }, From 8f7cdacd62cf194c0dd6e9277869511981cfdab6 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 23 Jul 2026 16:55:42 +0800 Subject: [PATCH 5/7] ci(jenkins): add Harbor build pipelines for mega apps Add Pipeline scripts for mono-engine, mega-ui, web-sync-server, and orion-server that checkout GitHub and push short-SHA/latest tags to Harbor. --- ci/jenkins/mega-ui/Jenkinsfile | 61 ++++++++++++++++++++++++++ ci/jenkins/mono-engine/Jenkinsfile | 61 ++++++++++++++++++++++++++ ci/jenkins/orion-server/Jenkinsfile | 61 ++++++++++++++++++++++++++ ci/jenkins/web-sync-server/Jenkinsfile | 61 ++++++++++++++++++++++++++ moon/apps/web/pages/[org]/oc/index.tsx | 2 +- 5 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 ci/jenkins/mega-ui/Jenkinsfile create mode 100644 ci/jenkins/mono-engine/Jenkinsfile create mode 100644 ci/jenkins/orion-server/Jenkinsfile create mode 100644 ci/jenkins/web-sync-server/Jenkinsfile diff --git a/ci/jenkins/mega-ui/Jenkinsfile b/ci/jenkins/mega-ui/Jenkinsfile new file mode 100644 index 000000000..e09a5d7d0 --- /dev/null +++ b/ci/jenkins/mega-ui/Jenkinsfile @@ -0,0 +1,61 @@ +pipeline { + agent any + + environment { + HARBOR_REGISTRY = 'registry.xuanwu.openatom.cn' + HARBOR_REPO = 'mega/mega-ui' + DOCKERFILE = 'moon/apps/web/Dockerfile' + BUILD_CONTEXT = 'moon' + GIT_URL = 'https://github.com/benjamin-747/mega' + GIT_BRANCH = 'main' + GIT_CREDENTIALS = 'd4cedbd2-3e68-411b-8367-0960974d7b6d' + HARBOR_CREDENTIALS = '3cefd32b-0fae-4749-b125-0a3f0c537f88' + } + + stages { + stage('Checkout') { + steps { + git branch: "${GIT_BRANCH}", + credentialsId: "${GIT_CREDENTIALS}", + url: "${GIT_URL}" + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim() + env.IMAGE_SHA = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}" + env.IMAGE_LATEST = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest" + env.IMAGE_SHA_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}-amd64" + env.IMAGE_LATEST_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest-amd64" + currentBuild.description = "${HARBOR_REPO}:${env.SHORT_SHA}" + } + echo "Building image: ${env.IMAGE_SHA}" + } + } + + stage('Build Docker Image') { + steps { + sh '/opt/docker-cli/docker build --network=host -f ${DOCKERFILE} -t ${IMAGE_SHA} ${BUILD_CONTEXT}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST_AMD64}' + } + } + + stage('Push to Harbor') { + steps { + withCredentials([ + usernamePassword( + credentialsId: "${HARBOR_CREDENTIALS}", + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh '/opt/docker-cli/docker login ${HARBOR_REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASS}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST_AMD64}' + sh '/opt/docker-cli/docker logout ${HARBOR_REGISTRY}' + } + } + } + } +} diff --git a/ci/jenkins/mono-engine/Jenkinsfile b/ci/jenkins/mono-engine/Jenkinsfile new file mode 100644 index 000000000..4dbc3ff34 --- /dev/null +++ b/ci/jenkins/mono-engine/Jenkinsfile @@ -0,0 +1,61 @@ +pipeline { + agent any + + environment { + HARBOR_REGISTRY = 'registry.xuanwu.openatom.cn' + HARBOR_REPO = 'mega/mono-engine' + DOCKERFILE = 'mono/Dockerfile' + BUILD_CONTEXT = '.' + GIT_URL = 'https://github.com/benjamin-747/mega' + GIT_BRANCH = 'main' + GIT_CREDENTIALS = 'd4cedbd2-3e68-411b-8367-0960974d7b6d' + HARBOR_CREDENTIALS = '3cefd32b-0fae-4749-b125-0a3f0c537f88' + } + + stages { + stage('Checkout') { + steps { + git branch: "${GIT_BRANCH}", + credentialsId: "${GIT_CREDENTIALS}", + url: "${GIT_URL}" + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim() + env.IMAGE_SHA = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}" + env.IMAGE_LATEST = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest" + env.IMAGE_SHA_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}-amd64" + env.IMAGE_LATEST_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest-amd64" + currentBuild.description = "${HARBOR_REPO}:${env.SHORT_SHA}" + } + echo "Building image: ${env.IMAGE_SHA}" + } + } + + stage('Build Docker Image') { + steps { + sh '/opt/docker-cli/docker build --network=host -f ${DOCKERFILE} -t ${IMAGE_SHA} ${BUILD_CONTEXT}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST_AMD64}' + } + } + + stage('Push to Harbor') { + steps { + withCredentials([ + usernamePassword( + credentialsId: "${HARBOR_CREDENTIALS}", + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh '/opt/docker-cli/docker login ${HARBOR_REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASS}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST_AMD64}' + sh '/opt/docker-cli/docker logout ${HARBOR_REGISTRY}' + } + } + } + } +} diff --git a/ci/jenkins/orion-server/Jenkinsfile b/ci/jenkins/orion-server/Jenkinsfile new file mode 100644 index 000000000..03423e814 --- /dev/null +++ b/ci/jenkins/orion-server/Jenkinsfile @@ -0,0 +1,61 @@ +pipeline { + agent any + + environment { + HARBOR_REGISTRY = 'registry.xuanwu.openatom.cn' + HARBOR_REPO = 'mega/orion-server' + DOCKERFILE = 'orion-server/Dockerfile' + BUILD_CONTEXT = '.' + GIT_URL = 'https://github.com/benjamin-747/mega' + GIT_BRANCH = 'main' + GIT_CREDENTIALS = 'd4cedbd2-3e68-411b-8367-0960974d7b6d' + HARBOR_CREDENTIALS = '3cefd32b-0fae-4749-b125-0a3f0c537f88' + } + + stages { + stage('Checkout') { + steps { + git branch: "${GIT_BRANCH}", + credentialsId: "${GIT_CREDENTIALS}", + url: "${GIT_URL}" + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim() + env.IMAGE_SHA = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}" + env.IMAGE_LATEST = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest" + env.IMAGE_SHA_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}-amd64" + env.IMAGE_LATEST_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest-amd64" + currentBuild.description = "${HARBOR_REPO}:${env.SHORT_SHA}" + } + echo "Building image: ${env.IMAGE_SHA}" + } + } + + stage('Build Docker Image') { + steps { + sh '/opt/docker-cli/docker build --network=host -f ${DOCKERFILE} -t ${IMAGE_SHA} ${BUILD_CONTEXT}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST_AMD64}' + } + } + + stage('Push to Harbor') { + steps { + withCredentials([ + usernamePassword( + credentialsId: "${HARBOR_CREDENTIALS}", + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh '/opt/docker-cli/docker login ${HARBOR_REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASS}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST_AMD64}' + sh '/opt/docker-cli/docker logout ${HARBOR_REGISTRY}' + } + } + } + } +} diff --git a/ci/jenkins/web-sync-server/Jenkinsfile b/ci/jenkins/web-sync-server/Jenkinsfile new file mode 100644 index 000000000..1417d23af --- /dev/null +++ b/ci/jenkins/web-sync-server/Jenkinsfile @@ -0,0 +1,61 @@ +pipeline { + agent any + + environment { + HARBOR_REGISTRY = 'registry.xuanwu.openatom.cn' + HARBOR_REPO = 'mega/web-sync-server' + DOCKERFILE = 'moon/apps/sync-server/Dockerfile' + BUILD_CONTEXT = 'moon' + GIT_URL = 'https://github.com/benjamin-747/mega' + GIT_BRANCH = 'main' + GIT_CREDENTIALS = 'd4cedbd2-3e68-411b-8367-0960974d7b6d' + HARBOR_CREDENTIALS = '3cefd32b-0fae-4749-b125-0a3f0c537f88' + } + + stages { + stage('Checkout') { + steps { + git branch: "${GIT_BRANCH}", + credentialsId: "${GIT_CREDENTIALS}", + url: "${GIT_URL}" + script { + env.SHORT_SHA = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim() + env.IMAGE_SHA = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}" + env.IMAGE_LATEST = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest" + env.IMAGE_SHA_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:${env.SHORT_SHA}-amd64" + env.IMAGE_LATEST_AMD64 = "${HARBOR_REGISTRY}/${HARBOR_REPO}:latest-amd64" + currentBuild.description = "${HARBOR_REPO}:${env.SHORT_SHA}" + } + echo "Building image: ${env.IMAGE_SHA}" + } + } + + stage('Build Docker Image') { + steps { + sh '/opt/docker-cli/docker build --network=host -f ${DOCKERFILE} -t ${IMAGE_SHA} ${BUILD_CONTEXT}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker tag ${IMAGE_SHA} ${IMAGE_LATEST_AMD64}' + } + } + + stage('Push to Harbor') { + steps { + withCredentials([ + usernamePassword( + credentialsId: "${HARBOR_CREDENTIALS}", + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh '/opt/docker-cli/docker login ${HARBOR_REGISTRY} -u ${HARBOR_USER} -p ${HARBOR_PASS}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST}' + sh '/opt/docker-cli/docker push ${IMAGE_SHA_AMD64}' + sh '/opt/docker-cli/docker push ${IMAGE_LATEST_AMD64}' + sh '/opt/docker-cli/docker logout ${HARBOR_REGISTRY}' + } + } + } + } +} diff --git a/moon/apps/web/pages/[org]/oc/index.tsx b/moon/apps/web/pages/[org]/oc/index.tsx index 4d15d58e1..f0e352629 100644 --- a/moon/apps/web/pages/[org]/oc/index.tsx +++ b/moon/apps/web/pages/[org]/oc/index.tsx @@ -240,7 +240,7 @@ const OrionClientPage: PageWithLayout = () => { {isAdmin ? (
diff --git a/moon/apps/web/components/ClView/filters/FilterDropdown.tsx b/moon/apps/web/components/ClView/filters/FilterDropdown.tsx index b643552a4..0ff47ff63 100644 --- a/moon/apps/web/components/ClView/filters/FilterDropdown.tsx +++ b/moon/apps/web/components/ClView/filters/FilterDropdown.tsx @@ -67,6 +67,15 @@ export function FilterDropdown({
) + // Prevent Radix from closing the menu when focusing/clicking the search input + const searchItem: MenuItem | null = hasSearch + ? { + type: 'item' as const, + label: , + onSelect: (e: Event) => e.preventDefault() + } + : null + const defaultTrigger = ( + + + + + + + ) +} diff --git a/moon/apps/web/components/Labels/NewLabelDialog.tsx b/moon/apps/web/components/Labels/NewLabelDialog.tsx index 97b3d487f..6643192ea 100644 --- a/moon/apps/web/components/Labels/NewLabelDialog.tsx +++ b/moon/apps/web/components/Labels/NewLabelDialog.tsx @@ -1,11 +1,30 @@ // components/Labels/NewLabelDialog.tsx import React, { useState } from 'react' -import { random } from 'colord' +import { colord, random } from 'colord' import { Button, Dialog, RefreshIcon, TextField } from '@gitmono/ui' import { getFontColor } from '@/utils/getFontColor' +const PRESET_COLORS = [ + '#b60205', + '#d93f0b', + '#fbca04', + '#0e8a16', + '#006b75', + '#1d76db', + '#0052cc', + '#5319e7', + '#e99695', + '#f9d0c4', + '#fef2c0', + '#c2e0c6', + '#bfdadc', + '#c5def5', + '#bfd4f2', + '#d4c5f9' +] + interface NewLabelDialogProps { isOpen: boolean onClose: () => void @@ -18,14 +37,19 @@ export const NewLabelDialog: React.FC = ({ isOpen, onClose, const [description, setDescription] = useState('') const fontColor = getFontColor(color) + const normalizedColor = colord(color).isValid() ? colord(color).toHex() : '#000000' const generateRandomColor = () => { setColor(random().toHex()) } + const handleColorInputChange = (value: string) => { + setColor(value.startsWith('#') ? value : `#${value}`) + } + const handleCreateLabel = () => { if (name.trim()) { - onCreateLabel(name, description, color) + onCreateLabel(name, description, normalizedColor) setName('') setDescription('') generateRandomColor() @@ -42,7 +66,7 @@ export const NewLabelDialog: React.FC = ({ isOpen, onClose,
= ({ isOpen, onClose,
-
- +
+ setColor(e.target.value)} + className='h-6 w-6 cursor-pointer border-none bg-transparent p-0' + title='Pick a color' + /> setColor(e.target.value)} + onChange={(e) => handleColorInputChange(e.target.value)} + placeholder='#ffffff' + spellCheck={false} />
+ +
+ {PRESET_COLORS.map((preset) => { + const selected = normalizedColor.toLowerCase() === preset.toLowerCase() + + return ( +
diff --git a/moon/apps/web/components/Providers/AuthAppProviders.tsx b/moon/apps/web/components/Providers/AuthAppProviders.tsx index 2052023d1..49ca080e6 100644 --- a/moon/apps/web/components/Providers/AuthAppProviders.tsx +++ b/moon/apps/web/components/Providers/AuthAppProviders.tsx @@ -14,6 +14,7 @@ import { AutoTimezoneSwitcher } from '@/components/AutoTimezoneSwitcher' import { IncomingCallRoomInvitationToast } from '@/components/Call/IncomingCallRoomInvitationToast' import { LocalCommandMenu } from '@/components/CommandMenu' import { FeedbackDialog } from '@/components/Feedback/FeedbackDialog' +import { GithubLoginRequiredDialog } from '@/components/GithubLoginRequiredDialog' import { GlobalKeyboardShortcuts } from '@/components/GlobalKeyboardShortcuts' import { PostComposer } from '@/components/PostComposer' import { AuthProvider } from '@/components/Providers/AuthProvider' @@ -79,6 +80,7 @@ export const AuthAppProviders: PageWithProviders = ({ children, allowLogged {children} + diff --git a/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts b/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts index faf226c57..05d8d30fd 100644 --- a/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts +++ b/moon/apps/web/hooks/OrionClient/useRunnerLogsSSE.ts @@ -6,6 +6,9 @@ export type RunnerLogsStatus = 'idle' | 'connecting' | 'streaming' | 'error' const MAX_LOG_CHARS = 400_000 +/** Older schedulers spam this every second while the VM is still provisioning. */ +const TRANSIENT_NO_VM_RE = /^Error:\s*No running VM for key\b/i + /** Strip CSI / OSC ANSI sequences so terminal-colored scheduler logs render cleanly in HTML. */ function stripAnsi(text: string): string { return text.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, '') @@ -17,6 +20,29 @@ function runnerLogsStreamUrl(vmId: string): string { return `${base}/api/v1/orion/runners/${encodeURIComponent(vmId)}/logs/stream` } +/** + * Drop repeated "No running VM" errors from older schedulers, keeping a single + * waiting notice until real log lines arrive. + */ +function filterTransientVmErrors(chunk: string, alreadyWaiting: boolean): { text: string; waiting: boolean } { + const lines = chunk.split('\n') + const kept: string[] = [] + let waiting = alreadyWaiting + + for (const line of lines) { + if (TRANSIENT_NO_VM_RE.test(line.trim())) { + if (!waiting) { + kept.push('Waiting for VM to finish provisioning…') + waiting = true + } + continue + } + kept.push(line) + } + + return { text: kept.join('\n'), waiting } +} + /** * Subscribe to mono-proxied Orion runner startup logs (SSE). * `streamKey` is a scheduler VM id or domain host (client hostname is the WS URL). @@ -27,6 +53,7 @@ export function useRunnerLogsSSE(streamKey: string | null) { const [status, setStatus] = useState('idle') const [error, setError] = useState(null) const esRef = useRef(null) + const waitingForVmRef = useRef(false) useEffect(() => { if (!streamKey) { @@ -35,12 +62,14 @@ export function useRunnerLogsSSE(streamKey: string | null) { setLogs('') setStatus('idle') setError(null) + waitingForVmRef.current = false return } setLogs('') setError(null) setStatus('connecting') + waitingForVmRef.current = false const es = new EventSource(runnerLogsStreamUrl(streamKey), { withCredentials: true }) @@ -51,9 +80,21 @@ export function useRunnerLogsSSE(streamKey: string | null) { } es.onmessage = (event) => { - const chunk = stripAnsi(event.data ?? '') + const raw = stripAnsi(event.data ?? '') + + if (!raw) return + + const { text: chunk, waiting } = filterTransientVmErrors(raw, waitingForVmRef.current) - if (!chunk) return + waitingForVmRef.current = waiting + + if (!chunk.trim()) return + + // Real log content arrived — clear the transient-wait gate so a later + // reprovision can announce waiting again if needed. + if (!TRANSIENT_NO_VM_RE.test(chunk.trim()) && !chunk.includes('Waiting for VM')) { + waitingForVmRef.current = false + } setLogs((prev) => { // EventSource joins multi-line SSE `data:` fields with `\n` but does not diff --git a/moon/apps/web/pages/[org]/oc/index.tsx b/moon/apps/web/pages/[org]/oc/index.tsx index f0e352629..a5f10ca1a 100644 --- a/moon/apps/web/pages/[org]/oc/index.tsx +++ b/moon/apps/web/pages/[org]/oc/index.tsx @@ -53,9 +53,15 @@ const OrionClientPage: PageWithLayout = () => { const [activeDomain, setActiveDomain] = React.useState(null) const [logSource, setLogSource] = React.useState(null) const [logClientId, setLogClientId] = React.useState(null) + const [copyFeedback, setCopyFeedback] = React.useState(false) const logPanelRef = React.useRef(null) + const logsScrollRef = React.useRef(null) + const logsPreRef = React.useRef(null) + const logsFollowRef = React.useRef(true) + const runnerLogsRef = React.useRef('') const perPage = 8 + const showingLogs = Boolean(activeLogKey) const { data: adminCheck } = useAdminCheck() const isAdmin = adminCheck?.data?.is_admin || false @@ -64,17 +70,39 @@ const OrionClientPage: PageWithLayout = () => { const runnerStatusVmId = logSource === 'runner' ? activeLogKey : null const { data: runnerStatus } = useGetRunnerStatus(runnerStatusVmId, activePhase) const { logs: runnerLogs, status: runnerLogsStatus, error: runnerLogsError } = useRunnerLogsSSE(activeLogKey) - const logsPreRef = React.useRef(null) - const logsFollowRef = React.useRef(true) + + runnerLogsRef.current = runnerLogs const { mutate, isPending, error } = usePostOrionClientsInfo() const [clientsPage, setClientsPage] = React.useState(null) + const copyLogsToClipboard = React.useCallback(async (text: string) => { + if (!text) return false + + try { + await navigator.clipboard.writeText(text) + setCopyFeedback(true) + window.setTimeout(() => setCopyFeedback(false), 1500) + return true + } catch { + return false + } + }, []) + React.useEffect(() => { - const el = logsPreRef.current + if (!logsFollowRef.current) return + + const el = logsScrollRef.current + + if (!el) return - if (!el || !logsFollowRef.current) return - el.scrollTop = el.scrollHeight + // Defer until after the
 text paints, otherwise scrollHeight is stale.
+    const id = window.requestAnimationFrame(() => {
+      if (!logsFollowRef.current || !logsScrollRef.current) return
+      logsScrollRef.current.scrollTop = logsScrollRef.current.scrollHeight
+    })
+
+    return () => window.cancelAnimationFrame(id)
   }, [runnerLogs])
 
   React.useEffect(() => {
@@ -116,12 +144,14 @@ const OrionClientPage: PageWithLayout = () => {
   }, [currentPage, debouncedHostname, perPage, statusFilter])
 
   const handleRefresh = React.useCallback(() => {
+    if (showingLogs) return
+
     mutate(requestPayload, {
       onSuccess: (data) => {
         setClientsPage(data)
       }
     })
-  }, [mutate, requestPayload])
+  }, [mutate, requestPayload, showingLogs])
 
   React.useEffect(() => {
     if (!runnerStatus) return
@@ -131,23 +161,33 @@ const OrionClientPage: PageWithLayout = () => {
     }
   }, [runnerStatus])
 
+  // Do not refresh the client list while the log panel is open.
   React.useEffect(() => {
+    if (showingLogs) return
     if (runnerStatus?.phase === 'running') {
       handleRefresh()
     }
-  }, [runnerStatus?.phase, handleRefresh])
-
-  const openLogPanel = React.useCallback((key: string, source: LogPanelSource, opts?: { domain?: string | null; clientId?: string | null; phase?: string | null }) => {
-    setActiveLogKey(key)
-    setLogSource(source)
-    setActiveDomain(opts?.domain ?? null)
-    setLogClientId(opts?.clientId ?? null)
-    setActivePhase(opts?.phase ?? null)
-    logsFollowRef.current = true
-    requestAnimationFrame(() => {
-      logPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
-    })
-  }, [])
+  }, [runnerStatus?.phase, handleRefresh, showingLogs])
+
+  const openLogPanel = React.useCallback(
+    (
+      key: string,
+      source: LogPanelSource,
+      opts?: { domain?: string | null; clientId?: string | null; phase?: string | null }
+    ) => {
+      setActiveLogKey(key)
+      setLogSource(source)
+      setActiveDomain(opts?.domain ?? null)
+      setLogClientId(opts?.clientId ?? null)
+      setActivePhase(opts?.phase ?? null)
+      logsFollowRef.current = true
+      requestAnimationFrame(() => {
+        logsScrollRef.current?.focus({ preventScroll: true })
+        logPanelRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+      })
+    },
+    []
+  )
 
   const handleCloseLogs = React.useCallback(() => {
     setActiveLogKey(null)
@@ -187,13 +227,48 @@ const OrionClientPage: PageWithLayout = () => {
     [openLogPanel]
   )
 
+  const handleLogsKeyDown = React.useCallback(
+    async (e: React.KeyboardEvent) => {
+      const meta = e.metaKey || e.ctrlKey
+
+      if (!meta) return
+
+      if (e.key === 'a' || e.key === 'A') {
+        e.preventDefault()
+        const pre = logsPreRef.current
+
+        if (!pre) return
+        const selection = window.getSelection()
+        const range = document.createRange()
+
+        range.selectNodeContents(pre)
+        selection?.removeAllRanges()
+        selection?.addRange(range)
+        return
+      }
+
+      if (e.key === 'c' || e.key === 'C') {
+        const selection = window.getSelection()?.toString() ?? ''
+        const text = selection || runnerLogsRef.current
+
+        if (!text) return
+        e.preventDefault()
+        await copyLogsToClipboard(text)
+      }
+    },
+    [copyLogsToClipboard]
+  )
+
+  // Fetch client list only while the log panel is closed.
   React.useEffect(() => {
+    if (showingLogs) return
+
     mutate(requestPayload, {
       onSuccess: (data) => {
         setClientsPage(data)
       }
     })
-  }, [mutate, requestPayload])
+  }, [mutate, requestPayload, showingLogs])
 
   const total = clientsPage?.total ?? 0
 
@@ -226,15 +301,17 @@ const OrionClientPage: PageWithLayout = () => {
       
         Orion Client
       
-      {/* AppLayout main is overflow-hidden; this page must own scrolling. */}
-      
+ {/* AppLayout main is overflow-hidden; this page must own scrolling when the list is visible. */} +

Orion Clients

- - Total clients {total} - + {!showingLogs ? ( + + Total clients {total} + + ) : null}
{isAdmin ? ( @@ -246,28 +323,28 @@ const OrionClientPage: PageWithLayout = () => { {isStartingRunner ? 'Starting…' : 'Start Runner'} ) : null} -
- {activeLogKey ? ( + {showingLogs ? (
- {logSource === 'client' && logClientId - ? `Client ${logClientId}` - : `Runner ${activeLogKey}`} + {logSource === 'client' && logClientId ? `Client ${logClientId}` : `Runner ${activeLogKey}`} {logSource === 'client' ? ( @@ -318,102 +395,139 @@ const OrionClientPage: PageWithLayout = () => { {logSource === 'client' ? 'Runner logs' : 'Startup logs'} - - {runnerLogsStatus === 'connecting' - ? 'Connecting…' - : runnerLogsStatus === 'streaming' - ? 'Live' - : runnerLogsStatus === 'error' - ? 'Disconnected' - : 'Idle'} - +
+ + {runnerLogsStatus === 'connecting' + ? 'Connecting…' + : runnerLogsStatus === 'streaming' + ? 'Live' + : runnerLogsStatus === 'error' + ? 'Disconnected' + : 'Idle'} + + {runnerLogs ? ( + + ) : null} +
{runnerLogsError ? ( {runnerLogsError} ) : null} -
 {
+                    // Stop auto-follow as soon as the user scrolls up.
+                    if (e.deltaY < 0) {
+                      logsFollowRef.current = false
+                    }
+                  }}
                   onScroll={(e) => {
                     const el = e.currentTarget
                     const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
 
-                    logsFollowRef.current = distanceFromBottom < 48
+                    logsFollowRef.current = distanceFromBottom < 40
                   }}
-                  className='h-80 max-h-80 w-full min-w-0 overflow-auto overscroll-contain whitespace-pre-wrap break-words rounded border border-gray-200 bg-black/90 p-3 font-mono text-xs leading-5 text-green-100 dark:border-gray-700'
+                  style={{ height: 320, maxHeight: 320, overflowY: 'auto', overflowX: 'auto' }}
+                  className='w-full cursor-text select-text rounded border border-gray-200 bg-black/90 outline-none focus:ring-2 focus:ring-blue-500/40 dark:border-gray-700'
                 >
-                  {runnerLogs ||
-                    (runnerLogsStatus === 'connecting'
-                      ? 'Waiting for log stream…'
-                      : 'No log lines yet. Logs appear while the runner is running.')}
-                
+
+                    {runnerLogs ||
+                      (runnerLogsStatus === 'connecting'
+                        ? 'Waiting for log stream…'
+                        : 'No log lines yet. Logs appear while the runner is running.')}
+                  
+
+ + Scroll inside the box to browse. ⌘/Ctrl+A select all, ⌘/Ctrl+C copy. Scroll to bottom to resume live + follow. +
) : null} -
+ {!showingLogs ?
: null}
-
-
- - +
+
+ + + +
+ setHostnameInput(e.target.value)} + placeholder='Search by Hostname' + className='w-full flex-1 border-none bg-transparent text-sm text-gray-700 outline-none ring-0 placeholder:text-gray-400 focus:outline-none focus:ring-0 dark:text-gray-100 dark:placeholder:text-gray-500' /> - -
- setHostnameInput(e.target.value)} - placeholder='Search by Hostname' - className='w-full flex-1 border-none bg-transparent text-sm text-gray-700 outline-none ring-0 placeholder:text-gray-400 focus:outline-none focus:ring-0 dark:text-gray-100 dark:placeholder:text-gray-500' - /> -
- - setStatusFilter(value)} - canViewLogs={isAdmin} - onViewLogs={handleViewClientLogs} - statusOptions={[ - { value: 'all', label: 'All statuses' }, - { value: 'idle', label: 'Idle' }, - { value: 'busy', label: 'Busy' }, - { value: 'downloading', label: '\u00A0\u00A0Downloading source' }, - { value: 'running', label: '\u00A0\u00A0Running build' }, - { value: 'error', label: 'Error' }, - { value: 'offline', label: 'Lost / Offline' } - ]} - /> - - {error ? ( - - Failed to load Orion clients: {error.message} - - ) : null} +
- {pageCount > 1 ? ( -
- setCurrentPage(page)} + setStatusFilter(value)} + canViewLogs={isAdmin} + onViewLogs={handleViewClientLogs} + statusOptions={[ + { value: 'all', label: 'All statuses' }, + { value: 'idle', label: 'Idle' }, + { value: 'busy', label: 'Busy' }, + { value: 'downloading', label: '\u00A0\u00A0Downloading source' }, + { value: 'running', label: '\u00A0\u00A0Running build' }, + { value: 'error', label: 'Error' }, + { value: 'offline', label: 'Lost / Offline' } + ]} /> -
+ + {error ? ( + + Failed to load Orion clients: {error.message} + + ) : null} + + {pageCount > 1 ? ( +
+ setCurrentPage(page)} + /> +
+ ) : null} + ) : null}
diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index 9bd8e9a1c..49b138712 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -4959,6 +4959,11 @@ export type EditFilePayload = { content: string /** force create new cl */ mode?: EditCLMode + /** + * Optional destination path for same-directory rename (e.g. "/project/dir/renamed.rs"). + * Must share the same parent directory as `path`. + */ + new_path?: string | null /** Full file path like "/project/dir/file.rs" */ path: string /** if true, skip build */ diff --git a/moon/script/gen-client b/moon/script/gen-client old mode 100644 new mode 100755 index e2970e9d2..b6d496650 --- a/moon/script/gen-client +++ b/moon/script/gen-client @@ -4,6 +4,22 @@ set -eou pipefail OPENAPI_FILE_PATH=api/gen/openapi_schema.json SWAGGER_FILE_PATH=api/gen/merged_swagger.json +GITMONO_FILE_PATH=api/gen/gitmono.json +GITMONO_OPENAPI_URL="${GITMONO_OPENAPI_URL:-http://localhost:8000/api/openapi.json}" + +# Fetch mono OpenAPI, pretty-print, and replace the checked-in dump +echo "Fetching OpenAPI from ${GITMONO_OPENAPI_URL} ..." +tmp_openapi="$(mktemp)" +trap 'rm -f "${tmp_openapi}"' EXIT +curl -fsS "${GITMONO_OPENAPI_URL}" -o "${tmp_openapi}" +node -e " +const fs = require('fs'); +const src = process.argv[1]; +const dest = process.argv[2]; +const json = JSON.parse(fs.readFileSync(src, 'utf8')); +fs.writeFileSync(dest, JSON.stringify(json, null, 2) + '\n'); +console.log('Updated ' + dest); +" "${tmp_openapi}" "${GITMONO_FILE_PATH}" # swagger-typescript-api does not work with ES Modules which is required for our prettier v3 plugins # and swagger-typescript-api automatically detects and runs prettier on generated files diff --git a/orion-scheduler/src/handlers.rs b/orion-scheduler/src/handlers.rs index dd3143185..092ef850e 100644 --- a/orion-scheduler/src/handlers.rs +++ b/orion-scheduler/src/handlers.rs @@ -829,6 +829,10 @@ fn hash_line(line: &str) -> u64 { /// First tick sends the last `INITIAL_TAIL_LINES` lines, then only newly /// appended lines on each subsequent tick. /// Multi-VM: pass `?domain=` or `?vm_id=` to select which runner's logs to stream. +/// +/// While the selected VM is still provisioning (no machine handle yet), the +/// stream emits a single waiting line instead of repeating "No running VM" +/// errors every tick. pub async fn logs_stream_handler( State(state): State>, Query(q): Query, @@ -838,6 +842,8 @@ pub async fn logs_stream_handler( let mut ticker = interval(std::time::Duration::from_secs(1)); let mut journal_cursor = LogCursor::default(); let mut orion_log_offset: u64 = 0; + let mut waiting_announced = false; + let mut failure_announced = false; loop { ticker.tick().await; @@ -849,8 +855,48 @@ pub async fn logs_stream_handler( ) .await { - Ok(snapshot) => snapshot, + Ok(snapshot) => { + waiting_announced = false; + failure_announced = false; + snapshot + } Err(e) => { + let msg = e.to_string(); + if is_vm_not_ready_error(&msg) { + match orion_deployer::get_status_by_key(&state, key.as_deref()).await { + Some(vm) if vm.phase == VmPhase::Provisioning => { + if !waiting_announced { + waiting_announced = true; + yield Ok(Event::default().data(format!( + "Waiting for VM {} to finish provisioning…", + vm.id + ))); + } + } + Some(vm) if vm.phase == VmPhase::Failed => { + if !failure_announced { + failure_announced = true; + let detail = vm + .error + .unwrap_or_else(|| "unknown error".to_string()); + yield Ok(Event::default().data(format!( + "VM {} failed: {}", + vm.id, detail + ))); + } + } + Some(_) | None => { + if !waiting_announced { + waiting_announced = true; + yield Ok(Event::default().data( + "Waiting for VM to become available…", + )); + } + } + } + continue; + } + yield Ok(Event::default().data(format!("Error: {}", e))); continue; } @@ -880,16 +926,32 @@ pub async fn logs_stream_handler( Sse::new(stream).keep_alive(axum::response::sse::KeepAlive::default()) } +fn is_vm_not_ready_error(msg: &str) -> bool { + msg.contains("No running VM for key") + || msg.contains("No VM is currently running") + || msg.contains("No VM machine handle available") +} + /// Append a log section with a title header and colored log lines to `output`. fn append_logs_section(output: &mut String, title: &str, lines: &[&str]) { use std::fmt::Write; - let _ = writeln!(output, "\n─── {} ───", title); + let mut wrote_any = false; for line in lines { let trimmed = line.trim(); - if trimmed.is_empty() { + if trimmed.is_empty() || is_noisy_orion_log_line(trimmed) { continue; } + if !wrote_any { + let _ = writeln!(output, "\n─── {} ───", title); + wrote_any = true; + } output.push_str(&format_log_line(trimmed)); output.push('\n'); } } + +/// Drop high-frequency routine lines that drown out useful startup/build output. +fn is_noisy_orion_log_line(line: &str) -> bool { + let lower = line.to_ascii_lowercase(); + lower.contains("sending heartbeat") || lower.contains("orion::ws: sending heartbeat") +} diff --git a/orion-scheduler/src/orion_deployer.rs b/orion-scheduler/src/orion_deployer.rs index c94ca4ede..b2f6895aa 100644 --- a/orion-scheduler/src/orion_deployer.rs +++ b/orion-scheduler/src/orion_deployer.rs @@ -336,6 +336,15 @@ pub async fn get_status_by_domain(state: &AppState, domain: &str) -> Option) -> Option { + let key = domain_or_vm_id?; + if let Some(vm) = state.get_vm_by_domain(key).await { + return Some(vm); + } + state.get_vm_by_id(key).await +} + pub async fn get_scorpio_status( state: &AppState, domain_or_vm_id: Option<&str>, diff --git a/orion/src/antares.rs b/orion/src/antares.rs index cbcd12fc2..4f7d94ae4 100644 --- a/orion/src/antares.rs +++ b/orion/src/antares.rs @@ -165,6 +165,8 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to /// # Arguments /// * `job_id` - Unique identifier for this build job /// * `cl` - Optional changelist layer name + /// * `cl_path` - Optional CL directory under the Buck root (e.g. `project/dagrs-derive`) + /// used to rebase CL-relative `files-list` paths onto the monorepo overlay. /// /// # Returns /// The `AntaresConfig` containing mountpoint and job metadata on success. @@ -172,16 +174,18 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id: &str, repo: &str, cl: Option<&str>, + cl_path: Option<&str>, ) -> Result { tracing::debug!( - "Mounting Antares job: job_id={}, repo={}, cl={:?}", + "Mounting Antares job: job_id={}, repo={}, cl={:?}, cl_path={:?}", job_id, repo, - cl + cl, + cl_path ); if let Some(cl_link) = cl { - return mount_job_with_prepopulated_cl(job_id, repo, cl_link).await; + return mount_job_with_prepopulated_cl(job_id, repo, cl_link, cl_path).await; } let mountpoint = AntaresPaths::from_global_config().mount_root.join(job_id); @@ -227,6 +231,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id: &str, repo: &str, cl_link: &str, + cl_path: Option<&str>, ) -> Result { let manager = get_manager().await?; let paths = AntaresPaths::from_global_config(); @@ -241,6 +246,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id = job_id, repo = repo, cl_link = cl_link, + cl_path = ?cl_path, upper_id = %upper_id, cl_id = %cl_id, upper_dir = %upper_dir.display(), @@ -260,11 +266,12 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id = job_id, repo = repo, cl_link = cl_link, + cl_path = ?cl_path, cl_dir = %cl_dir.display(), "DEBUG: About to populate CL overlay directory" ); - populate_cl_overlay_dir(job_id, repo, cl_link, &cl_dir).await?; + populate_cl_overlay_dir(job_id, repo, cl_link, cl_path, &cl_dir).await?; // DEBUG: Verify CL overlay directory contents after population match tokio::fs::read_dir(&cl_dir).await { @@ -639,14 +646,114 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to Ok(body.data.unwrap_or_default()) } - fn resolve_overlay_relative_path(repo: &str, entry_path: &str) -> Result { + /// Rebase a CL-relative file path onto the Buck root when `cl_path` is a + /// subdirectory of `repo` (same semantics as Mega `rebase_cl_relative_path`). + /// + /// Examples (`repo=/`, `cl_path=/project/dagrs-derive`): + /// - `src/lib.rs` → `project/dagrs-derive/src/lib.rs` + /// - `project/dagrs-derive/BUCK` → unchanged (already prefixed) + fn rebase_cl_relative_path(repo: &str, cl_path: &str, file: &str) -> String { + let repo = repo.trim_matches('/'); + let cl = cl_path.trim_matches('/'); + let file_str = file.trim().trim_start_matches('/').replace('\\', "/"); + + let cl_rel = if repo.is_empty() { + if cl.is_empty() { + return file_str; + } + cl.to_string() + } else if cl == repo { + return file_str; + } else if let Some(stripped) = cl.strip_prefix(&format!("{repo}/")) { + stripped.to_string() + } else { + return file_str; + }; + + if cl_rel.is_empty() { + return file_str; + } + + if file_str == cl_rel || file_str.starts_with(&format!("{cl_rel}/")) { + return file_str; + } + + format!("{cl_rel}/{file_str}") + } + + /// Longest common path-component prefix of `paths`, dropping a trailing + /// component when the prefix equals any full path (i.e. captured a filename). + fn common_dir_prefix<'a, I>(paths: I) -> Option + where + I: IntoIterator, + { + let split: Vec> = paths + .into_iter() + .map(|p| { + p.trim_matches('/') + .split('/') + .filter(|s| !s.is_empty()) + .collect::>() + }) + .filter(|parts| !parts.is_empty()) + .collect(); + if split.is_empty() { + return None; + } + + let mut prefix = split[0].clone(); + for parts in &split[1..] { + let n = prefix + .iter() + .zip(parts.iter()) + .take_while(|(a, b)| a == b) + .count(); + prefix.truncate(n); + if prefix.is_empty() { + return None; + } + } + + // If the LCP is exactly one of the input paths, it included a filename. + while !prefix.is_empty() && split.contains(&prefix) { + prefix.pop(); + } + if prefix.is_empty() { + return None; + } + Some(prefix.join("/")) + } + + /// Infer CL directory for overlay rebase from task change paths. + /// + /// Only when `repo` is the monorepo root (`/` / empty): take the longest + /// common directory prefix of `change_paths` (e.g. `project/dagrs-derive`). + /// For a registered sub-repo, return `None` and let repo-prefix logic apply. + pub fn infer_cl_path_from_changes(repo: &str, change_paths: &[&str]) -> Option { + if !repo.trim_matches('/').is_empty() { + return None; + } + common_dir_prefix(change_paths.iter().copied()) + } + + fn resolve_overlay_relative_path( + repo: &str, + entry_path: &str, + cl_path: Option<&str>, + ) -> Result { + let rebased = match cl_path { + Some(cl) if !cl.trim().is_empty() => rebase_cl_relative_path(repo, cl, entry_path), + _ => entry_path.trim().trim_start_matches('/').replace('\\', "/"), + }; + let repo_prefix = repo.trim_matches('/'); - let trimmed_entry = entry_path.trim().trim_start_matches('/'); + let trimmed_entry = rebased.trim().trim_start_matches('/'); // DEBUG: Log path resolution inputs tracing::debug!( repo = %repo, entry_path = %entry_path, + cl_path = ?cl_path, repo_prefix = %repo_prefix, trimmed_entry = %trimmed_entry, "DEBUG: resolve_overlay_relative_path inputs" @@ -686,7 +793,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to } Err(Box::new(io_other(format!( - "Rejected unsafe CL overlay path: repo={repo}, entry_path={entry_path}, relative={relative}" + "Rejected unsafe CL overlay path: repo={repo}, entry_path={entry_path}, cl_path={cl_path:?}, relative={relative}" )))) } @@ -774,6 +881,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id: &str, repo: &str, cl_link: &str, + cl_path: Option<&str>, cl_dir: &Path, ) -> Result<(), DynError> { if cl_dir.exists() { @@ -805,7 +913,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to let client = http_client()?; let mut applied_paths = Vec::new(); for file in files { - let overlay_path = resolve_overlay_relative_path(repo, &file.path)?; + let overlay_path = resolve_overlay_relative_path(repo, &file.path, cl_path)?; match file.action.as_str() { "new" | "modified" => { download_blob_to_path(&client, &file.sha, &cl_dir.join(&overlay_path)).await?; @@ -837,6 +945,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to job_id = job_id, repo = repo, cl_link = cl_link, + cl_path = ?cl_path, cl_dir = %cl_dir.display(), applied_file_count = applied_paths.len(), applied_files = ?applied_paths, @@ -1090,8 +1199,9 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to use tempfile::tempdir; use super::{ - fusermount_output_indicates_safe_detach, panic_payload_to_string, - remove_mountpoint_path, resolve_overlay_relative_path, run_with_panic_guard, + fusermount_output_indicates_safe_detach, infer_cl_path_from_changes, + panic_payload_to_string, rebase_cl_relative_path, remove_mountpoint_path, + resolve_overlay_relative_path, run_with_panic_guard, }; #[tokio::test] @@ -1117,7 +1227,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to #[test] fn test_resolve_overlay_relative_path_prefixes_repo_relative_path() { - let path = resolve_overlay_relative_path("/project/buck2_test", "src/main.rs") + let path = resolve_overlay_relative_path("/project/buck2_test", "src/main.rs", None) .expect("path should resolve"); assert_eq!(path.to_string_lossy(), "project/buck2_test/src/main.rs"); } @@ -1127,6 +1237,7 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to let path = resolve_overlay_relative_path( "/project/buck2_test", "project/buck2_test/toolchains/BUCK", + None, ) .expect("path should resolve"); assert_eq!(path.to_string_lossy(), "project/buck2_test/toolchains/BUCK"); @@ -1134,7 +1245,62 @@ Hint: set SCORPIO_CONFIG=/path/to/scorpio.toml or create /etc/scorpio/scorpio.to #[test] fn test_resolve_overlay_relative_path_rejects_escape_sequences() { - assert!(resolve_overlay_relative_path("/project/buck2_test", "../etc/passwd").is_err()); + assert!( + resolve_overlay_relative_path("/project/buck2_test", "../etc/passwd", None) + .is_err() + ); + } + + #[test] + fn test_rebase_cl_relative_path_prefixes_monorepo_subdir_cl() { + assert_eq!( + rebase_cl_relative_path("/", "/project/dagrs-derive", "src/lib.rs"), + "project/dagrs-derive/src/lib.rs" + ); + assert_eq!( + rebase_cl_relative_path("/", "/project/dagrs-derive", "project/dagrs-derive/BUCK"), + "project/dagrs-derive/BUCK" + ); + } + + #[test] + fn test_resolve_overlay_rebases_cl_path_under_monorepo_root() { + let path = + resolve_overlay_relative_path("/", "src/lib.rs", Some("project/dagrs-derive")) + .expect("path should resolve"); + assert_eq!(path.to_string_lossy(), "project/dagrs-derive/src/lib.rs"); + } + + #[test] + fn test_resolve_overlay_no_double_prefix_when_already_rebased() { + let path = resolve_overlay_relative_path( + "/", + "project/dagrs-derive/BUCK", + Some("/project/dagrs-derive"), + ) + .expect("path should resolve"); + assert_eq!(path.to_string_lossy(), "project/dagrs-derive/BUCK"); + } + + #[test] + fn test_infer_cl_path_from_changes_monorepo_root_only() { + let changes = [ + "project/dagrs-derive/BUCK", + "project/dagrs-derive/src/lib.rs", + "project/dagrs-derive/Cargo.toml", + ]; + assert_eq!( + infer_cl_path_from_changes("/", &changes).as_deref(), + Some("project/dagrs-derive") + ); + assert_eq!( + infer_cl_path_from_changes("", &changes).as_deref(), + Some("project/dagrs-derive") + ); + assert_eq!( + infer_cl_path_from_changes("/project/buck2_test", &["src/main.rs"]).as_deref(), + None + ); } #[tokio::test] @@ -1198,12 +1364,18 @@ mod imp { _job_id: &str, _repo: &str, _cl: Option<&str>, + _cl_path: Option<&str>, ) -> Result { Err(Box::new(std::io::Error::other( "Antares/scorpiofs is only supported on Linux", ))) } + /// Infer CL directory for overlay rebase (stub: always `None` off Linux). + pub fn infer_cl_path_from_changes(_repo: &str, _change_paths: &[&str]) -> Option { + None + } + /// Unmounting Antares requires `scorpiofs` (Linux-only in this repository). #[allow(dead_code)] pub async fn unmount_job(_job_id: &str) -> Result, DynError> { diff --git a/orion/src/buck_controller.rs b/orion/src/buck_controller.rs index 556b4fe26..e640f0830 100644 --- a/orion/src/buck_controller.rs +++ b/orion/src/buck_controller.rs @@ -200,6 +200,7 @@ async fn unmount_discovery_old_mount(task_id: &str, mounts: &mut AntaresMountPai /// # Arguments /// * `job_id` - Unique identifier for this build job /// * `cl` - Optional changelist layer name +/// * `cl_path` - Optional CL directory under the Buck root for overlay path rebase /// /// # Returns /// Returns a tuple `(mountpoint, mount_id)` on success. @@ -207,15 +208,17 @@ pub async fn mount_antares_fs( job_id: &str, repo: &str, cl: Option<&str>, + cl_path: Option<&str>, ) -> Result<(String, String), Box> { tracing::debug!( - "Preparing to mount Antares FS: job_id={}, repo={}, cl={:?}", + "Preparing to mount Antares FS: job_id={}, repo={}, cl={:?}, cl_path={:?}", job_id, repo, - cl + cl, + cl_path ); - let config = crate::antares::mount_job(job_id, repo, cl).await?; + let config = crate::antares::mount_job(job_id, repo, cl, cl_path).await?; let mountpoint = config.mountpoint.to_string_lossy().to_string(); let mount_id = config.job_id.clone(); @@ -1705,7 +1708,7 @@ pub async fn build( // naturally get separate daemons without needing `--isolation-dir`. let id_for_old_repo = format!("{id}-old-{attempt}"); let (old_repo_mount_point, _mount_id_old_repo) = - match mount_antares_fs(&id_for_old_repo, &repo, None).await { + match mount_antares_fs(&id_for_old_repo, &repo, None, None).await { Ok(mount) => mount, Err(err) => { cleanup_antares_mount( @@ -1719,28 +1722,43 @@ pub async fn build( } }; + let change_paths: Vec<&str> = changes.iter().map(|c| c.get().as_str()).collect(); + let inferred_cl_path = crate::antares::infer_cl_path_from_changes(&repo, &change_paths); + if let Some(ref cl_path) = inferred_cl_path { + tracing::info!( + cl_path = %cl_path, + "Inferred CL overlay path prefix from task changes (monorepo root)." + ); + } + let id_for_repo = format!("{id}-{attempt}"); - let (repo_mount_point, _mount_id) = - match mount_antares_fs(&id_for_repo, &repo, cl_arg).await { - Ok(mount) => mount, - Err(err) => { - cleanup_antares_mount( - &id, - &id_for_old_repo, - Some(&old_repo_mount_point), - "cleanup old-repo mount after failed new-repo mount", - ) - .await; - cleanup_antares_mount( - &id, - &id_for_repo, - None, - "cleanup after failed new-repo mount", - ) - .await; - return Err(err); - } - }; + let (repo_mount_point, _mount_id) = match mount_antares_fs( + &id_for_repo, + &repo, + cl_arg, + inferred_cl_path.as_deref(), + ) + .await + { + Ok(mount) => mount, + Err(err) => { + cleanup_antares_mount( + &id, + &id_for_old_repo, + Some(&old_repo_mount_point), + "cleanup old-repo mount after failed new-repo mount", + ) + .await; + cleanup_antares_mount( + &id, + &id_for_repo, + None, + "cleanup after failed new-repo mount", + ) + .await; + return Err(err); + } + }; let attempt_mounts = AntaresMountPair { old_mount_id: id_for_old_repo, diff --git a/orion/src/ws.rs b/orion/src/ws.rs index ebce6e57d..cc38aab32 100644 --- a/orion/src/ws.rs +++ b/orion/src/ws.rs @@ -296,7 +296,7 @@ async fn handle_connection( tokio::select! { biased; _ = heartbeat_interval.tick() => { - tracing::info!("Sending heartbeat..."); + tracing::debug!("Sending heartbeat..."); match serde_json::to_string(&WSMessage::Heartbeat) { Ok(payload) => { if let Err(e) = ws_sender.send(Message::Text(payload.into())).await { diff --git a/scripts/demo/build-demo-images-local.sh b/scripts/demo/build-demo-images-local.sh index 6f3f7df7a..b90795c83 100644 --- a/scripts/demo/build-demo-images-local.sh +++ b/scripts/demo/build-demo-images-local.sh @@ -6,12 +6,13 @@ set -euo pipefail # ============================================================================ # This script builds Docker images for the demo environment on your local machine. # The script automatically detects the platform (arm64/amd64) based on your machine. -# By default, images are only built locally. Use --push to push to AWS ECR. +# By default, images are only built locally. Use --push to push to AWS ECR and Harbor. # # Prerequisites: # - Docker Desktop with Buildx enabled # - (Optional) AWS CLI configured with credentials (only needed for --push) # - (Optional) Access to Amazon ECR Public (only needed for --push) +# - (Optional) docker login registry.xuanwu.openatom.cn (only needed for --push) # - QEMU (usually auto-installed by Docker Desktop) # # Usage: @@ -25,8 +26,8 @@ set -euo pipefail # ./scripts/demo/build-demo-images-local.sh --prune-buildkit # Build and prune buildkit cache after build # # If IMAGE_NAME is provided, only that image will be built. -# Otherwise, all 4 images will be built. -# Use --push flag to push images to AWS ECR (requires AWS credentials). +# Otherwise, all images will be built. +# Use --push to push to ECR Public and Harbor (registry.xuanwu.openatom.cn). # ============================================================================ # Colors for output @@ -39,6 +40,7 @@ NC='\033[0m' # No Color REGISTRY_ALIAS="m8q5m4u3" REPOSITORY="mega" REGISTRY="public.ecr.aws" +HARBOR_REGISTRY="${HARBOR_REGISTRY:-registry.xuanwu.openatom.cn}" SHOULD_PUSH=false # Default: only build, don't push SHOULD_PRUNE_BUILDKIT=false # Optional: prune buildkit cache after build BUILDX_BUILDER_ARGS=() @@ -315,6 +317,7 @@ build_and_push() { fi local image_tag_with_arch="${image_tag}-${arch_suffix}" local image_repo="${REGISTRY}/${REGISTRY_ALIAS}/${REPOSITORY}/${image_name}" + local harbor_repo="${HARBOR_REGISTRY}/${REPOSITORY}/${image_name}" # Verify paths exist (use absolute paths) local full_dockerfile="${REPO_ROOT}/${dockerfile_path}" @@ -395,6 +398,7 @@ build_and_push() { --platform "${TARGET_PLATFORMS}" --file "${dockerfile_path}" --tag "${image_repo}:${image_tag_with_arch}" + --tag "${harbor_repo}:${image_tag_with_arch}" --progress=auto --build-arg BUILDKIT_INLINE_CACHE=1 ) @@ -428,14 +432,21 @@ build_and_push() { # Push if requested (ensures single-arch manifest is uploaded) if [ "$SHOULD_PUSH" = "true" ]; then if ! docker push "${image_repo}:${image_tag_with_arch}"; then - log_error "Failed to push ${image_name}" + log_error "Failed to push ${image_name} to ECR" + return 1 + fi + if ! docker push "${harbor_repo}:${image_tag_with_arch}"; then + log_error "Failed to push ${image_name} to Harbor (${harbor_repo})" + log_error "Login first: docker login ${HARBOR_REGISTRY}" return 1 fi log_info "${image_name} built and pushed successfully ✓" - log_info " Image: ${image_repo}:${image_tag_with_arch}" + log_info " ECR: ${image_repo}:${image_tag_with_arch}" + log_info " Harbor: ${harbor_repo}:${image_tag_with_arch}" else log_info "${image_name} built successfully ✓" log_info " Image: ${image_repo}:${image_tag_with_arch} (local only)" + log_info " Harbor tag also applied: ${harbor_repo}:${image_tag_with_arch}" fi return 0 } diff --git a/scripts/init_mega/README.md b/scripts/init_mega/README.md index 259e7ec4f..f3d018583 100644 --- a/scripts/init_mega/README.md +++ b/scripts/init_mega/README.md @@ -73,12 +73,13 @@ In a temporary directory, it performs: 2. Configure the commit identity (repo-local): - `git config user.email mega-bot@example.com` - `git config user.name Mega Bot` + - `git config commit.gpgsign false` (and `git commit --no-gpg-sign`) so a global `commit.gpgsign=true` does not fail without a Mega Bot GPG key 3. Clone `buckal-bundles` inside the `toolchains` repo: - `git clone --depth 1 https://github.com/buck2hub/buckal-bundles.git` 4. Remove `buckal-bundles/.git` so it becomes a regular directory tracked by `toolchains` (vendoring). 5. Commit and push: - `git add .` - - `git commit -m "import buckal-bundles"` + - `git commit --no-gpg-sign -m "import buckal-bundles"` (skipped if already imported / no changes) - `git push` 6. Use Mega APIs to find and merge the corresponding CL: - `POST {base_url}/api/v1/cl/list` (paginate open CLs and match `title == "import buckal-bundles"`) diff --git a/scripts/init_mega/init_mega.py b/scripts/init_mega/init_mega.py index 6cf6640cd..4447d5444 100644 --- a/scripts/init_mega/init_mega.py +++ b/scripts/init_mega/init_mega.py @@ -53,8 +53,11 @@ def api_request(method, url, data=None, headers=None): return json.loads(resp_body) if resp_body else {} else: raise RuntimeError(f"API request failed with status {response.status}: {resp_body}") + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", errors="replace") + raise RuntimeError(f"API request to {url} failed: HTTP Error {e.code}: {e.reason}; body={body}") from e except Exception as e: - raise RuntimeError(f"API request to {url} failed: {e}") + raise RuntimeError(f"API request to {url} failed: {e}") from e def wait_for_server(base_url, timeout=60): """Waits for the Mega server to be ready.""" @@ -121,7 +124,18 @@ def merge_cl(base_url, link, timeout=60): else: print(f"Merge pending: {resp.get('err_message')}") except Exception as e: - print(f"Merge attempt failed: {e}") + # urllib HTTPError body often has the real reason (e.g. CL ref not found). + detail = "" + if hasattr(e, "__cause__") and e.__cause__ is not None: + cause = e.__cause__ + if hasattr(cause, "read"): + try: + detail = cause.read().decode("utf-8", errors="replace") + except Exception: + detail = str(cause) + else: + detail = str(cause) + print(f"Merge attempt failed: {e}" + (f" body={detail}" if detail else "")) time.sleep(2) @@ -139,9 +153,11 @@ def run_buckal_bundles_workflow(base_url): toolchains_dir = temp_path / "toolchains" - # Config git + # Config git (repo-local). Disable GPG so CI/dev machines with + # commit.gpgsign=true globally do not fail without a Mega Bot key. run_git(toolchains_dir, ["config", "user.email", GIT_USER_EMAIL]) run_git(toolchains_dir, ["config", "user.name", GIT_USER_NAME]) + run_git(toolchains_dir, ["config", "commit.gpgsign", "false"]) # Clone buckal-bundles inside print("Importing buckal-bundles...") @@ -157,7 +173,12 @@ def run_buckal_bundles_workflow(base_url): # Commit and push run_git(toolchains_dir, ["add", "."]) - run_git(toolchains_dir, ["commit", "-m", COMMIT_MSG]) + status = run_git(toolchains_dir, ["status", "--porcelain"], check=False) + if not status.stdout.strip(): + print("buckal-bundles already present with no changes; skipping commit/push/merge.") + return + + run_git(toolchains_dir, ["commit", "--no-gpg-sign", "-m", COMMIT_MSG]) run_git(toolchains_dir, ["push"]) # Handle merge request diff --git a/tmp/gha-cache-smoke/Dockerfile b/tmp/gha-cache-smoke/Dockerfile new file mode 100644 index 000000000..30994913a --- /dev/null +++ b/tmp/gha-cache-smoke/Dockerfile @@ -0,0 +1,8 @@ +# Tiny multi-layer image so BuildKit can show CACHED hits on re-import. +FROM alpine:3.20 + +RUN apk add --no-cache curl + +COPY hello.txt /hello.txt + +RUN cat /hello.txt && echo "gha-cache-smoke ok" \ No newline at end of file diff --git a/tmp/gha-cache-smoke/hello.txt b/tmp/gha-cache-smoke/hello.txt new file mode 100644 index 000000000..c70720339 --- /dev/null +++ b/tmp/gha-cache-smoke/hello.txt @@ -0,0 +1 @@ +hello from gha-cache-smoke From b6b8e85046dc4f7361856a7f2a88c714fcb8f301 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 24 Jul 2026 11:19:42 +0800 Subject: [PATCH 7/7] fix ui fmt --- .../web/components/CodeView/BlobView/BlobEditor.tsx | 9 ++------- .../web/components/GithubLoginRequiredDialog.tsx | 12 ++---------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/moon/apps/web/components/CodeView/BlobView/BlobEditor.tsx b/moon/apps/web/components/CodeView/BlobView/BlobEditor.tsx index 552bf2965..01d60745c 100644 --- a/moon/apps/web/components/CodeView/BlobView/BlobEditor.tsx +++ b/moon/apps/web/components/CodeView/BlobView/BlobEditor.tsx @@ -132,9 +132,7 @@ export default function BlobEditor({ fileContent, filePath, fileName, onCancel } return } - const destinationPath = pathSegments.length - ? `${pathSegments.join('/')}/${trimmedName}` - : trimmedName + const destinationPath = pathSegments.length ? `${pathSegments.join('/')}/${trimmedName}` : trimmedName try { await updateBlobMutation.mutateAsync({ @@ -152,10 +150,7 @@ export default function BlobEditor({ fileContent, filePath, fileName, onCancel } setIsCommitDialogOpen(false) onCancel() } catch (error: any) { - const msg = - error?.message || - error?.response?.data?.message || - 'Submit failed. Please try again.' + const msg = error?.message || error?.response?.data?.message || 'Submit failed. Please try again.' toast.error(msg) } diff --git a/moon/apps/web/components/GithubLoginRequiredDialog.tsx b/moon/apps/web/components/GithubLoginRequiredDialog.tsx index c648c23ff..229229b9d 100644 --- a/moon/apps/web/components/GithubLoginRequiredDialog.tsx +++ b/moon/apps/web/components/GithubLoginRequiredDialog.tsx @@ -9,11 +9,7 @@ import { useSignoutUser } from '@/hooks/useSignoutUser' const DISMISS_PREFIX = 'github-login-required-dismissed:' -function needsGithubRelogin(user: { - github_login?: string | null - integration?: boolean - system?: boolean -}): boolean { +function needsGithubRelogin(user: { github_login?: string | null; integration?: boolean; system?: boolean }): boolean { if (user.integration || user.system) return false return !user.github_login } @@ -92,11 +88,7 @@ export function GithubLoginRequiredDialog() { -