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

11
.github/CODEOWNERS vendored
View File

@@ -1 +1,10 @@
* @ivuorinen
# Global owners
* @ivuorinen
# Workflow files
.github/workflows/ @ivuorinen
# Security files
SECURITY.md @ivuorinen
suppressions.xml @ivuorinen
.gitleaks.toml @ivuorinen

View File

@@ -2,92 +2,144 @@
## 1. Purpose
A primary goal of @ivuorinen's repositories is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
A primary goal of @ivuorinen's repositories is to be inclusive to the largest
number of contributors, with the most varied and diverse backgrounds possible.
As such, we are committed to providing a friendly, safe and welcoming
environment for all, regardless of gender, sexual orientation, ability,
ethnicity, socioeconomic status, and religion (or lack thereof).
This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
This code of conduct outlines our expectations for all those who participate in
our community, as well as the consequences for unacceptable behavior.
We invite all those who participate in @ivuorinen's repositories to help us create safe and positive experiences for everyone.
We invite all those who participate in @ivuorinen's repositories to help us
create safe and positive experiences for everyone.
## 2. Open [Source/Culture/Tech] Citizenship
A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
A supplemental goal of this Code of Conduct is to increase
open [source/culture/tech] citizenship by encouraging participants to recognize
and strengthen the relationships between our actions and their effects on our
community.
Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
Communities mirror the societies in which they exist and positive action is
essential to counteract the many forms of inequality and abuses of power that
exist in society.
If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
If you see someone who is making an extra effort to ensure our community is
welcoming, friendly, and encourages all participants to contribute to the
fullest extent, we want to know.
## 3. Expected Behavior
The following behaviors are expected and requested of all community members:
* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
* Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
* Participate in an authentic and active way. In doing so, you contribute to the
health and longevity of this community.
* Exercise consideration and respect in your speech and actions.
* Attempt collaboration before conflict.
* Refrain from demeaning, discriminatory, or harassing behavior and speech.
* Be mindful of your surroundings and of your fellow participants. Alert
community leaders if you notice a dangerous situation, someone in distress, or
violations of this Code of Conduct, even if they seem inconsequential.
* Remember that community event venues may be shared with members of the public;
please be respectful to all patrons of these locations.
## 4. Unacceptable Behavior
The following behaviors are considered harassment and are unacceptable within our community:
The following behaviors are considered harassment and are unacceptable within
our community:
* Violence, threats of violence or violent language directed against another person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
* Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other people's personally identifying information ("doxing").
* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
* Inappropriate photography or recording.
* Inappropriate physical contact. You should have someone's consent before touching them.
* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
* Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations.
* Violence, threats of violence or violent language directed against another
person.
* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory
jokes and language.
* Posting or displaying sexually explicit or violent material.
* Posting or threatening to post other people's personally identifying
information ("doxing").
* Personal insults, particularly those related to gender, sexual orientation,
race, religion, or disability.
* Inappropriate photography or recording.
* Inappropriate physical contact. You should have someone's consent before
touching them.
* Unwelcome sexual attention. This includes, sexualized comments or jokes;
inappropriate touching, groping, and unwelcomed sexual advances.
* Deliberate intimidation, stalking or following (online or in person).
* Advocating for, or encouraging, any of the above behavior.
* Sustained disruption of community events, including talks and presentations.
## 5. Weapons Policy
No weapons will be allowed at @ivuorinen's repositories events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter.
No weapons will be allowed at @ivuorinen's repositories events, community
spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons
include but are not limited to guns, explosives (including fireworks), and large
knives such as those used for hunting or display, as well as any other item used
for the purpose of causing injury or harm to others. Anyone seen in possession
of one of these items will be asked to leave immediately, and will only be
allowed to return without the weapon. Community members are further expected to
comply with all state and local laws on this matter.
## 6. Consequences of Unacceptable Behavior
Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
Unacceptable behavior from any community member, including sponsors and those
with decision-making authority, will not be tolerated.
Anyone asked to stop unacceptable behavior is expected to comply immediately.
If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
If a community member engages in unacceptable behavior, the community organizers
may take any action they deem appropriate, up to and including a temporary ban
or permanent expulsion from the community without warning (and without refund in
the case of a paid event).
## 7. Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. ismo@ivuorinen.net.
If you are subject to or witness unacceptable behavior, or have any other
concerns, please notify a community organizer as soon as possible:
<ismo@ivuorinen.net>
Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
Additionally, community organizers are available to help community members
engage with local law enforcement or to otherwise help those experiencing
unacceptable behavior feel safe. In the context of in-person events, organizers
will also provide escorts as desired by the person experiencing distress.
## 8. Addressing Grievances
If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify @ivuorinen with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
If you feel you have been falsely or unfairly accused of violating this Code of
Conduct, you should notify @ivuorinen with a concise description of your
grievance. Your grievance will be handled in accordance with our existing
governing policies.
## 9. Scope
We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business.
We expect all community participants (contributors, paid or otherwise; sponsors;
and other guests) to abide by this Code of Conduct in all community
venues--online and in-person--as well as in all one-on-one communications
pertaining to community business.
This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
This code of conduct and its related procedures also applies to unacceptable
behavior occurring outside the scope of community activities when such behavior
has the potential to adversely affect the safety and well-being of community
members.
## 10. Contact info
@ivuorinen
ismo@ivuorinen.net
<ismo@ivuorinen.net>
## 11. License and attribution
The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
The Citizen Code of Conduct is distributed by [Stumptown Syndicate][stumptown]
under a [Creative Commons Attribution-ShareAlike license][cc-by-sa].
Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
Portions of text derived from the [Django Code of Conduct][django] and
the [Geek Feminism Anti-Harassment Policy][geek-feminism].
_Revision 2.3. Posted 6 March 2017._
* _Revision 2.3. Posted 6 March 2017._
* _Revision 2.2. Posted 4 February 2016._
* _Revision 2.1. Posted 23 June 2014._
* _Revision 2.0, adopted by the [Stumptown Syndicate][stumptown] board on 10
January 2013. Posted 17 March 2013._
_Revision 2.2. Posted 4 February 2016._
_Revision 2.1. Posted 23 June 2014._
_Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._
[stumptown]: https://github.com/stumpsyn
[cc-by-sa]: https://creativecommons.org/licenses/by-sa/3.0/
[django]: https://www.djangoproject.com/conduct/
[geek-feminism]: http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy

View File

@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -8,13 +8,15 @@ assignees: ivuorinen
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
A clear and concise description of any alternative solutions or features you've
considered.
**Additional context**
Add any other context or screenshots about the feature request here.

166
.github/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,166 @@
# Security Policy
## Supported Versions
| Version | Supported |
|---------| ------------------ |
| main | :white_check_mark: |
## Reporting a Vulnerability
1. **Do Not** open a public issue
2. Email security concerns to <ismo@ivuorinen.net>
3. Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work on a fix if validated.
## Security Measures
This repository implements:
- CodeQL scanning
- OWASP Dependency Check
- Snyk vulnerability scanning
- Gitleaks secret scanning
- Trivy vulnerability scanner
- MegaLinter code analysis
- Regular security updates
- Automated fix PRs
- Daily security scans
- Weekly metrics collection
## Vulnerability Suppressions
This repository uses OWASP Dependency Check for security scanning. Some vulnerabilities may be suppressed if:
1. They are false positives
2. They affect only test/development dependencies
3. They have been assessed and determined to not be exploitable in our context
### Suppression File
Suppressions are managed in `suppressions.xml` in the root directory. Each suppression must include:
- Detailed notes explaining why the vulnerability is suppressed
- Specific identifiers (CVE, package, etc.)
- Regular review date
### Adding New Suppressions
To add a new suppression:
1. Add the entry to `suppressions.xml`
2. Include detailed notes explaining the reason
3. Create a PR with the changes
4. Get security team review
### Reviewing Suppressions
Suppressions are reviewed:
- Monthly during security scans
- When related dependencies are updated
- During security audits
## Security Best Practices
When using these actions:
1. Pin to commit hashes instead of tags
2. Use least-privilege token permissions
3. Validate all inputs
4. Set appropriate timeouts
5. Configure required security scanners:
- Add `suppressions.xml` for OWASP Dependency Check
- Add `.gitleaks.toml` for Gitleaks configuration
## Required Secrets
The following secrets should be configured in your repository:
| Secret Name | Description | Required |
|-------------|-------------|----------|
| `SNYK_TOKEN` | Token for Snyk vulnerability scanning | Optional |
| `GITLEAKS_LICENSE` | License for Gitleaks scanning | Optional |
| `SLACK_WEBHOOK` | Webhook URL for Slack notifications | Optional |
| `SONAR_TOKEN` | Token for SonarCloud analysis | Optional |
| `FIXIMUS_TOKEN` | Token for automated fixes | Optional |
## Security Workflows
This repository includes several security-focused workflows:
1. **Daily Security Checks** (`security.yml`)
- Runs comprehensive security scans
- Creates automated fix PRs
- Generates security reports
2. **Action Security** (`action-security.yml`)
- Validates GitHub Action files
- Checks for hardcoded credentials
- Scans for vulnerabilities
3. **CodeQL Analysis** (`codeql.yml`)
- Analyzes code for security issues
- Runs on multiple languages
- Weekly scheduled scans
4. **Security Metrics** (`security-metrics.yml`)
- Collects security metrics
- Generates trend reports
- Weekly analysis
## Security Reports
Security scan results are available as:
1. SARIF reports in GitHub Security tab
2. Artifacts in workflow runs
3. Automated issues for critical findings
4. Weekly trend reports
5. Security metrics dashboard
## Automated Fixes
The repository automatically:
1. Creates PRs for fixable vulnerabilities
2. Updates dependencies with security issues
3. Fixes code security issues
4. Creates detailed fix documentation
## Regular Reviews
We conduct:
1. Daily automated security scans
2. Weekly metrics analysis
3. Monthly suppression reviews
4. Regular dependency updates
## Contributing
When contributing to this repository:
1. Follow security best practices
2. Do not commit sensitive information
3. Use provided security tools
4. Review security documentation
## Support
For security-related questions:
1. Review existing security documentation
2. Check closed security issues
3. Contact security team at <ismo@ivuorinen.net>
Do not open public issues for security concerns.
## License
The security policy and associated tools are covered under the repository's MIT license.

22
.github/renovate.json vendored
View File

@@ -1,6 +1,20 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>ivuorinen/renovate-config"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>ivuorinen/renovate-config"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true
},
{
"matchDepTypes": ["devDependencies"],
"automerge": true
}
],
"schedule": ["every weekend"],
"vulnerabilityAlerts": {
"labels": ["security"],
"assignees": ["ivuorinen"]
}
}

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