fix(kanban): scope worker guidance to dispatched tasks #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Site | |
| on: | |
| release: | |
| types: [published] | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'website/**' | |
| - 'skills/**' | |
| - 'optional-skills/**' | |
| - '.github/workflows/deploy-site.yml' | |
| workflow_dispatch: | |
| inputs: | |
| skills_index_run_id: | |
| description: 'Optional Build Skills Index run ID whose skills-index artifact should be deployed' | |
| required: false | |
| type: string | |
| rebuild_skills_index: | |
| description: 'Force a fresh multi-source crawl instead of reusing the latest healthy index' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| actions: read | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| jobs: | |
| deploy-vercel: | |
| # Triggered automatically on release publish (production cuts) and | |
| # manually via `gh workflow run deploy-site.yml` when an out-of-band | |
| # main commit needs to ship live before the next release tag — e.g. | |
| # a skills-index PR that doesn't touch website/** paths and so | |
| # doesn't auto-deploy via the deploy-docs path. | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Trigger Vercel Deploy | |
| run: curl -fsS --retry 3 --retry-delay 10 -X POST "${{ secrets.VERCEL_DEPLOY_HOOK }}" | |
| deploy-docs: | |
| if: github.repository == 'NousResearch/hermes-agent' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deploy.outputs.page_url }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Get GitHub App token | |
| id: app-token | |
| uses: ./.github/actions/get-app-token | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 26 | |
| cache: npm | |
| cache-dependency-path: website/package-lock.json | |
| - name: grab npm 12 | |
| run: | | |
| npm i -g npm@12 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: '3.11' | |
| - name: Install PyYAML for skill extraction | |
| uses: ./.github/actions/retry | |
| with: | |
| command: pip install pyyaml==6.0.2 httpx==0.28.1 | |
| - name: Prepare skills index (unified multi-source catalog) | |
| env: | |
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| SKILLS_INDEX_RUN_ID: ${{ github.event.inputs.skills_index_run_id || '' }} | |
| REBUILD_SKILLS_INDEX: ${{ github.event.inputs.rebuild_skills_index || 'false' }} | |
| run: | | |
| # The unified external catalog is expensive to crawl and can burn | |
| # through the repository installation's GitHub API quota when several | |
| # docs deploys land close together. Normal docs deploys therefore | |
| # reuse the latest healthy catalog: first the artifact from a | |
| # scheduled skills-index run, then the currently live index. Only a | |
| # manual force rebuild does a fresh crawl here. | |
| # | |
| # If we do crawl, the build remains fatal. build_skills_index.py runs | |
| # the health check BEFORE writing and exits non-zero on source | |
| # collapse, keeping the last good Pages deployment live instead of | |
| # publishing a degenerate catalog. | |
| set -euo pipefail | |
| INDEX_PATH="website/static/api/skills-index.json" | |
| mkdir -p "$(dirname "$INDEX_PATH")" | |
| validate_index() { | |
| python3 - "$INDEX_PATH" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| path = Path(sys.argv[1]) | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| except Exception as exc: | |
| print(f"invalid skills index JSON: {exc}", file=sys.stderr) | |
| sys.exit(1) | |
| skills = data.get("skills") | |
| if not isinstance(skills, list) or len(skills) < 1500: | |
| count = len(skills) if isinstance(skills, list) else "missing" | |
| print(f"skills index too small: {count}", file=sys.stderr) | |
| sys.exit(1) | |
| print(f"skills index ready: {len(skills)} skills") | |
| PY | |
| } | |
| if [ "$REBUILD_SKILLS_INDEX" = "true" ]; then | |
| python3 scripts/build_skills_index.py | |
| validate_index | |
| exit 0 | |
| fi | |
| if [ -n "$SKILLS_INDEX_RUN_ID" ]; then | |
| tmpdir="$(mktemp -d)" | |
| echo "Downloading skills-index artifact from run $SKILLS_INDEX_RUN_ID" | |
| if gh run download "$SKILLS_INDEX_RUN_ID" --name skills-index --dir "$tmpdir"; then | |
| candidate="$(find "$tmpdir" -name skills-index.json -type f | head -n 1 || true)" | |
| if [ -n "$candidate" ]; then | |
| cp "$candidate" "$INDEX_PATH" | |
| if validate_index; then | |
| exit 0 | |
| fi | |
| fi | |
| fi | |
| echo "::warning::Could not use skills-index artifact from run $SKILLS_INDEX_RUN_ID; trying live index" | |
| fi | |
| echo "Downloading currently live skills index" | |
| if curl -fsSL --retry 3 --retry-delay 5 \ | |
| "https://hermes-agent.nousresearch.com/docs/api/skills-index.json" \ | |
| -o "$INDEX_PATH" && validate_index; then | |
| exit 0 | |
| fi | |
| echo "::warning::Live skills index unavailable or unhealthy; falling back to a fresh crawl" | |
| rm -f "$INDEX_PATH" | |
| python3 scripts/build_skills_index.py | |
| validate_index | |
| - name: Extract skill metadata for dashboard | |
| run: python3 website/scripts/extract-skills.py | |
| - name: Regenerate per-skill docs pages + catalogs | |
| run: python3 website/scripts/generate-skill-docs.py | |
| - name: Install dependencies | |
| uses: ./.github/actions/retry | |
| with: | |
| command: npm ci | |
| working-directory: website | |
| - name: Build Docusaurus | |
| run: npm run build | |
| working-directory: website | |
| - name: Stage deployment | |
| run: | | |
| mkdir -p _site/docs | |
| cp -r website/build/* _site/docs/ | |
| # llms.txt / llms-full.txt are also published at the site root | |
| # (https://hermes-agent.nousresearch.com/llms.txt) because some | |
| # agents and IDE plugins probe the classic root-level path rather | |
| # than /docs/llms.txt. Same file, two URLs, one source of truth. | |
| if [ -f website/build/llms.txt ]; then | |
| cp website/build/llms.txt _site/llms.txt | |
| fi | |
| if [ -f website/build/llms-full.txt ]; then | |
| cp website/build/llms-full.txt _site/llms-full.txt | |
| fi | |
| # Pages serves exactly the newest artifact, so each deploy used to delete | |
| # the previous build's content-hashed JS/CSS while edge caches (Vercel → | |
| # Fastly, max-age=300 + stale-while-revalidate=3600) kept serving HTML | |
| # that referenced it — every asset request 404'd for up to ~65 minutes | |
| # after each deploy. With push-triggered deploys landing every ~15-30 | |
| # minutes, the docs were in that broken window most of the day (search, | |
| # being pure client JS, died first). Fix: keep a rolling pool of prior | |
| # builds' hashed assets and union-merge it into every artifact so stale | |
| # HTML keeps resolving. Hashed filenames are content-addressed, so a | |
| # collision is by definition the identical file — the merge never | |
| # overwrites current-build output (cp --update=none). | |
| - name: Restore asset retention pool | |
| uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: _asset_retention | |
| key: docs-asset-retention-${{ github.run_id }} | |
| restore-keys: | | |
| docs-asset-retention- | |
| - name: Merge retained assets from previous deploys | |
| run: | | |
| set -euo pipefail | |
| ASSET_DIRS="assets zh-Hans/assets" | |
| mkdir -p _asset_retention | |
| # 1) Add this build's hashed assets to the pool (fresh mtimes, so | |
| # assets still shipped by current builds never age out). | |
| for d in $ASSET_DIRS; do | |
| if [ -d "_site/docs/$d" ]; then | |
| mkdir -p "_asset_retention/$d" | |
| cp -a "_site/docs/$d/." "_asset_retention/$d/" | |
| fi | |
| done | |
| # 2) Drop pool entries no build has produced for 14 days — far | |
| # beyond any edge-cache or open-tab horizon. | |
| find _asset_retention -type f -mtime +14 -delete | |
| find _asset_retention -type d -empty -delete | |
| # 3) Union-merge the pool into the artifact; --update=none keeps the | |
| # current build authoritative for any path it produced. | |
| for d in $ASSET_DIRS; do | |
| if [ -d "_asset_retention/$d" ]; then | |
| mkdir -p "_site/docs/$d" | |
| cp -R --update=none "_asset_retention/$d/." "_site/docs/$d/" | |
| fi | |
| done | |
| echo "retention pool:" && du -sh _asset_retention | |
| echo "deployed assets:" && du -sh _site/docs/assets | |
| - name: Save asset retention pool | |
| # Always save under a fresh key (caches are immutable); restore-keys | |
| # prefix matching picks the newest on the next run. | |
| uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 | |
| with: | |
| path: _asset_retention | |
| key: docs-asset-retention-${{ github.run_id }} | |
| - name: Upload artifact | |
| uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 | |
| with: | |
| path: _site | |
| - name: Deploy to GitHub Pages | |
| id: deploy | |
| uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 |