From e57f7c0ace647da9be0bc2c975fb16cb927e39e9 Mon Sep 17 00:00:00 2001 From: "Thanh-Giang (River) Tan Nguyen" Date: Mon, 29 Dec 2025 21:52:01 +0700 Subject: [PATCH 1/5] fix: check for pixi before installing --- backend/app/utils/executor/ssh.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/app/utils/executor/ssh.py b/backend/app/utils/executor/ssh.py index 918cfb5..7eac29d 100644 --- a/backend/app/utils/executor/ssh.py +++ b/backend/app/utils/executor/ssh.py @@ -503,7 +503,12 @@ def _generate_script(self, server: str, job: WebJob, params: dict) -> str: mkdir -p $NXF_SINGULARITY_CACHEDIR # === Install pixi === -which pixi || curl -fsSL https://pixi.sh/install.sh | sh +if [ ! -f "$HOME/.pixi/bin/pixi" ]; then + curl -fsSL https://pixi.sh/install.sh | sh +else + echo "Pixi already installed." +fi + export PATH=$PATH:$HOME/.pixi/bin # Only append channels if not already present if ! pixi config get default-channels --global | grep -q bioconda; then From 1bfd9b59fe46d852076f83033607b2edb60fe639 Mon Sep 17 00:00:00 2001 From: "Thanh-Giang (River) Tan Nguyen" Date: Mon, 29 Dec 2025 23:22:45 +0700 Subject: [PATCH 2/5] fix: update docker images, fix cffi --- backend/Dockerfile | 7 +------ backend/requirements.txt | 3 ++- backend/run_server.sh | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 71deeb3..73b512c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,16 +1,12 @@ FROM pypy:3.11-slim - ENV PYTHONUNBUFFERED=1 WORKDIR /app - RUN apt-get update && apt-get install -y \ - build-essential pkg-config \ + build-essential pkg-config libuv1 \ libffi-dev libssl-dev libsodium-dev make libpq-dev \ && rm -rf /var/lib/apt/lists/* - COPY requirements.txt . RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt - COPY app ./app COPY domain ./domain COPY migrations ./migrations @@ -18,7 +14,6 @@ COPY pyproject.toml . COPY settings.yaml . COPY run_server.sh ./ RUN chmod +x run_server.sh -RUN apt-get update && apt-get install -y libuv1 EXPOSE 8000 RUN useradd -m appuser diff --git a/backend/requirements.txt b/backend/requirements.txt index ed5fe02..488ca76 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,4 +18,5 @@ celery==5.5.3 socketify==0.0.31 psycopg[binary]==3.3.2 psycopg-pool==3.3.0 -anyio==4.12.0 \ No newline at end of file +anyio==4.12.0 +cffi==1.18.0.dev0 \ No newline at end of file diff --git a/backend/run_server.sh b/backend/run_server.sh index 28917fb..b4fa7a3 100644 --- a/backend/run_server.sh +++ b/backend/run_server.sh @@ -13,4 +13,4 @@ echo "Starting BlackSheep app..." pypy -m socketify app.main:app \ --host 0.0.0.0 \ --port 8000 \ - --workers "$(nproc --all)" \ No newline at end of file + --workers 2 \ No newline at end of file From a4d7e7b0b478f05048bc95ff297e331d0f302dee Mon Sep 17 00:00:00 2001 From: "Thanh-Giang (River) Tan Nguyen" Date: Tue, 30 Dec 2025 20:17:36 +0700 Subject: [PATCH 3/5] feat: add download cmd for bombardier --- backend/perf/install.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/perf/install.sh b/backend/perf/install.sh index d1db931..580098d 100644 --- a/backend/perf/install.sh +++ b/backend/perf/install.sh @@ -12,7 +12,11 @@ tar xf pypy3.11-v7.3.20-linux64.tar.bz2 ./pypy3.11-v7.3.20-linux64/bin/pypy -mpip install -U pip wheel ./pypy3.11-v7.3.20-linux64/bin/pypy -mpip install -r requirements.txt +# download for bombardier +wget https://github.com/codesenberg/bombardier/releases/download/v2.0.2/bombardier-linux-amd64 +chmod +x bombardier-linux-amd64 +./bombardier-linux-amd64 -c 2000 -d 10s -l http://river-backend:8000/api/health # pypy3.11 socketify ./pypy3.11-v7.3.20-linux64/bin/pypy -m socketify app:app --worker 2 --port 8000 From c900656182c1a60b55bd62ad057eb9cf0b9cdf28 Mon Sep 17 00:00:00 2001 From: "Thanh-Giang (River) Tan Nguyen" Date: Tue, 30 Dec 2025 21:04:39 +0700 Subject: [PATCH 4/5] fix: fix response for pagination --- backend/app/service_analysis/controller.py | 2 +- .../service_analysis/tests/test_analysis.py | 18 +++++++------- backend/app/service_job/controller.py | 2 +- .../app/service_job/tests/test_api_web_job.py | 10 ++++---- .../app/service_organization/controller.py | 2 +- .../tests/test_organization.py | 14 +++++------ backend/app/service_project/controller.py | 2 +- .../app/service_project/tests/test_project.py | 24 +++++++++---------- .../features/project/components/Projects.tsx | 2 +- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/backend/app/service_analysis/controller.py b/backend/app/service_analysis/controller.py index f4858fa..fd20c1b 100644 --- a/backend/app/service_analysis/controller.py +++ b/backend/app/service_analysis/controller.py @@ -95,7 +95,7 @@ async def get_analysis( return ok( { - "item": result, + "items": result, "page": page, "page_size": page_size, "total": total_count, diff --git a/backend/app/service_analysis/tests/test_analysis.py b/backend/app/service_analysis/tests/test_analysis.py index aa137ee..88eeb9c 100644 --- a/backend/app/service_analysis/tests/test_analysis.py +++ b/backend/app/service_analysis/tests/test_analysis.py @@ -51,13 +51,13 @@ async def test_basic_analysis( # list instance as admin res_admin = await list_instance(test_client, f"{BASE_URL}/", cookies_admin, 200) - assert len(res_admin["item"]) == 1 - assert_instance_fields(res_admin["item"][0], EXPECTED_FIELDS) + assert len(res_admin["items"]) == 1 + assert_instance_fields(res_admin["items"][0], EXPECTED_FIELDS) # list instance as contributor res_contrib = await list_instance(test_client, f"{BASE_URL}/", cookies_contributor, 200) - assert len(res_contrib["item"]) == 1 - assert_instance_fields(res_contrib["item"][0], EXPECTED_FIELDS) + assert len(res_contrib["items"]) == 1 + assert_instance_fields(res_contrib["items"][0], EXPECTED_FIELDS) # delete as admin (should fail) delete_url = f"{BASE_URL}/{res_admin['item'][0]['id']}/" @@ -93,23 +93,23 @@ async def get_page(page, page_size): # page 1 data = await get_page(1, 10) assert data["page"] == 1 - assert len(data["item"]) == 10 + assert len(data["items"]) == 10 # page 2 data = await get_page(2, 10) assert data["page"] == 2 - assert len(data["item"]) == 10 + assert len(data["items"]) == 10 # last page data = await get_page(4, 10) assert data["page"] == 4 - assert len(data["item"]) == 0 + assert len(data["items"]) == 0 # too large page data = await get_page(99, 10) - assert data["item"] == [] + assert data["items"] == [] # search by url res = await test_client.get(f"{BASE_URL}/", query={"search": "repo-1"}, cookies=cookies_admin) data = await res.json() assert res.status == 200 - assert any("repo-1" in item["url"] for item in data["item"]) + assert any("repo-1" in item["url"] for item in data["items"]) diff --git a/backend/app/service_job/controller.py b/backend/app/service_job/controller.py index 3947c13..32e5d60 100644 --- a/backend/app/service_job/controller.py +++ b/backend/app/service_job/controller.py @@ -149,7 +149,7 @@ async def list_web_job( return ok( { - "item": result, + "items": result, "page": page, "page_size": page_size, "total": total_count, diff --git a/backend/app/service_job/tests/test_api_web_job.py b/backend/app/service_job/tests/test_api_web_job.py index 94a5a8b..1e078ff 100644 --- a/backend/app/service_job/tests/test_api_web_job.py +++ b/backend/app/service_job/tests/test_api_web_job.py @@ -80,8 +80,8 @@ async def test_create_list_job( ) assert res.status == 200 result = await res.json() - assert "item" in result - assert len(result["item"]) == 5 + assert "items" in result + assert len(result["items"]) == 5 assert result["total"] >= 15 # List jobs with pagination (page=3, page_size=5) @@ -92,8 +92,8 @@ async def test_create_list_job( ) assert res.status == 200 result = await res.json() - assert "item" in result - assert len(result["item"]) == 5 + assert "items" in result + assert len(result["items"]) == 5 # Search for a specific job by name search_name = "customer-03" @@ -104,7 +104,7 @@ async def test_create_list_job( ) assert res.status == 200 result = await res.json() - assert any(job["name"] == search_name for job in result["item"]) + assert any(job["name"] == search_name for job in result["items"]) # get log of current running job # TODO: test job log diff --git a/backend/app/service_organization/controller.py b/backend/app/service_organization/controller.py index 67638d6..3842d0b 100644 --- a/backend/app/service_organization/controller.py +++ b/backend/app/service_organization/controller.py @@ -69,7 +69,7 @@ async def list_organization( return ok( { - "item": result, + "items": result, "page": page, "page_size": page_size, "total": total_count, diff --git a/backend/app/service_organization/tests/test_organization.py b/backend/app/service_organization/tests/test_organization.py index 96a59dd..7fe2038 100644 --- a/backend/app/service_organization/tests/test_organization.py +++ b/backend/app/service_organization/tests/test_organization.py @@ -45,8 +45,8 @@ async def check_page(page, expected_count): res_data = await res.json() assert res.status == 200 assert res_data["page"] == page - assert len(res_data["item"]) == expected_count - for item in res_data["item"]: + assert len(res_data["items"]) == expected_count + for item in res_data["items"]: assert_instance_fields(item, expected_fields) await check_page(1, 10) @@ -58,19 +58,19 @@ async def check_page(page, expected_count): res_data = await res.json() assert res.status == 200 assert res_data["total"] == 1 - assert "29" in res_data["item"][0]["name"] + assert "29" in res_data["items"][0]["name"] # search by description res = await test_client.get(f"{BASE_URL}", query={"search": "Description-1"}, cookies=cookies_org_manager) res_data = await res.json() assert res.status == 200 assert res_data["total"] == 11 - assert len(res_data["item"]) == 11 - for item in res_data["item"]: + assert len(res_data["items"]) == 11 + for item in res_data["items"]: assert_instance_fields(item, expected_fields) # Update - update_org = res_data["item"][0] + update_org = res_data["items"][0] updated_org = await update_instance( test_client, f"{BASE_URL}/{update_org['id']}/", @@ -99,4 +99,4 @@ async def check_page(page, expected_count): cookies=cookies_org_manager, ) res_data = await res.json() - assert len(res_data["item"]) == 29 + assert len(res_data["items"]) == 29 diff --git a/backend/app/service_project/controller.py b/backend/app/service_project/controller.py index e3aac61..4d300d8 100644 --- a/backend/app/service_project/controller.py +++ b/backend/app/service_project/controller.py @@ -148,7 +148,7 @@ async def list_projects( return ok( { - "item": result, + "items": result, "page": page, "page_size": page_size, "total": total_count, diff --git a/backend/app/service_project/tests/test_project.py b/backend/app/service_project/tests/test_project.py index 6d0c08a..6117db2 100644 --- a/backend/app/service_project/tests/test_project.py +++ b/backend/app/service_project/tests/test_project.py @@ -184,8 +184,8 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["page"] == 1 - assert len(res_data["item"]) == 5 - for item in res_data["item"]: + assert len(res_data["items"]) == 5 + for item in res_data["items"]: assert_instance_fields(item, expected_fields) # second page @@ -193,8 +193,8 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["page"] == 2 - assert len(res_data["item"]) == 3 - for item in res_data["item"]: + assert len(res_data["items"]) == 3 + for item in res_data["items"]: assert_instance_fields(item, expected_fields) # Search name @@ -203,7 +203,7 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["total"] == 2 - assert "public project 1" in res_data["item"][0]["name"] + assert "public project 1" in res_data["items"][0]["name"] # failed due to invalid format filters res = await test_client.get( @@ -233,7 +233,7 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["total"] == 2 - assert "public project 1" in res_data["item"][0]["name"] + assert "public project 1" in res_data["items"][0]["name"] # Search by role res = await test_client.get( @@ -265,8 +265,8 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["total"] == 8 - assert len(res_data["item"]) == 5 - for item in res_data["item"]: + assert len(res_data["items"]) == 5 + for item in res_data["items"]: assert_instance_fields(item, expected_fields) # Get by desc sorting (asc by default) @@ -283,13 +283,13 @@ async def test_project_CRUD_and_search( res_data = await res.json() assert res.status == 200 assert res_data["total"] == 8 - assert len(res_data["item"]) == 5 - for item in res_data["item"]: + assert len(res_data["items"]) == 5 + for item in res_data["items"]: assert_instance_fields(item, expected_fields) - assert res_data["item"][0]["name"] == "user public project 1" # this one if not sorting will be last + assert res_data["items"][0]["name"] == "user public project 1" # this one if not sorting will be last # Test RBAC for update/delete - project_id = res_data["item"][0]["id"] + project_id = res_data["items"][0]["id"] update_data = {"name": "Edited name", "description": "Edited description"} for cookies in [cookies_contributor, cookies_viewer]: diff --git a/frontend/src/features/project/components/Projects.tsx b/frontend/src/features/project/components/Projects.tsx index b6d55a1..8bae452 100755 --- a/frontend/src/features/project/components/Projects.tsx +++ b/frontend/src/features/project/components/Projects.tsx @@ -45,7 +45,7 @@ const Projects: React.FC = () => { const [sorting, setSorting] = useState([]); const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 5, + pageSize: 10, }); // Fetch projects From 17ea501a575be962aa7344c6e92c8cadaa41e2ad Mon Sep 17 00:00:00 2001 From: "Thanh-Giang (River) Tan Nguyen" Date: Tue, 30 Dec 2025 21:31:30 +0700 Subject: [PATCH 5/5] fix: fix update for credential github --- frontend/src/features/credential/contexts/CredentialContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/features/credential/contexts/CredentialContext.tsx b/frontend/src/features/credential/contexts/CredentialContext.tsx index 9009f07..5f0e905 100755 --- a/frontend/src/features/credential/contexts/CredentialContext.tsx +++ b/frontend/src/features/credential/contexts/CredentialContext.tsx @@ -137,6 +137,7 @@ export const CredentialContextProvider: React.FC<{ children: ReactNode }> = ({ case CredentialTypeEnum.GITHUB: if ( existing.type === CredentialTypeEnum.GITHUB + && existing.username ) { update = true; }