@@ -13,50 +13,21 @@ function getArgValue(flag, fallback = null) {
1313 return value ?? fallback ;
1414}
1515
16- const docsDir = getArgValue ( '--dir ' , 'docs/github-documentation' ) ;
16+ const manifestPath = getArgValue ( '--manifest ' , 'docs/github-documentation/watch-list.json ' ) ;
1717const outputPath = getArgValue ( '--output' , null ) ;
1818const stateDir = getArgValue ( '--state-dir' , null ) ;
1919const remoteSnapshotsDir = getArgValue ( '--remote-snapshots-dir' , null ) ;
2020const useLocal = args . includes ( '--use-local' ) ;
21- const updateFrontmatter = args . includes ( '--update-frontmatter' ) ;
22- const replaceBody = args . includes ( '--replace-body' ) ;
21+ const updateManifest = args . includes ( '--update-manifest' ) || args . includes ( '--update-frontmatter' ) ;
2322const writeState = args . includes ( '--write-state' ) ;
2423const failOnChange = args . includes ( '--fail-on-change' ) ;
2524const writeSummary = args . includes ( '--write-summary' ) ;
2625const includeDiffSnippets = args . includes ( '--include-diff-snippets' ) ;
2726
28- if ( replaceBody && ! updateFrontmatter ) {
29- throw new Error ( '--replace-body requires --update-frontmatter' ) ;
30- }
31-
3227if ( writeState && ! stateDir ) {
3328 throw new Error ( '--write-state requires --state-dir' ) ;
3429}
3530
36- function splitFrontmatter ( text ) {
37- const match = text . match ( / ^ - - - \s * \r ? \n ( [ \s \S ] * ?) \r ? \n - - - \s * \r ? \n ? / ) ;
38- if ( ! match ) {
39- return null ;
40- }
41- const frontmatter = match [ 1 ] ;
42- const body = text . slice ( match [ 0 ] . length ) ;
43- return { frontmatter, body } ;
44- }
45-
46- function parseFrontmatterMap ( frontmatter ) {
47- const map = new Map ( ) ;
48- for ( const line of frontmatter . split ( / \r ? \n / ) ) {
49- const trimmed = line . trim ( ) ;
50- if ( ! trimmed ) continue ;
51- const idx = line . indexOf ( ':' ) ;
52- if ( idx === - 1 ) continue ;
53- const key = line . slice ( 0 , idx ) . trim ( ) ;
54- const value = line . slice ( idx + 1 ) . trim ( ) ;
55- map . set ( key , value ) ;
56- }
57- return map ;
58- }
59-
6031function normalizeBody ( text ) {
6132 const normalized = text . replace ( / \r \n / g, '\n' ) ;
6233 return normalized . replace ( / \n ? $ / , '\n' ) ;
@@ -89,52 +60,18 @@ function findFirstDiff(localBody, remoteBody, withSnippets) {
8960 return null ;
9061}
9162
92- function updateFrontmatterHash ( frontmatter , newHash ) {
93- const lines = frontmatter . split ( / \r ? \n / ) ;
94- const existingIndex = lines . findIndex ( ( line ) => line . trim ( ) . startsWith ( 'content-sha256:' ) ) ;
95- const newLine = `content-sha256: ${ newHash } ` ;
96-
97- if ( existingIndex !== - 1 ) {
98- lines [ existingIndex ] = newLine ;
99- } else {
100- const redirectIndex = lines . findIndex ( ( line ) => line . trim ( ) . startsWith ( 'redirect-link:' ) ) ;
101- if ( redirectIndex !== - 1 ) {
102- lines . splice ( redirectIndex + 1 , 0 , newLine ) ;
103- } else {
104- lines . push ( newLine ) ;
105- }
106- }
107-
108- return lines . join ( '\n' ) ;
109- }
110-
11163function resolveStateSnapshotPath ( filePath ) {
11264 if ( ! stateDir ) return null ;
11365 return path . join ( stateDir , 'snapshots' , path . basename ( filePath ) ) ;
11466}
11567
116- function readBaselineBody ( filePath , fallbackBody ) {
68+ function readBaselineBody ( filePath ) {
11769 const snapshotPath = resolveStateSnapshotPath ( filePath ) ;
118- if ( snapshotPath ) {
119- if ( fs . existsSync ( snapshotPath ) ) {
120- return {
121- body : normalizeBody ( fs . readFileSync ( snapshotPath , 'utf8' ) ) ,
122- source : 'state' ,
123- snapshotPath,
124- } ;
125- }
70+ if ( snapshotPath && fs . existsSync ( snapshotPath ) ) {
12671 return {
127- body : '' ,
128- source : 'none' ,
129- snapshotPath : null ,
130- } ;
131- }
132-
133- if ( fallbackBody . trim ( ) . length > 0 ) {
134- return {
135- body : normalizeBody ( fallbackBody ) ,
136- source : 'repo' ,
137- snapshotPath : null ,
72+ body : normalizeBody ( fs . readFileSync ( snapshotPath , 'utf8' ) ) ,
73+ source : 'state' ,
74+ snapshotPath,
13875 } ;
13976 }
14077
@@ -150,6 +87,51 @@ function writeBody(filePath, body) {
15087 fs . writeFileSync ( filePath , normalizeBody ( body ) , 'utf8' ) ;
15188}
15289
90+ function loadManifest ( filePath ) {
91+ if ( ! fs . existsSync ( filePath ) ) {
92+ throw new Error ( `Manifest file not found: ${ filePath } ` ) ;
93+ }
94+
95+ const raw = JSON . parse ( fs . readFileSync ( filePath , 'utf8' ) ) ;
96+ const documents = Array . isArray ( raw ) ? raw : raw . documents ;
97+
98+ if ( ! Array . isArray ( documents ) ) {
99+ throw new Error ( `Manifest must contain a documents array: ${ filePath } ` ) ;
100+ }
101+
102+ const normalizedDocs = documents . map ( ( doc , idx ) => {
103+ const file = String ( doc . file ?? '' ) . trim ( ) ;
104+ const redirectLink = String ( doc . redirect_link ?? '' ) . trim ( ) ;
105+ const expectedHash = doc . content_sha256 ? String ( doc . content_sha256 ) . trim ( ) : null ;
106+
107+ if ( ! file ) {
108+ throw new Error ( `Manifest document at index ${ idx } is missing file` ) ;
109+ }
110+
111+ if ( ! redirectLink ) {
112+ throw new Error ( `Manifest document at index ${ idx } is missing redirect_link` ) ;
113+ }
114+
115+ return {
116+ file,
117+ markdown_link : doc . markdown_link ? String ( doc . markdown_link ) . trim ( ) : null ,
118+ redirect_link : redirectLink ,
119+ content_sha256 : expectedHash ,
120+ } ;
121+ } ) ;
122+
123+ normalizedDocs . sort ( ( a , b ) => a . file . localeCompare ( b . file ) ) ;
124+
125+ return {
126+ root : raw ,
127+ documents : normalizedDocs ,
128+ write ( updatedDocuments ) {
129+ const payload = Array . isArray ( raw ) ? updatedDocuments : { ...raw , documents : updatedDocuments } ;
130+ fs . writeFileSync ( filePath , JSON . stringify ( payload , null , 2 ) + '\n' , 'utf8' ) ;
131+ } ,
132+ } ;
133+ }
134+
153135async function fetchRemoteBody ( url ) {
154136 const response = await fetch ( url , {
155137 headers : {
@@ -163,35 +145,18 @@ async function fetchRemoteBody(url) {
163145}
164146
165147async function main ( ) {
166- const entries = fs
167- . readdirSync ( docsDir )
168- . filter ( ( file ) => file . endsWith ( '.md' ) )
169- . filter ( ( file ) => file . toLowerCase ( ) !== 'readme.md' )
170- . sort ( )
171- . map ( ( file ) => path . join ( docsDir , file ) ) ;
148+ const manifest = loadManifest ( manifestPath ) ;
172149
173150 const results = [ ] ;
174151 let changedCount = 0 ;
175152
176- for ( const file of entries ) {
177- const text = fs . readFileSync ( file , 'utf8' ) ;
178- const parsed = splitFrontmatter ( text ) ;
179- if ( ! parsed ) {
180- throw new Error ( `Missing frontmatter in ${ file } ` ) ;
181- }
182-
183- const frontmatterMap = parseFrontmatterMap ( parsed . frontmatter ) ;
184- const redirectLink = frontmatterMap . get ( 'redirect-link' ) ;
185- const expectedHash = frontmatterMap . get ( 'content-sha256' ) ?? null ;
186-
187- if ( ! redirectLink ) {
188- throw new Error ( `Missing redirect-link in ${ file } ` ) ;
189- }
153+ const updatedDocuments = [ ] ;
190154
191- const baseline = readBaselineBody ( file , parsed . body ) ;
192- const remoteBody = useLocal ? baseline . body : normalizeBody ( await fetchRemoteBody ( redirectLink ) ) ;
155+ for ( const doc of manifest . documents ) {
156+ const baseline = readBaselineBody ( doc . file ) ;
157+ const remoteBody = useLocal ? baseline . body : normalizeBody ( await fetchRemoteBody ( doc . redirect_link ) ) ;
193158 const actualHash = sha256 ( remoteBody ) ;
194- const changed = expectedHash !== actualHash ;
159+ const changed = doc . content_sha256 !== actualHash ;
195160
196161 if ( changed ) changedCount += 1 ;
197162
@@ -201,30 +166,31 @@ async function main() {
201166
202167 let remoteSnapshotFile = null ;
203168 if ( remoteSnapshotsDir ) {
204- const remotePath = path . join ( remoteSnapshotsDir , path . basename ( file ) ) ;
169+ const remotePath = path . join ( remoteSnapshotsDir , path . basename ( doc . file ) ) ;
205170 writeBody ( remotePath , remoteBody ) ;
206171 remoteSnapshotFile = path . relative ( process . cwd ( ) , remotePath ) ;
207172 }
208173
209174 if ( writeState ) {
210- const snapshotPath = resolveStateSnapshotPath ( file ) ;
175+ const snapshotPath = resolveStateSnapshotPath ( doc . file ) ;
211176 if ( ! snapshotPath ) {
212- throw new Error ( `Unable to resolve state snapshot path for ${ file } ` ) ;
177+ throw new Error ( `Unable to resolve state snapshot path for ${ doc . file } ` ) ;
213178 }
214179 writeBody ( snapshotPath , remoteBody ) ;
215180 }
216181
217- if ( updateFrontmatter ) {
218- const nextFrontmatter = updateFrontmatterHash ( parsed . frontmatter , actualHash ) ;
219- const nextBody = replaceBody ? remoteBody : parsed . body ;
220- const nextText = `---\n ${ nextFrontmatter } \n---\n ${ nextBody } ` ;
221- fs . writeFileSync ( file , nextText , 'utf8' ) ;
222- }
182+ const updatedDoc = {
183+ ... doc ,
184+ content_sha256 : updateManifest ? actualHash : doc . content_sha256 ,
185+ } ;
186+
187+ updatedDocuments . push ( updatedDoc ) ;
223188
224189 results . push ( {
225- file : path . relative ( process . cwd ( ) , file ) ,
226- redirect_link : redirectLink ,
227- expected_hash : expectedHash ,
190+ file : doc . file ,
191+ markdown_link : doc . markdown_link ,
192+ redirect_link : doc . redirect_link ,
193+ expected_hash : doc . content_sha256 ,
228194 actual_hash : actualHash ,
229195 changed,
230196 baseline_source : baseline . source ,
@@ -234,11 +200,16 @@ async function main() {
234200 } ) ;
235201 }
236202
203+ if ( updateManifest ) {
204+ manifest . write ( updatedDocuments ) ;
205+ }
206+
237207 const payload = {
238208 changed : changedCount > 0 ,
239209 changed_count : changedCount ,
240210 state_dir : stateDir ,
241211 remote_snapshots_dir : remoteSnapshotsDir ,
212+ manifest_path : manifestPath ,
242213 results,
243214 } ;
244215
0 commit comments