Skip to content

Commit f62feac

Browse files
committed
feat: Add comprehensive CI/CD workflow with multi-platform builds
- Add ci-cd.yml workflow with automated build, test, and deployment - Implement security scanning with Trivy and Gosec - Add multi-platform binary builds (Linux, macOS, Windows - amd64/arm64) - Add Docker multi-arch builds with GHCR and Docker Hub support - Include integration testing and artifact uploads - Update README with CI/CD information
1 parent 186985e commit f62feac

File tree

2 files changed

+359
-2
lines changed

2 files changed

+359
-2
lines changed

.github/workflows/ci-cd.yml

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- develop
8+
pull_request:
9+
branches:
10+
- main
11+
- develop
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: write
16+
pull-requests: read
17+
packages: write
18+
id-token: write
19+
attestations: write
20+
21+
env:
22+
REGISTRY: ghcr.io
23+
IMAGE_NAME: ${{ github.repository }}
24+
GO_VERSION: "1.25.1"
25+
TESTCOVERAGE_THRESHOLD: 26
26+
27+
jobs:
28+
# Job 1: Build and Test
29+
build-and-test:
30+
name: Build and Test
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout code
34+
uses: actions/checkout@v5
35+
with:
36+
persist-credentials: false
37+
fetch-depth: 0
38+
39+
- name: Set up Go
40+
uses: actions/setup-go@v5
41+
with:
42+
go-version: ${{ env.GO_VERSION }}
43+
44+
- name: Cache Go modules
45+
uses: actions/cache@v4
46+
with:
47+
path: |
48+
~/.cache/go-build
49+
~/go/pkg/mod
50+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
51+
restore-keys: |
52+
${{ runner.os }}-go-
53+
54+
- name: Tidy dependencies
55+
run: go mod tidy
56+
57+
- name: Verify dependencies
58+
run: go mod verify
59+
60+
- name: Vet code
61+
run: go vet ./...
62+
63+
- name: Build binary
64+
run: make build
65+
66+
- name: Run tests with coverage
67+
run: make test-cov
68+
69+
- name: Check test coverage threshold
70+
run: |
71+
echo "Quality Gate: checking test coverage is above threshold ..."
72+
echo "Threshold : $TESTCOVERAGE_THRESHOLD %"
73+
totalCoverage=$(go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+')
74+
echo "Current test coverage : $totalCoverage %"
75+
if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 >= $2)}') )); then
76+
echo "Coverage check passed"
77+
else
78+
echo "Current test coverage is below threshold"
79+
echo "Please add more unit tests or adjust threshold to a lower value."
80+
exit 1
81+
fi
82+
83+
- name: Upload coverage reports
84+
uses: codecov/codecov-action@v5
85+
if: always()
86+
with:
87+
files: ./coverage.out
88+
flags: unittests
89+
name: codecov-umbrella
90+
fail_ci_if_error: false
91+
env:
92+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
93+
94+
- name: Upload build artifacts
95+
uses: actions/upload-artifact@v4
96+
with:
97+
name: github-repo-binary
98+
path: github-repo
99+
retention-days: 7
100+
101+
# Job 2: Lint
102+
lint:
103+
name: Lint Code
104+
runs-on: ubuntu-latest
105+
steps:
106+
- name: Checkout code
107+
uses: actions/checkout@v5
108+
with:
109+
persist-credentials: false
110+
111+
- name: Set up Go
112+
uses: actions/setup-go@v5
113+
with:
114+
go-version: ${{ env.GO_VERSION }}
115+
116+
- name: golangci-lint
117+
uses: golangci/golangci-lint-action@v8
118+
with:
119+
version: latest
120+
args: --timeout=5m
121+
122+
# Job 3: Security Scanning
123+
security-scan:
124+
name: Security Scan
125+
runs-on: ubuntu-latest
126+
permissions:
127+
security-events: write
128+
contents: read
129+
actions: read
130+
steps:
131+
- name: Checkout code
132+
uses: actions/checkout@v5
133+
with:
134+
persist-credentials: false
135+
136+
- name: Run Trivy vulnerability scanner
137+
uses: aquasecurity/trivy-action@master
138+
with:
139+
scan-type: 'fs'
140+
scan-ref: '.'
141+
format: 'sarif'
142+
output: 'trivy-results.sarif'
143+
severity: 'CRITICAL,HIGH'
144+
145+
- name: Upload Trivy results to GitHub Security tab
146+
uses: github/codeql-action/upload-sarif@v3
147+
if: always()
148+
with:
149+
sarif_file: 'trivy-results.sarif'
150+
151+
- name: Run Gosec Security Scanner
152+
uses: securego/gosec@master
153+
with:
154+
args: '-no-fail -fmt sarif -out gosec-results.sarif ./...'
155+
156+
- name: Upload Gosec results to GitHub Security tab
157+
uses: github/codeql-action/upload-sarif@v3
158+
if: always()
159+
with:
160+
sarif_file: 'gosec-results.sarif'
161+
162+
# Job 4: Build Multi-Platform Binaries
163+
build-binaries:
164+
name: Build Multi-Platform Binaries
165+
runs-on: ubuntu-latest
166+
needs: [build-and-test, lint]
167+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
168+
strategy:
169+
matrix:
170+
include:
171+
- os: linux
172+
arch: amd64
173+
output: github-repo-linux-amd64
174+
- os: linux
175+
arch: arm64
176+
output: github-repo-linux-arm64
177+
- os: darwin
178+
arch: amd64
179+
output: github-repo-darwin-amd64
180+
- os: darwin
181+
arch: arm64
182+
output: github-repo-darwin-arm64
183+
- os: windows
184+
arch: amd64
185+
output: github-repo-windows-amd64.exe
186+
steps:
187+
- name: Checkout code
188+
uses: actions/checkout@v5
189+
with:
190+
persist-credentials: false
191+
192+
- name: Set up Go
193+
uses: actions/setup-go@v5
194+
with:
195+
go-version: ${{ env.GO_VERSION }}
196+
197+
- name: Build binary for ${{ matrix.os }}/${{ matrix.arch }}
198+
env:
199+
GOOS: ${{ matrix.os }}
200+
GOARCH: ${{ matrix.arch }}
201+
CGO_ENABLED: 0
202+
run: |
203+
go build -ldflags="-s -w -X 'main.Version=${{ github.ref_name }}' -X 'main.GitCommitHash=${{ github.sha }}' -X 'main.BuiltAt=$(date -u +%Y-%m-%dT%H:%M:%SZ)'" -o ${{ matrix.output }}
204+
205+
- name: Upload binary artifact
206+
uses: actions/upload-artifact@v4
207+
with:
208+
name: ${{ matrix.output }}
209+
path: ${{ matrix.output }}
210+
retention-days: 30
211+
212+
# Job 5: Build and Push Docker Image
213+
docker-build-push:
214+
name: Build and Push Docker Image
215+
runs-on: ubuntu-latest
216+
needs: [build-and-test, lint]
217+
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
218+
steps:
219+
- name: Checkout code
220+
uses: actions/checkout@v5
221+
with:
222+
persist-credentials: false
223+
224+
- name: Set up Docker Buildx
225+
uses: docker/setup-buildx-action@v3
226+
227+
- name: Log in to GitHub Container Registry
228+
uses: docker/login-action@v3
229+
with:
230+
registry: ${{ env.REGISTRY }}
231+
username: ${{ github.actor }}
232+
password: ${{ secrets.GITHUB_TOKEN }}
233+
234+
- name: Log in to Docker Hub
235+
uses: docker/login-action@v3
236+
if: secrets.DOCKERHUB_TOKEN != ''
237+
with:
238+
username: ${{ secrets.DOCKERHUB_USERNAME }}
239+
password: ${{ secrets.DOCKERHUB_TOKEN }}
240+
241+
- name: Extract metadata
242+
id: meta
243+
uses: docker/metadata-action@v5
244+
with:
245+
images: |
246+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
247+
${{ secrets.DOCKERHUB_USERNAME }}/pvtr-github-repo
248+
tags: |
249+
type=ref,event=branch
250+
type=ref,event=pr
251+
type=semver,pattern={{version}}
252+
type=semver,pattern={{major}}.{{minor}}
253+
type=sha,prefix={{branch}}-
254+
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
255+
256+
- name: Build and push Docker image
257+
uses: docker/build-push-action@v6
258+
with:
259+
context: .
260+
platforms: linux/amd64,linux/arm64
261+
push: true
262+
tags: ${{ steps.meta.outputs.tags }}
263+
labels: ${{ steps.meta.outputs.labels }}
264+
cache-from: type=gha
265+
cache-to: type=gha,mode=max
266+
provenance: true
267+
sbom: true
268+
269+
- name: Attest Build Provenance
270+
uses: actions/attest-build-provenance@v2
271+
if: github.ref == 'refs/heads/main'
272+
with:
273+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
274+
subject-digest: ${{ steps.build.outputs.digest }}
275+
push-to-registry: true
276+
277+
# Job 6: Integration Test (Self-Test)
278+
integration-test:
279+
name: Integration Test
280+
runs-on: ubuntu-latest
281+
needs: [docker-build-push]
282+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
283+
steps:
284+
- name: Checkout code
285+
uses: actions/checkout@v5
286+
287+
- name: Create test config
288+
run: |
289+
cat > test-config.yml << EOF
290+
loglevel: info
291+
write-directory: evaluation_results
292+
write: true
293+
output: yaml
294+
services:
295+
self-test:
296+
plugin: github-repo
297+
policy:
298+
catalogs:
299+
- OSPS_B
300+
applicability:
301+
- Maturity Level 1
302+
vars:
303+
owner: ${{ github.repository_owner }}
304+
repo: ${{ github.event.repository.name }}
305+
token: \${{ secrets.GITHUB_TOKEN }}
306+
EOF
307+
308+
- name: Run self-assessment using Docker image
309+
run: |
310+
docker run --rm \
311+
-v $(pwd)/test-config.yml:/.privateer/config.yml \
312+
-v $(pwd)/evaluation_results:/evaluation_results \
313+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
314+
315+
- name: Upload evaluation results
316+
uses: actions/upload-artifact@v4
317+
if: always()
318+
with:
319+
name: evaluation-results
320+
path: evaluation_results/
321+
retention-days: 30
322+
323+
# Job 7: Notify on Success/Failure
324+
notify:
325+
name: Notify Status
326+
runs-on: ubuntu-latest
327+
needs: [build-and-test, lint, security-scan, docker-build-push]
328+
if: always() && (github.event_name == 'push' && github.ref == 'refs/heads/main')
329+
steps:
330+
- name: Check job statuses
331+
id: check
332+
run: |
333+
if [ "${{ needs.build-and-test.result }}" == "failure" ] || \
334+
[ "${{ needs.lint.result }}" == "failure" ] || \
335+
[ "${{ needs.security-scan.result }}" == "failure" ] || \
336+
[ "${{ needs.docker-build-push.result }}" == "failure" ]; then
337+
echo "status=failure" >> $GITHUB_OUTPUT
338+
else
339+
echo "status=success" >> $GITHUB_OUTPUT
340+
fi
341+
342+
- name: Post to Slack (if configured)
343+
if: env.SLACK_WEBHOOK_URL != ''
344+
uses: slackapi/slack-github-action@v2
345+
with:
346+
payload: |
347+
{
348+
"text": "GitHub Actions build result: ${{ steps.check.outputs.status }}\nRepository: ${{ github.repository }}\nBranch: ${{ github.ref_name }}\nCommit: ${{ github.sha }}\nAuthor: ${{ github.actor }}\nWorkflow: ${{ github.workflow }}\nRun: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
349+
}
350+
env:
351+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,22 @@ docker run \
2525

2626
## GitHub Actions Usage
2727

28-
We've pushed an image to docker hub for use in GitHub Actions.
28+
We've pushed images to GitHub Container Registry and Docker Hub for use in GitHub Actions.
2929

30-
You will also need to set up a GitHub personal access token with the repository read permissions. This token should be added to your config file, or — if using the example pipeline below — as a secret in your repository.
30+
You will need a GitHub personal access token with repository read permissions. This token should be added to your config file or as a secret in your repository.
3131

3232
### Example GHA Setup
3333

3434
- [Config](https://github.com/privateerproj/.github/blob/main/.github/pvtr-config.yml)
3535
- [Workflow Definition](https://github.com/privateerproj/.github/blob/main/.github/workflows/osps-baseline.yml)
3636
- [Action Results](https://github.com/privateerproj/.github/actions/runs/13691384519/job/38285134201)
3737

38+
### CI/CD Pipeline
39+
40+
This repository uses GitHub Actions for continuous integration and deployment. The workflow automatically builds, tests, performs security scanning, and publishes multi-platform binaries and Docker images.
41+
42+
For Docker Hub publishing, add `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` secrets to your repository settings.
43+
3844
## Contributing
3945

4046
Contributions are welcome! Please see our [Contributing Guidelines](.github/CONTRIBUTING.md) for more information.

0 commit comments

Comments
 (0)