mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 11:34:00 +00:00
feat: add GitHub Actions workflows for code quality and automation (#2)
This commit is contained in:
260
.github/workflows/action-security.yml
vendored
Normal file
260
.github/workflows/action-security.yml
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Action Security
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**/action.yml'
|
||||
- '**/action.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/action.yml'
|
||||
- '**/action.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze Action Security
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
pull-requests: read
|
||||
statuses: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check Required Configurations
|
||||
id: check-configs
|
||||
shell: bash
|
||||
run: |
|
||||
# Initialize all flags as false
|
||||
{
|
||||
echo "run_gitleaks=false"
|
||||
echo "run_trivy=true"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check Gitleaks configuration and license
|
||||
if [ -f ".gitleaks.toml" ] && [ -n "${{ secrets.GITLEAKS_LICENSE }}" ]; then
|
||||
echo "Gitleaks config and license found"
|
||||
echo "run_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Gitleaks config or license missing - skipping Gitleaks scan"
|
||||
fi
|
||||
|
||||
- name: Run actionlint
|
||||
uses: raven-actions/actionlint@v2
|
||||
with:
|
||||
cache: true
|
||||
fail-on-error: true
|
||||
shellcheck: false
|
||||
|
||||
- name: Run Gitleaks
|
||||
if: steps.check-configs.outputs.run_gitleaks == 'true'
|
||||
uses: gitleaks/gitleaks-action@v2.3.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||
with:
|
||||
config-path: .gitleaks.toml
|
||||
report-format: sarif
|
||||
report-path: gitleaks-report.sarif
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
security-checks: 'vuln,config,secret'
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
timeout: '10m'
|
||||
|
||||
- name: Verify SARIF files
|
||||
id: verify-sarif
|
||||
shell: bash
|
||||
run: |
|
||||
# Initialize outputs
|
||||
{
|
||||
echo "has_trivy=false"
|
||||
echo "has_gitleaks=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check Trivy results
|
||||
if [ -f "trivy-results.sarif" ]; then
|
||||
if jq -e . </dev/null 2>&1 <"trivy-results.sarif"; then
|
||||
echo "has_trivy=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Trivy SARIF file exists but is not valid JSON"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Gitleaks results if it ran
|
||||
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
|
||||
if [ -f "gitleaks-report.sarif" ]; then
|
||||
if jq -e . </dev/null 2>&1 <"gitleaks-report.sarif"; then
|
||||
echo "has_gitleaks=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::Gitleaks SARIF file exists but is not valid JSON"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload Trivy results
|
||||
if: steps.verify-sarif.outputs.has_trivy == 'true'
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
category: 'trivy'
|
||||
|
||||
- name: Upload Gitleaks results
|
||||
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: 'gitleaks-report.sarif'
|
||||
category: 'gitleaks'
|
||||
|
||||
- name: Archive security reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: security-reports-${{ github.run_id }}
|
||||
path: |
|
||||
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
|
||||
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Analyze Results
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
try {
|
||||
let totalIssues = 0;
|
||||
let criticalIssues = 0;
|
||||
|
||||
const analyzeSarif = (file, tool) => {
|
||||
if (!fs.existsSync(file)) {
|
||||
console.log(`No results file found for ${tool}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
return sarif.runs.reduce((acc, run) => {
|
||||
if (!run.results) return acc;
|
||||
|
||||
const critical = run.results.filter(r =>
|
||||
r.level === 'error' ||
|
||||
r.level === 'critical' ||
|
||||
(r.ruleId || '').toLowerCase().includes('critical')
|
||||
).length;
|
||||
|
||||
return {
|
||||
total: acc.total + run.results.length,
|
||||
critical: acc.critical + critical
|
||||
};
|
||||
}, { total: 0, critical: 0 });
|
||||
} catch (error) {
|
||||
console.log(`Error analyzing ${tool} results: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Only analyze results from tools that ran successfully
|
||||
const results = {
|
||||
trivy: ${{ steps.verify-sarif.outputs.has_trivy }} ?
|
||||
analyzeSarif('trivy-results.sarif', 'trivy') : null,
|
||||
gitleaks: ${{ steps.verify-sarif.outputs.has_gitleaks }} ?
|
||||
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
|
||||
};
|
||||
|
||||
// Aggregate results
|
||||
Object.entries(results).forEach(([tool, result]) => {
|
||||
if (result) {
|
||||
totalIssues += result.total;
|
||||
criticalIssues += result.critical;
|
||||
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create summary
|
||||
const summary = `## Security Scan Summary
|
||||
|
||||
- Total Issues Found: ${totalIssues}
|
||||
- Critical Issues: ${criticalIssues}
|
||||
|
||||
### Tool Breakdown
|
||||
${Object.entries(results)
|
||||
.filter(([_, r]) => r)
|
||||
.map(([tool, r]) =>
|
||||
`- ${tool}: ${r.total} total, ${r.critical} critical`
|
||||
).join('\n')}
|
||||
|
||||
### Tools Run Status
|
||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
|
||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
|
||||
`;
|
||||
|
||||
// Set output
|
||||
core.setOutput('total_issues', totalIssues);
|
||||
core.setOutput('critical_issues', criticalIssues);
|
||||
|
||||
// Add job summary
|
||||
await core.summary
|
||||
.addRaw(summary)
|
||||
.write();
|
||||
|
||||
// Fail if critical issues found
|
||||
if (criticalIssues > 0) {
|
||||
core.setFailed(`Found ${criticalIssues} critical security issues`);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`Analysis failed: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Notify on Critical Issues
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const critical = core.getInput('critical_issues');
|
||||
|
||||
const body = `🚨 Critical security issues found in GitHub Actions
|
||||
|
||||
${critical} critical security issues were found during the security scan.
|
||||
|
||||
### Scan Results
|
||||
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'Completed' || 'Skipped/Failed' }}
|
||||
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks == 'true' && 'Completed' || 'Skipped' }}
|
||||
|
||||
[View detailed scan results](https://github.com/${owner}/${repo}/actions/runs/${context.runId})
|
||||
|
||||
Please address these issues immediately.
|
||||
|
||||
> Note: Some security tools might have been skipped due to missing configurations.
|
||||
> Check the workflow run for details.`;
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title: '🚨 Critical Security Issues in Actions',
|
||||
body,
|
||||
labels: ['security', 'critical', 'actions'],
|
||||
assignees: ['ivuorinen']
|
||||
});
|
||||
231
.github/workflows/auto-approve.yml
vendored
Normal file
231
.github/workflows/auto-approve.yml
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Auto Approve
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-approve:
|
||||
name: 👍 Auto Approve
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check Required Secrets
|
||||
id: check-secrets
|
||||
run: |
|
||||
if [ -z "${{ secrets.APP_ID }}" ] || [ -z "${{ secrets.APP_PRIVATE_KEY }}" ]; then
|
||||
echo "::warning::GitHub App credentials not configured. Using GITHUB_TOKEN with limited functionality."
|
||||
echo "use_github_token=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "use_github_token=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Generate Token
|
||||
id: generate-token
|
||||
if: steps.check-secrets.outputs.use_github_token == 'false'
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add Initial Status Comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
const token_type = '${{ steps.check-secrets.outputs.use_github_token }}' === 'true'
|
||||
? 'GITHUB_TOKEN (limited functionality)'
|
||||
: 'GitHub App token';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: `⏳ Checking PR eligibility for auto-approval using ${token_type}...`
|
||||
});
|
||||
|
||||
- name: Check PR Eligibility
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Configuration for trusted conditions
|
||||
const trustedAuthors = ['dependabot[bot]', 'renovate[bot]', 'fiximus'];
|
||||
const trustedLabels = ['dependencies', 'automated-pr'];
|
||||
const excludedLabels = ['do-not-merge', 'work-in-progress'];
|
||||
const trustedPaths = ['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
|
||||
|
||||
// Results object to store all check results
|
||||
const results = {
|
||||
isTrustedAuthor: false,
|
||||
hasRequiredLabel: false,
|
||||
hasExcludedLabel: false,
|
||||
onlyTrustedFiles: false,
|
||||
limitedPermissions: '${{ steps.check-secrets.outputs.use_github_token }}' === 'true'
|
||||
};
|
||||
|
||||
// Check author
|
||||
results.isTrustedAuthor = trustedAuthors.includes(pr.user.login);
|
||||
|
||||
// Check labels
|
||||
results.hasRequiredLabel = pr.labels.some(label =>
|
||||
trustedLabels.includes(label.name)
|
||||
);
|
||||
|
||||
results.hasExcludedLabel = pr.labels.some(label =>
|
||||
excludedLabels.includes(label.name)
|
||||
);
|
||||
|
||||
try {
|
||||
// Get changed files
|
||||
const files = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
|
||||
// Check if only trusted paths are modified
|
||||
results.onlyTrustedFiles = files.data.every(file =>
|
||||
trustedPaths.some(path => file.filename.includes(path))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error checking files:', error);
|
||||
results.onlyTrustedFiles = false;
|
||||
}
|
||||
|
||||
// Store detailed results for the next step
|
||||
core.setOutput('results', JSON.stringify(results));
|
||||
|
||||
// Set final approval decision
|
||||
const shouldApprove = results.isTrustedAuthor &&
|
||||
results.hasRequiredLabel &&
|
||||
!results.hasExcludedLabel &&
|
||||
results.onlyTrustedFiles;
|
||||
|
||||
core.setOutput('should_approve', shouldApprove);
|
||||
|
||||
// Log results
|
||||
console.log('Eligibility check results:', results);
|
||||
|
||||
- name: Process Auto Approval
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Parse check results
|
||||
const results = JSON.parse('${{ steps.check.outputs.results }}');
|
||||
const shouldApprove = '${{ steps.check.outputs.should_approve }}' === 'true';
|
||||
|
||||
// Create status report
|
||||
let statusReport = `## 🔍 Auto Approval Check Results\n\n`;
|
||||
|
||||
if (results.limitedPermissions) {
|
||||
statusReport += `⚠️ **Note:** Running with limited permissions (GITHUB_TOKEN)\n\n`;
|
||||
}
|
||||
|
||||
statusReport += `### Checks\n`;
|
||||
statusReport += `- Trusted Author: ${results.isTrustedAuthor ? '✅' : '❌'}\n`;
|
||||
statusReport += `- Required Labels: ${results.hasRequiredLabel ? '✅' : '❌'}\n`;
|
||||
statusReport += `- Excluded Labels: ${!results.hasExcludedLabel ? '✅' : '❌'}\n`;
|
||||
statusReport += `- Trusted Files Only: ${results.onlyTrustedFiles ? '✅' : '❌'}\n\n`;
|
||||
|
||||
if (shouldApprove) {
|
||||
statusReport += `### ✅ Result: Auto-approved\n`;
|
||||
|
||||
try {
|
||||
// Create approval review
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
event: 'APPROVE',
|
||||
body: 'Automatically approved based on trusted conditions.'
|
||||
});
|
||||
|
||||
// Add auto-merge label
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: ['auto-merge']
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during approval:', error);
|
||||
statusReport += `\n⚠️ Error during approval process: ${error.message}\n`;
|
||||
}
|
||||
} else {
|
||||
statusReport += `### ❌ Result: Not eligible for auto-approval\n`;
|
||||
|
||||
if (results.limitedPermissions) {
|
||||
statusReport += `\n⚠️ Note: Some functionality may be limited due to running with GITHUB_TOKEN.\n`;
|
||||
statusReport += `Configure APP_ID and APP_PRIVATE_KEY for full functionality.\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add final status comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: statusReport
|
||||
});
|
||||
|
||||
- name: Handle Errors
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
const isLimitedPermissions = '${{ steps.check-secrets.outputs.use_github_token }}' === 'true';
|
||||
|
||||
const errorMessage = `## ❌ Auto Approval Error
|
||||
|
||||
The auto-approval process encountered an error.
|
||||
|
||||
### Troubleshooting
|
||||
- Check the [workflow logs](${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID})
|
||||
- Verify repository permissions
|
||||
- Ensure all required configurations are present
|
||||
|
||||
${isLimitedPermissions ? '⚠️ Note: Running with limited permissions (GITHUB_TOKEN)' : ''}
|
||||
`;
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: errorMessage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create error comment:', error);
|
||||
core.setFailed(`Failed to create error comment: ${error.message}`);
|
||||
}
|
||||
175
.github/workflows/auto-merge.yml
vendored
Normal file
175
.github/workflows/auto-merge.yml
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Auto Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false # Don't cancel as this could leave PRs in inconsistent state
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
name: 🤝 Auto Merge
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
statuses: read
|
||||
|
||||
steps:
|
||||
- name: Check Required Secrets
|
||||
id: check-secrets
|
||||
run: |
|
||||
# shellcheck disable=SC2016
|
||||
if [ -z "${{ secrets.APP_ID }}" ] || [ -z "${{ secrets.APP_PRIVATE_KEY }}" ]; then
|
||||
echo "::warning::GitHub App credentials not configured. Using GITHUB_TOKEN instead."
|
||||
echo "use_github_token=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "use_github_token=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Generate Token
|
||||
id: generate-token
|
||||
if: steps.check-secrets.outputs.use_github_token == 'false'
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Merge PR
|
||||
uses: pascalgn/automerge-action@v0.15.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
MERGE_LABELS: 'dependencies,automated-pr,!work-in-progress,!do-not-merge'
|
||||
MERGE_METHOD: 'squash'
|
||||
MERGE_COMMIT_MESSAGE: 'pull-request-title'
|
||||
MERGE_RETRIES: '6'
|
||||
MERGE_RETRY_SLEEP: '10000'
|
||||
MERGE_REQUIRED_APPROVALS: '0'
|
||||
MERGE_DELETE_BRANCH: 'true'
|
||||
UPDATE_LABELS: 'automerge'
|
||||
UPDATE_METHOD: 'rebase'
|
||||
MERGE_ERROR_FAIL: 'false'
|
||||
|
||||
- name: Check Merge Status
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
if (!pr) return;
|
||||
|
||||
try {
|
||||
const status = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
|
||||
if (status.data.merged) {
|
||||
console.log(`PR #${pr.number} was successfully merged`);
|
||||
|
||||
// Add merge success comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: '✅ Successfully auto-merged! Branch will be deleted.'
|
||||
});
|
||||
} else {
|
||||
console.log(`PR #${pr.number} is not merged. State: ${status.data.state}`);
|
||||
|
||||
// Check merge blockers
|
||||
if (status.data.mergeable_state === 'blocked') {
|
||||
console.log('PR is blocked from merging. Check branch protection rules.');
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: '⚠️ Auto-merge is blocked. Please check branch protection rules and resolve any conflicts.'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if using reduced permissions
|
||||
if ('${{ steps.check-secrets.outputs.use_github_token }}' === 'true') {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: '⚠️ Note: Running with reduced permissions as GitHub App credentials are not configured.'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking merge status:', error);
|
||||
core.setFailed(`Failed to check merge status: ${error.message}`);
|
||||
|
||||
// Add error comment to PR
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: `❌ Error checking merge status: ${error.message}`
|
||||
});
|
||||
} catch (commentError) {
|
||||
console.error('Failed to add error comment:', commentError);
|
||||
}
|
||||
}
|
||||
|
||||
- name: Remove Labels on Failure
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
if (!pr) return;
|
||||
|
||||
try {
|
||||
// Remove automerge label
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
name: 'automerge'
|
||||
}).catch(e => console.log('automerge label not found'));
|
||||
|
||||
// Add merge-failed label
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
labels: ['merge-failed']
|
||||
});
|
||||
|
||||
// Add failure comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: '❌ Auto-merge failed. The automerge label has been removed and merge-failed label added.'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error handling merge failure:', error);
|
||||
}
|
||||
183
.github/workflows/auto-rebase.yml
vendored
Normal file
183
.github/workflows/auto-rebase.yml
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Auto Rebase
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
auto-rebase:
|
||||
name: 🔄 Auto Rebase
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/rebase')) ||
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'auto-rebase')) ||
|
||||
github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Check Required Secrets
|
||||
id: check-secrets
|
||||
run: |
|
||||
if [ -z "${{ secrets.APP_ID }}" ] || [ -z "${{ secrets.APP_PRIVATE_KEY }}" ]; then
|
||||
echo "::warning::GitHub App credentials not configured. Using GITHUB_TOKEN instead."
|
||||
echo "use_github_token=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "${{ github.event_name }}" == "push" ]; then
|
||||
echo "::warning::Running with GITHUB_TOKEN on push events may have limited functionality."
|
||||
fi
|
||||
else
|
||||
echo "use_github_token=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Generate Token
|
||||
id: generate-token
|
||||
if: steps.check-secrets.outputs.use_github_token == 'false'
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add Initial Comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
|
||||
// Get PR number based on event type
|
||||
let prNumber;
|
||||
if (context.eventName === 'issue_comment') {
|
||||
prNumber = context.payload.issue.number;
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
prNumber = context.payload.pull_request.number;
|
||||
}
|
||||
|
||||
if (prNumber) {
|
||||
const token_type = '${{ steps.check-secrets.outputs.use_github_token }}' === 'true'
|
||||
? 'GITHUB_TOKEN (limited permissions)'
|
||||
: 'GitHub App token';
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: `🔄 Attempting rebase using ${token_type}...`
|
||||
});
|
||||
}
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Auto Rebase
|
||||
id: rebase
|
||||
continue-on-error: true
|
||||
uses: cirrus-actions/rebase@1.8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Handle Rebase Result
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.check-secrets.outputs.use_github_token == 'true' && github.token || steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
|
||||
// Get PR number based on event type
|
||||
let prNumber;
|
||||
if (context.eventName === 'issue_comment') {
|
||||
prNumber = context.payload.issue.number;
|
||||
} else if (context.eventName === 'pull_request_target') {
|
||||
prNumber = context.payload.pull_request.number;
|
||||
}
|
||||
|
||||
if (prNumber) {
|
||||
const rebaseSuccess = '${{ steps.rebase.outcome }}' === 'success';
|
||||
const usingGithubToken = '${{ steps.check-secrets.outputs.use_github_token }}' === 'true';
|
||||
|
||||
let commentBody;
|
||||
if (rebaseSuccess) {
|
||||
commentBody = '✅ Rebase completed successfully!';
|
||||
} else {
|
||||
commentBody = '❌ Rebase failed.\n\n';
|
||||
|
||||
if (usingGithubToken) {
|
||||
commentBody += '⚠️ Note: This workflow is running with reduced permissions (GITHUB_TOKEN).\n' +
|
||||
'For better functionality, configure APP_ID and APP_PRIVATE_KEY secrets.\n\n';
|
||||
}
|
||||
|
||||
commentBody += 'Please try to:\n' +
|
||||
'1. Resolve any conflicts manually\n' +
|
||||
'2. Ensure branch is not protected\n' +
|
||||
'3. Verify you have proper permissions';
|
||||
}
|
||||
|
||||
// Add result comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
// Handle labels
|
||||
try {
|
||||
// Remove auto-rebase label if it exists
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: 'auto-rebase'
|
||||
}).catch(e => console.log('auto-rebase label not found'));
|
||||
|
||||
// Add appropriate result label
|
||||
const resultLabel = rebaseSuccess ? 'rebase-succeeded' : 'rebase-failed';
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: [resultLabel]
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error handling labels:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set action status based on rebase result
|
||||
if ('${{ steps.rebase.outcome }}' !== 'success') {
|
||||
core.setFailed('Rebase failed');
|
||||
}
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
# Reset any pending changes
|
||||
git reset --hard
|
||||
git checkout main
|
||||
|
||||
# Clean up any temporary branches
|
||||
git fetch --prune
|
||||
43
.github/workflows/codeql.yml
vendored
Normal file
43
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
schedule:
|
||||
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript'] # Add languages used in your actions
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
14
.github/workflows/dependency-review.yml
vendored
Normal file
14
.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4.5.0
|
||||
211
.github/workflows/pr-lint.yml
vendored
211
.github/workflows/pr-lint.yml
vendored
@@ -1,17 +1,208 @@
|
||||
---
|
||||
name: PR Lint
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: MegaLinter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore: [master, main]
|
||||
pull_request:
|
||||
branches: [master, main]
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.github/*.md'
|
||||
- 'LICENSE'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
statuses: write
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.github/*.md'
|
||||
- 'LICENSE'
|
||||
|
||||
env:
|
||||
# Apply linter fixes configuration
|
||||
APPLY_FIXES: all
|
||||
APPLY_FIXES_EVENT: pull_request
|
||||
APPLY_FIXES_MODE: commit
|
||||
|
||||
# Disable linters that do not work or conflict
|
||||
DISABLE_LINTERS: REPOSITORY_DEVSKIM
|
||||
|
||||
# Additional settings
|
||||
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
GITHUB_TOKEN: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Report configuration
|
||||
REPORT_OUTPUT_FOLDER: megalinter-reports
|
||||
ENABLE_SUMMARY_REPORTER: true
|
||||
ENABLE_SARIF_REPORTER: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
SuperLinter:
|
||||
uses: ivuorinen/.github/.github/workflows/pr-lint.yml@main
|
||||
megalinter:
|
||||
name: MegaLinter
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: MegaLinter
|
||||
id: ml
|
||||
uses: oxsecurity/megalinter/flavors/cupcake@v8.4.0
|
||||
env:
|
||||
PARALLEL: true # Run linters in parallel
|
||||
FILTER_REGEX_EXCLUDE: '(\.automation/test|docs/json-schemas|\.github/workflows)'
|
||||
|
||||
# Error configuration
|
||||
ERROR_ON_MISSING_EXEC_BIT: true
|
||||
CLEAR_REPORT_FOLDER: true
|
||||
PRINT_ALPACA: false
|
||||
SHOW_ELAPSED_TIME: true
|
||||
|
||||
# File configuration
|
||||
YAML_YAMLLINT_CONFIG_FILE: .yamllint.yml
|
||||
YAML_PRETTIER_CONFIG_FILE: .prettierrc.yml
|
||||
YAML_YAMLLINT_FILTER_REGEX_EXCLUDE: '(\.automation/test|docs/json-schemas|\.github/workflows)'
|
||||
|
||||
- name: Check MegaLinter Results
|
||||
id: check-results
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ -f "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log" ]; then
|
||||
if grep -q "ERROR\|CRITICAL" "${{ env.REPORT_OUTPUT_FOLDER }}/megalinter.log"; then
|
||||
echo "Linting errors found"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "::warning::MegaLinter log file not found"
|
||||
fi
|
||||
|
||||
- name: Upload Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MegaLinter reports
|
||||
path: |
|
||||
megalinter-reports
|
||||
mega-linter.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload SARIF Report
|
||||
if: always() && hashFiles('megalinter-reports/sarif/*.sarif')
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: megalinter-reports/sarif
|
||||
category: megalinter
|
||||
|
||||
- name: Prepare Git for Fixes
|
||||
if: steps.ml.outputs.has_updated_sources == 1
|
||||
shell: bash
|
||||
run: |
|
||||
sudo chown -Rc $UID .git/
|
||||
git config --global user.name "fiximus"
|
||||
git config --global user.email "github-bot@ivuorinen.net"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: |
|
||||
steps.ml.outputs.has_updated_sources == 1 &&
|
||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
||||
env.APPLY_FIXES_MODE == 'pull_request' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
!contains(github.event.head_commit.message, 'skip fix')
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
commit-message: '[MegaLinter] Apply linters automatic fixes'
|
||||
title: '[MegaLinter] Apply linters automatic fixes'
|
||||
labels: bot
|
||||
branch: megalinter/fixes-${{ github.ref_name }}
|
||||
branch-suffix: timestamp
|
||||
delete-branch: true
|
||||
body: |
|
||||
## MegaLinter Fixes
|
||||
|
||||
MegaLinter has identified and fixed code style issues.
|
||||
|
||||
### 🔍 Changes Made
|
||||
- Automated code style fixes
|
||||
- Formatting improvements
|
||||
- Lint error corrections
|
||||
|
||||
### 📝 Notes
|
||||
- Please review the changes carefully
|
||||
- Run tests before merging
|
||||
- Verify formatting matches project standards
|
||||
|
||||
> Generated automatically by MegaLinter
|
||||
|
||||
- name: Commit Fixes
|
||||
if: |
|
||||
steps.ml.outputs.has_updated_sources == 1 &&
|
||||
(env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) &&
|
||||
env.APPLY_FIXES_MODE == 'commit' &&
|
||||
github.ref != 'refs/heads/main' &&
|
||||
(github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
!contains(github.event.head_commit.message, 'skip fix')
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
|
||||
commit_message: |
|
||||
style: apply MegaLinter fixes
|
||||
|
||||
[skip ci]
|
||||
commit_user_name: fiximus
|
||||
commit_user_email: github-bot@ivuorinen.net
|
||||
push_options: --force
|
||||
|
||||
- name: Create Status Check
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const status = '${{ steps.check-results.outputs.status }}';
|
||||
const conclusion = status === 'success' ? 'success' : 'failure';
|
||||
|
||||
const summary = `## MegaLinter Results
|
||||
|
||||
${status === 'success' ? '✅ All checks passed' : '❌ Issues found'}
|
||||
|
||||
[View detailed report](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})
|
||||
`;
|
||||
|
||||
await core.summary
|
||||
.addRaw(summary)
|
||||
.write();
|
||||
|
||||
if (status !== 'success') {
|
||||
core.setFailed('MegaLinter found issues');
|
||||
}
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
# Remove temporary files but keep reports
|
||||
find . -type f -name "megalinter.*" ! -name "megalinter-reports" -delete || true
|
||||
find . -type d -name ".megalinter" -exec rm -rf {} + || true
|
||||
|
||||
14
.github/workflows/release-drafter.yml
vendored
14
.github/workflows/release-drafter.yml
vendored
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Release Drafter
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
Draft:
|
||||
uses: ivuorinen/.github/.github/workflows/sync-labels.yml@main
|
||||
18
.github/workflows/release.yml
vendored
Normal file
18
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: softprops/action-gh-release@v2.2.1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
41
.github/workflows/scorecard.yml
vendored
Normal file
41
.github/workflows/scorecard.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Scorecard
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * 0'
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@v2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
174
.github/workflows/security-metrics.yml
vendored
Normal file
174
.github/workflows/security-metrics.yml
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Security Metrics Collection
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Security Checks']
|
||||
types:
|
||||
- completed
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly
|
||||
|
||||
jobs:
|
||||
collect-metrics:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Collect Metrics
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const metrics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
weekly: {
|
||||
scans: 0,
|
||||
vulnerabilities: {
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
},
|
||||
fixes: {
|
||||
submitted: 0,
|
||||
merged: 0
|
||||
},
|
||||
meanTimeToFix: null // Initialize as null instead of 0
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Collect scan metrics
|
||||
const scans = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'security.yml',
|
||||
created: `>${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()}`
|
||||
});
|
||||
|
||||
metrics.weekly.scans = scans.data.total_count;
|
||||
|
||||
// Collect vulnerability metrics
|
||||
const vulnIssues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: 'security',
|
||||
state: 'all',
|
||||
since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
});
|
||||
|
||||
// Calculate vulnerability metrics
|
||||
vulnIssues.data.forEach(issue => {
|
||||
if (issue.labels.find(l => l.name === 'critical')) metrics.weekly.vulnerabilities.critical++;
|
||||
if (issue.labels.find(l => l.name === 'high')) metrics.weekly.vulnerabilities.high++;
|
||||
if (issue.labels.find(l => l.name === 'medium')) metrics.weekly.vulnerabilities.medium++;
|
||||
if (issue.labels.find(l => l.name === 'low')) metrics.weekly.vulnerabilities.low++;
|
||||
});
|
||||
|
||||
// Calculate fix metrics
|
||||
const fixPRs = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'all',
|
||||
labels: 'security-fix'
|
||||
});
|
||||
|
||||
metrics.weekly.fixes.submitted = fixPRs.data.length;
|
||||
const mergedPRs = fixPRs.data.filter(pr => pr.merged);
|
||||
metrics.weekly.fixes.merged = mergedPRs.length;
|
||||
|
||||
// Calculate mean time to fix only if there are merged PRs
|
||||
if (mergedPRs.length > 0) {
|
||||
const fixTimes = mergedPRs.map(pr => {
|
||||
const mergedAt = new Date(pr.merged_at);
|
||||
const createdAt = new Date(pr.created_at);
|
||||
return mergedAt - createdAt;
|
||||
});
|
||||
|
||||
const totalTime = fixTimes.reduce((a, b) => a + b, 0);
|
||||
// Convert to hours and round to 2 decimal places
|
||||
metrics.weekly.meanTimeToFix = Number((totalTime / (fixTimes.length * 3600000)).toFixed(2));
|
||||
}
|
||||
|
||||
// Save metrics
|
||||
const fs = require('fs');
|
||||
fs.writeFileSync('security-metrics.json', JSON.stringify(metrics, null, 2));
|
||||
|
||||
// Generate report
|
||||
const report = generateMetricsReport(metrics);
|
||||
|
||||
// Create/update metrics dashboard
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: '📊 Weekly Security Metrics Report',
|
||||
body: generateReport(metrics),
|
||||
labels: ['metrics', 'security']
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to collect metrics: ${error.message}`);
|
||||
}
|
||||
|
||||
function generateReport(metrics) {
|
||||
const formatDuration = (hours) => {
|
||||
if (hours === null) return 'N/A';
|
||||
return `${hours} hours`;
|
||||
};
|
||||
|
||||
return `## 📊 Weekly Security Metrics Report
|
||||
|
||||
### Timeline
|
||||
- Report Generated: ${new Date().toISOString()}
|
||||
- Period: Last 7 days
|
||||
|
||||
### Security Scans
|
||||
- Total Scans Run: ${metrics.weekly.scans}
|
||||
|
||||
### Vulnerabilities
|
||||
- Critical: ${metrics.weekly.vulnerabilities.critical}
|
||||
- High: ${metrics.weekly.vulnerabilities.high}
|
||||
- Medium: ${metrics.weekly.vulnerabilities.medium}
|
||||
- Low: ${metrics.weekly.vulnerabilities.low}
|
||||
|
||||
### Fixes
|
||||
- PRs Submitted: ${metrics.weekly.fixes.submitted}
|
||||
- PRs Merged: ${metrics.weekly.fixes.merged}
|
||||
- Mean Time to Fix: ${formatDuration(metrics.weekly.meanTimeToFix)}
|
||||
|
||||
### Summary
|
||||
${generateSummary(metrics)}
|
||||
|
||||
> This report was automatically generated by the security metrics workflow.`;
|
||||
}
|
||||
|
||||
function generateSummary(metrics) {
|
||||
const total = Object.values(metrics.weekly.vulnerabilities).reduce((a, b) => a + b, 0);
|
||||
const fixRate = metrics.weekly.fixes.merged / metrics.weekly.fixes.submitted || 0;
|
||||
|
||||
let summary = [];
|
||||
|
||||
if (total === 0) {
|
||||
summary.push('✅ No vulnerabilities detected this week.');
|
||||
} else {
|
||||
summary.push(`⚠️ Detected ${total} total vulnerabilities.`);
|
||||
if (metrics.weekly.vulnerabilities.critical > 0) {
|
||||
summary.push(`🚨 ${metrics.weekly.vulnerabilities.critical} critical vulnerabilities require immediate attention!`);
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.weekly.fixes.submitted > 0) {
|
||||
summary.push(`🔧 Fix rate: ${(fixRate * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (metrics.weekly.meanTimeToFix !== null) {
|
||||
summary.push(`⏱️ Average time to fix: ${metrics.weekly.meanTimeToFix} hours`);
|
||||
}
|
||||
|
||||
return summary.join('\n');
|
||||
}
|
||||
134
.github/workflows/security-trends.yml
vendored
Normal file
134
.github/workflows/security-trends.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Security Trends Analysis
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Security Checks']
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
analyze-trends:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download latest results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: security-reports-${{ github.event.workflow_run.id }}
|
||||
path: latest-results
|
||||
|
||||
- name: Analyze Trends
|
||||
id: analyze
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
// ... [previous code remains the same until report generation]
|
||||
|
||||
// Generate trend report
|
||||
const report = generateTrendReport(trends);
|
||||
|
||||
// Save report explicitly for next step
|
||||
console.log('Writing trend report to file...');
|
||||
fs.writeFileSync('trend-report.md', report);
|
||||
console.log('Trend report saved successfully');
|
||||
|
||||
// Save history
|
||||
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
||||
|
||||
// Generate and save chart
|
||||
const chartData = generateChartData(history);
|
||||
fs.writeFileSync('security-trends.svg', chartData);
|
||||
|
||||
// Set outputs for other steps
|
||||
core.setOutput('has_vulnerabilities',
|
||||
trends.critical.current > 0 || trends.high.current > 0);
|
||||
core.setOutput('trend_status',
|
||||
trends.critical.trend > 0 || trends.high.trend > 0 ? 'worsening' : 'improving');
|
||||
core.setOutput('report_path', 'trend-report.md');
|
||||
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to analyze trends: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
- name: Verify Report File
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -f "trend-report.md" ]; then
|
||||
echo "::error::Trend report file not found"
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
else
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "size=$(stat -f%z trend-report.md)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create Trend Report Issue
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
steps.verify.outputs.exists == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const reportPath = 'trend-report.md';
|
||||
|
||||
console.log('Reading trend report from:', reportPath);
|
||||
console.log('File size:', '${{ steps.verify.outputs.size }}', 'bytes');
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
throw new Error('Trend report file not found despite verification');
|
||||
}
|
||||
|
||||
const report = fs.readFileSync(reportPath, 'utf8');
|
||||
if (!report.trim()) {
|
||||
throw new Error('Trend report file is empty');
|
||||
}
|
||||
|
||||
const hasVulnerabilities = '${{ steps.analyze.outputs.has_vulnerabilities }}' === 'true';
|
||||
const trendStatus = '${{ steps.analyze.outputs.trend_status }}';
|
||||
|
||||
const title = `📊 Security Trend Report - ${
|
||||
hasVulnerabilities ?
|
||||
`⚠️ Vulnerabilities ${trendStatus}` :
|
||||
'✅ No vulnerabilities'
|
||||
}`;
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: title,
|
||||
body: report,
|
||||
labels: ['security', 'metrics', hasVulnerabilities ? 'attention-required' : 'healthy']
|
||||
});
|
||||
|
||||
console.log('Successfully created trend report issue');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create trend report issue:', error);
|
||||
core.setFailed(`Failed to create trend report issue: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
# Remove temporary files but keep the history
|
||||
rm -f trend-report.md security-trends.svg
|
||||
echo "Cleaned up temporary files"
|
||||
487
.github/workflows/security.yml
vendored
Normal file
487
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,487 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Security Checks
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run daily at midnight
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/package.json'
|
||||
- '**/package-lock.json'
|
||||
- '**/yarn.lock'
|
||||
- '**/pnpm-lock.yaml'
|
||||
- '**/requirements.txt'
|
||||
- '**/Dockerfile'
|
||||
- '**/*.py'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
name: Security Analysis
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
issues: write
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check Required Secrets
|
||||
id: check-secrets
|
||||
shell: bash
|
||||
run: |
|
||||
# Initialize flags
|
||||
{
|
||||
echo "run_snyk=false"
|
||||
echo "run_slack=false"
|
||||
echo "run_sonarcloud=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check secrets
|
||||
if [ -n "${{ secrets.SNYK_TOKEN }}" ]; then
|
||||
echo "run_snyk=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Snyk token available"
|
||||
else
|
||||
echo "::warning::SNYK_TOKEN not set - Snyk scans will be skipped"
|
||||
fi
|
||||
|
||||
if [ -n "${{ secrets.SLACK_WEBHOOK }}" ]; then
|
||||
echo "run_slack=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Slack webhook available"
|
||||
else
|
||||
echo "::warning::SLACK_WEBHOOK not set - Slack notifications will be skipped"
|
||||
fi
|
||||
|
||||
if [ -n "${{ secrets.SONAR_TOKEN }}" ]; then
|
||||
echo "run_sonarcloud=true" >> "$GITHUB_OUTPUT"
|
||||
echo "SonarCloud token available"
|
||||
else
|
||||
echo "::warning::SONAR_TOKEN not set - SonarCloud analysis will be skipped"
|
||||
fi
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history for better analysis
|
||||
|
||||
- name: Run OWASP Dependency Check
|
||||
uses: dependency-check/Dependency-Check_Action@main
|
||||
with:
|
||||
project: 'GitHub Actions'
|
||||
path: '.'
|
||||
format: 'SARIF'
|
||||
out: 'reports'
|
||||
args: >
|
||||
--enableRetired
|
||||
--enableExperimental
|
||||
--failOnCVSS 7
|
||||
--suppress ${{ github.workspace }}/suppressions.xml
|
||||
|
||||
- name: Upload OWASP Results
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: reports/dependency-check-report.sarif
|
||||
category: owasp-dependency-check
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Run Snyk Scan
|
||||
id: snyk
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # Don't fail the workflow, we'll handle results
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --all-projects --sarif-file-output=snyk-results.sarif
|
||||
|
||||
- name: Upload Snyk Results
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: snyk-results.sarif
|
||||
category: snyk
|
||||
|
||||
- name: Analyze Vulnerabilities
|
||||
id: vuln-analysis
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const exec = require('@actions/exec');
|
||||
|
||||
async function analyzeSarif(filePath, tool) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
|
||||
try {
|
||||
const sarif = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
let counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
||||
|
||||
sarif.runs.forEach(run => {
|
||||
run.results?.forEach(result => {
|
||||
let severity;
|
||||
if (tool === 'snyk') {
|
||||
severity = result.ruleId.includes('critical') ? 'critical' :
|
||||
result.ruleId.includes('high') ? 'high' :
|
||||
result.ruleId.includes('medium') ? 'medium' : 'low';
|
||||
} else {
|
||||
severity = result.level === 'error' ? 'high' :
|
||||
result.level === 'warning' ? 'medium' : 'low';
|
||||
}
|
||||
counts[severity]++;
|
||||
});
|
||||
});
|
||||
|
||||
return counts;
|
||||
} catch (error) {
|
||||
console.error(`Error analyzing ${tool} results:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Analyze results from different tools
|
||||
const results = {
|
||||
owasp: await analyzeSarif('reports/dependency-check-report.sarif', 'owasp'),
|
||||
snyk: ${{ steps.check-secrets.outputs.run_snyk == 'true' }} ?
|
||||
await analyzeSarif('snyk-results.sarif', 'snyk') : null
|
||||
};
|
||||
|
||||
// Calculate totals
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
results,
|
||||
total: {
|
||||
critical: Object.values(results).reduce((sum, r) => sum + (r?.critical || 0), 0),
|
||||
high: Object.values(results).reduce((sum, r) => sum + (r?.high || 0), 0),
|
||||
medium: Object.values(results).reduce((sum, r) => sum + (r?.medium || 0), 0),
|
||||
low: Object.values(results).reduce((sum, r) => sum + (r?.low || 0), 0)
|
||||
}
|
||||
};
|
||||
|
||||
// Save summary
|
||||
fs.writeFileSync('vulnerability-summary.json', JSON.stringify(summary, null, 2));
|
||||
|
||||
// Set outputs for other steps
|
||||
core.setOutput('critical_count', summary.total.critical);
|
||||
core.setOutput('high_count', summary.total.high);
|
||||
|
||||
// Create/update status badge
|
||||
const badge = {
|
||||
schemaVersion: 1,
|
||||
label: 'vulnerabilities',
|
||||
message: `critical: ${summary.total.critical} high: ${summary.total.high}`,
|
||||
color: summary.total.critical > 0 ? 'red' :
|
||||
summary.total.high > 0 ? 'orange' : 'green'
|
||||
};
|
||||
|
||||
fs.writeFileSync('security-badge.json', JSON.stringify(badge));
|
||||
|
||||
// Generate markdown report
|
||||
const report = `## Security Scan Results
|
||||
|
||||
### Summary
|
||||
- Critical: ${summary.total.critical}
|
||||
- High: ${summary.total.high}
|
||||
- Medium: ${summary.total.medium}
|
||||
- Low: ${summary.total.low}
|
||||
|
||||
### Tool-specific Results
|
||||
${Object.entries(results)
|
||||
.filter(([_, r]) => r)
|
||||
.map(([tool, r]) => `
|
||||
#### ${tool.toUpperCase()}
|
||||
- Critical: ${r.critical}
|
||||
- High: ${r.high}
|
||||
- Medium: ${r.medium}
|
||||
- Low: ${r.low}
|
||||
`).join('\n')}
|
||||
`;
|
||||
|
||||
fs.writeFileSync('security-report.md', report);
|
||||
|
||||
// Write job summary
|
||||
await core.summary
|
||||
.addRaw(report)
|
||||
.write();
|
||||
|
||||
// Exit with error if critical/high vulnerabilities found
|
||||
if (summary.total.critical > 0 || summary.total.high > 0) {
|
||||
core.setFailed(`Found ${summary.total.critical} critical and ${summary.total.high} high severity vulnerabilities`);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`Analysis failed: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Archive Security Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: security-reports-${{ github.run_id }}
|
||||
path: |
|
||||
reports/
|
||||
snyk-results.sarif
|
||||
vulnerability-summary.json
|
||||
security-report.md
|
||||
security-badge.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Create Fix PRs
|
||||
if: always() && (steps.vuln-analysis.outputs.critical_count > 0 || steps.vuln-analysis.outputs.high_count > 0)
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
async function createFixPR(vulnerability) {
|
||||
const branchName = `security/fix-${vulnerability.id}`;
|
||||
|
||||
try {
|
||||
// Create branch
|
||||
await exec.exec('git', ['checkout', '-b', branchName]);
|
||||
|
||||
// Apply fixes based on vulnerability type
|
||||
if (vulnerability.tool === 'snyk') {
|
||||
await exec.exec('npx', ['snyk', 'fix']);
|
||||
} else if (vulnerability.tool === 'owasp') {
|
||||
// Update dependencies to fixed versions
|
||||
if (fs.existsSync('package.json')) {
|
||||
await exec.exec('npm', ['audit', 'fix']);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are changes
|
||||
const { stdout: status } = await exec.getExecOutput('git', ['status', '--porcelain']);
|
||||
if (!status) {
|
||||
console.log('No changes to commit');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Commit changes
|
||||
await exec.exec('git', ['config', 'user.name', 'fiximus']);
|
||||
await exec.exec('git', ['config', 'user.email', 'github-bot@ivuorinen.net']);
|
||||
await exec.exec('git', ['add', '.']);
|
||||
await exec.exec('git', ['commit', '-m', `fix: ${vulnerability.title}`]);
|
||||
await exec.exec('git', ['push', 'origin', branchName]);
|
||||
|
||||
// Create PR
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `🔒 Security: ${vulnerability.title}`,
|
||||
body: generatePRBody(vulnerability),
|
||||
head: branchName,
|
||||
base: 'main'
|
||||
});
|
||||
|
||||
// Add labels
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.data.number,
|
||||
labels: ['security-fix', 'automated-pr', 'dependencies']
|
||||
});
|
||||
|
||||
return pr.data.html_url;
|
||||
} catch (error) {
|
||||
console.error(`Failed to create fix PR: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function generatePRBody(vulnerability) {
|
||||
return `## Security Fix
|
||||
|
||||
### Vulnerability Details
|
||||
- ID: ${vulnerability.id}
|
||||
- Severity: ${vulnerability.severity}
|
||||
- Tool: ${vulnerability.tool}
|
||||
|
||||
### Changes Made
|
||||
${vulnerability.fixes || 'Dependency updates to resolve security vulnerabilities'}
|
||||
|
||||
### Testing
|
||||
- [ ] Verify fix resolves the vulnerability
|
||||
- [ ] Run security scan to confirm fix
|
||||
- [ ] Test affected functionality
|
||||
|
||||
### Notes
|
||||
- This PR was automatically generated
|
||||
- Please review changes carefully
|
||||
- Additional manual changes may be needed
|
||||
|
||||
> Generated by security workflow`;
|
||||
}
|
||||
|
||||
try {
|
||||
// Process vulnerabilities from both tools
|
||||
const vulnFiles = ['snyk-results.sarif', 'reports/dependency-check-report.sarif'];
|
||||
const fixableVulnerabilities = [];
|
||||
|
||||
for (const file of vulnFiles) {
|
||||
if (fs.existsSync(file)) {
|
||||
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
const tool = file.includes('snyk') ? 'snyk' : 'owasp';
|
||||
|
||||
sarif.runs.forEach(run => {
|
||||
run.results?.forEach(result => {
|
||||
if (result.level === 'error' || result.level === 'critical') {
|
||||
fixableVulnerabilities.push({
|
||||
id: result.ruleId,
|
||||
title: result.message.text,
|
||||
severity: result.level,
|
||||
tool,
|
||||
fixes: result.fixes
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create PRs for fixable vulnerabilities
|
||||
const prUrls = [];
|
||||
for (const vuln of fixableVulnerabilities) {
|
||||
const prUrl = await createFixPR(vuln);
|
||||
if (prUrl) prUrls.push(prUrl);
|
||||
}
|
||||
|
||||
core.setOutput('fix_prs', prUrls.join('\n'));
|
||||
|
||||
if (prUrls.length > 0) {
|
||||
console.log(`Created ${prUrls.length} fix PRs:`);
|
||||
prUrls.forEach(url => console.log(`- ${url}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to process vulnerabilities:', error);
|
||||
}
|
||||
|
||||
- name: Notify on Failure
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
try {
|
||||
const { repo, owner } = context.repo;
|
||||
const runUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
// Read vulnerability summary if available
|
||||
let vulnSummary = '';
|
||||
if (fs.existsSync('vulnerability-summary.json')) {
|
||||
const summary = JSON.parse(fs.readFileSync('vulnerability-summary.json', 'utf8'));
|
||||
vulnSummary = `
|
||||
### Vulnerability Counts
|
||||
- Critical: ${summary.total.critical}
|
||||
- High: ${summary.total.high}
|
||||
- Medium: ${summary.total.medium}
|
||||
- Low: ${summary.total.low}
|
||||
`;
|
||||
}
|
||||
|
||||
const message = `## 🚨 Security Check Failure
|
||||
|
||||
Security checks have failed in the workflow run.
|
||||
|
||||
### Details
|
||||
- Run: [View Results](${runUrl})
|
||||
- Timestamp: ${new Date().toISOString()}
|
||||
|
||||
${vulnSummary}
|
||||
|
||||
### Reports
|
||||
Security scan reports are available in the workflow artifacts.
|
||||
|
||||
### Next Steps
|
||||
1. Review the security reports
|
||||
2. Address identified vulnerabilities
|
||||
3. Re-run security checks
|
||||
|
||||
> This issue was automatically created by the security workflow.`;
|
||||
|
||||
// Create GitHub issue
|
||||
const issue = await github.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title: `🚨 Security Check Failure - ${new Date().toISOString().split('T')[0]}`,
|
||||
body: message,
|
||||
labels: ['security', 'automated-issue', 'high-priority'],
|
||||
assignees: ['ivuorinen']
|
||||
});
|
||||
|
||||
// Send Slack notification if configured
|
||||
if (process.env.SLACK_WEBHOOK) {
|
||||
const fetch = require('node-fetch');
|
||||
await fetch(process.env.SLACK_WEBHOOK, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: `🚨 Security checks failed in ${owner}/${repo}\nDetails: ${issue.data.html_url}`
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to send notifications:', error);
|
||||
core.setFailed(`Notification failed: ${error.message}`);
|
||||
}
|
||||
|
||||
- name: Cleanup Old Issues
|
||||
if: always()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const { repo, owner } = context.repo;
|
||||
|
||||
const oldIssues = await github.rest.issues.listForRepo({
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
labels: 'automated-issue,security',
|
||||
sort: 'created',
|
||||
direction: 'desc'
|
||||
});
|
||||
|
||||
// Keep only the latest 3 issues
|
||||
const issuesToClose = oldIssues.data.slice(3);
|
||||
|
||||
for (const issue of issuesToClose) {
|
||||
await github.rest.issues.update({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed',
|
||||
state_reason: 'completed'
|
||||
});
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: '🔒 Auto-closing this issue as newer security check results are available.'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old issues:', error);
|
||||
}
|
||||
44
.github/workflows/stale.yml
vendored
44
.github/workflows/stale.yml
vendored
@@ -1,18 +1,52 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Stale
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 8 * * *"
|
||||
- cron: '0 8 * * *'
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
name: 🧹 Clean up stale issues and PRs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write # only for delete-branch option
|
||||
issues: write
|
||||
pull-requests: write
|
||||
packages: read
|
||||
statuses: read
|
||||
uses: ivuorinen/.github/.github/workflows/stale.yml@main
|
||||
|
||||
steps:
|
||||
- name: 🚀 Run stale
|
||||
uses: actions/stale@v9.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
remove-stale-when-updated: true
|
||||
stale-issue-label: 'stale'
|
||||
exempt-issue-labels: 'no-stale,help-wanted'
|
||||
stale-issue-message: >
|
||||
There hasn't been any activity on this issue recently, so we
|
||||
clean up some of the older and inactive issues.
|
||||
|
||||
Please make sure to update to the latest version and
|
||||
check if that solves the issue. Let us know if that works for you
|
||||
by leaving a comment 👍
|
||||
|
||||
This issue has now been marked as stale and will be closed if no
|
||||
further activity occurs. Thanks!
|
||||
stale-pr-label: 'stale'
|
||||
exempt-pr-labels: 'no-stale'
|
||||
stale-pr-message: >
|
||||
There hasn't been any activity on this pull request recently. This
|
||||
pull request has been automatically marked as stale because of that
|
||||
and will be closed if no further activity occurs within 7 days.
|
||||
Thank you for your contributions.
|
||||
|
||||
250
.github/workflows/sync-labels.yml
vendored
250
.github/workflows/sync-labels.yml
vendored
@@ -1,21 +1,255 @@
|
||||
---
|
||||
name: Sync labels
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Sync Labels
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- '.github/labels.yml'
|
||||
- '.github/workflows/sync-labels.yml'
|
||||
schedule:
|
||||
- cron: "34 5 * * *"
|
||||
- cron: '34 5 * * *' # 5:34 AM UTC every day
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
SyncLabels:
|
||||
uses: ivuorinen/.github/.github/workflows/sync-labels.yml@main
|
||||
labels:
|
||||
name: ♻️ Sync Labels
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Validate Labels File
|
||||
id: validate
|
||||
shell: bash
|
||||
run: |
|
||||
LABELS_URL="https://raw.githubusercontent.com/ivuorinen/actions/refs/heads/main/sync-labels/labels.yml"
|
||||
LABELS_FILE="labels.yml"
|
||||
|
||||
echo "Downloading labels file..."
|
||||
if ! curl -s --retry 3 --retry-delay 5 -o "$LABELS_FILE" "$LABELS_URL"; then
|
||||
echo "::error::Failed to download labels file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating YAML format..."
|
||||
if ! yq eval "$LABELS_FILE" > /dev/null 2>&1; then
|
||||
echo "::error::Invalid YAML format in labels file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required fields in each label
|
||||
echo "Validating label definitions..."
|
||||
INVALID=0
|
||||
HAS_NAME=0
|
||||
HAS_COLOR=0
|
||||
HAS_DESCRIPTION=0
|
||||
CURRENT_LABEL=""
|
||||
|
||||
check_label_completion() {
|
||||
if [[ $HAS_NAME -eq 1 && $HAS_COLOR -eq 1 && $HAS_DESCRIPTION -eq 1 ]]; then
|
||||
echo "✓ Valid label: $CURRENT_LABEL"
|
||||
else
|
||||
echo "✗ Invalid label: $CURRENT_LABEL (missing:"
|
||||
[[ $HAS_NAME -eq 0 ]] && echo " - name"
|
||||
[[ $HAS_COLOR -eq 0 ]] && echo " - color"
|
||||
[[ $HAS_DESCRIPTION -eq 0 ]] && echo " - description"
|
||||
echo ")"
|
||||
INVALID=$((INVALID + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
|
||||
if [[ "$line" =~ ^-.*$ ]]; then
|
||||
# Check previous label completion before starting new one
|
||||
if [[ -n "$CURRENT_LABEL" ]]; then
|
||||
check_label_completion
|
||||
fi
|
||||
# New label definition found, reset checks
|
||||
HAS_NAME=0
|
||||
HAS_COLOR=0
|
||||
HAS_DESCRIPTION=0
|
||||
CURRENT_LABEL="(new label)"
|
||||
elif [[ "$line" =~ ^[[:space:]]+name:[[:space:]]*(.+)$ ]]; then
|
||||
HAS_NAME=1
|
||||
CURRENT_LABEL="${BASH_REMATCH[1]}"
|
||||
elif [[ "$line" =~ ^[[:space:]]+color:[[:space:]]*([0-9A-Fa-f]{6})$ ]]; then
|
||||
HAS_COLOR=1
|
||||
elif [[ "$line" =~ ^[[:space:]]+description:[[:space:]]+.+$ ]]; then
|
||||
HAS_DESCRIPTION=1
|
||||
fi
|
||||
done < "$LABELS_FILE"
|
||||
|
||||
# Check the last label
|
||||
if [[ -n "$CURRENT_LABEL" ]]; then
|
||||
check_label_completion
|
||||
fi
|
||||
|
||||
echo "Validation complete. Found $INVALID invalid label(s)."
|
||||
|
||||
if [ $INVALID -ne 0 ]; then
|
||||
echo "::error::Found $INVALID invalid label definition(s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Labels file validated successfully"
|
||||
echo "labels_file=$LABELS_FILE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Create validation summary
|
||||
{
|
||||
echo "## Label Validation Results"
|
||||
echo
|
||||
echo "- Total invalid labels: $INVALID"
|
||||
echo "- File: \`$LABELS_FILE\`"
|
||||
echo "- Timestamp: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
echo
|
||||
echo "All labels have required fields:"
|
||||
echo "- name"
|
||||
echo "- color (6-digit hex)"
|
||||
echo "- description"
|
||||
} > validation-report.md
|
||||
|
||||
- name: Run Label Syncer
|
||||
id: sync
|
||||
uses: micnncim/action-label-syncer@3abd5e6e7981d5a790c6f8a7494374bd8c74b9c6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
manifest: ${{ steps.validate.outputs.labels_file }}
|
||||
prune: true
|
||||
|
||||
- name: Verify Label Sync
|
||||
id: verify
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Verifying labels..."
|
||||
|
||||
# Get current labels from GitHub
|
||||
CURRENT_LABELS=$(gh api repos/${{ github.repository }}/labels --jq '.[].name')
|
||||
|
||||
# Get expected labels from file
|
||||
EXPECTED_LABELS=$(yq eval '.[] | .name' "${{ steps.validate.outputs.labels_file }}")
|
||||
|
||||
# Compare labels
|
||||
MISSING_LABELS=0
|
||||
while IFS= read -r label; do
|
||||
if ! echo "$CURRENT_LABELS" | grep -q "^${label}$"; then
|
||||
echo "::warning::Label not synced: $label"
|
||||
MISSING_LABELS=1
|
||||
fi
|
||||
done <<< "$EXPECTED_LABELS"
|
||||
|
||||
if [ $MISSING_LABELS -eq 1 ]; then
|
||||
echo "::error::Some labels failed to sync"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All labels verified successfully"
|
||||
|
||||
- name: Generate Label Report
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "# Label Sync Report"
|
||||
echo "## Current Labels"
|
||||
echo
|
||||
# shellcheck disable=SC2016
|
||||
gh api repos/${{ github.repository }}/labels --jq '.[] | "- **" + .name + "** (`#" + .color + "`): " + .description' | sort
|
||||
echo
|
||||
echo "## Sync Status"
|
||||
echo "- ✅ Success: ${{ steps.sync.outcome == 'success' }}"
|
||||
echo "- 🔍 Verified: ${{ steps.verify.outcome == 'success' }}"
|
||||
echo
|
||||
echo "Generated at: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||||
} > label-report.md
|
||||
|
||||
- name: Upload Label Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: label-sync-report
|
||||
path: label-report.md
|
||||
retention-days: 7
|
||||
|
||||
- name: Notify on Failure
|
||||
if: failure()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
try {
|
||||
const { repo, owner } = context.repo;
|
||||
const runUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
const body = `## ⚠️ Label Sync Failed
|
||||
|
||||
The label synchronization workflow has failed.
|
||||
|
||||
### Details
|
||||
- Workflow: [View Run](${runUrl})
|
||||
- Repository: ${owner}/${repo}
|
||||
- Timestamp: ${new Date().toISOString()}
|
||||
|
||||
### Status
|
||||
- Validation: ${{ steps.validate.outcome }}
|
||||
- Sync: ${{ steps.sync.outcome }}
|
||||
- Verification: ${{ steps.verify.outcome }}
|
||||
|
||||
Please check the workflow logs for more details.
|
||||
|
||||
> This issue was automatically created by the label sync workflow.`;
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner,
|
||||
repo,
|
||||
title: '⚠️ Label Sync Failure',
|
||||
body,
|
||||
labels: ['automation', 'bug'],
|
||||
assignees: ['ivuorinen'],
|
||||
}).catch(error => {
|
||||
if (error.status === 403) {
|
||||
console.error('Permission denied while creating issue. Please check workflow permissions.');
|
||||
} else if (error.status === 429) {
|
||||
console.error('Rate limit exceeded. Please try again later.');
|
||||
} else {
|
||||
console.error(`Failed to create issue: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create issue:', error);
|
||||
}
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
# Remove temporary files
|
||||
rm -f labels.yml
|
||||
|
||||
# Remove lock files if they exist
|
||||
find . -name ".lock" -type f -delete
|
||||
|
||||
Reference in New Issue
Block a user