From 6310b56f2610a14337d30a1edb5c6b01b70ab28f Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 20 Jul 2026 19:23:37 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01TqrhBhC3GEcKyaw8RTk8G6 --- .gitea/workflows/release-skill.yml | 90 ++++++++++++++++++++++++++++++ .gitignore | 3 + README.md | 26 ++++++++- scripts/build-skill-zip.sh | 48 ++++++++++++++++ 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/release-skill.yml create mode 100755 scripts/build-skill-zip.sh diff --git a/.gitea/workflows/release-skill.yml b/.gitea/workflows/release-skill.yml new file mode 100644 index 0000000..3e8513b --- /dev/null +++ b/.gitea/workflows/release-skill.yml @@ -0,0 +1,90 @@ +name: Package skill + +# Builds the installable Claude-skill zip (dist/cycling-training-.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})." diff --git a/.gitignore b/.gitignore index 5b2ff57..1e10615 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index e9a423c..1798a3d 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,35 @@ git clone .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-.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-.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 diff --git a/scripts/build-skill-zip.sh b/scripts/build-skill-zip.sh new file mode 100755 index 0000000..66fa671 --- /dev/null +++ b/scripts/build-skill-zip.sh @@ -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}" )