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)
.claude/settings.local.json
# Build output (skill zip packages)
dist/
# OS / editor cruft
.DS_Store
*.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
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
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
- **`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}" )