ci: package skill as installable zip via Gitea Actions

Add a reproducible way to build the Claude-skill package and publish it:

- scripts/build-skill-zip.sh: stages SKILL.md + README + references/ + assets/
  into a zip whose single top-level cycling-training/ folder is the layout
  Claude Desktop / claude.ai import expects. Validates SKILL.md frontmatter.
- .gitea/workflows/release-skill.yml: on tag v* builds the zip and attaches it
  to a Gitea release (via the API using the automatic token); on manual
  dispatch uploads it as a run artifact.
- README: document the packaging/release flow and Claude Desktop zip-import.
- .gitignore: ignore dist/ build output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqrhBhC3GEcKyaw8RTk8G6
This commit is contained in:
2026-07-20 19:23:37 -04:00
parent 517986781f
commit 6310b56f26
4 changed files with 166 additions and 1 deletions
+90
View File
@@ -0,0 +1,90 @@
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:
# * A registered Actions runner (image with bash/zip/curl, e.g. catthehacker/ubuntu).
# * Actions enabled for this repo (Settings > Actions).
# * The automatic ${{ secrets.GITHUB_TOKEN }} must have contents:write so the
# release step can create the release / upload the asset (granted via the
# `permissions` block below on Gitea >= 1.20).
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
permissions:
contents: write
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.GITHUB_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})."
+3
View File
@@ -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
+25 -1
View File
@@ -64,11 +64,35 @@ 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).
- **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 and
**attaches it to a Gitea release** for that tag.
- **Run the workflow manually** (workflow_dispatch) → builds the zip and uploads it as a run
artifact you can download.
The workflow needs a registered Actions runner, Actions enabled for the repo, and the automatic
`GITHUB_TOKEN` to have `contents:write` (granted via the workflow's `permissions` block on
Gitea ≥ 1.20).
## 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
+48
View File
@@ -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}" )