docugate 0.3.1 → 0.4.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 +496 -494
- package/dist/api.d.ts +2 -0
- package/dist/api.js +27 -0
- package/dist/cli.js +16 -0
- package/dist/commands.js +30 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +64 -0
- package/dist/lockfiles.d.ts +24 -0
- package/dist/lockfiles.js +203 -0
- package/dist/project.d.ts +72 -0
- package/dist/project.js +246 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +1 -1
- package/dist/sync/command.js +45 -7
- package/dist/sync/workflow.d.ts +2 -0
- package/dist/sync/workflow.js +51 -45
- package/package.json +1 -1
package/dist/project.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { apiFetch } from './api.js';
|
|
6
|
+
import { packageVersion } from './banner.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
8
|
+
import { guessRole } from './init.js';
|
|
9
|
+
import { scanLockfiles } from './lockfiles.js';
|
|
10
|
+
import { defaultGit } from './sync/changes.js';
|
|
11
|
+
import { repoFromRemote } from './sync/pr.js';
|
|
12
|
+
import { bold, dim, green, plural, red, yellow } from './ui.js';
|
|
13
|
+
// A repository connected to a DocuGate account. `docugate connect` links it, and
|
|
14
|
+
// from then on the dashboard decides which docs it updates, how often and what
|
|
15
|
+
// kind of project it is. Everything that talks to DocuGate on a project's behalf
|
|
16
|
+
// lives here, and none of it may ever be the reason a sync fails.
|
|
17
|
+
export const PROJECT_TYPES = ['frontend', 'backend', 'serverless', 'monorepo'];
|
|
18
|
+
export const FREQUENCIES = ['push', 'daily', 'weekly', 'off'];
|
|
19
|
+
const TOKENS_FILE = () => join(process.env.DOCUGATE_HOME ?? join(homedir(), '.docugate'), 'projects.json');
|
|
20
|
+
const base = () => process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
|
|
21
|
+
function readTokens() {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(TOKENS_FILE(), 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** `owner/name` from the git remote, or the folder's name without one. */
|
|
30
|
+
export function detectRepo(root) {
|
|
31
|
+
try {
|
|
32
|
+
const repo = repoFromRemote(defaultGit(['remote', 'get-url', 'origin'], root));
|
|
33
|
+
if (repo)
|
|
34
|
+
return repo;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Not a git repository, or no remote.
|
|
38
|
+
}
|
|
39
|
+
return basename(root);
|
|
40
|
+
}
|
|
41
|
+
/** The token for this repository: CI's secret first, then what `connect` saved. */
|
|
42
|
+
export function projectToken(root, env = process.env) {
|
|
43
|
+
return env.DOCUGATE_PROJECT_TOKEN || readTokens()[detectRepo(root)] || null;
|
|
44
|
+
}
|
|
45
|
+
/** A suggestion only. The person confirms it, and the dashboard can change it. */
|
|
46
|
+
export function detectType(root) {
|
|
47
|
+
const has = (name) => existsSync(join(root, name));
|
|
48
|
+
let workspaces = has('pnpm-workspace.yaml') || has('turbo.json') || has('nx.json');
|
|
49
|
+
try {
|
|
50
|
+
workspaces ||= Boolean(JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).workspaces);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// No package.json, or an unreadable one.
|
|
54
|
+
}
|
|
55
|
+
const role = guessRole(root);
|
|
56
|
+
if (workspaces || role === 'both')
|
|
57
|
+
return 'monorepo';
|
|
58
|
+
if (['serverless.yml', 'serverless.ts', 'wrangler.toml', 'wrangler.jsonc', 'netlify.toml', 'vercel.json', 'template.yaml'].some(has))
|
|
59
|
+
return 'serverless';
|
|
60
|
+
return role === 'frontend' ? 'frontend' : 'backend';
|
|
61
|
+
}
|
|
62
|
+
/** Where the backend lives in a monorepo, which is where the key-check package installs. */
|
|
63
|
+
export function detectBackendDir(root) {
|
|
64
|
+
return ['backend', 'server', 'api', 'apps/api', 'apps/server', 'packages/api', 'packages/server'].find((d) => existsSync(join(root, d))) ?? '';
|
|
65
|
+
}
|
|
66
|
+
async function projectFetch(token, path, init = {}) {
|
|
67
|
+
return fetch(`${base()}/api/projects/self${path}`, {
|
|
68
|
+
...init,
|
|
69
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
70
|
+
signal: AbortSignal.timeout(20_000),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** This project's settings, or null: unlinked, offline, or a revoked token. */
|
|
74
|
+
export async function readSettings(token) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await projectFetch(token, '');
|
|
77
|
+
return res.ok ? (await res.json()) : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Tells the dashboard what a run did. A failure here is not the run's failure. */
|
|
84
|
+
export async function reportRun(token, run) {
|
|
85
|
+
try {
|
|
86
|
+
await projectFetch(token, '/runs', { method: 'POST', body: JSON.stringify({ ...run, version: packageVersion() }) });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// The docs matter more than the log line.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function runAudit(root, token) {
|
|
93
|
+
const owner = detectRepo(root).split('/').length === 2 ? detectRepo(root).split('/')[0] : undefined;
|
|
94
|
+
if (loadConfig(root).config.audit === false) {
|
|
95
|
+
return { checked: false, reason: 'audit_off', findings: [], files: [], unchecked: [], packages: 0 };
|
|
96
|
+
}
|
|
97
|
+
const scan = scanLockfiles(root, owner);
|
|
98
|
+
const none = { findings: [], files: scan.files, unchecked: scan.unchecked, packages: scan.dependencies.length };
|
|
99
|
+
try {
|
|
100
|
+
const res = await projectFetch(token, '/dependencies', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: JSON.stringify({ dependencies: scan.dependencies, unchecked: scan.unchecked }),
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
return { checked: false, reason: `status_${res.status}`, ...none };
|
|
106
|
+
const body = (await res.json());
|
|
107
|
+
return { ...none, ...body };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { checked: false, reason: 'unreachable', ...none };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const SEVERITY_ORDER = ['critical', 'high', 'moderate', 'low', 'unknown'];
|
|
114
|
+
/** Critical and high by name with their fix; the rest as a count. */
|
|
115
|
+
export function printFindings(answer, log = console.log) {
|
|
116
|
+
if (!answer.checked) {
|
|
117
|
+
const why = answer.reason === 'audit_off' ? 'it is turned off for this project' : 'DocuGate could not finish the lookup';
|
|
118
|
+
log(` ${yellow('Dependencies not checked')}: ${why}. This does not mean they are safe.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const urgent = answer.findings.filter((f) => f.severity === 'critical' || f.severity === 'high');
|
|
122
|
+
if (!answer.findings.length)
|
|
123
|
+
log(` ${green('No known vulnerabilities')} in ${plural(answer.packages, 'package')} checked.`);
|
|
124
|
+
else {
|
|
125
|
+
log(` ${red(plural(answer.findings.length, 'known vulnerability').replace('vulnerabilitys', 'vulnerabilities'))} in ${plural(answer.packages, 'package')}. Please fix these:`);
|
|
126
|
+
for (const f of urgent.sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity))) {
|
|
127
|
+
log(` ${red(f.severity.padEnd(8))} ${bold(`${f.package}@${f.version}`)} ${f.fixedIn ? `upgrade to ${f.fixedIn}` : 'no fix published yet'}`);
|
|
128
|
+
log(` ${dim(`${f.summary} ${f.url}`)}`);
|
|
129
|
+
}
|
|
130
|
+
if (urgent.length < answer.findings.length)
|
|
131
|
+
log(` ${dim(`and ${answer.findings.length - urgent.length} of lower severity, listed in the dashboard's Security screen.`)}`);
|
|
132
|
+
}
|
|
133
|
+
for (const file of answer.unchecked)
|
|
134
|
+
log(` ${yellow('Not checked')}: ${file} could not be read, or uses a private package source.`);
|
|
135
|
+
}
|
|
136
|
+
/** `docugate audit`. Returns the exit code. */
|
|
137
|
+
export async function auditCommand(root, args, env = process.env) {
|
|
138
|
+
if (args.failOn && !['critical', 'high', 'moderate', 'low'].includes(args.failOn)) {
|
|
139
|
+
console.error(`${red('error')} --fail-on must be critical, high, moderate or low.`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
const token = projectToken(root, env);
|
|
143
|
+
if (!token) {
|
|
144
|
+
console.error(`${red('error')} This project is not connected. Run docugate connect first.`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const answer = await runAudit(root, token);
|
|
148
|
+
if (args.json)
|
|
149
|
+
console.log(JSON.stringify(answer, null, 2));
|
|
150
|
+
else {
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(` ${bold('Dependency check')} ${dim(`${plural(answer.files.length, 'lockfile')} read`)}`);
|
|
153
|
+
printFindings(answer);
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
if (!answer.checked)
|
|
157
|
+
return args.failOn ? 1 : 0;
|
|
158
|
+
if (args.failOn) {
|
|
159
|
+
const limit = SEVERITY_ORDER.indexOf(args.failOn);
|
|
160
|
+
if (answer.findings.some((f) => SEVERITY_ORDER.indexOf(f.severity) <= limit))
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
async function choose(question, options, fallback, ask) {
|
|
166
|
+
const answer = (await ask(` ${question} ${dim(`(${options.join(', ')})`)} [${fallback}] `)).trim().toLowerCase();
|
|
167
|
+
return options.includes(answer) ? answer : fallback;
|
|
168
|
+
}
|
|
169
|
+
/** `docugate connect`. Returns the exit code. */
|
|
170
|
+
export async function connectCommand(root, args) {
|
|
171
|
+
if (args.type && !PROJECT_TYPES.includes(args.type)) {
|
|
172
|
+
console.error(`${red('error')} --type must be one of ${PROJECT_TYPES.join(', ')}.`);
|
|
173
|
+
return 2;
|
|
174
|
+
}
|
|
175
|
+
if (args.frequency && !FREQUENCIES.includes(args.frequency)) {
|
|
176
|
+
console.error(`${red('error')} --frequency must be one of ${FREQUENCIES.join(', ')}.`);
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
const repo = detectRepo(root);
|
|
180
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI && !args.yes;
|
|
181
|
+
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
182
|
+
const ask = async (q) => (rl ? rl.question(q) : '');
|
|
183
|
+
try {
|
|
184
|
+
const { spaces } = (await (await apiFetch('/api/spaces/mine')).json());
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(` ${bold('Connect')} ${repo}`);
|
|
187
|
+
console.log(` ${dim('Shared with DocuGate: this repository’s name, the tool running the CLI, the outcome of each sync, and the names,')}`);
|
|
188
|
+
console.log(` ${dim('versions and lockfile paths of public packages, to check them for known vulnerabilities. Never your code.')}`);
|
|
189
|
+
console.log();
|
|
190
|
+
let spaceId = null;
|
|
191
|
+
if (args.space) {
|
|
192
|
+
const match = spaces.find((s) => s.slug === args.space);
|
|
193
|
+
if (!match) {
|
|
194
|
+
console.error(`${red('error')} You have no space called "${args.space}".`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
spaceId = match.id;
|
|
198
|
+
}
|
|
199
|
+
else if (rl && spaces.length) {
|
|
200
|
+
spaces.forEach((s, i) => console.log(` ${i + 1}. ${s.name} ${dim(s.slug)}`));
|
|
201
|
+
const n = Number((await ask(` Which documentation does this project update? ${dim('(number, or empty for none yet)')} `)).trim());
|
|
202
|
+
spaceId = spaces[n - 1]?.id ?? null;
|
|
203
|
+
}
|
|
204
|
+
else if (spaces.length === 1) {
|
|
205
|
+
spaceId = spaces[0].id;
|
|
206
|
+
}
|
|
207
|
+
const guessed = detectType(root);
|
|
208
|
+
const type = args.type ?? (await choose('What kind of project is this?', PROJECT_TYPES, guessed, ask));
|
|
209
|
+
let backendDir = args.backendDir ?? '';
|
|
210
|
+
if (type === 'monorepo' && !backendDir) {
|
|
211
|
+
const found = detectBackendDir(root);
|
|
212
|
+
backendDir = (await ask(` Which folder holds the backend? [${found || 'none'}] `)).trim() || found;
|
|
213
|
+
}
|
|
214
|
+
const frequency = args.frequency ?? (await choose('How often should docs be checked?', FREQUENCIES, 'weekly', ask));
|
|
215
|
+
const res = await apiFetch('/api/projects', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: JSON.stringify({ repo, type, backendDir, frequency, spaceId, audit: !args.noAudit }),
|
|
218
|
+
});
|
|
219
|
+
const { token } = (await res.json());
|
|
220
|
+
const path = TOKENS_FILE();
|
|
221
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
222
|
+
writeFileSync(path, JSON.stringify({ ...readTokens(), [repo]: token }, null, 2) + '\n', { mode: 0o600 });
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(` ${green('Connected.')} Change any of this in the dashboard, under Projects.`);
|
|
225
|
+
console.log();
|
|
226
|
+
if (type === 'frontend')
|
|
227
|
+
console.log(` ${dim('A frontend needs no server package.')}`);
|
|
228
|
+
else {
|
|
229
|
+
const where = type === 'monorepo' && backendDir ? `in ${backendDir}` : 'in your backend';
|
|
230
|
+
console.log(` ${dim(`To check API keys, install the package ${where}:`)} npm i docugate`);
|
|
231
|
+
}
|
|
232
|
+
console.log();
|
|
233
|
+
console.log(` For scheduled syncs, add this token as a GitHub secret. It is shown once:`);
|
|
234
|
+
console.log(` gh secret set DOCUGATE_PROJECT_TOKEN --body ${token}`);
|
|
235
|
+
console.log(` ${dim('Then run docugate sync init --force so the workflow follows the dashboard.')}`);
|
|
236
|
+
console.log();
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error(`${red('error')} ${error.message}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
rl?.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
package/dist/rules.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export type DocugateConfig = {
|
|
|
49
49
|
title?: string;
|
|
50
50
|
role?: 'frontend' | 'backend' | 'both';
|
|
51
51
|
/** How often DocuGate checks the docs still match the code. */
|
|
52
|
+
/** False keeps package names on this machine: no vulnerability check. */
|
|
53
|
+
audit?: boolean;
|
|
52
54
|
freshness?: 'push' | 'daily' | 'weekly' | 'off';
|
|
53
55
|
/** The DocuGate space the docs publish to, as owner/slug. */
|
|
54
56
|
space?: string;
|
package/dist/rules.js
CHANGED
|
@@ -59,4 +59,4 @@ export function firstHeading(content) {
|
|
|
59
59
|
return heading ? heading[1].trim() : null;
|
|
60
60
|
}
|
|
61
61
|
export const FRESHNESS_VALUES = ['push', 'daily', 'weekly', 'off'];
|
|
62
|
-
export const KNOWN_KEYS = new Set(['$schema', 'docsDir', 'title', 'role', 'freshness', 'space', 'sidebar', 'sync', 'api']);
|
|
62
|
+
export const KNOWN_KEYS = new Set(['$schema', 'docsDir', 'title', 'role', 'freshness', 'audit', 'space', 'sidebar', 'sync', 'api']);
|
package/dist/sync/command.js
CHANGED
|
@@ -10,6 +10,7 @@ import { defaultGit, SyncError } from './changes.js';
|
|
|
10
10
|
import { installHook, removeHook } from './hook.js';
|
|
11
11
|
import { runSync } from './run.js';
|
|
12
12
|
import { repoFromRemote } from './pr.js';
|
|
13
|
+
import { printFindings, projectToken, readSettings, reportRun, runAudit } from '../project.js';
|
|
13
14
|
import { writeWorkflow, WORKFLOW_FILE } from './workflow.js';
|
|
14
15
|
const out = (line = '') => console.log(line);
|
|
15
16
|
/** `docugate sync ...`. Returns the exit code. */
|
|
@@ -34,12 +35,37 @@ export async function syncCommand(root, args, deps = {}) {
|
|
|
34
35
|
if (args.maxFiles && !(Number.isInteger(options.maxFiles) && options.maxFiles > 0)) {
|
|
35
36
|
throw new SyncError('--max-files needs a whole number above zero.');
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// A dry run and the git hook stay local: free, private and quick.
|
|
39
|
+
const token = options.dryRun || quiet ? null : projectToken(root, deps.env ?? process.env);
|
|
40
|
+
const settings = token ? await readSettings(token) : null;
|
|
41
|
+
const trigger = (deps.env ?? process.env).GITHUB_EVENT_NAME ?? 'manual';
|
|
42
|
+
// Only an automatic wake-up waits for its turn. Asking by hand always runs.
|
|
43
|
+
if (token && settings && !settings.due && (trigger === 'push' || trigger === 'schedule')) {
|
|
44
|
+
out(` ${dim(`Not due: this project syncs ${settings.frequency === 'off' ? 'never (off in the dashboard)' : settings.frequency}.`)}`);
|
|
45
|
+
await reportRun(token, { status: 'skipped', trigger });
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
result = await runSync(root, options, {
|
|
51
|
+
...deps,
|
|
52
|
+
log: quiet ? undefined : (line) => out(` ${dim(line)}`),
|
|
53
|
+
confirm: interactive && !options.yes ? ask : undefined,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (token)
|
|
58
|
+
await reportRun(token, { status: 'failed', trigger, commit: headCommit(root), reason: error instanceof SyncError ? error.message : 'The sync stopped with an unexpected error.' });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const code = report(result, quiet);
|
|
62
|
+
if (token) {
|
|
63
|
+
await reportRun(token, runOutcome(result, trigger, headCommit(root)));
|
|
64
|
+
// An audit that fails or finds something warns. It never fails the sync.
|
|
65
|
+
if (settings?.audit)
|
|
66
|
+
printFindings(await runAudit(root, token));
|
|
67
|
+
}
|
|
68
|
+
return code;
|
|
43
69
|
}
|
|
44
70
|
catch (error) {
|
|
45
71
|
// A hook must never be the reason a commit looks broken.
|
|
@@ -52,6 +78,18 @@ export async function syncCommand(root, args, deps = {}) {
|
|
|
52
78
|
throw error;
|
|
53
79
|
}
|
|
54
80
|
}
|
|
81
|
+
function headCommit(root) {
|
|
82
|
+
try {
|
|
83
|
+
return defaultGit(['rev-parse', '--short', 'HEAD'], root).trim();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function runOutcome(result, trigger, commit) {
|
|
90
|
+
const pages = result.written.length;
|
|
91
|
+
return { status: pages ? 'updated' : 'no_changes', pages, prUrl: result.pr?.url, trigger, commit };
|
|
92
|
+
}
|
|
55
93
|
async function ask(question) {
|
|
56
94
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
57
95
|
try {
|
|
@@ -169,7 +207,7 @@ function syncInit(root, args) {
|
|
|
169
207
|
/* not a git repository: the workflow still gets written with main */
|
|
170
208
|
}
|
|
171
209
|
}
|
|
172
|
-
const result = writeWorkflow(root, { freshness, docsDir: loaded.docsDir, branch, version: packageVersion(), provider: loaded.config.sync?.provider }, args.bools.has('force'));
|
|
210
|
+
const result = writeWorkflow(root, { freshness, docsDir: loaded.docsDir, branch, version: packageVersion(), provider: loaded.config.sync?.provider, managed: Boolean(projectToken(root)) }, args.bools.has('force'));
|
|
173
211
|
out();
|
|
174
212
|
if (result.status === 'off') {
|
|
175
213
|
out(` ${yellow('freshness is "off"')}, so no workflow was written. Pick how often docs should be checked:`);
|
package/dist/sync/workflow.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type WorkflowOptions = {
|
|
|
8
8
|
/** The version to run, so a new release never changes what CI does by surprise. */
|
|
9
9
|
version: string;
|
|
10
10
|
provider?: 'anthropic' | 'openai';
|
|
11
|
+
/** Connected to DocuGate: it wakes often, and the dashboard decides whether a run is due. */
|
|
12
|
+
managed?: boolean;
|
|
11
13
|
};
|
|
12
14
|
/** The workflow file, or null when `freshness` is "off". */
|
|
13
15
|
export declare function buildWorkflow(o: WorkflowOptions): string | null;
|
package/dist/sync/workflow.js
CHANGED
|
@@ -11,54 +11,60 @@ const CRON = {
|
|
|
11
11
|
};
|
|
12
12
|
/** The workflow file, or null when `freshness` is "off". */
|
|
13
13
|
export function buildWorkflow(o) {
|
|
14
|
-
if (o.freshness === 'off')
|
|
14
|
+
if (o.freshness === 'off' && !o.managed)
|
|
15
15
|
return null;
|
|
16
16
|
const secret = o.provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
17
17
|
const docs = o.docsDir === '.' ? null : o.docsDir.replace(/^\/|\/$/g, '');
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
${on.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
18
|
+
const push = [
|
|
19
|
+
' push:',
|
|
20
|
+
` branches: [${o.branch}]`,
|
|
21
|
+
' paths-ignore:',
|
|
22
|
+
...(docs ? [` - '${docs}/**'`] : []),
|
|
23
|
+
` - '.docugate/**'`,
|
|
24
|
+
];
|
|
25
|
+
const on = o.managed
|
|
26
|
+
? [...push, ' schedule:', ` - cron: '${CRON.daily}'`]
|
|
27
|
+
: o.freshness === 'push'
|
|
28
|
+
? push
|
|
29
|
+
: [' schedule:', ` - cron: '${CRON[o.freshness]}'`];
|
|
30
|
+
const when = o.managed
|
|
31
|
+
? `Wakes on every push and every day; the DocuGate dashboard decides whether a sync is due.`
|
|
32
|
+
: null;
|
|
33
|
+
return `# Written by \`docugate sync init\`. It keeps your docs in step with your code.
|
|
34
|
+
# ${when ?? ''}${when ? '' : o.freshness === 'push' ? 'Runs on every push' : o.freshness === 'daily' ? 'Runs every day' : 'Runs every Monday'}, reads what changed, and opens a pull request with the docs
|
|
35
|
+
# edits. Nothing is merged for you. Change how often in docugate.json ("freshness"),
|
|
36
|
+
# then run \`docugate sync init --force\`.
|
|
37
|
+
name: DocuGate sync
|
|
38
|
+
|
|
39
|
+
on:
|
|
40
|
+
${on.join('\n')}
|
|
41
|
+
workflow_dispatch:
|
|
42
|
+
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
pull-requests: write
|
|
46
|
+
|
|
47
|
+
# One sync at a time, so two runs never fight over the same branch.
|
|
48
|
+
concurrency:
|
|
49
|
+
group: docugate-sync
|
|
50
|
+
cancel-in-progress: false
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
sync:
|
|
54
|
+
# A sync's own commits and branch never start another sync.
|
|
55
|
+
if: \${{ !contains(github.event.head_commit.message, '${SYNC_TAG}') && !startsWith(github.ref_name, '${SYNC_BRANCH_PREFIX}') }}
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
with:
|
|
60
|
+
fetch-depth: 0
|
|
61
|
+
- uses: actions/setup-node@v4
|
|
62
|
+
with:
|
|
63
|
+
node-version: 22
|
|
64
|
+
- run: npx --yes docugate@${o.version} sync --pr --yes
|
|
65
|
+
env:
|
|
66
|
+
${secret}: \${{ secrets.${secret} }}
|
|
67
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}${o.managed ? '\n DOCUGATE_PROJECT_TOKEN: \${{ secrets.DOCUGATE_PROJECT_TOKEN }}' : ''}
|
|
62
68
|
`;
|
|
63
69
|
}
|
|
64
70
|
/** Write the workflow. An existing one that differs is left alone unless `force`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docugate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Command-line tools for DocuGate: set up, check and prepare the documentation DocuGate reads straight from your repository. Also checks DocuGate API keys from Express, Hono and Fastify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|