@nan0web/payload-release 0.0.0-stage → 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/README.md +35 -2
- package/package.json +58 -3
- package/releases/0/1/v0.1.0/task.md +49 -0
- package/releases/0/1/v0.1.0/task.spec.js +156 -0
- package/releases/0/1/v0.1.0/user.md +34 -0
- package/src/admin.jsx +726 -0
- package/src/collections/Releases.js +308 -0
- package/src/domain/ReleaseConfigModel.js +56 -0
- package/src/index.js +166 -0
- package/src/server/discovery.js +205 -0
- package/src/server/endpoint.js +59 -0
- package/src/server/markdown.js +64 -0
- package/types/admin.d.ts +38 -0
- package/types/collections/Releases.d.ts +11 -0
- package/types/domain/ReleaseConfigModel.d.ts +48 -0
- package/types/index.d.ts +27 -0
- package/types/server/discovery.d.ts +49 -0
- package/types/server/endpoint.d.ts +12 -0
- package/types/server/markdown.d.ts +16 -0
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nan0web/payload-release
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Payload CMS 3.x plugin for release tracking, idempotency, execution logs, and Markdown specification preview.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Releases Collection**: Automatic recording of release runs with tabbed navigation:
|
|
8
|
+
- **General**: Status, version, command (`seed`, `fresh`, `migrate`), execution duration, Git SHA, Git Dirty state, Node.js and pnpm versions.
|
|
9
|
+
- **Steps (N)**: Execution steps with duration and output.
|
|
10
|
+
- **Parity (N)**: Data parity check between CSV/source rows and DB records.
|
|
11
|
+
- **Issues (N)**: Structured audit messages (`info`, `warning`, `error`).
|
|
12
|
+
- **Files (N)**: Release artifacts with size, SHA-256 hash, and embedded modal preview.
|
|
13
|
+
- **Embedded Markdown Modal**: Built-in preview for `task.md` and `user.md` directly in the Payload Admin panel.
|
|
14
|
+
- **DB-FS Secure Endpoints**: Read release artifacts without blocking Node.js procedural calls in client components.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @nan0web/payload-release
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { buildConfig } from 'payload'
|
|
26
|
+
import { payloadRelease } from '@nan0web/payload-release'
|
|
27
|
+
|
|
28
|
+
export default buildConfig({
|
|
29
|
+
plugins: [
|
|
30
|
+
payloadRelease({
|
|
31
|
+
releasesDir: 'releases',
|
|
32
|
+
allowPreview: true,
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nan0web/payload-release",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Payload CMS plugin for release tracking, idempotency, execution logs, and Markdown specification preview.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nan0web/payload-cms.git",
|
|
10
|
+
"directory": "packages/payload-release"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/nan0web/payload-cms/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/nan0web/payload-cms/tree/main/packages/payload-release#readme",
|
|
16
|
+
"main": "src/index.js",
|
|
17
|
+
"types": "types/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./types/index.d.ts",
|
|
21
|
+
"import": "./src/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./admin": {
|
|
24
|
+
"types": "./types/admin.d.ts",
|
|
25
|
+
"import": "./src/admin.jsx"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"types",
|
|
31
|
+
"docs",
|
|
32
|
+
"releases",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@payloadcms/ui": ">=3.0.0",
|
|
38
|
+
"payload": ">=3.0.0",
|
|
39
|
+
"react": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@nan0web/types": "^3.4.0",
|
|
43
|
+
"markdown-it": "^14.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"husky": "^9.1.7",
|
|
47
|
+
"knip": "^5.61.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"prebuild": "rm -rf types/",
|
|
51
|
+
"build": "tsc",
|
|
52
|
+
"knip": "pnpm dlx knip --production",
|
|
53
|
+
"test:releases": "node --test --test-timeout=9999 'releases/**/*.spec.js'",
|
|
54
|
+
"test": "node --test --test-timeout=9999 'tests/**/*.test.js' 'src/**/*.test.js' 'releases/**/*.spec.js'",
|
|
55
|
+
"test:e2e": "node --test --test-name-pattern=e2e",
|
|
56
|
+
"test:all": "pnpm test && pnpm build && pnpm knip",
|
|
57
|
+
"pack:check": "pnpm pack --pack-destination .artifacts",
|
|
58
|
+
"pack:local": "pnpm pack --pack-destination .artifacts",
|
|
59
|
+
"release:validate": "dilo validate packages/payload-release"
|
|
60
|
+
}
|
|
6
61
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: done
|
|
3
|
+
type: minor
|
|
4
|
+
epic: EPIC-PAYLOAD-RELEASE
|
|
5
|
+
sprint: Sprint 43
|
|
6
|
+
tracker: https://github.com/nan0web/payload-cms/issues/{id}
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# v0.1.0 — payload-release: Audit, Idempotency & Markdown Preview
|
|
10
|
+
|
|
11
|
+
## Опис та Мета
|
|
12
|
+
Плагін `@nan0web/payload-release` для Payload CMS 3.x, що забезпечує автоматичну фіксацію запусків релізів, контроль версій (Idempotency), вбудований журнал аудиту (Steps, Parity, Issues, Files) та компактний перегляд специфікацій `task.md` і `user.md` у форматі Markdown безпосередньо в інтерфейсі адмін-панелі.
|
|
13
|
+
|
|
14
|
+
## Команда
|
|
15
|
+
- **Lead / Architect:** Олександр Снігірьов (@snigirev)
|
|
16
|
+
- **AI Lead Developer:** Agent Antigravity (@agy)
|
|
17
|
+
- **Auditor:** Security Bot (@sec-bot)
|
|
18
|
+
|
|
19
|
+
## Робочий План та Задачі
|
|
20
|
+
|
|
21
|
+
### 1. Колекція `Releases` з компактним Tabbed Layout
|
|
22
|
+
- [x] [#REL-1] Нативна розбивка форми релізу на вкладки (Tabs): General, Steps, Parity, Issues, Files.
|
|
23
|
+
- [x] [#REL-2] Відображення бейджів кількості в заголовках вкладок (steps, parity, issues, files).
|
|
24
|
+
- [x] [#REL-3] Забезпечення суворого Read-Only доступу в адмін-панелі (заборона create, update, delete, приховування кнопки Create New).
|
|
25
|
+
|
|
26
|
+
### 2. Вбудований Markdown Modal Viewer
|
|
27
|
+
- [x] [#VIEW-1] Реалізація кнопки перегляду (`👁️`) біля артефактів `task.md` та `user.md`.
|
|
28
|
+
- [x] [#VIEW-2] Вбудований модальний переглядач Markdown зі стилями та закриттям за клавішею `Esc` або кнопкою `✕`.
|
|
29
|
+
|
|
30
|
+
### 3. Серверні Ендпоінти та Автономне Читання
|
|
31
|
+
- [x] [#SRV-1] Безпечний Local API / REST ендпоінт для отримання сирого або відрендереного файлу (`GET /api/releases/:id/file?path=...`).
|
|
32
|
+
- [x] [#SRV-2] Захист від path traversal та безпечне читання артефактів релізу.
|
|
33
|
+
|
|
34
|
+
### 4. Zero-Config Plugin Interface та Auto-Discovery
|
|
35
|
+
- [x] [#PLG-1] Підтримка виклику без обов'язкових параметрів `payloadRelease()`.
|
|
36
|
+
- [x] [#PLG-2] Автоматичне виявлення релізів з файлової системи (проєктні релізи та вбудований реліз плагіна) під час ініціалізації (`onInit`).
|
|
37
|
+
|
|
38
|
+
## Критерії Прийнятності (DoD)
|
|
39
|
+
- [x] [#DOD-1] Пакет створено в монорепо `payload-cms/packages/payload-release` з чистою структурою `package.json`, `tsconfig.json` та `@nan0web` залежностями.
|
|
40
|
+
- [x] [#DOD-2] Колекція `releases` реєструється автоматично через плагін `payloadRelease()`.
|
|
41
|
+
- [x] [#DOD-3] Усі масиви полів форми релізу мають бейджі кількості в заголовках вкладок.
|
|
42
|
+
- [x] [#DOD-4] Файли `task.md` та `user.md` відображаються у списку `files` та доступні для читання у вбудованій модалці.
|
|
43
|
+
- [x] [#DOD-5] Забезпечено суворий **Read-Only доступ**: заборонено `create`, `update`, `delete`, кнопка `Create New` прихована.
|
|
44
|
+
- [x] [#DOD-6] Підтримується виклик без параметрів: `payloadRelease()`.
|
|
45
|
+
- [x] [#DOD-7] Релізи автоматично завантажуються із файлової системи (включно із вбудованим демонстраційним релізом `v0.1.0`).
|
|
46
|
+
- [x] [#DOD-8] Оновлено контрактні тести `task.spec.js` для верифікації прав доступу та автозавантаження.
|
|
47
|
+
- [x] [#DOD-9] Пройдено інспекцію без викликів `console.warn` / `console.error`.
|
|
48
|
+
|
|
49
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
payloadRelease,
|
|
6
|
+
ReleaseConfigModel,
|
|
7
|
+
createReleaseFileEndpoint,
|
|
8
|
+
discoverReleases,
|
|
9
|
+
parseMarkdownCheckboxes,
|
|
10
|
+
} from '../../../../src/index.js'
|
|
11
|
+
|
|
12
|
+
describe('payload-release v0.1.0 contract', () => {
|
|
13
|
+
it('exports valid plugin function and ReleaseConfigModel', () => {
|
|
14
|
+
assert.equal(typeof payloadRelease, 'function')
|
|
15
|
+
assert.ok(ReleaseConfigModel)
|
|
16
|
+
assert.equal(typeof ReleaseConfigModel.releasesDir, 'object')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('configures Releases collection with tabs, badges and strict read-only access', () => {
|
|
20
|
+
const plugin = payloadRelease({
|
|
21
|
+
releasesDir: 'releases',
|
|
22
|
+
allowPreview: true,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const config = plugin({ collections: [] })
|
|
26
|
+
assert.ok(Array.isArray(config.collections))
|
|
27
|
+
const releasesCol = config.collections.find((c) => c.slug === 'releases')
|
|
28
|
+
assert.ok(releasesCol, 'Releases collection must be registered')
|
|
29
|
+
assert.equal(releasesCol.admin?.useAsTitle, 'version')
|
|
30
|
+
|
|
31
|
+
// Verify strict Read-Only access controls
|
|
32
|
+
assert.ok(releasesCol.access, 'Access configuration must exist')
|
|
33
|
+
assert.equal(typeof releasesCol.access.create, 'function')
|
|
34
|
+
assert.equal(releasesCol.access.create(), false, 'create access must be forbidden')
|
|
35
|
+
assert.equal(typeof releasesCol.access.update, 'function')
|
|
36
|
+
assert.equal(releasesCol.access.update(), false, 'update access must be forbidden')
|
|
37
|
+
assert.equal(typeof releasesCol.access.delete, 'function')
|
|
38
|
+
assert.equal(releasesCol.access.delete(), false, 'delete access must be forbidden')
|
|
39
|
+
assert.equal(typeof releasesCol.access.read, 'function')
|
|
40
|
+
assert.equal(releasesCol.access.read(), true, 'read access must be allowed')
|
|
41
|
+
|
|
42
|
+
// Check tabs layout
|
|
43
|
+
const tabsField = releasesCol.fields.find((f) => f.type === 'tabs')
|
|
44
|
+
assert.ok(tabsField, 'Tabs field must exist')
|
|
45
|
+
assert.ok(Array.isArray(tabsField.tabs))
|
|
46
|
+
assert.equal(tabsField.tabs.length, 5)
|
|
47
|
+
|
|
48
|
+
const resolvedLabels = tabsField.tabs.map((t) => {
|
|
49
|
+
if (typeof t.label === 'function') {
|
|
50
|
+
return t.label({ data: { steps: [{}], parity: [{}], issues: [{}], files: [{}] } })
|
|
51
|
+
}
|
|
52
|
+
return t.name || t.label
|
|
53
|
+
})
|
|
54
|
+
assert.ok(resolvedLabels.some((n) => typeof n === 'string' && n.includes('General')))
|
|
55
|
+
assert.ok(resolvedLabels.some((n) => typeof n === 'string' && n.includes('Steps')))
|
|
56
|
+
assert.ok(resolvedLabels.some((n) => typeof n === 'string' && n.includes('Parity')))
|
|
57
|
+
assert.ok(resolvedLabels.some((n) => typeof n === 'string' && n.includes('Issues')))
|
|
58
|
+
assert.ok(resolvedLabels.some((n) => typeof n === 'string' && n.includes('Files')))
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('supports zero-config invocation without options', () => {
|
|
62
|
+
const plugin = payloadRelease()
|
|
63
|
+
assert.equal(typeof plugin, 'function')
|
|
64
|
+
|
|
65
|
+
const config = plugin({ collections: [], endpoints: [] })
|
|
66
|
+
assert.ok(config.collections.some((c) => c.slug === 'releases'))
|
|
67
|
+
assert.ok(Array.isArray(config.endpoints))
|
|
68
|
+
assert.equal(typeof config.onInit, 'function')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('registers release file reading endpoint', () => {
|
|
72
|
+
const plugin = payloadRelease()
|
|
73
|
+
const config = plugin({ collections: [], endpoints: [] })
|
|
74
|
+
assert.ok(Array.isArray(config.endpoints))
|
|
75
|
+
const fileEndpoint = config.endpoints.find(
|
|
76
|
+
(e) => e.path === '/api/releases/:id/file' || e.path === '/releases/:id/file'
|
|
77
|
+
)
|
|
78
|
+
assert.ok(fileEndpoint, 'Release file endpoint must be registered')
|
|
79
|
+
assert.equal(fileEndpoint.method, 'get')
|
|
80
|
+
assert.equal(typeof fileEndpoint.handler, 'function')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('safely serves file content and markdown rendering via endpoint handler', async () => {
|
|
84
|
+
const handler = createReleaseFileEndpoint({
|
|
85
|
+
releasesDir: 'releases',
|
|
86
|
+
readReleaseFile: async (relPath) => {
|
|
87
|
+
if (relPath.includes('task.md')) {
|
|
88
|
+
return '# Task Spec\n\n- [x] Done item\n- [ ] Todo item'
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const req = {
|
|
95
|
+
url: 'http://payload.local/api/releases/123/file?path=0/1/v0.1.0/task.md',
|
|
96
|
+
routeParams: { id: '123' },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const res = await handler(req)
|
|
100
|
+
assert.equal(res.status, 200)
|
|
101
|
+
const data = await res.json()
|
|
102
|
+
assert.ok(data.found)
|
|
103
|
+
assert.match(data.raw, /# Task Spec/)
|
|
104
|
+
assert.match(data.html, /<h1>Task Spec<\/h1>/)
|
|
105
|
+
assert.match(data.html, /Done item/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('rejects path traversal attempts in file endpoint', async () => {
|
|
109
|
+
const handler = createReleaseFileEndpoint({
|
|
110
|
+
releasesDir: 'releases',
|
|
111
|
+
readReleaseFile: async () => 'secret',
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const req = {
|
|
115
|
+
url: 'http://payload.local/api/releases/123/file?path=../../etc/passwd',
|
|
116
|
+
routeParams: { id: '123' },
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const res = await handler(req)
|
|
120
|
+
assert.equal(res.status, 400)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('automatically discovers releases and parses task checkboxes from filesystem', async () => {
|
|
124
|
+
const releases = await discoverReleases()
|
|
125
|
+
assert.ok(Array.isArray(releases))
|
|
126
|
+
assert.ok(releases.length >= 1)
|
|
127
|
+
|
|
128
|
+
const v010 = releases.find((r) => r.version === 'v0.1.0')
|
|
129
|
+
assert.ok(v010, 'v0.1.0 release must be discovered')
|
|
130
|
+
assert.equal(v010.status, 'ok')
|
|
131
|
+
assert.ok(Array.isArray(v010.files))
|
|
132
|
+
assert.ok(v010.files.some((f) => f.path.endsWith('task.md')))
|
|
133
|
+
assert.ok(v010.files.some((f) => f.path.endsWith('user.md')))
|
|
134
|
+
assert.ok(Array.isArray(v010.steps))
|
|
135
|
+
assert.ok(v010.steps.length > 0)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('synchronizes discovered releases in onInit hook', async () => {
|
|
139
|
+
const plugin = payloadRelease()
|
|
140
|
+
const config = plugin({ collections: [] })
|
|
141
|
+
|
|
142
|
+
const created = []
|
|
143
|
+
const mockPayload = {
|
|
144
|
+
find: async () => ({ docs: [] }),
|
|
145
|
+
create: async ({ collection, data }) => {
|
|
146
|
+
created.push({ collection, data })
|
|
147
|
+
return data
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await config.onInit(mockPayload)
|
|
152
|
+
assert.ok(created.length >= 1)
|
|
153
|
+
assert.equal(created[0].collection, 'releases')
|
|
154
|
+
assert.equal(created[0].data.version, 'v0.1.0')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# user metrics — payload-release v0.1.0
|
|
2
|
+
|
|
3
|
+
## Метрики Виконання
|
|
4
|
+
- **Цільова версія:** v0.1.0
|
|
5
|
+
- **Статус:** done
|
|
6
|
+
- **Витрачений час розробки (Годин):** 2.8
|
|
7
|
+
- **Кількість ітерацій:** 4
|
|
8
|
+
- **Фінальний бал RRS:** 345
|
|
9
|
+
|
|
10
|
+
## Опис Реалізованих Задач (Task Log)
|
|
11
|
+
- **[#REL-1, #REL-2] Tabbed Layout & Badges:** Форму колекції `Releases` розбито на компактні вкладки із динамічними бейджами кількості елементів (`steps`, `parity`, `issues`, `files`).
|
|
12
|
+
- **[#REL-3, #DOD-5] Суворий Read-Only доступ:** Встановлено заборону створення (`create: false`), редагування (`update: false`) та видалення (`delete: false`). Кнопку `Create New` приховано.
|
|
13
|
+
- **[#VIEW-1, #VIEW-2] Вбудований Markdown Modal Viewer:** Додано інтерактивну кнопку перегляду (`👁️`) біля артефактів `task.md` та `user.md` із відкриттям модального вікна та підтримкою `Esc` / `✕`.
|
|
14
|
+
- **[#SRV-1, #SRV-2] Безпечні REST/Local API Ендпоінти:** Реалізовано обробник `/api/releases/:id/file` з обов'язковою санітизацією шляхів від path traversal.
|
|
15
|
+
- **[#PLG-1, #PLG-2] Zero-Config Plugin & Auto-Discovery:** Плагін підключається через `payloadRelease()` без аргументів і автоматично синхронізує релізи під час старту.
|
|
16
|
+
|
|
17
|
+
## Журнал Зворотного Зв'язку (Feedback & Notes)
|
|
18
|
+
- **Настанова Архітектора від 14.10.2026:**
|
|
19
|
+
1. Колекція `releases` у Payload CMS призначена **виключно для перегляду**.
|
|
20
|
+
2. Плагін повинен запускатися максимально просто: `payloadRelease()` без зайвих конфігурацій.
|
|
21
|
+
3. Релізи мають надходити автоматично із файлової системи / внутрішнього сховища і одразу бути доступними у списку адмінки.
|
|
22
|
+
4. Клієнтський інтерфейс адмінки залишається стандартним нативним клієнтом Payload CMS.
|
|
23
|
+
|
|
24
|
+
## Статус та Послідовність Кроків (Pipeline)
|
|
25
|
+
- [x] **Крок B (Виконано):** Інтеграція та сумісність з опублікованими `@nan0web/release@3.5.0` та `@nan0web/release.app@0.3.0`.
|
|
26
|
+
- Релізні документи приведені до PMaC-стандарту.
|
|
27
|
+
- Валідатор `dilo validate packages/payload-release` повертає **PASSED ✅** (100% валідність, 9/9 завдань виконано).
|
|
28
|
+
- Скрипт `pnpm run release:validate` додано у `package.json`.
|
|
29
|
+
- `pnpm test:all` (контрактні тести, типи, knip) зелений.
|
|
30
|
+
- [x] **Крок A (Виконано):** Інтеграційне тестування та перевірка в `testing-app` (підключення плагіна `payloadRelease()`, реєстрація колекції `releases`, генерація importMap, ручна візуальна перевірка адмінки).
|
|
31
|
+
- [x] **Крок C (Готово до релізу):** Усі контракти, типи та валідація пакету `dilo validate` пройдені успішно. Пакунок готовий до публікації `@nan0web/payload-release@0.1.0`.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|