Skip to content

chore(ci): migrate Trivy image scan to org-wide reusable Grype workflow #218

chore(ci): migrate Trivy image scan to org-wide reusable Grype workflow

chore(ci): migrate Trivy image scan to org-wide reusable Grype workflow #218

Workflow file for this run

name: CI
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
env:
GO_VERSION: '1.25'
REGISTRY: docker.io/supporttools
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Lint job - runs golangci-lint
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
- name: Run golangci-lint
run: golangci-lint run --timeout=5m
# Test job - runs unit and integration tests
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download dependencies
run: go mod download
- name: Run unit tests
run: go test ./pkg/... ./cmd/... -v -race -covermode=atomic -coverprofile=coverage.out -short
- name: Run integration tests
run: |
if [ -d "test/integration" ]; then
go test ./test/integration/... -v -race -covermode=atomic -coverprofile=coverage-integration.out
else
echo "Integration tests not yet implemented - skipping"
fi
- name: Generate coverage report
run: |
go tool cover -func=coverage.out | grep total | awk '{print "Unit test coverage: " $3}'
if [ -f coverage-integration.out ]; then
go tool cover -func=coverage-integration.out | grep total | awk '{print "Integration test coverage: " $3}'
fi
- name: Check coverage threshold
run: |
if [ ! -f coverage.out ]; then
echo "::error::Coverage file not found"
exit 1
fi
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
if [ -z "$COVERAGE" ]; then
echo "::error::Failed to extract coverage percentage"
exit 1
fi
THRESHOLD=70
echo "Current coverage: ${COVERAGE}%"
echo "Minimum threshold: ${THRESHOLD}%"
if (( $(echo "$COVERAGE < $THRESHOLD" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below minimum threshold of ${THRESHOLD}%"
exit 1
fi
echo "Coverage check passed!"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
continue-on-error: true # Codecov is informational; inline check above is the hard gate
with:
files: ./coverage.out,./coverage-integration.out
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
# Pinger ICMP integration - runs the real raw-ICMP loopback test under privilege.
#
# Kept as a SEPARATE job (not a step in `test`) so a privileged-socket flake on
# the runner does not block the main unit-test/coverage job. The runner user
# compiles the test binary (preserving the Go env / cache), then runs ONLY the
# integration test via sudo so it has CAP_NET_RAW. NODE_DOCTOR_ICMP_INTEGRATION=1
# makes socket/permission errors HARD failures so a misconfigured runner surfaces
# loudly instead of silently passing without exercising real ICMP.
pinger-icmp-integration:
name: Pinger ICMP Integration
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download dependencies
run: go mod download
- name: Compile network test binary
run: go test -c -o /tmp/nd-network.test ./pkg/monitors/network/
- name: Run ICMP integration test (privileged)
run: |
sudo NODE_DOCTOR_ICMP_INTEGRATION=1 /tmp/nd-network.test \
-test.run '^TestDefaultPinger_Integration$' -test.v
# Security scan - gosec
security-gosec:
name: Security Scan (gosec)
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Run gosec
uses: securego/gosec@v2.25.0
with:
args: '-no-fail -fmt sarif -out gosec-results.sarif ./...'
- name: Upload gosec results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
continue-on-error: true # gosec SARIF format may have compatibility issues
with:
sarif_file: gosec-results.sarif
# Build job - builds the binary
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download dependencies
run: go mod download
- name: Build binary
run: |
VERSION=$(date +%s)
GIT_COMMIT=$(git rev-parse HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
mkdir -p bin
cd cmd/node-doctor
go build \
-ldflags="-X main.Version=${VERSION} -X main.GitCommit=${GIT_COMMIT} -X main.BuildTime=${BUILD_TIME}" \
-o ../../bin/node-doctor
- name: Build overlay-test-server binary
run: |
mkdir -p bin
cd cmd/overlay-test-server
go build -o ../../bin/overlay-test-server
- name: Test binary
run: ./bin/node-doctor --version
- name: Upload binary artifact
uses: actions/upload-artifact@v7
with:
name: node-doctor-binary
path: bin/node-doctor
retention-days: 7
# Docker build and push - only on tags
docker:
name: Docker Build & Push
runs-on: ubuntu-latest
needs: [lint, test, build, security-gosec]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
security-events: write
contents: read
outputs:
tag: ${{ steps.tag.outputs.TAG }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract tag name
id: tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build metadata
id: meta
run: |
VERSION=${{ steps.tag.outputs.TAG }}
GIT_COMMIT=$(git rev-parse HEAD)
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "GIT_COMMIT=${GIT_COMMIT}" >> $GITHUB_OUTPUT
echo "BUILD_TIME=${BUILD_TIME}" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ env.REGISTRY }}/node-doctor:${{ steps.tag.outputs.TAG }}
${{ env.REGISTRY }}/node-doctor:latest
build-args: |
VERSION=${{ steps.meta.outputs.VERSION }}
GIT_COMMIT=${{ steps.meta.outputs.GIT_COMMIT }}
BUILD_TIME=${{ steps.meta.outputs.BUILD_TIME }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Grype vulnerability scan - INFORMATIONAL (SARIF/report only, no build gate).
# Replaces the former Trivy scan. NOTE: node-doctor publishes images to Docker Hub
# (docker.io/supporttools), not Harbor, so unlike other repos wired to this reusable
# workflow the `image` input below is a Docker Hub ref rather than a
# harbor.support.tools ref. The reusable workflow's Harbor login step only affects
# pulls from harbor.support.tools and does not block grype from pulling this
# public Docker Hub image directly.
grype-scan:
name: Grype Scan (informational)
needs: [docker]
uses: SupportTools/ci-runners/.github/workflows/grype-scan.yml@main

Check failure on line 281 in .github/workflows/ci.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/ci.yml

Invalid workflow file

error parsing called workflow ".github/workflows/ci.yml" -> "SupportTools/ci-runners/.github/workflows/grype-scan.yml@main" : workflow was not found. See https://docs.github.com/actions/learn-github-actions/reusing-workflows#access-to-reusable-workflows for more information.
with:
image: docker.io/supporttools/node-doctor:${{ needs.docker.outputs.tag }}
fail-on: high
blocking: false
permissions:
contents: read
id-token: write
security-events: write
# Summary job - provides overall status
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs: [lint, test, build, security-gosec]
if: always()
steps:
- name: Check job results
run: |
if [[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.build.result }}" != "success" ]] || \
[[ "${{ needs.security-gosec.result }}" != "success" ]]; then
echo "One or more CI jobs failed"
exit 1
fi
echo "All CI jobs passed successfully!"