-
Notifications
You must be signed in to change notification settings - Fork 2
326 lines (280 loc) · 12.9 KB
/
Copy pathquality-gates.yml
File metadata and controls
326 lines (280 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
name: Quality Gates
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
# Cancel in-progress runs for the same branch/PR to reduce CI load
# New pushes to the same branch will cancel older in-progress runs
concurrency:
group: quality-gates-${{ github.ref }}
cancel-in-progress: true
# Least-privilege permissions - explicitly set for security hardening
# For fork PRs, we restrict permissions further at the job level
# Note: Fork PRs from external contributors run with read-only permissions by default
permissions:
contents: read # Required: checkout code and read repository
pull-requests: write # Required: PR comments (diff-cover, dependency-review) - only for same-repo PRs
checks: write # Required: publish test results annotations - only for same-repo PRs
# Detect if this is a fork PR (used in job conditions)
# Fork PRs: github.event.pull_request.head.repo.full_name != github.repository
jobs:
# Dependency Review - runs on PRs to detect vulnerable dependency changes
# NOTE: Skip for fork PRs - requires write permissions for PR comments
# Fork PRs will still get vulnerability detection via the quality-checks job validators
dependency-review:
runs-on: ubuntu-latest
# Only run for same-repo PRs (not forks) - comment-summary-in-pr requires write access
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Dependency Review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4
continue-on-error: true
with:
# Fail on high and critical severity vulnerabilities
fail-on-severity: high
# Deny specific problematic licenses (optional, can be expanded)
deny-licenses: GPL-3.0, AGPL-3.0
# Block PRs that introduce vulnerable dependencies
comment-summary-in-pr: on-failure
# Note: The action automatically scans manifest files changed in the PR
# tasks/* workspace deps are excluded since they are not committed to the repo
# Use warn-only: true for brownfield-friendly approach to unknown licenses
warn-only: false
quality-checks:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# Run on both Ubuntu and macOS for cross-platform script compatibility
os: [ubuntu-latest, macos-latest]
# Enforce supported Python versions (defined in pyproject.toml: requires-python = ">=3.11")
python-version: ["3.11", "3.12"]
# Fork PR hardening: detect fork context for conditional caching and permissions
env:
IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
# Fetch full history for diff-cover comparison
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: ${{ matrix.python-version }}
# Disable pip cache for fork PRs to prevent cache poisoning
cache: ${{ env.IS_FORK_PR == 'true' && '' || 'pip' }}
# Cache virtualenv - with fork PR restrictions
# Fork PRs: restore-only (no writes) to prevent cache poisoning attacks
# Same-repo PRs and pushes: full cache read/write
- name: Cache virtualenv (restore-only for forks)
if: env.IS_FORK_PR == 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: .venv
key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
venv-${{ matrix.os }}-${{ matrix.python-version }}-
- name: Cache virtualenv (full access for same-repo)
if: env.IS_FORK_PR != 'true'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: .venv
key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
venv-${{ matrix.os }}-${{ matrix.python-version }}-
- name: Install actionlint
run: |
# Install actionlint for workflow linting (OS-specific binaries)
if [[ "${{ runner.os }}" == "Linux" ]]; then
curl -sL https://github.com/rhysd/actionlint/releases/download/v1.7.4/actionlint_1.7.4_linux_amd64.tar.gz | tar xz -C /tmp
sudo mv /tmp/actionlint /usr/local/bin/
elif [[ "${{ runner.os }}" == "macOS" ]]; then
ARCH="$(uname -m)"
if [[ "${ARCH}" == "arm64" ]]; then
curl -sL https://github.com/rhysd/actionlint/releases/download/v1.7.4/actionlint_1.7.4_darwin_arm64.tar.gz | tar xz -C /tmp
else
curl -sL https://github.com/rhysd/actionlint/releases/download/v1.7.4/actionlint_1.7.4_darwin_amd64.tar.gz | tar xz -C /tmp
fi
sudo mv /tmp/actionlint /usr/local/bin/
fi
- name: Install hadolint
if: runner.os == 'Linux'
run: |
# Install hadolint for Dockerfile linting (OS-specific binaries)
HADOLINT_VERSION="2.12.0"
if [[ "${{ runner.os }}" == "Linux" ]]; then
curl -fsSL "https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64" -o /tmp/hadolint
chmod +x /tmp/hadolint
sudo mv /tmp/hadolint /usr/local/bin/
fi
- name: Run actionlint on workflows
run: ./scripts/actionlint.sh
- name: Run hadolint on Dockerfiles
if: runner.os == 'Linux'
run: ./scripts/hadolint.sh
- name: Check GitHub Actions are pinned to SHAs
run: ./scripts/check-pinned-actions.sh
- name: Run ShellCheck on scripts
run: ./scripts/shellcheck.sh
- name: Check dependency lock files
run: ./scripts/check-deps.sh
- name: Check dependency hashes integrity
run: ./scripts/check-hashes.sh
# Run hardcoded values scan early for fast feedback on secrets/paths
# This runs before full lint to fail fast on security issues
- name: Check for hardcoded secrets and paths
run: ./scripts/check-hardcoded-values.sh
- name: Run lint checks
run: ./scripts/lint.sh
- name: Run type checking
run: ./scripts/typecheck.sh
- name: Run tests with coverage (strict warnings)
run: ./scripts/test.sh -W error::DeprecationWarning -W error::PendingDeprecationWarning -W error::RuntimeWarning -W error::ResourceWarning
env:
CI: true
- name: Run diff coverage analysis
id: diff-cover
if: github.event_name == 'pull_request'
run: |
# Generate diff-cover report in markdown format for PR comment
# Brownfield-friendly: warn-only mode initially (DIFF_COVER_WARN_ONLY=true)
# Set DIFF_COVER_FAIL_UNDER to enforce minimum coverage on changed lines
DIFF_COVER_WARN_ONLY=true \
DIFF_COVER_HTML=diff-cover.html \
./scripts/diff-cover.sh 2>&1 | tee diff-cover-output.txt
# Extract summary for PR comment using grouped redirects (SC2129)
{
echo "## Diff Coverage Report (${{ matrix.os }}/Python ${{ matrix.python-version }})"
echo ""
# Extract coverage percentage from diff-cover output
if grep -q "Diff Coverage:" diff-cover-output.txt; then
grep "Diff Coverage:" diff-cover-output.txt
else
echo "No changed lines found in coverage scope."
fi
echo ""
echo "<details>"
echo "<summary>Full diff coverage output</summary>"
echo ""
echo '```'
cat diff-cover-output.txt
echo '```'
echo "</details>"
} > diff-cover-summary.md
continue-on-error: true
- name: Post diff coverage summary to PR
# Only post one comment per PR (from ubuntu-latest/Python 3.11 job)
# Skip for fork PRs - requires pull-requests: write permission
if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' && env.IS_FORK_PR != 'true'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const fs = require('fs');
// Read the diff-cover summary
let summary = '';
try {
summary = fs.readFileSync('diff-cover-summary.md', 'utf8');
} catch (e) {
summary = '## Diff Coverage Report\n\nNo diff coverage data available.';
}
// Find existing comment to update
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('## Diff Coverage Report')
);
const body = summary + '\n\n---\n*Posted by Quality Gates CI*';
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
- name: Upload diff coverage report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: github.event_name == 'pull_request'
with:
name: diff-cover-report-${{ matrix.os }}-py${{ matrix.python-version }}
path: |
diff-cover.html
diff-cover-output.txt
diff-cover-summary.md
retention-days: 7
- name: Run security scan
run: ./scripts/security-scan.sh
- name: Run Bandit SAST scan
run: ./scripts/bandit.sh
- name: Run Semgrep security scan
run: ./scripts/semgrep.sh
- name: Run license compliance scan
run: ./scripts/license-scan.sh
- name: Generate SBOM
run: ./scripts/sbom.sh
- name: Validate OpenAPI schema
run: ./scripts/openapi-validate.sh
- name: Check for OpenAPI breaking changes
run: ./scripts/openapi-diff.sh
- name: Upload coverage reports
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
if: success()
with:
files: .coverage
fail_ci_if_error: false
continue-on-error: true
- name: Upload coverage HTML report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: coverage-report-${{ matrix.os }}-py${{ matrix.python-version }}
path: htmlcov/
retention-days: 7
- name: Upload JUnit test results
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: junit-results-${{ matrix.os }}-py${{ matrix.python-version }}
path: junit.xml
retention-days: 7
- name: Upload coverage XML
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: coverage-xml-${{ matrix.os }}-py${{ matrix.python-version }}
path: coverage.xml
retention-days: 7
- name: Publish test results (Ubuntu only)
# EnricoMi/publish-unit-test-result-action only supports Linux runners
# Skip for fork PRs - requires checks: write permission for annotations
uses: EnricoMi/publish-unit-test-result-action@27d65e188ec43221b20d26de30f4892fad91df2f # v2
if: always() && matrix.os == 'ubuntu-latest' && env.IS_FORK_PR != 'true'
with:
files: junit.xml
check_name: Test Results (${{ matrix.os }}/Python ${{ matrix.python-version }})
- name: Upload SBOM artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: sbom-${{ matrix.os }}-py${{ matrix.python-version }}
path: |
docs/sbom/sbom.json
docs/sbom/sbom.xml
retention-days: 30