feat: add GitHub Actions workflows for code quality and automation (#2)

This commit is contained in:
2025-02-02 00:42:19 +02:00
committed by GitHub
parent af6ecdf6ca
commit 210aa969b3
105 changed files with 8807 additions and 408 deletions

260
.github/workflows/action-security.yml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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);
}

View File

@@ -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.

View File

@@ -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