From df0dddde8e9dd0bae342f10719a722e95e8f2ff6 Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Thu, 9 Jul 2026 14:42:37 +0800
Subject: [PATCH 001/180] ci: add release promotion automation
---
.github/workflows/pack-bundle.yml | 4 +-
.github/workflows/promote-develop-to-main.yml | 142 ++++++++++++++++++
.github/workflows/release-please.yml | 65 ++++++++
3 files changed, 209 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/promote-develop-to-main.yml
create mode 100644 .github/workflows/release-please.yml
diff --git a/.github/workflows/pack-bundle.yml b/.github/workflows/pack-bundle.yml
index 2d80de1a..96a2557f 100644
--- a/.github/workflows/pack-bundle.yml
+++ b/.github/workflows/pack-bundle.yml
@@ -157,8 +157,8 @@ jobs:
BUNDLE=$(ls auplc-bundle-*.tar.gz)
TAG="${{ github.event.workflow_run.head_branch }}"
- # Upload to the existing release. Releases are created manually with
- # proper release notes before tagging; CI only attaches the bundle.
+ # Upload to the release created by the release workflow. If the
+ # release is not available yet, keep the bundle artifact for retry.
if gh release view "${TAG}" &>/dev/null; then
gh release upload "${TAG}" "${BUNDLE}" --clobber
echo "Bundle uploaded to release ${TAG}"
diff --git a/.github/workflows/promote-develop-to-main.yml b/.github/workflows/promote-develop-to-main.yml
new file mode 100644
index 00000000..13bbc6de
--- /dev/null
+++ b/.github/workflows/promote-develop-to-main.yml
@@ -0,0 +1,142 @@
+# Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved.
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+name: Promote Develop to Main
+
+on:
+ workflow_dispatch:
+ inputs:
+ head_branch:
+ description: 'Integration branch to promote'
+ required: true
+ default: develop
+ type: string
+ base_branch:
+ description: 'Release branch that receives the promotion PR'
+ required: true
+ default: main
+ type: string
+ enable_auto_merge:
+ description: 'Enable auto-merge for the promotion PR after checks pass'
+ required: true
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: write
+
+concurrency:
+ group: promote-${{ inputs.head_branch }}-to-${{ inputs.base_branch }}
+ cancel-in-progress: false
+
+jobs:
+ open-promotion-pr:
+ name: Open promotion PR
+ runs-on: ubuntu-latest
+ env:
+ GH_TOKEN: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }}
+ BASE_BRANCH: ${{ inputs.base_branch }}
+ HEAD_BRANCH: ${{ inputs.head_branch }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Create or reuse promotion PR
+ id: promote
+ run: |
+ set -euo pipefail
+
+ git fetch --no-tags origin "${BASE_BRANCH}" "${HEAD_BRANCH}"
+
+ ahead=$(git rev-list --count "origin/${BASE_BRANCH}..origin/${HEAD_BRANCH}")
+ behind=$(git rev-list --count "origin/${HEAD_BRANCH}..origin/${BASE_BRANCH}")
+
+ echo "ahead=${ahead}" >> "${GITHUB_OUTPUT}"
+ echo "behind=${behind}" >> "${GITHUB_OUTPUT}"
+
+ if [[ "${ahead}" == "0" ]]; then
+ echo "${HEAD_BRANCH} has no commits to promote into ${BASE_BRANCH}."
+ echo "pr_url=" >> "${GITHUB_OUTPUT}"
+ exit 0
+ fi
+
+ existing_pr=$(
+ gh pr list \
+ --base "${BASE_BRANCH}" \
+ --head "${HEAD_BRANCH}" \
+ --state open \
+ --json url \
+ --jq '.[0].url // ""'
+ )
+
+ if [[ -n "${existing_pr}" ]]; then
+ echo "Reusing existing promotion PR: ${existing_pr}"
+ echo "pr_url=${existing_pr}" >> "${GITHUB_OUTPUT}"
+ gh pr comment "${existing_pr}" --body \
+ "Promotion check refreshed: ${HEAD_BRANCH} is ${ahead} commit(s) ahead of ${BASE_BRANCH} and ${behind} commit(s) behind."
+ exit 0
+ fi
+
+ body_file=$(mktemp)
+ cat > "${body_file}" <
> "${GITHUB_OUTPUT}"
+
+ - name: Enable auto-merge
+ if: inputs.enable_auto_merge && steps.promote.outputs.pr_url != ''
+ run: gh pr merge --auto --merge "${{ steps.promote.outputs.pr_url }}"
+
+ - name: Summarize promotion
+ run: |
+ {
+ echo "## Promotion summary"
+ echo
+ echo "- Head branch: \`${HEAD_BRANCH}\`"
+ echo "- Base branch: \`${BASE_BRANCH}\`"
+ echo "- Commits ahead: \`${{ steps.promote.outputs.ahead }}\`"
+ echo "- Commits behind: \`${{ steps.promote.outputs.behind }}\`"
+ if [[ -n "${{ steps.promote.outputs.pr_url }}" ]]; then
+ echo "- Promotion PR: ${{ steps.promote.outputs.pr_url }}"
+ else
+ echo "- Promotion PR: not created because there are no commits to promote"
+ fi
+ } >> "${GITHUB_STEP_SUMMARY}"
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 00000000..02451ad6
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,65 @@
+# Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved.
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+name: Release Please
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: release-please-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ release-please:
+ name: Prepare or publish release
+ runs-on: ubuntu-latest
+ outputs:
+ release_created: ${{ steps.release.outputs.release_created }}
+ tag_name: ${{ steps.release.outputs.tag_name }}
+ version: ${{ steps.release.outputs.version }}
+ steps:
+ - name: Run release-please
+ id: release
+ uses: googleapis/release-please-action@v4
+ with:
+ token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }}
+ target-branch: main
+ release-type: simple
+
+ - name: Summarize release-please result
+ run: |
+ {
+ echo "## Release Please summary"
+ echo
+ echo "- Release created: \`${{ steps.release.outputs.release_created || 'false' }}\`"
+ echo "- Tag: \`${{ steps.release.outputs.tag_name || 'not created' }}\`"
+ echo "- Version: \`${{ steps.release.outputs.version || 'not created' }}\`"
+ echo
+ echo "Use a repository secret named \`RELEASE_AUTOMATION_TOKEN\` from a"
+ echo "GitHub App or fine-grained PAT if tag or release events must"
+ echo "trigger downstream workflows."
+ } >> "${GITHUB_STEP_SUMMARY}"
From 4efa5c66134b003aaf077d621b2904a1b3d9347f Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Thu, 9 Jul 2026 17:30:39 +0800
Subject: [PATCH 002/180] ci: harden release automation workflows
---
.github/workflows/promote-develop-to-main.yml | 10 +++++++---
.github/workflows/release-please.yml | 20 ++++++++++++++-----
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/promote-develop-to-main.yml b/.github/workflows/promote-develop-to-main.yml
index 13bbc6de..4e99c690 100644
--- a/.github/workflows/promote-develop-to-main.yml
+++ b/.github/workflows/promote-develop-to-main.yml
@@ -26,12 +26,16 @@ on:
description: 'Integration branch to promote'
required: true
default: develop
- type: string
+ type: choice
+ options:
+ - develop
base_branch:
description: 'Release branch that receives the promotion PR'
required: true
default: main
- type: string
+ type: choice
+ options:
+ - main
enable_auto_merge:
description: 'Enable auto-merge for the promotion PR after checks pass'
required: true
@@ -52,7 +56,7 @@ jobs:
name: Open promotion PR
runs-on: ubuntu-latest
env:
- GH_TOKEN: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }}
+ GH_TOKEN: ${{ github.token }}
BASE_BRANCH: ${{ inputs.base_branch }}
HEAD_BRANCH: ${{ inputs.head_branch }}
steps:
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 02451ad6..f9e83772 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -42,11 +42,22 @@ jobs:
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
+ - name: Verify release automation token
+ env:
+ RELEASE_AUTOMATION_TOKEN: ${{ secrets.RELEASE_AUTOMATION_TOKEN }}
+ run: |
+ if [[ -z "${RELEASE_AUTOMATION_TOKEN}" ]]; then
+ echo "RELEASE_AUTOMATION_TOKEN is required for release automation." >&2
+ echo "Use a repository-scoped GitHub App token or fine-grained PAT." >&2
+ echo "The token must create release tags that trigger downstream workflows." >&2
+ exit 1
+ fi
+
- name: Run release-please
id: release
- uses: googleapis/release-please-action@v4
+ uses: googleapis/release-please-action@8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5 # v4
with:
- token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }}
+ token: ${{ secrets.RELEASE_AUTOMATION_TOKEN }}
target-branch: main
release-type: simple
@@ -59,7 +70,6 @@ jobs:
echo "- Tag: \`${{ steps.release.outputs.tag_name || 'not created' }}\`"
echo "- Version: \`${{ steps.release.outputs.version || 'not created' }}\`"
echo
- echo "Use a repository secret named \`RELEASE_AUTOMATION_TOKEN\` from a"
- echo "GitHub App or fine-grained PAT if tag or release events must"
- echo "trigger downstream workflows."
+ echo "\`RELEASE_AUTOMATION_TOKEN\` is required so release-created tags"
+ echo "can trigger downstream Docker image and bundle workflows."
} >> "${GITHUB_STEP_SUMMARY}"
From f9693454405588301f7b8aa9288229128f255adc Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:46:26 +0800
Subject: [PATCH 003/180] feat(config): add 9600 GRE accelerator baseline
---
runtime/values-multi-nodes.yaml.example | 7 +++++++
runtime/values.yaml | 7 +++++++
2 files changed, 14 insertions(+)
diff --git a/runtime/values-multi-nodes.yaml.example b/runtime/values-multi-nodes.yaml.example
index 25c90da6..20e6d217 100644
--- a/runtime/values-multi-nodes.yaml.example
+++ b/runtime/values-multi-nodes.yaml.example
@@ -189,6 +189,13 @@ custom:
amd.com/gpu.product-name: "AMD_Radeon_AI_PRO_R9700"
env: {}
quotaRate: 4
+ 9600gre:
+ displayName: "AMD Radeon™ RX 9600 GRE (Desktop GPU)"
+ description: "RDNA 4.0 (gfx120x) | Compute Units 32 | 12GB GDDR6"
+ nodeSelector:
+ amd.com/gpu.product-name: "AMD_Radeon_RX_9600_GRE"
+ env: {}
+ quotaRate: 4
# --------------------------------------------------------------------------
# Course Resources
diff --git a/runtime/values.yaml b/runtime/values.yaml
index d1455e20..383eb8bd 100644
--- a/runtime/values.yaml
+++ b/runtime/values.yaml
@@ -268,6 +268,13 @@ custom:
amd.com/gpu.product-name: "AMD_Radeon_AI_PRO_R9700"
env: {}
quotaRate: 4
+ 9600gre:
+ displayName: "AMD Radeon™ RX 9600 GRE (Desktop GPU)"
+ description: "RDNA 4.0 (gfx120x) | Compute Units 32 | 12GB GDDR6"
+ nodeSelector:
+ amd.com/gpu.product-name: "AMD_Radeon_RX_9600_GRE"
+ env: {}
+ quotaRate: 4
# ============================================================================
# Course Resources Configuration
From a27dc758cb3abcf74c01d1b5331f66f79f4c3aaa Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:46:33 +0800
Subject: [PATCH 004/180] feat(installer): treat 9600 GRE as a curated SKU
---
auplc_installer/gpu.py | 4 ++--
tests/installer/test_gpu.py | 6 +++++-
tests/installer/test_overlay.py | 12 ++++++++++++
3 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/auplc_installer/gpu.py b/auplc_installer/gpu.py
index b967a90d..3c10305f 100644
--- a/auplc_installer/gpu.py
+++ b/auplc_installer/gpu.py
@@ -48,8 +48,8 @@
# Accelerator keys defined in runtime/values.yaml custom.accelerators. When
# the resolved accel_key is not in this list, ``overlay.py`` injects a full
# minimal accelerator stanza so helm install succeeds without values.yaml
-# edits (useful for ad-hoc SKUs like 9600gre).
-GPU_CURATED_SKU_KEYS = ("phx", "strix", "strix-halo", "9070xt", "r9700")
+# edits (useful for ad-hoc SKUs not yet promoted to the default values).
+GPU_CURATED_SKU_KEYS = ("phx", "strix", "strix-halo", "9070xt", "r9700", "9600gre")
def is_curated_sku(key: str) -> bool:
diff --git a/tests/installer/test_gpu.py b/tests/installer/test_gpu.py
index af20b4a4..32f4285d 100644
--- a/tests/installer/test_gpu.py
+++ b/tests/installer/test_gpu.py
@@ -136,8 +136,12 @@ def test_is_curated_sku(key: str) -> None:
assert is_curated_sku(key)
+def test_is_curated_sku_true_for_9600gre() -> None:
+ assert is_curated_sku("9600gre")
+
+
def test_is_curated_sku_false_for_unknown() -> None:
- assert not is_curated_sku("9600gre")
+ assert not is_curated_sku("totally-unknown")
def test_resolve_gpu_config_known_short_name() -> None:
diff --git a/tests/installer/test_overlay.py b/tests/installer/test_overlay.py
index 7d1d1c9f..9fb69757 100644
--- a/tests/installer/test_overlay.py
+++ b/tests/installer/test_overlay.py
@@ -122,6 +122,18 @@ def test_curated_sku_with_product_name_emits_node_selector() -> None:
assert accelerators["strix-halo"]["nodeSelector"]["amd.com/gpu.product-name"] == "AMD_Radeon_8060S_Graphics"
+def test_9600gre_uses_curated_overlay_path() -> None:
+ cfg = GpuConfig()
+ append_product(cfg, "AMD_Radeon_RX_9600_GRE")
+ text, parsed = _render(cfg, courses=CourseSelection.default())
+ accel = parsed["custom"]["accelerators"]["9600gre"]
+ assert accel["nodeSelector"]["amd.com/gpu.product-name"] == "AMD_Radeon_RX_9600_GRE"
+ assert "displayName" not in accel
+ assert "description" not in accel
+ assert "quotaRate" not in accel
+ assert "SKU '9600gre' is not curated in values.yaml" not in text
+
+
def test_basic_emits_filtered_teams_mapping() -> None:
_, parsed = _render(
_strix_halo_cfg(),
From 1f6e23f56b13ebb9ed6811883c4028ede7b7663d Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 11:58:41 +0800
Subject: [PATCH 005/180] feat(config): add GPU resource image overrides
---
runtime/values-multi-nodes.yaml.example | 117 +++++++++++++++++--
runtime/values.yaml | 113 +++++++++++++++++-
tests/installer/test_values_gpu_overrides.py | 66 +++++++++++
3 files changed, 282 insertions(+), 14 deletions(-)
create mode 100644 tests/installer/test_values_gpu_overrides.py
diff --git a/runtime/values-multi-nodes.yaml.example b/runtime/values-multi-nodes.yaml.example
index 20e6d217..5084148e 100644
--- a/runtime/values-multi-nodes.yaml.example
+++ b/runtime/values-multi-nodes.yaml.example
@@ -284,7 +284,25 @@ custom:
subDescription: "GPU Accelerated Environment"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
allowGitClone: true
resourceType: "notebook"
code-gpu:
@@ -293,7 +311,25 @@ custom:
subDescription: "GPU-accelerated development workspace"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
allowGitClone: true
launchMode: "code-server"
resourceType: "browser-ide"
@@ -303,41 +339,104 @@ custom:
subDescription: "Suitable for CV experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
resourceType: "notebook"
- # acceleratorOverrides: (optional) per-accelerator image and env overrides
- # Use this when one course is available on multiple GPU targets with
- # different image tags.
- # 9070xt:
- # image: "ghcr.io/your-org/auplc-cv:latest-gfx120x"
- # r9700:
- # image: "ghcr.io/your-org/auplc-cv:latest-gfx120x"
- # strix-halo:
- # image: "ghcr.io/your-org/auplc-cv:latest-gfx1151"
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
Course-DL:
group: "TEACHING LABS"
description: "Deep Learning Course"
subDescription: "Suitable for DL experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
resourceType: "notebook"
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
Course-LLM:
group: "TEACHING LABS"
description: "Large Language Models Course"
subDescription: "Suitable for LLM experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
resourceType: "notebook"
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
Course-PhySim:
group: "TEACHING LABS"
description: "Genesis Physical Simulation Course"
subDescription: "Suitable for physical simulation experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
resourceType: "notebook"
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
# --------------------------------------------------------------------------
# Team Permission Configuration
diff --git a/runtime/values.yaml b/runtime/values.yaml
index 383eb8bd..ed7f1bc5 100644
--- a/runtime/values.yaml
+++ b/runtime/values.yaml
@@ -371,7 +371,25 @@ custom:
subDescription: "GPU Accelerated Environment"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-base:latest-gfx120x"
allowGitClone: true
defaultPath: "/home/jovyan"
resourceType: "notebook"
@@ -381,7 +399,25 @@ custom:
subDescription: "GPU-accelerated development workspace"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx120x"
allowGitClone: true
launchMode: "code-server"
defaultPath: "/home/jovyan"
@@ -392,12 +428,25 @@ custom:
subDescription: "Suitable for CV experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
- # acceleratorOverrides: (optional) per-accelerator image and env overrides
- # 9070xt:
- # image: "ghcr.io/your-org/auplc-cv:"
- # r9700:
- # image: "ghcr.io/your-org/auplc-cv:"
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-cv:latest-gfx120x"
defaultPath: "/opt/workspace/CV"
resourceType: "notebook"
Course-DL:
@@ -406,7 +455,25 @@ custom:
subDescription: "Suitable for DL experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-dl:latest-gfx120x"
defaultPath: "/opt/workspace/DL"
resourceType: "notebook"
Course-LLM:
@@ -415,7 +482,25 @@ custom:
subDescription: "Suitable for LLM experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-llm:latest-gfx120x"
defaultPath: "/opt/workspace/LLM"
resourceType: "notebook"
Course-PhySim:
@@ -424,7 +509,25 @@ custom:
subDescription: "Suitable for physical simulation experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
+ - phx
+ - strix
- strix-halo
+ - 9070xt
+ - r9700
+ - 9600gre
+ acceleratorOverrides:
+ phx:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx110x"
+ strix:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx1150"
+ strix-halo:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx1151"
+ 9070xt:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
+ r9700:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
+ 9600gre:
+ image: "ghcr.io/amdresearch/auplc-physim:latest-gfx120x"
defaultPath: "/opt/workspace/PhySim"
resourceType: "notebook"
diff --git a/tests/installer/test_values_gpu_overrides.py b/tests/installer/test_values_gpu_overrides.py
new file mode 100644
index 00000000..1f3934cf
--- /dev/null
+++ b/tests/installer/test_values_gpu_overrides.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2025 Advanced Micro Devices, Inc. All rights reserved.
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import yaml
+
+ROOT = Path(__file__).resolve().parents[2]
+
+VALUES_FILES = (
+ ROOT / "runtime" / "values.yaml",
+ ROOT / "runtime" / "values-multi-nodes.yaml.example",
+)
+
+GPU_ACCELERATOR_TAGS = {
+ "phx": "gfx110x",
+ "strix": "gfx1150",
+ "strix-halo": "gfx1151",
+ "9070xt": "gfx120x",
+ "r9700": "gfx120x",
+ "9600gre": "gfx120x",
+}
+
+GPU_RESOURCE_IMAGES = {
+ "gpu": "auplc-base",
+ "code-gpu": "auplc-code-gpu",
+ "Course-CV": "auplc-cv",
+ "Course-DL": "auplc-dl",
+ "Course-LLM": "auplc-llm",
+ "Course-PhySim": "auplc-physim",
+}
+
+
+def _load_values(path: Path) -> dict:
+ return yaml.safe_load(path.read_text(encoding="utf-8"))
+
+
+def test_default_values_expose_supported_gpu_accelerators() -> None:
+ expected_keys = list(GPU_ACCELERATOR_TAGS)
+
+ for values_file in VALUES_FILES:
+ values = _load_values(values_file)
+ accelerators = values["custom"]["accelerators"]
+ metadata = values["custom"]["resources"]["metadata"]
+
+ for accelerator_key in expected_keys:
+ assert accelerator_key in accelerators, values_file
+
+ for resource_key in GPU_RESOURCE_IMAGES:
+ assert metadata[resource_key]["acceleratorKeys"] == expected_keys, values_file
+
+
+def test_default_values_route_gpu_resources_to_supported_image_tags() -> None:
+ for values_file in VALUES_FILES:
+ values = _load_values(values_file)
+ metadata = values["custom"]["resources"]["metadata"]
+
+ for resource_key, image_name in GPU_RESOURCE_IMAGES.items():
+ overrides = metadata[resource_key]["acceleratorOverrides"]
+ assert set(overrides) == set(GPU_ACCELERATOR_TAGS), values_file
+
+ for accelerator_key, gpu_target in GPU_ACCELERATOR_TAGS.items():
+ assert overrides[accelerator_key]["image"] == (
+ f"ghcr.io/amdresearch/{image_name}:latest-{gpu_target}"
+ ), values_file
From e5555072ee562c1fda3efe5ccf09652ab1c65678 Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:49:24 +0800
Subject: [PATCH 006/180] fix(config): keep GPU accelerator visibility
conservative
---
runtime/values-multi-nodes.yaml.example | 34 +++-----------------
runtime/values.yaml | 34 +++-----------------
tests/installer/test_values_gpu_overrides.py | 9 ++++--
3 files changed, 15 insertions(+), 62 deletions(-)
diff --git a/runtime/values-multi-nodes.yaml.example b/runtime/values-multi-nodes.yaml.example
index 5084148e..473c16ad 100644
--- a/runtime/values-multi-nodes.yaml.example
+++ b/runtime/values-multi-nodes.yaml.example
@@ -283,13 +283,12 @@ custom:
description: "Basic GPU Environment"
subDescription: "GPU Accelerated Environment"
accelerator: "GPU"
+ # Add only accelerators that exist in this cluster and are validated for
+ # this resource. Image overrides for the curated accelerators are
+ # preconfigured below, so enabling one normally only requires adding its
+ # key here.
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-base:latest-gfx110x"
@@ -311,12 +310,7 @@ custom:
subDescription: "GPU-accelerated development workspace"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx110x"
@@ -339,12 +333,7 @@ custom:
subDescription: "Suitable for CV experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
resourceType: "notebook"
acceleratorOverrides:
phx:
@@ -365,12 +354,7 @@ custom:
subDescription: "Suitable for DL experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
resourceType: "notebook"
acceleratorOverrides:
phx:
@@ -391,12 +375,7 @@ custom:
subDescription: "Suitable for LLM experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
resourceType: "notebook"
acceleratorOverrides:
phx:
@@ -417,12 +396,7 @@ custom:
subDescription: "Suitable for physical simulation experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
resourceType: "notebook"
acceleratorOverrides:
phx:
diff --git a/runtime/values.yaml b/runtime/values.yaml
index ed7f1bc5..f50dc60d 100644
--- a/runtime/values.yaml
+++ b/runtime/values.yaml
@@ -370,13 +370,12 @@ custom:
description: "Basic GPU Environment"
subDescription: "GPU Accelerated Environment"
accelerator: "GPU"
+ # Add only accelerators that this deployment should expose to users.
+ # Image overrides for the curated accelerators are preconfigured below,
+ # so enabling another GPU family normally only requires adding its key
+ # here from custom.accelerators.
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-base:latest-gfx110x"
@@ -399,12 +398,7 @@ custom:
subDescription: "GPU-accelerated development workspace"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-code-gpu:latest-gfx110x"
@@ -428,12 +422,7 @@ custom:
subDescription: "Suitable for CV experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-cv:latest-gfx110x"
@@ -455,12 +444,7 @@ custom:
subDescription: "Suitable for DL experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-dl:latest-gfx110x"
@@ -482,12 +466,7 @@ custom:
subDescription: "Suitable for LLM experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-llm:latest-gfx110x"
@@ -509,12 +488,7 @@ custom:
subDescription: "Suitable for physical simulation experiments with GPU"
accelerator: "GPU"
acceleratorKeys:
- - phx
- - strix
- strix-halo
- - 9070xt
- - r9700
- - 9600gre
acceleratorOverrides:
phx:
image: "ghcr.io/amdresearch/auplc-physim:latest-gfx110x"
diff --git a/tests/installer/test_values_gpu_overrides.py b/tests/installer/test_values_gpu_overrides.py
index 1f3934cf..7f6ef6ad 100644
--- a/tests/installer/test_values_gpu_overrides.py
+++ b/tests/installer/test_values_gpu_overrides.py
@@ -42,13 +42,18 @@ def test_default_values_expose_supported_gpu_accelerators() -> None:
for values_file in VALUES_FILES:
values = _load_values(values_file)
accelerators = values["custom"]["accelerators"]
- metadata = values["custom"]["resources"]["metadata"]
for accelerator_key in expected_keys:
assert accelerator_key in accelerators, values_file
+
+def test_default_values_keep_visible_gpu_accelerators_conservative() -> None:
+ for values_file in VALUES_FILES:
+ values = _load_values(values_file)
+ metadata = values["custom"]["resources"]["metadata"]
+
for resource_key in GPU_RESOURCE_IMAGES:
- assert metadata[resource_key]["acceleratorKeys"] == expected_keys, values_file
+ assert metadata[resource_key]["acceleratorKeys"] == ["strix-halo"], values_file
def test_default_values_route_gpu_resources_to_supported_image_tags() -> None:
From 9d89191b2c23f3beda22166e2e5b7b8be655abe7 Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 15:09:00 +0800
Subject: [PATCH 007/180] fix(installer): override preconfigured accelerator
images
---
auplc_installer/overlay.py | 15 +++++----------
tests/installer/test_overlay.py | 12 ++++++++++++
2 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/auplc_installer/overlay.py b/auplc_installer/overlay.py
index b2d984d9..5815f391 100644
--- a/auplc_installer/overlay.py
+++ b/auplc_installer/overlay.py
@@ -122,16 +122,11 @@ def emit_overlay(
buf.write(" acceleratorKeys:\n")
for sku in cfg.skus:
buf.write(f" - {sku.accel_key}\n")
- if not homogeneous_target:
- base_name = _RESOURCE_IMAGE_BASE[resource]
- wrote_overrides = False
- for sku in cfg.skus:
- if sku.gpu_target != cfg.gpu_target:
- if not wrote_overrides:
- buf.write(" acceleratorOverrides:\n")
- wrote_overrides = True
- buf.write(f" {sku.accel_key}:\n")
- buf.write(f' image: "{image_registry}/{base_name}:{image_tag}-{sku.gpu_target}"\n')
+ base_name = _RESOURCE_IMAGE_BASE[resource]
+ buf.write(" acceleratorOverrides:\n")
+ for sku in cfg.skus:
+ buf.write(f" {sku.accel_key}:\n")
+ buf.write(f' image: "{image_registry}/{base_name}:{image_tag}-{sku.gpu_target}"\n')
# --- teams.mapping filter (only when course selection is in effect) ---
if filter_courses:
diff --git a/tests/installer/test_overlay.py b/tests/installer/test_overlay.py
index 9fb69757..b028cdd9 100644
--- a/tests/installer/test_overlay.py
+++ b/tests/installer/test_overlay.py
@@ -115,6 +115,17 @@ def test_resource_images_use_primary_tag() -> None:
assert images["Course-PhySim"] == "ghcr.io/amdresearch/auplc-physim:v1.0-gfx1151"
+def test_homogeneous_target_emits_matching_accelerator_overrides() -> None:
+ _, parsed = _render(_strix_halo_cfg(), courses=CourseSelection.default())
+ gpu_metadata = parsed["custom"]["resources"]["metadata"]["gpu"]
+ overrides = gpu_metadata["acceleratorOverrides"]
+ assert overrides == {
+ "strix-halo": {
+ "image": "ghcr.io/amdresearch/auplc-base:v1.0-gfx1151",
+ },
+ }
+
+
def test_curated_sku_with_product_name_emits_node_selector() -> None:
_, parsed = _render(_strix_halo_cfg(), courses=CourseSelection.default())
accelerators = parsed["custom"]["accelerators"]
@@ -206,6 +217,7 @@ def test_mixed_targets_emit_accelerator_overrides() -> None:
gpu_metadata = parsed["custom"]["resources"]["metadata"]["gpu"]
assert "acceleratorOverrides" in gpu_metadata
overrides = gpu_metadata["acceleratorOverrides"]
+ assert overrides["strix-halo"]["image"] == "ghcr.io/amdresearch/auplc-base:v1.0-gfx1151"
assert "r9700" in overrides
assert overrides["r9700"]["image"] == "ghcr.io/amdresearch/auplc-base:v1.0-gfx120x"
From 972b4db25b715bc5e78a5f899f0bd5e4bf14ff8a Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Fri, 10 Jul 2026 15:14:49 +0800
Subject: [PATCH 008/180] docs(config): note accelerator override tag
compatibility
---
runtime/values-multi-nodes.yaml.example | 3 +++
runtime/values.yaml | 3 +++
2 files changed, 6 insertions(+)
diff --git a/runtime/values-multi-nodes.yaml.example b/runtime/values-multi-nodes.yaml.example
index 473c16ad..3db3cffa 100644
--- a/runtime/values-multi-nodes.yaml.example
+++ b/runtime/values-multi-nodes.yaml.example
@@ -287,6 +287,9 @@ custom:
# this resource. Image overrides for the curated accelerators are
# preconfigured below, so enabling one normally only requires adding its
# key here.
+ # If you override GPU resource images with custom tags or registries,
+ # also override the matching acceleratorOverrides entries so accelerator
+ # selection does not fall back to the default latest-* images.
acceleratorKeys:
- strix-halo
acceleratorOverrides:
diff --git a/runtime/values.yaml b/runtime/values.yaml
index f50dc60d..da79243a 100644
--- a/runtime/values.yaml
+++ b/runtime/values.yaml
@@ -374,6 +374,9 @@ custom:
# Image overrides for the curated accelerators are preconfigured below,
# so enabling another GPU family normally only requires adding its key
# here from custom.accelerators.
+ # If you override GPU resource images with custom tags or registries,
+ # also override the matching acceleratorOverrides entries so accelerator
+ # selection does not fall back to the default latest-* images.
acceleratorKeys:
- strix-halo
acceleratorOverrides:
From 91cf3e8753ce6586352a2d22d83a242468f70ae3 Mon Sep 17 00:00:00 2001
From: Mario Ruiz <11815099+mariodruiz@users.noreply.github.com>
Date: Tue, 14 Jul 2026 15:36:36 +0100
Subject: [PATCH 009/180] fix(scripts): use quota REST API in manage_users
instead of kubectl exec
---
scripts/manage_users.py | 189 +++++++++++-----------------------------
1 file changed, 51 insertions(+), 138 deletions(-)
diff --git a/scripts/manage_users.py b/scripts/manage_users.py
index a6d63c9c..8808e039 100644
--- a/scripts/manage_users.py
+++ b/scripts/manage_users.py
@@ -69,7 +69,6 @@
import os
import secrets
import string
-import subprocess
import sys
import pandas as pd
@@ -168,6 +167,49 @@ def batch_set_passwords(self, users: list[dict], force_change: bool = True) -> t
except requests.exceptions.RequestException as e:
return False, {"error": str(e)}
+ def get_quota(self, username: str) -> int | None:
+ """Get a user's quota balance via the admin API."""
+ username = self.normalize_username(username)
+ try:
+ response = requests.get(f"{self.hub_url}/hub/admin/api/quota/{username}", headers=self.headers)
+ if response.status_code == 200:
+ return response.json().get("balance")
+ return None
+ except requests.exceptions.RequestException:
+ return None
+
+ def set_quota(self, username: str, amount: int) -> tuple[bool, str]:
+ """Set a user's quota balance via the admin API."""
+ return self._modify_quota(username, {"action": "set", "amount": amount})
+
+ def add_quota(self, username: str, amount: int) -> tuple[bool, str]:
+ """Add to a user's quota balance via the admin API."""
+ return self._modify_quota(username, {"action": "add", "amount": amount})
+
+ def _modify_quota(self, username: str, payload: dict) -> tuple[bool, str]:
+ """Post a quota modification and return (success, message)."""
+ username = self.normalize_username(username)
+ try:
+ response = requests.post(
+ f"{self.hub_url}/hub/admin/api/quota/{username}", headers=self.headers, json=payload
+ )
+ data = response.json()
+ if response.status_code == 200:
+ return True, str(data.get("balance", ""))
+ return False, data.get("error", f"HTTP {response.status_code}")
+ except requests.exceptions.RequestException as e:
+ return False, str(e)
+
+ def list_quotas(self) -> list[dict] | None:
+ """List all user quota balances via the admin API."""
+ try:
+ response = requests.get(f"{self.hub_url}/hub/admin/api/quota", headers=self.headers)
+ if response.status_code == 200:
+ return response.json().get("users", [])
+ return None
+ except requests.exceptions.RequestException:
+ return None
+
def _check_connection(self) -> bool:
"""Check if connection to JupyterHub is working"""
try:
@@ -664,125 +706,8 @@ def cmd_set_passwords(args, manager: JupyterHubUserManager):
# ============ Quota Management Commands ============
-def set_quota_in_pod(username: str, amount: int, namespace: str = "jupyterhub") -> bool:
- """Set quota for a user via kubectl exec."""
- username = username.strip().lower()
-
- python_code = f'''
-import sys
-sys.path.insert(0, "/etc/jupyterhub")
-from quota_manager import get_quota_manager
-
-qm = get_quota_manager()
-qm.set_balance("{username}", {amount}, "cli_admin")
-print("OK")
-'''
-
- try:
- result = subprocess.run(
- ["kubectl", "--namespace", namespace, "exec", "deployment/hub", "--", "python3", "-c", python_code],
- capture_output=True,
- text=True,
- timeout=30,
- )
- return result.returncode == 0 and "OK" in result.stdout
- except Exception as e:
- print(f" Error: {e}")
- return False
-
-
-def add_quota_in_pod(username: str, amount: int, namespace: str = "jupyterhub") -> bool:
- """Add quota to a user via kubectl exec."""
- username = username.strip().lower()
-
- python_code = f'''
-import sys
-sys.path.insert(0, "/etc/jupyterhub")
-from quota_manager import get_quota_manager
-
-qm = get_quota_manager()
-qm.add_quota("{username}", {amount}, "cli_admin")
-print("OK")
-'''
-
- try:
- result = subprocess.run(
- ["kubectl", "--namespace", namespace, "exec", "deployment/hub", "--", "python3", "-c", python_code],
- capture_output=True,
- text=True,
- timeout=30,
- )
- return result.returncode == 0 and "OK" in result.stdout
- except Exception as e:
- print(f" Error: {e}")
- return False
-
-
-def get_quota_from_pod(username: str, namespace: str = "jupyterhub") -> int | None:
- """Get quota balance for a user via kubectl exec."""
- username = username.strip().lower()
-
- python_code = f'''
-import sys
-sys.path.insert(0, "/etc/jupyterhub")
-from quota_manager import get_quota_manager
-
-qm = get_quota_manager()
-balance = qm.get_balance("{username}")
-print(f"BALANCE:{{balance}}")
-'''
-
- try:
- result = subprocess.run(
- ["kubectl", "--namespace", namespace, "exec", "deployment/hub", "--", "python3", "-c", python_code],
- capture_output=True,
- text=True,
- timeout=30,
- )
- if result.returncode == 0:
- for line in result.stdout.split("\n"):
- if line.startswith("BALANCE:"):
- return int(line.split(":")[1])
- return None
- except Exception:
- return None
-
-
-def list_quota_from_pod(namespace: str = "jupyterhub") -> list[dict] | None:
- """Get all user quota balances via kubectl exec."""
- python_code = """
-import sys
-import json
-sys.path.insert(0, "/etc/jupyterhub")
-from quota_manager import get_quota_manager
-
-qm = get_quota_manager()
-balances = qm.get_all_balances()
-print("JSON:" + json.dumps(balances))
-"""
-
- try:
- result = subprocess.run(
- ["kubectl", "--namespace", namespace, "exec", "deployment/hub", "--", "python3", "-c", python_code],
- capture_output=True,
- text=True,
- timeout=30,
- )
- if result.returncode == 0:
- import json
-
- for line in result.stdout.split("\n"):
- if line.startswith("JSON:"):
- return json.loads(line[5:])
- return None
- except Exception:
- return None
-
-
def cmd_set_quota(args, manager: JupyterHubUserManager):
"""Set quota for users"""
- namespace = args.namespace
-
if args.file:
users = load_users_from_file(args.file)
print(f"📄 Loaded {len(users)} users from {args.file}")
@@ -802,14 +727,14 @@ def cmd_set_quota(args, manager: JupyterHubUserManager):
print(f" ⚠️ Skipping {username}: no quota amount specified")
continue
- success = set_quota_in_pod(username, int(amount), namespace)
+ success, message = manager.set_quota(username, int(amount))
if success:
print(f" ✅ Set {amount} quota for: {username}")
results["success"] += 1
output_data.append({"username": username, "quota": amount})
else:
- print(f" ❌ Failed: {username}")
+ print(f" ❌ Failed: {username}: {message}")
results["failed"] += 1
print("\n" + "=" * 50)
@@ -821,7 +746,6 @@ def cmd_set_quota(args, manager: JupyterHubUserManager):
def cmd_add_quota(args, manager: JupyterHubUserManager):
"""Add quota to users"""
- namespace = args.namespace
amount = args.amount
if args.file:
@@ -839,12 +763,12 @@ def cmd_add_quota(args, manager: JupyterHubUserManager):
if not username:
continue
- success = add_quota_in_pod(username, amount, namespace)
+ success, message = manager.add_quota(username, amount)
if success:
print(f" ✅ Added {amount} quota to: {username}")
results["success"] += 1
else:
- print(f" ❌ Failed: {username}")
+ print(f" ❌ Failed: {username}: {message}")
results["failed"] += 1
print("\n" + "=" * 50)
@@ -856,9 +780,7 @@ def cmd_add_quota(args, manager: JupyterHubUserManager):
def cmd_list_quota(args, manager: JupyterHubUserManager):
"""List all user quota balances"""
- namespace = args.namespace
-
- balances = list_quota_from_pod(namespace)
+ balances = manager.list_quotas()
if balances is None:
print("❌ Failed to retrieve quota balances")
@@ -978,28 +900,19 @@ def main():
setpw_parser.add_argument("--output", "-o", help="Output file to save usernames and passwords")
# Set-quota command
- setquota_parser = subparsers.add_parser("set-quota", help="Set quota for users (requires kubectl)")
+ setquota_parser = subparsers.add_parser("set-quota", help="Set quota for users")
setquota_parser.add_argument("users", nargs="*", help="Username(s) to set quota for")
setquota_parser.add_argument("--file", "-f", help="CSV or Excel file with username,quota columns")
setquota_parser.add_argument("--amount", "-a", type=int, help="Quota amount (when using usernames)")
- setquota_parser.add_argument(
- "--namespace", "-n", default="jupyterhub", help="Kubernetes namespace (default: jupyterhub)"
- )
# Add-quota command
- addquota_parser = subparsers.add_parser("add-quota", help="Add quota to users (requires kubectl)")
+ addquota_parser = subparsers.add_parser("add-quota", help="Add quota to users")
addquota_parser.add_argument("users", nargs="*", help="Username(s) to add quota to")
addquota_parser.add_argument("--file", "-f", help="CSV or Excel file with usernames")
addquota_parser.add_argument("--amount", "-a", type=int, required=True, help="Quota amount to add")
- addquota_parser.add_argument(
- "--namespace", "-n", default="jupyterhub", help="Kubernetes namespace (default: jupyterhub)"
- )
# List-quota command
- listquota_parser = subparsers.add_parser("list-quota", help="List all user quota balances (requires kubectl)")
- listquota_parser.add_argument(
- "--namespace", "-n", default="jupyterhub", help="Kubernetes namespace (default: jupyterhub)"
- )
+ subparsers.add_parser("list-quota", help="List all user quota balances")
args = parser.parse_args()
From cf2fa91f18e627bc5dee9bb064d9565ff5478d04 Mon Sep 17 00:00:00 2001
From: Mario Ruiz <11815099+mariodruiz@users.noreply.github.com>
Date: Tue, 14 Jul 2026 16:13:49 +0100
Subject: [PATCH 010/180] Make code more robust
---
scripts/manage_users.py | 36 ++++++++++++++++++++++--------------
1 file changed, 22 insertions(+), 14 deletions(-)
diff --git a/scripts/manage_users.py b/scripts/manage_users.py
index 8808e039..2a989c42 100644
--- a/scripts/manage_users.py
+++ b/scripts/manage_users.py
@@ -167,17 +167,6 @@ def batch_set_passwords(self, users: list[dict], force_change: bool = True) -> t
except requests.exceptions.RequestException as e:
return False, {"error": str(e)}
- def get_quota(self, username: str) -> int | None:
- """Get a user's quota balance via the admin API."""
- username = self.normalize_username(username)
- try:
- response = requests.get(f"{self.hub_url}/hub/admin/api/quota/{username}", headers=self.headers)
- if response.status_code == 200:
- return response.json().get("balance")
- return None
- except requests.exceptions.RequestException:
- return None
-
def set_quota(self, username: str, amount: int) -> tuple[bool, str]:
"""Set a user's quota balance via the admin API."""
return self._modify_quota(username, {"action": "set", "amount": amount})
@@ -221,7 +210,7 @@ def _check_connection(self) -> bool:
print(f"❌ Connection failed with status {response.status_code}")
print(f"Response: {response.text}")
return False
- except Exception as e:
+ except requests.exceptions.RequestException as e:
print(f"❌ Connection error: {e}")
return False
@@ -382,7 +371,7 @@ def get_user(self, username: str) -> dict | None:
if response.status_code == 200:
return response.json()
return None
- except Exception:
+ except requests.exceptions.RequestException:
return None
def set_admin(self, username: str, admin: bool = True) -> bool:
@@ -714,6 +703,14 @@ def cmd_set_quota(args, manager: JupyterHubUserManager):
else:
users = [{"username": u} for u in args.users]
+ if not users:
+ print("❌ No users specified")
+ return
+
+ if not args.file and args.amount is None:
+ print("❌ --amount is required when specifying usernames (or use --file with a quota column)")
+ return
+
results = {"success": 0, "failed": 0}
output_data = []
@@ -727,7 +724,14 @@ def cmd_set_quota(args, manager: JupyterHubUserManager):
print(f" ⚠️ Skipping {username}: no quota amount specified")
continue
- success, message = manager.set_quota(username, int(amount))
+ try:
+ amount = int(amount)
+ except (TypeError, ValueError):
+ print(f" ⚠️ Skipping {username}: invalid quota amount '{amount}'")
+ results["failed"] += 1
+ continue
+
+ success, message = manager.set_quota(username, amount)
if success:
print(f" ✅ Set {amount} quota for: {username}")
@@ -754,6 +758,10 @@ def cmd_add_quota(args, manager: JupyterHubUserManager):
else:
usernames = args.users
+ if not usernames:
+ print("❌ No users specified")
+ return
+
print(f"\n🔄 Adding {amount} quota to {len(usernames)} users...")
results = {"success": 0, "failed": 0}
From a48b95f86c458be02abf5a59456f1afbbba00438 Mon Sep 17 00:00:00 2001
From: ShifZhan <252984256+MioYuuIH@users.noreply.github.com>
Date: Thu, 16 Jul 2026 10:14:10 +0800
Subject: [PATCH 011/180] feat(admin): add suffix width control for user
generation
---
.../admin/src/components/CreateUserModal.tsx | 29 +++++++++++++++++--
1 file changed, 26 insertions(+), 3 deletions(-)
diff --git a/runtime/hub/frontend/apps/admin/src/components/CreateUserModal.tsx b/runtime/hub/frontend/apps/admin/src/components/CreateUserModal.tsx
index a1b87c9f..ff4f4f42 100644
--- a/runtime/hub/frontend/apps/admin/src/components/CreateUserModal.tsx
+++ b/runtime/hub/frontend/apps/admin/src/components/CreateUserModal.tsx
@@ -51,13 +51,17 @@ export function CreateUserModal({ show, onHide, onSuccess, quotaEnabled = false,
const [prefix, setPrefix] = useState('');
const [count, setCount] = useState(10);
const [startNum, setStartNum] = useState(1);
+ const [suffixWidth, setSuffixWidth] = useState(2);
const [quotaValue, setQuotaValue] = useState(String(defaultQuota || 0));
const handleGenerateNames = useCallback(() => {
if (!prefix.trim()) return;
- const names = Array.from({ length: count }, (_, i) => `${prefix.trim()}${startNum + i}`);
+ const names = Array.from({ length: count }, (_, i) => {
+ const suffix = String(startNum + i);
+ return `${prefix.trim()}${suffixWidth > 0 ? suffix.padStart(suffixWidth, '0') : suffix}`;
+ });
setUsernames(names.join('\n'));
- }, [prefix, count, startNum]);
+ }, [prefix, count, startNum, suffixWidth]);
const generateRandomPassword = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
@@ -214,6 +218,7 @@ export function CreateUserModal({ show, onHide, onSuccess, quotaEnabled = false,
setPrefix('');
setCount(10);
setStartNum(1);
+ setSuffixWidth(2);
setQuotaValue(String(defaultQuota || 0));
onHide();
};
@@ -276,11 +281,29 @@ export function CreateUserModal({ show, onHide, onSuccess, quotaEnabled = false,
min={0}
max={9999}
value={startNum}
- onChange={(e) => setStartNum(parseInt(e.target.value) || 1)}
+ onChange={(e) => {
+ const value = parseInt(e.target.value);
+ setStartNum(Number.isNaN(value) ? 1 : value);
+ }}
style={{ width: 70 }}
/>
+