-
Notifications
You must be signed in to change notification settings - Fork 0
256 lines (233 loc) · 11.6 KB
/
Copy pathtutorial-pr-checks.yml
File metadata and controls
256 lines (233 loc) · 11.6 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
# Reusable workflow — called by per-repo callers via:
# uses: sap-tutorials/tutorial-ci/.github/workflows/tutorial-pr-checks.yml@v1
#
# Runs notify-only PR content checks (markdownlint, cspell, gitleaks, lychee) over
# changed markdown, emits inline annotations, and uploads a normalised
# findings artifact for the trusted post-results.yml comment poster.
# Always exits 0 — never blocks merge.
name: Tutorial PR Checks
on:
workflow_call:
permissions:
contents: read
jobs:
checks:
runs-on: ubuntu-latest
steps:
# ── 1. Checkout with full history so the diff base is reachable ────────
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── 1b. Sparse-checkout tutorial-ci config + scripts into _ci/ ───────────
# Uses path:_ci so the consumer repo at workspace root is untouched.
# ref:v1 pins to the stable release tag — same repo that hosts this file.
- name: Checkout tutorial-ci config + scripts
continue-on-error: true
uses: actions/checkout@v4
with:
repository: sap-tutorials/tutorial-ci
ref: v1
sparse-checkout: |
config
scripts
sparse-checkout-cone-mode: false
path: _ci
# ── 2. Compute list of changed markdown files ──────────────────────────
- name: Compute changed markdown
run: |
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- '*.md' \
> changed.txt 2>/dev/null || true
echo "Changed markdown files:"
cat changed.txt || echo "(none)"
# ── 3. Detect whether any markdown changed (skip linters if not) ─────────
- name: Check for changed markdown
id: md-changed
run: |
if [ -s changed.txt ]; then
echo "has_md=true" >> "$GITHUB_OUTPUT"
else
echo "has_md=false" >> "$GITHUB_OUTPUT"
echo "No markdown files changed — skipping linters."
fi
# ── 4. Node.js for markdownlint-cli2, normaliser + annotation scripts ───
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
# ── 5. markdownlint-cli2 → ml.json (changed files only) ─────────────────
# Uses markdownlint-cli2-formatter-json so ml.json is a true JSON array
# of {fileName, lineNumber, ruleNames, ruleDescription} objects that the
# normaliser consumes directly — no fragile text parsing.
# Rules are read from _ci/config/markdownlint.yaml (single source of truth);
# the formatter is appended at runtime — no hand-maintained duplicate
# ruleset. --config accepts full cli2 JSONC configs.
- name: Run markdownlint-cli2
if: steps.md-changed.outputs.has_md == 'true'
continue-on-error: true
run: |
npm install --global markdownlint-cli2 markdownlint-cli2-formatter-json 2>/dev/null
# Build cli2 JSONC from _ci/config/markdownlint.yaml (single source of truth)
# then append the JSON formatter — no hand-maintained inline copy.
node -e "
const src = require('fs').readFileSync('_ci/config/markdownlint.yaml', 'utf8');
const rules = {};
for (const line of src.split('\n')) {
const m = line.match(/^(\w+):\s*(true|false)/);
if (m) rules[m[1]] = m[2] !== 'false';
}
// markdownlint-cli2 requires rule toggles under a 'config' key;
// top-level rule keys are ignored (this was silently dropping every disable).
const cfg = { config: rules, outputFormatters: [['markdownlint-cli2-formatter-json']] };
require('fs').writeFileSync('/tmp/ml-ci2.jsonc', JSON.stringify(cfg));
"
# Run against changed files only; formatter writes markdownlint-cli2-results.json
xargs -a changed.txt markdownlint-cli2 --config /tmp/ml-ci2.jsonc || true
# Rename to ml.json; fall back to empty array if formatter produced nothing
mv markdownlint-cli2-results.json ml.json 2>/dev/null || echo '[]' > ml.json
# ── 5b. cspell spell check → sp.json (changed files only, notify-only) ───
# Notice severity — advisory spelling, never blocks merge. Uses the
# committed SAP-terminology dictionary at _ci/config/sap-dictionary.txt
# (imported from the legacy tutorial-checker whitelist — no runtime fetch).
# JSON reporter → sp-raw.json, then converted to the shaped array that
# normalize-findings.js's cspell branch reads (repo-relative paths).
- name: Run cspell spell check
if: steps.md-changed.outputs.has_md == 'true'
continue-on-error: true
run: |
npm install --global cspell @cspell/cspell-json-reporter 2>/dev/null
# Single invocation (space-separated list, like lychee) so the JSON
# reporter emits exactly one JSON object to stdout — xargs batching
# would concatenate multiple objects into invalid JSON.
FILES=$(tr '\n' ' ' < changed.txt)
cspell lint \
--config _ci/config/cspell.json \
--no-progress \
--reporter @cspell/cspell-json-reporter \
$FILES > sp-raw.json 2>/dev/null || true
# Convert {issues:[{uri,row,text,suggestionsEx}]} → [{file,line,word,suggestions}]
# with repo-relative file paths (reporter emits absolute file:// URIs).
node -e "
const fs = require('fs');
const { fileURLToPath } = require('node:url');
const path = require('node:path');
let raw = {};
try { raw = JSON.parse(fs.readFileSync('sp-raw.json', 'utf8')); } catch {}
const issues = Array.isArray(raw.issues) ? raw.issues : [];
const shaped = issues.map((i) => {
let file = i.uri || '';
try { file = path.relative(process.cwd(), fileURLToPath(i.uri)).replace(/\\/g, '/'); } catch {}
const suggestions = Array.isArray(i.suggestionsEx)
? i.suggestionsEx.map((s) => s && s.word).filter(Boolean)
: (Array.isArray(i.suggestions) ? i.suggestions : []);
return { file, line: i.row ?? 0, word: i.text, suggestions };
});
fs.writeFileSync('sp.json', JSON.stringify(shaped));
" || echo '[]' > sp.json
# ── 6. gitleaks → gl.json (full-tree scan — correct security posture) ───
# Install pinned binary then run in no-git mode for a clean JSON report.
- name: Install gitleaks
run: |
GITLEAKS_VERSION=8.21.2
curl -sSfL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
| tar -xz -C /usr/local/bin gitleaks
- name: Run gitleaks
continue-on-error: true
run: |
gitleaks detect \
--no-git \
--report-format json \
--report-path gl.json \
-c _ci/config/gitleaks.toml \
--source . \
|| true
# ── 7. lychee link check → ly.json (changed files only) ─────────────────
# Uses lycheeverse/lychee-action@v2 so lychee is always available.
# Changed files are passed via a step output (space-separated).
# Output is transformed to the {fail_map} shape normalize-findings.js reads.
- name: Prepare lychee file list
id: lychee-files
if: steps.md-changed.outputs.has_md == 'true'
run: echo "files=$(tr '\n' ' ' < changed.txt)" >> "$GITHUB_OUTPUT"
- name: Run lychee link check
if: steps.md-changed.outputs.has_md == 'true'
continue-on-error: true
uses: lycheeverse/lychee-action@v2
with:
args: --config _ci/config/lychee.toml --format json --output ly-raw.json ${{ steps.lychee-files.outputs.files }}
- name: Normalize lychee output to fail_map shape
if: steps.md-changed.outputs.has_md == 'true'
continue-on-error: true
run: |
node -e "
const fs = require('fs');
let raw = {};
try { raw = JSON.parse(fs.readFileSync('ly-raw.json', 'utf8')); } catch {}
const failMap = raw.fail_map ?? {};
for (const entries of Object.values(failMap)) {
for (const e of entries) {
if (typeof e.status !== 'string')
e.status = typeof e.status === 'object'
? (e.status.text ?? JSON.stringify(e.status))
: String(e.status);
}
}
fs.writeFileSync('ly.json', JSON.stringify({ fail_map: failMap }));
" || echo '{}' > ly.json
# ── 7b. Capture changed-file list as step output for composite action ────
- name: Set changed-markdown output
id: changed-md-list
if: steps.md-changed.outputs.has_md == 'true'
run: |
{
echo 'files<<EOF'
cat changed.txt
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# ── 7c. Content checker → content.json (changed files only) ──────────────
- name: Run content checker
if: steps.md-changed.outputs.has_md == 'true'
uses: sap-tutorials/tutorial-ci/checker@v1
with:
files: ${{ steps.changed-md-list.outputs.files }}
output: content.json
# ── 7. Normalise tool outputs into unified findings.json ───────────────
# The CLI shim in normalize-findings.js reads the five JSON files
# (falls back to empty arrays/object on missing/invalid JSON) and
# prints a single findings array to stdout.
- name: Normalize findings
if: always()
continue-on-error: true
run: |
node _ci/scripts/normalize-findings.js ml.json gl.json ly.json content.json sp.json > findings.json
# ── 8. Emit GitHub workflow annotations ────────────────────────────────
# Reads findings.json and prints ::warning:: / ::notice:: commands.
# GitHub Actions picks these up and attaches them to the PR diff.
- name: Emit annotations
if: always()
continue-on-error: true
run: node _ci/scripts/emit-annotations.js
# ── 9. Write PR metadata for the comment-poster workflow ───────────────
- name: Write pr-meta.json
if: always()
run: |
printf '{"pr_number":%s,"head_sha":"%s","base_sha":"%s","repo":"%s"}\n' \
"${{ github.event.pull_request.number || 0 }}" \
"${{ github.event.pull_request.head.sha || github.sha }}" \
"${{ github.event.pull_request.base.sha || '' }}" \
"${{ github.repository }}" \
> pr-meta.json
# ── 10. Upload artifact consumed by post-results.yml ──────────────────
- name: Upload tutorial-ci-findings artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: tutorial-ci-findings
path: |
findings.json
pr-meta.json
# ── 11. Always succeed — notify-only, never blocks merge ───────────────
- name: Always succeed
if: always()
run: exit 0