mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 00:07:01 +08:00
fix(workflow): fix github workflow
This commit is contained in:
175
.github/workflows/README.md
vendored
Normal file
175
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# GitHub Actions Workflows
|
||||||
|
|
||||||
|
This directory contains the GitHub Actions workflows for the NOFX project.
|
||||||
|
|
||||||
|
## 📚 Documentation Index
|
||||||
|
|
||||||
|
- **[README.md](./README.md)** - This file, overview of all workflows
|
||||||
|
- **[PERMISSIONS.md](./PERMISSIONS.md)** - Detailed permission analysis and security model
|
||||||
|
- **[TRIGGERS.md](./TRIGGERS.md)** - Comparison of event triggers (pull_request vs pull_request_target vs workflow_run)
|
||||||
|
- **[FORK_PR_FLOW.md](./FORK_PR_FLOW.md)** - Complete analysis of what happens when a fork PR is submitted
|
||||||
|
- **[FLOW_DIAGRAM.md](./FLOW_DIAGRAM.md)** - Visual flow diagrams and quick reference
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
**Want to understand how fork PRs work?** → Read [FLOW_DIAGRAM.md](./FLOW_DIAGRAM.md)
|
||||||
|
|
||||||
|
**Need security details?** → Read [PERMISSIONS.md](./PERMISSIONS.md)
|
||||||
|
|
||||||
|
**Confused about triggers?** → Read [TRIGGERS.md](./TRIGGERS.md)
|
||||||
|
|
||||||
|
## PR Check Workflows
|
||||||
|
|
||||||
|
We use a **two-workflow pattern** to safely handle PR checks from both internal and fork PRs:
|
||||||
|
|
||||||
|
### 1. `pr-checks-run.yml` - Execute Checks
|
||||||
|
|
||||||
|
**Trigger:** On pull request (opened, synchronize, reopened)
|
||||||
|
|
||||||
|
**Permissions:** Read-only
|
||||||
|
|
||||||
|
**Purpose:** Executes all PR checks with read-only permissions, making it safe for fork PRs.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Checks PR title format (Conventional Commits)
|
||||||
|
- ✅ Calculates PR size
|
||||||
|
- ✅ Runs backend checks (Go formatting, vet, tests)
|
||||||
|
- ✅ Runs frontend checks (linting, type checking, build)
|
||||||
|
- ✅ Saves all results as artifacts
|
||||||
|
|
||||||
|
**Security:** Safe for fork PRs because it only has read permissions and cannot access secrets or modify the repository.
|
||||||
|
|
||||||
|
### 2. `pr-checks-comment.yml` - Post Results
|
||||||
|
|
||||||
|
**Trigger:** When `pr-checks-run.yml` completes (workflow_run)
|
||||||
|
|
||||||
|
**Permissions:** Write (pull-requests, issues)
|
||||||
|
|
||||||
|
**Purpose:** Posts check results as PR comments, running in the main repository context.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Downloads artifacts from `pr-checks-run.yml`
|
||||||
|
- ✅ Reads check results
|
||||||
|
- ✅ Posts a comprehensive comment to the PR
|
||||||
|
|
||||||
|
**Security:** Safe because:
|
||||||
|
- Runs in the main repository context (not fork context)
|
||||||
|
- Has write permissions but doesn't execute untrusted code
|
||||||
|
- Only reads pre-generated results from artifacts
|
||||||
|
|
||||||
|
### 3. `pr-checks.yml` - Strict Checks
|
||||||
|
|
||||||
|
**Trigger:** On pull request
|
||||||
|
|
||||||
|
**Permissions:** Read + conditional write
|
||||||
|
|
||||||
|
**Purpose:** Runs mandatory checks that must pass before PR can be merged.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Validates PR title (blocks merge if invalid)
|
||||||
|
- ✅ Auto-labels PR based on size and files changed (non-fork only)
|
||||||
|
- ✅ Runs backend tests (Go)
|
||||||
|
- ✅ Runs frontend tests (React/TypeScript)
|
||||||
|
- ✅ Security scanning (Trivy, Gitleaks)
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Fork PRs: Only runs read-only operations (tests, security scans)
|
||||||
|
- Non-fork PRs: Can add labels and comments
|
||||||
|
- Uses `continue-on-error` for operations that may fail on forks
|
||||||
|
|
||||||
|
## Why Two Workflows for PR Checks?
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
When a PR comes from a forked repository:
|
||||||
|
- GitHub restricts `GITHUB_TOKEN` permissions for security
|
||||||
|
- Fork PRs cannot write comments, add labels, or access secrets
|
||||||
|
- This prevents malicious contributors from:
|
||||||
|
- Stealing repository secrets
|
||||||
|
- Modifying workflow files to execute malicious code
|
||||||
|
- Spamming issues/PRs with automated comments
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
**Two-Workflow Pattern:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fork PR Submitted
|
||||||
|
↓
|
||||||
|
[pr-checks-run.yml]
|
||||||
|
- Runs with read-only permissions
|
||||||
|
- Executes all checks safely
|
||||||
|
- Saves results to artifacts
|
||||||
|
↓
|
||||||
|
[pr-checks-comment.yml]
|
||||||
|
- Triggered by workflow_run
|
||||||
|
- Runs in main repo context (has write permissions)
|
||||||
|
- Downloads artifacts
|
||||||
|
- Posts comment with results
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach:
|
||||||
|
- ✅ Allows fork PRs to run checks
|
||||||
|
- ✅ Safely posts results as comments
|
||||||
|
- ✅ Prevents security vulnerabilities
|
||||||
|
- ✅ Follows GitHub's best practices
|
||||||
|
|
||||||
|
### Can workflow_run Comment on Fork PRs?
|
||||||
|
|
||||||
|
**Yes! ✅ The permissions are sufficient.**
|
||||||
|
|
||||||
|
**Key Understanding:**
|
||||||
|
- `workflow_run` executes in the **base repository** context
|
||||||
|
- Fork PRs exist in the **base repository** (not in the fork)
|
||||||
|
- The base repository's `GITHUB_TOKEN` has write permissions
|
||||||
|
- Therefore, `workflow_run` can comment on fork PRs
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Fork PR code runs in isolated environment (read-only)
|
||||||
|
- Comment workflow doesn't execute fork code
|
||||||
|
- Only reads pre-generated artifact data
|
||||||
|
|
||||||
|
**For detailed permission analysis, see:** [PERMISSIONS.md](./PERMISSIONS.md)
|
||||||
|
|
||||||
|
## Workflow Comparison
|
||||||
|
|
||||||
|
| Workflow | Fork PRs | Write Access | Blocks Merge | Purpose |
|
||||||
|
|----------|----------|--------------|--------------|---------|
|
||||||
|
| `pr-checks-run.yml` | ✅ Yes | ❌ No | ❌ No | Advisory checks |
|
||||||
|
| `pr-checks-comment.yml` | ✅ Yes | ✅ Yes* | ❌ No | Post results |
|
||||||
|
| `pr-checks.yml` | ✅ Yes | ⚠️ Partial | ✅ Yes | Mandatory checks |
|
||||||
|
|
||||||
|
\* Write access only in main repo context, not available to fork PR code
|
||||||
|
|
||||||
|
## File History
|
||||||
|
|
||||||
|
- `pr-checks-advisory.yml.old` - Old advisory workflow that failed on fork PRs (deprecated)
|
||||||
|
- Now replaced by the two-workflow pattern (`pr-checks-run.yml` + `pr-checks-comment.yml`)
|
||||||
|
|
||||||
|
## Testing the Workflows
|
||||||
|
|
||||||
|
### Test with a Fork PR
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Make changes in your fork
|
||||||
|
3. Create a PR to the main repository
|
||||||
|
4. Observe:
|
||||||
|
- `pr-checks-run.yml` runs successfully with read-only access
|
||||||
|
- `pr-checks-comment.yml` posts results as a comment
|
||||||
|
- `pr-checks.yml` runs tests but skips labeling
|
||||||
|
|
||||||
|
### Test with a Branch PR
|
||||||
|
|
||||||
|
1. Create a branch in the main repository
|
||||||
|
2. Make changes
|
||||||
|
3. Create a PR
|
||||||
|
4. Observe:
|
||||||
|
- All workflows run with full permissions
|
||||||
|
- Labels are added automatically
|
||||||
|
- Comments are posted
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GitHub Actions: Keeping your GitHub Actions and workflows secure Part 1](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)
|
||||||
|
- [Safely posting comments from untrusted workflows](https://securitylab.github.com/research/github-actions-building-blocks/)
|
||||||
|
- [GitHub Actions: workflow_run trigger](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run)
|
||||||
239
.github/workflows/pr-checks-comment.yml
vendored
Normal file
239
.github/workflows/pr-checks-comment.yml
vendored
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
name: PR Checks - Comment
|
||||||
|
|
||||||
|
# This workflow posts PR check results as comments
|
||||||
|
# Runs in the main repo context with write permissions (SAFE)
|
||||||
|
# Triggered after pr-checks-run.yml completes
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["PR Checks - Run"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
# Write permissions - SAFE because runs in main repo context
|
||||||
|
# This token has write access to the base repository
|
||||||
|
# Fork PRs exist in the base repo, so we can comment on them
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
actions: read # Needed to download artifacts
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Post Check Results
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if the workflow was triggered by a pull_request event
|
||||||
|
if: github.event.workflow_run.event == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: List downloaded artifacts
|
||||||
|
run: |
|
||||||
|
echo "=== Checking downloaded artifacts ==="
|
||||||
|
ls -la artifacts/ || echo "No artifacts directory"
|
||||||
|
find artifacts/ -type f || echo "No files found"
|
||||||
|
|
||||||
|
- name: Read PR info results
|
||||||
|
id: pr-info
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ -f artifacts/pr-info-results/pr-info.json ]; then
|
||||||
|
echo "=== PR Info JSON ==="
|
||||||
|
cat artifacts/pr-info-results/pr-info.json
|
||||||
|
echo "pr_number=$(jq -r '.pr_number' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "title_status=$(jq -r '.title_status' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "title_message=$(jq -r '.title_message' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "size=$(jq -r '.size' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "lines=$(jq -r '.lines' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "size_suggestion=$(jq -r '.size_suggestion' artifacts/pr-info-results/pr-info.json)" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "pr_number=0" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ PR info artifact not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Read backend results
|
||||||
|
id: backend
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ -f artifacts/backend-results/backend-results.json ]; then
|
||||||
|
echo "=== Backend Results JSON ==="
|
||||||
|
cat artifacts/backend-results/backend-results.json
|
||||||
|
echo "fmt_status=$(jq -r '.fmt_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "vet_status=$(jq -r '.vet_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "test_status=$(jq -r '.test_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Read output files
|
||||||
|
if [ -f artifacts/backend-results/fmt-files.txt ]; then
|
||||||
|
echo "fmt_files<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/fmt-files.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/backend-results/vet-output-short.txt ]; then
|
||||||
|
echo "vet_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/vet-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/backend-results/test-output-short.txt ]; then
|
||||||
|
echo "test_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/test-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Backend results artifact not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Read frontend results
|
||||||
|
id: frontend
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ -f artifacts/frontend-results/frontend-results.json ]; then
|
||||||
|
echo "=== Frontend Results JSON ==="
|
||||||
|
cat artifacts/frontend-results/frontend-results.json
|
||||||
|
echo "lint_status=$(jq -r '.lint_status' artifacts/frontend-results/frontend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "typecheck_status=$(jq -r '.typecheck_status' artifacts/frontend-results/frontend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_status=$(jq -r '.build_status' artifacts/frontend-results/frontend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Read output files
|
||||||
|
if [ -f artifacts/frontend-results/lint-output-short.txt ]; then
|
||||||
|
echo "lint_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/frontend-results/lint-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/frontend-results/typecheck-output-short.txt ]; then
|
||||||
|
echo "typecheck_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/frontend-results/typecheck-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/frontend-results/build-output-short.txt ]; then
|
||||||
|
echo "build_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/frontend-results/build-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Frontend results artifact not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post combined comment
|
||||||
|
if: steps.pr-info.outputs.pr_number != '0'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = ${{ steps.pr-info.outputs.pr_number }};
|
||||||
|
|
||||||
|
// PR Info section
|
||||||
|
const titleStatus = '${{ steps.pr-info.outputs.title_status }}' || '⚠️ Unknown';
|
||||||
|
const prSize = '${{ steps.pr-info.outputs.size }}' || '⚠️ Unknown';
|
||||||
|
const prLines = '${{ steps.pr-info.outputs.lines }}' || '0';
|
||||||
|
const sizeSuggestion = '${{ steps.pr-info.outputs.size_suggestion }}' || '';
|
||||||
|
|
||||||
|
let comment = '## 🤖 PR Checks Results\n\n';
|
||||||
|
comment += 'Thank you for your contribution! Here are the automated check results:\n\n';
|
||||||
|
|
||||||
|
// PR Info
|
||||||
|
comment += '### 📋 PR Information\n\n';
|
||||||
|
comment += '**Title Format:** ' + titleStatus + '\n';
|
||||||
|
comment += '**PR Size:** ' + prSize + ' (' + prLines + ' lines changed)\n';
|
||||||
|
if (sizeSuggestion) {
|
||||||
|
comment += '\n💡 **Suggestion:** ' + sizeSuggestion + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend checks
|
||||||
|
const fmtStatus = '${{ steps.backend.outputs.fmt_status }}';
|
||||||
|
const vetStatus = '${{ steps.backend.outputs.vet_status }}';
|
||||||
|
const testStatus = '${{ steps.backend.outputs.test_status }}';
|
||||||
|
|
||||||
|
if (fmtStatus || vetStatus || testStatus) {
|
||||||
|
comment += '\n### 🔧 Backend Checks\n\n';
|
||||||
|
|
||||||
|
if (fmtStatus) {
|
||||||
|
comment += '**Go Formatting:** ' + fmtStatus + '\n';
|
||||||
|
const fmtFiles = `${{ steps.backend.outputs.fmt_files }}`;
|
||||||
|
if (fmtFiles && fmtFiles.trim()) {
|
||||||
|
comment += '<details><summary>Files needing formatting</summary>\n\n```\n' + fmtFiles + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vetStatus) {
|
||||||
|
comment += '**Go Vet:** ' + vetStatus + '\n';
|
||||||
|
const vetOutput = `${{ steps.backend.outputs.vet_output }}`;
|
||||||
|
if (vetOutput && vetOutput.trim()) {
|
||||||
|
comment += '<details><summary>Issues found</summary>\n\n```\n' + vetOutput.substring(0, 1000) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testStatus) {
|
||||||
|
comment += '**Tests:** ' + testStatus + '\n';
|
||||||
|
const testOutput = `${{ steps.backend.outputs.test_output }}`;
|
||||||
|
if (testOutput && testOutput.trim()) {
|
||||||
|
comment += '<details><summary>Test output</summary>\n\n```\n' + testOutput.substring(0, 1000) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n**Fix locally:**\n';
|
||||||
|
comment += '```bash\n';
|
||||||
|
comment += 'go fmt ./... # Format code\n';
|
||||||
|
comment += 'go vet ./... # Check for issues\n';
|
||||||
|
comment += 'go test ./... # Run tests\n';
|
||||||
|
comment += '```\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend checks
|
||||||
|
const lintStatus = '${{ steps.frontend.outputs.lint_status }}';
|
||||||
|
const typecheckStatus = '${{ steps.frontend.outputs.typecheck_status }}';
|
||||||
|
const buildStatus = '${{ steps.frontend.outputs.build_status }}';
|
||||||
|
|
||||||
|
if (lintStatus || typecheckStatus || buildStatus) {
|
||||||
|
comment += '\n### ⚛️ Frontend Checks\n\n';
|
||||||
|
|
||||||
|
if (lintStatus) {
|
||||||
|
comment += '**Linting:** ' + lintStatus + '\n';
|
||||||
|
const lintOutput = `${{ steps.frontend.outputs.lint_output }}`;
|
||||||
|
if (lintOutput && lintOutput.trim()) {
|
||||||
|
comment += '<details><summary>Issues found</summary>\n\n```\n' + lintOutput.substring(0, 500) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typecheckStatus) {
|
||||||
|
comment += '**Type Checking:** ' + typecheckStatus + '\n';
|
||||||
|
const typecheckOutput = `${{ steps.frontend.outputs.typecheck_output }}`;
|
||||||
|
if (typecheckOutput && typecheckOutput.trim()) {
|
||||||
|
comment += '<details><summary>Type errors</summary>\n\n```\n' + typecheckOutput.substring(0, 500) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildStatus) {
|
||||||
|
comment += '**Build:** ' + buildStatus + '\n';
|
||||||
|
const buildOutput = `${{ steps.frontend.outputs.build_output }}`;
|
||||||
|
if (buildOutput && buildOutput.trim()) {
|
||||||
|
comment += '<details><summary>Build output</summary>\n\n```\n' + buildOutput.substring(0, 500) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n**Fix locally:**\n';
|
||||||
|
comment += '```bash\n';
|
||||||
|
comment += 'cd web\n';
|
||||||
|
comment += 'npm run lint -- --fix # Fix linting\n';
|
||||||
|
comment += 'npm run type-check # Check types\n';
|
||||||
|
comment += 'npm run build # Test build\n';
|
||||||
|
comment += '```\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n---\n\n';
|
||||||
|
comment += '### 📖 Resources\n\n';
|
||||||
|
comment += '- [Contributing Guidelines](https://github.com/tinkle-community/nofx/blob/dev/CONTRIBUTING.md)\n';
|
||||||
|
comment += '- [Migration Guide](https://github.com/tinkle-community/nofx/blob/dev/docs/community/MIGRATION_ANNOUNCEMENT.md)\n\n';
|
||||||
|
comment += '**Questions?** Feel free to ask in the comments! 🙏\n\n';
|
||||||
|
comment += '---\n\n';
|
||||||
|
comment += '*These checks are advisory and won\'t block your PR from being merged. This comment is automatically generated from [pr-checks-run.yml](https://github.com/tinkle-community/nofx/blob/dev/.github/workflows/pr-checks-run.yml).*';
|
||||||
|
|
||||||
|
// Post comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: comment
|
||||||
|
});
|
||||||
238
.github/workflows/pr-checks-run.yml
vendored
Normal file
238
.github/workflows/pr-checks-run.yml
vendored
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
name: PR Checks - Run
|
||||||
|
|
||||||
|
# This workflow runs all PR checks with read-only permissions
|
||||||
|
# Safe for fork PRs - results are saved as artifacts
|
||||||
|
# Companion workflow (pr-checks-comment.yml) will post comments
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
branches: [main, dev]
|
||||||
|
|
||||||
|
# Read-only permissions - safe for fork PRs
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-info:
|
||||||
|
name: PR Information
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check PR title format
|
||||||
|
id: check-title
|
||||||
|
run: |
|
||||||
|
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
|
||||||
|
# Check if title follows conventional commits
|
||||||
|
if echo "$PR_TITLE" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|security)(\(.+\))?: .+"; then
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
echo "message=PR title follows Conventional Commits format" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Suggestion" >> $GITHUB_OUTPUT
|
||||||
|
echo "message=Consider using Conventional Commits format: type(scope): description" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Calculate PR size and save results
|
||||||
|
id: pr-size
|
||||||
|
run: |
|
||||||
|
ADDITIONS=${{ github.event.pull_request.additions }}
|
||||||
|
DELETIONS=${{ github.event.pull_request.deletions }}
|
||||||
|
TOTAL=$((ADDITIONS + DELETIONS))
|
||||||
|
|
||||||
|
if [ $TOTAL -lt 100 ]; then
|
||||||
|
SIZE="🟢 Small"
|
||||||
|
SUGGESTION=""
|
||||||
|
elif [ $TOTAL -lt 500 ]; then
|
||||||
|
SIZE="🟡 Medium"
|
||||||
|
SUGGESTION=""
|
||||||
|
else
|
||||||
|
SIZE="🔴 Large"
|
||||||
|
SUGGESTION="Consider breaking this into smaller PRs for easier review"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Save all results to JSON file
|
||||||
|
cat > pr-info.json <<EOF
|
||||||
|
{
|
||||||
|
"pr_number": ${{ github.event.pull_request.number }},
|
||||||
|
"title_status": "${{ steps.check-title.outputs.status }}",
|
||||||
|
"title_message": "${{ steps.check-title.outputs.message }}",
|
||||||
|
"size": "$SIZE",
|
||||||
|
"lines": $TOTAL,
|
||||||
|
"size_suggestion": "$SUGGESTION"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat pr-info.json
|
||||||
|
|
||||||
|
- name: Upload PR info results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pr-info-results
|
||||||
|
path: pr-info.json
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
backend-checks:
|
||||||
|
name: Backend Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libta-lib-dev || true
|
||||||
|
go mod download || true
|
||||||
|
|
||||||
|
- name: Check Go formatting
|
||||||
|
id: go-fmt
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
UNFORMATTED=$(gofmt -l . 2>/dev/null || echo "")
|
||||||
|
if [ -n "$UNFORMATTED" ]; then
|
||||||
|
echo "status=⚠️ Needs formatting" >> $GITHUB_OUTPUT
|
||||||
|
echo "$UNFORMATTED" | head -10 > fmt-files.txt
|
||||||
|
else
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
echo "" > fmt-files.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run go vet
|
||||||
|
id: go-vet
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if go vet ./... 2>&1 | tee vet-output.txt; then
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Issues found" >> $GITHUB_OUTPUT
|
||||||
|
cat vet-output.txt | head -20 > vet-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
id: go-test
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if go test ./... -v 2>&1 | tee test-output.txt; then
|
||||||
|
echo "status=✅ Passed" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Failed" >> $GITHUB_OUTPUT
|
||||||
|
cat test-output.txt | tail -30 > test-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Save backend results
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
cat > backend-results.json <<EOF
|
||||||
|
{
|
||||||
|
"pr_number": ${{ github.event.pull_request.number }},
|
||||||
|
"fmt_status": "${{ steps.go-fmt.outputs.status }}",
|
||||||
|
"vet_status": "${{ steps.go-vet.outputs.status }}",
|
||||||
|
"test_status": "${{ steps.go-test.outputs.status }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload backend results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: backend-results
|
||||||
|
path: |
|
||||||
|
backend-results.json
|
||||||
|
fmt-files.txt
|
||||||
|
vet-output-short.txt
|
||||||
|
test-output-short.txt
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
frontend-checks:
|
||||||
|
name: Frontend Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Check if web directory exists
|
||||||
|
id: check-web
|
||||||
|
run: |
|
||||||
|
if [ -d "web" ]; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
id: lint
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if npm run lint 2>&1 | tee lint-output.txt; then
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Issues found" >> $GITHUB_OUTPUT
|
||||||
|
cat lint-output.txt | head -20 > lint-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
id: typecheck
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if npm run type-check 2>&1 | tee typecheck-output.txt; then
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Issues found" >> $GITHUB_OUTPUT
|
||||||
|
cat typecheck-output.txt | head -20 > typecheck-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
id: build
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if npm run build 2>&1 | tee build-output.txt; then
|
||||||
|
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Failed" >> $GITHUB_OUTPUT
|
||||||
|
cat build-output.txt | tail -20 > build-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Save frontend results
|
||||||
|
if: always() && steps.check-web.outputs.exists == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: |
|
||||||
|
cat > frontend-results.json <<EOF
|
||||||
|
{
|
||||||
|
"pr_number": ${{ github.event.pull_request.number }},
|
||||||
|
"lint_status": "${{ steps.lint.outputs.status }}",
|
||||||
|
"typecheck_status": "${{ steps.typecheck.outputs.status }}",
|
||||||
|
"build_status": "${{ steps.build.outputs.status }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload frontend results
|
||||||
|
if: always() && steps.check-web.outputs.exists == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-results
|
||||||
|
path: |
|
||||||
|
web/frontend-results.json
|
||||||
|
web/lint-output-short.txt
|
||||||
|
web/typecheck-output-short.txt
|
||||||
|
web/build-output-short.txt
|
||||||
|
retention-days: 1
|
||||||
53
.github/workflows/pr-checks.yml
vendored
53
.github/workflows/pr-checks.yml
vendored
@@ -7,11 +7,13 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
- main
|
- main
|
||||||
|
|
||||||
# Default permissions for all jobs (can be overridden per job)
|
# 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:
|
permissions:
|
||||||
contents: read # Read repository contents
|
contents: read # Read repository contents
|
||||||
pull-requests: write # Manage PRs (labels, comments)
|
pull-requests: write # Manage PRs (labels, comments) - only works for non-fork PRs
|
||||||
issues: write # Manage issues (PRs are issues)
|
issues: write # Manage issues (PRs are issues) - only works for non-fork PRs
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Validate PR title and description
|
# Validate PR title and description
|
||||||
@@ -50,6 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check PR size
|
- name: Check PR size
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
continue-on-error: true # Don't fail for fork PRs
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
@@ -57,6 +60,9 @@ jobs:
|
|||||||
const deletions = pr.deletions;
|
const deletions = pr.deletions;
|
||||||
const total = additions + 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 label = '';
|
||||||
let comment = '';
|
let comment = '';
|
||||||
|
|
||||||
@@ -71,22 +77,31 @@ jobs:
|
|||||||
comment = '🚨 This PR is **large** (>' + total + ' lines changed). Please consider breaking it into smaller, focused PRs for easier review.';
|
comment = '🚨 This PR is **large** (>' + total + ' lines changed). Please consider breaking it into smaller, focused PRs for easier review.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add size label
|
// Only add labels/comments for non-fork PRs (fork PRs don't have write permission)
|
||||||
await github.rest.issues.addLabels({
|
if (!isFork) {
|
||||||
owner: context.repo.owner,
|
try {
|
||||||
repo: context.repo.repo,
|
// Add size label
|
||||||
issue_number: pr.number,
|
await github.rest.issues.addLabels({
|
||||||
labels: [label]
|
owner: context.repo.owner,
|
||||||
});
|
repo: context.repo.repo,
|
||||||
|
issue_number: pr.number,
|
||||||
|
labels: [label]
|
||||||
|
});
|
||||||
|
|
||||||
// Add comment for large PRs
|
// Add comment for large PRs
|
||||||
if (total >= 1000) {
|
if (total >= 1000) {
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: pr.number,
|
issue_number: pr.number,
|
||||||
body: comment
|
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 tests
|
# Backend tests
|
||||||
@@ -186,6 +201,8 @@ jobs:
|
|||||||
auto-label:
|
auto-label:
|
||||||
name: Auto Label PR
|
name: Auto Label PR
|
||||||
runs-on: ubuntu-latest
|
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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|||||||
Reference in New Issue
Block a user