@nitra/cursor 1.8.138 → 1.8.140

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/mdc/abie.mdc CHANGED
@@ -222,7 +222,7 @@ patches:
222
222
  path: /spec/template/spec/containers/-
223
223
  value:
224
224
  name: СЕРВІС-p
225
- image: nginx:alpine
225
+ image: nginx:alpine-slim
226
226
  ports:
227
227
  - containerPort: 8081
228
228
  protocol: TCP
package/mdc/docker.mdc CHANGED
@@ -21,6 +21,10 @@ alwaysApply: false
21
21
 
22
22
  Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) — цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
23
23
 
24
+ ## Мінімальні образи
25
+
26
+ Якщо використовується nginx, то намагайся щоб тег був `alpine-slim`
27
+
24
28
  ## компіляція
25
29
 
26
30
  Якщо проект має bun install крок, та не є фронтенд проектом (тобто не має bun build крок), то потрібно щоб була компіляція коду, і далі у фінальному образі був тільки бінарник і Цей образ не містив компілятора, npm, Bun — тільки runtime libs. Наприклад:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.138",
3
+ "version": "1.8.140",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -9,7 +9,8 @@
9
9
  * - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
10
10
  * містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
11
11
  * `mirror.gcr.io/library/python:*`
12
- * - frontend: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
12
+ * - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` (preferred) або `mirror.gcr.io/library/nginx:*`,
13
+ * `mirror.gcr.io/openresty/openresty:*`
13
14
  *
14
15
  * Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
15
16
  * то очікується компіляція в один бінарник через `bun build --compile` у build stage, а у
@@ -18,6 +19,11 @@
18
19
  * Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
19
20
  * дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
20
21
  *
22
+ * Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`, `mirror.gcr.io/library/nginx`) у
23
+ * будь-якому `FROM` очікується тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
24
+ * `alpine` / інші. `nginx-unprivileged` запускається від не-root користувача (uid=101) без явного
25
+ * `USER` у Dockerfile — перевірка non-root для нього пропускається.
26
+ *
21
27
  * Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
22
28
  * тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
23
29
  * Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
@@ -36,6 +42,9 @@ const BUN_BUILD_COMPILE_RE = /\bbun\s+build\b[^\n]*\s--compile\b/iu
36
42
  const BUN_WORD_RE = /\bbun\b/iu
37
43
  const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
38
44
 
45
+ const NGINX_MIRROR_PREFIX = 'mirror.gcr.io/library/nginx'
46
+ const NGINX_UNPRIVILEGED_MIRROR_PREFIX = 'mirror.gcr.io/nginxinc/nginx-unprivileged'
47
+
39
48
  /**
40
49
  * @typedef {{
41
50
  * line: number
@@ -89,6 +98,7 @@ const RUNTIME_IMAGES = /** @type {const} */ ([
89
98
  'mirror.gcr.io/library/php',
90
99
  'mirror.gcr.io/library/python',
91
100
  'mirror.gcr.io/library/nginx',
101
+ 'mirror.gcr.io/nginxinc/nginx-unprivileged',
92
102
  'mirror.gcr.io/openresty/openresty'
93
103
  ])
94
104
 
@@ -98,7 +108,7 @@ const DEBIAN_VIA_MIRROR_RE = /^mirror\.gcr\.io\/library\/debian:(.+)$/i
98
108
  /**
99
109
  * Чи ref фінального `FROM` відповідає дозволеним у docker.mdc (multistage / runtime).
100
110
  * @param {string} lastLower ref без digest, lower case
101
- * @returns {boolean}
111
+ * @returns {boolean} true, якщо образ дозволений як фінальний runtime
102
112
  */
103
113
  function isAllowedFinalRuntimeImage(lastLower) {
104
114
  if (lastLower === 'scratch' || lastLower.startsWith('scratch:')) {
@@ -184,7 +194,9 @@ export function getBunCompileHint(fileContent) {
184
194
  const hasBunInstall = BUN_INSTALL_RE.test(fileContent)
185
195
  const isFinalAlpine = lastLower.startsWith('mirror.gcr.io/library/alpine:')
186
196
  const isFinalFrontend =
187
- lastLower.startsWith('mirror.gcr.io/library/nginx:') || lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
197
+ lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
198
+ lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`) ||
199
+ lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
188
200
 
189
201
  if (!hasBunInstall) return null
190
202
  if (!isFinalAlpine) return null
@@ -203,6 +215,32 @@ export function getBunCompileHint(fileContent) {
203
215
  return null
204
216
  }
205
217
 
218
+ /**
219
+ * Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged` або
220
+ * `mirror.gcr.io/library/nginx`) у `FROM` вказано тег `alpine-slim` (docker.mdc).
221
+ *
222
+ * @param {string} fileContent вміст Dockerfile/Containerfile
223
+ * @returns {string | null} повідомлення помилки або null
224
+ */
225
+ export function getNginxAlpineSlimTagHint(fileContent) {
226
+ const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX, NGINX_MIRROR_PREFIX]
227
+ for (const { line, image } of parseFromStages(fileContent)) {
228
+ const noDigest = (image.split('@')[0] || '').trim()
229
+ const d = noDigest.toLowerCase()
230
+ const prefix = prefixes.find(p => d.startsWith(`${p}:`) || d === p)
231
+ if (prefix) {
232
+ if (d === prefix) {
233
+ return `рядок ${line}: \`FROM ${prefix}\` має явний тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз без тега (типово latest)`
234
+ }
235
+ const tag = noDigest.slice(prefix.length + 1)
236
+ if (tag.toLowerCase() !== 'alpine-slim') {
237
+ return `рядок ${line}: для nginx потрібен тег \`alpine-slim\` (docker.mdc: мінімальні образи), зараз: \`${tag}\``
238
+ }
239
+ }
240
+ }
241
+ return null
242
+ }
243
+
206
244
  /**
207
245
  * Перевіряє вимогу "non-root" у фінальному runtime stage (docker.mdc).
208
246
  *
@@ -218,6 +256,8 @@ export function getNonRootRuntimeHint(fileContent) {
218
256
  if (stages.length === 0) return null
219
257
 
220
258
  const last = stages.at(-1)
259
+ const lastImage = (last?.from.image || '').split('@')[0] || ''
260
+ const lastLower = lastImage.toLowerCase()
221
261
  const lastStageContent = last?.stageContent || ''
222
262
 
223
263
  /** @type {string | null} */
@@ -230,6 +270,8 @@ export function getNonRootRuntimeHint(fileContent) {
230
270
  }
231
271
 
232
272
  if (!lastUserToken) {
273
+ // nginx-unprivileged вже запускається від uid=101 (nginx) без явного USER у Dockerfile
274
+ if (lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`)) return null
233
275
  return 'у фінальному stage має бути `USER <non-root>` (наприклад `USER app`) — принцип non-root (docker.mdc: не превілейований образ)'
234
276
  }
235
277
 
@@ -282,6 +324,11 @@ export async function check() {
282
324
  fail(`${rel} (non-root): ${nonRootHint}`)
283
325
  }
284
326
 
327
+ const nginxSlimHint = getNginxAlpineSlimTagHint(content)
328
+ if (nginxSlimHint) {
329
+ fail(`${rel} (nginx tag): ${nginxSlimHint}`)
330
+ }
331
+
285
332
  const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
286
333
  const tail = (stdout + stderr).trim()
287
334
  if (ok) {
@@ -110,14 +110,15 @@ export function normalizeHubRepoPath(imageToken) {
110
110
  }
111
111
 
112
112
  const HUB_REPOS_REQUIRING_MIRROR = /** @type {const} */ (
113
- new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node'])
113
+ new Set(['oven/bun', 'library/alpine', 'library/nginx', 'library/node', 'nginxinc/nginx-unprivileged'])
114
114
  )
115
115
 
116
116
  const EXPECTED_MIRROR = /** @type {const} */ ({
117
117
  'oven/bun': 'mirror.gcr.io/oven/bun',
118
118
  'library/alpine': 'mirror.gcr.io/library/alpine',
119
119
  'library/nginx': 'mirror.gcr.io/library/nginx',
120
- 'library/node': 'mirror.gcr.io/library/node'
120
+ 'library/node': 'mirror.gcr.io/library/node',
121
+ 'nginxinc/nginx-unprivileged': 'mirror.gcr.io/nginxinc/nginx-unprivileged'
121
122
  })
122
123
 
123
124
  /**