Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/workflows/releaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,14 @@ jobs:
id-token: write
uses: ./.github/workflows/image-build-and-publish.yml

skills-build-and-push:
name: Build and Publish Skills
skills-publish:
name: Publish Skills
needs: [ release-binaries ]
permissions:
contents: read
packages: write
uses: ./.github/workflows/skills-build-and-publish.yml
with:
push: true
id-token: write # To do keyless signing of the pushed skill artifacts
uses: ./.github/workflows/skills-publish.yml

publish-helm:
name: Publish Helm Chart
Expand Down Expand Up @@ -351,7 +350,7 @@ jobs:
- compute-build-flags
- release-binaries
- image-build-and-push
- skills-build-and-push
- skills-publish
- publish-helm
if: ${{ failure() }}
runs-on: ubuntu-slim
Expand Down
20 changes: 16 additions & 4 deletions .github/workflows/run-on-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,22 @@ jobs:
packages: write
id-token: write
uses: ./.github/workflows/image-build-and-publish.yml
skills-build-and-push:
name: Build and Publish Skills
# Named for what it does: this verifies skills still build. Publishing
# happens only from releaser.yml, via skills-publish.yml.
skills-build:
name: Build Skills
needs: [linting, tests, codegen]
permissions:
contents: read
packages: write
uses: ./.github/workflows/skills-build-and-publish.yml
uses: ./.github/workflows/skills-build.yml
skills-keyless-e2e:
name: Skills Keyless Signing E2E (staging)
needs: [linting, tests, codegen]
# Informational, not a merge gate: it signs against Sigstore's public
# staging instance, whose outages this repository does not control. The
# continue-on-error that makes it non-blocking lives on the called
# workflow's own job — GitHub rejects that key on a job that uses `uses:`.
permissions:
contents: read
id-token: write
uses: ./.github/workflows/skills-keyless-signing-e2e.yml
19 changes: 17 additions & 2 deletions .github/workflows/run-on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,22 @@ jobs:
skills-build:
name: Build Skills
needs: [linting, tests, codegen]
# Build-only, and deliberately calls a workflow that publishes nothing:
# this executes code from the pull request, so it must not hold the
# packages:write or id-token:write that skills-publish.yml uses to sign
# and push a release.
permissions:
contents: read
packages: write
uses: ./.github/workflows/skills-build-and-publish.yml
uses: ./.github/workflows/skills-build.yml
# Deliberately no skills-keyless-signing-e2e job here. It needs
# id-token: write to mint an ambient OIDC token, and it builds and runs
# repository code to use it — which on a pull request is code from the
# pull request. Even though the certificate it produces cannot impersonate
# the release signer (a different workflow path means a different SAN, and
# lock provenance pins that path), the job's environment carries
# ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN, from which that code could mint a
# token for *any* audience and exfiltrate it. Any external trust policy
# matching this repository without pinning the ref would accept it. The
# job runs post-merge from run-on-main.yml instead, where the code is
# trusted; it was already non-blocking, so it never gated a merge anyway.
# To exercise it on a branch, dispatch the "Main build" workflow there.
107 changes: 107 additions & 0 deletions .github/workflows/skills-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#
# Copyright 2025 Stacklok, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Verification-only build of the distributable Claude Code Agent Skills in
# skills/ — it proves every skill still packages cleanly, and publishes
# nothing.
#
# This is deliberately a separate workflow from skills-publish.yml rather
# than the same one behind a `push: false` input. The build runs repository
# code (`thv serve`, `thv skill build`), which on a pull request is code
# from the pull request itself; keeping publishing in its own file means
# this job never needs packages: write or id-token: write, so a pull request
# cannot reach the credentials that sign a release. GitHub validates a
# called workflow's permissions against its caller's statically, so a shared
# workflow would have forced every caller to grant the union regardless of
# the input.

name: Build Skills

on:
workflow_call:

jobs:
skills-build:
name: Build Skills
runs-on: ubuntu-latest
permissions:
contents: read

env:
BASE_REPO: "ghcr.io/stacklok/toolhive/skills"

steps:
- name: Checkout repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: 'stable'

- name: Compute version number
id: version-string
uses: ./.github/actions/compute-version

- name: Build thv binary
run: go build -o ./thv ./cmd/thv

- name: Start thv serve
run: |
./thv serve --host 127.0.0.1 --port 8080 > /tmp/thv-serve.log 2>&1 &
echo "THV_PID=$!" >> "$GITHUB_ENV"

# Wait for the server to be ready
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:8080/health > /dev/null 2>&1; then
echo "thv serve is ready (PID: $!)"
break
fi
if [ "$i" -eq 30 ]; then
echo "thv serve failed to start after 30s; logs:"
cat /tmp/thv-serve.log
exit 1
fi
sleep 1
done

# Verify process is still alive after health check
kill -0 "$!" 2>/dev/null || { echo "thv serve exited unexpectedly; logs:"; cat /tmp/thv-serve.log; exit 1; }

- name: Build skills
env:
TAG: ${{ steps.version-string.outputs.tag }}
run: |
set -euo pipefail

for skill_dir in skills/*/; do
# Skip if no skills exist
[ -d "$skill_dir" ] || continue

skill_name=$(basename "$skill_dir")
# Tagged with the real destination even though nothing is pushed,
# so the reference is validated exactly as a release would build it.
ref="${BASE_REPO}/${skill_name}:${TAG}"

echo "Building skill: ${skill_name} -> ${ref}"
built_ref=$(./thv skill build "$skill_dir" --tag "$ref")
echo "Built: ${built_ref}"
done

- name: Stop thv serve
if: always()
run: kill "$THV_PID" 2>/dev/null || true
Loading
Loading