appstore-precheck 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +23 -0
- package/bin/cli.js +14 -2
- package/package.json +1 -1
- package/skills/appstore-precheck/SKILL.md +1 -1
- package/skills/appstore-precheck/references/methodology.md +25 -0
- package/skills/appstore-precheck/scripts/sarif.sh +40 -0
- package/skills/appstore-precheck/scripts/scan.sh +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,70 @@ All notable changes to this project are documented here. Versioning follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.11.0] - 2026-07-02
|
|
9
|
+
|
|
10
|
+
Roadmap #3: **rejection-outcome feedback loop** — a third, honestly-scoped measurement
|
|
11
|
+
axis. Reporting layer only; the scan and GREEN/YELLOW/RED verdict are untouched.
|
|
12
|
+
|
|
13
|
+
Alongside the synthetic and real-panel corpora, the tool now tracks **real App Store
|
|
14
|
+
review outcomes** (approved / rejected + Apple's cited guideline) in a committed,
|
|
15
|
+
human-reviewed ledger (`corpus/outcomes/ledger.json`, starts empty) and summarizes them
|
|
16
|
+
in `docs/scorecard.md`. The summary is **honesty-floored**: below 10 recorded outcomes
|
|
17
|
+
it shows only a raw tally and computes **no rate**; at/above the floor it may show a
|
|
18
|
+
directional recall estimate with a permanent survivorship-bias caveat. Because the
|
|
19
|
+
ledger is committed and read offline, the section is deterministic and covered by the
|
|
20
|
+
existing `scorecard.sh --check` — no network, no new CI job.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`corpus/outcomes/ledger.json`** (empty `[]`) + **`corpus/outcomes/README.md`** —
|
|
24
|
+
record schema, anonymization/privacy rules (no verbatim Apple text, no real identity),
|
|
25
|
+
contribution/review process, and the sample-size floor.
|
|
26
|
+
- **`scripts/scorecard-outcomes.sh`** — pure `bash`+`jq`, offline, deterministic; renders
|
|
27
|
+
the "Real App Store outcomes (n=N)" section with the honesty floor. Also
|
|
28
|
+
`scorecard.sh --outcomes`.
|
|
29
|
+
- The outcomes section is baked into `docs/scorecard.md`; Methodology now describes three
|
|
30
|
+
measurements.
|
|
31
|
+
- **`.github/ISSUE_TEMPLATE/app-store-outcome.md`** — anonymized outcome contribution that
|
|
32
|
+
funnels into a maintainer-reviewed PR.
|
|
33
|
+
- Test: `tests/test-scorecard-outcomes.sh`.
|
|
34
|
+
|
|
35
|
+
### Notes
|
|
36
|
+
- Reporting/measurement only — outcome data never influences the verdict or which rules
|
|
37
|
+
fire (a future, human-only decision). No verbatim Apple Resolution Center text is stored.
|
|
38
|
+
The scan path (`scan.sh`/`verdict.sh`/`bin/cli.js`/`action.yml`) is unchanged.
|
|
39
|
+
|
|
40
|
+
## [1.10.0] - 2026-07-02
|
|
41
|
+
|
|
42
|
+
Roadmap #4: **SARIF output + opt-in GitHub PR annotations** — read-only, no auto-fix.
|
|
43
|
+
|
|
44
|
+
The scanner can now emit its deterministic findings as **SARIF 2.1.0**
|
|
45
|
+
(`scan.sh --format sarif`, also `npx appstore-precheck --format sarif`), built from
|
|
46
|
+
the same structured findings as `--format json` with pure `jq` (no new dependency).
|
|
47
|
+
`results[]` contains the non-suppressed FAIL and WARN findings (FAIL → `error`,
|
|
48
|
+
WARN → `warning`); PASS and suppressed findings are excluded; findings that carry a
|
|
49
|
+
`file`/`line` become SARIF `physicalLocation`s so GitHub can anchor annotations. Only
|
|
50
|
+
the deterministic scan findings are included — the agent-mode Pierre deep-review
|
|
51
|
+
findings are not (SARIF is a deterministic CI artifact).
|
|
52
|
+
|
|
53
|
+
The GitHub Action gains two **opt-in** inputs (both default `false`, so existing
|
|
54
|
+
usage is unchanged): `sarif` uploads the SARIF to code-scanning via `upload-sarif`
|
|
55
|
+
(PR annotations + Security tab; needs `permissions: security-events: write`), and
|
|
56
|
+
`annotations` emits inline `::error`/`::warning` PR annotations. Both run even when
|
|
57
|
+
the scan verdict fails, and neither modifies the project.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- **`skills/appstore-precheck/scripts/sarif.sh`** — `render_sarif()`, a SARIF 2.1.0
|
|
61
|
+
writer over the findings buffer (pure `jq`).
|
|
62
|
+
- **`scan.sh --format sarif`** (and `bin/cli.js --format text|json|sarif`).
|
|
63
|
+
- **`action.yml`** opt-in inputs `sarif` and `annotations` (default off): SARIF
|
|
64
|
+
upload via `github/codeql-action/upload-sarif@v3` and inline PR annotations.
|
|
65
|
+
- Tests: `tests/test-sarif.sh` and `tests/test-action-sarif.sh`.
|
|
66
|
+
|
|
67
|
+
### Notes
|
|
68
|
+
- Read-only preserved: the scanner writes only to stdout; the Action redirects SARIF
|
|
69
|
+
to a CI workspace file. No auto-fix. Default `--format text`/`--format json` output
|
|
70
|
+
and the Action's default behavior are byte-identical to before.
|
|
71
|
+
|
|
8
72
|
## [1.9.0] - 2026-07-02
|
|
9
73
|
|
|
10
74
|
Smarter analysis (roadmap #2a): **screenshot vision**, in two layers.
|
package/README.md
CHANGED
|
@@ -264,6 +264,29 @@ job on a RED verdict; set `fail-on: YELLOW` to be stricter:
|
|
|
264
264
|
Both inputs are optional: with none set, the action scans the repo root and fails the job only on
|
|
265
265
|
a RED verdict. No App Store Connect credentials are needed; the action runs the static scan only.
|
|
266
266
|
|
|
267
|
+
### SARIF & PR annotations (opt-in)
|
|
268
|
+
|
|
269
|
+
The Action can surface findings as GitHub code-scanning results and inline PR annotations. Both are
|
|
270
|
+
off by default; enable either or both:
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
permissions:
|
|
274
|
+
contents: read
|
|
275
|
+
security-events: write # required for SARIF upload
|
|
276
|
+
jobs:
|
|
277
|
+
precheck:
|
|
278
|
+
runs-on: ubuntu-latest
|
|
279
|
+
steps:
|
|
280
|
+
- uses: actions/checkout@v4
|
|
281
|
+
- uses: berkayturk/appstore-precheck@v1
|
|
282
|
+
with:
|
|
283
|
+
sarif: true # upload SARIF to the Security tab + PR annotations
|
|
284
|
+
annotations: true # also emit inline ::error/::warning annotations
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Locally / via npx: `npx appstore-precheck --format sarif > results.sarif`. The scan stays read-only;
|
|
288
|
+
nothing is auto-fixed.
|
|
289
|
+
|
|
267
290
|
## How it works
|
|
268
291
|
|
|
269
292
|
| Phase | Step |
|
package/bin/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ function printHelp() {
|
|
|
36
36
|
`Options:\n` +
|
|
37
37
|
` --dir <path> Directory to scan (default: current directory)\n` +
|
|
38
38
|
` --fail-on <level> Exit non-zero at RED (default) or YELLOW\n` +
|
|
39
|
+
` --format <fmt> Output format: text (default), json, or sarif\n` +
|
|
39
40
|
` -v, --version Print the version and exit\n` +
|
|
40
41
|
` -h, --help Show this help and exit\n` +
|
|
41
42
|
`\n` +
|
|
@@ -53,7 +54,7 @@ function fail(message, code) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function parseArgs(argv) {
|
|
56
|
-
const opts = { dir: process.cwd(), failOn: 'RED' };
|
|
57
|
+
const opts = { dir: process.cwd(), failOn: 'RED', format: 'text' };
|
|
57
58
|
for (let i = 0; i < argv.length; i++) {
|
|
58
59
|
const a = argv[i];
|
|
59
60
|
if (a === '-h' || a === '--help') { printHelp(); process.exit(0); }
|
|
@@ -69,6 +70,12 @@ function parseArgs(argv) {
|
|
|
69
70
|
opts.failOn = v;
|
|
70
71
|
continue;
|
|
71
72
|
}
|
|
73
|
+
if (a === '--format') {
|
|
74
|
+
const v = (argv[++i] || '').toLowerCase();
|
|
75
|
+
if (v !== 'text' && v !== 'json' && v !== 'sarif') fail('--format must be text, json, or sarif', 64);
|
|
76
|
+
opts.format = v;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
72
79
|
fail(`unknown option: ${a} (try --help)`, 64);
|
|
73
80
|
}
|
|
74
81
|
return opts;
|
|
@@ -84,7 +91,8 @@ function main() {
|
|
|
84
91
|
fail(`not a directory: ${opts.dir}`, 64);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
const
|
|
94
|
+
const scanArgs = opts.format === 'text' ? [SCAN] : [SCAN, '--format', opts.format];
|
|
95
|
+
const scan = spawnSync('bash', scanArgs, {
|
|
88
96
|
cwd: opts.dir,
|
|
89
97
|
encoding: 'utf8',
|
|
90
98
|
maxBuffer: 32 * 1024 * 1024,
|
|
@@ -97,6 +105,10 @@ function main() {
|
|
|
97
105
|
const scanOut = scan.stdout || '';
|
|
98
106
|
process.stdout.write(scanOut);
|
|
99
107
|
|
|
108
|
+
if (opts.format !== 'text') {
|
|
109
|
+
process.exit(scan.status === 0 ? 0 : (scan.status || 0));
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
const verdict = spawnSync('bash', [VERDICT], { input: scanOut, encoding: 'utf8' });
|
|
101
113
|
if (verdict.error) fail(`failed to compute the verdict: ${verdict.error.message}`, 70);
|
|
102
114
|
const summary = verdict.stdout || '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-precheck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill with native Claude Code, Cursor, and Codex plugins, plus an npx CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Berkay Turk (https://github.com/berkayturk)",
|
|
@@ -11,6 +11,8 @@ whole file to run the skill.
|
|
|
11
11
|
- [Phase 4: Pierre deep review (28 checks)](#phase-4-pierre-deep-review-28-semantic-checks)
|
|
12
12
|
- [Auto-detection rules](#auto-detection-rules)
|
|
13
13
|
- [Verdict thresholds](#verdict-thresholds)
|
|
14
|
+
- [SARIF output](#sarif-output---format-sarif)
|
|
15
|
+
- [Real App Store outcomes](#real-app-store-outcomes-corpusoutcomes)
|
|
14
16
|
- [Pre-submit manual checklist](#pre-submit-manual-checklist)
|
|
15
17
|
|
|
16
18
|
---
|
|
@@ -221,6 +223,29 @@ blocks, but it can be the fifth WARN that tips GREEN into YELLOW.
|
|
|
221
223
|
|
|
222
224
|
---
|
|
223
225
|
|
|
226
|
+
## SARIF output (`--format sarif`)
|
|
227
|
+
|
|
228
|
+
`scan.sh --format sarif` emits a SARIF 2.1.0 log built from the same structured findings as
|
|
229
|
+
`--format json` (pure `jq`, no new dependency). `results[]` contains the non-suppressed FAIL and
|
|
230
|
+
WARN findings (FAIL → `error`, WARN → `warning`); PASS and suppressed findings are excluded. Findings
|
|
231
|
+
that carry a `file`/`line` become SARIF `physicalLocation`s so GitHub can anchor PR annotations. Only
|
|
232
|
+
the deterministic scan findings are included — the agent-mode Pierre deep-review findings are not
|
|
233
|
+
(SARIF is a deterministic CI artifact). The GitHub Action uploads this via `upload-sarif` and/or
|
|
234
|
+
emits inline `::error`/`::warning` annotations, both opt-in.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Real App Store outcomes (`corpus/outcomes/`)
|
|
239
|
+
|
|
240
|
+
Beyond the synthetic and real-panel corpora, the tool tracks real Apple review outcomes (approved /
|
|
241
|
+
rejected + cited guideline) in a committed, human-reviewed ledger (`corpus/outcomes/ledger.json`),
|
|
242
|
+
summarized in `docs/scorecard.md` by `scripts/scorecard-outcomes.sh`. It is honesty-floored: no rate
|
|
243
|
+
is computed below 10 records (raw tally only), and a permanent survivorship-bias caveat applies once
|
|
244
|
+
a rate is shown. It is a reporting layer only — it never influences the GREEN/YELLOW/RED verdict or
|
|
245
|
+
which rules fire. Contribute an outcome via the "App Store outcome" issue template.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
224
249
|
## Pre-submit manual checklist
|
|
225
250
|
|
|
226
251
|
Things the scanner cannot verify; confirm by hand before you submit:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sarif.sh — SARIF 2.1.0 output layer for scan.sh. Sourced; pure jq over the
|
|
3
|
+
# findings buffer. No side effects, no output on load. bash 3.2 compatible.
|
|
4
|
+
|
|
5
|
+
: "${PRECHECK_VERSION:=dev}"
|
|
6
|
+
|
|
7
|
+
# render_sarif -> prints a SARIF 2.1.0 log built from the FINDINGS_TMP buffer.
|
|
8
|
+
# results[] = non-suppressed FAIL/WARN only (PASS + suppressed excluded);
|
|
9
|
+
# level: FAIL->error, WARN->warning. rules[] = distinct non-empty ruleIds present.
|
|
10
|
+
render_sarif() {
|
|
11
|
+
local buf="${FINDINGS_TMP:-/dev/null}"
|
|
12
|
+
local uri="https://github.com/berkayturk/appstore-precheck"
|
|
13
|
+
local help="https://github.com/berkayturk/appstore-precheck/blob/main/skills/appstore-precheck/references/methodology.md"
|
|
14
|
+
if [[ ! -s "$buf" ]]; then
|
|
15
|
+
jq -nc --arg v "$PRECHECK_VERSION" --arg u "$uri" '
|
|
16
|
+
{"$schema":"https://json.schemastore.org/sarif-2.1.0.json", "version":"2.1.0",
|
|
17
|
+
"runs":[{"tool":{"driver":{"name":"appstore-precheck","version":$v,"informationUri":$u,"rules":[]}},"results":[]}]}'
|
|
18
|
+
return 0
|
|
19
|
+
fi
|
|
20
|
+
jq -s --arg v "$PRECHECK_VERSION" --arg u "$uri" --arg help "$help" '
|
|
21
|
+
(map(select(.suppressed==false and (.severity=="FAIL" or .severity=="WARN")))) as $issues
|
|
22
|
+
| ($issues
|
|
23
|
+
| map({id:.rule_id, text:.guideline})
|
|
24
|
+
| map(select(.id != "" and .id != null))
|
|
25
|
+
| unique_by(.id)
|
|
26
|
+
| map({id:.id, name:.id, shortDescription:{text:.text}, helpUri:$help})) as $rules
|
|
27
|
+
| ($issues | map(
|
|
28
|
+
( (if (.rule_id // "") == "" then {} else {ruleId:.rule_id} end)
|
|
29
|
+
+ {level:(if .severity=="FAIL" then "error" else "warning" end),
|
|
30
|
+
message:{text:.message}}
|
|
31
|
+
+ (if .file != null
|
|
32
|
+
then {locations:[{physicalLocation:(
|
|
33
|
+
{artifactLocation:{uri:.file}}
|
|
34
|
+
+ (if .line != null then {region:{startLine:.line}} else {} end))}]}
|
|
35
|
+
else {locations:[]} end) )
|
|
36
|
+
)) as $results
|
|
37
|
+
| {"$schema":"https://json.schemastore.org/sarif-2.1.0.json", "version":"2.1.0",
|
|
38
|
+
"runs":[{"tool":{"driver":{"name":"appstore-precheck","version":$v,"informationUri":$u,"rules":$rules}},"results":$results}]}
|
|
39
|
+
' "$buf"
|
|
40
|
+
}
|
|
@@ -16,6 +16,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/findings.sh"
|
|
|
16
16
|
source "$(dirname "${BASH_SOURCE[0]}")/suppress.sh"
|
|
17
17
|
source "$(dirname "${BASH_SOURCE[0]}")/project-model.sh"
|
|
18
18
|
source "$(dirname "${BASH_SOURCE[0]}")/image-dims.sh"
|
|
19
|
+
source "$(dirname "${BASH_SOURCE[0]}")/sarif.sh"
|
|
19
20
|
FINDINGS_TMP="$(mktemp)"; export FINDINGS_TMP
|
|
20
21
|
trap 'rm -f "$FINDINGS_TMP"' EXIT
|
|
21
22
|
FORMAT="text"
|
|
@@ -28,13 +29,13 @@ PRECHECK_VERSION="$(grep -m1 -E '^[[:space:]]*version:' "$(dirname "${BASH_SOURC
|
|
|
28
29
|
while [[ $# -gt 0 ]]; do
|
|
29
30
|
case "$1" in
|
|
30
31
|
--format)
|
|
31
|
-
if [[ $# -lt 2 ]]; then echo "scan.sh: --format needs a value (text|json)" >&2; exit 64; fi
|
|
32
|
+
if [[ $# -lt 2 ]]; then echo "scan.sh: --format needs a value (text|json|sarif)" >&2; exit 64; fi
|
|
32
33
|
FORMAT="$2"; shift 2 ;;
|
|
33
34
|
--format=*) FORMAT="${1#*=}"; shift ;;
|
|
34
35
|
*) shift ;;
|
|
35
36
|
esac
|
|
36
37
|
done
|
|
37
|
-
[[ "$FORMAT" == json || "$FORMAT" == text ]] || { echo "scan.sh: --format must be text|json" >&2; exit 64; }
|
|
38
|
+
[[ "$FORMAT" == json || "$FORMAT" == text || "$FORMAT" == sarif ]] || { echo "scan.sh: --format must be text|json|sarif" >&2; exit 64; }
|
|
38
39
|
|
|
39
40
|
CONFIG="${APPSTORE_PRECHECK_CONFIG:-.appstore-precheck.json}"
|
|
40
41
|
have_jq() { command -v jq >/dev/null 2>&1; }
|
|
@@ -202,7 +203,7 @@ SUB_KEY="$(cfg '.disclosureKeys.subscription' 'subscription_disclosure')"
|
|
|
202
203
|
TRIAL_KEY="$(cfg '.disclosureKeys.trial' 'subscription_trial_disclosure')"
|
|
203
204
|
CHECK_FAMILY="$(cfg_bool '.optionalChecks.familyControls')"
|
|
204
205
|
|
|
205
|
-
if [[ "$FORMAT"
|
|
206
|
+
if [[ "$FORMAT" != text ]]; then exec 4>&1 1>/dev/null; fi
|
|
206
207
|
|
|
207
208
|
echo "PASS: layout — ios='${IOS_DIR:-?}' metadata='${META_DIR:-?}' xcstrings='${XCSTRINGS:-?}' locales=${#LOCALES[@]}"
|
|
208
209
|
|
|
@@ -1011,4 +1012,5 @@ echo "---END-OF-SCAN---"
|
|
|
1011
1012
|
if [[ "$FORMAT" == text && "${_SUPPRESSED_COUNT:-0}" -gt 0 ]]; then
|
|
1012
1013
|
printf '(%s finding(s) suppressed via .precheck-ignore)\n' "$_SUPPRESSED_COUNT"
|
|
1013
1014
|
fi
|
|
1014
|
-
if [[ "$FORMAT" == json ]]; then exec 1>&4 4>&-; render_json;
|
|
1015
|
+
if [[ "$FORMAT" == json ]]; then exec 1>&4 4>&-; render_json;
|
|
1016
|
+
elif [[ "$FORMAT" == sarif ]]; then exec 1>&4 4>&-; render_sarif; fi
|