@serve.zone/gitops 2.13.1
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/.smartconfig.json +114 -0
- package/binary/gitops.ts +4 -0
- package/changelog.md +185 -0
- package/cli.child.js +4 -0
- package/cli.js +4 -0
- package/cli.ts.js +5 -0
- package/deno.json +10 -0
- package/dist_serve/bundle.js +36362 -0
- package/dist_serve/index.html +33 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/cache/classes.cache.cleaner.d.ts +23 -0
- package/dist_ts/cache/classes.cache.cleaner.js +61 -0
- package/dist_ts/cache/classes.cached.document.d.ts +30 -0
- package/dist_ts/cache/classes.cached.document.js +101 -0
- package/dist_ts/cache/classes.cachedb.d.ts +22 -0
- package/dist_ts/cache/classes.cachedb.js +58 -0
- package/dist_ts/cache/classes.secrets.scan.service.d.ts +51 -0
- package/dist_ts/cache/classes.secrets.scan.service.js +237 -0
- package/dist_ts/cache/documents/classes.cached.project.d.ts +13 -0
- package/dist_ts/cache/documents/classes.cached.project.js +101 -0
- package/dist_ts/cache/documents/classes.cached.secret.d.ts +24 -0
- package/dist_ts/cache/documents/classes.cached.secret.js +158 -0
- package/dist_ts/cache/documents/index.d.ts +2 -0
- package/dist_ts/cache/documents/index.js +3 -0
- package/dist_ts/cache/index.d.ts +7 -0
- package/dist_ts/cache/index.js +6 -0
- package/dist_ts/classes/actionlog.d.ts +19 -0
- package/dist_ts/classes/actionlog.js +44 -0
- package/dist_ts/classes/connectionmanager.d.ts +57 -0
- package/dist_ts/classes/connectionmanager.js +247 -0
- package/dist_ts/classes/gitopsapp.d.ts +30 -0
- package/dist_ts/classes/gitopsapp.js +101 -0
- package/dist_ts/classes/jobmanager.d.ts +47 -0
- package/dist_ts/classes/jobmanager.js +301 -0
- package/dist_ts/classes/jobrunners/autobookstackdocs.runner.d.ts +29 -0
- package/dist_ts/classes/jobrunners/autobookstackdocs.runner.js +361 -0
- package/dist_ts/classes/jobrunners/base.jobrunner.d.ts +14 -0
- package/dist_ts/classes/jobrunners/base.jobrunner.js +3 -0
- package/dist_ts/classes/jobrunners/index.d.ts +5 -0
- package/dist_ts/classes/jobrunners/index.js +14 -0
- package/dist_ts/classes/managedsecrets.manager.d.ts +47 -0
- package/dist_ts/classes/managedsecrets.manager.js +247 -0
- package/dist_ts/classes/syncmanager.d.ts +189 -0
- package/dist_ts/classes/syncmanager.js +1787 -0
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +32 -0
- package/dist_ts/logging.d.ts +49 -0
- package/dist_ts/logging.js +134 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +25 -0
- package/dist_ts/opsserver/classes.opsserver.js +70 -0
- package/dist_ts/opsserver/handlers/actionlog.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/actionlog.handler.js +24 -0
- package/dist_ts/opsserver/handlers/actions.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/actions.handler.js +38 -0
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +19 -0
- package/dist_ts/opsserver/handlers/admin.handler.js +96 -0
- package/dist_ts/opsserver/handlers/connections.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/connections.handler.js +109 -0
- package/dist_ts/opsserver/handlers/groups.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/groups.handler.js +24 -0
- package/dist_ts/opsserver/handlers/index.d.ts +13 -0
- package/dist_ts/opsserver/handlers/index.js +14 -0
- package/dist_ts/opsserver/handlers/jobs.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/jobs.handler.js +146 -0
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/logs.handler.js +21 -0
- package/dist_ts/opsserver/handlers/managedsecrets.handler.d.ts +11 -0
- package/dist_ts/opsserver/handlers/managedsecrets.handler.js +110 -0
- package/dist_ts/opsserver/handlers/pipelines.handler.d.ts +31 -0
- package/dist_ts/opsserver/handlers/pipelines.handler.js +204 -0
- package/dist_ts/opsserver/handlers/projects.handler.d.ts +9 -0
- package/dist_ts/opsserver/handlers/projects.handler.js +24 -0
- package/dist_ts/opsserver/handlers/secrets.handler.d.ts +10 -0
- package/dist_ts/opsserver/handlers/secrets.handler.js +171 -0
- package/dist_ts/opsserver/handlers/sync.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/sync.handler.js +166 -0
- package/dist_ts/opsserver/handlers/webhook.handler.d.ts +7 -0
- package/dist_ts/opsserver/handlers/webhook.handler.js +55 -0
- package/dist_ts/opsserver/helpers/guards.d.ts +5 -0
- package/dist_ts/opsserver/helpers/guards.js +12 -0
- package/dist_ts/opsserver/index.d.ts +1 -0
- package/dist_ts/opsserver/index.js +2 -0
- package/dist_ts/paths.d.ts +9 -0
- package/dist_ts/paths.js +13 -0
- package/dist_ts/plugins.d.ts +25 -0
- package/dist_ts/plugins.js +32 -0
- package/dist_ts/providers/classes.baseprovider.d.ts +51 -0
- package/dist_ts/providers/classes.baseprovider.js +17 -0
- package/dist_ts/providers/classes.giteaprovider.d.ts +40 -0
- package/dist_ts/providers/classes.giteaprovider.js +224 -0
- package/dist_ts/providers/classes.gitlabprovider.d.ts +39 -0
- package/dist_ts/providers/classes.gitlabprovider.js +207 -0
- package/dist_ts/providers/index.d.ts +3 -0
- package/dist_ts/providers/index.js +4 -0
- package/dist_ts/storage/classes.storagemanager.d.ts +33 -0
- package/dist_ts/storage/classes.storagemanager.js +135 -0
- package/dist_ts/storage/index.d.ts +2 -0
- package/dist_ts/storage/index.js +2 -0
- package/dist_ts/timers.d.ts +4 -0
- package/dist_ts/timers.js +24 -0
- package/dist_ts_bundled/bundle.d.ts +4 -0
- package/dist_ts_bundled/bundle.js +12 -0
- package/dist_ts_interfaces/data/actionlog.d.ts +12 -0
- package/dist_ts_interfaces/data/actionlog.js +2 -0
- package/dist_ts_interfaces/data/branch.d.ts +8 -0
- package/dist_ts_interfaces/data/branch.js +2 -0
- package/dist_ts_interfaces/data/connection.d.ts +12 -0
- package/dist_ts_interfaces/data/connection.js +2 -0
- package/dist_ts_interfaces/data/group.d.ts +10 -0
- package/dist_ts_interfaces/data/group.js +2 -0
- package/dist_ts_interfaces/data/identity.d.ts +7 -0
- package/dist_ts_interfaces/data/identity.js +2 -0
- package/dist_ts_interfaces/data/index.d.ts +11 -0
- package/dist_ts_interfaces/data/index.js +12 -0
- package/dist_ts_interfaces/data/job.d.ts +37 -0
- package/dist_ts_interfaces/data/job.js +2 -0
- package/dist_ts_interfaces/data/managedsecret.d.ts +37 -0
- package/dist_ts_interfaces/data/managedsecret.js +2 -0
- package/dist_ts_interfaces/data/pipeline.d.ts +22 -0
- package/dist_ts_interfaces/data/pipeline.js +2 -0
- package/dist_ts_interfaces/data/project.d.ts +12 -0
- package/dist_ts_interfaces/data/project.js +2 -0
- package/dist_ts_interfaces/data/secret.d.ts +11 -0
- package/dist_ts_interfaces/data/secret.js +2 -0
- package/dist_ts_interfaces/data/sync.d.ts +34 -0
- package/dist_ts_interfaces/data/sync.js +2 -0
- package/dist_ts_interfaces/index.d.ts +5 -0
- package/dist_ts_interfaces/index.js +8 -0
- package/dist_ts_interfaces/plugins.d.ts +2 -0
- package/dist_ts_interfaces/plugins.js +4 -0
- package/dist_ts_interfaces/requests/actionlog.d.ts +15 -0
- package/dist_ts_interfaces/requests/actionlog.js +3 -0
- package/dist_ts_interfaces/requests/actions.d.ts +31 -0
- package/dist_ts_interfaces/requests/actions.js +3 -0
- package/dist_ts_interfaces/requests/admin.d.ts +31 -0
- package/dist_ts_interfaces/requests/admin.js +3 -0
- package/dist_ts_interfaces/requests/connections.d.ts +71 -0
- package/dist_ts_interfaces/requests/connections.js +3 -0
- package/dist_ts_interfaces/requests/groups.d.ts +14 -0
- package/dist_ts_interfaces/requests/groups.js +3 -0
- package/dist_ts_interfaces/requests/index.d.ts +13 -0
- package/dist_ts_interfaces/requests/index.js +14 -0
- package/dist_ts_interfaces/requests/jobs.d.ts +86 -0
- package/dist_ts_interfaces/requests/jobs.js +3 -0
- package/dist_ts_interfaces/requests/logs.d.ts +14 -0
- package/dist_ts_interfaces/requests/logs.js +3 -0
- package/dist_ts_interfaces/requests/managedsecrets.d.ts +84 -0
- package/dist_ts_interfaces/requests/managedsecrets.js +3 -0
- package/dist_ts_interfaces/requests/pipelines.d.ts +55 -0
- package/dist_ts_interfaces/requests/pipelines.js +3 -0
- package/dist_ts_interfaces/requests/projects.d.ts +14 -0
- package/dist_ts_interfaces/requests/projects.js +3 -0
- package/dist_ts_interfaces/requests/secrets.d.ts +72 -0
- package/dist_ts_interfaces/requests/secrets.js +3 -0
- package/dist_ts_interfaces/requests/sync.d.ts +120 -0
- package/dist_ts_interfaces/requests/sync.js +3 -0
- package/dist_ts_interfaces/requests/webhook.d.ts +13 -0
- package/dist_ts_interfaces/requests/webhook.js +3 -0
- package/license +21 -0
- package/package.json +81 -0
- package/readme.md +177 -0
- package/readme.todo.md +3 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/cache/classes.cache.cleaner.ts +69 -0
- package/ts/cache/classes.cached.document.ts +57 -0
- package/ts/cache/classes.cachedb.ts +72 -0
- package/ts/cache/classes.secrets.scan.service.ts +267 -0
- package/ts/cache/documents/classes.cached.project.ts +32 -0
- package/ts/cache/documents/classes.cached.secret.ts +81 -0
- package/ts/cache/documents/index.ts +2 -0
- package/ts/cache/index.ts +7 -0
- package/ts/classes/actionlog.ts +57 -0
- package/ts/classes/connectionmanager.ts +263 -0
- package/ts/classes/gitopsapp.ts +128 -0
- package/ts/classes/jobmanager.ts +337 -0
- package/ts/classes/jobrunners/autobookstackdocs.runner.ts +435 -0
- package/ts/classes/jobrunners/base.jobrunner.ts +16 -0
- package/ts/classes/jobrunners/index.ts +17 -0
- package/ts/classes/managedsecrets.manager.ts +322 -0
- package/ts/classes/syncmanager.ts +2117 -0
- package/ts/index.ts +37 -0
- package/ts/logging.ts +162 -0
- package/ts/opsserver/classes.opsserver.ts +86 -0
- package/ts/opsserver/handlers/actionlog.handler.ts +30 -0
- package/ts/opsserver/handlers/actions.handler.ts +50 -0
- package/ts/opsserver/handlers/admin.handler.ts +122 -0
- package/ts/opsserver/handlers/connections.handler.ts +162 -0
- package/ts/opsserver/handlers/groups.handler.ts +32 -0
- package/ts/opsserver/handlers/index.ts +13 -0
- package/ts/opsserver/handlers/jobs.handler.ts +189 -0
- package/ts/opsserver/handlers/logs.handler.ts +29 -0
- package/ts/opsserver/handlers/managedsecrets.handler.ts +158 -0
- package/ts/opsserver/handlers/pipelines.handler.ts +281 -0
- package/ts/opsserver/handlers/projects.handler.ts +32 -0
- package/ts/opsserver/handlers/secrets.handler.ts +224 -0
- package/ts/opsserver/handlers/sync.handler.ts +224 -0
- package/ts/opsserver/handlers/webhook.handler.ts +62 -0
- package/ts/opsserver/helpers/guards.ts +16 -0
- package/ts/opsserver/index.ts +1 -0
- package/ts/paths.ts +19 -0
- package/ts/plugins.ts +38 -0
- package/ts/providers/classes.baseprovider.ts +99 -0
- package/ts/providers/classes.giteaprovider.ts +279 -0
- package/ts/providers/classes.gitlabprovider.ts +265 -0
- package/ts/providers/index.ts +3 -0
- package/ts/storage/classes.storagemanager.ts +144 -0
- package/ts/storage/index.ts +2 -0
- package/ts/timers.ts +34 -0
- package/ts_interfaces/data/actionlog.ts +13 -0
- package/ts_interfaces/data/branch.ts +9 -0
- package/ts_interfaces/data/connection.ts +13 -0
- package/ts_interfaces/data/group.ts +10 -0
- package/ts_interfaces/data/identity.ts +7 -0
- package/ts_interfaces/data/index.ts +11 -0
- package/ts_interfaces/data/job.ts +42 -0
- package/ts_interfaces/data/managedsecret.ts +41 -0
- package/ts_interfaces/data/pipeline.ts +32 -0
- package/ts_interfaces/data/project.ts +12 -0
- package/ts_interfaces/data/secret.ts +11 -0
- package/ts_interfaces/data/sync.ts +37 -0
- package/ts_interfaces/index.ts +9 -0
- package/ts_interfaces/plugins.ts +6 -0
- package/ts_interfaces/requests/actionlog.ts +19 -0
- package/ts_interfaces/requests/actions.ts +39 -0
- package/ts_interfaces/requests/admin.ts +43 -0
- package/ts_interfaces/requests/connections.ts +95 -0
- package/ts_interfaces/requests/groups.ts +18 -0
- package/ts_interfaces/requests/index.ts +13 -0
- package/ts_interfaces/requests/jobs.ts +118 -0
- package/ts_interfaces/requests/logs.ts +18 -0
- package/ts_interfaces/requests/managedsecrets.ts +112 -0
- package/ts_interfaces/requests/pipelines.ts +71 -0
- package/ts_interfaces/requests/projects.ts +18 -0
- package/ts_interfaces/requests/secrets.ts +92 -0
- package/ts_interfaces/requests/sync.ts +157 -0
- package/ts_interfaces/requests/webhook.ts +18 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/appstate.ts +1251 -0
- package/ts_web/elements/gitops-dashboard.ts +350 -0
- package/ts_web/elements/index.ts +10 -0
- package/ts_web/elements/shared/css.ts +29 -0
- package/ts_web/elements/shared/index.ts +1 -0
- package/ts_web/elements/views/actionlog/index.ts +101 -0
- package/ts_web/elements/views/actions/index.ts +209 -0
- package/ts_web/elements/views/buildlog/index.ts +196 -0
- package/ts_web/elements/views/connections/index.ts +260 -0
- package/ts_web/elements/views/groups/index.ts +134 -0
- package/ts_web/elements/views/jobs/index.ts +424 -0
- package/ts_web/elements/views/managedsecrets/index.ts +502 -0
- package/ts_web/elements/views/overview/index.ts +86 -0
- package/ts_web/elements/views/pipelines/index.ts +561 -0
- package/ts_web/elements/views/projects/index.ts +149 -0
- package/ts_web/elements/views/secrets/index.ts +310 -0
- package/ts_web/elements/views/sync/index.ts +512 -0
- package/ts_web/index.ts +7 -0
- package/ts_web/plugins.ts +15 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { logger } from '../../logging.js';
|
|
3
|
+
import type * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import type { BaseProvider } from '../../providers/classes.baseprovider.js';
|
|
5
|
+
import type { StorageManager } from '../../storage/index.js';
|
|
6
|
+
import { BaseJobRunner, type IJobRunContext } from './base.jobrunner.js';
|
|
7
|
+
|
|
8
|
+
const KEYCHAIN_PREFIX = 'keychain:';
|
|
9
|
+
const BATCH_SIZE = 5;
|
|
10
|
+
const BATCH_DELAY_MS = 200;
|
|
11
|
+
const HASH_STORAGE_PREFIX = '/job-hashes/';
|
|
12
|
+
|
|
13
|
+
/** Simple SHA-256 hex hash */
|
|
14
|
+
async function sha256(content: string): Promise<string> {
|
|
15
|
+
const data = new TextEncoder().encode(content);
|
|
16
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
17
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* If the first line of markdown is a heading matching the book name (e.g. "# @org/repo"),
|
|
22
|
+
* strip it — the book title already provides that context.
|
|
23
|
+
*/
|
|
24
|
+
const REDUNDANT_HEADINGS = ['changelog'];
|
|
25
|
+
|
|
26
|
+
function stripRedundantHeading(markdown: string, bookName: string): string {
|
|
27
|
+
const firstNewline = markdown.indexOf('\n');
|
|
28
|
+
const firstLine = (firstNewline === -1 ? markdown : markdown.slice(0, firstNewline)).trim();
|
|
29
|
+
if (firstLine.startsWith('# ')) {
|
|
30
|
+
const heading = firstLine.slice(2).trim().toLowerCase();
|
|
31
|
+
const fullName = bookName.toLowerCase();
|
|
32
|
+
const repoName = bookName.split('/').pop()?.toLowerCase() || '';
|
|
33
|
+
// Strip headings matching the book/repo name (with optional trailing decorations)
|
|
34
|
+
if (heading === fullName || heading === repoName
|
|
35
|
+
|| heading.startsWith(fullName + ' ') || heading.startsWith(repoName + ' ')) {
|
|
36
|
+
return firstNewline === -1 ? '' : markdown.slice(firstNewline + 1);
|
|
37
|
+
}
|
|
38
|
+
// Strip generic redundant headings (e.g. "# Changelog")
|
|
39
|
+
if (REDUNDANT_HEADINGS.includes(heading)) {
|
|
40
|
+
return firstNewline === -1 ? '' : markdown.slice(firstNewline + 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return markdown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Persisted content hashes for a job — avoids unnecessary BookStack updates */
|
|
47
|
+
type ContentHashMap = Record<string, string>; // page key → sha256 hash
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mapping:
|
|
51
|
+
* Git org/group → BookStack Shelf
|
|
52
|
+
* Git repository → BookStack Book (named @group/repo, assigned to shelf)
|
|
53
|
+
* readme.md → BookStack Page (named "readme.md for @group/repo")
|
|
54
|
+
* docs/*.md → BookStack Pages (named "docs/file.md for @group/repo")
|
|
55
|
+
*
|
|
56
|
+
* Content hashes are persisted in storage to skip updates when content hasn't changed.
|
|
57
|
+
*/
|
|
58
|
+
export class AutoBookstackDocsRunner extends BaseJobRunner {
|
|
59
|
+
readonly jobType: interfaces.data.TJobType = 'autobookstackdocs';
|
|
60
|
+
|
|
61
|
+
async execute(context: IJobRunContext): Promise<void> {
|
|
62
|
+
const config = context.jobConfig.autoBookstackDocsConfig;
|
|
63
|
+
if (!config) {
|
|
64
|
+
throw new Error('Missing autoBookstackDocsConfig');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 1. Resolve BookStack token from keychain
|
|
68
|
+
let tokenSecret = config.bookstackTarget.tokenSecret;
|
|
69
|
+
if (tokenSecret.startsWith(KEYCHAIN_PREFIX)) {
|
|
70
|
+
const keychainId = tokenSecret.slice(KEYCHAIN_PREFIX.length);
|
|
71
|
+
const resolved = await context.smartSecret.getSecret(keychainId);
|
|
72
|
+
if (!resolved) {
|
|
73
|
+
throw new Error('Could not retrieve BookStack token from keychain');
|
|
74
|
+
}
|
|
75
|
+
tokenSecret = resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Connect to BookStack
|
|
79
|
+
const bookstack = new plugins.bookstackClient.BookStackAccount(
|
|
80
|
+
config.bookstackTarget.baseUrl,
|
|
81
|
+
config.bookstackTarget.tokenId,
|
|
82
|
+
tokenSecret,
|
|
83
|
+
);
|
|
84
|
+
await bookstack.testConnection();
|
|
85
|
+
logger.jobLog('info', `Connected to BookStack at ${config.bookstackTarget.baseUrl}`, 'bookstack');
|
|
86
|
+
|
|
87
|
+
// 3. Load persisted content hashes (for diff-based updates)
|
|
88
|
+
const hashKey = `${HASH_STORAGE_PREFIX}${context.jobConfig.id}.json`;
|
|
89
|
+
const oldHashes: ContentHashMap = (await context.storageManager.getJSON<ContentHashMap>(hashKey)) || {};
|
|
90
|
+
const newHashes: ContentHashMap = {};
|
|
91
|
+
|
|
92
|
+
// 4. Pre-load existing shelves for find-or-create
|
|
93
|
+
const existingShelves = await bookstack.getShelves();
|
|
94
|
+
const shelfMap = new Map<string, plugins.bookstackClient.BookStackShelf>();
|
|
95
|
+
for (const shelf of existingShelves) {
|
|
96
|
+
shelfMap.set(shelf.name.toLowerCase(), shelf);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Track desired state for deletion reconciliation
|
|
100
|
+
const syncedShelfNames = new Set<string>();
|
|
101
|
+
const syncedBookNames = new Map<string, Set<string>>();
|
|
102
|
+
const shelfBookIds = new Map<string, number[]>();
|
|
103
|
+
|
|
104
|
+
let pagesCreated = 0;
|
|
105
|
+
let pagesUpdated = 0;
|
|
106
|
+
let pagesSkipped = 0;
|
|
107
|
+
|
|
108
|
+
// 5. Process each source connection
|
|
109
|
+
for (const connectionId of config.sourceConnectionIds) {
|
|
110
|
+
const conn = context.connectionManager.getConnection(connectionId);
|
|
111
|
+
if (!conn || conn.status === 'paused') {
|
|
112
|
+
logger.jobLog('warn', `Skipping connection ${connectionId} (not found or paused)`, 'bookstack');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const provider = context.connectionManager.getProvider(connectionId);
|
|
117
|
+
const groups = await provider.getGroups();
|
|
118
|
+
logger.jobLog('info', `Connection "${conn.name}": ${groups.length} groups`, 'bookstack');
|
|
119
|
+
|
|
120
|
+
for (const group of groups) {
|
|
121
|
+
// Apply group filters
|
|
122
|
+
if (config.includeGroups && config.includeGroups.length > 0) {
|
|
123
|
+
if (!config.includeGroups.some((g) => g.toLowerCase() === group.name.toLowerCase() || g.toLowerCase() === group.fullPath.toLowerCase())) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (config.excludeGroups && config.excludeGroups.length > 0) {
|
|
128
|
+
if (config.excludeGroups.some((g) => g.toLowerCase() === group.name.toLowerCase() || g.toLowerCase() === group.fullPath.toLowerCase())) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Find or create shelf for this org/group
|
|
134
|
+
const shelfName = group.fullPath || group.name;
|
|
135
|
+
const shelfKey = shelfName.toLowerCase();
|
|
136
|
+
syncedShelfNames.add(shelfKey);
|
|
137
|
+
if (!syncedBookNames.has(shelfKey)) {
|
|
138
|
+
syncedBookNames.set(shelfKey, new Set());
|
|
139
|
+
}
|
|
140
|
+
if (!shelfBookIds.has(shelfKey)) {
|
|
141
|
+
shelfBookIds.set(shelfKey, []);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let shelf = shelfMap.get(shelfKey);
|
|
145
|
+
if (!shelf) {
|
|
146
|
+
shelf = await bookstack.createShelf({
|
|
147
|
+
name: shelfName,
|
|
148
|
+
description: group.description || `Documentation for ${shelfName}`,
|
|
149
|
+
});
|
|
150
|
+
shelfMap.set(shelfKey, shelf);
|
|
151
|
+
logger.jobLog('info', `Created shelf: ${shelfName}`, 'bookstack');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get projects in this group
|
|
155
|
+
const projects = await provider.getGroupProjects(group.id);
|
|
156
|
+
|
|
157
|
+
// Process repos in batches
|
|
158
|
+
for (let i = 0; i < projects.length; i += BATCH_SIZE) {
|
|
159
|
+
if (i > 0) await new Promise((r) => setTimeout(r, BATCH_DELAY_MS));
|
|
160
|
+
const batch = projects.slice(i, i + BATCH_SIZE);
|
|
161
|
+
const results = await Promise.all(
|
|
162
|
+
batch.map((project) => {
|
|
163
|
+
const bookName = `@${group.fullPath}/${project.name}`;
|
|
164
|
+
syncedBookNames.get(shelfKey)!.add(bookName.toLowerCase());
|
|
165
|
+
return this.syncRepoAsBook(
|
|
166
|
+
provider, bookstack, context.storageManager, project, bookName, config,
|
|
167
|
+
group.visibility, oldHashes, newHashes,
|
|
168
|
+
);
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
for (const result of results) {
|
|
172
|
+
if (result) {
|
|
173
|
+
shelfBookIds.get(shelfKey)!.push(result.bookId);
|
|
174
|
+
pagesCreated += result.created;
|
|
175
|
+
pagesUpdated += result.updated;
|
|
176
|
+
pagesSkipped += result.skipped;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Assign all synced books to this shelf (only if book list changed)
|
|
182
|
+
const bookIds = shelfBookIds.get(shelfKey)!;
|
|
183
|
+
if (bookIds.length > 0) {
|
|
184
|
+
const shelfHashKey = `__shelf__${shelfKey}`;
|
|
185
|
+
const desiredShelfState = bookIds.slice().sort((a, b) => a - b).join(',');
|
|
186
|
+
if (oldHashes[shelfHashKey] !== desiredShelfState) {
|
|
187
|
+
await shelf.update({ books: bookIds });
|
|
188
|
+
logger.jobLog('info', `Updated shelf "${shelfName}" book assignments`, 'bookstack');
|
|
189
|
+
}
|
|
190
|
+
newHashes[shelfHashKey] = desiredShelfState;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 6. Deletion reconciliation
|
|
196
|
+
if (config.propagateDeletes) {
|
|
197
|
+
await this.reconcileDeletes(bookstack, shelfMap, syncedShelfNames, syncedBookNames);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 7. Persist updated content hashes
|
|
201
|
+
await context.storageManager.setJSON(hashKey, newHashes);
|
|
202
|
+
|
|
203
|
+
logger.jobLog('success', `Sync complete — ${pagesCreated} created, ${pagesUpdated} updated, ${pagesSkipped} unchanged`, 'bookstack');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Syncs a single git repository as a BookStack Book.
|
|
208
|
+
* Returns book ID + page stats, or null if skipped (no docs).
|
|
209
|
+
*/
|
|
210
|
+
private async syncRepoAsBook(
|
|
211
|
+
provider: BaseProvider,
|
|
212
|
+
bookstack: plugins.bookstackClient.BookStackAccount,
|
|
213
|
+
storageManager: StorageManager,
|
|
214
|
+
project: interfaces.data.IProject,
|
|
215
|
+
bookName: string,
|
|
216
|
+
config: interfaces.data.IAutoBookstackDocsConfig,
|
|
217
|
+
groupVisibility: string,
|
|
218
|
+
oldHashes: ContentHashMap,
|
|
219
|
+
newHashes: ContentHashMap,
|
|
220
|
+
): Promise<{ bookId: number; created: number; updated: number; skipped: number } | null> {
|
|
221
|
+
try {
|
|
222
|
+
// 0. Collect tags from package.json keywords + git repo topics
|
|
223
|
+
const tags = await this.collectTags(provider, project);
|
|
224
|
+
|
|
225
|
+
// Collect all pages to sync for this repo
|
|
226
|
+
const pagesToSync: { name: string; content: string; hash: string }[] = [];
|
|
227
|
+
|
|
228
|
+
// 1. Fetch explicit root files (e.g. readme.md)
|
|
229
|
+
for (const filePath of config.filePaths) {
|
|
230
|
+
let content = await provider.getFileContent(project.fullPath, filePath);
|
|
231
|
+
if (content) {
|
|
232
|
+
content = stripRedundantHeading(content, bookName);
|
|
233
|
+
const tagsStr = tags.map((t) => t.name).join(',');
|
|
234
|
+
const hash = await sha256(content + '\0' + tagsStr);
|
|
235
|
+
const pageName = `${filePath} for ${bookName}`;
|
|
236
|
+
pagesToSync.push({ name: pageName, content, hash });
|
|
237
|
+
newHashes[pageName.toLowerCase()] = hash;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. Scan doc directories for .md files
|
|
242
|
+
for (const dir of config.docDirs) {
|
|
243
|
+
const entries = await provider.getDirectoryContents(project.fullPath, dir);
|
|
244
|
+
if (!entries) continue;
|
|
245
|
+
|
|
246
|
+
const mdFiles = entries.filter(
|
|
247
|
+
(e) => e.type === 'file' && e.name.toLowerCase().endsWith('.md'),
|
|
248
|
+
);
|
|
249
|
+
for (const mdFile of mdFiles) {
|
|
250
|
+
let content = await provider.getFileContent(project.fullPath, mdFile.path);
|
|
251
|
+
if (content) {
|
|
252
|
+
content = stripRedundantHeading(content, bookName);
|
|
253
|
+
const tagsStr = tags.map((t) => t.name).join(',');
|
|
254
|
+
const hash = await sha256(content + '\0' + tagsStr);
|
|
255
|
+
const pageName = `${mdFile.path} for ${bookName}`;
|
|
256
|
+
pagesToSync.push({ name: pageName, content, hash });
|
|
257
|
+
newHashes[pageName.toLowerCase()] = hash;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Skip repos that have no docs at all
|
|
263
|
+
if (pagesToSync.length === 0) return null;
|
|
264
|
+
|
|
265
|
+
// 3. Find or create book for this repo
|
|
266
|
+
const book = await this.findOrCreateBook(bookstack, bookName, project.description || '');
|
|
267
|
+
|
|
268
|
+
// 4. Get existing pages in this book
|
|
269
|
+
const existingPages = await book.getPages();
|
|
270
|
+
const pageMap = new Map<string, plugins.bookstackClient.BookStackPage>();
|
|
271
|
+
for (const p of existingPages) {
|
|
272
|
+
pageMap.set(p.name.toLowerCase(), p);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 5. Create or update pages (only if content changed)
|
|
276
|
+
let created = 0;
|
|
277
|
+
let updated = 0;
|
|
278
|
+
let skipped = 0;
|
|
279
|
+
const syncedPageNames = new Set<string>();
|
|
280
|
+
|
|
281
|
+
for (const page of pagesToSync) {
|
|
282
|
+
syncedPageNames.add(page.name.toLowerCase());
|
|
283
|
+
const existing = pageMap.get(page.name.toLowerCase());
|
|
284
|
+
|
|
285
|
+
if (existing) {
|
|
286
|
+
// Check hash — skip update if content hasn't changed
|
|
287
|
+
const oldHash = oldHashes[page.name.toLowerCase()];
|
|
288
|
+
if (oldHash === page.hash) {
|
|
289
|
+
skipped++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
await existing.update({ markdown: page.content, tags });
|
|
293
|
+
updated++;
|
|
294
|
+
} else {
|
|
295
|
+
await bookstack.createPage({
|
|
296
|
+
book_id: book.id,
|
|
297
|
+
name: page.name,
|
|
298
|
+
markdown: page.content,
|
|
299
|
+
tags,
|
|
300
|
+
});
|
|
301
|
+
created++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 6. Delete stale pages within this book
|
|
306
|
+
if (config.propagateDeletes) {
|
|
307
|
+
for (const [name, page] of pageMap) {
|
|
308
|
+
if (!syncedPageNames.has(name)) {
|
|
309
|
+
await page.delete();
|
|
310
|
+
logger.jobLog('warn', `Deleted stale page "${page.name}" from book "${bookName}"`, 'bookstack');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 7. Sync visibility — only update permissions if visibility state changed
|
|
316
|
+
if (config.syncVisibility) {
|
|
317
|
+
const isPrivate = project.visibility === 'private' || project.visibility === 'internal'
|
|
318
|
+
|| groupVisibility === 'private' || groupVisibility === 'internal';
|
|
319
|
+
const visKey = `__vis__${bookName.toLowerCase()}`;
|
|
320
|
+
const desiredVis = isPrivate ? `private:${config.privateRoleId || ''}` : 'public';
|
|
321
|
+
const oldVis = oldHashes[visKey];
|
|
322
|
+
newHashes[visKey] = desiredVis;
|
|
323
|
+
|
|
324
|
+
if (oldVis !== desiredVis) {
|
|
325
|
+
if (isPrivate) {
|
|
326
|
+
const permUpdate: any = {
|
|
327
|
+
fallback_permissions: { inheriting: false, view: false, create: false, update: false, delete: false },
|
|
328
|
+
};
|
|
329
|
+
if (config.privateRoleId) {
|
|
330
|
+
permUpdate.role_permissions = [{ role_id: config.privateRoleId, view: true, create: false, update: false, delete: false }];
|
|
331
|
+
}
|
|
332
|
+
await bookstack.updateContentPermissions('book', book.id, permUpdate);
|
|
333
|
+
logger.jobLog('info', `Restricted book "${bookName}" (private)`, 'bookstack');
|
|
334
|
+
} else {
|
|
335
|
+
await bookstack.updateContentPermissions('book', book.id, {
|
|
336
|
+
fallback_permissions: { inheriting: true },
|
|
337
|
+
});
|
|
338
|
+
logger.jobLog('info', `Set book "${bookName}" to public`, 'bookstack');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (created > 0 || updated > 0) {
|
|
344
|
+
logger.jobLog('info', `${bookName}: ${created} created, ${updated} updated, ${skipped} unchanged`, 'bookstack');
|
|
345
|
+
}
|
|
346
|
+
return { bookId: book.id, created, updated, skipped };
|
|
347
|
+
} catch (err) {
|
|
348
|
+
logger.jobLog('warn', `Failed to sync repo ${bookName}: ${err}`, 'bookstack');
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Removes stale BookStack shelves and orphaned books.
|
|
355
|
+
*/
|
|
356
|
+
private async reconcileDeletes(
|
|
357
|
+
bookstack: plugins.bookstackClient.BookStackAccount,
|
|
358
|
+
shelfMap: Map<string, plugins.bookstackClient.BookStackShelf>,
|
|
359
|
+
syncedShelfNames: Set<string>,
|
|
360
|
+
syncedBookNames: Map<string, Set<string>>,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
// Build flat set of all desired book names
|
|
363
|
+
const allDesiredBookNames = new Set<string>();
|
|
364
|
+
for (const bookNames of syncedBookNames.values()) {
|
|
365
|
+
for (const name of bookNames) {
|
|
366
|
+
allDesiredBookNames.add(name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Delete stale shelves
|
|
371
|
+
for (const [shelfKey, shelf] of shelfMap) {
|
|
372
|
+
if (!syncedShelfNames.has(shelfKey)) {
|
|
373
|
+
await shelf.delete();
|
|
374
|
+
logger.jobLog('warn', `Deleted stale shelf "${shelf.name}"`, 'bookstack');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Delete all orphaned books
|
|
379
|
+
const allBooks = await bookstack.getBooks();
|
|
380
|
+
for (const book of allBooks) {
|
|
381
|
+
if (!allDesiredBookNames.has(book.name.toLowerCase())) {
|
|
382
|
+
await book.delete();
|
|
383
|
+
logger.jobLog('warn', `Deleted orphaned book "${book.name}"`, 'bookstack');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Collects tags from package.json keywords + git repo topics, deduplicated.
|
|
390
|
+
*/
|
|
391
|
+
private async collectTags(
|
|
392
|
+
provider: BaseProvider,
|
|
393
|
+
project: interfaces.data.IProject,
|
|
394
|
+
): Promise<{ name: string; value: string }[]> {
|
|
395
|
+
const tagNames = new Set<string>();
|
|
396
|
+
|
|
397
|
+
// Add git repo topics
|
|
398
|
+
if (project.topics) {
|
|
399
|
+
for (const topic of project.topics) {
|
|
400
|
+
if (topic) tagNames.add(topic.toLowerCase());
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Fetch package.json and extract keywords
|
|
405
|
+
try {
|
|
406
|
+
const pkgJson = await provider.getFileContent(project.fullPath, 'package.json');
|
|
407
|
+
if (pkgJson) {
|
|
408
|
+
const pkg = JSON.parse(pkgJson);
|
|
409
|
+
if (Array.isArray(pkg.keywords)) {
|
|
410
|
+
for (const kw of pkg.keywords) {
|
|
411
|
+
if (typeof kw === 'string' && kw) tagNames.add(kw.toLowerCase());
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
// package.json missing or invalid — no keywords
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return Array.from(tagNames).map((name) => ({ name, value: '' }));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private async findOrCreateBook(
|
|
423
|
+
bookstack: plugins.bookstackClient.BookStackAccount,
|
|
424
|
+
bookName: string,
|
|
425
|
+
description: string,
|
|
426
|
+
): Promise<plugins.bookstackClient.BookStackBook> {
|
|
427
|
+
const books = await bookstack.getBooks();
|
|
428
|
+
const existing = books.find((b) => b.name.toLowerCase() === bookName.toLowerCase());
|
|
429
|
+
if (existing) return existing;
|
|
430
|
+
return bookstack.createBook({
|
|
431
|
+
name: bookName,
|
|
432
|
+
description: description || `Documentation for ${bookName}`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type * as interfaces from '../../../ts_interfaces/index.js';
|
|
2
|
+
import type { ConnectionManager } from '../connectionmanager.js';
|
|
3
|
+
import type { StorageManager } from '../../storage/index.js';
|
|
4
|
+
import type * as plugins from '../../plugins.js';
|
|
5
|
+
|
|
6
|
+
export interface IJobRunContext {
|
|
7
|
+
jobConfig: interfaces.data.IJobConfig;
|
|
8
|
+
connectionManager: ConnectionManager;
|
|
9
|
+
storageManager: StorageManager;
|
|
10
|
+
smartSecret: plugins.smartsecret.SmartSecret;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export abstract class BaseJobRunner {
|
|
14
|
+
abstract readonly jobType: interfaces.data.TJobType;
|
|
15
|
+
abstract execute(context: IJobRunContext): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as interfaces from '../../../ts_interfaces/index.js';
|
|
2
|
+
import { BaseJobRunner } from './base.jobrunner.js';
|
|
3
|
+
import { AutoBookstackDocsRunner } from './autobookstackdocs.runner.js';
|
|
4
|
+
|
|
5
|
+
export { BaseJobRunner, type IJobRunContext } from './base.jobrunner.js';
|
|
6
|
+
export { AutoBookstackDocsRunner } from './autobookstackdocs.runner.js';
|
|
7
|
+
|
|
8
|
+
const runners = new Map<interfaces.data.TJobType, BaseJobRunner>();
|
|
9
|
+
runners.set('autobookstackdocs', new AutoBookstackDocsRunner());
|
|
10
|
+
|
|
11
|
+
export function getRunner(jobType: interfaces.data.TJobType): BaseJobRunner {
|
|
12
|
+
const runner = runners.get(jobType);
|
|
13
|
+
if (!runner) {
|
|
14
|
+
throw new Error(`No runner registered for job type: ${jobType}`);
|
|
15
|
+
}
|
|
16
|
+
return runner;
|
|
17
|
+
}
|