Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ uses: qBraid/upload-course-action@v0.1.0-beta
- Fixed JSend response parsing in deploy and polling scripts to support `{status, data}` envelope format
- Added guard in `action.yml` to fail early with a clear error when `course_custom_id` is empty after create/update step
- Replaced silent `pass` with warning logs when API response parsing fails
- Failed kernel deployment polling immediately on terminal API errors instead of retrying until timeout
- Made the deploy-kernel wrapper reuse the shared Dockerfile validator implementation

### Changed
- Treat active pre-existing kernels as a successful deploy outcome for rerun-safe custom kernel workflows

### Added
- Unit tests for JSend response handling in course creation and polling
Expand Down
88 changes: 88 additions & 0 deletions deploy-kernel/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: 'Deploy Custom Kernel to qBraid'
description: 'Validates and deploys a custom Jupyter kernel image to qBraid qBook'
author: 'qBraid'

branding:
icon: 'cpu'
color: 'purple'

inputs:
api-key:
description: 'qBraid API key for authentication'
required: true
dockerfile-path:
description: 'Path to the kernel Dockerfile'
required: false
default: 'Dockerfile.kernel'
kernel-name:
description: 'Unique kernel name (lowercase alphanumeric + underscore, 3-64 chars)'
required: true
language:
description: 'Kernel language (python, cpp, julia, r, rust, go, javascript)'
required: true
display-name:
description: 'Human-readable display name for the kernel'
required: true
context-dir:
description: 'Directory containing additional build context files (requirements.txt, kernel.json, etc.)'
required: false
default: '.'
api-base-url:
description: 'qBraid API base URL'
required: false
default: 'https://api-v2.qbraid.com/api/v1'
timeout-seconds:
description: 'Maximum time to wait for deployment to finish'
required: false
default: '1800'

outputs:
kernel-name:
description: 'The deployed kernel name'
value: ${{ steps.deploy-kernel.outputs.kernel-name }}
image-uri:
description: 'The Artifact Registry image URI'
value: ${{ steps.deploy-kernel.outputs.image-uri }}
status:
description: 'Deployment status (active, failed, timeout)'
value: ${{ steps.deploy-kernel.outputs.status }}
build-id:
description: 'Cloud Build ID'
value: ${{ steps.deploy-kernel.outputs.build-id }}

runs:
using: 'composite'
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
shell: bash
run: |
python -m pip install -q -r "${{ github.action_path }}/requirements.txt"

- name: Validate Dockerfile
shell: bash
run: |
python ${{ github.action_path }}/src/scripts/validate_dockerfile.py \
"${{ inputs.dockerfile-path }}" \
"${{ inputs.context-dir }}"

- name: Deploy kernel
id: deploy-kernel
shell: bash
env:
QBRAID_API_KEY: ${{ inputs.api-key }}
QBRAID_DEPLOY_TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The composite action exports QBRAID_DEPLOY_TIMEOUT_SECONDS, but deploy_kernel.py doesn't read this environment variable (timeout is passed via --timeout-seconds). This extra env var can confuse users—either remove it or wire the script to honor it when the CLI arg isn't provided.

Suggested change
QBRAID_DEPLOY_TIMEOUT_SECONDS: ${{ inputs.timeout-seconds }}

Copilot uses AI. Check for mistakes.
run: |
echo "::add-mask::$QBRAID_API_KEY"
python ${{ github.action_path }}/src/scripts/deploy_kernel.py \
--dockerfile-path "${{ inputs.dockerfile-path }}" \
--kernel-name "${{ inputs.kernel-name }}" \
--language "${{ inputs.language }}" \
--display-name "${{ inputs.display-name }}" \
--context-dir "${{ inputs.context-dir }}" \
--api-base-url "${{ inputs.api-base-url }}" \
--timeout-seconds "${{ inputs.timeout-seconds }}"
1 change: 1 addition & 0 deletions deploy-kernel/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests>=2.31.0
34 changes: 34 additions & 0 deletions deploy-kernel/src/scripts/context_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Delegate to the shared context-files helper (single source of truth)."""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path


def _load_shared_context_files_module():
repo_root = Path(__file__).resolve().parents[3]
shared_path = repo_root / "src" / "scripts" / "context_files.py"

spec = importlib.util.spec_from_file_location("shared_context_files", shared_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"Unable to load shared context_files from {shared_path}")

module = importlib.util.module_from_spec(spec)
sys.path.insert(0, str(shared_path.parent))
try:
spec.loader.exec_module(module)
finally:
sys.path.pop(0)
return module


_shared = _load_shared_context_files_module()

MAX_CONTEXT_FILE_BYTES = _shared.MAX_CONTEXT_FILE_BYTES
SKIPPED_CONTEXT_FILES = _shared.SKIPPED_CONTEXT_FILES
SKIPPED_CONTEXT_SUFFIXES = _shared.SKIPPED_CONTEXT_SUFFIXES
SKIPPED_CONTEXT_SUBSTRINGS = _shared.SKIPPED_CONTEXT_SUBSTRINGS
is_skipped_context_file = _shared.is_skipped_context_file
list_context_files = _shared.list_context_files
Loading
Loading