Add QA test outputs, build scripts, and Dockerfile validation
- Created `qa-test-output-after-fix.txt` and `qa-test-output.txt` to log results of certificate page authentication tests. - Added `build.sh` for deterministic backend builds in CI, utilizing `go list` for efficiency. - Introduced `codeql_scan.sh` for CodeQL database creation and analysis for Go and JavaScript/TypeScript. - Implemented `dockerfile_check.sh` to validate Dockerfiles for base image and package manager mismatches. - Added `sourcery_precommit_wrapper.sh` to facilitate Sourcery CLI usage in pre-commit hooks.
This commit is contained in:
32
.github/workflows/auto-add-to-project.yml
vendored
Normal file
32
.github/workflows/auto-add-to-project.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Auto-add issues and PRs to Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Determine project URL presence
|
||||
id: project_check
|
||||
run: |
|
||||
if [ -n "${{ secrets.PROJECT_URL }}" ]; then
|
||||
echo "has_project=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_project=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Add issue or PR to project
|
||||
if: steps.project_check.outputs.has_project == 'true'
|
||||
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
project-url: ${{ secrets.PROJECT_URL }}
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
|
||||
- name: Skip summary
|
||||
if: steps.project_check.outputs.has_project == 'false'
|
||||
run: echo "PROJECT_URL secret missing; skipping project assignment." >> $GITHUB_STEP_SUMMARY
|
||||
17
.github/workflows/auto-changelog.yml
vendored
Normal file
17
.github/workflows/auto-changelog.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Auto Changelog (Release Drafter)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
update-draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Draft Release
|
||||
uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6
|
||||
env:
|
||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||
74
.github/workflows/auto-label-issues.yml
vendored
Normal file
74
.github/workflows/auto-label-issues.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Auto-label Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label based on title and body
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title.toLowerCase();
|
||||
const body = issue.body ? issue.body.toLowerCase() : '';
|
||||
const labels = [];
|
||||
|
||||
// Priority detection
|
||||
if (title.includes('[critical]') || body.includes('priority: critical')) {
|
||||
labels.push('critical');
|
||||
} else if (title.includes('[high]') || body.includes('priority: high')) {
|
||||
labels.push('high');
|
||||
} else if (title.includes('[medium]') || body.includes('priority: medium')) {
|
||||
labels.push('medium');
|
||||
} else if (title.includes('[low]') || body.includes('priority: low')) {
|
||||
labels.push('low');
|
||||
}
|
||||
|
||||
// Milestone detection
|
||||
if (title.includes('[alpha]') || body.includes('milestone: alpha')) {
|
||||
labels.push('alpha');
|
||||
} else if (title.includes('[beta]') || body.includes('milestone: beta')) {
|
||||
labels.push('beta');
|
||||
} else if (title.includes('[post-beta]') || body.includes('milestone: post-beta')) {
|
||||
labels.push('post-beta');
|
||||
}
|
||||
|
||||
// Category detection
|
||||
if (title.includes('architecture') || body.includes('architecture')) labels.push('architecture');
|
||||
if (title.includes('backend') || body.includes('backend')) labels.push('backend');
|
||||
if (title.includes('frontend') || body.includes('frontend')) labels.push('frontend');
|
||||
if (title.includes('security') || body.includes('security')) labels.push('security');
|
||||
if (title.includes('ssl') || title.includes('tls') || body.includes('certificate')) labels.push('ssl');
|
||||
if (title.includes('sso') || body.includes('single sign-on')) labels.push('sso');
|
||||
if (title.includes('waf') || body.includes('web application firewall')) labels.push('waf');
|
||||
if (title.includes('crowdsec') || body.includes('crowdsec')) labels.push('crowdsec');
|
||||
if (title.includes('caddy') || body.includes('caddy')) labels.push('caddy');
|
||||
if (title.includes('database') || body.includes('database')) labels.push('database');
|
||||
if (title.includes('ui') || title.includes('interface')) labels.push('ui');
|
||||
if (title.includes('docker') || title.includes('deployment')) labels.push('deployment');
|
||||
if (title.includes('monitoring') || title.includes('logging')) labels.push('monitoring');
|
||||
if (title.includes('documentation') || title.includes('docs')) labels.push('documentation');
|
||||
if (title.includes('test') || body.includes('testing')) labels.push('testing');
|
||||
if (title.includes('performance') || body.includes('optimization')) labels.push('performance');
|
||||
if (title.includes('plus') || body.includes('premium feature')) labels.push('plus');
|
||||
|
||||
// Feature detection
|
||||
if (title.includes('feature') || body.includes('feature request')) labels.push('feature');
|
||||
|
||||
// Only add labels if we detected any
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
|
||||
console.log(`Added labels: ${labels.join(', ')}`);
|
||||
}
|
||||
108
.github/workflows/auto-versioning.yml
vendored
Normal file
108
.github/workflows/auto-versioning.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Auto Versioning and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate Semantic Version
|
||||
id: semver
|
||||
uses: paulhatch/semantic-version@a8f8f59fd7f0625188492e945240f12d7ad2dca3 # v5.4.0
|
||||
with:
|
||||
# The prefix to use to create tags
|
||||
tag_prefix: "v"
|
||||
# A string which, if present in the git log, indicates that a major version increase is required
|
||||
major_pattern: "(MAJOR)"
|
||||
# A string which, if present in the git log, indicates that a minor version increase is required
|
||||
minor_pattern: "(feat)"
|
||||
# Pattern to determine formatting
|
||||
version_format: "${major}.${minor}.${patch}"
|
||||
# If no tags are found, this version is used
|
||||
version_from_branch: "0.0.0"
|
||||
# This helps it search through history to find the last tag
|
||||
search_commit_body: true
|
||||
# Important: This enables the output 'changed' which your other steps rely on
|
||||
enable_prerelease_mode: false
|
||||
|
||||
- name: Show version
|
||||
run: |
|
||||
echo "Next version: ${{ steps.semver.outputs.version }}"
|
||||
|
||||
- id: create_tag
|
||||
name: Create annotated tag and push
|
||||
if: ${{ steps.semver.outputs.changed }}
|
||||
run: |
|
||||
# Ensure a committer identity is configured in the runner so git tag works
|
||||
git config --global user.email "actions@github.com"
|
||||
git config --global user.name "GitHub Actions"
|
||||
|
||||
# Normalize the version: remove any leading 'v' so we don't end up with 'vvX.Y.Z'
|
||||
RAW="${{ steps.semver.outputs.version }}"
|
||||
VERSION_NO_V="${RAW#v}"
|
||||
|
||||
TAG="v${VERSION_NO_V}"
|
||||
echo "TAG=${TAG}"
|
||||
|
||||
# If tag already exists, skip creation to avoid failure
|
||||
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||
echo "Tag ${TAG} already exists; skipping tag creation"
|
||||
else
|
||||
git tag -a "${TAG}" -m "Release ${TAG}"
|
||||
git push origin "${TAG}"
|
||||
fi
|
||||
|
||||
# Export the tag for downstream steps
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||
|
||||
- name: Determine tag
|
||||
id: determine_tag
|
||||
run: |
|
||||
# Prefer created tag output; if empty fallback to semver version
|
||||
TAG="${{ steps.create_tag.outputs.tag }}"
|
||||
if [ -z "$TAG" ]; then
|
||||
# semver.version contains a tag value like 'vX.Y.Z' or fallback 'v0.0.0'
|
||||
VERSION_RAW="${{ steps.semver.outputs.version }}"
|
||||
VERSION_NO_V="${VERSION_RAW#v}"
|
||||
TAG="v${VERSION_NO_V}"
|
||||
fi
|
||||
echo "Determined tag: $TAG"
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for existing GitHub Release
|
||||
id: check_release
|
||||
run: |
|
||||
TAG=${{ steps.determine_tag.outputs.tag }}
|
||||
echo "Checking for release for tag: ${TAG}"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${CHARON_TOKEN}" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}") || true
|
||||
if [ "${STATUS}" = "200" ]; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release (tag-only, no workspace changes)
|
||||
if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }}
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
with:
|
||||
tag_name: ${{ steps.determine_tag.outputs.tag }}
|
||||
name: Release ${{ steps.determine_tag.outputs.tag }}
|
||||
generate_release_notes: true
|
||||
make_latest: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
63
.github/workflows/benchmark.yml
vendored
Normal file
63
.github/workflows/benchmark.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Go Benchmark
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
paths:
|
||||
- 'backend/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
paths:
|
||||
- 'backend/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Performance Regression Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Run Benchmark
|
||||
working-directory: backend
|
||||
run: go test -bench=. -benchmem -run='^$' ./... | tee output.txt
|
||||
|
||||
- name: Store Benchmark Result
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Go Benchmark
|
||||
tool: 'go'
|
||||
output-file-path: backend/output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
# Show alert with commit comment on detection of performance regression
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: false
|
||||
# Enable Job Summary for PRs
|
||||
summary-always: true
|
||||
|
||||
- name: Run Perf Asserts
|
||||
working-directory: backend
|
||||
env:
|
||||
PERF_MAX_MS_GETSTATUS_P95: 500ms
|
||||
PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms
|
||||
PERF_MAX_MS_LISTDECISIONS_P95: 2000ms
|
||||
run: |
|
||||
echo "## 🔍 Running performance assertions (TestPerf)" >> $GITHUB_STEP_SUMMARY
|
||||
go test -run TestPerf -v ./internal/api/handlers -count=1 | tee perf-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
62
.github/workflows/caddy-major-monitor.yml
vendored
Normal file
62
.github/workflows/caddy-major-monitor.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Monitor Caddy Major Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 7 * * 1' # Mondays at 07:17 UTC
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check-caddy-major:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for Caddy v3 and open issue
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const upstream = { owner: 'caddyserver', repo: 'caddy' };
|
||||
const { data: releases } = await github.rest.repos.listReleases({
|
||||
...upstream,
|
||||
per_page: 50,
|
||||
});
|
||||
const latestV3 = releases.find(r => /^v3\./.test(r.tag_name));
|
||||
if (!latestV3) {
|
||||
core.info('No Caddy v3 release detected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueTitle = `Track upgrade to Caddy v3 (${latestV3.tag_name})`;
|
||||
|
||||
const { data: existing } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (existing.some(i => i.title === issueTitle)) {
|
||||
core.info('Issue already exists — nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = [
|
||||
'Caddy v3 has been released upstream and detected by the scheduled monitor.',
|
||||
'',
|
||||
`Detected release: ${latestV3.tag_name} (${latestV3.html_url})`,
|
||||
'',
|
||||
'- Create a feature branch to evaluate the v3 migration.',
|
||||
'- Review breaking changes and update Docker base images/workflows.',
|
||||
'- Validate Trivy scans and update any policies as needed.',
|
||||
'',
|
||||
'Current policy: remain on latest 2.x until v3 is validated.'
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: issueTitle,
|
||||
body,
|
||||
});
|
||||
77
.github/workflows/codecov-upload.yml
vendored
Normal file
77
.github/workflows/codecov-upload.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Upload Coverage to Codecov (Push only)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
- 'feature/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
backend-codecov:
|
||||
name: Backend Codecov Upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Run Go tests with coverage
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
run: |
|
||||
bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Upload backend coverage to Codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./backend/coverage.txt
|
||||
flags: backend
|
||||
fail_ci_if_error: true
|
||||
|
||||
frontend-codecov:
|
||||
name: Frontend Codecov Upload
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: '24.11.1'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend tests and coverage
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ./frontend/coverage
|
||||
flags: frontend
|
||||
fail_ci_if_error: true
|
||||
53
.github/workflows/codeql.yml
vendored
Normal file
53
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: CodeQL - Analyze
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
schedule:
|
||||
- cron: '0 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: CodeQL analysis (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
# Skip forked PRs where CPMP_TOKEN lacks security-events permissions
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
pull-requests: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript-typescript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
78
.github/workflows/create-labels.yml
vendored
Normal file
78
.github/workflows/create-labels.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Create Project Labels
|
||||
|
||||
# This workflow only runs manually to set up labels
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-labels:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Create all project labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const labels = [
|
||||
// Priority labels
|
||||
{ name: 'critical', color: 'B60205', description: 'Must have for the release, blocks other work' },
|
||||
{ name: 'high', color: 'D93F0B', description: 'Important feature, should be included' },
|
||||
{ name: 'medium', color: 'FBCA04', description: 'Nice to have, can be deferred' },
|
||||
{ name: 'low', color: '0E8A16', description: 'Future enhancement, not urgent' },
|
||||
|
||||
// Milestone labels
|
||||
{ name: 'alpha', color: '5319E7', description: 'Part of initial alpha release' },
|
||||
{ name: 'beta', color: '0052CC', description: 'Part of beta release' },
|
||||
{ name: 'post-beta', color: '006B75', description: 'Post-beta enhancement' },
|
||||
|
||||
// Category labels
|
||||
{ name: 'architecture', color: 'C5DEF5', description: 'System design and structure' },
|
||||
{ name: 'backend', color: '1D76DB', description: 'Server-side code' },
|
||||
{ name: 'frontend', color: '5EBEFF', description: 'UI/UX code' },
|
||||
{ name: 'feature', color: 'A2EEEF', description: 'New functionality' },
|
||||
{ name: 'security', color: 'EE0701', description: 'Security-related' },
|
||||
{ name: 'ssl', color: 'F9D0C4', description: 'SSL/TLS certificates' },
|
||||
{ name: 'sso', color: 'D4C5F9', description: 'Single Sign-On' },
|
||||
{ name: 'waf', color: 'B60205', description: 'Web Application Firewall' },
|
||||
{ name: 'crowdsec', color: 'FF6B6B', description: 'CrowdSec integration' },
|
||||
{ name: 'caddy', color: '1F6FEB', description: 'Caddy-specific' },
|
||||
{ name: 'database', color: '006B75', description: 'Database-related' },
|
||||
{ name: 'ui', color: '7057FF', description: 'User interface' },
|
||||
{ name: 'deployment', color: '0E8A16', description: 'Docker, installation' },
|
||||
{ name: 'monitoring', color: 'FEF2C0', description: 'Logging and statistics' },
|
||||
{ name: 'documentation', color: '0075CA', description: 'Docs and guides' },
|
||||
{ name: 'testing', color: 'BFD4F2', description: 'Test suite' },
|
||||
{ name: 'performance', color: 'EDEDED', description: 'Optimization' },
|
||||
{ name: 'community', color: 'D876E3', description: 'Community building' },
|
||||
{ name: 'plus', color: 'FFD700', description: 'Premium/"Plus" feature' },
|
||||
{ name: 'enterprise', color: '8B4513', description: 'Enterprise-grade feature' }
|
||||
];
|
||||
|
||||
for (const label of labels) {
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description
|
||||
});
|
||||
console.log(`✓ Created label: ${label.name}`);
|
||||
} catch (error) {
|
||||
if (error.status === 422) {
|
||||
console.log(`⚠ Label already exists: ${label.name}`);
|
||||
// Update the label if it exists
|
||||
await github.rest.issues.updateLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description
|
||||
});
|
||||
console.log(`✓ Updated label: ${label.name}`);
|
||||
} else {
|
||||
console.error(`✗ Error creating label ${label.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
268
.github/workflows/docker-build.yml
vendored
Normal file
268
.github/workflows/docker-build.yml
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
name: Docker Build, Publish & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
- feature/beta-release
|
||||
# Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
- feature/beta-release
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
outputs:
|
||||
skip_build: ${{ steps.skip.outputs.skip_build }}
|
||||
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Normalize image name
|
||||
run: |
|
||||
IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV
|
||||
- name: Determine skip condition
|
||||
id: skip
|
||||
env:
|
||||
ACTOR: ${{ github.actor }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
HEAD_MSG: ${{ github.event.head_commit.message }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
should_skip=false
|
||||
pr_title=""
|
||||
if [ "$EVENT" = "pull_request" ]; then
|
||||
pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '')
|
||||
fi
|
||||
if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
# Always build on beta-release branch to ensure artifacts for testing
|
||||
if [[ "$REF" == "refs/heads/feature/beta-release" ]]; then
|
||||
should_skip=false
|
||||
echo "Force building on beta-release branch"
|
||||
fi
|
||||
|
||||
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Resolve Caddy base digest
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: caddy
|
||||
run: |
|
||||
docker pull caddy:2-alpine
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
|
||||
echo "image=$DIGEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to Container Registry
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
|
||||
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }}
|
||||
type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }}
|
||||
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
|
||||
- name: Build and push Docker image
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
|
||||
|
||||
- name: Run Trivy scan (table output)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'table'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
exit-code: '0'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner (SARIF)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check Trivy SARIF exists
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy-check
|
||||
run: |
|
||||
if [ -f trivy-results.sarif ]; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload Trivy results
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
|
||||
uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create summary
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
run: |
|
||||
echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test-image:
|
||||
name: Test Docker Image
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Normalize image name
|
||||
run: |
|
||||
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
|
||||
IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')
|
||||
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV
|
||||
- name: Determine image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker image
|
||||
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
- name: Create Docker Network
|
||||
run: docker network create charon-test-net
|
||||
|
||||
- name: Run Upstream Service (whoami)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name whoami \
|
||||
--network charon-test-net \
|
||||
traefik/whoami
|
||||
|
||||
- name: Run Charon Container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name test-container \
|
||||
--network charon-test-net \
|
||||
-p 8080:8080 \
|
||||
-p 80:80 \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
- name: Run Integration Test
|
||||
run: ./scripts/integration-test.sh
|
||||
|
||||
- name: Check container logs
|
||||
if: always()
|
||||
run: docker logs test-container
|
||||
|
||||
- name: Stop container
|
||||
if: always()
|
||||
run: |
|
||||
docker stop test-container whoami || true
|
||||
docker rm test-container whoami || true
|
||||
docker network rm charon-test-net || true
|
||||
|
||||
- name: Create test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Integration Test**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
trivy-pr-app-only:
|
||||
name: Trivy (PR) - App-only
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Build image locally for PR
|
||||
run: |
|
||||
docker build -t charon:pr-${{ github.sha }} .
|
||||
|
||||
- name: Extract `charon` binary from image
|
||||
run: |
|
||||
CONTAINER=$(docker create charon:pr-${{ github.sha }})
|
||||
docker cp ${CONTAINER}:/app/charon ./charon_binary || true
|
||||
docker rm ${CONTAINER} || true
|
||||
|
||||
- name: Run Trivy filesystem scan on `charon` (fail PR on HIGH/CRITICAL)
|
||||
run: |
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy -v $PWD:/workdir aquasec/trivy:latest fs --exit-code 1 --severity CRITICAL,HIGH /workdir/charon_binary
|
||||
23
.github/workflows/docker-lint.yml
vendored
Normal file
23
.github/workflows/docker-lint.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Docker Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
|
||||
jobs:
|
||||
hadolint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Run Hadolint
|
||||
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: warning
|
||||
276
.github/workflows/docker-publish.yml
vendored
Normal file
276
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
name: Docker Build, Publish & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
- feature/beta-release
|
||||
# Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
- feature/beta-release
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
security-events: write
|
||||
|
||||
outputs:
|
||||
skip_build: ${{ steps.skip.outputs.skip_build }}
|
||||
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Normalize image name
|
||||
run: |
|
||||
echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine skip condition
|
||||
id: skip
|
||||
env:
|
||||
ACTOR: ${{ github.actor }}
|
||||
EVENT: ${{ github.event_name }}
|
||||
HEAD_MSG: ${{ github.event.head_commit.message }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
should_skip=false
|
||||
pr_title=""
|
||||
if [ "$EVENT" = "pull_request" ]; then
|
||||
pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '')
|
||||
fi
|
||||
if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
||||
if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
||||
|
||||
# Always build on beta-release branch to ensure artifacts for testing
|
||||
if [[ "$REF" == "refs/heads/feature/beta-release" ]]; then
|
||||
should_skip=false
|
||||
echo "Force building on beta-release branch"
|
||||
fi
|
||||
|
||||
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Resolve Caddy base digest
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: caddy
|
||||
run: |
|
||||
docker pull caddy:2-alpine
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
|
||||
echo "image=$DIGEST" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to Container Registry
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
|
||||
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }}
|
||||
type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }}
|
||||
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
|
||||
|
||||
- name: Run Trivy scan (table output)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'table'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
exit-code: '0'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner (SARIF)
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
||||
with:
|
||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check Trivy SARIF exists
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
||||
id: trivy-check
|
||||
run: |
|
||||
if [ -f trivy-results.sarif ]; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Upload Trivy results
|
||||
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
|
||||
uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create summary
|
||||
if: steps.skip.outputs.skip_build != 'true'
|
||||
run: |
|
||||
echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
test-image:
|
||||
name: Test Docker Image
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Normalize image name
|
||||
run: |
|
||||
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
|
||||
echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||
|
||||
- name: Determine image tag
|
||||
id: tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then
|
||||
echo "tag=dev" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull Docker image
|
||||
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Create Docker Network
|
||||
run: docker network create charon-test-net
|
||||
|
||||
- name: Run Upstream Service (whoami)
|
||||
run: |
|
||||
docker run -d \
|
||||
--name whoami \
|
||||
--network charon-test-net \
|
||||
traefik/whoami
|
||||
|
||||
- name: Run Charon Container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name test-container \
|
||||
--network charon-test-net \
|
||||
-p 8080:8080 \
|
||||
-p 80:80 \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Run Integration Test
|
||||
run: ./scripts/integration-test.sh
|
||||
|
||||
- name: Check container logs
|
||||
if: always()
|
||||
run: docker logs test-container
|
||||
|
||||
- name: Stop container
|
||||
if: always()
|
||||
run: |
|
||||
docker stop test-container whoami || true
|
||||
docker rm test-container whoami || true
|
||||
docker network rm charon-test-net || true
|
||||
|
||||
- name: Create test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Integration Test**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
trivy-pr-app-only:
|
||||
name: Trivy (PR) - App-only
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Build image locally for PR
|
||||
run: |
|
||||
docker build -t charon:pr-${{ github.sha }} .
|
||||
|
||||
- name: Extract `charon` binary from image
|
||||
run: |
|
||||
CONTAINER=$(docker create charon:pr-${{ github.sha }})
|
||||
docker cp ${CONTAINER}:/app/charon ./charon_binary || true
|
||||
docker rm ${CONTAINER} || true
|
||||
|
||||
- name: Run Trivy filesystem scan on `charon` (fail PR on HIGH/CRITICAL)
|
||||
run: |
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy -v $PWD:/workdir aquasec/trivy:latest fs --exit-code 1 --severity CRITICAL,HIGH /workdir/charon_binary
|
||||
shell: bash
|
||||
353
.github/workflows/docs.yml
vendored
Normal file
353
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
name: Deploy Documentation to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Deploy docs when pushing to main
|
||||
paths:
|
||||
- 'docs/**' # Only run if docs folder changes
|
||||
- 'README.md' # Or if README changes
|
||||
- '.github/workflows/docs.yml' # Or if this workflow changes
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
# Sets permissions to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Documentation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Step 1: Get the code
|
||||
- name: 📥 Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
# Step 2: Set up Node.js (for building any JS-based doc tools)
|
||||
- name: 🔧 Set up Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: '24.11.1'
|
||||
|
||||
# Step 3: Create a beautiful docs site structure
|
||||
- name: 📝 Build documentation site
|
||||
run: |
|
||||
# Create output directory
|
||||
mkdir -p _site
|
||||
|
||||
# Copy all markdown files
|
||||
cp README.md _site/
|
||||
cp -r docs _site/
|
||||
|
||||
# Create a simple HTML index that looks nice
|
||||
cat > _site/index.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Charon - Documentation</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #1d4ed8;
|
||||
--primary-hover: #1e40af;
|
||||
}
|
||||
body {
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 100%);
|
||||
padding: 3rem 0;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
header h1 {
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
header p {
|
||||
color: #e0e7ff;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.card h3 {
|
||||
color: #60a5fa;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card p {
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card a:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.badge-beginner {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
.badge-advanced {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
color: #64748b;
|
||||
border-top: 1px solid #334155;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>🚀 Charon</h1>
|
||||
<p>Make your websites easy to reach - No coding required!</p>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<section>
|
||||
<h2>👋 Welcome!</h2>
|
||||
<p style="font-size: 1.1rem; color: #cbd5e1;">
|
||||
This documentation will help you get started with Charon.
|
||||
Whether you're a complete beginner or an experienced developer, we've got you covered!
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<h2 style="margin-top: 3rem;">📚 Getting Started</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🏠 Getting Started Guide <span class="badge badge-beginner">Start Here</span></h3>
|
||||
<p>Your first setup in just 5 minutes! We'll walk you through everything step by step.</p>
|
||||
<a href="docs/getting-started.html">Read the Guide →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📖 README <span class="badge badge-beginner">Essential</span></h3>
|
||||
<p>Learn what the app does, how to install it, and see examples of what you can build.</p>
|
||||
<a href="README.html">Read More →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📥 Import Guide</h3>
|
||||
<p>Already using Caddy? Learn how to bring your existing configuration into the app.</p>
|
||||
<a href="docs/import-guide.html">Import Your Configs →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">🔧 Developer Documentation</h2>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>🔌 API Reference <span class="badge badge-advanced">Advanced</span></h3>
|
||||
<p>Complete REST API documentation with examples in JavaScript and Python.</p>
|
||||
<a href="docs/api.html">View API Docs →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💾 Database Schema <span class="badge badge-advanced">Advanced</span></h3>
|
||||
<p>Understand how data is stored, relationships, and backup strategies.</p>
|
||||
<a href="docs/database-schema.html">View Schema →</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>✨ Contributing Guide</h3>
|
||||
<p>Want to help make this better? Learn how to contribute code, docs, or ideas.</p>
|
||||
<a href="CONTRIBUTING.html">Start Contributing →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">📋 All Documentation</h2>
|
||||
<div class="card">
|
||||
<h3>📚 Documentation Index</h3>
|
||||
<p>Browse all available documentation organized by topic and skill level.</p>
|
||||
<a href="docs/index.html">View Full Index →</a>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 3rem;">🆘 Need Help?</h2>
|
||||
<div class="card" style="background: #1e3a8a; border-color: #1e40af;">
|
||||
<h3 style="color: #dbeafe;">Get Support</h3>
|
||||
<p style="color: #bfdbfe;">
|
||||
Stuck? Have questions? We're here to help!
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem;">
|
||||
<a href="https://github.com/Wikid82/charon/discussions"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
💬 Ask a Question
|
||||
</a>
|
||||
<a href="https://github.com/Wikid82/charon/issues"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
🐛 Report a Bug
|
||||
</a>
|
||||
<a href="https://github.com/Wikid82/charon"
|
||||
style="background: white; color: #1e40af; padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;">
|
||||
⭐ View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Built with ❤️ by <a href="https://github.com/Wikid82" style="color: #60a5fa;">@Wikid82</a></p>
|
||||
<p>Made for humans, not just techies!</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Convert markdown files to HTML using a simple converter
|
||||
npm install -g marked
|
||||
|
||||
# Convert each markdown file
|
||||
for file in _site/docs/*.md; do
|
||||
if [ -f "$file" ]; then
|
||||
filename=$(basename "$file" .md)
|
||||
marked "$file" -o "_site/docs/${filename}.html" --gfm
|
||||
fi
|
||||
done
|
||||
|
||||
# Convert README and CONTRIBUTING
|
||||
marked _site/README.md -o _site/README.html --gfm
|
||||
if [ -f "CONTRIBUTING.md" ]; then
|
||||
cp CONTRIBUTING.md _site/
|
||||
marked _site/CONTRIBUTING.md -o _site/CONTRIBUTING.html --gfm
|
||||
fi
|
||||
|
||||
# Add simple styling to all HTML files
|
||||
for html_file in _site/*.html _site/docs/*.html; do
|
||||
if [ -f "$html_file" ] && [ "$html_file" != "_site/index.html" ]; then
|
||||
# Add a header with navigation to each page
|
||||
temp_file="${html_file}.tmp"
|
||||
cat > "$temp_file" << 'HEADER'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Caddy Proxy Manager Plus - Documentation</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<style>
|
||||
body { background-color: #0f172a; color: #e2e8f0; }
|
||||
nav { background: #1e293b; padding: 1rem; margin-bottom: 2rem; }
|
||||
nav a { color: #60a5fa; margin-right: 1rem; text-decoration: none; }
|
||||
nav a:hover { color: #93c5fd; }
|
||||
main { max-width: 900px; margin: 0 auto; padding: 2rem; }
|
||||
a { color: #60a5fa; }
|
||||
code { background: #1e293b; color: #fbbf24; padding: 0.2rem 0.4rem; border-radius: 4px; }
|
||||
pre { background: #1e293b; padding: 1rem; border-radius: 8px; overflow-x: auto; }
|
||||
pre code { background: none; padding: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/charon/">🏠 Home</a>
|
||||
<a href="/charon/docs/index.html">📚 Docs</a>
|
||||
<a href="/charon/docs/getting-started.html">🚀 Get Started</a>
|
||||
<a href="https://github.com/Wikid82/charon">⭐ GitHub</a>
|
||||
</nav>
|
||||
<main>
|
||||
HEADER
|
||||
|
||||
# Append original content
|
||||
cat "$html_file" >> "$temp_file"
|
||||
|
||||
# Add footer
|
||||
cat >> "$temp_file" << 'FOOTER'
|
||||
</main>
|
||||
<footer style="text-align: center; padding: 2rem; color: #64748b;">
|
||||
<p>Caddy Proxy Manager Plus - Built with ❤️ for the community</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
FOOTER
|
||||
|
||||
mv "$temp_file" "$html_file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Documentation site built successfully!"
|
||||
|
||||
# Step 4: Upload the built site
|
||||
- name: 📤 Upload artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
with:
|
||||
path: '_site'
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
# Deploy to GitHub Pages
|
||||
- name: 🚀 Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
|
||||
# Create a summary
|
||||
- name: 📋 Create deployment summary
|
||||
run: |
|
||||
echo "## 🎉 Documentation Deployed!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Your documentation is now live at:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "🔗 ${{ steps.deployment.outputs.page_url }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📚 What's Included" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Getting Started Guide" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Complete README" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- API Documentation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Database Schema" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Import Guide" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Contributing Guidelines" >> $GITHUB_STEP_SUMMARY
|
||||
34
.github/workflows/dry-run-history-rewrite.yml
vendored
Normal file
34
.github/workflows/dry-run-history-rewrite.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: History Rewrite Dry-Run
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # daily at 02:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
preview-history:
|
||||
name: Dry-run preview for history rewrite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Debug git info
|
||||
run: |
|
||||
git --version
|
||||
git rev-parse --is-shallow-repository || true
|
||||
git status --porcelain
|
||||
|
||||
- name: Make CI script executable
|
||||
run: chmod +x scripts/ci/dry_run_history_rewrite.sh
|
||||
|
||||
- name: Run dry-run history check
|
||||
run: |
|
||||
scripts/ci/dry_run_history_rewrite.sh --paths 'backend/codeql-db,codeql-db,codeql-db-js,codeql-db-go' --strip-size 50
|
||||
32
.github/workflows/history-rewrite-tests.yml
vendored
Normal file
32
.github/workflows/history-rewrite-tests.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: History Rewrite Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'scripts/history-rewrite/**'
|
||||
- '.github/workflows/history-rewrite-tests.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/history-rewrite/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout with full history
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y bats shellcheck
|
||||
|
||||
- name: Run Bats tests
|
||||
run: |
|
||||
bats ./scripts/history-rewrite/tests || exit 1
|
||||
|
||||
- name: ShellCheck scripts
|
||||
run: |
|
||||
shellcheck scripts/history-rewrite/*.sh || true
|
||||
48
.github/workflows/pr-checklist.yml
vendored
Normal file
48
.github/workflows/pr-checklist.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: PR Checklist Validation (History Rewrite)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate history-rewrite checklist (conditional)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Validate PR checklist (only for history-rewrite changes)
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const pr = await github.rest.pulls.get({owner, repo, pull_number: prNumber});
|
||||
const body = (pr.data && pr.data.body) || '';
|
||||
|
||||
// Determine if this PR modifies history-rewrite related files
|
||||
const filesResp = await github.rest.pulls.listFiles({ owner, repo, pull_number: prNumber });
|
||||
const files = filesResp.data.map(f => f.filename.toLowerCase());
|
||||
const relevant = files.some(fn => fn.startsWith('scripts/history-rewrite/') || fn.startsWith('docs/plans/history_rewrite.md') || fn.includes('history-rewrite'));
|
||||
if (!relevant) {
|
||||
core.info('No history-rewrite related files changed; skipping checklist validation.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a set of named checks with robust regex patterns for checkbox and phrase variants
|
||||
const checks = [
|
||||
{ name: 'preview_removals.sh mention', pattern: /preview_removals\.sh/i },
|
||||
{ name: 'data/backups mention', pattern: /data\/?backups/i },
|
||||
// Accept checked checkbox variants and inline code/backtick usage for the '--force' phrase
|
||||
{ name: 'explicit non-run of --force', pattern: /(?:\[\s*[xX]\s*\]\s*)?(?:i will not run|will not run|do not run|don'?t run|won'?t run)\b[^\n]*--force/i },
|
||||
];
|
||||
|
||||
const missing = checks.filter(c => !c.pattern.test(body)).map(c => c.name);
|
||||
if (missing.length > 0) {
|
||||
// Post a comment to the PR with instructions for filling the checklist
|
||||
const commentBody = `Hi! This PR touches history-rewrite artifacts and requires the checklist in .github/PULL_REQUEST_TEMPLATE/history-rewrite.md. The following items are missing in your PR body: ${missing.join(', ')}\n\nPlease update the PR description using the history-rewrite template and re-run checks.`;
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: commentBody });
|
||||
core.setFailed('Missing required checklist items: ' + missing.join(', '));
|
||||
}
|
||||
161
.github/workflows/propagate-changes.yml
vendored
Normal file
161
.github/workflows/propagate-changes.yml
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
name: Propagate Changes Between Branches
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
propagate:
|
||||
name: Create PR to synchronize branches
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'github-actions[bot]' && github.event.pusher != null
|
||||
steps:
|
||||
- name: Set up Node (for github-script)
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: '24.11.1'
|
||||
|
||||
- name: Propagate Changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const currentBranch = context.ref.replace('refs/heads/', '');
|
||||
|
||||
async function createPR(src, base) {
|
||||
if (src === base) return;
|
||||
|
||||
core.info(`Checking propagation from ${src} to ${base}...`);
|
||||
|
||||
// Check for existing open PRs
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${src}`,
|
||||
base: base,
|
||||
});
|
||||
|
||||
if (pulls.length > 0) {
|
||||
core.info(`Existing PR found for ${src} -> ${base}. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare commits to see if src is ahead of base
|
||||
try {
|
||||
const compare = await github.rest.repos.compareCommits({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: base,
|
||||
head: src,
|
||||
});
|
||||
|
||||
// If src is not ahead, nothing to merge
|
||||
if (compare.data.ahead_by === 0) {
|
||||
core.info(`${src} is not ahead of ${base}. No propagation needed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If files changed include history-rewrite or other sensitive scripts,
|
||||
// avoid automatic propagation. This prevents bypassing checklist validation
|
||||
// and manual review for potentially destructive changes.
|
||||
let files = (compare.data.files || []).map(f => (f.filename || '').toLowerCase());
|
||||
|
||||
// Fallback: if compare.files is empty/truncated, aggregate files from the commit list
|
||||
if (files.length === 0 && Array.isArray(compare.data.commits) && compare.data.commits.length > 0) {
|
||||
for (const commit of compare.data.commits) {
|
||||
const commitData = await github.rest.repos.getCommit({ owner: context.repo.owner, repo: context.repo.repo, ref: commit.sha });
|
||||
for (const f of (commitData.data.files || [])) {
|
||||
files.push((f.filename || '').toLowerCase());
|
||||
}
|
||||
}
|
||||
files = Array.from(new Set(files));
|
||||
}
|
||||
|
||||
// Load propagation config (list of sensitive paths) from .github/propagate-config.yml when available
|
||||
let configPaths = ['scripts/history-rewrite/', 'data/backups', 'docs/plans/history_rewrite.md', '.github/workflows/'];
|
||||
try {
|
||||
const configResp = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path: '.github/propagate-config.yml', ref: src });
|
||||
const contentStr = Buffer.from(configResp.data.content, 'base64').toString('utf8');
|
||||
const lines = contentStr.split(/\r?\n/);
|
||||
let inSensitive = false;
|
||||
const parsedPaths = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!inSensitive && trimmed.startsWith('sensitive_paths:')) { inSensitive = true; continue; }
|
||||
if (inSensitive) {
|
||||
if (trimmed.startsWith('-')) parsedPaths.push(trimmed.substring(1).trim());
|
||||
else if (trimmed.length === 0) continue; else break;
|
||||
}
|
||||
}
|
||||
if (parsedPaths.length > 0) configPaths = parsedPaths.map(p => p.toLowerCase());
|
||||
} catch (err) { core.info('No .github/propagate-config.yml or parse failure; using defaults.'); }
|
||||
|
||||
const sensitive = files.some(fn => configPaths.some(sp => fn.startsWith(sp) || fn.includes(sp)));
|
||||
if (sensitive) {
|
||||
core.info(`${src} -> ${base} contains sensitive changes (${files.join(', ')}). Skipping automatic propagation.`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// If base branch doesn't exist, etc.
|
||||
core.warning(`Error comparing ${src} to ${base}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PR
|
||||
try {
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Propagate changes from ${src} into ${base}`,
|
||||
head: src,
|
||||
base: base,
|
||||
body: `Automated PR to propagate changes from ${src} into ${base}.\n\nTriggered by push to ${currentBranch}.`,
|
||||
draft: true,
|
||||
});
|
||||
core.info(`Created PR #${pr.data.number} to merge ${src} into ${base}`);
|
||||
// Add an 'auto-propagate' label to the created PR and create the label if missing
|
||||
try {
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'auto-propagate' });
|
||||
} catch (e) {
|
||||
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: 'auto-propagate', color: '7dd3fc', description: 'Automatically created propagate PRs' });
|
||||
}
|
||||
await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.data.number, labels: ['auto-propagate'] });
|
||||
} catch (labelErr) {
|
||||
core.warning('Failed to ensure or add auto-propagate label: ' + labelErr.message);
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(`Failed to create PR from ${src} to ${base}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBranch === 'main') {
|
||||
// Main -> Development
|
||||
await createPR('main', 'development');
|
||||
} else if (currentBranch === 'development') {
|
||||
// Development -> Feature branches
|
||||
const branches = await github.paginate(github.rest.repos.listBranches, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
const featureBranches = branches
|
||||
.map(b => b.name)
|
||||
.filter(name => name.startsWith('feature/'));
|
||||
|
||||
core.info(`Found ${featureBranches.length} feature branches: ${featureBranches.join(', ')}`);
|
||||
|
||||
for (const featureBranch of featureBranches) {
|
||||
await createPR('development', featureBranch);
|
||||
}
|
||||
}
|
||||
env:
|
||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||
CPMP_TOKEN: ${{ secrets.CPMP_TOKEN }}
|
||||
170
.github/workflows/quality-checks.yml
vendored
Normal file
170
.github/workflows/quality-checks.yml
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
name: Quality Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
|
||||
jobs:
|
||||
backend-quality:
|
||||
name: Backend (Go)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
cache-dependency-path: backend/go.sum
|
||||
|
||||
- name: Repo health check
|
||||
run: |
|
||||
bash scripts/repo_health_check.sh
|
||||
|
||||
- name: Run Go tests
|
||||
id: go-tests
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
run: |
|
||||
bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Go Test Summary
|
||||
if: always()
|
||||
working-directory: backend
|
||||
run: |
|
||||
echo "## 🔧 Backend Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.go-tests.outcome }}" == "success" ]; then
|
||||
echo "✅ **All tests passed**" >> $GITHUB_STEP_SUMMARY
|
||||
PASS_COUNT=$(grep -c "^--- PASS" test-output.txt || echo "0")
|
||||
echo "- Tests passed: $PASS_COUNT" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **Tests failed**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Failed Tests:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
grep -E "^--- FAIL|FAIL\s+github" test-output.txt || echo "See logs for details"
|
||||
grep -E "^--- FAIL|FAIL\s+github" test-output.txt >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Codecov upload moved to `codecov-upload.yml` which is push-only.
|
||||
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
||||
with:
|
||||
version: latest
|
||||
working-directory: backend
|
||||
args: --timeout=5m
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Perf Asserts
|
||||
working-directory: backend
|
||||
env:
|
||||
# Conservative defaults to avoid flakiness on CI; tune as necessary
|
||||
PERF_MAX_MS_GETSTATUS_P95: 500ms
|
||||
PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms
|
||||
PERF_MAX_MS_LISTDECISIONS_P95: 2000ms
|
||||
run: |
|
||||
echo "## 🔍 Running performance assertions (TestPerf)" >> $GITHUB_STEP_SUMMARY
|
||||
go test -run TestPerf -v ./internal/api/handlers -count=1 | tee perf-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
frontend-quality:
|
||||
name: Frontend (React)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Repo health check
|
||||
run: |
|
||||
bash scripts/repo_health_check.sh
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '24.11.1'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Check if frontend was modified in PR
|
||||
id: check-frontend
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
echo "frontend_changed=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
# Try to fetch the PR base ref. This may fail for forked PRs or other cases.
|
||||
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 || true
|
||||
|
||||
# Compute changed files against the PR base ref, fallback to origin/main, then fallback to last 10 commits
|
||||
CHANGED=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD 2>/dev/null || echo "")
|
||||
echo "Changed files (base ref):\n$CHANGED"
|
||||
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "Base ref diff empty or failed; fetching origin/main for fallback..."
|
||||
git fetch origin main --depth=1 || true
|
||||
CHANGED=$(git diff --name-only origin/main...HEAD 2>/dev/null || echo "")
|
||||
echo "Changed files (main fallback):\n$CHANGED"
|
||||
fi
|
||||
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "Still empty; falling back to diffing last 10 commits from HEAD..."
|
||||
CHANGED=$(git diff --name-only HEAD~10...HEAD 2>/dev/null || echo "")
|
||||
echo "Changed files (HEAD~10 fallback):\n$CHANGED"
|
||||
fi
|
||||
|
||||
if echo "$CHANGED" | grep -q '^frontend/'; then
|
||||
echo "frontend_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
if: ${{ github.event_name == 'push' || steps.check-frontend.outputs.frontend_changed == 'true' }}
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend tests and coverage
|
||||
id: frontend-tests
|
||||
working-directory: ${{ github.workspace }}
|
||||
if: ${{ github.event_name == 'push' || steps.check-frontend.outputs.frontend_changed == 'true' }}
|
||||
run: |
|
||||
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Frontend Test Summary
|
||||
if: always()
|
||||
working-directory: frontend
|
||||
run: |
|
||||
echo "## ⚛️ Frontend Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.frontend-tests.outcome }}" == "success" ]; then
|
||||
echo "✅ **All tests passed**" >> $GITHUB_STEP_SUMMARY
|
||||
# Extract test counts from vitest output
|
||||
if grep -q "Tests:" test-output.txt; then
|
||||
grep "Tests:" test-output.txt | tail -1 >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "❌ **Tests failed**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Failed Tests:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
# Extract failed test info from vitest output
|
||||
grep -E "FAIL|✕|×|AssertionError|Error:" test-output.txt | head -30 >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Codecov upload moved to `codecov-upload.yml` which is push-only.
|
||||
|
||||
|
||||
|
||||
- name: Run frontend lint
|
||||
working-directory: frontend
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
59
.github/workflows/release-goreleaser.yml
vendored
Normal file
59
.github/workflows/release-goreleaser.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Release (GoReleaser)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Use the built-in CHARON_TOKEN by default for GitHub API operations.
|
||||
# If you need to provide a PAT with elevated permissions, add a CHARON_TOKEN secret
|
||||
# at the repo or organization level and update the env here accordingly.
|
||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
||||
with:
|
||||
node-version: '24.11.1'
|
||||
|
||||
- name: Build Frontend
|
||||
working-directory: frontend
|
||||
run: |
|
||||
# Inject version into frontend build from tag (if present)
|
||||
VERSION=$${GITHUB_REF#refs/tags/}
|
||||
echo "VITE_APP_VERSION=$$VERSION" >> $GITHUB_ENV
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Install Cross-Compilation Tools (Zig)
|
||||
uses: goto-bus-stop/setup-zig@v2
|
||||
with:
|
||||
version: 0.13.0
|
||||
|
||||
# CHARON_TOKEN is set from CHARON_TOKEN or CPMP_TOKEN (fallback), defaulting to GITHUB_TOKEN
|
||||
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
# CGO settings are handled in .goreleaser.yaml via Zig
|
||||
48
.github/workflows/renovate.yml
vendored
Normal file
48
.github/workflows/renovate.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *' # daily 05:00 EST
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Choose Renovate Token
|
||||
run: |
|
||||
# Prefer explicit tokens (CHARON_TOKEN > CPMP_TOKEN) if provided; otherwise use the default GITHUB_TOKEN
|
||||
if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then
|
||||
echo "Using CHARON_TOKEN" >&2
|
||||
echo "GITHUB_TOKEN=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV
|
||||
elif [ -n "${{ secrets.CPMP_TOKEN }}" ]; then
|
||||
echo "Using CPMP_TOKEN fallback" >&2
|
||||
echo "GITHUB_TOKEN=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using default GITHUB_TOKEN from Actions" >&2
|
||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Fail-fast if token not set
|
||||
run: |
|
||||
if [ -z "${{ env.GITHUB_TOKEN }}" ]; then
|
||||
echo "ERROR: No Renovate token provided. Set CHARON_TOKEN, CPMP_TOKEN, or rely on default GITHUB_TOKEN." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Renovate
|
||||
uses: renovatebot/github-action@5712c6a41dea6cdf32c72d92a763bd417e6606aa # v44.0.5
|
||||
with:
|
||||
configurationFile: .github/renovate.json
|
||||
token: ${{ env.GITHUB_TOKEN }}
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
103
.github/workflows/renovate_prune.yml
vendored
Normal file
103
.github/workflows/renovate_prune.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: "Prune Renovate Branches"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * *' # daily at 03:00 UTC
|
||||
pull_request:
|
||||
types: [closed] # also run when any PR is closed (makes pruning near-real-time)
|
||||
|
||||
permissions:
|
||||
contents: write # required to delete branch refs
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
prune:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: prune-renovate-branches
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
BRANCH_PREFIX: "renovate/" # adjust if you use a different prefix
|
||||
|
||||
steps:
|
||||
- name: Choose GitHub Token
|
||||
run: |
|
||||
if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then
|
||||
echo "Using CHARON_TOKEN" >&2
|
||||
echo "CHARON_TOKEN=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using CPMP_TOKEN fallback" >&2
|
||||
echo "CHARON_TOKEN=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Prune renovate branches
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ env.CHARON_TOKEN }}
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const branchPrefix = (process.env.BRANCH_PREFIX || 'renovate/').replace(/^refs\/heads\//, '');
|
||||
const refPrefix = `heads/${branchPrefix}`; // e.g. "heads/renovate/"
|
||||
|
||||
core.info(`Searching for refs with prefix: ${refPrefix}`);
|
||||
|
||||
// List matching refs (branches) under the prefix
|
||||
let refs;
|
||||
try {
|
||||
refs = await github.rest.git.listMatchingRefs({
|
||||
owner,
|
||||
repo,
|
||||
ref: refPrefix
|
||||
});
|
||||
} catch (err) {
|
||||
core.info(`No matching refs or API error: ${err.message}`);
|
||||
refs = { data: [] };
|
||||
}
|
||||
|
||||
for (const r of refs.data) {
|
||||
const fullRef = r.ref; // "refs/heads/renovate/..."
|
||||
const branchName = fullRef.replace('refs/heads/', '');
|
||||
core.info(`Evaluating branch: ${branchName}`);
|
||||
|
||||
// Find PRs for this branch (head = "owner:branch")
|
||||
const prs = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
head: `${owner}:${branchName}`,
|
||||
state: 'all',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
let shouldDelete = false;
|
||||
if (!prs.data || prs.data.length === 0) {
|
||||
core.info(`No PRs found for ${branchName} — marking for deletion.`);
|
||||
shouldDelete = true;
|
||||
} else {
|
||||
// If none of the PRs are open, safe to delete
|
||||
const hasOpen = prs.data.some(p => p.state === 'open');
|
||||
if (!hasOpen) {
|
||||
core.info(`All PRs for ${branchName} are closed — marking for deletion.`);
|
||||
shouldDelete = true;
|
||||
} else {
|
||||
core.info(`Open PR(s) exist for ${branchName} — skipping deletion.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `heads/${branchName}`
|
||||
});
|
||||
core.info(`Deleted branch: ${branchName}`);
|
||||
} catch (delErr) {
|
||||
core.warning(`Failed to delete ${branchName}: ${delErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- name: Done
|
||||
run: echo "Prune run completed."
|
||||
39
.github/workflows/repo-health.yml
vendored
Normal file
39
.github/workflows/repo-health.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Repo Health Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
repo_health:
|
||||
name: Repo health
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git --version
|
||||
git lfs install --local || true
|
||||
|
||||
- name: Run repo health check
|
||||
env:
|
||||
MAX_MB: 100
|
||||
LFS_ALLOW_MB: 50
|
||||
run: |
|
||||
bash scripts/repo_health_check.sh
|
||||
|
||||
- name: Upload health output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
with:
|
||||
name: repo-health-output
|
||||
path: |
|
||||
/tmp/repo_big_files.txt
|
||||
103
.github/workflows/waf-integration.yml
vendored
Normal file
103
.github/workflows/waf-integration.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: WAF Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, development, 'feature/**' ]
|
||||
paths:
|
||||
- 'backend/internal/caddy/**'
|
||||
- 'backend/internal/models/security*.go'
|
||||
- 'scripts/coraza_integration.sh'
|
||||
- 'Dockerfile'
|
||||
- '.github/workflows/waf-integration.yml'
|
||||
pull_request:
|
||||
branches: [ main, development ]
|
||||
paths:
|
||||
- 'backend/internal/caddy/**'
|
||||
- 'backend/internal/models/security*.go'
|
||||
- 'scripts/coraza_integration.sh'
|
||||
- 'Dockerfile'
|
||||
- '.github/workflows/waf-integration.yml'
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
waf-integration:
|
||||
name: Coraza WAF Integration
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg VCS_REF=${{ github.sha }} \
|
||||
-t charon:local .
|
||||
|
||||
- name: Run WAF integration tests
|
||||
id: waf-test
|
||||
run: |
|
||||
chmod +x scripts/coraza_integration.sh
|
||||
scripts/coraza_integration.sh 2>&1 | tee waf-test-output.txt
|
||||
exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Dump Debug Info on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "## 🔍 Debug Information" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "### Container Status" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
docker ps -a --filter "name=charon" --filter "name=coraza" >> $GITHUB_STEP_SUMMARY 2>&1 || true
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "### Caddy Admin Config" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
curl -s http://localhost:2019/config 2>/dev/null | head -200 >> $GITHUB_STEP_SUMMARY || echo "Could not retrieve Caddy config" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "### Charon Container Logs (last 100 lines)" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
docker logs charon-debug 2>&1 | tail -100 >> $GITHUB_STEP_SUMMARY || echo "No container logs available" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "### WAF Ruleset Files" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
docker exec charon-debug sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null && echo "---" && cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' >> $GITHUB_STEP_SUMMARY || echo "No ruleset files found" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: WAF Integration Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🛡️ WAF Integration Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${{ steps.waf-test.outcome }}" == "success" ]; then
|
||||
echo "✅ **All WAF tests passed**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Test Results:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
grep -E "^✓|^===|^Coraza" waf-test-output.txt || echo "See logs for details"
|
||||
grep -E "^✓|^===|^Coraza" waf-test-output.txt >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "❌ **WAF tests failed**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Failure Details:" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
grep -E "^✗|Unexpected|Error|failed" waf-test-output.txt | head -20 >> $GITHUB_STEP_SUMMARY || echo "See logs for details" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker rm -f charon-debug || true
|
||||
docker rm -f coraza-backend || true
|
||||
docker network rm containers_default || true
|
||||
Reference in New Issue
Block a user