524 lines
19 KiB
YAML
524 lines
19 KiB
YAML
name: Nightly Build & Package
|
|
on:
|
|
schedule:
|
|
# Daily at 09:00 UTC (4am EST / 5am EDT)
|
|
- cron: '0 9 * * *'
|
|
workflow_dispatch:
|
|
inputs:
|
|
reason:
|
|
description: "Why are you running this manually?"
|
|
required: true
|
|
default: "manual trigger"
|
|
skip_tests:
|
|
description: "Skip test-nightly-image job?"
|
|
required: false
|
|
default: "false"
|
|
|
|
env:
|
|
GO_VERSION: '1.26.0'
|
|
NODE_VERSION: '24.12.0'
|
|
GOTOOLCHAIN: auto
|
|
GHCR_REGISTRY: ghcr.io
|
|
DOCKERHUB_REGISTRY: docker.io
|
|
IMAGE_NAME: wikid82/charon
|
|
|
|
jobs:
|
|
sync-development-to-nightly:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
has_changes: ${{ steps.sync.outputs.has_changes }}
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: nightly
|
|
fetch-depth: 0
|
|
token: ${{ secrets.CHARON_CI_TRIGGER_TOKEN || secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Configure Git
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
- name: Sync development to nightly
|
|
id: sync
|
|
env:
|
|
HAS_TRIGGER_TOKEN: ${{ secrets.CHARON_CI_TRIGGER_TOKEN != '' }}
|
|
run: |
|
|
# Fetch both branches to ensure we have the latest remote state
|
|
git fetch origin development
|
|
git fetch origin nightly
|
|
|
|
# Sync local nightly with remote nightly to prevent non-fast-forward errors
|
|
echo "Syncing local nightly with remote nightly..."
|
|
git reset --hard origin/nightly
|
|
|
|
# Check if there are differences between remote branches
|
|
if git diff --quiet origin/nightly origin/development; then
|
|
echo "No changes to sync from development to nightly"
|
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "Syncing changes from development to nightly"
|
|
# Fast-forward merge development into nightly
|
|
git merge origin/development --ff-only -m "chore: sync from development branch [skip ci]" || {
|
|
# If fast-forward fails, force reset to development
|
|
echo "Fast-forward not possible, resetting nightly to development"
|
|
git reset --hard origin/development
|
|
}
|
|
if [[ "$HAS_TRIGGER_TOKEN" != "true" ]]; then
|
|
echo "::warning title=Using GITHUB_TOKEN fallback::Set CHARON_CI_TRIGGER_TOKEN to ensure push-triggered workflows run on nightly."
|
|
fi
|
|
# Force push to handle cases where nightly diverged from development
|
|
git push --force origin nightly
|
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
trigger-nightly-validation:
|
|
name: Trigger Nightly Validation Workflows
|
|
needs: sync-development-to-nightly
|
|
if: needs.sync-development-to-nightly.outputs.has_changes == 'true'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
steps:
|
|
- name: Dispatch Missing Nightly Validation Workflows
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
|
|
const { data: nightlyBranch } = await github.rest.repos.getBranch({
|
|
owner,
|
|
repo,
|
|
branch: 'nightly',
|
|
});
|
|
const nightlyHeadSha = nightlyBranch.commit.sha;
|
|
core.info(`Current nightly HEAD: ${nightlyHeadSha}`);
|
|
|
|
const workflows = [
|
|
{ id: 'e2e-tests-split.yml' },
|
|
{ id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } },
|
|
{ id: 'supply-chain-verify.yml' },
|
|
{ id: 'codeql.yml' },
|
|
];
|
|
|
|
core.info('Skipping security-pr.yml: PR-only workflow intentionally excluded from nightly non-PR dispatch');
|
|
|
|
for (const workflow of workflows) {
|
|
const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({
|
|
owner,
|
|
repo,
|
|
workflow_id: workflow.id,
|
|
branch: 'nightly',
|
|
per_page: 50,
|
|
});
|
|
|
|
const hasRunForHead = workflowRuns.workflow_runs.some(
|
|
(run) => run.head_sha === nightlyHeadSha,
|
|
);
|
|
|
|
if (hasRunForHead) {
|
|
core.info(`Skipping dispatch for ${workflow.id}; run already exists for nightly HEAD`);
|
|
continue;
|
|
}
|
|
|
|
await github.rest.actions.createWorkflowDispatch({
|
|
owner,
|
|
repo,
|
|
workflow_id: workflow.id,
|
|
ref: 'nightly',
|
|
...(workflow.inputs ? { inputs: workflow.inputs } : {}),
|
|
});
|
|
core.info(`Dispatched ${workflow.id} on nightly (missing run for HEAD)`);
|
|
}
|
|
|
|
build-and-push-nightly:
|
|
needs: sync-development-to-nightly
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }}
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
id-token: write
|
|
outputs:
|
|
version: ${{ steps.meta.outputs.version }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
digest: ${{ steps.build.outputs.digest }}
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: nightly
|
|
fetch-depth: 0
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up QEMU
|
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
|
with:
|
|
registry: ${{ env.GHCR_REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Log in to Docker Hub
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
|
with:
|
|
registry: docker.io
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
|
with:
|
|
images: |
|
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=raw,value=nightly
|
|
type=raw,value=nightly-{{date 'YYYY-MM-DD'}}
|
|
type=sha,prefix=nightly-,format=short
|
|
labels: |
|
|
org.opencontainers.image.title=Charon Nightly
|
|
org.opencontainers.image.description=Nightly build of Charon
|
|
|
|
- name: Build and push Docker image
|
|
id: build
|
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
build-args: |
|
|
VERSION=nightly-${{ github.sha }}
|
|
VCS_REF=${{ github.sha }}
|
|
BUILD_DATE=${{ github.event.repository.pushed_at }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
provenance: true
|
|
sbom: true
|
|
|
|
- name: Record nightly image digest
|
|
run: |
|
|
echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Generate SBOM
|
|
id: sbom_primary
|
|
continue-on-error: true
|
|
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
|
with:
|
|
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}
|
|
format: cyclonedx-json
|
|
output-file: sbom-nightly.json
|
|
syft-version: v1.42.1
|
|
|
|
- name: Generate SBOM fallback with pinned Syft
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [[ "${{ steps.sbom_primary.outcome }}" == "success" ]] && [[ -s sbom-nightly.json ]] && jq -e . sbom-nightly.json >/dev/null 2>&1; then
|
|
echo "Primary SBOM generation succeeded with valid JSON; skipping fallback"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback"
|
|
|
|
SYFT_VERSION="v1.42.1"
|
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64) ARCH="amd64" ;;
|
|
aarch64|arm64) ARCH="arm64" ;;
|
|
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
esac
|
|
|
|
TARBALL="syft_${SYFT_VERSION#v}_${OS}_${ARCH}.tar.gz"
|
|
BASE_URL="https://github.com/anchore/syft/releases/download/${SYFT_VERSION}"
|
|
|
|
curl -fsSLo "$TARBALL" "${BASE_URL}/${TARBALL}"
|
|
curl -fsSLo checksums.txt "${BASE_URL}/syft_${SYFT_VERSION#v}_checksums.txt"
|
|
|
|
grep " ${TARBALL}$" checksums.txt > checksum_line.txt
|
|
sha256sum -c checksum_line.txt
|
|
|
|
tar -xzf "$TARBALL" syft
|
|
chmod +x syft
|
|
|
|
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" -o cyclonedx-json=sbom-nightly.json
|
|
|
|
- name: Verify SBOM artifact
|
|
if: always()
|
|
run: |
|
|
set -euo pipefail
|
|
test -s sbom-nightly.json
|
|
jq -e . sbom-nightly.json >/dev/null
|
|
jq -e '
|
|
.bomFormat == "CycloneDX"
|
|
and (.specVersion | type == "string" and length > 0)
|
|
and has("version")
|
|
and has("metadata")
|
|
and (.components | type == "array")
|
|
' sbom-nightly.json >/dev/null
|
|
|
|
- name: Upload SBOM artifact
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: sbom-nightly
|
|
path: sbom-nightly.json
|
|
retention-days: 30
|
|
|
|
# Install Cosign for keyless signing
|
|
- name: Install Cosign
|
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
|
|
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign GHCR Image
|
|
run: |
|
|
echo "Signing GHCR nightly image with keyless signing..."
|
|
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
|
echo "✅ GHCR nightly image signed successfully"
|
|
|
|
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign Docker Hub Image
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
run: |
|
|
echo "Signing Docker Hub nightly image with keyless signing..."
|
|
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
|
echo "✅ Docker Hub nightly image signed successfully"
|
|
|
|
# Attach SBOM to Docker Hub image
|
|
- name: Attach SBOM to Docker Hub
|
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
|
run: |
|
|
echo "Attaching SBOM to Docker Hub nightly image..."
|
|
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
|
echo "✅ SBOM attached to Docker Hub nightly image"
|
|
|
|
test-nightly-image:
|
|
needs: build-and-push-nightly
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: nightly
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
|
with:
|
|
registry: ${{ env.GHCR_REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Pull nightly image
|
|
run: docker pull "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
|
|
|
- name: Run container smoke test
|
|
run: |
|
|
docker run --name charon-nightly -d \
|
|
-p 8080:8080 \
|
|
"${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
|
|
|
# Wait for container to start
|
|
sleep 10
|
|
|
|
# Check container is running
|
|
docker ps | grep charon-nightly
|
|
|
|
# Basic health check
|
|
curl -f http://localhost:8080/health || exit 1
|
|
|
|
# Cleanup
|
|
docker stop charon-nightly
|
|
docker rm charon-nightly
|
|
|
|
# NOTE: Standalone binary builds removed - Charon uses Docker-only deployment
|
|
# The build-nightly-release job that ran GoReleaser for Windows/macOS/Linux binaries
|
|
# was removed because:
|
|
# 1. Charon is distributed exclusively via Docker images
|
|
# 2. Cross-compilation was failing due to Unix-specific syscalls
|
|
# 3. No users download standalone binaries (all use Docker)
|
|
# If standalone binaries are needed in the future, re-add the job with Linux-only targets
|
|
|
|
verify-nightly-supply-chain:
|
|
needs: build-and-push-nightly
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
security-events: write
|
|
|
|
steps:
|
|
- name: Checkout nightly branch
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
ref: nightly
|
|
|
|
- name: Set lowercase image name
|
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
|
|
|
- name: Download SBOM
|
|
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
|
with:
|
|
name: sbom-nightly
|
|
|
|
- name: Scan with Grype
|
|
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
|
|
with:
|
|
sbom: sbom-nightly.json
|
|
fail-build: false
|
|
severity-cutoff: high
|
|
|
|
- name: Scan with Trivy
|
|
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
|
with:
|
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-nightly.outputs.digest }}
|
|
format: 'sarif'
|
|
output: 'trivy-nightly.sarif'
|
|
|
|
- name: Upload Trivy results
|
|
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
|
with:
|
|
sarif_file: 'trivy-nightly.sarif'
|
|
category: 'trivy-nightly'
|
|
|
|
- name: Security severity policy summary
|
|
run: |
|
|
{
|
|
echo "## 🔐 Nightly Supply Chain Severity Policy"
|
|
echo ""
|
|
echo "- Blocking: Critical, High"
|
|
echo "- Medium: non-blocking by default (report + triage SLA)"
|
|
echo "- Policy file: .github/security-severity-policy.yml"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Check for Critical/High CVEs
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
jq -e . trivy-nightly.sarif >/dev/null
|
|
|
|
CRITICAL_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 9.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
HIGH_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 7.0 and $score < 9.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
MEDIUM_COUNT=$(jq -r '
|
|
[
|
|
.runs[] as $run
|
|
| ($run.tool.driver.rules // []) as $rules
|
|
| $run.results[]?
|
|
| . as $result
|
|
| (
|
|
(
|
|
if (($result.ruleIndex | type) == "number") then
|
|
($rules[$result.ruleIndex].properties["security-severity"] // empty)
|
|
else
|
|
empty
|
|
end
|
|
)
|
|
// ([
|
|
$rules[]?
|
|
| select((.id // "") == ($result.ruleId // ""))
|
|
| .properties["security-severity"]
|
|
][0] // empty)
|
|
// empty
|
|
) as $securitySeverity
|
|
| (try ($securitySeverity | tonumber) catch empty) as $score
|
|
| select($score != null and $score >= 4.0 and $score < 7.0)
|
|
] | length
|
|
' trivy-nightly.sarif)
|
|
|
|
{
|
|
echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
|
echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$HIGH_COUNT" -gt 0 ]; then
|
|
echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})"
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$MEDIUM_COUNT" -gt 0 ]; then
|
|
echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml"
|
|
fi
|
|
|
|
echo "✅ No Critical/High vulnerabilities found"
|