3 Commits

Author SHA1 Message Date
Chris Farhood 98bde394db 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
2026-07-20 23:27:20 +00:00
Chris Farhood 67d2ba9f91 ci: use RELEASE_TOKEN + publish to packages registry
- Release step now authenticates with the RELEASE_TOKEN Actions secret
  (repo write) instead of the automatic token; drop the now-unneeded
  permissions block.
- Add a step that publishes the skill zip to the Gitea generic packages
  registry (cycling-training-skill@<tag>) using the REGISTRY_TOKEN secret,
  handling 200/201/409 responses.
- README: document the registry download URL and both secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqrhBhC3GEcKyaw8RTk8G6
2026-07-20 19:26:43 -04:00
Chris Farhood 6310b56f26 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
2026-07-20 19:23:37 -04:00
4 changed files with 194 additions and 1 deletions
+114
View File
@@ -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
+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
+29 -1
View File
@@ -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
+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}" )