vocareum-publisher
v1.3.7
Published
Publish assignment content from GitHub to Vocareum
Maintainers
Readme
Vocareum Publisher
Push assignment content from GitHub to Vocareum.
A CLI tool and GitHub Action that enables instructors to maintain assignment content in Git with full version control while seamlessly syncing to Vocareum.
Released components (the CLI and the VS Code extension are versioned independently — see the CHANGELOG, npm, and the Releases page for current versions):
- CLI package:
vocareum-publisheron npm (npm install -g vocareum-publisher) - VS Code extension:
vocgit— see vscode-extension/ for installation
Video Walkthrough - Watch a quick demo of vocgit in action.
Features
- Git-First Workflow: GitHub is the source of truth for all assignment content
- CLI Tool: Local development and push via command line
- GitHub Action: Automated CI/CD push on git push
- Change Detection: Only uploads changed content (efficient)
- Template-Based Creation: Create new assignments from templates
- Validation: Verify configuration before pushing
- Forward-Compatible Settings: Unrecognized Vocareum settings fields are preserved under
_unknown_settingsinvocareum.yamland passed back through on push, so new platform features never silently drop out of your config
Installation
npm install -g vocareum-publisherQuick Start
1. Initialize a Course Repository
mkdir my-course && cd my-course
git init
vocgit init2. Create an Assignment
vocgit new lab1-intro
# Follow interactive prompts3. Add Content
Add your files to the generated directory structure:
lab1-intro/
├── part1/
│ ├── startercode/ # Student-visible starter code
│ ├── scripts/ # Grading scripts
│ ├── docs/ # Documentation
│ ├── data/ # Datasets
│ └── private/ # Private filesvocgit new scaffolds one part (part1) with default directories:
startercode, scripts, docs, data, and private.
If needed, add lib and asnlib manually and include them in parts[].directories.
4. Validate and Push
vocgit validate
vocgit push5. Commit and Push
git add .
git commit -m "Add Lab 1"
git pushConfiguration
All configuration is stored in vocareum.yaml:
version: "1.0"
vocareum:
org_id: "12345"
course_id: "67890"
templates: # Named templates for creating new assignments
- id: "99999"
name: "Standard Lab"
course_id: "67890" # Same course
- id: "88888"
name: "Cloud Lab"
course_id: "11111" # Template in different course
- id: "77777"
name: "Timed Exam"
course_id: "67890"
excluded_assignments: # Assignment IDs to hide from orphan detection
- "111222"
- "333444"
course_settings: # Optional course metadata sync
name: "Intro to ML"
description: "Spring section"
assignments:
- assignment_id: "11111"
name: "Lab 1: Introduction"
path: "lab1-intro"
sync_settings: true # Optional override; defaults to publish_options.sync_settings
settings: # Optional assignment settings
nosubmit: false
publish: true
publish_grades: false
auto_submit: false
grading_on_submit: true
exam_mode: "TIMED" # NO_EXAM, SCHEDULED, TIMED, TIMED_UNRESTRICTED
exam_duration: 120
num_attempts: 3
grading_visibility: "ALL" # ALL or ASSIGNED
_observed_settings: # Pull-only API fields; vocgit does not push these
description: "Introduction to the course"
parts:
- part_id: "22222"
path: "part1"
name: "Part 1: Setup"
sync_settings: true # Optional part-level override
settings: # Optional part settings
submission_filters:
include: ["*.py"]
exclude: ["*.pyc"]
session_length: "60" # minutes
late_penalty_percent: 10 # Accepted by API; may not echo on read
late_penalty_percent_rule: "max score"
deadlinedate: "2025-03-15T23:59:00Z"
number_of_submissions: 5
lab_interface:
panels: ["Console"]
controls: ["Reset"]
- assignment_id: null
name: "Lab 2: Classification"
assignment_name_for_lookup: "Lab 2: Classification" # Optional name-based ID discovery
path: "lab2-classification"
parts:
- part_id: null
path: "part1"
name: "Part 1: Implementation"
settings:
cloud_labs: true
session_length: "60"
labtype: "JupyterLab"
endlab: true
publish_options:
on_missing_id: "skip"
auto_commit: false
sync_settings: true # Set false to sync files only, not settings
sync_deletes: false
publish_history:
- timestamp: "2026-02-12T22:30:00Z"
commit_sha: "abc123def456"
published_by: "github-actions"
status: "failed" # success | failed
content_state:
"lab1-intro/part1/startercode": "9a7f..."
failed:
- type: "file"
id: "22222/startercode/main.py"
error: "Timed out after 30000ms waiting for part update (txn=123)"Understanding Settings Sync
vocgit keeps assignment and part settings in sync between vocareum.yaml and Vocareum. The fields you set under settings: are pushed on vocgit push, and vocgit pull detects when Vocareum's values have drifted from yours.
Because Vocareum's API treats fields inconsistently — some are writable, some are read-only, some it accepts but never reports back — vocgit sorts every settings field into one of a few groups. After a pull, you may notice new keys appear in your YAML. These are expected and safe to leave alone:
- Top-level settings (e.g.
nosubmit,session_length,submission_filters) — fields vocgit both reads and writes. Edit these to change behavior on Vocareum. _observed_settings— fields Vocareum reports on read but does not accept on write (or that only apply at create time). vocgit records them so your config reflects the real server state, but it never pushes them. Informational only._unknown_settings— fields vocgit doesn't recognize yet (typically new Vocareum features). vocgit preserves them verbatim and passes them back through unchanged on the next push, so nothing is silently lost. When these appear, vocgit prints an end-of-run notice asking you to file an issue so the field can be promoted to a supported setting.
"Accepted but not confirmed" fields. Some writable fields (e.g. exam_mode, exam_duration, deadlinedate, late_penalty_percent) are accepted by the API on write but are not echoed back on read. vocgit writes them and trusts the success response — it just can't read them back to confirm the value applied, so it won't report drift on them.
Opting out of settings sync
Set sync_settings: false to sync files only and leave Vocareum's settings untouched. This is useful when settings are managed in the Vocareum UI and you only want vocgit to push content.
Precedence (most specific wins): part → assignment → publish_options.sync_settings → defaults to true.
publish_options:
sync_settings: false # Global: don't sync any settings
assignments:
- name: "Lab 1"
sync_settings: true # ...except this assignment
parts:
- path: "part1"
sync_settings: false # ...but not this partWhen settings sync is disabled for an assignment or part, vocgit skips both pushing its settings and reporting settings drift for it on pull. The settings stay in vocareum.yaml — they're just ignored until you re-enable sync.
CLI Commands
| Command | Description |
|---------|-------------|
| vocgit init | Initialize a new course repository |
| vocgit new <path> | Create new assignment structure |
| vocgit validate | Validate configuration and structure |
| vocgit fix | Interactively fix validation issues |
| vocgit pull | Import or exclude orphaned assignments from Vocareum |
| vocgit status | Show current local sync status (default command) |
| vocgit push | Push content to Vocareum |
vocgit # Same as: vocgit status
vocgit status --verbose # Include per-assignment details
vocgit status --json # Machine-readable content sync status (for tooling)Workspace Root (--root)
Assignment and part paths in vocareum.yaml resolve against the workspace
root. By default this is the current directory, and that only works when the
config file sits directly inside it (the normal case — repo root, GitHub
Action, VS Code extension).
If you run vocgit from somewhere else, or keep the config in a subdirectory, you must say where the workspace root is — guessing it can corrupt sync state:
vocgit push --config ../course-repo/vocareum.yaml --root ../course-repo
vocgit status --config configs/vocareum.yaml --root . # nested config, cwd-relative pathsWithout --root in those situations the command fails with an explanation
rather than hashing the wrong directories. vocgit also refuses to read, upload,
or delete anything outside the workspace root, including via symlinks.
Push Options
vocgit push --dry-run # Preview changes
vocgit push --assignment lab1 # Push specific assignment
vocgit push --force-all # Re-upload everything
vocgit push --sync-deletes # Delete files not in Git (experimental)
vocgit push --non-interactive # Skip confirmation prompt
vocgit push --verbose # Detailed loggingPull Command
The pull command helps you manage assignment sync issues:
- Orphaned assignments - exist in Vocareum but not in your local config
- Stale assignments - exist in your config but were deleted from Vocareum
- Settings drift - settings in Vocareum differ from your local config
- Content drift - files in Vocareum differ from your local files (opt-in via
--content)
This is useful when:
- You've created assignments directly in the Vocareum UI
- You're onboarding an existing course to Git-based management
- Assignments were created or deleted by another team member
- Settings were changed in Vocareum UI and you want to sync them locally
- Files were edited directly in Vocareum and you want to pull those changes
vocgit pull # Interactive mode (no content drift check)
vocgit pull --verbose # Show detailed output
vocgit pull --non-interactive # Skip all issuesContent drift detection is opt-in. By default, vocgit pull (and vocgit pull --batch) does not download remote files to diff them — direct file edits in the Vocareum UI are NOT reconciled unless you ask for them. Add --content to enable the check. You can scope it with --assignment <name|id> (repeatable) and --part <part_id> (a part's part_id, not its directory name; requires exactly one --assignment):
vocgit pull --content # check all assignments for content drift
vocgit pull --batch --content # batch sync including content drift
vocgit pull --content --assignment lab1 # scope to lab1 only
vocgit pull --content --assignment lab1 --part <part_id> # scope to one part by its part_id--skip-content is a separate flag that controls orphan-import behavior only — it tells vocgit to skip downloading files for newly imported orphan assignments (useful to retry after a failed pull). It has no effect on content drift detection.
Remote content downloads fail closed if a pull would exceed built-in safety
limits: 5,000 files, 100 MiB for any single file, or 500 MiB total for one
downloadContent pass. These limits protect local machines and CI runners from
unexpectedly large or malformed remote file listings.
For orphaned assignments (in Vocareum, not in config):
- Import: Download content and add to your local repository
- Exclude: Hide from future scans (add to
excluded_assignments) - Skip: Do nothing
For stale assignments (in config, deleted from Vocareum):
- Reset ID: Clear assignment_id to allow re-creation from template
- Remove: Delete the assignment from config entirely
- Exclude: Keep in config but skip during sync
- Skip: Do nothing
For settings drift (local settings differ from Vocareum):
- Pull: Update local config with settings from Vocareum
- Keep: Keep local settings (will overwrite Vocareum on next push)
- Skip: Do nothing for now
Set publish_options.sync_settings: false to skip course, assignment, and part settings sync while still syncing files. Assignments and parts can override this with their own sync_settings value; part settings take precedence over assignment settings, which take precedence over the global publish option. Disabled settings remain in vocareum.yaml but are ignored for drift detection and push updates.
For content drift (files in Vocareum differ from local — requires --content):
- Pull: Download remote files (overwrites local files)
- Keep: Keep local files (will overwrite Vocareum on next push)
- Skip: Do nothing for now
Example workflow (with --content to include content drift):
$ vocgit pull --content
ℹ Scanning for assignment sync issues...
ℹ Found 1 orphaned assignment(s) in Vocareum.
[1/1] Lab 3: Advanced Topics (ID: 555666)
? What would you like to do? Import to local repository
? Local directory name: lab3-advanced
Part 1/2: downloaded 5 files
Part 2/2: downloaded 3 files
✓ Imported "Lab 3: Advanced Topics" to lab3-advanced/
ℹ Found 1 stale assignment(s) in config (deleted from Vocareum).
[1/1] Old Lab (ID: 777888, path: old-lab)
? This assignment was deleted from Vocareum. What would you like to do?
Reset ID (allow re-creation from template)
✓ Reset ID for "Old Lab" - will be re-created on next push
ℹ Found 1 assignment(s) with settings drift.
[1/1] Lab 1: Introduction (ID: 11111)
Part "Part 1" settings changed:
- session_length: "60" → "120"
- cloud_labs: false → true
? What would you like to do? Pull settings from Vocareum (update local config)
✓ Will update local settings for "Lab 1: Introduction"
ℹ Found 1 assignment(s) with content changes on Vocareum.
[1/1] Lab 2: Data Analysis (ID: 22222)
Content changes:
~ docs/README.md (modified)
+ scripts/new_test.py (new on remote)
? What would you like to do? Pull remote files (overwrite local)
✓ Pulled content changes for "Lab 2: Data Analysis"
Summary:
Imported: 1
Settings pulled: 1
Content pulled: 1
Excluded: 0
Removed: 0
Reset: 1
Skipped: 0
ℹ Updated vocareum.yamlThrottling the Vocareum API
vocgit schedules all Vocareum API calls through a built-in request throttle to avoid hitting rate limits, especially when pulling or pushing many assignments. The throttle is configurable via a vocareum.throttle block in vocareum.yaml:
vocareum:
org_id: "..."
course_id: "..."
throttle:
max_concurrency: 1
min_interval_ms: 300
jitter: true| Field | Default | Range | Description |
|-------|---------|-------|-------------|
| max_concurrency | 1 | 1–5 | Maximum number of concurrent Vocareum API requests |
| min_interval_ms | 300 | 0–60000 | Minimum milliseconds between request starts |
| jitter | true | — | Add random jitter to the start spacing to avoid thundering herd |
Requests are FIFO-scheduled: each request waits until both the concurrency slot and the minimum interval are satisfied before starting. Jitter applies to the start spacing only — it does not affect retry backoff.
Environment variable overrides (take precedence over vocareum.yaml):
| Variable | Overrides |
|----------|-----------|
| VOCAREUM_MAX_CONCURRENCY | vocareum.throttle.max_concurrency |
| VOCAREUM_MIN_REQUEST_INTERVAL_MS | vocareum.throttle.min_interval_ms |
| VOCAREUM_THROTTLE_JITTER | vocareum.throttle.jitter ("true" / "false") |
CI note: if you run vocgit from multiple GitHub Actions runners against the same course concurrently, use a concurrency: group to serialize them rather than raising max_concurrency:
concurrency:
group: vocareum-publish-${{ github.repository }}
cancel-in-progress: falseGitHub Action
The Action is a composite action: it installs the published vocgit CLI (pinned to the action's version) and runs vocgit push. It supports both v2 token auth and v3 OAuth.
v2 token auth:
name: Push to Vocareum
on:
push:
branches: [main]
paths: ['lab*/**', 'vocareum.yaml']
jobs:
push-to-vocareum:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Push to Vocareum
uses: ddlin/vocareum-publisher@v1
with:
config-file: vocareum.yaml
api-key: ${{ secrets.VOCAREUM_API_KEY }}
non-interactive: truev3 OAuth:
- name: Push to Vocareum (v3 OAuth)
uses: ddlin/vocareum-publisher@v1
with:
auth: oauth
client-id: ${{ secrets.VOCAREUM_OAUTH_CLIENT_ID }}
client-secret: ${{ secrets.VOCAREUM_OAUTH_CLIENT_SECRET }}
non-interactive: trueSupported action inputs (action.yml): config-file, root (workspace root, default .), api-key, auth, client-id, client-secret, dry-run, non-interactive, assignment, part, force-all, sync-deletes, auto-commit (ignored in CI), verbose. Output: success. Requires Node.js (GitHub-hosted runners include it). api-key is optional — omit it when using auth: oauth.
Directory Structure
course-repo/
├── vocareum.yaml # Configuration
├── lab1-intro/
│ └── part1/
│ ├── startercode/ # Student-visible starter code
│ ├── scripts/ # Grading scripts
│ ├── lib/ # Grading libraries (hidden)
│ ├── asnlib/ # Assignment libraries
│ ├── docs/ # Documentation
│ ├── data/ # Datasets
│ └── private/ # Private files
└── lab2-analysis/
└── ...Supported Directory Types
| Directory | Description | Synced |
|-----------|-------------|--------|
| startercode | Student-visible starter files | ✓ |
| scripts | Grading and setup scripts | ✓ |
| lib | Grading libraries (hidden from students) | ✓ |
| asnlib | Assignment libraries | ✓ |
| docs | Documentation files | ✓ |
| data | Datasets and resources | ✓ |
| private | Private course files | ✓ |
| course | Course-level shared files | ✗ |
Note: The
coursedirectory is NOT synced. It contains course-wide shared files (symlinks) that are shared across all assignments. Syncing these would cause infinite update loops. Manage course-level files directly in the Vocareum UI.
Configure which directories to sync per part:
parts:
- part_id: "123"
path: "part1"
directories: ["startercode", "scripts", "lib"] # Only sync theseImportant Notes
All IDs Are Strings
Vocareum API returns all IDs as strings. Always use string types:
# Correct
assignment_id: "12345"
# Wrong
assignment_id: 12345Local Creation, CI/CD Updates
- Create assignments locally using
vocgit new - Commit IDs to Git before CI/CD runs
- CI/CD only updates existing assignments
Template Selection
Templates can exist in any course within your organization. When you have multiple templates configured, vocgit new will prompt you to select which template to use:
$ vocgit new lab3
Multiple templates available.
? Select template for this assignment:
❯ Standard Lab (99999)
Cloud Lab (course:11111, id:88888)
Timed Exam (77777)Templates in the same course as your main course_id show just the ID. Templates in different courses show both the course and assignment ID for clarity.
The selected template ID is stored per-assignment in vocareum.yaml:
assignments:
- name: "Lab 3"
path: "lab3"
template_assignment_id: "88888" # Selected: Cloud LabNever Auto-Commit in CI/CD
The auto_commit option should only be used locally. In CI/CD it is force-disabled by the CLI.
Push Confirmation Behavior
- Local CLI prompts for confirmation before executing push.
--non-interactiveskips prompts.- CI/GitHub Actions automatically run non-interactive.
API Contract Notes
- Authentication header:
Authorization: Token <token> - Base API path:
https://api.vocareum.com/api/v2/ - Assignment copy:
POST /api/v2/courses/{courseId}/assignmentswith body:{ "method": "copy", "source": "<templateAssignmentId>", "name": "<newName>" }- Polls transaction endpoint for up to 60 seconds until complete
- Content updates: part
PUTwithcontent[].zipcontent(base64 zip)- Uses
reset: 1to clear directory before upload (ensures exact Git state) - All files in directory uploaded together as a single ZIP
- Uses
- Part updates may return
transactionid; CLI pollsGET /api/v2/transaction/{id} - Failed push runs are stored in
publish_historywithstatus: failedandfailed[]entries
ID Discovery
When an assignment or part ID is missing from config but exists in Vocareum:
- Assignment IDs are discovered by name lookup (prevents duplicate creation)
- Part IDs are discovered by seqnum mapping
- Discovered IDs are automatically saved to
vocareum.yaml
API Credentials
To use vocgit, you need a Vocareum Personal Access Token with the appropriate permissions.
Generating a Token
- Log in to Vocareum Labs
- Go to Profile > Settings > Personal Access Tokens
- Click Generate New Token
- Enter a description (e.g., "git-api")
- Set Token scope to Global
- Select the required permissions (see below)
- Click Generate and copy the token immediately (it won't be shown again)
Required Permissions
Select the following permissions when creating your token:
| Category | Permissions | Notes | |----------|-------------|-------| | courses | GET: List courses | | | assignments | GET: List assignments for a course | | | | POST: Create or copy an assignment for a course | | | | PUT: Update an assignment for a course | | | parts | GET: List parts for an assignment | | | | PUT: Update a part's data content | | | files | GET: Get the URL of a content file | | | rubrics | GET, POST, PUT, DELETE | Optional (future feature) |
All other permissions are optional.

Storing Your Token
For local CLI use:
export VOCAREUM_API_KEY="your-token-here"Or add to your shell profile (~/.bashrc, ~/.zshrc).
For GitHub Actions:
- Go to your repository's Settings > Secrets and variables > Actions
- Click New repository secret
- Name:
VOCAREUM_API_KEY - Value: Your token
Authentication: v2 Token vs v3 OAuth
vocgit supports two authentication modes:
| Mode | Flag / Env | Credential env vars | API base |
|------|-----------|---------------------|----------|
| token (default) | --auth token / VOCAREUM_AUTH_MODE=token | VOCAREUM_API_KEY or VOCAREUM_API_TOKEN | https://api.vocareum.com/api/v2 |
| oauth (opt-in) | --auth oauth / VOCAREUM_AUTH_MODE=oauth | VOCAREUM_OAUTH_CLIENT_ID + VOCAREUM_OAUTH_CLIENT_SECRET | https://labs.vocareum.com/api/v3 |
v2 token (default) — Set VOCAREUM_API_KEY (or the alias VOCAREUM_API_TOKEN) to your Personal Access Token. Requests carry Authorization: Token <token> against https://api.vocareum.com/api/v2. No additional configuration needed.
v3 OAuth (opt-in) — Set VOCAREUM_OAUTH_CLIENT_ID and VOCAREUM_OAUTH_CLIENT_SECRET and select the mode via --auth oauth or VOCAREUM_AUTH_MODE=oauth. vocgit performs an OAuth client-credentials exchange at https://labs.vocareum.com/api/v3/oauth/token, caches the resulting access token for the lifetime of the process, and retries automatically on a single 401 response. Requests carry Authorization: Bearer <access_token> against https://labs.vocareum.com/api/v3.
Optional URL overrides for v3 OAuth:
| Variable | Default |
|----------|---------|
| VOCAREUM_API_V3_BASE_URL | https://labs.vocareum.com/api/v3 |
| VOCAREUM_OAUTH_TOKEN_URL | https://labs.vocareum.com/api/v3/oauth/token |
Security guidance
Prefer environment variables or a secrets manager over CLI flags:
--client-id/--client-secretCLI flags exist but are discouraged — they appear in shell history and process listings.- Never put OAuth credentials in
vocareum.yaml; the file is committed to Git.
Usage examples
# Opt in per-invocation
vocgit push --auth oauth
# Opt in for the whole shell session
export VOCAREUM_AUTH_MODE=oauth
vocgit pull
vocgit pushCI / GitHub Actions example
Store credentials as repository secrets (VOCAREUM_OAUTH_CLIENT_ID, VOCAREUM_OAUTH_CLIENT_SECRET) and pass them as environment variables in your workflow step:
- name: Push to Vocareum (v3 OAuth)
env:
VOCAREUM_AUTH_MODE: oauth
VOCAREUM_OAUTH_CLIENT_ID: ${{ secrets.VOCAREUM_OAUTH_CLIENT_ID }}
VOCAREUM_OAUTH_CLIENT_SECRET: ${{ secrets.VOCAREUM_OAUTH_CLIENT_SECRET }}
run: vocgit push --non-interactiveEnvironment Variables
| Variable | Description |
|----------|-------------|
| VOCAREUM_API_KEY | v2 Personal Access Token (default auth mode) |
| VOCAREUM_API_TOKEN | Alias for VOCAREUM_API_KEY |
| VOCAREUM_AUTH_MODE | Auth mode: token (default) or oauth |
| VOCAREUM_OAUTH_CLIENT_ID | v3 OAuth client ID (required when VOCAREUM_AUTH_MODE=oauth) |
| VOCAREUM_OAUTH_CLIENT_SECRET | v3 OAuth client secret (required when VOCAREUM_AUTH_MODE=oauth) |
| VOCAREUM_API_V3_BASE_URL | Override the v3 API base URL (default: https://labs.vocareum.com/api/v3) |
| VOCAREUM_OAUTH_TOKEN_URL | Override the v3 token endpoint URL (default: https://labs.vocareum.com/api/v3/oauth/token) |
| VOCAREUM_MAX_CONCURRENCY | Override vocareum.throttle.max_concurrency (1–5) |
| VOCAREUM_MIN_REQUEST_INTERVAL_MS | Override vocareum.throttle.min_interval_ms (0–60000 ms) |
| VOCAREUM_THROTTLE_JITTER | Override vocareum.throttle.jitter (true / false) |
| VOCAREUM_LOG_LEVEL | Log level: ERROR, WARN, INFO, DEBUG, TRACE |
License
MIT License - see LICENSE for details.
