CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/tessl-workflow-installer

Implements Tessl skill review GitHub Actions workflows in your repository through an interactive, configuration-first wizard.

Does it follow best practices?

Evaluation99%

1.24x

Agent success when using this tile

Validation for skill structure

Overview
Skills
Evals
Files

single-workflow.md

Tessl Skill Review CI/CD v4: Single-Workflow (Internal Use)

Overview

This version combines review and commenting into a single workflow for internal repositories where all contributors are trusted. Simpler setup with the same score diff and caching features.

Features

  • Score diff tracking with emoji indicators (🔺 🔻 ➡️)
  • Dimension-level diffs in expandable details
  • Persistent cache in git (.github/.tessl/skill-review-cache.json)
  • Auto-commit cache updates to main
  • Single workflow - No separate comment workflow needed

Setup Instructions

Prerequisites

  1. Tessl CLI - Workflow installs @tessl/cli
  2. API Key - Store TESSL_API_KEY in GitHub Secrets
  3. Internal repo - Only trusted contributors (no external forks)

Step 1: Create Single Workflow File

Create .github/workflows/tessl-skill-review.yml:

name: Tessl Skill Review

on:
  pull_request:
    branches: [main]
    paths:
      - '**/SKILL.md'
      - '**/skills/**'
      - '.github/workflows/tessl-skill-review.yml'
  push:
    branches: [main]
    paths:
      - '**/SKILL.md'
      - '**/skills/**'
  workflow_dispatch:

permissions:
  contents: write        # Required for cache commits
  pull-requests: write   # Required for PR comments

jobs:
  review-skills:
    name: Review Skills
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Tessl CLI
        run: npm install -g @tessl/cli

      - name: Detect changed skills
        id: detect
        env:
          EVENT_NAME: ${{ github.event_name }}
          BASE_REF: ${{ github.base_ref }}
        run: |
          if [[ "$EVENT_NAME" == "pull_request" ]]; then
            CHANGED_SKILLS=$(git diff --name-only --diff-filter=ACMR \
              "origin/${BASE_REF}"...HEAD \
              -- '**/SKILL.md' '**/skills/**' | \
              grep 'SKILL.md$' | \
              xargs -I {} dirname {} | \
              sort -u)
          else
            # workflow_dispatch or push: find all skills
            CHANGED_SKILLS=$(find . -name "SKILL.md" -not -path "./node_modules/*" -not -path "./.git/*" | \
              xargs -I {} dirname {} | \
              sed 's|^\./||' | \
              sort -u)
          fi

          if [[ -z "$CHANGED_SKILLS" ]]; then
            echo "No skill changes detected."
            echo "skills=" >> "$GITHUB_OUTPUT"
          else
            echo "Skills to review:"
            echo "$CHANGED_SKILLS"
            EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
            echo "skills<<${EOF_MARKER}" >> "$GITHUB_OUTPUT"
            echo "$CHANGED_SKILLS" >> "$GITHUB_OUTPUT"
            echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT"
          fi

      - name: Read review cache
        if: steps.detect.outputs.skills != ''
        id: cache
        run: |
          CACHE_FILE=".github/.tessl/skill-review-cache.json"

          if [[ -f "$CACHE_FILE" ]]; then
            echo "Cache file found, loading..."
            if CACHE_CONTENT=$(cat "$CACHE_FILE" 2>&1); then
              # Validate JSON
              if echo "$CACHE_CONTENT" | jq empty 2>/dev/null; then
                echo "cache_exists=true" >> "$GITHUB_OUTPUT"
                # Export cache to environment for review step
                EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
                echo "REVIEW_CACHE<<${EOF_MARKER}" >> "$GITHUB_ENV"
                echo "$CACHE_CONTENT" >> "$GITHUB_ENV"
                echo "${EOF_MARKER}" >> "$GITHUB_ENV"
              else
                echo "::warning::Cache file is invalid JSON, ignoring"
                echo "cache_exists=false" >> "$GITHUB_OUTPUT"
              fi
            else
              echo "::warning::Cache file exists but cannot be read: $CACHE_CONTENT"
              echo "cache_exists=false" >> "$GITHUB_OUTPUT"
            fi
          else
            echo "No cache file found, will create new one"
            echo "cache_exists=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Run skill reviews
        if: steps.detect.outputs.skills != ''
        id: review
        env:
          SKILLS: ${{ steps.detect.outputs.skills }}
          TESSL_API_KEY: ${{ secrets.TESSL_API_KEY }}
        run: |
          FAILED=0
          TABLE="| Skill | Status | Review Score | Change |"
          TABLE="${TABLE}\n|-------|--------|--------------|--------|"
          DETAILS=""

          # Create temporary file for cache entries
          CACHE_FILE_TEMP=$(mktemp)
          echo "Cache entries file: $CACHE_FILE_TEMP"

          while IFS= read -r dir; do
            [[ -z "$dir" ]] && continue
            echo "::group::Reviewing $dir"

            # Run review with --json flag
            JSON_OUTPUT=$(tessl skill review --json "$dir" 2>&1)
            echo "$JSON_OUTPUT"
            echo "::endgroup::"

            # Extract JSON (skip everything before first '{')
            JSON=$(echo "$JSON_OUTPUT" | sed -n '/{/,$p')

            # Look up previous score from cache
            PREV_SCORE=""
            PREV_DESC=""
            PREV_CONTENT=""
            if [[ -n "$REVIEW_CACHE" ]]; then
              CACHE_ENTRY=$(echo "$REVIEW_CACHE" | jq -r --arg path "$dir" '.skills[$path] // empty')
              if [[ -n "$CACHE_ENTRY" ]]; then
                PREV_SCORE=$(echo "$CACHE_ENTRY" | jq -r '.score // empty')
                PREV_DESC=$(echo "$CACHE_ENTRY" | jq -r '.dimensions.description // empty')
                PREV_CONTENT=$(echo "$CACHE_ENTRY" | jq -r '.dimensions.content // empty')
              fi
            fi

            # Validate PREV_SCORE is numeric
            if [[ -n "$PREV_SCORE" && ! "$PREV_SCORE" =~ ^[0-9]+$ ]]; then
              echo "::warning::Invalid previous score for $dir: $PREV_SCORE, ignoring"
              PREV_SCORE=""
            fi

            # Validate PREV_DESC and PREV_CONTENT are numeric
            if [[ -n "$PREV_DESC" && ! "$PREV_DESC" =~ ^[0-9]+$ ]]; then
              echo "::warning::Invalid previous description score for $dir: $PREV_DESC, ignoring"
              PREV_DESC=""
            fi
            if [[ -n "$PREV_CONTENT" && ! "$PREV_CONTENT" =~ ^[0-9]+$ ]]; then
              echo "::warning::Invalid previous content score for $dir: $PREV_CONTENT, ignoring"
              PREV_CONTENT=""
            fi

            # Extract fields via jq
            PASSED=$(echo "$JSON" | jq -r '.validation.overallPassed // false')

            # Calculate average score from all 8 dimensions
            AVG_SCORE=$(echo "$JSON" | jq -r '
              def avg(obj): (obj.scores | to_entries | map(.value.score) | add) / (obj.scores | length) * 100 / 3;
              (
                [(.descriptionJudge.evaluation | avg(.)), (.contentJudge.evaluation | avg(.))] | add / 2
              ) | round
            ')

            # Validate AVG_SCORE is numeric before arithmetic
            if [[ ! "$AVG_SCORE" =~ ^[0-9]+$ ]]; then
              echo "::error::Invalid average score calculated for $dir: $AVG_SCORE"
              AVG_SCORE=0
            fi

            # Calculate diff
            CHANGE=""
            if [[ -n "$PREV_SCORE" ]]; then
              DIFF=$((AVG_SCORE - PREV_SCORE))
              if [[ $DIFF -gt 0 ]]; then
                CHANGE="🔺 +${DIFF}% (was ${PREV_SCORE}%)"
              elif [[ $DIFF -lt 0 ]]; then
                CHANGE="🔻 ${DIFF}% (was ${PREV_SCORE}%)"
              else
                CHANGE="➡️ no change"
              fi
            fi

            # Build status column
            if [[ "$PASSED" == "true" ]]; then
              STATUS="✅ PASSED"
            else
              # Extract first validation error
              ERROR=$(echo "$JSON" | jq -r '
                .validation.checks
                | map(select(.status != "passed"))
                | first
                | .message // "Validation failed"
              ' | cut -c1-60)
              STATUS="❌ FAILED — ${ERROR}"
              FAILED=1
            fi

            DIR_DISPLAY=$(echo "$dir" | tr '|' '/')
            TABLE="${TABLE}\n| \`${DIR_DISPLAY}\` | ${STATUS} | ${AVG_SCORE}% | ${CHANGE} |"

            # Calculate dimension scores for cache and details
            DESC_SCORE=$(echo "$JSON" | jq -r '
              (.descriptionJudge.evaluation.scores | to_entries | map(.value.score) | add) * 100 / ((.descriptionJudge.evaluation.scores | length) * 3) | round
            ')
            CONTENT_SCORE=$(echo "$JSON" | jq -r '
              (.contentJudge.evaluation.scores | to_entries | map(.value.score) | add) * 100 / ((.contentJudge.evaluation.scores | length) * 3) | round
            ')

            # Validate dimension scores
            if [[ ! "$DESC_SCORE" =~ ^[0-9]+$ ]]; then
              echo "::warning::Invalid description score for $dir: $DESC_SCORE, using 0"
              DESC_SCORE=0
            fi
            if [[ ! "$CONTENT_SCORE" =~ ^[0-9]+$ ]]; then
              echo "::warning::Invalid content score for $dir: $CONTENT_SCORE, using 0"
              CONTENT_SCORE=0
            fi

            # --- Extract detailed review for collapsible section ---
            DESC_EVAL=$(echo "$JSON" | jq -r '.descriptionJudge.evaluation |
              "  Description: " + ((.scores | to_entries | map(.value.score) | add) * 100 / ((.scores | length) * 3) | round | tostring) + "%\n" +
              (.scores | to_entries | map("    \(.key): \(.value.score)/3 - \(.value.reasoning)") | join("\n")) + "\n\n" +
              "    Assessment: " + .overall_assessment
            ')

            CONTENT_EVAL=$(echo "$JSON" | jq -r '.contentJudge.evaluation |
              "  Content: " + ((.scores | to_entries | map(.value.score) | add) * 100 / ((.scores | length) * 3) | round | tostring) + "%\n" +
              (.scores | to_entries | map("    \(.key): \(.value.score)/3 - \(.value.reasoning)") | join("\n")) + "\n\n" +
              "    Assessment: " + .overall_assessment
            ')

            # Extract suggestions
            SUGGESTIONS=$(echo "$JSON" | jq -r '
              [.descriptionJudge.evaluation.suggestions // [], .contentJudge.evaluation.suggestions // []]
              | flatten
              | map("- " + .)
              | join("\n")
            ')

            # Build collapsible details block
            DETAILS="${DETAILS}\n\n<details>\n<summary><strong>${DIR_DISPLAY}</strong> — ${AVG_SCORE}% (${STATUS#* })</summary>\n\n"

            # Show score comparison if previous exists (all three must be valid)
            if [[ -n "$PREV_SCORE" && -n "$PREV_DESC" && -n "$PREV_CONTENT" ]]; then
              DETAILS="${DETAILS}**Previous:** ${PREV_SCORE}% (Description: ${PREV_DESC}%, Content: ${PREV_CONTENT}%)\n"
              DETAILS="${DETAILS}**Current:**  ${AVG_SCORE}% (Description: ${DESC_SCORE}%, Content: ${CONTENT_SCORE}%)\n\n"
              DETAILS="${DETAILS}---\n\n"
            fi

            DETAILS="${DETAILS}\`\`\`\n${DESC_EVAL}\n\n${CONTENT_EVAL}\n\`\`\`\n"

            if [[ -n "$SUGGESTIONS" ]]; then
              DETAILS="${DETAILS}\n**Suggestions:**\n\n${SUGGESTIONS}\n"
            fi

            DETAILS="${DETAILS}\n</details>"

            # Calculate content hash
            if [[ ! -f "$dir/SKILL.md" ]]; then
              echo "::error::SKILL.md not found for $dir"
              continue
            fi
            CONTENT_HASH=$(shasum -a 256 "$dir/SKILL.md" 2>&1)
            if [[ $? -ne 0 ]]; then
              echo "::error::Failed to calculate hash for $dir: $CONTENT_HASH"
              continue
            fi
            CONTENT_HASH="sha256:$(echo "$CONTENT_HASH" | awk '{print $1}')"

            # Build cache entry (compact to single line)
            if ! CACHE_ENTRY=$(jq -nc \
              --arg score "$AVG_SCORE" \
              --arg passed "$PASSED" \
              --arg hash "$CONTENT_HASH" \
              --arg ts "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
              --arg desc "$DESC_SCORE" \
              --arg content "$CONTENT_SCORE" \
              '{
                score: ($score | tonumber),
                validation_passed: ($passed == "true"),
                content_hash: $hash,
                timestamp: $ts,
                dimensions: {
                  description: ($desc | tonumber),
                  content: ($content | tonumber)
                }
              }'); then
              echo "::error::Failed to build cache entry for $dir"
              continue
            fi

            # Write cache entry to file (tab-separated: path<tab>json)
            printf '%s\t%s\n' "$dir" "$CACHE_ENTRY" >> "$CACHE_FILE_TEMP"

          done <<< "$SKILLS"

          # Save cache entries file path for update step
          echo "CACHE_ENTRIES_FILE=$CACHE_FILE_TEMP" >> "$GITHUB_ENV"
          echo "Wrote $(wc -l < "$CACHE_FILE_TEMP") cache entries to $CACHE_FILE_TEMP"

          # Build PR comment body
          COMMENT_BODY=$(printf '%b' "<!-- tessl-skill-review -->\n## Tessl Skill Review Results\n\n${TABLE}\n\n---\n\n### Detailed Review\n${DETAILS}\n\n---\n_Checks: frontmatter validity, required fields, body structure, examples, line count._\n_Review score is informational — not used for pass/fail gating._")

          EOF_MARKER=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
          echo "comment<<${EOF_MARKER}" >> "$GITHUB_OUTPUT"
          echo "$COMMENT_BODY" >> "$GITHUB_OUTPUT"
          echo "${EOF_MARKER}" >> "$GITHUB_OUTPUT"

          echo "$COMMENT_BODY" >> "$GITHUB_STEP_SUMMARY"

          if [[ "$FAILED" -eq 1 ]]; then
            echo "::error::One or more skills failed validation checks."
            exit 1
          fi

      - name: Update review cache
        if: always() && steps.detect.outputs.skills != ''
        id: update-cache
        run: |
          CACHE_FILE=".github/.tessl/skill-review-cache.json"
          mkdir -p .github/.tessl

          # Load existing cache or create new structure
          if [[ -f "$CACHE_FILE" ]]; then
            if CACHE=$(cat "$CACHE_FILE" 2>&1); then
              if ! echo "$CACHE" | jq empty 2>/dev/null; then
                echo "::warning::Cache file is invalid JSON, recreating"
                CACHE='{"version":"1","last_updated":"","skills":{}}'
              fi
            else
              echo "::warning::Cache file exists but cannot be read: $CACHE"
              CACHE='{"version":"1","last_updated":"","skills":{}}'
            fi
          else
            echo "Creating new cache file..."
            CACHE='{"version":"1","last_updated":"","skills":{}}'
          fi

          # Update timestamp
          TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
          if ! CACHE=$(echo "$CACHE" | jq --arg ts "$TIMESTAMP" '.last_updated = $ts'); then
            echo "::error::Failed to update cache timestamp"
            exit 1
          fi

          # Merge cache updates (using TAB delimiter)
          MERGED_COUNT=0
          FAILED_COUNT=0

          while IFS=$'\t' read -r skill_path entry_json; do
            [[ -z "$skill_path" ]] && continue
            if NEW_CACHE=$(echo "$CACHE" | jq --arg path "$skill_path" --argjson entry "$entry_json" \
              '.skills[$path] = $entry' 2>&1); then
              CACHE="$NEW_CACHE"
              MERGED_COUNT=$((MERGED_COUNT + 1))
            else
              echo "::warning::Failed to merge cache entry for $skill_path: $NEW_CACHE"
              FAILED_COUNT=$((FAILED_COUNT + 1))
              continue
            fi
          done < "$CACHE_ENTRIES_FILE"

          # Write cache file
          if ! echo "$CACHE" | jq '.' > "$CACHE_FILE"; then
            echo "::error::Failed to write cache file"
            exit 1
          fi

          # Report accurate merge counts
          if [[ $FAILED_COUNT -gt 0 ]]; then
            echo "Cache updated with $MERGED_COUNT entries ($FAILED_COUNT failed)"
          else
            echo "Cache updated with $MERGED_COUNT entries"
          fi

      - name: Upload cache file
        if: always() && steps.update-cache.outcome == 'success'
        uses: actions/upload-artifact@v4
        with:
          name: skill-review-cache
          path: .github/.tessl/skill-review-cache.json

      - name: Find existing PR comment
        if: github.event_name == 'pull_request' && steps.detect.outputs.skills != ''
        id: find-comment
        uses: peter-evans/find-comment@v3
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: '<!-- tessl-skill-review -->'

      - name: Post or update PR comment
        if: github.event_name == 'pull_request' && steps.detect.outputs.skills != ''
        uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-id: ${{ steps.find-comment.outputs.comment-id }}
          edit-mode: replace
          body: ${{ steps.review.outputs.comment }}

  commit-cache:
    name: Commit Cache
    runs-on: ubuntu-latest
    needs: review-skills
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    permissions:
      contents: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Download cache file
        uses: actions/download-artifact@v4
        with:
          name: skill-review-cache

      - name: Move cache to correct location
        run: |
          mkdir -p .github/.tessl
          mv skill-review-cache.json .github/.tessl/skill-review-cache.json

      - name: Check for cache changes
        id: check
        run: |
          if git diff --quiet HEAD .github/.tessl/skill-review-cache.json; then
            echo "changed=false" >> "$GITHUB_OUTPUT"
          else
            echo "changed=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Commit cache
        if: steps.check.outputs.changed == 'true'
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .github/.tessl/skill-review-cache.json
          git commit -m "chore: update skill review cache [skip ci]"
          if ! git push; then
            echo "::error::Failed to push cache update to main"
            exit 1
          fi

Step 2: Initialize Cache File

Create .github/.tessl/skill-review-cache.json:

mkdir -p .github/.tessl
cat > .github/.tessl/skill-review-cache.json << 'EOF'
{
  "version": "1",
  "last_updated": "2026-02-24T00:00:00Z",
  "skills": {}
}
EOF
git add .github/.tessl/skill-review-cache.json
git commit -m "feat: initialize skill review cache"
git push

Step 3: Add GitHub Secret

  1. Go to repository → Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Name: TESSL_API_KEY
  4. Value: Your Tessl API key
  5. Click "Add secret"

What's Different from Two-Workflow Version

✅ Removed

  • Second workflow file (tessl-skill-review-comment.yml) - No longer needed
  • Artifact save/download for PR comments - Posts directly instead
  • actions: read permission - Not needed without workflow_run

✅ Added

  • pull-requests: write permission - Allows direct PR commenting
  • Find existing comment step - Searches for previous bot comments
  • Post or update comment step - Creates or updates PR comment inline

✅ Simplified

  • One file to maintain - Single workflow instead of two
  • Faster execution - No workflow_run delay
  • Easier debugging - All steps in one place

Key Changes Explained

Permission Addition

permissions:
  contents: write        # For cache commits (same as before)
  pull-requests: write   # NEW: For PR comments

Direct PR Commenting

Instead of saving to artifact:

# OLD (two-workflow):
- name: Save PR comment artifact
  run: |
    mkdir -p pr-comment
    echo "$COMMENT_BODY" > pr-comment/comment.md

Now posts directly:

# NEW (single-workflow):
- name: Post or update PR comment
  uses: peter-evans/create-or-update-comment@v4
  with:
    issue-number: ${{ github.event.pull_request.number }}
    body: ${{ steps.review.outputs.comment }}

Comment Update Logic

Finds and updates existing comments to avoid spam:

- name: Find existing PR comment
  uses: peter-evans/find-comment@v3
  with:
    comment-author: 'github-actions[bot]'
    body-includes: '<!-- tessl-skill-review -->'

Security Considerations

✅ Safe for Internal Use Because:

  • All contributors have write access already
  • No untrusted fork contributions
  • Secrets are only exposed to trusted team members
  • PR code runs in controlled environment

❌ Do NOT Use If:

  • You accept PRs from external forks
  • Contributors don't have write access
  • Public open-source repository
  • Untrusted external contributors

Testing the Workflow

1. Initial Setup Test

# After creating workflow, trigger manually
gh workflow run "Tessl Skill Review" --ref main

Check that:

  • ✅ Reviews run successfully
  • ✅ Cache file populates
  • ✅ No PR comment (expected for manual trigger)

2. PR Comment Test

# Create a test branch
git checkout -b test/verify-workflow

# Make a small change to a skill
echo "# Test change" >> path/to/SKILL.md

# Push and create PR
git add .
git commit -m "test: verify workflow"
git push -u origin test/verify-workflow
gh pr create --title "Test workflow" --body "Testing single-workflow setup"

Check that:

  • ✅ Workflow runs on PR
  • ✅ Comment appears in PR
  • ✅ Score diff shows (if cache exists)

3. Cache Commit Test

# Merge the test PR
gh pr merge --squash

# Wait for workflow to complete (~30 seconds)
sleep 30

# Check for cache commit
git pull
git log --oneline -3

Check that:

  • ✅ New commit: "chore: update skill review cache [skip ci]"
  • ✅ Cache file contains skill entries
  • ✅ Workflow didn't trigger again (skip ci worked)

Troubleshooting

PR Comment Not Appearing

Check:

  1. Workflow has pull-requests: write permission
  2. Bot has access to repository
  3. Check workflow logs for errors

Fix:

# Verify permissions in workflow file
grep -A 2 "permissions:" .github/workflows/tessl-skill-review.yml

Comment Updates Not Working

Symptom: New comment created on each push instead of updating

Cause: Can't find previous comment

Fix: Verify the marker is consistent:

# Comment must include this HTML comment
body: "<!-- tessl-skill-review -->\n## Tessl Skill Review Results..."

Permission Denied on Push

Symptom: "refusing to allow a GitHub App to create or update workflow"

Cause: Workflow trying to modify .github/workflows/ in commit

Fix: Ensure cache commit only touches cache file:

# Should only stage cache file, not workflow
git add .github/.tessl/skill-review-cache.json

Migration from Two-Workflow Version

If you already have the two-workflow setup:

1. Delete Second Workflow

rm .github/workflows/tessl-skill-review-comment.yml
git add .github/workflows/tessl-skill-review-comment.yml
git commit -m "refactor: remove separate comment workflow"

2. Replace Main Workflow

Replace .github/workflows/tessl-skill-review.yml with the single-workflow version above.

3. Update Permissions

Ensure your repository settings allow Actions to create and approve pull requests:

  1. Go to Settings → Actions → General
  2. Scroll to "Workflow permissions"
  3. Select "Read and write permissions"
  4. Check "Allow GitHub Actions to create and approve pull requests"
  5. Save

4. Test

Create a test PR to verify commenting works.

Advantages of Single-Workflow

⚡ Performance

  • ~5-10 seconds faster - No workflow_run delay
  • Immediate feedback - Comment appears right away

🛠️ Maintainability

  • One file - Half the code to maintain
  • Simpler debugging - All steps in one place
  • Fewer artifacts - Less storage usage

👀 Visibility

  • Clearer logs - Everything in one workflow run
  • Easier troubleshooting - Single place to look

Best Practices

1. Use Branch Protection

Require workflow to pass before merging:

# In repository settings:
# Branches → Add rule → Require status checks
# Select: "Review Skills"

2. Monitor Workflow Usage

Check Actions usage regularly:

# View recent runs
gh run list --workflow="Tessl Skill Review" --limit 10

3. Keep Cache Clean

Periodically review cache size:

# Check cache file size
ls -lh .github/.tessl/skill-review-cache.json

# View cache contents
cat .github/.tessl/skill-review-cache.json | jq '.skills | keys'

Support

For issues:

  • Permission errors: Verify repository Action settings
  • Comment not posting: Check pull-requests: write permission
  • Cache issues: Same troubleshooting as two-workflow version

Changelog

Single-Workflow Version (2026-02-24)

  • Combined review and comment workflows into one
  • Added direct PR commenting with peter-evans/create-or-update-comment
  • Removed artifact-based comment passing
  • Simplified permissions and setup
  • Only for internal/trusted repositories

Install with Tessl CLI

npx tessl i tessl-labs/tessl-workflow-installer@0.0.4

README.md

single-workflow.md

SKILL.md

summary.md

tessl-workflow-installer-review.md

TESTING.md

tile.json

two-workflow.md