Publish skill to github
Skill xuluoforcainiao/publish-skill-to-github/publish-skill-to-github
Publish or update QoderWork skills to GitHub as public repositories with versioned releases. Use when the user wants to upload skills to GitHub, sync skill changes to an existing repo, create releases, or when any skill modification needs to be mirrored to GitHub. Handles both first-time publishing and incremental updates.From its SKILL.md
npx -y skills add xuluoforcainiao/publish-skill-to-github --skill publish-skill-to-githubAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.0 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
Publish / Update Skills to GitHub
Purpose
Sync local QoderWork skills to GitHub repositories. Covers both first-time publishing (create repo + upload) and incremental updates (update existing files in repo). Designed to work reliably on Windows with curl and PowerShell fallbacks.
When to Use
- User says "upload this skill to GitHub" or "publish my skill"
- User says "update the skill on GitHub" or "sync changes to GitHub"
- User modifies a skill and wants the GitHub mirror updated
- User mentions releasing, versioning, or open-sourcing a skill
Prerequisites
- GitHub account username
- GitHub Personal Access Token (classic) with
reposcope - Skills exist under
%USERPROFILE%\.qoderwork\skills\<skill-name>\
Token handling: The agent must ask the user for the token. Never persist tokens in files or chat logs. Use it only during the current operation.
Windows Environment: Critical Differences
On Windows, bash one-liners fail due to quoting and path issues. Use PowerShell or file-based JSON instead of inline JSON strings.
Base64 Encoding (Windows)
Windows has no base64 -w 0. Use PowerShell:
$bytes = [IO.File]::ReadAllBytes("C:\path\to\file.md")
$b64 = [Convert]::ToBase64String($bytes)
Or for a quick inline command:
[Convert]::ToBase64String([IO.File]::ReadAllBytes('SKILL.md')) | Set-Clipboard
JSON Payload Best Practice (Windows)
Never use inline JSON with curl on Windows cmd. Instead:
- Write JSON to a temp file
- Use
curl -d @temp.json
Example for creating a repo:
$body = @{ name = "xms-login"; private = $false; description = "QoderWork skill: xms-login" } | ConvertTo-Json -Compress
$body | Out-File -Encoding utf8 "C:\temp\repo.json" -NoNewline
curl -s -X POST "https://api.github.com/user/repos" `
-H "Authorization: token $TOKEN" `
-H "Accept: application/vnd.github.v3+json" `
-H "Content-Type: application/json" `
-d "@C:\temp\repo.json"
GitHub API via PowerShell (Recommended on Windows)
For complex operations, use Invoke-RestMethod instead of curl:
$headers = @{
Authorization = "token $TOKEN"
Accept = "application/vnd.github.v3+json"
}
# Create repo
$body = @{ name = "xms-login"; private = $false } | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/user/repos" -Method POST -Headers $headers -Body $body
# Upload file
$content = [Convert]::ToBase64String([IO.File]::ReadAllBytes("SKILL.md"))
$body = @{ message = "Update SKILL.md"; content = $content } | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/repos/$OWNER/$REPO/contents/$SKILL/SKILL.md" -Method PUT -Headers $headers -Body $body
Workflow A: First-Time Publish
Step 1: Verify local skill
$skill = "xms-login"
$skillDir = "$env:USERPROFILE\.qoderwork\skills\$skill"
Test-Path "$skillDir\SKILL.md" # Must be true
Step 2: Check if repo already exists
$owner = "xuluoforcainiao"
$repo = $skill
$TOKEN = "ghp_xxxxxxxx" # Ask user for this
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo" -Method GET -Headers @{ Authorization = "token $TOKEN" } -ErrorAction SilentlyContinue
if ($response) {
Write-Host "Repo already exists. Use Workflow B (Update) instead."
return
}
Step 3: Create repository
$body = @{
name = $repo
private = $false
description = "QoderWork skill: $skill"
} | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/user/repos" -Method POST `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
If 422 Unprocessable Entity: repo already exists. Proceed to Workflow B.
Step 4: Upload all skill files
$skillDir = "$env:USERPROFILE\.qoderwork\skills\$skill"
$files = Get-ChildItem -Path $skillDir -Recurse -File
foreach ($file in $files) {
$relPath = $file.FullName.Substring($skillDir.Length + 1).Replace("\", "/")
$repoPath = "$skill/$relPath"
$content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($file.FullName))
$body = @{
message = "Add $relPath"
content = $content
} | ConvertTo-Json -Compress
try {
Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/contents/$repoPath" -Method PUT `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
Write-Host "Uploaded: $repoPath"
} catch {
Write-Host "FAILED: $repoPath - $($_.Exception.Message)"
}
Start-Sleep -Milliseconds 500 # Rate limit buffer
}
Critical: The file path inside the repo MUST be
$skill/$relPath(e.g.,xms-login/SKILL.md), NOT justSKILL.md. GitHub skill discovery expects the nested directory structure.
Step 5: Add topics (best-effort)
$body = @{ names = @("agent-skills") } | ConvertTo-Json -Compress
try {
Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/topics" -Method PUT `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.mercy-preview+json" } `
-Body $body
} catch {
Write-Host "Topics update failed (non-critical): $($_.Exception.Message)"
}
Topics API may return 404 for newly created repos. This is non-critical; retry after 5 seconds if desired.
Step 6: Create release
$body = @{
tag_name = "v1.0.0"
target_commitish = "main"
name = "v1.0.0"
body = "Initial release of $skill"
draft = $false
prerelease = $false
} | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/releases" -Method POST `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
Workflow B: Update Existing Repo
Use this when the repo already exists and you need to sync modified files.
Step 1: Identify changed files
Compare local files against GitHub. For simplicity, upload all files (idempotent if content matches):
$skill = "xms-login"
$owner = "xuluoforcainiao"
$repo = $skill
$skillDir = "$env:USERPROFILE\.qoderwork\skills\$skill"
Step 2: Upload with SHA (update) or create
For existing files, GitHub requires the file's current SHA. If you don't have it, use this strategy:
Strategy: Try PUT without SHA first. If 422, fetch SHA and retry.
function Upload-File($localPath, $repoPath) {
$content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($localPath))
# Attempt 1: Create or update without SHA (works for new files)
$body = @{
message = "Update $repoPath"
content = $content
} | ConvertTo-Json -Compress
try {
Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/contents/$repoPath" -Method PUT `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
return
} catch {
$err = $_.Exception.Message
if ($err -like "*422*") {
# File exists but SHA mismatch or required. Fetch SHA.
try {
$fileInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/contents/$repoPath" -Method GET `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" }
$sha = $fileInfo.sha
# Attempt 2: Update with SHA
$body = @{
message = "Update $repoPath"
content = $content
sha = $sha
} | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$repo/contents/$repoPath" -Method PUT `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
return
} catch {
Write-Host "Final failure for $repoPath : $($_.Exception.Message)"
}
} else {
Write-Host "Upload failed for $repoPath : $err"
}
}
}
Step 3: Batch upload all files
$files = Get-ChildItem -Path $skillDir -Recurse -File
foreach ($file in $files) {
$relPath = $file.FullName.Substring($skillDir.Length + 1).Replace("\", "/")
$repoPath = "$skill/$relPath"
Upload-File $file.FullName $repoPath
Start-Sleep -Milliseconds 300
}
Workflow C: Batch Publish Multiple Skills
$skills = @("xms-export-automation", "xms-login", "xms-browser-interaction", "xms-environment-compatibility")
$owner = "xuluoforcainiao"
foreach ($skill in $skills) {
Write-Host "==================== $skill ===================="
# Check if repo exists
$exists = $false
try {
$null = Invoke-RestMethod -Uri "https://api.github.com/repos/$owner/$skill" -Method GET `
-Headers @{ Authorization = "token $TOKEN" } -ErrorAction Stop
$exists = $true
Write-Host "Repo exists, updating..."
} catch {
Write-Host "Repo does not exist, creating..."
}
if (-not $exists) {
# Create repo
$body = @{ name = $skill; private = $false; description = "QoderWork skill: $skill" } | ConvertTo-Json -Compress
Invoke-RestMethod -Uri "https://api.github.com/user/repos" -Method POST `
-Headers @{ Authorization = "token $TOKEN"; Accept = "application/vnd.github.v3+json" } `
-Body $body
Start-Sleep -Seconds 2
}
# Upload all files (using Workflow B logic)
$skillDir = "$env:USERPROFILE\.qoderwork\skills\$skill"
$files = Get-ChildItem -Path $skillDir -Recurse -File
foreach ($file in $files) {
$relPath = $file.FullName.Substring($skillDir.Length + 1).Replace("\", "/")
$repoPath = "$skill/$relPath"
# ... call Upload-File function ...
Start-Sleep -Milliseconds 300
}
Write-Host "DONE: $skill"
}
Sensitive File Exclusions
Before uploading, exclude files that should never be public:
| Pattern | Reason |
|---|---|
credentials.json | API keys |
*.key, *.pem | Private keys |
.env | Environment variables |
heartbeat-state.json | Runtime state |
config.json with secrets | Credentials |
Add to the upload loop:
$excludePatterns = @("credentials.json", "heartbeat-state.json", ".env")
if ($excludePatterns -contains $file.Name) {
Write-Host "Skipping sensitive file: $($file.Name)"
continue
}
Common Errors & Recovery
| Error | Cause | Fix |
|---|---|---|
422 Unprocessable Entity on file upload | File already exists and SHA not provided | Fetch SHA via GET, then retry PUT with sha field |
404 Not Found on topics API | Repo too new, topics endpoint not ready | Retry after 5s, or skip (non-critical) |
401 Unauthorized | Token invalid or expired | Ask user for a new token |
403 Rate Limit | Too many requests | Wait 60 seconds and retry |
| JSON parse error in curl | Windows quoting issues | Use PowerShell ConvertTo-Json + file-based -d @file |
| Base64 line wrapping | Used certutil instead of PowerShell | Use [Convert]::ToBase64String which never wraps |
Anti-Patterns
- Never commit GitHub tokens into repositories
- Never use inline JSON with curl on Windows cmd
- Never upload
credentials.jsonor.envfiles - Never place files at repo root; always use nested
repo-name/skill-name/structure - Do not use
base64command on Windows; it behaves differently from Linux
Post-Update Verification
After uploading, verify key files are accessible:
$urls = @(
"https://api.github.com/repos/$owner/$skill/contents/$skill/SKILL.md",
"https://api.github.com/repos/$owner/$skill/contents/$skill/CHANGELOG.md"
)
foreach ($url in $urls) {
try {
$resp = Invoke-RestMethod -Uri $url -Method GET -Headers @{ Authorization = "token $TOKEN" }
Write-Host "OK: $($resp.name) ($($resp.size) bytes)"
} catch {
Write-Host "MISSING: $url"
}
}
What ships with it: 1 file
759 B alongside SKILL.md
- README.md759 B