name: PR Checks on: pull_request: types: [opened, synchronize, reopened, edited] branches: - dev - main # Default permissions for all jobs # Note: Fork PRs won't have write access for security # Advisory checks use separate workflow (pr-checks-run.yml + pr-checks-comment.yml) permissions: contents: read # Read repository contents pull-requests: write # Manage PRs (labels, comments) - only works for non-fork PRs issues: write # Manage issues (PRs are issues) - only works for non-fork PRs jobs: # Validate PR title and description validate-pr: name: Validate PR Format runs-on: ubuntu-latest # Inherits workflow-level permissions (contents: read, pull-requests: write, issues: write) steps: - name: Check PR title format id: semantic-pr continue-on-error: true # Don't block PR if title format is invalid uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: types: | feat fix docs style refactor perf test chore ci security build scopes: | exchange trader ai api ui frontend backend security deps workflow github actions config docker build release requireScope: false - name: Comment on invalid PR title if: steps.semantic-pr.outcome == 'failure' uses: actions/github-script@v7 continue-on-error: true # Don't fail for fork PRs with: script: | const prTitle = context.payload.pull_request.title; const isFork = context.payload.pull_request.head.repo.full_name !== context.payload.pull_request.base.repo.full_name; const comment = [ '## ⚠️ PR Title Format Suggestion', '', "Your PR title doesn't follow the Conventional Commits format, but **this won't block your PR from being merged**.", '', `**Current title:** \`${prTitle}\``, '', '**Recommended format:** `type(scope): description`', '', '### Valid types:', '`feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `security`, `build`', '', '### Common scopes (optional):', '`exchange`, `trader`, `ai`, `api`, `ui`, `frontend`, `backend`, `security`, `deps`, `workflow`, `github`, `actions`, `config`, `docker`, `build`, `release`', '', '### Examples:', '- `feat(trader): add new trading strategy`', '- `fix(api): resolve authentication issue`', '- `docs: update README`', '- `chore(deps): update dependencies`', '- `ci(workflow): improve GitHub Actions`', '', '**Note:** This is a suggestion to improve consistency. Your PR can still be reviewed and merged.', '', '---', '*This is an automated comment. You can update the PR title anytime.*' ].join('\n'); if (!isFork) { try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, body: comment }); } catch (error) { console.log('Could not post comment (expected for fork PRs):', error.message); } } else { console.log('Fork PR - comment will be posted by pr-checks-comment.yml'); } - name: Check PR size uses: actions/github-script@v7 continue-on-error: true # Don't fail for fork PRs with: script: | const pr = context.payload.pull_request; const additions = pr.additions; const deletions = pr.deletions; const total = additions + deletions; // Check if this is a fork PR const isFork = pr.head.repo.full_name !== pr.base.repo.full_name; let label = ''; let comment = ''; if (total < 300) { label = 'size: small'; comment = '✅ This PR is **small** and easy to review!'; } else if (total < 1000) { label = 'size: medium'; comment = '⚠️ This PR is **medium** sized. Consider breaking it into smaller PRs if possible.'; } else { label = 'size: large'; comment = '🚨 This PR is **large** (>' + total + ' lines changed). Please consider breaking it into smaller, focused PRs for easier review.'; } // Only add labels/comments for non-fork PRs (fork PRs don't have write permission) if (!isFork) { try { // Add size label await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, labels: [label] }); // Add comment for large PRs if (total >= 1000) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number, body: comment }); } } catch (error) { console.log('Failed to add label/comment (expected for fork PRs):', error.message); } } else { console.log('Fork PR detected - skipping label/comment (will be handled by pr-checks-comment.yml)'); } # Backend checks (simplified - no TA-Lib required) backend-checks: name: Backend Code Quality (Go) runs-on: ubuntu-latest permissions: contents: read # Only need read access for testing steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.21' - name: Cache Go modules uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download dependencies run: go mod download - name: Run go fmt continue-on-error: true # Don't block PR if formatting issues found run: | if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then echo "⚠️ Code formatting issues found. Please run 'go fmt ./...' locally." echo "" echo "Files needing formatting:" gofmt -s -l . echo "" echo "This is a warning and won't block your PR from being merged." exit 1 else echo "✅ All Go files are properly formatted" fi - name: Run go vet run: go vet ./... - name: Build run: go build -v -o nofx # Frontend tests frontend-tests: name: Frontend Tests (React/TypeScript) runs-on: ubuntu-latest permissions: contents: read # Only need read access for testing steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '18' - name: Cache Node modules uses: actions/cache@v4 with: path: web/node_modules key: ${{ runner.os }}-node-${{ hashFiles('web/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies working-directory: ./web run: npm ci - name: Build and Type Check working-directory: ./web run: npm run build # Note: build script runs "tsc && vite build" which includes type checking # Auto-label based on files changed auto-label: name: Auto Label PR runs-on: ubuntu-latest # Only run for non-fork PRs (fork PRs don't have write permission) if: github.event.pull_request.head.repo.full_name == github.repository permissions: contents: read pull-requests: write issues: write # Required: PRs are issues, labeler needs to modify issue labels steps: - uses: actions/labeler@v5 with: configuration-path: .github/labeler.yml repo-token: ${{ secrets.GITHUB_TOKEN }} # Check for security issues security-check: name: Security Scan runs-on: ubuntu-latest permissions: contents: read security-events: write # Required: Upload SARIF results to GitHub Security steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy results uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: 'trivy-results.sarif' # Check for secrets in code secrets-check: name: Check for Secrets runs-on: ubuntu-latest permissions: contents: read # Only need read access for scanning steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run TruffleHog OSS uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.pull_request.base.sha }} head: ${{ github.event.pull_request.head.sha }} extra_args: --debug --only-verified # All checks passed all-checks: name: All Checks Passed runs-on: ubuntu-latest needs: [validate-pr, backend-checks, frontend-tests, security-check, secrets-check] if: always() permissions: contents: read # Only need read access for status checking steps: - name: Check all jobs run: | # Note: validate-pr uses continue-on-error, so it won't block even if title format is invalid # We only care about actual test failures echo "validate-pr: ${{ needs.validate-pr.result }}" echo "backend-checks: ${{ needs.backend-checks.result }}" echo "frontend-tests: ${{ needs.frontend-tests.result }}" echo "security-check: ${{ needs.security-check.result }}" echo "secrets-check: ${{ needs.secrets-check.result }}" # Check if any critical checks failed (excluding validate-pr which is advisory) if [[ "${{ needs.backend-checks.result }}" == "failure" ]] || \ [[ "${{ needs.frontend-tests.result }}" == "failure" ]] || \ [[ "${{ needs.security-check.result }}" == "failure" ]] || \ [[ "${{ needs.secrets-check.result }}" == "failure" ]]; then echo "❌ Critical checks failed" exit 1 else echo "✅ All critical checks passed!" if [[ "${{ needs.validate-pr.result }}" != "success" ]]; then echo "ℹ️ Note: PR title format check is advisory only and doesn't block merging" fi fi