diff --git a/.github/workflows/cgo.yml b/.github/workflows/cgo.yml new file mode 100644 index 00000000000..60964a7aee3 --- /dev/null +++ b/.github/workflows/cgo.yml @@ -0,0 +1,1684 @@ +name: CGO + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - '**.go' + - 'cmd/**' + - 'pkg/**' + - 'scripts/**' + - 'go.mod' + - 'go.sum' + - 'Makefile' + - 'actions/setup/sh/**' + - '.github/workflows/ci.yml' + - '.github/workflows/cgo.yml' + - '.github/workflows/**/*.md' + - '.github/aw/releases.json' + - '.github/aw/releases.schema.json' + - 'install-gh-aw.sh' + workflow_dispatch: +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-test + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Display Go environment + run: | + echo "Go environment:" + go env | grep -E "GOPROXY|GOSUMDB|GOMODCACHE|GOPRIVATE" + echo "" + echo "Module cache location: $(go env GOMODCACHE)" + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + # Use -x for verbose output to see what's being downloaded + if go mod download -x; then + echo "✅ Successfully downloaded Go modules" + break + else + EXIT_CODE=$? + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" + echo "This indicates that proxy.golang.org is unreachable or returning errors" + echo "" + echo "Diagnostic information:" + echo "- GOPROXY: $(go env GOPROXY)" + echo "- GOSUMDB: $(go env GOSUMDB)" + echo "- Network connectivity: checking proxy.golang.org..." + if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then + echo " ✓ proxy.golang.org is reachable" + else + echo " ✗ proxy.golang.org is NOT reachable" + fi + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Pre-flight check - Validate test dependencies + run: | + echo "Validating that test dependencies are available..." + echo "This ensures go test can compile test packages without network access." + echo "" + + # List all test dependencies to ensure they're in the cache + # This will fail fast if any dependencies are missing + echo "Checking test dependencies for all packages..." + if go list -test -deps ./... >/dev/null 2>&1; then + echo "✅ All test dependencies are available" + else + echo "❌ Failed to resolve test dependencies" + echo "" + echo "Attempting to show which dependencies are missing:" + go list -test -deps ./... 2>&1 || true + exit 1 + fi + + echo "" + echo "Module cache statistics:" + echo "- Cache directory: $(go env GOMODCACHE)" + if [ -d "$(go env GOMODCACHE)" ]; then + echo "- Cache size: $(du -sh $(go env GOMODCACHE) 2>/dev/null | cut -f1 || echo 'unknown')" + echo "- Number of cached modules: $(find $(go env GOMODCACHE) -name "go.mod" 2>/dev/null | wc -l || echo 'unknown')" + fi + + - name: Run unit tests with coverage + id: run-unit-tests + run: | + set -o pipefail + # Run tests with JSON output for artifacts, but also show failures + go test -v -parallel=8 -timeout=3m -run='^Test' -tags '!integration' -coverprofile=coverage.out -json ./... | tee test-result-unit.json + + # Check if tests failed by looking at JSON output + if grep -q '"Action":"fail"' test-result-unit.json; then + echo "❌ Tests failed - see output above" + exit 1 + fi + + # Generate coverage HTML report + go tool cover -html=coverage.out -o coverage.html + + - name: Report test failures + if: failure() && steps.run-unit-tests.outcome == 'failure' + run: | + echo "## 🔍 Unit Test Failure Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Analyzing unit test results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Run the failure report script + if ./scripts/report-test-failures.sh test-result-unit.json | tee /tmp/failure-report.txt; then + echo "No failures detected in JSON output (unexpected - tests failed but no failure records found)" >> $GITHUB_STEP_SUMMARY + else + # Script found failures - add to summary + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/failure-report.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + fi + + # Coverage reports for recent builds only - 7 days is sufficient for debugging recent changes + - name: Upload coverage report + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: coverage-report + path: coverage.html + retention-days: 7 + + - name: Upload unit test results + if: always() # Upload even if tests fail so canary-go can track coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-result-cgo-unit + path: test-result-unit.json + retention-days: 14 + + canary-go: + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: + - test + if: always() # Run even if some tests fail to report coverage + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: List unit tests in codebase + run: | + set -euo pipefail + echo "Extracting unit test function names from source files..." + go test -list='^Test' -tags '!integration' ./... 2>/dev/null | grep '^Test[A-Z]' | grep -v '^TestMain$' | sort -u > all-tests.txt + echo "Found $(wc -l < all-tests.txt) unit tests in codebase" + + - name: Download unit test result artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + path: test-results + pattern: test-result-cgo-unit + merge-multiple: false + + - name: List downloaded artifacts + run: | + set -euo pipefail + echo "Downloaded unit test result artifacts:" + find test-results -type f -name "*.json" | sort + echo "" + echo "Total JSON files: $(find test-results -type f -name \"*.json\" | wc -l)" + + - name: Extract executed tests from artifacts + run: | + set -euo pipefail + echo "Extracting test names from JSON artifacts..." + ./scripts/extract-executed-tests.sh test-results > executed-tests.txt + echo "Found $(wc -l < executed-tests.txt) executed tests" + + - name: Compare test coverage + run: | + ./scripts/compare-test-coverage.sh all-tests.txt executed-tests.txt + + - name: Upload test coverage report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-coverage-analysis-cgo + path: | + all-tests.txt + executed-tests.txt + retention-days: 14 + + build: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-build + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Node.js + id: setup-node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: actions/setup/js/package-lock.json + - name: Report Node cache status + run: | + if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then + echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY + fi + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + - name: npm ci + run: npm ci + working-directory: ./actions/setup/js + - name: Build code + run: make build + + - name: Upload Linux binary + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: gh-aw-linux-amd64 + path: gh-aw + retention-days: 14 + + - name: Report binary download instructions + run: | + RUN_ID="${{ github.run_id }}" + REPO="${{ github.repository }}" + SHA="${{ github.sha }}" + echo "## 📦 gh-aw Binary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The \`gh-aw\` Linux (amd64) binary has been uploaded as an artifact." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Download with GitHub CLI" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "gh run download ${RUN_ID} --repo ${REPO} --name gh-aw-linux-amd64" >> $GITHUB_STEP_SUMMARY + echo "chmod +x gh-aw" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Download with curl" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "# Requires a GitHub token with actions:read scope" >> $GITHUB_STEP_SUMMARY + echo "ARTIFACT_ID=\$(curl -fsSL -H \"Authorization: Bearer \$GH_TOKEN\" \\" >> $GITHUB_STEP_SUMMARY + echo " \"https://api.github.com/repos/${REPO}/actions/runs/${RUN_ID}/artifacts\" \\" >> $GITHUB_STEP_SUMMARY + echo " | jq -r '.artifacts[] | select(.name==\"gh-aw-linux-amd64\") | .id')" >> $GITHUB_STEP_SUMMARY + echo "curl -fsSL -H \"Authorization: Bearer \$GH_TOKEN\" \\" >> $GITHUB_STEP_SUMMARY + echo " -L \"https://api.github.com/repos/${REPO}/actions/artifacts/\$ARTIFACT_ID/zip\" \\" >> $GITHUB_STEP_SUMMARY + echo " -o gh-aw-linux-amd64.zip" >> $GITHUB_STEP_SUMMARY + echo "unzip gh-aw-linux-amd64.zip && chmod +x gh-aw" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Recompile workflows with this build" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY + echo "./gh-aw compile --action-mode release --action-tag ${SHA}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + - name: Rebuild lock files + run: make recompile + env: + GH_TOKEN: ${{ github.token }} + + build-wasm: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-build-wasm + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + # Use -x for verbose output to see what's being downloaded + if go mod download -x; then + echo "✅ Successfully downloaded Go modules" + break + else + EXIT_CODE=$? + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" + echo "This indicates that proxy.golang.org is unreachable or returning errors" + echo "" + echo "Diagnostic information:" + echo "- GOPROXY: $(go env GOPROXY)" + echo "- GOSUMDB: $(go env GOSUMDB)" + echo "- Network connectivity: checking proxy.golang.org..." + if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then + echo " ✓ proxy.golang.org is reachable" + else + echo " ✗ proxy.golang.org is NOT reachable" + fi + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Build WebAssembly binary + run: make build-wasm + + - name: Report binary size + run: | + echo "## WebAssembly Build" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if ls gh-aw.wasm 1>/dev/null 2>&1; then + SIZE=$(stat --format="%s" gh-aw.wasm) + SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE/1048576}") + echo "✅ Build succeeded" >> $GITHUB_STEP_SUMMARY + echo "- **Binary:** gh-aw.wasm" >> $GITHUB_STEP_SUMMARY + echo "- **Size:** ${SIZE_MB} MB (${SIZE} bytes)" >> $GITHUB_STEP_SUMMARY + else + echo "❌ No .wasm binary found" >> $GITHUB_STEP_SUMMARY + ls -la *.wasm 2>/dev/null || echo "No wasm files in working directory" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Run wasm golden tests (Go string API) + run: make test-wasm-golden + + - name: Set up Node.js + uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + with: + node-version: '20' + + - name: Run wasm binary golden tests (Node.js) + run: node scripts/test-wasm-golden.mjs + + validate-yaml: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check for ANSI escape sequences in YAML files + run: | + echo "🔍 Scanning YAML workflow files for ANSI escape sequences..." + + # Find all YAML files in .github/workflows directory + YAML_FILES=$(find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) | sort) + + # Track if any ANSI codes are found + FOUND_ANSI=0 + + # Check each file for ANSI escape sequences + for file in $YAML_FILES; do + # Use grep to find ANSI escape sequences (ESC [ ... letter) + # The pattern matches: \x1b followed by [ followed by optional digits/semicolons followed by a letter + if grep -P '\x1b\[[0-9;]*[a-zA-Z]' "$file" > /dev/null 2>&1; then + echo "❌ ERROR: Found ANSI escape sequences in: $file" + echo "" + echo "Lines with ANSI codes:" + grep -n -P '\x1b\[[0-9;]*[a-zA-Z]' "$file" || true + echo "" + FOUND_ANSI=1 + fi + done + + if [ $FOUND_ANSI -eq 1 ]; then + echo "" + echo "💡 ANSI escape sequences detected in YAML files!" + echo "" + echo "These are terminal color codes that break YAML parsing." + echo "Common causes:" + echo " - Copy-pasting from colored terminal output" + echo " - Text editors preserving ANSI codes" + echo " - Scripts generating colored output" + echo "" + echo "To fix:" + echo " 1. Remove the ANSI codes from the affected files" + echo " 2. Run 'make recompile' to regenerate workflow files" + echo " 3. Use '--no-color' flags when capturing command output" + echo "" + exit 1 + fi + + echo "✅ No ANSI escape sequences found in YAML files" + + - name: Check for release-compiled lock files + run: | + echo "🔍 Checking .lock.yml files for release build compilation..." + + # Find all .lock.yml files in the repository + LOCK_FILES=$(find . -type f -name "*.lock.yml" | sort) + + if [ -z "$LOCK_FILES" ]; then + echo "⚠️ WARNING: No .lock.yml files found" + exit 0 + fi + + # Track if any release-compiled files are found + FOUND_RELEASE=0 + + # Check each file for version numbers in the header + # Release builds include version like: "# This file was automatically generated by gh-aw (v1.0.0). DO NOT EDIT." + # Dev builds do not: "# This file was automatically generated by gh-aw. DO NOT EDIT." + for file in $LOCK_FILES; do + # Look for the pattern: "by gh-aw (v" or "by gh-aw (0" or similar version patterns + # This matches versions like (v1.0.0), (0.1.0), etc. + if grep -E '# This file was automatically generated by gh-aw \([v0-9]' "$file" > /dev/null 2>&1; then + echo "❌ ERROR: Found release-compiled lock file: $file" + echo "" + echo "Header line:" + grep -E '# This file was automatically generated by gh-aw \([v0-9]' "$file" || true + echo "" + FOUND_RELEASE=1 + fi + done + + if [ $FOUND_RELEASE -eq 1 ]; then + echo "" + echo "💡 Lock files should NOT be compiled with a release build!" + echo "" + echo "Lock files in the repository must be compiled with development builds." + echo "Release builds include version numbers in the header, which should only" + echo "appear in released binaries, not in source-controlled workflow files." + echo "" + echo "To fix:" + echo " 1. Build the CLI with 'make build' (dev build, no release flag)" + echo " 2. Run 'make recompile' to regenerate all lock files" + echo " 3. Commit the updated lock files" + echo "" + echo "The release build flag is only set during the release process via:" + echo " scripts/build-release.sh (sets -X main.isRelease=true)" + echo "" + exit 1 + fi + + echo "✅ All lock files compiled with development build (no version in header)" + + - name: Check agent file URLs use main branch + run: | + echo "🔍 Checking .github/agents/agentic-workflows.agent.md for correct branch URLs..." + + AGENT_FILE=".github/agents/agentic-workflows.agent.md" + + if [ ! -f "$AGENT_FILE" ]; then + echo "⚠️ WARNING: $AGENT_FILE not found, skipping check" + exit 0 + fi + + # Check for URLs that don't use 'main' branch + # Pattern matches: https://github.com/github/gh-aw/blob/{anything-except-main}/ + # Uses negative lookahead to exclude 'main' + INVALID_URLS=$(grep -n 'https://github.com/github/gh-aw/blob/' "$AGENT_FILE" | grep -v '/blob/main/' || true) + + if [ -n "$INVALID_URLS" ]; then + echo "❌ ERROR: Found URLs not using 'main' branch in $AGENT_FILE" + echo "" + echo "Lines with invalid URLs:" + echo "$INVALID_URLS" + echo "" + echo "💡 All GitHub URLs in agent files must reference the 'main' branch!" + echo "" + echo "URLs should use the pattern:" + echo " https://github.com/github/gh-aw/blob/main/.github/aw/..." + echo "" + echo "To fix:" + echo " 1. Edit $AGENT_FILE" + echo " 2. Replace all 'blob/{commit-hash}/' or 'blob/{tag}/' with 'blob/main/'" + echo " 3. Commit the updated file" + echo "" + exit 1 + fi + + echo "✅ All URLs in $AGENT_FILE correctly use 'main' branch" + + - name: Validate releases.json structure and version formats + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const fs = require('fs'); + const CONFIG_FILE = '.github/aw/releases.json'; + const SCHEMA_FILE = '.github/aw/releases.schema.json'; + + core.info(`🔍 Validating ${CONFIG_FILE} against ${SCHEMA_FILE}...`); + + if (!fs.existsSync(CONFIG_FILE)) { + core.setFailed(`ERROR: ${CONFIG_FILE} not found`); + return; + } + if (!fs.existsSync(SCHEMA_FILE)) { + core.setFailed(`ERROR: ${SCHEMA_FILE} not found`); + return; + } + + let config; + try { + config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); + } catch (err) { + core.setFailed(`ERROR: ${CONFIG_FILE} is not valid JSON: ${err.message}`); + return; + } + core.info(`✅ ${CONFIG_FILE} is valid JSON`); + + const errors = []; + + // Check additionalProperties (only allow known keys) + const allowedKeys = new Set(['$schema', 'blockedVersions', 'minimumVersion', 'minRecommendedVersion']); + for (const key of Object.keys(config)) { + if (!allowedKeys.has(key)) { + errors.push(`Unknown property: '${key}'`); + } + } + + // Validate blockedVersions + if ('blockedVersions' in config) { + const bv = config.blockedVersions; + if (!Array.isArray(bv)) { + errors.push("'blockedVersions' must be an array"); + } else { + const versionPattern = /^v[0-9]+\.[0-9]+\.[0-9]+$/; + const seen = new Set(); + bv.forEach((v, i) => { + if (typeof v !== 'string') { + errors.push(`'blockedVersions[${i}]' must be a string`); + } else if (!versionPattern.test(v)) { + errors.push(`'blockedVersions[${i}]' ('${v}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3')`); + } else if (seen.has(v)) { + errors.push(`'blockedVersions' contains duplicate entry: '${v}'`); + } else { + seen.add(v); + } + }); + } + } + + // Validate minimumVersion + if ('minimumVersion' in config) { + const mv = config.minimumVersion; + if (typeof mv !== 'string') { + errors.push("'minimumVersion' must be a string"); + } else if (mv !== '' && !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(mv)) { + errors.push(`'minimumVersion' ('${mv}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3' or empty string)`); + } + } + + // Validate minRecommendedVersion + if ('minRecommendedVersion' in config) { + const mrv = config.minRecommendedVersion; + if (typeof mrv !== 'string') { + errors.push("'minRecommendedVersion' must be a string"); + } else if (mrv !== '' && !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(mrv)) { + errors.push(`'minRecommendedVersion' ('${mrv}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3' or empty string)`); + } + } + + if (errors.length > 0) { + core.setFailed(`❌ ${CONFIG_FILE} schema validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`); + return; + } + + core.info(`✅ ${CONFIG_FILE} is valid and conforms to ${SCHEMA_FILE}`); + + bench: + # Only run benchmarks on main branch for performance tracking + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-bench + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Run benchmarks + run: make bench + + - name: Display benchmark summary + run: | + echo "## 📊 Benchmark Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + # Show compiler benchmarks from the results + grep "BenchmarkCompile" bench_results.txt | head -20 >> $GITHUB_STEP_SUMMARY || echo "No benchmark results found" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📁 Full results saved to artifact: benchmark-results" >> $GITHUB_STEP_SUMMARY + + # Benchmark results for performance trend analysis - 14 days allows comparison across multiple runs + - name: Save benchmark results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: benchmark-results + path: bench_results.txt + if-no-files-found: ignore + retention-days: 14 + + check-validator-sizes: + name: Check validator file sizes + runs-on: ubuntu-latest + timeout-minutes: 10 + # Non-blocking: report violations but don't fail the build until existing files are cleaned up + continue-on-error: true + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check validator file sizes + id: check + run: | + set +e + OUTPUT=$(NO_COLOR=1 bash scripts/check-validator-sizes.sh 2>&1) + EXIT_CODE=$? + echo "$OUTPUT" + echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" + echo "## Validator File Size Check" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Checking \`*_validation.go\` files against the 768-line hard limit (AGENTS.md)." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" + echo "$OUTPUT" >> "$GITHUB_STEP_SUMMARY" + echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" + if [ "$EXIT_CODE" -ne 0 ]; then + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "⚠️ This check is currently non-blocking. Fix violations to keep the codebase healthy." >> "$GITHUB_STEP_SUMMARY" + fi + exit "$EXIT_CODE" + + lint-go: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-lint-go + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 # Fetch all history for incremental linting + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + # Go formatting check (fast, no deps needed) + - name: Check Go formatting + run: | + unformatted=$(go fmt ./...) + if [ -n "$unformatted" ]; then + echo "❌ Code is not formatted. Run 'make fmt' to fix." >> $GITHUB_STEP_SUMMARY + echo "Unformatted files:" >> $GITHUB_STEP_SUMMARY + echo "$unformatted" >> $GITHUB_STEP_SUMMARY + echo "" + echo "To fix this locally, run:" + echo " make fmt" + echo "" + echo "Or format individual files with:" + echo " go fmt ./path/to/file.go" + exit 1 + fi + echo "✅ Go formatting check passed" >> $GITHUB_STEP_SUMMARY + + # Install golangci-lint binary (avoiding GPL dependencies) + # Downloads pre-built binary from GitHub releases instead of using go install + - name: Install golangci-lint + run: | + GOLANGCI_LINT_VERSION="v2.8.0" + GOOS=$(go env GOOS) + GOARCH=$(go env GOARCH) + GOPATH=$(go env GOPATH) + BINARY_NAME="golangci-lint" + + echo "Installing golangci-lint $GOLANGCI_LINT_VERSION for $GOOS/$GOARCH..." + DOWNLOAD_URL="https://github.com/golangci/golangci-lint/releases/download/$GOLANGCI_LINT_VERSION/golangci-lint-${GOLANGCI_LINT_VERSION#v}-$GOOS-$GOARCH.tar.gz" + ARCHIVE="/tmp/golangci-lint.tar.gz" + + # Save archive to disk first so we can verify it before extracting. + # --fail causes curl to exit non-zero on HTTP errors (4xx/5xx), + # --retry 3 retries transient network failures automatically. + curl --fail --retry 3 -sSL "$DOWNLOAD_URL" -o "$ARCHIVE" + tar -xz -C /tmp -f "$ARCHIVE" + mkdir -p "$GOPATH/bin" + mv /tmp/golangci-lint-*/$BINARY_NAME "$GOPATH/bin/$BINARY_NAME" + chmod +x "$GOPATH/bin/$BINARY_NAME" + + echo "✓ golangci-lint $GOLANGCI_LINT_VERSION installed" + "$GOPATH/bin/$BINARY_NAME" version + + # Run golangci-lint via Makefile for consistency + # Uses incremental linting on PRs for faster CI (50-75% speedup) + # Performance optimizations in .golangci.yml: + # - timeout: 5m prevents hanging + # - modules-download-mode: readonly uses cached modules only + - name: Run golangci-lint + run: | + export PATH="$PATH:$(go env GOPATH)/bin" + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Incremental linting on PRs - only check changed files + # This provides 50-75% faster linting on typical PRs + BASE_REF="origin/${{ github.base_ref }}" + if git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then + echo "Using incremental lint against $BASE_REF" + make golint-incremental BASE_REF="$BASE_REF" + else + echo "⚠️ Base ref $BASE_REF not found, falling back to full lint" + make golint + fi + else + # Full scan on main branch to ensure comprehensive coverage + make golint + fi + + # Error message linting (requires Go only) + - name: Lint error messages + run: make lint-errors + + actions-build: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-actions-build + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Build actions + run: make actions-build + + - name: Validate actions + run: make actions-validate + + fuzz: + # Only run fuzz tests on main branch (10s is insufficient for PRs) + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-fuzz-${{ matrix.group }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + include: + - group: Parser + tests: >- + FuzzParseFrontmatter:./pkg/parser/ + FuzzScheduleParser:./pkg/parser/ + FuzzRuntimeImportExpressionValidation:./pkg/parser/ + FuzzRuntimeImportProcessExpressions:./pkg/parser/ + - group: Workflow-Core + tests: >- + FuzzExpressionParser:./pkg/workflow/ + FuzzMentionsFiltering:./pkg/workflow/ + FuzzSanitizeOutput:./pkg/workflow/ + FuzzSanitizeIncomingText:./pkg/workflow/ + FuzzSanitizeLabelContent:./pkg/workflow/ + FuzzWrapExpressionsInTemplateConditionals:./pkg/workflow/ + - group: Workflow-Parsing + tests: >- + FuzzYAMLParsing:./pkg/workflow/ + FuzzTemplateRendering:./pkg/workflow/ + FuzzInputValidation:./pkg/workflow/ + FuzzNetworkPermissions:./pkg/workflow/ + FuzzSafeJobConfig:./pkg/workflow/ + FuzzParseLabelTriggerShorthand:./pkg/workflow/ + - group: Workflow-Triggers + tests: >- + FuzzExpandLabelTriggerShorthand:./pkg/workflow/ + FuzzValidateNoTemplateInjection:./pkg/workflow/ + FuzzRemoveHeredocContent:./pkg/workflow/ + FuzzMarkdownCodeRegionBalancer:./pkg/workflow/ + FuzzParseTriggerShorthand:./pkg/workflow/ + FuzzTriggerIRToYAMLMap:./pkg/workflow/ + FuzzParseInputDefinition:./pkg/workflow/ + FuzzParseInputDefinitions:./pkg/workflow/ + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + env: + GOPROXY: "https://proxy.golang.org,direct" + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This indicates that proxy.golang.org is unreachable or returning errors" + echo "- Network connectivity: checking proxy.golang.org..." + if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then + echo " ✓ proxy.golang.org is reachable" + else + echo " ✗ proxy.golang.org is NOT reachable" + fi + echo "Please check network connectivity or re-run the workflow once the proxy recovers" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Run fuzz tests (${{ matrix.group }}) + run: | + set -o pipefail + # Create directory for fuzz results + mkdir -p fuzz-results + + # Helper function to run fuzz test and handle context deadline + # Go fuzz tests can exit with status 1 and "context deadline exceeded" when + # they reach the -fuzztime limit. We treat this as expected success to allow + # all fuzz targets to run instead of stopping at the first timeout. + run_fuzz_test() { + local fuzz_name=$1 + local package=$2 + local output_file="fuzz-results/${fuzz_name}.txt" + + echo "Running ${fuzz_name}..." + if go test -run='^$' -fuzz="^${fuzz_name}$" -fuzztime=10s "${package}" 2>&1 | tee "${output_file}"; then + echo "✅ ${fuzz_name} completed successfully" + return 0 + else + # Check if the failure was due to context deadline (expected) + if grep -q "context deadline exceeded" "${output_file}"; then + echo "✅ ${fuzz_name} completed (context deadline reached as expected)" + return 0 + else + echo "❌ ${fuzz_name} failed with unexpected error" + return 1 + fi + fi + } + + # Run fuzz tests for this matrix group + for entry in ${{ matrix.tests }}; do + fuzz_name="${entry%%:*}" + package="${entry##*:}" + run_fuzz_test "${fuzz_name}" "${package}" + done + + # Copy fuzz corpus data (testdata/fuzz directories) + echo "Copying fuzz corpus data..." + find ./pkg -path "*/testdata/fuzz" -type d | while read -r dir; do + pkg_name=$(echo "$dir" | sed 's|^\./pkg/||' | sed 's|/testdata/fuzz$||') + echo "Copying corpus from $dir to fuzz-results/corpus/$pkg_name/" + mkdir -p "fuzz-results/corpus/$pkg_name" + cp -r "$dir"/* "fuzz-results/corpus/$pkg_name/" 2>/dev/null || echo "No corpus data in $dir" + done + + - name: Upload fuzz test results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: fuzz-results-${{ matrix.group }} + path: fuzz-results/ + retention-days: 14 + + security: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-security + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Run security regression tests + run: make test-security + + security-scan: + # Only run security scans on main branch to reduce PR overhead + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 10 # Prevent jobs from hanging indefinitely + permissions: + contents: read + strategy: + fail-fast: false + matrix: + tool: + - name: zizmor + flag: --zizmor + - name: actionlint + flag: --actionlint + - name: poutine + flag: --poutine + concurrency: + group: ci-${{ github.ref }}-security-scan-${{ matrix.tool.name }} + cancel-in-progress: true + name: "Security Scan: ${{ matrix.tool.name }}" + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Build gh-aw + run: make build + + - name: Run ${{ matrix.tool.name }} security scan on poem workflow + run: ./gh-aw compile poem-bot ${{ matrix.tool.flag }} --verbose + + mcp-server-compile-test: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-mcp-server-compile-test + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Build gh-aw binary + run: make build + + - name: Create test workflow with error + run: | + mkdir -p .github/workflows + cat > .github/workflows/test-invalid.md << 'EOF' + --- + on: push + engine: copilot + invalid_field: this will cause an error + --- + # Test Invalid Workflow + + This workflow has an invalid field that will cause a compilation error. + EOF + + - name: Test MCP server compile tool + run: | + # Create a test script using the MCP Go SDK + cat > test_mcp_compile.go << 'GOEOF' + package main + + import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + ) + + func main() { + // Create MCP client + client := mcp.NewClient(&mcp.Implementation{ + Name: "ci-test-client", + Version: "1.0.0", + }, nil) + + // Start the MCP server as a subprocess with absolute path + binaryPath := "./gh-aw" + serverCmd := exec.Command(binaryPath, "mcp-server", "--cmd", binaryPath) + transport := &mcp.CommandTransport{Command: serverCmd} + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Connect to the server + session, err := client.Connect(ctx, transport, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect to MCP server: %v\n", err) + os.Exit(1) + } + defer session.Close() + + fmt.Println("✅ Successfully connected to MCP server") + + // Call the compile tool with the invalid workflow + params := &mcp.CallToolParams{ + Name: "compile", + Arguments: map[string]any{ + "workflows": []string{"test-invalid.md"}, + }, + } + + result, err := session.CallTool(ctx, params) + if err != nil { + fmt.Fprintf(os.Stderr, "MCP tool call returned error (this is expected): %v\n", err) + // Check if the error contains expected error information + fmt.Println("✅ Compile tool correctly returned an error") + os.Exit(0) + } + + // Get the result content + if len(result.Content) == 0 { + fmt.Fprintln(os.Stderr, "❌ Expected non-empty result from compile tool") + os.Exit(1) + } + + textContent, ok := result.Content[0].(*mcp.TextContent) + if !ok { + fmt.Fprintln(os.Stderr, "❌ Expected text content from compile tool") + os.Exit(1) + } + + fmt.Printf("Compile tool output:\n%s\n", textContent.Text) + + // Parse the JSON output to check for errors + var compileResults []map[string]any + if err := json.Unmarshal([]byte(textContent.Text), &compileResults); err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse JSON output: %v\n", err) + os.Exit(1) + } + + // Check if the workflow is marked as invalid + if len(compileResults) == 0 { + fmt.Fprintln(os.Stderr, "❌ Expected at least one workflow result") + os.Exit(1) + } + + result0 := compileResults[0] + valid, ok := result0["valid"].(bool) + if !ok { + fmt.Fprintln(os.Stderr, "❌ Expected 'valid' field in result") + os.Exit(1) + } + + if valid { + fmt.Fprintln(os.Stderr, "❌ Expected workflow to be invalid") + os.Exit(1) + } + + // Check that errors field exists and has at least one error + errors, ok := result0["errors"].([]any) + if !ok || len(errors) == 0 { + fmt.Fprintln(os.Stderr, "❌ Expected errors array with at least one error") + os.Exit(1) + } + + fmt.Println("✅ Compile tool correctly reported validation errors:") + errorsJSON, _ := json.MarshalIndent(errors, " ", " ") + fmt.Printf(" %s\n", string(errorsJSON)) + + os.Exit(0) + } + GOEOF + + # Run the test + go run test_mcp_compile.go + + - name: Report test results + if: always() + run: | + echo "## MCP Server Compile Tool Test" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY + echo "1. The gh-aw MCP server can be started successfully" >> $GITHUB_STEP_SUMMARY + echo "2. The compile tool can be invoked through the MCP server" >> $GITHUB_STEP_SUMMARY + echo "3. The compile tool correctly detects and reports validation errors" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Test completed successfully" >> $GITHUB_STEP_SUMMARY + + cross-platform-build: + name: Build & Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: + - macos-latest + - windows-latest + concurrency: + group: ci-${{ github.ref }}-cross-platform-${{ matrix.os }} + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + shell: bash + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + shell: bash + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Create test workflow + shell: bash + run: | + mkdir -p .github/workflows + cat > .github/workflows/test-cross-platform.md << 'EOF' + --- + on: push + engine: copilot + --- + # Test Workflow for Cross-Platform CI + + This is a simple test workflow to verify the compile command works correctly. + + ## Task + Echo hello world. + EOF + + - name: Test compile command + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Testing compile command on ${{ matrix.os }}..." + + # Determine binary name based on OS + if [[ "$RUNNER_OS" == "Windows" ]]; then + BINARY="./gh-aw.exe" + else + BINARY="./gh-aw" + fi + + # Verify binary exists + if [ ! -f "$BINARY" ]; then + echo "❌ Binary not found: $BINARY" + ls -la + exit 1 + fi + + # Run compile command + "$BINARY" compile test-cross-platform --verbose + + # Check if lock file was generated + if [ -f ".github/workflows/test-cross-platform.lock.yml" ]; then + echo "✅ Compile succeeded - lock file generated" + else + echo "❌ Compile failed - no lock file generated" + exit 1 + fi + + echo "## Cross-Platform Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Successfully compiled workflow on ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Platform:** ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY + echo "**Binary:** $BINARY" >> $GITHUB_STEP_SUMMARY + echo "**Go version:** $(go version)" >> $GITHUB_STEP_SUMMARY + + - name: Clean up test files + if: always() + shell: bash + run: | + rm -f .github/workflows/test-cross-platform.md + rm -f .github/workflows/test-cross-platform.lock.yml + + alpine-container-test: + name: Alpine Container Test + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-alpine-container + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break + else + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Build Linux binary for Alpine + run: make build-linux + + - name: Build Alpine Docker image + run: | + echo "Building Alpine Docker image..." + docker build -t gh-aw-alpine:test \ + --build-arg BINARY=gh-aw-linux-amd64 \ + -f Dockerfile . + echo "✅ Alpine Docker image built successfully" + + - name: Test Docker image basic commands + run: | + echo "Testing Docker image basic commands..." + docker run --rm gh-aw-alpine:test --version + docker run --rm gh-aw-alpine:test --help + echo "✅ Basic commands work" + + - name: Create test workflow in container + run: | + echo "Creating test workflow file..." + mkdir -p test-workspace/.github/workflows + cat > test-workspace/.github/workflows/test-alpine.md << 'EOF' + --- + on: push + engine: copilot + --- + # Test Workflow for Alpine Container + + This is a simple test workflow to verify the compile command works correctly in Alpine container. + + ## Task + Echo hello from Alpine container. + EOF + echo "✅ Test workflow created" + + - name: Run compile through Alpine container + run: | + echo "Running compile command through Alpine container..." + docker run --rm \ + -v "$(pwd)/test-workspace:/workspace" \ + -w /workspace \ + gh-aw-alpine:test compile test-alpine --verbose + + echo "✅ Compile command executed" + + - name: Verify lock file generation + run: | + echo "Verifying lock file was generated..." + if [ -f "test-workspace/.github/workflows/test-alpine.lock.yml" ]; then + echo "✅ Lock file generated successfully" + echo "" + echo "Lock file contents:" + head -20 test-workspace/.github/workflows/test-alpine.lock.yml + else + echo "❌ Lock file not found" + ls -la test-workspace/.github/workflows/ + exit 1 + fi + + - name: Generate test summary + if: always() + run: | + echo "## Alpine Container Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY + echo "1. The Alpine Docker image can be built successfully" >> $GITHUB_STEP_SUMMARY + echo "2. The gh-aw binary works correctly in Alpine Linux" >> $GITHUB_STEP_SUMMARY + echo "3. The compile command can process workflows in the container" >> $GITHUB_STEP_SUMMARY + echo "4. Lock files are generated correctly" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ -f "test-workspace/.github/workflows/test-alpine.lock.yml" ]; then + echo "✅ All tests passed successfully" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Lock file generation failed" >> $GITHUB_STEP_SUMMARY + fi + + - name: Clean up test files + if: always() + run: | + rm -rf test-workspace + + safe-outputs-conformance: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run Safe Outputs Conformance Checker + id: conformance + continue-on-error: true + run: | + echo "## Safe Outputs Conformance Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Run the conformance checker and capture output + if ./scripts/check-safe-outputs-conformance.sh > conformance-output.txt 2>&1; then + echo "✅ All conformance checks passed" >> $GITHUB_STEP_SUMMARY + EXIT_CODE=0 + else + EXIT_CODE=$? + if [ $EXIT_CODE -eq 2 ]; then + echo "⚠️ Critical conformance issues found (treated as warning)" >> $GITHUB_STEP_SUMMARY + elif [ $EXIT_CODE -eq 1 ]; then + echo "⚠️ High priority conformance issues found (treated as warning)" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Conformance check completed with warnings" >> $GITHUB_STEP_SUMMARY + fi + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Conformance Check Output" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat conformance-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + # Also output to console for visibility + echo "=== Conformance Check Results ===" + cat conformance-output.txt + + # Always succeed (treat as warning only) + exit 0 + + - name: Upload conformance report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: safe-outputs-conformance-report + path: conformance-output.txt + retention-days: 7 diff --git a/.github/workflows/ci-coach.md b/.github/workflows/ci-coach.md index ccef9c3a9ed..74ea9b6cfe8 100644 --- a/.github/workflows/ci-coach.md +++ b/.github/workflows/ci-coach.md @@ -72,7 +72,7 @@ Follow the optimization strategies defined in the `ci-optimization-strategies` s - Verify catch-all matrix groups exist for packages with specific patterns - Identify coverage gaps and propose fixes if needed - **Use canary job outputs** to detect missing tests: - - Review `test-coverage-analysis` artifact from the `canary_go` job + - Review `test-coverage-analysis-cgo` artifact from the `canary-go` job - The canary job compares `all-tests.txt` (all tests in codebase) vs `executed-tests.txt` (tests that actually ran) - If canary job fails, investigate which tests are missing from the CI matrix - Ensure all tests defined in `*_test.go` files are covered by at least one test job pattern diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84777daf49a..62c5bdc7ff6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,2261 +1,549 @@ name: CI - -on: - push: - branches: [main] - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - '**.go' - - 'pkg/workflow/**' - - 'actions/**' - - '.github/workflows/ci.yml' - - '.github/workflows/**/*.md' - - '.github/aw/releases.json' - - '.github/aw/releases.schema.json' - - 'install-gh-aw.sh' - workflow_dispatch: -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-test - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Display Go environment - run: | - echo "Go environment:" - go env | grep -E "GOPROXY|GOSUMDB|GOMODCACHE|GOPRIVATE" - echo "" - echo "Module cache location: $(go env GOMODCACHE)" - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - # Use -x for verbose output to see what's being downloaded - if go mod download -x; then - echo "✅ Successfully downloaded Go modules" - break - else - EXIT_CODE=$? - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" - echo "This indicates that proxy.golang.org is unreachable or returning errors" - echo "" - echo "Diagnostic information:" - echo "- GOPROXY: $(go env GOPROXY)" - echo "- GOSUMDB: $(go env GOSUMDB)" - echo "- Network connectivity: checking proxy.golang.org..." - if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then - echo " ✓ proxy.golang.org is reachable" - else - echo " ✗ proxy.golang.org is NOT reachable" - fi - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Pre-flight check - Validate test dependencies - run: | - echo "Validating that test dependencies are available..." - echo "This ensures go test can compile test packages without network access." - echo "" - - # List all test dependencies to ensure they're in the cache - # This will fail fast if any dependencies are missing - echo "Checking test dependencies for all packages..." - if go list -test -deps ./... >/dev/null 2>&1; then - echo "✅ All test dependencies are available" - else - echo "❌ Failed to resolve test dependencies" - echo "" - echo "Attempting to show which dependencies are missing:" - go list -test -deps ./... 2>&1 || true - exit 1 - fi - - echo "" - echo "Module cache statistics:" - echo "- Cache directory: $(go env GOMODCACHE)" - if [ -d "$(go env GOMODCACHE)" ]; then - echo "- Cache size: $(du -sh $(go env GOMODCACHE) 2>/dev/null | cut -f1 || echo 'unknown')" - echo "- Number of cached modules: $(find $(go env GOMODCACHE) -name "go.mod" 2>/dev/null | wc -l || echo 'unknown')" - fi - - - name: Run unit tests with coverage - id: run-unit-tests - run: | - set -o pipefail - # Run tests with JSON output for artifacts, but also show failures - go test -v -parallel=8 -timeout=3m -run='^Test' -tags '!integration' -coverprofile=coverage.out -json ./... | tee test-result-unit.json - - # Check if tests failed by looking at JSON output - if grep -q '"Action":"fail"' test-result-unit.json; then - echo "❌ Tests failed - see output above" - exit 1 - fi - - # Generate coverage HTML report - go tool cover -html=coverage.out -o coverage.html - - - name: Report test failures - if: failure() && steps.run-unit-tests.outcome == 'failure' - run: | - echo "## 🔍 Unit Test Failure Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Analyzing unit test results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Run the failure report script - if ./scripts/report-test-failures.sh test-result-unit.json | tee /tmp/failure-report.txt; then - echo "No failures detected in JSON output (unexpected - tests failed but no failure records found)" >> $GITHUB_STEP_SUMMARY - else - # Script found failures - add to summary - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/failure-report.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - fi - - # Coverage reports for recent builds only - 7 days is sufficient for debugging recent changes - - name: Upload coverage report - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: coverage-report - path: coverage.html - retention-days: 7 - - - name: Upload unit test results - if: always() # Upload even if tests fail so canary_go can track coverage - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-result-unit - path: test-result-unit.json - retention-days: 14 - - integration: - runs-on: ubuntu-latest - timeout-minutes: 25 - permissions: - contents: read - strategy: - fail-fast: false - matrix: - test-group: - - name: "CLI Compile & Poutine" - packages: "./pkg/cli" - pattern: "^TestCompile[^W]|TestPoutine" # Exclude TestCompileWorkflows to avoid duplicates - - name: "CLI Safe Update" - packages: "./pkg/cli" - pattern: "^TestSafeUpdate" - - name: "CLI MCP Connectivity" - packages: "./pkg/cli" - pattern: "TestMCPInspectPlaywright|TestMCPGateway" - - name: "CLI MCP Inspect GitHub" - packages: "./pkg/cli" - pattern: "TestMCPInspectGitHub" - - name: "CLI MCP Other" - packages: "./pkg/cli" - pattern: "TestMCPAdd|TestMCPServer|TestMCPConfig" - - name: "CLI Audit Logs & Firewall" - packages: "./pkg/cli" - pattern: "TestLogs|TestFirewall|TestNoStopTime|TestLocalWorkflow|^TestAudit|^TestInspect" - - name: "CLI Progress Flag" # Isolate slow test (~65s for TestProgressFlagSignature) - packages: "./pkg/cli" - pattern: "TestProgressFlagSignature" - - name: "CLI HTTP MCP Connect" # Isolate slow HTTP MCP connection tests (~43s) - packages: "./pkg/cli" - pattern: "TestConnectHTTPMCPServer" - - name: "CLI Compile Workflows" # Isolate slow workflow compilation test - packages: "./pkg/cli" - pattern: "TestCompileWorkflows_EmptyMarkdown" - - name: "CLI Security Tools" # Group security tool compilation tests - packages: "./pkg/cli" - pattern: "TestCompileWithZizmor|TestCompileWithPoutine|TestCompileWithPoutineAndZizmor" - - name: "CLI Add & List Commands" - packages: "./pkg/cli" - pattern: "^TestAdd|^TestList" - - name: "CLI Update Command" - packages: "./pkg/cli" - pattern: "^TestUpdate" - - name: "CLI Docker Build" # Isolate slow Docker tests (~43s) - packages: "./pkg/cli" - pattern: "TestDockerBuild|TestDockerImage" - - name: "CLI Completion & Other" # Remaining catch-all (reduced from original) - packages: "./pkg/cli" - pattern: "" # Catch-all for tests not matched by other CLI patterns - skip_pattern: "^TestCompile[^W]|TestPoutine|TestSafeUpdate|TestMCPInspectPlaywright|TestMCPGateway|TestMCPAdd|TestMCPInspectGitHub|TestMCPServer|TestMCPConfig|TestLogs|TestFirewall|TestNoStopTime|TestLocalWorkflow|TestProgressFlagSignature|TestConnectHTTPMCPServer|TestCompileWorkflows_EmptyMarkdown|TestCompileWithZizmor|TestCompileWithPoutine|TestCompileWithPoutineAndZizmor|^TestAdd|^TestList|^TestUpdate|^TestAudit|^TestInspect|TestDockerBuild|TestDockerImage" - - name: "Workflow Compiler" - packages: "./pkg/workflow" - pattern: "TestCompile|TestWorkflow|TestGenerate|TestParse" - - name: "Workflow Tools & MCP" - packages: "./pkg/workflow" - pattern: "TestMCP|TestTool|TestSkill|TestPlaywright|TestFirewall" - - name: "Workflow Validation" - packages: "./pkg/workflow" - pattern: "TestValidat|TestLock|TestError|TestWarning" - - name: "Workflow Features" - packages: "./pkg/workflow" - pattern: "SafeOutputs|CreatePullRequest|OutputLabel|HasSafeOutputs|GitHub|Git|PushToPullRequest|BuildFromAllowed|TestAgent|TestCopilot|TestCustom|TestEngine|TestModel|TestNetwork|TestOpenAI|TestProvider" - - name: "Workflow Rendering & Bundling" - packages: "./pkg/workflow" - pattern: "Render|Bundle|Script|WritePromptText" - - name: "Workflow Infra" - packages: "./pkg/workflow" - pattern: "^TestCache|TestCacheDependencies|TestCacheKey|TestValidateCache|TestPermissions|TestPackageExtractor|TestCollectPackagesFromWorkflow|Dependabot|Security|PII|Runtime|Setup|Install|Download|Version|Binary|String|Sanitize|Normalize|Trim|Clean|Format" - - name: "Workflow Actions & Containers" - packages: "./pkg/workflow" - pattern: "^TestAction[^P]|Container" - - name: "CMD Tests" # All cmd/gh-aw integration tests - packages: "./cmd/gh-aw" - pattern: "" - skip_pattern: "" # No other groups cover cmd tests - - name: "Parser Remote Fetch & Cache" - packages: "./pkg/parser" - pattern: "TestDownloadFileFromGitHub|TestResolveIncludePath|TestDownloadIncludeFromWorkflowSpec|TestImportCache" - - name: "Parser Location & Validation" - packages: "./pkg/parser" - pattern: "" # Catch-all for tests not matched by other Parser patterns - skip_pattern: "TestDownloadFileFromGitHub|TestResolveIncludePath|TestDownloadIncludeFromWorkflowSpec|TestImportCache" - - name: "Workflow Misc Part 2" # Remaining workflow tests - packages: "./pkg/workflow" - pattern: "" - skip_pattern: "TestCompile|TestWorkflow|TestGenerate|TestParse|TestMCP|TestTool|TestSkill|TestPlaywright|TestFirewall|TestValidat|TestLock|TestError|TestWarning|SafeOutputs|CreatePullRequest|OutputLabel|HasSafeOutputs|GitHub|Git|PushToPullRequest|BuildFromAllowed|Render|Bundle|Script|WritePromptText|^TestCache|TestCacheDependencies|TestCacheKey|TestValidateCache|^TestAction[^P]|Container|Dependabot|Security|PII|TestPermissions|TestPackageExtractor|TestCollectPackagesFromWorkflow|TestAgent|TestCopilot|TestCustom|TestEngine|TestModel|TestNetwork|TestOpenAI|TestProvider|String|Sanitize|Normalize|Trim|Clean|Format|Runtime|Setup|Install|Download|Version|Binary" - concurrency: - group: ci-${{ github.ref }}-integration-${{ matrix.test-group.name }} - cancel-in-progress: true - name: "Integration: ${{ matrix.test-group.name }}" - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Display Go environment - run: | - echo "Go environment:" - go env | grep -E "GOPROXY|GOSUMDB|GOMODCACHE|GOPRIVATE" - echo "" - echo "Module cache location: $(go env GOMODCACHE)" - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - # Use -x for verbose output to see what's being downloaded - if go mod download -x; then - echo "✅ Successfully downloaded Go modules" - break - else - EXIT_CODE=$? - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" - echo "This indicates that proxy.golang.org is unreachable or returning errors" - echo "" - echo "Diagnostic information:" - echo "- GOPROXY: $(go env GOPROXY)" - echo "- GOSUMDB: $(go env GOSUMDB)" - echo "- Network connectivity: checking proxy.golang.org..." - if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then - echo " ✓ proxy.golang.org is reachable" - else - echo " ✗ proxy.golang.org is NOT reachable" - fi - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Pre-flight check - Validate test dependencies - run: | - echo "Validating that test dependencies are available..." - echo "This ensures go test can compile test packages without network access." - echo "" - - # List all test dependencies to ensure they're in the cache - # This will fail fast if any dependencies are missing - echo "Checking test dependencies for ${{ matrix.test-group.packages }}..." - if go list -test -deps ${{ matrix.test-group.packages }} >/dev/null 2>&1; then - echo "✅ All test dependencies are available" - else - echo "❌ Failed to resolve test dependencies" - echo "" - echo "Attempting to show which dependencies are missing:" - go list -test -deps ${{ matrix.test-group.packages }} 2>&1 || true - exit 1 - fi - - echo "" - echo "Module cache statistics:" - echo "- Cache directory: $(go env GOMODCACHE)" - if [ -d "$(go env GOMODCACHE)" ]; then - echo "- Cache size: $(du -sh $(go env GOMODCACHE) 2>/dev/null | cut -f1 || echo 'unknown')" - echo "- Number of cached modules: $(find $(go env GOMODCACHE) -name "go.mod" 2>/dev/null | wc -l || echo 'unknown')" - fi - - - name: Build gh-aw binary for integration tests - run: make build - - - name: Run integration tests - ${{ matrix.test-group.name }} - id: run-tests - run: | - set -o pipefail - # Sanitize the test group name for use in filename - SAFE_NAME=$(echo "${{ matrix.test-group.name }}" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g') - - if [ -z "${{ matrix.test-group.pattern }}" ]; then - # Catch-all group: run with -skip to exclude tests matched by other groups - if [ -n "${{ matrix.test-group.skip_pattern || '' }}" ]; then - go test -v -parallel=8 -timeout=10m -tags 'integration' -skip '${{ matrix.test-group.skip_pattern }}' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" - else - go test -v -parallel=8 -timeout=10m -tags 'integration' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" - fi - else - go test -v -parallel=8 -timeout=10m -tags 'integration' -run '${{ matrix.test-group.pattern }}' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" - fi - - - name: Report test failures - if: failure() && steps.run-tests.outcome == 'failure' - run: | - # Sanitize the test group name to match the file created in the previous step - SAFE_NAME=$(echo "${{ matrix.test-group.name }}" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g') - - echo "## 🔍 Test Failure Analysis" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Analyzing test results for: **${{ matrix.test-group.name }}**" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Run the failure report script - if ./scripts/report-test-failures.sh "test-result-integration-${SAFE_NAME}.json" | tee /tmp/failure-report.txt; then - echo "No failures detected in JSON output (unexpected - tests failed but no failure records found)" >> $GITHUB_STEP_SUMMARY - else - # Script found failures - add to summary - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - cat /tmp/failure-report.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - fi - - - name: Upload integration test results - if: always() # Upload even if tests fail so canary_go can track coverage - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-result-integration-${{ matrix.test-group.name }} - path: test-result-integration-*.json - retention-days: 14 - - canary_go: - runs-on: ubuntu-latest - timeout-minutes: 15 - needs: [integration] # test dependency removed - download-artifact fetches by name, not job dependency - if: always() # Run even if some tests fail to report coverage - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: List all tests in codebase - run: | - set -euo pipefail - echo "Extracting all test function names from source files..." - ./scripts/list-all-tests.sh > all-tests.txt - echo "Found $(wc -l < all-tests.txt) tests in codebase" - - - name: Download all test result artifacts - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 - with: - path: test-results - pattern: test-result-* - merge-multiple: false - - - name: List downloaded artifacts - run: | - set -euo pipefail - echo "Downloaded test result artifacts:" - find test-results -type f -name "*.json" | sort - echo "" - echo "Total JSON files: $(find test-results -type f -name "*.json" | wc -l)" - - - name: Extract executed tests from artifacts - run: | - set -euo pipefail - echo "Extracting test names from JSON artifacts..." - ./scripts/extract-executed-tests.sh test-results > executed-tests.txt - echo "Found $(wc -l < executed-tests.txt) executed tests" - - - name: Compare test coverage - run: | - ./scripts/compare-test-coverage.sh all-tests.txt executed-tests.txt - - - name: Upload test coverage report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-coverage-analysis - path: | - all-tests.txt - executed-tests.txt - retention-days: 14 - - update: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-update - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Test update command (dry-run) - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "Testing update command to ensure it runs successfully..." - # Run update with verbose flag to check for and apply workflow updates from source repositories - # The command may modify workflow files if upstream updates are available - ./gh-aw update --verbose - echo "✅ Update command executed successfully" >> $GITHUB_STEP_SUMMARY - - build: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-build - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Node.js - id: setup-node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: "24" - cache: npm - cache-dependency-path: actions/setup/js/package-lock.json - - name: Report Node cache status - run: | - if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then - echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY - fi - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - name: npm ci - run: npm ci - working-directory: ./actions/setup/js - - name: Build code - run: make build - - - name: Upload Linux binary - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: gh-aw-linux-amd64 - path: gh-aw - retention-days: 14 - - - name: Report binary download instructions - run: | - RUN_ID="${{ github.run_id }}" - REPO="${{ github.repository }}" - SHA="${{ github.sha }}" - echo "## 📦 gh-aw Binary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The \`gh-aw\` Linux (amd64) binary has been uploaded as an artifact." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Download with GitHub CLI" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "gh run download ${RUN_ID} --repo ${REPO} --name gh-aw-linux-amd64" >> $GITHUB_STEP_SUMMARY - echo "chmod +x gh-aw" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Download with curl" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "# Requires a GitHub token with actions:read scope" >> $GITHUB_STEP_SUMMARY - echo "ARTIFACT_ID=\$(curl -fsSL -H \"Authorization: Bearer \$GH_TOKEN\" \\" >> $GITHUB_STEP_SUMMARY - echo " \"https://api.github.com/repos/${REPO}/actions/runs/${RUN_ID}/artifacts\" \\" >> $GITHUB_STEP_SUMMARY - echo " | jq -r '.artifacts[] | select(.name==\"gh-aw-linux-amd64\") | .id')" >> $GITHUB_STEP_SUMMARY - echo "curl -fsSL -H \"Authorization: Bearer \$GH_TOKEN\" \\" >> $GITHUB_STEP_SUMMARY - echo " -L \"https://api.github.com/repos/${REPO}/actions/artifacts/\$ARTIFACT_ID/zip\" \\" >> $GITHUB_STEP_SUMMARY - echo " -o gh-aw-linux-amd64.zip" >> $GITHUB_STEP_SUMMARY - echo "unzip gh-aw-linux-amd64.zip && chmod +x gh-aw" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Recompile workflows with this build" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "./gh-aw compile --action-mode release --action-tag ${SHA}" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - - name: Rebuild lock files - run: make recompile - env: - GH_TOKEN: ${{ github.token }} - - build-wasm: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-build-wasm - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - # Use -x for verbose output to see what's being downloaded - if go mod download -x; then - echo "✅ Successfully downloaded Go modules" - break - else - EXIT_CODE=$? - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" - echo "This indicates that proxy.golang.org is unreachable or returning errors" - echo "" - echo "Diagnostic information:" - echo "- GOPROXY: $(go env GOPROXY)" - echo "- GOSUMDB: $(go env GOSUMDB)" - echo "- Network connectivity: checking proxy.golang.org..." - if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then - echo " ✓ proxy.golang.org is reachable" - else - echo " ✗ proxy.golang.org is NOT reachable" - fi - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build WebAssembly binary - run: make build-wasm - - - name: Report binary size - run: | - echo "## WebAssembly Build" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if ls gh-aw.wasm 1>/dev/null 2>&1; then - SIZE=$(stat --format="%s" gh-aw.wasm) - SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE/1048576}") - echo "✅ Build succeeded" >> $GITHUB_STEP_SUMMARY - echo "- **Binary:** gh-aw.wasm" >> $GITHUB_STEP_SUMMARY - echo "- **Size:** ${SIZE_MB} MB (${SIZE} bytes)" >> $GITHUB_STEP_SUMMARY - else - echo "❌ No .wasm binary found" >> $GITHUB_STEP_SUMMARY - ls -la *.wasm 2>/dev/null || echo "No wasm files in working directory" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Run wasm golden tests (Go string API) - run: make test-wasm-golden - - - name: Set up Node.js - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 - with: - node-version: '20' - - - name: Run wasm binary golden tests (Node.js) - run: node scripts/test-wasm-golden.mjs - - validate-yaml: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check for ANSI escape sequences in YAML files - run: | - echo "🔍 Scanning YAML workflow files for ANSI escape sequences..." - - # Find all YAML files in .github/workflows directory - YAML_FILES=$(find .github/workflows -type f \( -name "*.yml" -o -name "*.yaml" \) | sort) - - # Track if any ANSI codes are found - FOUND_ANSI=0 - - # Check each file for ANSI escape sequences - for file in $YAML_FILES; do - # Use grep to find ANSI escape sequences (ESC [ ... letter) - # The pattern matches: \x1b followed by [ followed by optional digits/semicolons followed by a letter - if grep -P '\x1b\[[0-9;]*[a-zA-Z]' "$file" > /dev/null 2>&1; then - echo "❌ ERROR: Found ANSI escape sequences in: $file" - echo "" - echo "Lines with ANSI codes:" - grep -n -P '\x1b\[[0-9;]*[a-zA-Z]' "$file" || true - echo "" - FOUND_ANSI=1 - fi - done - - if [ $FOUND_ANSI -eq 1 ]; then - echo "" - echo "💡 ANSI escape sequences detected in YAML files!" - echo "" - echo "These are terminal color codes that break YAML parsing." - echo "Common causes:" - echo " - Copy-pasting from colored terminal output" - echo " - Text editors preserving ANSI codes" - echo " - Scripts generating colored output" - echo "" - echo "To fix:" - echo " 1. Remove the ANSI codes from the affected files" - echo " 2. Run 'make recompile' to regenerate workflow files" - echo " 3. Use '--no-color' flags when capturing command output" - echo "" - exit 1 - fi - - echo "✅ No ANSI escape sequences found in YAML files" - - - name: Check for release-compiled lock files - run: | - echo "🔍 Checking .lock.yml files for release build compilation..." - - # Find all .lock.yml files in the repository - LOCK_FILES=$(find . -type f -name "*.lock.yml" | sort) - - if [ -z "$LOCK_FILES" ]; then - echo "⚠️ WARNING: No .lock.yml files found" - exit 0 - fi - - # Track if any release-compiled files are found - FOUND_RELEASE=0 - - # Check each file for version numbers in the header - # Release builds include version like: "# This file was automatically generated by gh-aw (v1.0.0). DO NOT EDIT." - # Dev builds do not: "# This file was automatically generated by gh-aw. DO NOT EDIT." - for file in $LOCK_FILES; do - # Look for the pattern: "by gh-aw (v" or "by gh-aw (0" or similar version patterns - # This matches versions like (v1.0.0), (0.1.0), etc. - if grep -E '# This file was automatically generated by gh-aw \([v0-9]' "$file" > /dev/null 2>&1; then - echo "❌ ERROR: Found release-compiled lock file: $file" - echo "" - echo "Header line:" - grep -E '# This file was automatically generated by gh-aw \([v0-9]' "$file" || true - echo "" - FOUND_RELEASE=1 - fi - done - - if [ $FOUND_RELEASE -eq 1 ]; then - echo "" - echo "💡 Lock files should NOT be compiled with a release build!" - echo "" - echo "Lock files in the repository must be compiled with development builds." - echo "Release builds include version numbers in the header, which should only" - echo "appear in released binaries, not in source-controlled workflow files." - echo "" - echo "To fix:" - echo " 1. Build the CLI with 'make build' (dev build, no release flag)" - echo " 2. Run 'make recompile' to regenerate all lock files" - echo " 3. Commit the updated lock files" - echo "" - echo "The release build flag is only set during the release process via:" - echo " scripts/build-release.sh (sets -X main.isRelease=true)" - echo "" - exit 1 - fi - - echo "✅ All lock files compiled with development build (no version in header)" - - - name: Check agent file URLs use main branch - run: | - echo "🔍 Checking .github/agents/agentic-workflows.agent.md for correct branch URLs..." - - AGENT_FILE=".github/agents/agentic-workflows.agent.md" - - if [ ! -f "$AGENT_FILE" ]; then - echo "⚠️ WARNING: $AGENT_FILE not found, skipping check" - exit 0 - fi - - # Check for URLs that don't use 'main' branch - # Pattern matches: https://github.com/github/gh-aw/blob/{anything-except-main}/ - # Uses negative lookahead to exclude 'main' - INVALID_URLS=$(grep -n 'https://github.com/github/gh-aw/blob/' "$AGENT_FILE" | grep -v '/blob/main/' || true) - - if [ -n "$INVALID_URLS" ]; then - echo "❌ ERROR: Found URLs not using 'main' branch in $AGENT_FILE" - echo "" - echo "Lines with invalid URLs:" - echo "$INVALID_URLS" - echo "" - echo "💡 All GitHub URLs in agent files must reference the 'main' branch!" - echo "" - echo "URLs should use the pattern:" - echo " https://github.com/github/gh-aw/blob/main/.github/aw/..." - echo "" - echo "To fix:" - echo " 1. Edit $AGENT_FILE" - echo " 2. Replace all 'blob/{commit-hash}/' or 'blob/{tag}/' with 'blob/main/'" - echo " 3. Commit the updated file" - echo "" - exit 1 - fi - - echo "✅ All URLs in $AGENT_FILE correctly use 'main' branch" - - - name: Validate releases.json structure and version formats - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - with: - script: | - const fs = require('fs'); - const CONFIG_FILE = '.github/aw/releases.json'; - const SCHEMA_FILE = '.github/aw/releases.schema.json'; - - core.info(`🔍 Validating ${CONFIG_FILE} against ${SCHEMA_FILE}...`); - - if (!fs.existsSync(CONFIG_FILE)) { - core.setFailed(`ERROR: ${CONFIG_FILE} not found`); - return; - } - if (!fs.existsSync(SCHEMA_FILE)) { - core.setFailed(`ERROR: ${SCHEMA_FILE} not found`); - return; - } - - let config; - try { - config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); - } catch (err) { - core.setFailed(`ERROR: ${CONFIG_FILE} is not valid JSON: ${err.message}`); - return; - } - core.info(`✅ ${CONFIG_FILE} is valid JSON`); - - const errors = []; - - // Check additionalProperties (only allow known keys) - const allowedKeys = new Set(['$schema', 'blockedVersions', 'minimumVersion', 'minRecommendedVersion']); - for (const key of Object.keys(config)) { - if (!allowedKeys.has(key)) { - errors.push(`Unknown property: '${key}'`); - } - } - - // Validate blockedVersions - if ('blockedVersions' in config) { - const bv = config.blockedVersions; - if (!Array.isArray(bv)) { - errors.push("'blockedVersions' must be an array"); - } else { - const versionPattern = /^v[0-9]+\.[0-9]+\.[0-9]+$/; - const seen = new Set(); - bv.forEach((v, i) => { - if (typeof v !== 'string') { - errors.push(`'blockedVersions[${i}]' must be a string`); - } else if (!versionPattern.test(v)) { - errors.push(`'blockedVersions[${i}]' ('${v}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3')`); - } else if (seen.has(v)) { - errors.push(`'blockedVersions' contains duplicate entry: '${v}'`); - } else { - seen.add(v); - } - }); - } - } - - // Validate minimumVersion - if ('minimumVersion' in config) { - const mv = config.minimumVersion; - if (typeof mv !== 'string') { - errors.push("'minimumVersion' must be a string"); - } else if (mv !== '' && !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(mv)) { - errors.push(`'minimumVersion' ('${mv}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3' or empty string)`); - } - } - - // Validate minRecommendedVersion - if ('minRecommendedVersion' in config) { - const mrv = config.minRecommendedVersion; - if (typeof mrv !== 'string') { - errors.push("'minRecommendedVersion' must be a string"); - } else if (mrv !== '' && !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(mrv)) { - errors.push(`'minRecommendedVersion' ('${mrv}') does not match expected version pattern (vMAJOR.MINOR.PATCH, e.g. 'v1.2.3' or empty string)`); - } - } - - if (errors.length > 0) { - core.setFailed(`❌ ${CONFIG_FILE} schema validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`); - return; - } - - core.info(`✅ ${CONFIG_FILE} is valid and conforms to ${SCHEMA_FILE}`); - - js: - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: validate-yaml - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-js - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Node.js - id: setup-node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: "24" - cache: npm - cache-dependency-path: actions/setup/js/package-lock.json - - name: Report Node cache status - run: | - if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then - echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY - fi - - name: Install npm dependencies - run: cd actions/setup/js && npm ci - - name: Setup prompt templates for tests - run: | - mkdir -p ${{ runner.temp }}/gh-aw/prompts - cp actions/setup/md/*.md ${{ runner.temp }}/gh-aw/prompts/ - - name: Run tests - run: cd actions/setup/js && npm test - - js-integration-live-api: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: validate-yaml - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-js-integration-live-api - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Node.js - id: setup-node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: "24" - cache: npm - cache-dependency-path: actions/setup/js/package-lock.json - - - name: Report Node cache status - run: | - if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then - echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Install npm dependencies - run: cd actions/setup/js && npm ci - - - name: Run live GitHub API integration test - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "## 🔍 Live GitHub API Integration Test" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -z "$GITHUB_TOKEN" ]; then - echo "⚠️ GITHUB_TOKEN not available - test will be skipped" >> $GITHUB_STEP_SUMMARY - echo "ℹ️ This is expected in forks or when secrets are not available" >> $GITHUB_STEP_SUMMARY - cd actions/setup/js && npm test -- frontmatter_hash_github_api.test.cjs - else - echo "✅ GITHUB_TOKEN available - running live API test" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - cd actions/setup/js && npm test -- frontmatter_hash_github_api.test.cjs - echo "" >> $GITHUB_STEP_SUMMARY - echo "✨ Live API test completed successfully" >> $GITHUB_STEP_SUMMARY - fi - - bench: - # Only run benchmarks on main branch for performance tracking - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-bench - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Run benchmarks - run: make bench - - - name: Display benchmark summary - run: | - echo "## 📊 Benchmark Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - # Show compiler benchmarks from the results - grep "BenchmarkCompile" bench_results.txt | head -20 >> $GITHUB_STEP_SUMMARY || echo "No benchmark results found" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "📁 Full results saved to artifact: benchmark-results" >> $GITHUB_STEP_SUMMARY - - # Benchmark results for performance trend analysis - 14 days allows comparison across multiple runs - - name: Save benchmark results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: benchmark-results - path: bench_results.txt - if-no-files-found: ignore - retention-days: 14 - - check-validator-sizes: - name: Check validator file sizes - runs-on: ubuntu-latest - timeout-minutes: 10 - # Non-blocking: report violations but don't fail the build until existing files are cleaned up - continue-on-error: true - permissions: - contents: read - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Check validator file sizes - id: check - run: | - set +e - OUTPUT=$(NO_COLOR=1 bash scripts/check-validator-sizes.sh 2>&1) - EXIT_CODE=$? - echo "$OUTPUT" - echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" - echo "## Validator File Size Check" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Checking \`*_validation.go\` files against the 768-line hard limit (AGENTS.md)." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" - echo "$OUTPUT" >> "$GITHUB_STEP_SUMMARY" - echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY" - if [ "$EXIT_CODE" -ne 0 ]; then - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "⚠️ This check is currently non-blocking. Fix violations to keep the codebase healthy." >> "$GITHUB_STEP_SUMMARY" - fi - exit "$EXIT_CODE" - - lint-go: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-lint-go - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 # Fetch all history for incremental linting - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - # Go formatting check (fast, no deps needed) - - name: Check Go formatting - run: | - unformatted=$(go fmt ./...) - if [ -n "$unformatted" ]; then - echo "❌ Code is not formatted. Run 'make fmt' to fix." >> $GITHUB_STEP_SUMMARY - echo "Unformatted files:" >> $GITHUB_STEP_SUMMARY - echo "$unformatted" >> $GITHUB_STEP_SUMMARY - echo "" - echo "To fix this locally, run:" - echo " make fmt" - echo "" - echo "Or format individual files with:" - echo " go fmt ./path/to/file.go" - exit 1 - fi - echo "✅ Go formatting check passed" >> $GITHUB_STEP_SUMMARY - - # Install golangci-lint binary (avoiding GPL dependencies) - # Downloads pre-built binary from GitHub releases instead of using go install - - name: Install golangci-lint - run: | - GOLANGCI_LINT_VERSION="v2.8.0" - GOOS=$(go env GOOS) - GOARCH=$(go env GOARCH) - GOPATH=$(go env GOPATH) - BINARY_NAME="golangci-lint" - - echo "Installing golangci-lint $GOLANGCI_LINT_VERSION for $GOOS/$GOARCH..." - DOWNLOAD_URL="https://github.com/golangci/golangci-lint/releases/download/$GOLANGCI_LINT_VERSION/golangci-lint-${GOLANGCI_LINT_VERSION#v}-$GOOS-$GOARCH.tar.gz" - ARCHIVE="/tmp/golangci-lint.tar.gz" - - # Save archive to disk first so we can verify it before extracting. - # --fail causes curl to exit non-zero on HTTP errors (4xx/5xx), - # --retry 3 retries transient network failures automatically. - curl --fail --retry 3 -sSL "$DOWNLOAD_URL" -o "$ARCHIVE" - tar -xz -C /tmp -f "$ARCHIVE" - mkdir -p "$GOPATH/bin" - mv /tmp/golangci-lint-*/$BINARY_NAME "$GOPATH/bin/$BINARY_NAME" - chmod +x "$GOPATH/bin/$BINARY_NAME" - - echo "✓ golangci-lint $GOLANGCI_LINT_VERSION installed" - "$GOPATH/bin/$BINARY_NAME" version - - # Run golangci-lint via Makefile for consistency - # Uses incremental linting on PRs for faster CI (50-75% speedup) - # Performance optimizations in .golangci.yml: - # - timeout: 5m prevents hanging - # - modules-download-mode: readonly uses cached modules only - - name: Run golangci-lint - run: | - export PATH="$PATH:$(go env GOPATH)/bin" - if [ "${{ github.event_name }}" = "pull_request" ]; then - # Incremental linting on PRs - only check changed files - # This provides 50-75% faster linting on typical PRs - BASE_REF="origin/${{ github.base_ref }}" - if git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then - echo "Using incremental lint against $BASE_REF" - make golint-incremental BASE_REF="$BASE_REF" - else - echo "⚠️ Base ref $BASE_REF not found, falling back to full lint" - make golint - fi - else - # Full scan on main branch to ensure comprehensive coverage - make golint - fi - - # Error message linting (requires Go only) - - name: Lint error messages - run: make lint-errors - - lint-js: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-lint-js - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Node.js - id: setup-node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: "24" - cache: npm - cache-dependency-path: actions/setup/js/package-lock.json - - - name: Report Node cache status - run: | - if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then - echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Install npm dependencies - run: cd actions/setup/js && npm ci - - # JavaScript and JSON formatting checks - - name: Lint JavaScript files - run: make lint-cjs - - - name: Check JSON formatting - run: make fmt-check-json - - audit: - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-audit - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Run dependency audit (human-readable) - run: | - echo "## Dependency Health Audit" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - ./gh-aw upgrade --audit 2>&1 | tee audit_output.txt || true - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - head -100 audit_output.txt >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - - name: Run dependency audit (JSON) - id: audit_json - run: | - # Run audit with JSON output for agent-friendly parsing - ./gh-aw upgrade --audit --json > audit.json 2>&1 - - # Display summary in GitHub Actions - echo "✅ Dependency audit completed" >> $GITHUB_STEP_SUMMARY - - # Extract key metrics - TOTAL_DEPS=$(jq '.summary.total_dependencies' audit.json) - OUTDATED=$(jq '.summary.outdated_count' audit.json) - SECURITY=$(jq '.summary.security_advisories' audit.json) - V0_PERCENT=$(jq '.summary.v0_percentage' audit.json) - - echo "📊 **Audit Results:**" >> $GITHUB_STEP_SUMMARY - echo "- Total dependencies: $TOTAL_DEPS" >> $GITHUB_STEP_SUMMARY - echo "- Outdated: $OUTDATED" >> $GITHUB_STEP_SUMMARY - echo "- Security advisories: $SECURITY" >> $GITHUB_STEP_SUMMARY - echo "- v0.x exposure: ${V0_PERCENT}%" >> $GITHUB_STEP_SUMMARY - - - name: Upload audit results - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: dependency-audit - path: | - audit.json - audit_output.txt - retention-days: 14 - - actions-build: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-actions-build - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build actions - run: make actions-build - - - name: Validate actions - run: make actions-validate - - fuzz: - # Only run fuzz tests on main branch (10s is insufficient for PRs) - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - concurrency: - group: ci-${{ github.ref }}-fuzz-${{ matrix.group }} - cancel-in-progress: true - strategy: - fail-fast: false - matrix: - include: - - group: Parser - tests: >- - FuzzParseFrontmatter:./pkg/parser/ - FuzzScheduleParser:./pkg/parser/ - FuzzRuntimeImportExpressionValidation:./pkg/parser/ - FuzzRuntimeImportProcessExpressions:./pkg/parser/ - - group: Workflow-Core - tests: >- - FuzzExpressionParser:./pkg/workflow/ - FuzzMentionsFiltering:./pkg/workflow/ - FuzzSanitizeOutput:./pkg/workflow/ - FuzzSanitizeIncomingText:./pkg/workflow/ - FuzzSanitizeLabelContent:./pkg/workflow/ - FuzzWrapExpressionsInTemplateConditionals:./pkg/workflow/ - - group: Workflow-Parsing - tests: >- - FuzzYAMLParsing:./pkg/workflow/ - FuzzTemplateRendering:./pkg/workflow/ - FuzzInputValidation:./pkg/workflow/ - FuzzNetworkPermissions:./pkg/workflow/ - FuzzSafeJobConfig:./pkg/workflow/ - FuzzParseLabelTriggerShorthand:./pkg/workflow/ - - group: Workflow-Triggers - tests: >- - FuzzExpandLabelTriggerShorthand:./pkg/workflow/ - FuzzValidateNoTemplateInjection:./pkg/workflow/ - FuzzRemoveHeredocContent:./pkg/workflow/ - FuzzMarkdownCodeRegionBalancer:./pkg/workflow/ - FuzzParseTriggerShorthand:./pkg/workflow/ - FuzzTriggerIRToYAMLMap:./pkg/workflow/ - FuzzParseInputDefinition:./pkg/workflow/ - FuzzParseInputDefinitions:./pkg/workflow/ - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - env: - GOPROXY: "https://proxy.golang.org,direct" - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This indicates that proxy.golang.org is unreachable or returning errors" - echo "- Network connectivity: checking proxy.golang.org..." - if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then - echo " ✓ proxy.golang.org is reachable" - else - echo " ✗ proxy.golang.org is NOT reachable" - fi - echo "Please check network connectivity or re-run the workflow once the proxy recovers" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Run fuzz tests (${{ matrix.group }}) - run: | - set -o pipefail - # Create directory for fuzz results - mkdir -p fuzz-results - - # Helper function to run fuzz test and handle context deadline - # Go fuzz tests can exit with status 1 and "context deadline exceeded" when - # they reach the -fuzztime limit. We treat this as expected success to allow - # all fuzz targets to run instead of stopping at the first timeout. - run_fuzz_test() { - local fuzz_name=$1 - local package=$2 - local output_file="fuzz-results/${fuzz_name}.txt" - - echo "Running ${fuzz_name}..." - if go test -run='^$' -fuzz="^${fuzz_name}$" -fuzztime=10s "${package}" 2>&1 | tee "${output_file}"; then - echo "✅ ${fuzz_name} completed successfully" - return 0 - else - # Check if the failure was due to context deadline (expected) - if grep -q "context deadline exceeded" "${output_file}"; then - echo "✅ ${fuzz_name} completed (context deadline reached as expected)" - return 0 - else - echo "❌ ${fuzz_name} failed with unexpected error" - return 1 - fi - fi - } - - # Run fuzz tests for this matrix group - for entry in ${{ matrix.tests }}; do - fuzz_name="${entry%%:*}" - package="${entry##*:}" - run_fuzz_test "${fuzz_name}" "${package}" - done - - # Copy fuzz corpus data (testdata/fuzz directories) - echo "Copying fuzz corpus data..." - find ./pkg -path "*/testdata/fuzz" -type d | while read -r dir; do - pkg_name=$(echo "$dir" | sed 's|^\./pkg/||' | sed 's|/testdata/fuzz$||') - echo "Copying corpus from $dir to fuzz-results/corpus/$pkg_name/" - mkdir -p "fuzz-results/corpus/$pkg_name" - cp -r "$dir"/* "fuzz-results/corpus/$pkg_name/" 2>/dev/null || echo "No corpus data in $dir" - done - - - name: Upload fuzz test results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: fuzz-results-${{ matrix.group }} - path: fuzz-results/ - retention-days: 14 - - security: +on: + schedule: + - cron: 0 * * * * + workflow_dispatch: {} +jobs: + changes: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 5 permissions: contents: read - concurrency: - group: ci-${{ github.ref }}-security - cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Run security regression tests - run: make test-security - - security-scan: - # Only run security scans on main branch to reduce PR overhead - if: github.ref == 'refs/heads/main' + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 2 + - name: Detect recent changes + id: detect + shell: bash + run: "if [ \"${{ github.event_name }}\" != \"schedule\" ]; then\n echo \"has_changes=true\" >> \"$GITHUB_OUTPUT\"\n echo \"✅ Non-scheduled run: integration tests enabled\" >> \"$GITHUB_STEP_SUMMARY\"\n exit 0\nfi\n\nCHANGES_IN_LAST_HOUR=$(git log --since='1 hour ago' --pretty=format:'%H' | wc -l | tr -d ' ')\nif [ \"$CHANGES_IN_LAST_HOUR\" -gt 0 ]; then\n echo \"has_changes=true\" >> \"$GITHUB_OUTPUT\"\n echo \"✅ Detected $CHANGES_IN_LAST_HOUR commit(s) in the last hour\" >> \"$GITHUB_STEP_SUMMARY\"\nelse\n echo \"has_changes=false\" >> \"$GITHUB_OUTPUT\"\n echo \"ℹ️ No commits in the last hour; skipping integration jobs\" >> \"$GITHUB_STEP_SUMMARY\"\nfi" + outputs: + has_changes: ${{ steps.detect.outputs.has_changes }} + integration: + name: "Integration: ${{ matrix.test-group.name }}" + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest - timeout-minutes: 10 # Prevent jobs from hanging indefinitely + timeout-minutes: 25 permissions: contents: read strategy: fail-fast: false matrix: - tool: - - name: zizmor - flag: --zizmor - - name: actionlint - flag: --actionlint - - name: poutine - flag: --poutine + test-group: + - name: "CLI Compile & Poutine" + packages: "./pkg/cli" + pattern: "^TestCompile[^W]|TestPoutine" # Exclude TestCompileWorkflows to avoid duplicates + - name: "CLI Safe Update" + packages: "./pkg/cli" + pattern: "^TestSafeUpdate" + - name: "CLI MCP Connectivity" + packages: "./pkg/cli" + pattern: "TestMCPInspectPlaywright|TestMCPGateway" + - name: "CLI MCP Inspect GitHub" + packages: "./pkg/cli" + pattern: "TestMCPInspectGitHub" + - name: "CLI MCP Other" + packages: "./pkg/cli" + pattern: "TestMCPAdd|TestMCPServer|TestMCPConfig" + - name: "CLI Audit Logs & Firewall" + packages: "./pkg/cli" + pattern: "TestLogs|TestFirewall|TestNoStopTime|TestLocalWorkflow|^TestAudit|^TestInspect" + - name: "CLI Progress Flag" # Isolate slow test (~65s for TestProgressFlagSignature) + packages: "./pkg/cli" + pattern: "TestProgressFlagSignature" + - name: "CLI HTTP MCP Connect" # Isolate slow HTTP MCP connection tests (~43s) + packages: "./pkg/cli" + pattern: "TestConnectHTTPMCPServer" + - name: "CLI Compile Workflows" # Isolate slow workflow compilation test + packages: "./pkg/cli" + pattern: "TestCompileWorkflows_EmptyMarkdown" + - name: "CLI Security Tools" # Group security tool compilation tests + packages: "./pkg/cli" + pattern: "TestCompileWithZizmor|TestCompileWithPoutine|TestCompileWithPoutineAndZizmor" + - name: "CLI Add & List Commands" + packages: "./pkg/cli" + pattern: "^TestAdd|^TestList" + - name: "CLI Update Command" + packages: "./pkg/cli" + pattern: "^TestUpdate" + - name: "CLI Docker Build" # Isolate slow Docker tests (~43s) + packages: "./pkg/cli" + pattern: "TestDockerBuild|TestDockerImage" + - name: "CLI Completion & Other" # Remaining catch-all (reduced from original) + packages: "./pkg/cli" + pattern: "" # Catch-all for tests not matched by other CLI patterns + skip_pattern: "^TestCompile[^W]|TestPoutine|TestSafeUpdate|TestMCPInspectPlaywright|TestMCPGateway|TestMCPAdd|TestMCPInspectGitHub|TestMCPServer|TestMCPConfig|TestLogs|TestFirewall|TestNoStopTime|TestLocalWorkflow|TestProgressFlagSignature|TestConnectHTTPMCPServer|TestCompileWorkflows_EmptyMarkdown|TestCompileWithZizmor|TestCompileWithPoutine|TestCompileWithPoutineAndZizmor|^TestAdd|^TestList|^TestUpdate|^TestAudit|^TestInspect|TestDockerBuild|TestDockerImage" + - name: "Workflow Compiler" + packages: "./pkg/workflow" + pattern: "TestCompile|TestWorkflow|TestGenerate|TestParse" + - name: "Workflow Tools & MCP" + packages: "./pkg/workflow" + pattern: "TestMCP|TestTool|TestSkill|TestPlaywright|TestFirewall" + - name: "Workflow Validation" + packages: "./pkg/workflow" + pattern: "TestValidat|TestLock|TestError|TestWarning" + - name: "Workflow Features" + packages: "./pkg/workflow" + pattern: "SafeOutputs|CreatePullRequest|OutputLabel|HasSafeOutputs|GitHub|Git|PushToPullRequest|BuildFromAllowed|TestAgent|TestCopilot|TestCustom|TestEngine|TestModel|TestNetwork|TestOpenAI|TestProvider" + - name: "Workflow Rendering & Bundling" + packages: "./pkg/workflow" + pattern: "Render|Bundle|Script|WritePromptText" + - name: "Workflow Infra" + packages: "./pkg/workflow" + pattern: "^TestCache|TestCacheDependencies|TestCacheKey|TestValidateCache|TestPermissions|TestPackageExtractor|TestCollectPackagesFromWorkflow|Dependabot|Security|PII|Runtime|Setup|Install|Download|Version|Binary|String|Sanitize|Normalize|Trim|Clean|Format" + - name: "Workflow Actions & Containers" + packages: "./pkg/workflow" + pattern: "^TestAction[^P]|Container" + - name: "CMD Tests" # All cmd/gh-aw integration tests + packages: "./cmd/gh-aw" + pattern: "" + skip_pattern: "" # No other groups cover cmd tests + - name: "Parser Remote Fetch & Cache" + packages: "./pkg/parser" + pattern: "TestDownloadFileFromGitHub|TestResolveIncludePath|TestDownloadIncludeFromWorkflowSpec|TestImportCache" + - name: "Parser Location & Validation" + packages: "./pkg/parser" + pattern: "" # Catch-all for tests not matched by other Parser patterns + skip_pattern: "TestDownloadFileFromGitHub|TestResolveIncludePath|TestDownloadIncludeFromWorkflowSpec|TestImportCache" + - name: "Workflow Misc Part 2" # Remaining workflow tests + packages: "./pkg/workflow" + pattern: "" + skip_pattern: "TestCompile|TestWorkflow|TestGenerate|TestParse|TestMCP|TestTool|TestSkill|TestPlaywright|TestFirewall|TestValidat|TestLock|TestError|TestWarning|SafeOutputs|CreatePullRequest|OutputLabel|HasSafeOutputs|GitHub|Git|PushToPullRequest|BuildFromAllowed|Render|Bundle|Script|WritePromptText|^TestCache|TestCacheDependencies|TestCacheKey|TestValidateCache|^TestAction[^P]|Container|Dependabot|Security|PII|TestPermissions|TestPackageExtractor|TestCollectPackagesFromWorkflow|TestAgent|TestCopilot|TestCustom|TestEngine|TestModel|TestNetwork|TestOpenAI|TestProvider|String|Sanitize|Normalize|Trim|Clean|Format|Runtime|Setup|Install|Download|Version|Binary" concurrency: - group: ci-${{ github.ref }}-security-scan-${{ matrix.tool.name }} + group: ci-${{ github.ref }}-integration-${{ matrix.test-group.name }} cancel-in-progress: true - name: "Security Scan: ${{ matrix.tool.name }}" steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Display Go environment + run: | + echo "Go environment:" + go env | grep -E "GOPROXY|GOSUMDB|GOMODCACHE|GOPRIVATE" + echo "" + echo "Module cache location: $(go env GOMODCACHE)" + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + # Use -x for verbose output to see what's being downloaded + if go mod download -x; then + echo "✅ Successfully downloaded Go modules" + break else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 + EXIT_CODE=$? + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts (exit code: $EXIT_CODE)" + echo "This indicates that proxy.golang.org is unreachable or returning errors" + echo "" + echo "Diagnostic information:" + echo "- GOPROXY: $(go env GOPROXY)" + echo "- GOSUMDB: $(go env GOSUMDB)" + echo "- Network connectivity: checking proxy.golang.org..." + if curl -s -I --connect-timeout 5 https://proxy.golang.org/golang.org/@v/list >/dev/null 2>&1; then + echo " ✓ proxy.golang.org is reachable" + else + echo " ✗ proxy.golang.org is NOT reachable" fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY + exit 1 fi - done - - - name: Build gh-aw - run: make build - - - name: Run ${{ matrix.tool.name }} security scan on poem workflow - run: ./gh-aw compile poem-bot ${{ matrix.tool.flag }} --verbose - - health-smoke-copilot: - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: read - actions: read - concurrency: - group: ci-${{ github.ref }}-health-smoke-copilot - cancel-in-progress: true - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Build gh-aw - run: make build - - - name: Run health on smoke-copilot - id: health_check - run: | - set -e - ./gh-aw health smoke-copilot --json > health_output.json - echo "Health command output:" - cat health_output.json - - # Validate JSON structure - if ! jq -e '.total_runs' health_output.json > /dev/null 2>&1; then - echo "❌ total_runs field missing from health JSON output" - exit 1 - fi - TOTAL_RUNS=$(jq '.total_runs' health_output.json) - echo "✅ total_runs: $TOTAL_RUNS" - - if ! jq -e '.success_rate' health_output.json > /dev/null 2>&1; then - echo "❌ success_rate field missing from health JSON output" - exit 1 - fi - SUCCESS_RATE=$(jq '.success_rate' health_output.json) - echo "✅ success_rate: $SUCCESS_RATE" - - if ! jq -e '.workflow_name' health_output.json > /dev/null 2>&1; then - echo "❌ workflow_name field missing from health JSON output" - exit 1 - fi - WORKFLOW_NAME=$(jq -r '.workflow_name' health_output.json) - echo "✅ workflow_name: $WORKFLOW_NAME" + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Pre-flight check - Validate test dependencies + run: | + echo "Validating that test dependencies are available..." + echo "This ensures go test can compile test packages without network access." + echo "" + + # List all test dependencies to ensure they're in the cache + # This will fail fast if any dependencies are missing + echo "Checking test dependencies for ${{ matrix.test-group.packages }}..." + if go list -test -deps ${{ matrix.test-group.packages }} >/dev/null 2>&1; then + echo "✅ All test dependencies are available" + else + echo "❌ Failed to resolve test dependencies" + echo "" + echo "Attempting to show which dependencies are missing:" + go list -test -deps ${{ matrix.test-group.packages }} 2>&1 || true + exit 1 + fi + + echo "" + echo "Module cache statistics:" + echo "- Cache directory: $(go env GOMODCACHE)" + if [ -d "$(go env GOMODCACHE)" ]; then + echo "- Cache size: $(du -sh $(go env GOMODCACHE) 2>/dev/null | cut -f1 || echo 'unknown')" + echo "- Number of cached modules: $(find $(go env GOMODCACHE) -name "go.mod" 2>/dev/null | wc -l || echo 'unknown')" + fi + + - name: Build gh-aw binary for integration tests + run: make build + + - name: Run integration tests - ${{ matrix.test-group.name }} + id: run-tests + run: | + set -o pipefail + # Sanitize the test group name for use in filename + SAFE_NAME=$(echo "${{ matrix.test-group.name }}" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g') + + if [ -z "${{ matrix.test-group.pattern }}" ]; then + # Catch-all group: run with -skip to exclude tests matched by other groups + if [ -n "${{ matrix.test-group.skip_pattern || '' }}" ]; then + go test -v -parallel=8 -timeout=10m -tags 'integration' -skip '${{ matrix.test-group.skip_pattern }}' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" + else + go test -v -parallel=8 -timeout=10m -tags 'integration' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" + fi + else + go test -v -parallel=8 -timeout=10m -tags 'integration' -run '${{ matrix.test-group.pattern }}' -json ${{ matrix.test-group.packages }} | tee "test-result-integration-${SAFE_NAME}.json" + fi + + - name: Report test failures + if: failure() && steps.run-tests.outcome == 'failure' + run: | + # Sanitize the test group name to match the file created in the previous step + SAFE_NAME=$(echo "${{ matrix.test-group.name }}" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g') + + echo "## 🔍 Test Failure Analysis" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Analyzing test results for: **${{ matrix.test-group.name }}**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Run the failure report script + if ./scripts/report-test-failures.sh "test-result-integration-${SAFE_NAME}.json" | tee /tmp/failure-report.txt; then + echo "No failures detected in JSON output (unexpected - tests failed but no failure records found)" >> $GITHUB_STEP_SUMMARY + else + # Script found failures - add to summary + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cat /tmp/failure-report.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + fi - echo "✅ All health JSON structure validations passed" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Upload integration test results + if: always() # Upload even if tests fail so canary_go can track coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-result-integration-${{ matrix.test-group.name }} + path: test-result-integration-*.json + retention-days: 14 - mcp-server-compile-test: + update: + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read concurrency: - group: ci-${{ github.ref }}-mcp-server-compile-test + group: ci-${{ github.ref }}-update cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 + fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY fi + done - - name: Build gh-aw binary - run: make build - - - name: Create test workflow with error - run: | - mkdir -p .github/workflows - cat > .github/workflows/test-invalid.md << 'EOF' - --- - on: push - engine: copilot - invalid_field: this will cause an error - --- - # Test Invalid Workflow - - This workflow has an invalid field that will cause a compilation error. - EOF - - - name: Test MCP server compile tool - run: | - # Create a test script using the MCP Go SDK - cat > test_mcp_compile.go << 'GOEOF' - package main - - import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "time" - - "github.com/modelcontextprotocol/go-sdk/mcp" - ) - - func main() { - // Create MCP client - client := mcp.NewClient(&mcp.Implementation{ - Name: "ci-test-client", - Version: "1.0.0", - }, nil) - - // Start the MCP server as a subprocess with absolute path - binaryPath := "./gh-aw" - serverCmd := exec.Command(binaryPath, "mcp-server", "--cmd", binaryPath) - transport := &mcp.CommandTransport{Command: serverCmd} - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // Connect to the server - session, err := client.Connect(ctx, transport, nil) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to connect to MCP server: %v\n", err) - os.Exit(1) - } - defer session.Close() - - fmt.Println("✅ Successfully connected to MCP server") - - // Call the compile tool with the invalid workflow - params := &mcp.CallToolParams{ - Name: "compile", - Arguments: map[string]any{ - "workflows": []string{"test-invalid.md"}, - }, - } - - result, err := session.CallTool(ctx, params) - if err != nil { - fmt.Fprintf(os.Stderr, "MCP tool call returned error (this is expected): %v\n", err) - // Check if the error contains expected error information - fmt.Println("✅ Compile tool correctly returned an error") - os.Exit(0) - } - - // Get the result content - if len(result.Content) == 0 { - fmt.Fprintln(os.Stderr, "❌ Expected non-empty result from compile tool") - os.Exit(1) - } - - textContent, ok := result.Content[0].(*mcp.TextContent) - if !ok { - fmt.Fprintln(os.Stderr, "❌ Expected text content from compile tool") - os.Exit(1) - } - - fmt.Printf("Compile tool output:\n%s\n", textContent.Text) - - // Parse the JSON output to check for errors - var compileResults []map[string]any - if err := json.Unmarshal([]byte(textContent.Text), &compileResults); err != nil { - fmt.Fprintf(os.Stderr, "Failed to parse JSON output: %v\n", err) - os.Exit(1) - } - - // Check if the workflow is marked as invalid - if len(compileResults) == 0 { - fmt.Fprintln(os.Stderr, "❌ Expected at least one workflow result") - os.Exit(1) - } - - result0 := compileResults[0] - valid, ok := result0["valid"].(bool) - if !ok { - fmt.Fprintln(os.Stderr, "❌ Expected 'valid' field in result") - os.Exit(1) - } - - if valid { - fmt.Fprintln(os.Stderr, "❌ Expected workflow to be invalid") - os.Exit(1) - } - - // Check that errors field exists and has at least one error - errors, ok := result0["errors"].([]any) - if !ok || len(errors) == 0 { - fmt.Fprintln(os.Stderr, "❌ Expected errors array with at least one error") - os.Exit(1) - } - - fmt.Println("✅ Compile tool correctly reported validation errors:") - errorsJSON, _ := json.MarshalIndent(errors, " ", " ") - fmt.Printf(" %s\n", string(errorsJSON)) - - os.Exit(0) - } - GOEOF + - name: Verify dependencies + run: go mod verify - # Run the test - go run test_mcp_compile.go + - name: Build gh-aw binary + run: make build - - name: Report test results - if: always() - run: | - echo "## MCP Server Compile Tool Test" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY - echo "1. The gh-aw MCP server can be started successfully" >> $GITHUB_STEP_SUMMARY - echo "2. The compile tool can be invoked through the MCP server" >> $GITHUB_STEP_SUMMARY - echo "3. The compile tool correctly detects and reports validation errors" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Test completed successfully" >> $GITHUB_STEP_SUMMARY + - name: Test update command (dry-run) + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Testing update command to ensure it runs successfully..." + # Run update with verbose flag to check for and apply workflow updates from source repositories + # The command may modify workflow files if upstream updates are available + ./gh-aw update --verbose + echo "✅ Update command executed successfully" >> $GITHUB_STEP_SUMMARY - cross-platform-build: - name: Build & Test on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + js-integration-live-api: + if: ${{ needs.changes.outputs.has_changes == 'true' && (github.ref == 'refs/heads/main') }} + needs: + - changes + - validate-yaml + runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read - strategy: - fail-fast: false - matrix: - os: - - macos-latest - - windows-latest concurrency: - group: ci-${{ github.ref }}-cross-platform-${{ matrix.os }} + group: ci-${{ github.ref }}-js-integration-live-api cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - shell: bash - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - shell: bash - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Create test workflow - shell: bash - run: | - mkdir -p .github/workflows - cat > .github/workflows/test-cross-platform.md << 'EOF' - --- - on: push - engine: copilot - --- - # Test Workflow for Cross-Platform CI - - This is a simple test workflow to verify the compile command works correctly. - - ## Task - Echo hello world. - EOF - - - name: Test compile command - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "Testing compile command on ${{ matrix.os }}..." - - # Determine binary name based on OS - if [[ "$RUNNER_OS" == "Windows" ]]; then - BINARY="./gh-aw.exe" - else - BINARY="./gh-aw" - fi - - # Verify binary exists - if [ ! -f "$BINARY" ]; then - echo "❌ Binary not found: $BINARY" - ls -la - exit 1 - fi - - # Run compile command - "$BINARY" compile test-cross-platform --verbose - - # Check if lock file was generated - if [ -f ".github/workflows/test-cross-platform.lock.yml" ]; then - echo "✅ Compile succeeded - lock file generated" - else - echo "❌ Compile failed - no lock file generated" - exit 1 - fi - - echo "## Cross-Platform Test Results" >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + id: setup-node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: actions/setup/js/package-lock.json + + - name: Report Node cache status + run: | + if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then + echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Install npm dependencies + run: cd actions/setup/js && npm ci + + - name: Run live GitHub API integration test + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "## 🔍 Live GitHub API Integration Test" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -z "$GITHUB_TOKEN" ]; then + echo "⚠️ GITHUB_TOKEN not available - test will be skipped" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ This is expected in forks or when secrets are not available" >> $GITHUB_STEP_SUMMARY + cd actions/setup/js && npm test -- frontmatter_hash_github_api.test.cjs + else + echo "✅ GITHUB_TOKEN available - running live API test" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Successfully compiled workflow on ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY + cd actions/setup/js && npm test -- frontmatter_hash_github_api.test.cjs echo "" >> $GITHUB_STEP_SUMMARY - echo "**Platform:** ${{ matrix.os }}" >> $GITHUB_STEP_SUMMARY - echo "**Binary:** $BINARY" >> $GITHUB_STEP_SUMMARY - echo "**Go version:** $(go version)" >> $GITHUB_STEP_SUMMARY - - - name: Clean up test files - if: always() - shell: bash - run: | - rm -f .github/workflows/test-cross-platform.md - rm -f .github/workflows/test-cross-platform.lock.yml - - alpine-container-test: - name: Alpine Container Test + echo "✨ Live API test completed successfully" >> $GITHUB_STEP_SUMMARY + fi + + audit: + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 15 permissions: contents: read concurrency: - group: ci-${{ github.ref }}-alpine-container + group: ci-${{ github.ref }}-audit cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies with retry + if: steps.setup-go.outputs.cache-hit != 'true' + run: | + set -e + MAX_RETRIES=3 + RETRY_DELAY=5 + + for i in $(seq 1 $MAX_RETRIES); do + echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." + if go mod download; then + echo "✅ Successfully downloaded Go modules" + break else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies with retry - if: steps.setup-go.outputs.cache-hit != 'true' - run: | - set -e - MAX_RETRIES=3 - RETRY_DELAY=5 - - for i in $(seq 1 $MAX_RETRIES); do - echo "Attempt $i of $MAX_RETRIES: Downloading Go modules..." - if go mod download; then - echo "✅ Successfully downloaded Go modules" - break - else - if [ $i -eq $MAX_RETRIES ]; then - echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" - echo "This may indicate that proxy.golang.org is unreachable" - echo "Please check network connectivity or consider vendoring dependencies" - exit 1 - fi - echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY + if [ $i -eq $MAX_RETRIES ]; then + echo "❌ Failed to download Go modules after $MAX_RETRIES attempts" + echo "This may indicate that proxy.golang.org is unreachable" + echo "Please check network connectivity or consider vendoring dependencies" + exit 1 fi - done - - - name: Verify dependencies - run: go mod verify - - - name: Build Linux binary for Alpine - run: make build-linux - - - name: Build Alpine Docker image - run: | - echo "Building Alpine Docker image..." - docker build -t gh-aw-alpine:test \ - --build-arg BINARY=gh-aw-linux-amd64 \ - -f Dockerfile . - echo "✅ Alpine Docker image built successfully" - - - name: Test Docker image basic commands - run: | - echo "Testing Docker image basic commands..." - docker run --rm gh-aw-alpine:test --version - docker run --rm gh-aw-alpine:test --help - echo "✅ Basic commands work" - - - name: Create test workflow in container - run: | - echo "Creating test workflow file..." - mkdir -p test-workspace/.github/workflows - cat > test-workspace/.github/workflows/test-alpine.md << 'EOF' - --- - on: push - engine: copilot - --- - # Test Workflow for Alpine Container - - This is a simple test workflow to verify the compile command works correctly in Alpine container. - - ## Task - Echo hello from Alpine container. - EOF - echo "✅ Test workflow created" - - - name: Run compile through Alpine container - run: | - echo "Running compile command through Alpine container..." - docker run --rm \ - -v "$(pwd)/test-workspace:/workspace" \ - -w /workspace \ - gh-aw-alpine:test compile test-alpine --verbose - - echo "✅ Compile command executed" - - - name: Verify lock file generation - run: | - echo "Verifying lock file was generated..." - if [ -f "test-workspace/.github/workflows/test-alpine.lock.yml" ]; then - echo "✅ Lock file generated successfully" - echo "" - echo "Lock file contents:" - head -20 test-workspace/.github/workflows/test-alpine.lock.yml - else - echo "❌ Lock file not found" - ls -la test-workspace/.github/workflows/ - exit 1 - fi - - - name: Generate test summary - if: always() - run: | - echo "## Alpine Container Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY - echo "1. The Alpine Docker image can be built successfully" >> $GITHUB_STEP_SUMMARY - echo "2. The gh-aw binary works correctly in Alpine Linux" >> $GITHUB_STEP_SUMMARY - echo "3. The compile command can process workflows in the container" >> $GITHUB_STEP_SUMMARY - echo "4. Lock files are generated correctly" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ -f "test-workspace/.github/workflows/test-alpine.lock.yml" ]; then - echo "✅ All tests passed successfully" >> $GITHUB_STEP_SUMMARY - else - echo "❌ Lock file generation failed" >> $GITHUB_STEP_SUMMARY - fi + echo "⚠️ Download failed, retrying in ${RETRY_DELAY}s..." + sleep $RETRY_DELAY + fi + done + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Run dependency audit (human-readable) + run: | + echo "## Dependency Health Audit" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ./gh-aw upgrade --audit 2>&1 | tee audit_output.txt || true + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + head -100 audit_output.txt >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + - name: Run dependency audit (JSON) + id: audit_json + run: | + # Run audit with JSON output for agent-friendly parsing + ./gh-aw upgrade --audit --json > audit.json 2>&1 + + # Display summary in GitHub Actions + echo "✅ Dependency audit completed" >> $GITHUB_STEP_SUMMARY + + # Extract key metrics + TOTAL_DEPS=$(jq '.summary.total_dependencies' audit.json) + OUTDATED=$(jq '.summary.outdated_count' audit.json) + SECURITY=$(jq '.summary.security_advisories' audit.json) + V0_PERCENT=$(jq '.summary.v0_percentage' audit.json) + + echo "📊 **Audit Results:**" >> $GITHUB_STEP_SUMMARY + echo "- Total dependencies: $TOTAL_DEPS" >> $GITHUB_STEP_SUMMARY + echo "- Outdated: $OUTDATED" >> $GITHUB_STEP_SUMMARY + echo "- Security advisories: $SECURITY" >> $GITHUB_STEP_SUMMARY + echo "- v0.x exposure: ${V0_PERCENT}%" >> $GITHUB_STEP_SUMMARY + + - name: Upload audit results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: dependency-audit + path: | + audit.json + audit_output.txt + retention-days: 14 - - name: Clean up test files - if: always() - run: | - rm -rf test-workspace - - safe-outputs-conformance: + health-smoke-copilot: + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read + actions: read + concurrency: + group: ci-${{ github.ref }}-health-smoke-copilot + cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Run Safe Outputs Conformance Checker - id: conformance - continue-on-error: true - run: | - echo "## Safe Outputs Conformance Check" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Run the conformance checker and capture output - if ./scripts/check-safe-outputs-conformance.sh > conformance-output.txt 2>&1; then - echo "✅ All conformance checks passed" >> $GITHUB_STEP_SUMMARY - EXIT_CODE=0 - else - EXIT_CODE=$? - if [ $EXIT_CODE -eq 2 ]; then - echo "⚠️ Critical conformance issues found (treated as warning)" >> $GITHUB_STEP_SUMMARY - elif [ $EXIT_CODE -eq 1 ]; then - echo "⚠️ High priority conformance issues found (treated as warning)" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Conformance check completed with warnings" >> $GITHUB_STEP_SUMMARY - fi - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Conformance Check Output" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat conformance-output.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - # Also output to console for visibility - echo "=== Conformance Check Results ===" - cat conformance-output.txt - - # Always succeed (treat as warning only) - exit 0 - - - name: Upload conformance report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: safe-outputs-conformance-report - path: conformance-output.txt - retention-days: 7 + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Build gh-aw + run: make build + + - name: Run health on smoke-copilot + id: health_check + run: | + set -e + ./gh-aw health smoke-copilot --json > health_output.json + echo "Health command output:" + cat health_output.json + + # Validate JSON structure + if ! jq -e '.total_runs' health_output.json > /dev/null 2>&1; then + echo "❌ total_runs field missing from health JSON output" + exit 1 + fi + TOTAL_RUNS=$(jq '.total_runs' health_output.json) + echo "✅ total_runs: $TOTAL_RUNS" + + if ! jq -e '.success_rate' health_output.json > /dev/null 2>&1; then + echo "❌ success_rate field missing from health JSON output" + exit 1 + fi + SUCCESS_RATE=$(jq '.success_rate' health_output.json) + echo "✅ success_rate: $SUCCESS_RATE" + + if ! jq -e '.workflow_name' health_output.json > /dev/null 2>&1; then + echo "❌ workflow_name field missing from health JSON output" + exit 1 + fi + WORKFLOW_NAME=$(jq -r '.workflow_name' health_output.json) + echo "✅ workflow_name: $WORKFLOW_NAME" + + echo "✅ All health JSON structure validations passed" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} integration-add: name: Integration Add Workflows + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -2264,197 +552,199 @@ jobs: group: ci-${{ github.ref }}-integration-add cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies - run: go mod download - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Verify gh-aw binary - run: | - ./gh-aw --help - ./gh-aw version - - - name: Clone githubnext/agentics repository - run: | - echo "Cloning githubnext/agentics repository..." - cd /tmp - git clone --depth 1 --filter=blob:none https://github.com/githubnext/agentics.git - echo "✅ Repository cloned successfully" - - - name: List workflows from agentics - id: list-workflows - run: | - echo "Listing workflow files from githubnext/agentics..." - cd /tmp/agentics/workflows - - # Get list of all .md workflow files (just the names without .md extension) - WORKFLOWS=$(ls *.md | sed 's/\.md$//') - - echo "Found workflows:" - echo "$WORKFLOWS" - - # Count workflows - WORKFLOW_COUNT=$(echo "$WORKFLOWS" | wc -l) - echo "Total workflows found: $WORKFLOW_COUNT" - - # Save workflow list for next step - echo "$WORKFLOWS" > /tmp/workflow-list.txt - echo "workflow_count=$WORKFLOW_COUNT" >> $GITHUB_OUTPUT - - - name: Compare gh aw list with git clone - run: | - set -e - echo "## Comparing 'gh aw list' output with git clone results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # List workflows using gh aw list command with custom path - echo "Running: ./gh-aw list --repo githubnext/agentics --path workflows --json" - ./gh-aw list --repo githubnext/agentics --path workflows --json > /tmp/gh-aw-list.json - - # Extract workflow names from JSON output - echo "Extracting workflow names from gh aw list output..." - jq -r '.[].workflow' /tmp/gh-aw-list.json | sort > /tmp/gh-aw-workflows.txt - - # Get workflow names from git clone (already in /tmp/workflow-list.txt) - echo "Sorting git clone workflow list..." - sort /tmp/workflow-list.txt > /tmp/git-workflows-sorted.txt - - # Display both lists - echo "### Workflows from 'gh aw list --repo githubnext/agentics --path workflows'" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/gh-aw-workflows.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Verify gh-aw binary + run: | + ./gh-aw --help + ./gh-aw version + + - name: Clone githubnext/agentics repository + run: | + echo "Cloning githubnext/agentics repository..." + cd /tmp + git clone --depth 1 --filter=blob:none https://github.com/githubnext/agentics.git + echo "✅ Repository cloned successfully" + + - name: List workflows from agentics + id: list-workflows + run: | + echo "Listing workflow files from githubnext/agentics..." + cd /tmp/agentics/workflows + + # Get list of all .md workflow files (just the names without .md extension) + WORKFLOWS=$(ls *.md | sed 's/\.md$//') + + echo "Found workflows:" + echo "$WORKFLOWS" + + # Count workflows + WORKFLOW_COUNT=$(echo "$WORKFLOWS" | wc -l) + echo "Total workflows found: $WORKFLOW_COUNT" + + # Save workflow list for next step + echo "$WORKFLOWS" > /tmp/workflow-list.txt + echo "workflow_count=$WORKFLOW_COUNT" >> $GITHUB_OUTPUT + + - name: Compare gh aw list with git clone + run: | + set -e + echo "## Comparing 'gh aw list' output with git clone results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # List workflows using gh aw list command with custom path + echo "Running: ./gh-aw list --repo githubnext/agentics --path workflows --json" + ./gh-aw list --repo githubnext/agentics --path workflows --json > /tmp/gh-aw-list.json + + # Extract workflow names from JSON output + echo "Extracting workflow names from gh aw list output..." + jq -r '.[].workflow' /tmp/gh-aw-list.json | sort > /tmp/gh-aw-workflows.txt + + # Get workflow names from git clone (already in /tmp/workflow-list.txt) + echo "Sorting git clone workflow list..." + sort /tmp/workflow-list.txt > /tmp/git-workflows-sorted.txt + + # Display both lists + echo "### Workflows from 'gh aw list --repo githubnext/agentics --path workflows'" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/gh-aw-workflows.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "### Workflows from git clone" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/git-workflows-sorted.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Compare the two lists + if diff -u /tmp/git-workflows-sorted.txt /tmp/gh-aw-workflows.txt > /tmp/diff-output.txt; then + echo "✅ **SUCCESS**: Workflow lists match!" >> $GITHUB_STEP_SUMMARY + echo "The 'gh aw list' command returned the same workflows as the git clone." >> $GITHUB_STEP_SUMMARY + echo "" + echo "✅ Workflow lists match!" + else + echo "❌ **FAILURE**: Workflow lists do not match!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - echo "### Workflows from git clone" >> $GITHUB_STEP_SUMMARY + echo "### Differences" >> $GITHUB_STEP_SUMMARY + echo '```diff' >> $GITHUB_STEP_SUMMARY + cat /tmp/diff-output.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - cat /tmp/git-workflows-sorted.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Compare the two lists - if diff -u /tmp/git-workflows-sorted.txt /tmp/gh-aw-workflows.txt > /tmp/diff-output.txt; then - echo "✅ **SUCCESS**: Workflow lists match!" >> $GITHUB_STEP_SUMMARY - echo "The 'gh aw list' command returned the same workflows as the git clone." >> $GITHUB_STEP_SUMMARY - echo "" - echo "✅ Workflow lists match!" - else - echo "❌ **FAILURE**: Workflow lists do not match!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Differences" >> $GITHUB_STEP_SUMMARY - echo '```diff' >> $GITHUB_STEP_SUMMARY - cat /tmp/diff-output.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" - echo "❌ Workflow lists do not match!" - echo "Differences:" - cat /tmp/diff-output.txt - exit 1 - fi - - - name: Add workflows one by one - id: add-workflows - env: - GH_TOKEN: ${{ github.token }} - run: | - cd /home/runner/work/gh-aw/gh-aw - - echo "## Adding Workflows from githubnext/agentics" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Workflow | Status | Details |" >> $GITHUB_STEP_SUMMARY - echo "|----------|--------|---------|" >> $GITHUB_STEP_SUMMARY - - SUCCESS_COUNT=0 - FAILURE_COUNT=0 - - # Read workflow list - while IFS= read -r workflow; do - echo "Processing workflow: $workflow" - - # Try to add the workflow using gh aw add - if ./gh-aw add "githubnext/agentics/$workflow" --force 2>&1 | tee /tmp/add-${workflow}.log; then - echo "✅ Successfully added: $workflow" - echo "| $workflow | ✅ Success | Added successfully |" >> $GITHUB_STEP_SUMMARY - SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) - else - EXIT_CODE=$? - echo "❌ Failed to add: $workflow (exit code: $EXIT_CODE)" - - # Extract error message from log - ERROR_MSG=$(tail -5 /tmp/add-${workflow}.log | tr '\n' ' ' | cut -c1-100) - echo "| $workflow | ❌ Failed | Exit code: $EXIT_CODE - ${ERROR_MSG}... |" >> $GITHUB_STEP_SUMMARY - FAILURE_COUNT=$((FAILURE_COUNT + 1)) - fi - - echo "---" - done < /tmp/workflow-list.txt - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Summary" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Successful: $SUCCESS_COUNT" >> $GITHUB_STEP_SUMMARY - echo "- ❌ Failed: $FAILURE_COUNT" >> $GITHUB_STEP_SUMMARY - echo "- Total: ${{ steps.list-workflows.outputs.workflow_count }}" >> $GITHUB_STEP_SUMMARY - - echo "success_count=$SUCCESS_COUNT" >> $GITHUB_OUTPUT - echo "failure_count=$FAILURE_COUNT" >> $GITHUB_OUTPUT - - # Report overall result echo "" - echo "=====================================" - echo "Integration Test Results" - echo "=====================================" - echo "Successful additions: $SUCCESS_COUNT" - echo "Failed additions: $FAILURE_COUNT" - echo "Total workflows: ${{ steps.list-workflows.outputs.workflow_count }}" - echo "=====================================" - - - name: Check for added workflows - run: | - echo "Checking for added workflow files..." - if [ -d ".github/workflows" ]; then - echo "Found workflows directory" - ls -la .github/workflows/*.md 2>/dev/null | head -20 || echo "No .md files found" + echo "❌ Workflow lists do not match!" + echo "Differences:" + cat /tmp/diff-output.txt + exit 1 + fi + + - name: Add workflows one by one + id: add-workflows + env: + GH_TOKEN: ${{ github.token }} + run: | + cd /home/runner/work/gh-aw/gh-aw + + echo "## Adding Workflows from githubnext/agentics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Workflow | Status | Details |" >> $GITHUB_STEP_SUMMARY + echo "|----------|--------|---------|" >> $GITHUB_STEP_SUMMARY + + SUCCESS_COUNT=0 + FAILURE_COUNT=0 + + # Read workflow list + while IFS= read -r workflow; do + echo "Processing workflow: $workflow" + + # Try to add the workflow using gh aw add + if ./gh-aw add "githubnext/agentics/$workflow" --force 2>&1 | tee /tmp/add-${workflow}.log; then + echo "✅ Successfully added: $workflow" + echo "| $workflow | ✅ Success | Added successfully |" >> $GITHUB_STEP_SUMMARY + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) else - echo "No .github/workflows directory found" - fi - - - name: Test result summary - if: always() - run: | - echo "=== Agentics Workflows Integration Test Summary ===" - echo "This test validates that gh-aw can successfully add workflows" - echo "from the githubnext/agentics repository." - echo "" - echo "Test completed with:" - echo "- Success count: ${{ steps.add-workflows.outputs.success_count }}" - echo "- Failure count: ${{ steps.add-workflows.outputs.failure_count }}" + EXIT_CODE=$? + echo "❌ Failed to add: $workflow (exit code: $EXIT_CODE)" + + # Extract error message from log + ERROR_MSG=$(tail -5 /tmp/add-${workflow}.log | tr '\n' ' ' | cut -c1-100) + echo "| $workflow | ❌ Failed | Exit code: $EXIT_CODE - ${ERROR_MSG}... |" >> $GITHUB_STEP_SUMMARY + FAILURE_COUNT=$((FAILURE_COUNT + 1)) + fi + + echo "---" + done < /tmp/workflow-list.txt + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Summary" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Successful: $SUCCESS_COUNT" >> $GITHUB_STEP_SUMMARY + echo "- ❌ Failed: $FAILURE_COUNT" >> $GITHUB_STEP_SUMMARY + echo "- Total: ${{ steps.list-workflows.outputs.workflow_count }}" >> $GITHUB_STEP_SUMMARY + + echo "success_count=$SUCCESS_COUNT" >> $GITHUB_OUTPUT + echo "failure_count=$FAILURE_COUNT" >> $GITHUB_OUTPUT + + # Report overall result + echo "" + echo "=====================================" + echo "Integration Test Results" + echo "=====================================" + echo "Successful additions: $SUCCESS_COUNT" + echo "Failed additions: $FAILURE_COUNT" + echo "Total workflows: ${{ steps.list-workflows.outputs.workflow_count }}" + echo "=====================================" + + - name: Check for added workflows + run: | + echo "Checking for added workflow files..." + if [ -d ".github/workflows" ]; then + echo "Found workflows directory" + ls -la .github/workflows/*.md 2>/dev/null | head -20 || echo "No .md files found" + else + echo "No .github/workflows directory found" + fi + + - name: Test result summary + if: always() + run: | + echo "=== Agentics Workflows Integration Test Summary ===" + echo "This test validates that gh-aw can successfully add workflows" + echo "from the githubnext/agentics repository." + echo "" + echo "Test completed with:" + echo "- Success count: ${{ steps.add-workflows.outputs.success_count }}" + echo "- Failure count: ${{ steps.add-workflows.outputs.failure_count }}" integration-marketplace-compile: name: Integration Compile gh-aw-marketplace - if: ${{ false }} - needs: [build] + if: ${{ needs.changes.outputs.has_changes == 'true' && (false) }} + needs: + - changes + - build runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -2463,57 +753,60 @@ jobs: group: ci-${{ github.ref }}-integration-marketplace-compile cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Download gh-aw binary artifact - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 - with: - name: gh-aw-linux-amd64 - path: . - - - name: Prepare gh-aw binary - run: chmod +x ./gh-aw - - - name: Clone github/gh-aw-marketplace repository - run: | - echo "Cloning github/gh-aw-marketplace repository..." - cd /tmp - AUTH_HEADER=$(printf 'x-access-token:%s' '${{ github.token }}' | base64 | tr -d '\n') - git -c http.https://github.com/.extraheader="Authorization: Basic ${AUTH_HEADER}" clone --depth 1 https://github.com/github/gh-aw-marketplace.git - echo "✅ Repository cloned successfully" - - - name: Compile gh-aw-marketplace workflows - run: | - set -euo pipefail - - MARKETPLACE_DIR="/tmp/gh-aw-marketplace" - WORKFLOW_DIR="${MARKETPLACE_DIR}/.github/workflows" - LOG_FILE="/tmp/gh-aw-marketplace-compile.log" - - if [ ! -d "$WORKFLOW_DIR" ]; then - echo "❌ Expected workflow directory not found: $WORKFLOW_DIR" - echo "Available workflow-like directories:" - find "$MARKETPLACE_DIR" -maxdepth 3 -type d -name workflows || true - exit 1 - fi - - echo "Compiling workflows from: $WORKFLOW_DIR" - ./gh-aw compile --dir "$WORKFLOW_DIR" --strict --no-check-update 2>&1 | tee "$LOG_FILE" - - WARNINGS=$(grep -nEi '(⚠|(^|[[:space:]])(warning:|warnings?:|warn[[:space:]:]))' "$LOG_FILE" || true) - if [ -n "$WARNINGS" ]; then - echo "❌ Compilation produced warnings; failing the job." - echo "$WARNINGS" - exit 1 - fi - - echo "✅ gh-aw-marketplace workflows compiled with no warnings or errors." - echo "## gh-aw-marketplace compile" >> $GITHUB_STEP_SUMMARY - echo "✅ All workflows compiled with no warnings or errors." >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download gh-aw binary artifact + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + with: + name: gh-aw-linux-amd64 + path: . + + - name: Prepare gh-aw binary + run: chmod +x ./gh-aw + + - name: Clone github/gh-aw-marketplace repository + run: | + echo "Cloning github/gh-aw-marketplace repository..." + cd /tmp + AUTH_HEADER=$(printf 'x-access-token:%s' '${{ github.token }}' | base64 | tr -d '\n') + git -c http.https://github.com/.extraheader="Authorization: Basic ${AUTH_HEADER}" clone --depth 1 https://github.com/github/gh-aw-marketplace.git + echo "✅ Repository cloned successfully" + + - name: Compile gh-aw-marketplace workflows + run: | + set -euo pipefail + + MARKETPLACE_DIR="/tmp/gh-aw-marketplace" + WORKFLOW_DIR="${MARKETPLACE_DIR}/.github/workflows" + LOG_FILE="/tmp/gh-aw-marketplace-compile.log" + + if [ ! -d "$WORKFLOW_DIR" ]; then + echo "❌ Expected workflow directory not found: $WORKFLOW_DIR" + echo "Available workflow-like directories:" + find "$MARKETPLACE_DIR" -maxdepth 3 -type d -name workflows || true + exit 1 + fi + + echo "Compiling workflows from: $WORKFLOW_DIR" + ./gh-aw compile --dir "$WORKFLOW_DIR" --strict --no-check-update 2>&1 | tee "$LOG_FILE" + + WARNINGS=$(grep -nEi '(⚠|(^|[[:space:]])(warning:|warnings?:|warn[[:space:]:]))' "$LOG_FILE" || true) + if [ -n "$WARNINGS" ]; then + echo "❌ Compilation produced warnings; failing the job." + echo "$WARNINGS" + exit 1 + fi + + echo "✅ gh-aw-marketplace workflows compiled with no warnings or errors." + echo "## gh-aw-marketplace compile" >> $GITHUB_STEP_SUMMARY + echo "✅ All workflows compiled with no warnings or errors." >> $GITHUB_STEP_SUMMARY integration-update: name: Integration Update - Preserve Local Imports + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -2522,162 +815,165 @@ jobs: group: ci-${{ github.ref }}-integration-update cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies - run: go mod download - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Set up isolated test workspace - run: | - mkdir -p /tmp/test-update-workspace/.github/workflows - cd /tmp/test-update-workspace - git init -q - git config user.email "test@example.com" - git config user.name "Test" - echo "✅ Test workspace initialised at /tmp/test-update-workspace" - - - name: Add a workflow from githubnext/agentics - id: add-workflow - env: - GH_TOKEN: ${{ github.token }} - run: | - cd /tmp/test-update-workspace - - echo "## Integration Update Test: Preserve Local Imports" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # Add daily-team-status which uses shared imports in githubnext/agentics - if /home/runner/work/gh-aw/gh-aw/gh-aw add githubnext/agentics/workflows/daily-team-status.md --force 2>&1; then - echo "✅ Added workflow successfully" >> $GITHUB_STEP_SUMMARY - else - echo "❌ Failed to add workflow" >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - WORKFLOW=".github/workflows/daily-team-status.md" - if [ ! -f "$WORKFLOW" ]; then - echo "❌ Workflow file not found after add" - exit 1 - fi - - # Check if the workflow has relative imports - if grep -q "^imports:" "$WORKFLOW"; then - echo "✅ Workflow has an imports field" >> $GITHUB_STEP_SUMMARY - # Collect relative import paths (those without "@" — not yet cross-repo refs). - # Python is used for robust YAML-aware parsing instead of fragile AWK patterns. - printf '%s\n' \ - 'import sys, re' \ - '' \ - 'content = open(sys.argv[1]).read()' \ - '# Find the imports: block (list items under the key, indented with 2+ spaces)' \ - 'm = re.search(r"^imports:\n((?:[ \t]+-[ \t]+.+\n)+)", content, re.MULTILINE)' \ - 'if m:' \ - ' for line in m.group(1).splitlines():' \ - ' val = line.strip().lstrip("- ").strip()' \ - ' if val and "@" not in val:' \ - ' print(val)' \ - > /tmp/gh-aw-check-imports.py - RELATIVE_IMPORTS=$(python3 /tmp/gh-aw-check-imports.py "$WORKFLOW") - if [ -n "$RELATIVE_IMPORTS" ]; then - echo "has_relative_imports=true" >> $GITHUB_OUTPUT - echo "$RELATIVE_IMPORTS" > /tmp/relative-imports.txt - echo "Relative import paths: $RELATIVE_IMPORTS" >> $GITHUB_STEP_SUMMARY - else - echo "has_relative_imports=false" >> $GITHUB_OUTPUT - echo "⚠️ All imports already use cross-repo refs; skipping local-file preservation check" >> $GITHUB_STEP_SUMMARY - fi + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Set up isolated test workspace + run: | + mkdir -p /tmp/test-update-workspace/.github/workflows + cd /tmp/test-update-workspace + git init -q + git config user.email "test@example.com" + git config user.name "Test" + echo "✅ Test workspace initialised at /tmp/test-update-workspace" + + - name: Add a workflow from githubnext/agentics + id: add-workflow + env: + GH_TOKEN: ${{ github.token }} + run: | + cd /tmp/test-update-workspace + + echo "## Integration Update Test: Preserve Local Imports" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Add daily-team-status which uses shared imports in githubnext/agentics + if /home/runner/work/gh-aw/gh-aw/gh-aw add githubnext/agentics/workflows/daily-team-status.md --force 2>&1; then + echo "✅ Added workflow successfully" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Failed to add workflow" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + WORKFLOW=".github/workflows/daily-team-status.md" + if [ ! -f "$WORKFLOW" ]; then + echo "❌ Workflow file not found after add" + exit 1 + fi + + # Check if the workflow has relative imports + if grep -q "^imports:" "$WORKFLOW"; then + echo "✅ Workflow has an imports field" >> $GITHUB_STEP_SUMMARY + # Collect relative import paths (those without "@" — not yet cross-repo refs). + # Python is used for robust YAML-aware parsing instead of fragile AWK patterns. + printf '%s\n' \ + 'import sys, re' \ + '' \ + 'content = open(sys.argv[1]).read()' \ + '# Find the imports: block (list items under the key, indented with 2+ spaces)' \ + 'm = re.search(r"^imports:\n((?:[ \t]+-[ \t]+.+\n)+)", content, re.MULTILINE)' \ + 'if m:' \ + ' for line in m.group(1).splitlines():' \ + ' val = line.strip().lstrip("- ").strip()' \ + ' if val and "@" not in val:' \ + ' print(val)' \ + > /tmp/gh-aw-check-imports.py + RELATIVE_IMPORTS=$(python3 /tmp/gh-aw-check-imports.py "$WORKFLOW") + if [ -n "$RELATIVE_IMPORTS" ]; then + echo "has_relative_imports=true" >> $GITHUB_OUTPUT + echo "$RELATIVE_IMPORTS" > /tmp/relative-imports.txt + echo "Relative import paths: $RELATIVE_IMPORTS" >> $GITHUB_STEP_SUMMARY else echo "has_relative_imports=false" >> $GITHUB_OUTPUT - echo "⚠️ No imports field found in added workflow; skipping preservation check" >> $GITHUB_STEP_SUMMARY + echo "⚠️ All imports already use cross-repo refs; skipping local-file preservation check" >> $GITHUB_STEP_SUMMARY + fi + else + echo "has_relative_imports=false" >> $GITHUB_OUTPUT + echo "⚠️ No imports field found in added workflow; skipping preservation check" >> $GITHUB_STEP_SUMMARY + fi + + - name: Create local copies of the shared import files + if: steps.add-workflow.outputs.has_relative_imports == 'true' + run: | + cd /tmp/test-update-workspace + echo "Creating local shared files..." >> $GITHUB_STEP_SUMMARY + while IFS= read -r import_path; do + local_file=".github/workflows/$import_path" + mkdir -p "$(dirname "$local_file")" + echo "# Local shared file (simulating user-copied content)" > "$local_file" + echo "✅ Created: $local_file" >> $GITHUB_STEP_SUMMARY + done < /tmp/relative-imports.txt + + - name: Run gh aw update --force + if: steps.add-workflow.outputs.has_relative_imports == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + cd /tmp/test-update-workspace + echo "Running gh aw update --force --no-compile daily-team-status ..." + /home/runner/work/gh-aw/gh-aw/gh-aw update daily-team-status --force --no-compile 2>&1 + echo "✅ Update completed" >> $GITHUB_STEP_SUMMARY + + - name: Verify local import paths are preserved after update + if: steps.add-workflow.outputs.has_relative_imports == 'true' + run: | + cd /tmp/test-update-workspace + WORKFLOW=".github/workflows/daily-team-status.md" + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Import paths after \`gh aw update\`" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + grep -A10 "^imports:" "$WORKFLOW" >> $GITHUB_STEP_SUMMARY || true + echo '```' >> $GITHUB_STEP_SUMMARY + + FAILED=false + while IFS= read -r import_path; do + local_file=".github/workflows/$import_path" + + # Confirm the local file still exists (sanity check) + if [ ! -f "$local_file" ]; then + echo "⚠️ Local file not found (was it deleted?): $local_file" + continue + fi + + # The import entry in the workflow file must still be the relative path, + # NOT a cross-repo reference (which would contain "@" and "owner/repo/"). + if grep -qF "- $import_path" "$WORKFLOW"; then + echo "✅ Relative import preserved: $import_path" >> $GITHUB_STEP_SUMMARY + else + echo "❌ FAIL: '$import_path' was rewritten even though the local file exists" >> $GITHUB_STEP_SUMMARY + echo "❌ Current imports section:" + grep -A10 "^imports:" "$WORKFLOW" || true + FAILED=true fi + done < /tmp/relative-imports.txt - - name: Create local copies of the shared import files - if: steps.add-workflow.outputs.has_relative_imports == 'true' - run: | - cd /tmp/test-update-workspace - echo "Creating local shared files..." >> $GITHUB_STEP_SUMMARY - while IFS= read -r import_path; do - local_file=".github/workflows/$import_path" - mkdir -p "$(dirname "$local_file")" - echo "# Local shared file (simulating user-copied content)" > "$local_file" - echo "✅ Created: $local_file" >> $GITHUB_STEP_SUMMARY - done < /tmp/relative-imports.txt - - - name: Run gh aw update --force - if: steps.add-workflow.outputs.has_relative_imports == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - cd /tmp/test-update-workspace - echo "Running gh aw update --force --no-compile daily-team-status ..." - /home/runner/work/gh-aw/gh-aw/gh-aw update daily-team-status --force --no-compile 2>&1 - echo "✅ Update completed" >> $GITHUB_STEP_SUMMARY - - - name: Verify local import paths are preserved after update - if: steps.add-workflow.outputs.has_relative_imports == 'true' - run: | - cd /tmp/test-update-workspace - WORKFLOW=".github/workflows/daily-team-status.md" - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Import paths after \`gh aw update\`" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - grep -A10 "^imports:" "$WORKFLOW" >> $GITHUB_STEP_SUMMARY || true - echo '```' >> $GITHUB_STEP_SUMMARY - - FAILED=false - while IFS= read -r import_path; do - local_file=".github/workflows/$import_path" - - # Confirm the local file still exists (sanity check) - if [ ! -f "$local_file" ]; then - echo "⚠️ Local file not found (was it deleted?): $local_file" - continue - fi - - # The import entry in the workflow file must still be the relative path, - # NOT a cross-repo reference (which would contain "@" and "owner/repo/"). - if grep -qF "- $import_path" "$WORKFLOW"; then - echo "✅ Relative import preserved: $import_path" >> $GITHUB_STEP_SUMMARY - else - echo "❌ FAIL: '$import_path' was rewritten even though the local file exists" >> $GITHUB_STEP_SUMMARY - echo "❌ Current imports section:" - grep -A10 "^imports:" "$WORKFLOW" || true - FAILED=true - fi - done < /tmp/relative-imports.txt - - if [ "$FAILED" = "true" ]; then - echo "❌ **FAILURE**: gh aw update rewrote local relative imports to cross-repo paths" >> $GITHUB_STEP_SUMMARY - exit 1 - fi + if [ "$FAILED" = "true" ]; then + echo "❌ **FAILURE**: gh aw update rewrote local relative imports to cross-repo paths" >> $GITHUB_STEP_SUMMARY + exit 1 + fi - echo "✅ **SUCCESS**: All local relative import paths were preserved by gh aw update" >> $GITHUB_STEP_SUMMARY + echo "✅ **SUCCESS**: All local relative import paths were preserved by gh aw update" >> $GITHUB_STEP_SUMMARY integration-unauthenticated-add: name: Integration Unauthenticated Add (Public Repo) + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -2686,57 +982,60 @@ jobs: group: ci-${{ github.ref }}-integration-unauthenticated-add cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies - run: go mod download - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Run unauthenticated integration tests + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Run unauthenticated integration tests # Explicitly clear all GitHub auth tokens to reproduce the agentic-workflow # environment where gh CLI is not authenticated. Tests must succeed for public # repositories via the raw URL / git fallback path. - env: - GITHUB_TOKEN: "" - GH_TOKEN: "" - run: | - set -o pipefail - go test -v -parallel=4 -timeout=10m -tags 'integration' -json \ - -run 'TestAddPublicWorkflowUnauthenticated|TestDownloadFileFromGitHubUnauthenticated' \ - ./pkg/cli/ ./pkg/parser/ \ - | tee test-result-integration-unauthenticated.json - - - name: Upload test results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-result-integration-unauthenticated - path: test-result-integration-unauthenticated.json - retention-days: 14 + env: + GITHUB_TOKEN: "" + GH_TOKEN: "" + run: | + set -o pipefail + go test -v -parallel=4 -timeout=10m -tags 'integration' -json \ + -run 'TestAddPublicWorkflowUnauthenticated|TestDownloadFileFromGitHubUnauthenticated' \ + ./pkg/cli/ ./pkg/parser/ \ + | tee test-result-integration-unauthenticated.json + + - name: Upload test results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-result-integration-unauthenticated + path: test-result-integration-unauthenticated.json + retention-days: 14 integration-add-dispatch-workflow: name: Integration Add with dispatch-workflow Dependencies + if: ${{ needs.changes.outputs.has_changes == 'true' }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -2745,54 +1044,56 @@ jobs: group: ci-${{ github.ref }}-integration-add-dispatch-workflow cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Set up Go - id: setup-go - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 - with: - go-version-file: go.mod - cache: true - - - name: Report Go cache status - run: | - if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then - echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY - else - echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY - fi - - - name: Download dependencies - run: go mod download - - - name: Verify dependencies - run: go mod verify - - - name: Build gh-aw binary - run: make build - - - name: Run dispatch-workflow add integration tests - env: - GH_TOKEN: ${{ github.token }} - run: | - set -o pipefail - go test -v -parallel=4 -timeout=10m -tags 'integration' -json \ - -run 'TestAddWorkflowWithDispatchWorkflow' \ - ./pkg/cli/ \ - | tee test-result-integration-add-dispatch-workflow.json - - - name: Upload test results - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: test-result-integration-add-dispatch-workflow - path: test-result-integration-add-dispatch-workflow.json - retention-days: 14 + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Go + id: setup-go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 + with: + go-version-file: go.mod + cache: true + + - name: Report Go cache status + run: | + if [ "${{ steps.setup-go.outputs.cache-hit }}" == "true" ]; then + echo "✅ Go cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Go cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Download dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Build gh-aw binary + run: make build + + - name: Run dispatch-workflow add integration tests + env: + GH_TOKEN: ${{ github.token }} + run: | + set -o pipefail + go test -v -parallel=4 -timeout=10m -tags 'integration' -json \ + -run 'TestAddWorkflowWithDispatchWorkflow' \ + ./pkg/cli/ \ + | tee test-result-integration-add-dispatch-workflow.json + + - name: Upload test results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: test-result-integration-add-dispatch-workflow + path: test-result-integration-add-dispatch-workflow.json + retention-days: 14 integration-release-availability: - if: github.ref == 'refs/heads/main' name: Integration Release Availability + if: ${{ needs.changes.outputs.has_changes == 'true' && (github.ref == 'refs/heads/main') }} + needs: + - changes runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -2803,100 +1104,102 @@ jobs: env: GH_TOKEN: ${{ github.token }} steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Extract versions from constants.go - id: versions - run: | - CONSTANTS="pkg/constants/version_constants.go" - - # Helper: regex a Version constant value from version_constants.go - extract() { - grep -oP "${1}"'\s+Version\s*=\s*"\K[^"]+' "$CONSTANTS" - } - - FIREWALL_VERSION=$(extract "DefaultFirewallVersion") - MCPG_VERSION=$(extract "DefaultMCPGatewayVersion") - GITHUB_MCP_VERSION=$(extract "DefaultGitHubMCPServerVersion") - - echo "firewall_version=$FIREWALL_VERSION" >> $GITHUB_OUTPUT - echo "mcpg_version=$MCPG_VERSION" >> $GITHUB_OUTPUT - echo "github_mcp_version=$GITHUB_MCP_VERSION" >> $GITHUB_OUTPUT - - echo "Extracted versions from pkg/constants/version_constants.go:" - echo " gh-aw-firewall: $FIREWALL_VERSION" - echo " gh-aw-mcpg: $MCPG_VERSION" - echo " github-mcp-server: $GITHUB_MCP_VERSION" - - - name: Check gh-aw-firewall release - env: - VERSION: ${{ steps.versions.outputs.firewall_version }} - run: | - set -e - REPO="github/gh-aw-firewall" - - echo "## Checking gh-aw-firewall ${VERSION}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "Checking GitHub release: ${REPO}@${VERSION}..." - if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then - echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - else - echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - sleep 2 # Avoid GitHub API rate limiting between checks - - - name: Check gh-aw-mcpg release - env: - VERSION: ${{ steps.versions.outputs.mcpg_version }} - run: | - set -e - REPO="github/gh-aw-mcpg" - - echo "## Checking gh-aw-mcpg ${VERSION}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "Checking GitHub release: ${REPO}@${VERSION}..." - if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then - echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - else - echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY - sleep 2 # Avoid GitHub API rate limiting between checks - - - name: Check github-mcp-server release - env: - VERSION: ${{ steps.versions.outputs.github_mcp_version }} - run: | - set -e - REPO="github/github-mcp-server" - - echo "## Checking github-mcp-server ${VERSION}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - echo "Checking GitHub release: ${REPO}@${VERSION}..." - if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then - echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - else - echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY - exit 1 - fi - - echo "" >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Extract versions from constants.go + id: versions + run: | + CONSTANTS="pkg/constants/version_constants.go" + + # Helper: regex a Version constant value from version_constants.go + extract() { + grep -oP "${1}"'\s+Version\s*=\s*"\K[^"]+' "$CONSTANTS" + } + + FIREWALL_VERSION=$(extract "DefaultFirewallVersion") + MCPG_VERSION=$(extract "DefaultMCPGatewayVersion") + GITHUB_MCP_VERSION=$(extract "DefaultGitHubMCPServerVersion") + + echo "firewall_version=$FIREWALL_VERSION" >> $GITHUB_OUTPUT + echo "mcpg_version=$MCPG_VERSION" >> $GITHUB_OUTPUT + echo "github_mcp_version=$GITHUB_MCP_VERSION" >> $GITHUB_OUTPUT + + echo "Extracted versions from pkg/constants/version_constants.go:" + echo " gh-aw-firewall: $FIREWALL_VERSION" + echo " gh-aw-mcpg: $MCPG_VERSION" + echo " github-mcp-server: $GITHUB_MCP_VERSION" + + - name: Check gh-aw-firewall release + env: + VERSION: ${{ steps.versions.outputs.firewall_version }} + run: | + set -e + REPO="github/gh-aw-firewall" + + echo "## Checking gh-aw-firewall ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "Checking GitHub release: ${REPO}@${VERSION}..." + if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then + echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + else + echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + sleep 2 # Avoid GitHub API rate limiting between checks + + - name: Check gh-aw-mcpg release + env: + VERSION: ${{ steps.versions.outputs.mcpg_version }} + run: | + set -e + REPO="github/gh-aw-mcpg" + + echo "## Checking gh-aw-mcpg ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "Checking GitHub release: ${REPO}@${VERSION}..." + if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then + echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + else + echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY + sleep 2 # Avoid GitHub API rate limiting between checks + + - name: Check github-mcp-server release + env: + VERSION: ${{ steps.versions.outputs.github_mcp_version }} + run: | + set -e + REPO="github/github-mcp-server" + + echo "## Checking github-mcp-server ${VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "Checking GitHub release: ${REPO}@${VERSION}..." + if gh release view "${VERSION}" --repo "${REPO}" > /dev/null 2>&1; then + echo "✅ GitHub release ${VERSION} is available for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + else + echo "❌ GitHub release ${VERSION} not found for ${REPO}" | tee -a $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "" >> $GITHUB_STEP_SUMMARY sh-difc-proxy: - if: github.ref == 'refs/heads/main' name: DIFC Proxy sh Integration Test + if: ${{ needs.changes.outputs.has_changes == 'true' && (github.ref == 'refs/heads/main') }} + needs: + - changes + - validate-yaml runs-on: ubuntu-latest timeout-minutes: 15 - needs: validate-yaml permissions: contents: read packages: read @@ -2906,209 +1209,211 @@ jobs: env: DIFC_PROXY_IMAGE: ghcr.io/github/gh-aw-mcpg:v0.2.8 steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Log in to GitHub Container Registry + - name: Log in to GitHub Container Registry # SECURITY: token moved to env mapping to prevent shell interpretation # of the token value as syntax - env: - GITHUB_TOKEN: ${{ github.token }} - run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - - - name: Pull DIFC proxy image - run: bash actions/setup/sh/download_docker_images.sh "$DIFC_PROXY_IMAGE" - - - name: Start DIFC proxy - env: - GH_TOKEN: ${{ github.token }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_REPOSITORY: ${{ github.repository }} - DIFC_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - run: | - bash actions/setup/sh/start_difc_proxy.sh - - - name: Verify DIFC proxy started - env: - GH_HOST: localhost:18443 - GH_REPO: ${{ github.repository }} - GITHUB_API_URL: https://localhost:18443/api/v3 - GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql - NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt - run: | - if [ "${GH_HOST}" != "localhost:18443" ]; then - echo "❌ DIFC proxy did not start (GH_HOST=${GH_HOST:-not set})" - echo "Docker container logs:" - docker logs awmg-proxy 2>&1 || true - echo "" - echo "Persisted proxy logs (if any) from /tmp/gh-aw/proxy-logs:" - if [ -d "/tmp/gh-aw/proxy-logs" ]; then - for f in /tmp/gh-aw/proxy-logs/*; do - [ -e "$f" ] || continue - echo "---- $f ----" - cat "$f" || true - echo "" - done - else - echo "No /tmp/gh-aw/proxy-logs directory found." - fi - echo "" - echo "Persisted MCP logs (if any) from /tmp/gh-aw/mcp-logs:" - if [ -d "/tmp/gh-aw/mcp-logs" ]; then - for f in /tmp/gh-aw/mcp-logs/*; do - [ -e "$f" ] || continue - echo "---- $f ----" - cat "$f" || true - echo "" - done - else - echo "No /tmp/gh-aw/mcp-logs directory found." - fi - exit 1 + env: + GITHUB_TOKEN: ${{ github.token }} + run: echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Pull DIFC proxy image + run: bash actions/setup/sh/download_docker_images.sh "$DIFC_PROXY_IMAGE" + + - name: Start DIFC proxy + env: + GH_TOKEN: ${{ github.token }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + DIFC_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' + run: | + bash actions/setup/sh/start_difc_proxy.sh + + - name: Verify DIFC proxy started + env: + GH_HOST: localhost:18443 + GH_REPO: ${{ github.repository }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + run: | + if [ "${GH_HOST}" != "localhost:18443" ]; then + echo "❌ DIFC proxy did not start (GH_HOST=${GH_HOST:-not set})" + echo "Docker container logs:" + docker logs awmg-proxy 2>&1 || true + echo "" + echo "Persisted proxy logs (if any) from /tmp/gh-aw/proxy-logs:" + if [ -d "/tmp/gh-aw/proxy-logs" ]; then + for f in /tmp/gh-aw/proxy-logs/*; do + [ -e "$f" ] || continue + echo "---- $f ----" + cat "$f" || true + echo "" + done + else + echo "No /tmp/gh-aw/proxy-logs directory found." fi - echo "✅ DIFC proxy started (GH_HOST=${GH_HOST})" - - - name: Test gh CLI through proxy - env: - GH_TOKEN: ${{ github.token }} - GH_HOST: localhost:18443 - GH_REPO: ${{ github.repository }} - GITHUB_API_URL: https://localhost:18443/api/v3 - GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql - NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt - run: | - echo "Testing gh CLI through DIFC proxy (GH_HOST=${GH_HOST})..." - repo_name=$(gh api /repos/${{ github.repository }} --jq '.full_name') - echo "✅ gh CLI works through DIFC proxy (repo: $repo_name)" - - - name: Test actions/github-script with proxy env - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - GH_HOST: localhost:18443 - GH_REPO: ${{ github.repository }} - GITHUB_API_URL: https://localhost:18443/api/v3 - GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql - NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt - with: - github-token: ${{ github.token }} - script: | - // GITHUB_API_URL is a protected GitHub Actions default env variable and - // cannot be overridden via $GITHUB_ENV; verify GH_HOST instead. - const expectedGhHost = 'localhost:18443'; - const ghHost = process.env.GH_HOST; - console.log(`GH_HOST: ${ghHost}`); - if (ghHost !== expectedGhHost) { - throw new Error(`Expected GH_HOST to be "${expectedGhHost}", got: ${ghHost}`); - } - const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); - const { data } = await github.rest.repos.get({ owner, repo }); - console.log(`✅ actions/github-script works with proxy env active (repo: ${data.full_name})`); - - - name: Stop DIFC proxy - if: always() - run: bash actions/setup/sh/stop_difc_proxy.sh - - - name: Generate summary - if: always() - run: | - echo "## DIFC Proxy sh Integration Test" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY - echo "1. \`start_difc_proxy.sh\` starts the proxy container" >> $GITHUB_STEP_SUMMARY - echo "2. \`gh\` CLI calls are routed through the proxy (\`GH_HOST=localhost:18443\`)" >> $GITHUB_STEP_SUMMARY - echo "3. \`actions/github-script\` sees the proxy env (\`GH_HOST=localhost:18443\`)" >> $GITHUB_STEP_SUMMARY - echo "4. \`stop_difc_proxy.sh\` stops the proxy container" >> $GITHUB_STEP_SUMMARY + echo "" + echo "Persisted MCP logs (if any) from /tmp/gh-aw/mcp-logs:" + if [ -d "/tmp/gh-aw/mcp-logs" ]; then + for f in /tmp/gh-aw/mcp-logs/*; do + [ -e "$f" ] || continue + echo "---- $f ----" + cat "$f" || true + echo "" + done + else + echo "No /tmp/gh-aw/mcp-logs directory found." + fi + exit 1 + fi + echo "✅ DIFC proxy started (GH_HOST=${GH_HOST})" + + - name: Test gh CLI through proxy + env: + GH_TOKEN: ${{ github.token }} + GH_HOST: localhost:18443 + GH_REPO: ${{ github.repository }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + run: | + echo "Testing gh CLI through DIFC proxy (GH_HOST=${GH_HOST})..." + repo_name=$(gh api /repos/${{ github.repository }} --jq '.full_name') + echo "✅ gh CLI works through DIFC proxy (repo: $repo_name)" + + - name: Test actions/github-script with proxy env + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_HOST: localhost:18443 + GH_REPO: ${{ github.repository }} + GITHUB_API_URL: https://localhost:18443/api/v3 + GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql + NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt + with: + github-token: ${{ github.token }} + script: | + // GITHUB_API_URL is a protected GitHub Actions default env variable and + // cannot be overridden via $GITHUB_ENV; verify GH_HOST instead. + const expectedGhHost = 'localhost:18443'; + const ghHost = process.env.GH_HOST; + console.log(`GH_HOST: ${ghHost}`); + if (ghHost !== expectedGhHost) { + throw new Error(`Expected GH_HOST to be "${expectedGhHost}", got: ${ghHost}`); + } + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + const { data } = await github.rest.repos.get({ owner, repo }); + console.log(`✅ actions/github-script works with proxy env active (repo: ${data.full_name})`); + + - name: Stop DIFC proxy + if: always() + run: bash actions/setup/sh/stop_difc_proxy.sh + + - name: Generate summary + if: always() + run: | + echo "## DIFC Proxy sh Integration Test" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This test verifies that:" >> $GITHUB_STEP_SUMMARY + echo "1. \`start_difc_proxy.sh\` starts the proxy container" >> $GITHUB_STEP_SUMMARY + echo "2. \`gh\` CLI calls are routed through the proxy (\`GH_HOST=localhost:18443\`)" >> $GITHUB_STEP_SUMMARY + echo "3. \`actions/github-script\` sees the proxy env (\`GH_HOST=localhost:18443\`)" >> $GITHUB_STEP_SUMMARY + echo "4. \`stop_difc_proxy.sh\` stops the proxy container" >> $GITHUB_STEP_SUMMARY sh-gh-host-pr-checkout-repro: - if: github.ref == 'refs/heads/main' - name: GH_HOST Proxy PR Checkout Repro (Issue #23461) + name: GH_HOST Proxy PR Checkout Repro (Issue + if: ${{ needs.changes.outputs.has_changes == 'true' && (github.ref == 'refs/heads/main') }} + needs: + - changes + - validate-yaml runs-on: ubuntu-latest timeout-minutes: 10 - needs: validate-yaml permissions: contents: read concurrency: group: ci-${{ github.ref }}-sh-gh-host-pr-checkout-repro cancel-in-progress: true steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Start local proxy - run: | - # Start a simple HTTP server on port 19443 to simulate a local proxy. - # This is the "local proxy" referenced in the repro scenario for issue #23461: - # a server IS running on the proxy host, but git remotes still point to the real - # GitHub host, causing `gh pr checkout` to fail. - python3 -m http.server 19443 --bind 127.0.0.1 --directory /tmp >/tmp/local-proxy.log 2>&1 & - echo "LOCAL_PROXY_PID=$!" >> "$GITHUB_ENV" - sleep 1 - echo "Local proxy started on port 19443 (simulating a DIFC-style proxy)" - - - name: Set GH_HOST to simulate proxy-rewritten environment - run: | - # Set GH_HOST=localhost:19443 directly to simulate the proxy-rewritten environment - # described in issue #23461. We write directly to $GITHUB_ENV here because - # GITHUB_SERVER_URL is a runner-managed variable that cannot be overridden via - # step-level env:, so running configure_gh_for_ghe.sh with - # GITHUB_SERVER_URL=http://localhost:19443 would be silently ignored. - echo "GH_HOST=localhost:19443" >> "$GITHUB_ENV" - echo "Set GH_HOST=localhost:19443 (simulating a DIFC proxy-rewritten environment)" - - - name: Repro - gh pr checkout fails when GH_HOST points to proxy without matching remote - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "GH_HOST=${GH_HOST}" - echo "Git remotes (origin still points to github.com, not the local proxy):" - git remote -v - - # Attempt gh pr checkout. GH_HOST=localhost:19443 but the only git remote - # is origin pointing to github.com — no remote matches the proxy host. - # gh CLI should exit non-zero with the characteristic mismatch error. - # Any PR number works here: gh validates GH_HOST against git remotes before - # making any API call, so the failure occurs regardless of whether PR #1 exists. - set +e - error_output=$(gh pr checkout 1 2>&1) - exit_code=$? - set -e - - echo "gh pr checkout exit code: ${exit_code}" - echo "Output: ${error_output}" - - if [ "${exit_code}" -eq 0 ]; then - echo "❌ Expected gh pr checkout to fail but it succeeded unexpectedly" - exit 1 - fi - - if echo "${error_output}" | grep -q "none of the git remotes"; then - echo "✅ Issue #23461 reproduced: GH_HOST mismatch causes gh pr checkout to fail" - echo " Error: ${error_output}" - else - echo "❌ gh pr checkout failed, but not with the expected GH_HOST/git remote mismatch error" - echo " Unexpected error output: ${error_output}" - exit 1 - fi - - - name: Stop local proxy - if: always() - run: | - if [ -n "${LOCAL_PROXY_PID:-}" ]; then - kill "${LOCAL_PROXY_PID}" 2>/dev/null || true - echo "Local proxy stopped" - fi - - - name: Generate summary - if: always() - run: | - echo "## GH_HOST Proxy PR Checkout Repro (Issue [#23461](https://github.com/github/gh-aw/issues/23461))" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "This test reproduces the failure mode described in issue #23461:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "1. A local HTTP server starts on \`localhost:19443\` (the \"local proxy\")" >> $GITHUB_STEP_SUMMARY - echo "2. \`configure_gh_for_ghe.sh\` sets \`GH_HOST=localhost:19443\` (simulating a proxy-rewritten environment)" >> $GITHUB_STEP_SUMMARY - echo "3. Git remote \`origin\` still points to the real GitHub host (not the proxy)" >> $GITHUB_STEP_SUMMARY - echo "4. \`gh pr checkout\` fails because no git remote matches \`GH_HOST\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The existing \`sh-difc-proxy\` job uses the full Docker-based DIFC proxy; this job" >> $GITHUB_STEP_SUMMARY - echo "provides a lightweight local-proxy repro that does not require Docker." >> $GITHUB_STEP_SUMMARY + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Start local proxy + run: | + # Start a simple HTTP server on port 19443 to simulate a local proxy. + # This is the "local proxy" referenced in the repro scenario for issue #23461: + # a server IS running on the proxy host, but git remotes still point to the real + # GitHub host, causing `gh pr checkout` to fail. + python3 -m http.server 19443 --bind 127.0.0.1 --directory /tmp >/tmp/local-proxy.log 2>&1 & + echo "LOCAL_PROXY_PID=$!" >> "$GITHUB_ENV" + sleep 1 + echo "Local proxy started on port 19443 (simulating a DIFC-style proxy)" + + - name: Set GH_HOST to simulate proxy-rewritten environment + run: | + # Set GH_HOST=localhost:19443 directly to simulate the proxy-rewritten environment + # described in issue #23461. We write directly to $GITHUB_ENV here because + # GITHUB_SERVER_URL is a runner-managed variable that cannot be overridden via + # step-level env:, so running configure_gh_for_ghe.sh with + # GITHUB_SERVER_URL=http://localhost:19443 would be silently ignored. + echo "GH_HOST=localhost:19443" >> "$GITHUB_ENV" + echo "Set GH_HOST=localhost:19443 (simulating a DIFC proxy-rewritten environment)" + + - name: Repro - gh pr checkout fails when GH_HOST points to proxy without matching remote + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "GH_HOST=${GH_HOST}" + echo "Git remotes (origin still points to github.com, not the local proxy):" + git remote -v + + # Attempt gh pr checkout. GH_HOST=localhost:19443 but the only git remote + # is origin pointing to github.com — no remote matches the proxy host. + # gh CLI should exit non-zero with the characteristic mismatch error. + # Any PR number works here: gh validates GH_HOST against git remotes before + # making any API call, so the failure occurs regardless of whether PR #1 exists. + set +e + error_output=$(gh pr checkout 1 2>&1) + exit_code=$? + set -e + + echo "gh pr checkout exit code: ${exit_code}" + echo "Output: ${error_output}" + + if [ "${exit_code}" -eq 0 ]; then + echo "❌ Expected gh pr checkout to fail but it succeeded unexpectedly" + exit 1 + fi + + if echo "${error_output}" | grep -q "none of the git remotes"; then + echo "✅ Issue #23461 reproduced: GH_HOST mismatch causes gh pr checkout to fail" + echo " Error: ${error_output}" + else + echo "❌ gh pr checkout failed, but not with the expected GH_HOST/git remote mismatch error" + echo " Unexpected error output: ${error_output}" + exit 1 + fi + + - name: Stop local proxy + if: always() + run: | + if [ -n "${LOCAL_PROXY_PID:-}" ]; then + kill "${LOCAL_PROXY_PID}" 2>/dev/null || true + echo "Local proxy stopped" + fi + + - name: Generate summary + if: always() + run: | + echo "## GH_HOST Proxy PR Checkout Repro (Issue [#23461](https://github.com/github/gh-aw/issues/23461))" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "This test reproduces the failure mode described in issue #23461:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "1. A local HTTP server starts on \`localhost:19443\` (the \"local proxy\")" >> $GITHUB_STEP_SUMMARY + echo "2. \`configure_gh_for_ghe.sh\` sets \`GH_HOST=localhost:19443\` (simulating a proxy-rewritten environment)" >> $GITHUB_STEP_SUMMARY + echo "3. Git remote \`origin\` still points to the real GitHub host (not the proxy)" >> $GITHUB_STEP_SUMMARY + echo "4. \`gh pr checkout\` fails because no git remote matches \`GH_HOST\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The existing \`sh-difc-proxy\` job uses the full Docker-based DIFC proxy; this job" >> $GITHUB_STEP_SUMMARY + echo "provides a lightweight local-proxy repro that does not require Docker." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/cjs.yml b/.github/workflows/cjs.yml new file mode 100644 index 00000000000..c5e1b3bd362 --- /dev/null +++ b/.github/workflows/cjs.yml @@ -0,0 +1,86 @@ +name: CJS + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'actions/setup/js/**' + - 'actions/setup/md/**' + - 'scripts/**/*.js' + - '.github/workflows/ci.yml' + - '.github/workflows/cjs.yml' + workflow_dispatch: + +jobs: + js: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-js + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up Node.js + id: setup-node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: actions/setup/js/package-lock.json + - name: Report Node cache status + run: | + if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then + echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY + fi + - name: Install npm dependencies + run: cd actions/setup/js && npm ci + - name: Setup prompt templates for tests + run: | + mkdir -p ${{ runner.temp }}/gh-aw/prompts + cp actions/setup/md/*.md ${{ runner.temp }}/gh-aw/prompts/ + - name: Run tests + run: cd actions/setup/js && npm test + + lint-js: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + concurrency: + group: ci-${{ github.ref }}-lint-js + cancel-in-progress: true + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + id: setup-node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + node-version: "24" + cache: npm + cache-dependency-path: actions/setup/js/package-lock.json + + - name: Report Node cache status + run: | + if [ "${{ steps.setup-node.outputs.cache-hit }}" == "true" ]; then + echo "✅ Node cache hit" >> $GITHUB_STEP_SUMMARY + else + echo "⚠️ Node cache miss" >> $GITHUB_STEP_SUMMARY + fi + + - name: Install npm dependencies + run: cd actions/setup/js && npm ci + + - name: Lint JavaScript files + run: make lint-cjs + + - name: Check JSON formatting + run: make fmt-check-json