Merge pull request 'Package skill as installable zip (Gitea Actions)' (#2) from ci/skill-zip-release into main
Package skill / package (push) Successful in 3s
Package skill / package (push) Successful in 3s
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
name: Package skill
|
||||||
|
|
||||||
|
# Builds the installable Claude-skill zip (dist/cycling-training-<version>.zip).
|
||||||
|
#
|
||||||
|
# - Push a tag v* -> builds the zip AND attaches it to a Gitea release.
|
||||||
|
# - Run manually -> builds the zip and uploads it as a run artifact.
|
||||||
|
#
|
||||||
|
# Requirements on the Gitea side (already provisioned):
|
||||||
|
# * A registered Actions runner (image with bash/zip/curl, e.g. catthehacker/ubuntu).
|
||||||
|
# * Actions enabled for this repo (Settings > Actions).
|
||||||
|
# * Actions secrets:
|
||||||
|
# - RELEASE_TOKEN : token with repo write, used to create the release + upload the asset.
|
||||||
|
# - REGISTRY_TOKEN : token with package write, used to publish the zip to the
|
||||||
|
# Gitea generic packages registry.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version label for the zip (e.g. v1.0.0)'
|
||||||
|
required: false
|
||||||
|
default: 'manual'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Determine version
|
||||||
|
id: ver
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build skill zip
|
||||||
|
run: bash scripts/build-skill-zip.sh "${{ steps.ver.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Upload run artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: cycling-training-skill-${{ steps.ver.outputs.version }}
|
||||||
|
path: dist/*.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Attach zip to release
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
SERVER: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
VERSION: ${{ steps.ver.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ZIP="$(ls dist/*.zip | head -1)"
|
||||||
|
echo "Publishing ${ZIP} to ${SERVER}/${REPO} release ${VERSION}"
|
||||||
|
|
||||||
|
# Create the release for this tag; if it already exists, look up its id.
|
||||||
|
REL_ID="$(curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${SERVER}/api/v1/repos/${REPO}/releases" \
|
||||||
|
-d "{\"tag_name\":\"${VERSION}\",\"name\":\"${VERSION}\",\"body\":\"Installable Claude-skill package for cycling-training. Download the .zip below and import it in Claude Desktop / claude.ai (Settings > Skills) or unzip into your Claude Code skills directory.\"}" \
|
||||||
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)"
|
||||||
|
|
||||||
|
if [ -z "${REL_ID}" ]; then
|
||||||
|
REL_ID="$(curl -sS \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${SERVER}/api/v1/repos/${REPO}/releases/tags/${VERSION}" \
|
||||||
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${REL_ID}" ]; then
|
||||||
|
echo "ERROR: could not create or find a release for tag ${VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-F "attachment=@${ZIP}" \
|
||||||
|
"${SERVER}/api/v1/repos/${REPO}/releases/${REL_ID}/assets?name=$(basename "${ZIP}")"
|
||||||
|
echo "Attached $(basename "${ZIP}") to release ${VERSION} (id ${REL_ID})."
|
||||||
|
|
||||||
|
- name: Publish zip to Gitea packages registry
|
||||||
|
if: github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
SERVER: ${{ github.server_url }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
VERSION: ${{ steps.ver.outputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ZIP="$(ls dist/*.zip | head -1)"
|
||||||
|
FILE="$(basename "${ZIP}")"
|
||||||
|
PACKAGE="cycling-training-skill"
|
||||||
|
URL="${SERVER}/api/packages/${OWNER}/generic/${PACKAGE}/${VERSION}/${FILE}"
|
||||||
|
echo "Uploading ${FILE} to generic package ${PACKAGE}@${VERSION}"
|
||||||
|
CODE="$(curl -sS -o /tmp/reg_resp.txt -w '%{http_code}' -X PUT \
|
||||||
|
-H "Authorization: token ${REGISTRY_TOKEN}" \
|
||||||
|
--upload-file "${ZIP}" \
|
||||||
|
"${URL}")"
|
||||||
|
echo "HTTP ${CODE}"; cat /tmp/reg_resp.txt 2>/dev/null || true; echo
|
||||||
|
case "${CODE}" in
|
||||||
|
200|201) echo "Published ${FILE} to packages registry." ;;
|
||||||
|
409) echo "Version ${VERSION} already present in registry — skipping." ;;
|
||||||
|
*) echo "ERROR: registry upload failed (HTTP ${CODE})" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# Local Claude Code settings (machine-specific)
|
# Local Claude Code settings (machine-specific)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Build output (skill zip packages)
|
||||||
|
dist/
|
||||||
|
|
||||||
# OS / editor cruft
|
# OS / editor cruft
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -64,11 +64,39 @@ git clone <this-repo> .claude/skills/cycling-training
|
|||||||
Claude auto-discovers any `SKILL.md` under a `skills/` directory. The `description` field in the
|
Claude auto-discovers any `SKILL.md` under a `skills/` directory. The `description` field in the
|
||||||
frontmatter is what Claude matches against to decide when to load the skill, so keep it intact.
|
frontmatter is what Claude matches against to decide when to load the skill, so keep it intact.
|
||||||
|
|
||||||
**Claude.ai / Claude Desktop:** upload or sync the folder as a skill per the current skills UI.
|
**Claude Desktop / claude.ai (import a zip):** the skill ships as a packaged `.zip` whose single
|
||||||
|
top-level `cycling-training/` folder holds `SKILL.md` and the docs — the layout Claude expects. Get
|
||||||
|
the zip one of two ways, then import it in **Settings → Skills**:
|
||||||
|
|
||||||
|
- **From a release:** download `cycling-training-<version>.zip` from the repo's Releases (produced
|
||||||
|
automatically — see *Packaging & releases* below).
|
||||||
|
- **From the packages registry:** the same zip is published to the Gitea generic packages registry:
|
||||||
|
```
|
||||||
|
https://git.farh.net/api/packages/farhoodlabs/generic/cycling-training-skill/<version>/cycling-training-<version>.zip
|
||||||
|
```
|
||||||
|
- **Build it yourself:**
|
||||||
|
```bash
|
||||||
|
scripts/build-skill-zip.sh v1.0.0 # -> dist/cycling-training-v1.0.0.zip
|
||||||
|
```
|
||||||
|
|
||||||
Once installed, this skill loads automatically when you ask cycling-training questions, and it
|
Once installed, this skill loads automatically when you ask cycling-training questions, and it
|
||||||
pairs with your connected Intervals.icu MCP server for the live data.
|
pairs with your connected Intervals.icu MCP server for the live data.
|
||||||
|
|
||||||
|
## Packaging & releases
|
||||||
|
|
||||||
|
`scripts/build-skill-zip.sh [version]` stages the installable files and produces
|
||||||
|
`dist/cycling-training-<version>.zip` (validating that `SKILL.md` carries `name`/`description`
|
||||||
|
frontmatter). A Gitea Actions workflow, `.gitea/workflows/release-skill.yml`, runs it:
|
||||||
|
|
||||||
|
- **Push a tag `v*`** (e.g. `git tag v1.0.0 && git push origin v1.0.0`) → builds the zip,
|
||||||
|
**attaches it to a Gitea release** for that tag, and **publishes it to the packages registry**.
|
||||||
|
- **Run the workflow manually** (workflow_dispatch) → builds the zip and uploads it as a run
|
||||||
|
artifact you can download.
|
||||||
|
|
||||||
|
The workflow uses two Actions secrets: `RELEASE_TOKEN` (repo write — creates the release and
|
||||||
|
uploads the asset) and `REGISTRY_TOKEN` (package write — publishes to the generic packages
|
||||||
|
registry). A registered Actions runner and Actions-enabled repo are also required.
|
||||||
|
|
||||||
## How to read the docs
|
## How to read the docs
|
||||||
|
|
||||||
- **`SKILL.md` is a router, not a manual.** It points to the one reference doc that answers the
|
- **`SKILL.md` is a router, not a manual.** It points to the one reference doc that answers the
|
||||||
|
|||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Package the cycling-training skill into a zip that installs as a Claude skill
|
||||||
|
# (Claude Desktop / claude.ai "Skills", or Claude Code).
|
||||||
|
#
|
||||||
|
# The archive contains a single top-level `cycling-training/` directory holding
|
||||||
|
# SKILL.md + supporting docs — the layout Claude expects when importing a skill.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/build-skill-zip.sh [version]
|
||||||
|
# Example:
|
||||||
|
# scripts/build-skill-zip.sh v1.0.0 -> dist/cycling-training-v1.0.0.zip
|
||||||
|
# scripts/build-skill-zip.sh -> dist/cycling-training-dev.zip
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SKILL_NAME="cycling-training"
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
VERSION="${1:-dev}"
|
||||||
|
OUT_DIR="${ROOT}/dist"
|
||||||
|
STAGE="${OUT_DIR}/${SKILL_NAME}"
|
||||||
|
ZIP_NAME="${SKILL_NAME}-${VERSION}.zip"
|
||||||
|
|
||||||
|
# The files that make up the installable skill (repo/CI cruft is excluded).
|
||||||
|
INCLUDE=(SKILL.md README.md references assets)
|
||||||
|
|
||||||
|
rm -rf "${OUT_DIR}"
|
||||||
|
mkdir -p "${STAGE}"
|
||||||
|
|
||||||
|
for item in "${INCLUDE[@]}"; do
|
||||||
|
if [ ! -e "${ROOT}/${item}" ]; then
|
||||||
|
echo "ERROR: expected '${item}' not found at repo root" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp -R "${ROOT}/${item}" "${STAGE}/"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Sanity checks: a skill is only valid with a SKILL.md that has name + description.
|
||||||
|
test -f "${STAGE}/SKILL.md" || { echo "ERROR: SKILL.md missing from package" >&2; exit 1; }
|
||||||
|
grep -q "^name:" "${STAGE}/SKILL.md" || { echo "ERROR: SKILL.md has no 'name:' frontmatter" >&2; exit 1; }
|
||||||
|
grep -q "^description:" "${STAGE}/SKILL.md" || { echo "ERROR: SKILL.md has no 'description:' frontmatter" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Build the zip with the skill folder as the single top-level entry.
|
||||||
|
( cd "${OUT_DIR}" && zip -r -q "${ZIP_NAME}" "${SKILL_NAME}" -x '*.DS_Store' )
|
||||||
|
|
||||||
|
echo "Built ${OUT_DIR}/${ZIP_NAME}"
|
||||||
|
echo "Contents:"
|
||||||
|
( cd "${OUT_DIR}" && unzip -l "${ZIP_NAME}" )
|
||||||
Reference in New Issue
Block a user