@readystack/target-api-36-store-deadline-lint 0.1.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/LICENSE.txt +2 -0
- package/README.md +35 -0
- package/cli.js +74 -0
- package/license.js +63 -0
- package/package.json +32 -0
- package/rules.json +114 -0
- package/strings.json +10 -0
package/LICENSE.txt
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Target API 36 & Xcode 26 Store Deadline Lint
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Finds the targetSdk, NDK, Gradle plugin and Xcode versions in your repo that Google Play and App Store Connect already refuse, with the date each one stopped being accepted.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx @readystack/target-api-36-store-deadline-lint file
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 18+. The same 16 rules as the VS Code extension, from a terminal or CI.
|
|
14
|
+
|
|
15
|
+
## Free
|
|
16
|
+
|
|
17
|
+
- Checks the build file you have open against all 16 dated rules and names every version the stores already refuse, with the date and the version to move to - no key, no limit, no watermark.
|
|
18
|
+
- `--rules` lists every rule
|
|
19
|
+
|
|
20
|
+
## With a licence ($29 once)
|
|
21
|
+
|
|
22
|
+
- Scans every build file in the repository, re-checks on save, fails the CI build before a blocked version can merge, and exports a dated release-readiness report.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
@readystack/target-api-36-store-deadline-lint --dir ./templates --report html --out report.html
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Google Play's extension window for the API 36 requirement closes 2026-11-01; freelance market rates put a senior Android developer near $128 an hour.
|
|
29
|
+
|
|
30
|
+
7-day refund, no questions. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
31
|
+
|
|
32
|
+
[Get a licence](https://buy.polar.sh/polar_cl_vq5XThHMGTzYpT8dmUA38JFX8y3rRSnEzoY8U3wn2Hx)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
<!-- target api level 36 deadline -->
|
package/cli.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs'), path = require('path');
|
|
5
|
+
const RULES = require('./rules.json');
|
|
6
|
+
const S = require('./strings.json');
|
|
7
|
+
const lic = require('./license.js');
|
|
8
|
+
function scan(text) {
|
|
9
|
+
const lines = String(text).split(/\r?\n/); const hits = [];
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
for (const r of RULES) {
|
|
12
|
+
let re; try { re = new RegExp(r.pattern, r.flags || ''); } catch (e) { continue; }
|
|
13
|
+
if (re.test(lines[i])) hits.push({ line: i + 1, msg: r.message, fix: r.fix || null, sev: r.sev || 'warn' });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return hits;
|
|
17
|
+
}
|
|
18
|
+
function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
|
|
19
|
+
function walk(dir, exts, out) {
|
|
20
|
+
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
21
|
+
for (const e of ents) {
|
|
22
|
+
if (e.name === 'node_modules' || e.name === '.git' || e.name.startsWith('.')) continue;
|
|
23
|
+
const p = path.join(dir, e.name);
|
|
24
|
+
if (e.isDirectory()) walk(p, exts, out);
|
|
25
|
+
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
30
|
+
function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
|
|
31
|
+
function render(rows, fmt) {
|
|
32
|
+
if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULES.length, files: rows }, null, 1);
|
|
33
|
+
if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
|
|
34
|
+
if (fmt === 'html') {
|
|
35
|
+
const n = rows.reduce((a, r) => a + r.hits.length, 0);
|
|
36
|
+
let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
|
|
37
|
+
o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULES.length + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
|
|
38
|
+
for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
|
|
39
|
+
return o + '</table>';
|
|
40
|
+
}
|
|
41
|
+
let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
|
|
42
|
+
return o;
|
|
43
|
+
}
|
|
44
|
+
function help() {
|
|
45
|
+
return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
|
|
46
|
+
' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
|
|
47
|
+
' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
48
|
+
' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
49
|
+
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
50
|
+
' ' + S.bin + ' --rules list the ' + RULES.length + ' rules', '',
|
|
51
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once, 7-day refund): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
52
|
+
}
|
|
53
|
+
(async function main() {
|
|
54
|
+
try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { for (const r of _feed.rules) RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
|
|
55
|
+
const a = process.argv.slice(2);
|
|
56
|
+
const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
|
|
57
|
+
if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
|
|
58
|
+
if (a.includes('--rules')) { process.stdout.write(RULES.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + r.message).join('\n') + '\n'); return; }
|
|
59
|
+
if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
|
|
60
|
+
const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
|
|
61
|
+
const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
|
|
62
|
+
const paid = !!(dir || fmt || ci);
|
|
63
|
+
if (paid) {
|
|
64
|
+
const r = await lic.ensure();
|
|
65
|
+
if (!r.ok) { process.stderr.write(S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ', once): ' + lic.BUY_URL + '\n'); process.exit(2); }
|
|
66
|
+
}
|
|
67
|
+
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
68
|
+
if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
|
|
69
|
+
const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
|
|
70
|
+
const text = render(rows, fmt || 'text');
|
|
71
|
+
if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
72
|
+
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
73
|
+
if (ci && errors) process.exit(1);
|
|
74
|
+
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
package/license.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
|
|
2
|
+
'use strict';
|
|
3
|
+
const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
|
|
4
|
+
const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
|
|
5
|
+
const BUY_URL = 'https://buy.polar.sh/polar_cl_vq5XThHMGTzYpT8dmUA38JFX8y3rRSnEzoY8U3wn2Hx';
|
|
6
|
+
const SLUG = 'target-api-36-store-deadline-lint';
|
|
7
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
8
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
9
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
10
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
11
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
12
|
+
function validate(key) {
|
|
13
|
+
return new Promise(function (resolve) {
|
|
14
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
15
|
+
const body = JSON.stringify({ key: key, organization_id: ORG_ID });
|
|
16
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
17
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
18
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
19
|
+
res.on('end', function () {
|
|
20
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
21
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
22
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
26
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
27
|
+
req.write(body); req.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function ensure(explicitKey) {
|
|
31
|
+
const st = load();
|
|
32
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
33
|
+
if (!key) return { ok: false, why: 'no_key' };
|
|
34
|
+
const age = Date.now() - (st.okAt || 0);
|
|
35
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
36
|
+
const r = await validate(String(key).trim());
|
|
37
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
38
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
39
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
40
|
+
}
|
|
41
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
42
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
43
|
+
function pullFeed() {
|
|
44
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
45
|
+
if (!key) return Promise.resolve(cached);
|
|
46
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
47
|
+
return new Promise(function (resolve) {
|
|
48
|
+
let req;
|
|
49
|
+
try {
|
|
50
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
51
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
52
|
+
res.on('end', function () {
|
|
53
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
54
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
55
|
+
catch (e) { resolve(cached); }
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} catch (e) { return resolve(cached); }
|
|
59
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
60
|
+
req.on('error', function () { resolve(cached); });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/target-api-36-store-deadline-lint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Finds the targetSdk, NDK, Gradle plugin and Xcode versions in your repo that Google Play and App Store Connect already refuse, with the date each one stopped being accepted.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"android",
|
|
11
|
+
"targetsdk",
|
|
12
|
+
"xcode",
|
|
13
|
+
"react-native",
|
|
14
|
+
"expo"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://getreadystack.com",
|
|
17
|
+
"funding": "https://buy.polar.sh/polar_cl_vq5XThHMGTzYpT8dmUA38JFX8y3rRSnEzoY8U3wn2Hx",
|
|
18
|
+
"bin": {
|
|
19
|
+
"target-api-36-store-deadline-lint": "cli.js"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"cli.js",
|
|
26
|
+
"license.js",
|
|
27
|
+
"rules.json",
|
|
28
|
+
"strings.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE.txt"
|
|
31
|
+
]
|
|
32
|
+
}
|
package/rules.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"pattern": "targetSdk(?:Version)?\\s*(?:=|:|\\s)\\s*[\"']?(?:1[4-9]|2[0-9]|3[0-4])\\b",
|
|
4
|
+
"flags": "",
|
|
5
|
+
"sev": "error",
|
|
6
|
+
"message": "targetSdk below 35. Google Play stopped accepting updates without API 36 on 2026-08-31, and at this level your listing is already hidden from new users whose device runs a newer Android than you target. Extension requests close 2026-11-01.",
|
|
7
|
+
"fix": "targetSdk 36"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"pattern": "targetSdk(?:Version)?\\s*(?:=|:|\\s)\\s*[\"']?35\\b",
|
|
11
|
+
"flags": "",
|
|
12
|
+
"sev": "error",
|
|
13
|
+
"message": "targetSdk 35 (Android 15). Google Play has rejected updates without API 36 since 2026-08-31. You can request an extension in Play Console until 2026-11-01; after that no update ships at all.",
|
|
14
|
+
"fix": "targetSdk 36"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"pattern": "compileSdk(?:Version)?\\s*(?:=|:|\\s)\\s*[\"']?(?:2[0-9]|3[0-5])\\b",
|
|
18
|
+
"flags": "",
|
|
19
|
+
"sev": "error",
|
|
20
|
+
"message": "compileSdk below 36. You cannot compile against Android 16, and Gradle refuses a compileSdk lower than targetSdk.",
|
|
21
|
+
"fix": "compileSdk 36"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"pattern": "buildToolsVersion\\s*(?:=|:|\\s)\\s*[\"'](?:2[0-9]|3[0-5])\\.",
|
|
25
|
+
"flags": "",
|
|
26
|
+
"sev": "warn",
|
|
27
|
+
"message": "Build tools below 36.0.0 cannot build API 36 sources.",
|
|
28
|
+
"fix": "buildToolsVersion \"36.0.0\""
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"pattern": "flutter\\.(?:target|compile)SdkVersion",
|
|
32
|
+
"flags": "",
|
|
33
|
+
"sev": "warn",
|
|
34
|
+
"message": "This level is inherited from whichever Flutter SDK the build machine has, not from this file, so it can differ between your laptop and CI. Pin it here.",
|
|
35
|
+
"fix": "targetSdk = 36"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"pattern": "ndkVersion\\s*(?:=|:|\\s)\\s*[\"'](?:1[0-9]|2[0-7])\\.",
|
|
39
|
+
"flags": "",
|
|
40
|
+
"sev": "error",
|
|
41
|
+
"message": "NDK below r28 does not align native libraries to 16 KB. Google Play has blocked releases containing native code that target Android 15 or higher since 2025-11-01.",
|
|
42
|
+
"fix": "NDK r28 or newer"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"pattern": "com\\.android\\.tools\\.build:gradle:(?:[0-7]\\.|8\\.[0-4]\\b)",
|
|
46
|
+
"flags": "",
|
|
47
|
+
"sev": "error",
|
|
48
|
+
"message": "Android Gradle Plugin below 8.5.1 does not produce 16 KB-aligned native libraries, which Google Play has required since 2025-11-01.",
|
|
49
|
+
"fix": "com.android.tools.build:gradle:8.5.1 or newer"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"pattern": "windowOptOutEdgeToEdgeEnforcement",
|
|
53
|
+
"flags": "",
|
|
54
|
+
"sev": "warn",
|
|
55
|
+
"message": "Android 16 removes this opt-out. At targetSdk 36 the flag is ignored and the app draws edge-to-edge, so content can sit under the system bars.",
|
|
56
|
+
"fix": "Handle window insets instead of opting out"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"pattern": "\"react-native\"\\s*:\\s*\"[^\"]*0\\.(?:[0-6][0-9]|7[0-9]|80)\\b",
|
|
60
|
+
"flags": "",
|
|
61
|
+
"sev": "warn",
|
|
62
|
+
"message": "React Native below 0.81 defaults to targetSdk 35. Android 16 support landed in 0.81.",
|
|
63
|
+
"fix": "react-native 0.81 or newer"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"pattern": "\"expo\"\\s*:\\s*\"[^\"]*(?:4[0-9]|5[0-3])\\.",
|
|
67
|
+
"flags": "",
|
|
68
|
+
"sev": "warn",
|
|
69
|
+
"message": "Expo SDK below 54 defaults to targetSdk 35. Either upgrade, or set compileSdkVersion and targetSdkVersion to 36 through expo-build-properties.",
|
|
70
|
+
"fix": "expo 54 or newer"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"pattern": "android\\.hardware\\.type\\.(?:watch|television|automotive)",
|
|
74
|
+
"flags": "",
|
|
75
|
+
"sev": "info",
|
|
76
|
+
"message": "Wear OS, Android TV and Automotive run on their own target-level timetable, so the 2026-08-31 API 36 requirement does not apply to this form factor the same way. Confirm this track in Play Console.",
|
|
77
|
+
"fix": "Check the form-factor track in Play Console"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"pattern": "xcode[-_]?version\\s*:\\s*[\"']?(?:1[0-6]|[0-9])\\b",
|
|
81
|
+
"flags": "i",
|
|
82
|
+
"sev": "error",
|
|
83
|
+
"message": "CI is pinned to an Xcode below 26. App Store Connect has rejected uploads not built with the iOS 26 SDK since 2026-04-28 (ITMS-90725).",
|
|
84
|
+
"fix": "xcode-version: '26.0'"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"pattern": "runs-on\\s*:\\s*.*macos-(?:11|12|13|14)\\b",
|
|
88
|
+
"flags": "i",
|
|
89
|
+
"sev": "error",
|
|
90
|
+
"message": "This runner image does not ship Xcode 26, so the archive it produces cannot be uploaded to App Store Connect after 2026-04-28.",
|
|
91
|
+
"fix": "A runner image that ships Xcode 26"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"pattern": "Xcode_?(?:1[0-6]|[0-9])(?:\\.[0-9]+)*\\.app",
|
|
95
|
+
"flags": "",
|
|
96
|
+
"sev": "error",
|
|
97
|
+
"message": "The build points at an Xcode below 26. Uploads built with an older SDK have been rejected since 2026-04-28.",
|
|
98
|
+
"fix": "/Applications/Xcode_26.app"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"pattern": "xc(?:odes|version)\\s*\\(\\s*version\\s*:\\s*[\"'](?:1[0-6]|[0-9])",
|
|
102
|
+
"flags": "",
|
|
103
|
+
"sev": "error",
|
|
104
|
+
"message": "Fastlane selects an Xcode below 26, so the resulting build cannot be uploaded to App Store Connect since 2026-04-28.",
|
|
105
|
+
"fix": "version: '26.0'"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"pattern": "platform\\s*:ios\\s*,\\s*[\"'](?:[89]|1[0-4])(?:\\.[0-9]+)?[\"']",
|
|
109
|
+
"flags": "",
|
|
110
|
+
"sev": "warn",
|
|
111
|
+
"message": "Deployment target below iOS 15. Not a store block by itself, but confirm the project still builds under the iOS 26 SDK that uploads have required since 2026-04-28.",
|
|
112
|
+
"fix": "Confirm the build under Xcode 26"
|
|
113
|
+
}
|
|
114
|
+
]
|
package/strings.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Target API 36 & Xcode 26 Store Deadline Lint",
|
|
3
|
+
"subtitle": "Finds the targetSdk, NDK, Gradle plugin and Xcode versions in your repo that Google Play and App Store Connect already refuse, with the date each one stopped being accepted.",
|
|
4
|
+
"bin": "target-api-36-store-deadline-lint",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Checks the build file you have open against all 16 dated rules and names every version the stores already refuse, with the date and the version to move to - no key, no limit, no watermark.",
|
|
7
|
+
"paid": "Scans every build file in the repository, re-checks on save, fails the CI build before a blocked version can merge, and exports a dated release-readiness report.",
|
|
8
|
+
"need_key": "This option needs a licence (Target API 36 & Xcode 26 Store Deadline Lint).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|