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