@@ -14,7 +14,8 @@ const { execSync } = require("child_process");
1414 * BRANCH_NAME: Branch name to push to
1515 * MAX_FILE_SIZE: Maximum file size in bytes
1616 * MAX_FILE_COUNT: Maximum number of files per commit
17- * FILE_GLOB_FILTER: Optional space-separated list of file patterns (e.g., "*.md *.txt")
17+ * FILE_GLOB_FILTER: Optional space-separated list of file patterns (e.g., "*.md metrics/ ** /*")
18+ * Supports * (matches any chars except /) and ** (matches any chars including /)
1819 * GH_TOKEN: GitHub token for authentication
1920 * GITHUB_RUN_ID: Workflow run ID for commit messages
2021 */
@@ -86,47 +87,70 @@ async function main() {
8687 fs . mkdirSync ( destMemoryPath , { recursive : true } ) ;
8788 core . info ( `Destination directory: ${ destMemoryPath } ` ) ;
8889
89- // Read files from artifact directory and validate before copying
90+ // Recursively scan and collect files from artifact directory
9091 let filesToCopy = [ ] ;
91- try {
92- const files = fs . readdirSync ( sourceMemoryPath , { withFileTypes : true } ) ;
93-
94- for ( const file of files ) {
95- if ( ! file . isFile ( ) ) {
96- continue ; // Skip directories
97- }
98-
99- const fileName = file . name ;
100- const sourceFilePath = path . join ( sourceMemoryPath , fileName ) ;
101- const stats = fs . statSync ( sourceFilePath ) ;
102-
103- // Validate file name patterns if filter is set
104- if ( fileGlobFilter ) {
105- const patterns = fileGlobFilter . split ( / \s + / ) . map ( pattern => {
106- // Escape backslashes first to prevent escaping issues, then escape dots and convert asterisks
107- const regexPattern = pattern . replace ( / \\ / g, "\\\\" ) . replace ( / \. / g, "\\." ) . replace ( / \* / g, "[^/]*" ) ;
108- return new RegExp ( `^${ regexPattern } $` ) ;
109- } ) ;
110-
111- if ( ! patterns . some ( pattern => pattern . test ( fileName ) ) ) {
112- core . error ( `File does not match allowed patterns: ${ fileName } ` ) ;
113- core . error ( `Allowed patterns: ${ fileGlobFilter } ` ) ;
114- core . setFailed ( "File pattern validation failed" ) ;
115- return ;
92+
93+ /**
94+ * Recursively scan directory and collect files
95+ * @param {string } dirPath - Directory to scan
96+ * @param {string } relativePath - Relative path from sourceMemoryPath (for nested files)
97+ */
98+ function scanDirectory ( dirPath , relativePath = "" ) {
99+ const entries = fs . readdirSync ( dirPath , { withFileTypes : true } ) ;
100+
101+ for ( const entry of entries ) {
102+ const fullPath = path . join ( dirPath , entry . name ) ;
103+ const relativeFilePath = relativePath ? path . join ( relativePath , entry . name ) : entry . name ;
104+
105+ if ( entry . isDirectory ( ) ) {
106+ // Recursively scan subdirectory
107+ scanDirectory ( fullPath , relativeFilePath ) ;
108+ } else if ( entry . isFile ( ) ) {
109+ const stats = fs . statSync ( fullPath ) ;
110+
111+ // Validate file name patterns if filter is set
112+ if ( fileGlobFilter ) {
113+ const patterns = fileGlobFilter . split ( / \s + / ) . map ( pattern => {
114+ // Convert glob pattern to regex that supports directory wildcards
115+ // ** matches any path segment (including /)
116+ // * matches any characters except /
117+ let regexPattern = pattern
118+ . replace ( / \\ / g, "\\\\" ) // Escape backslashes
119+ . replace ( / \. / g, "\\." ) // Escape dots
120+ . replace ( / \* \* / g, "<!DOUBLESTAR>" ) // Temporarily replace **
121+ . replace ( / \* / g, "[^/]*" ) // Single * matches non-slash chars
122+ . replace ( / < ! D O U B L E S T A R > / g, ".*" ) ; // ** matches everything including /
123+ return new RegExp ( `^${ regexPattern } $` ) ;
124+ } ) ;
125+
126+ if ( ! patterns . some ( pattern => pattern . test ( relativeFilePath ) ) ) {
127+ core . error ( `File does not match allowed patterns: ${ relativeFilePath } ` ) ;
128+ core . error ( `Allowed patterns: ${ fileGlobFilter } ` ) ;
129+ core . setFailed ( "File pattern validation failed" ) ;
130+ throw new Error ( "File pattern validation failed" ) ;
131+ }
116132 }
133+
134+ // Validate file size
135+ if ( stats . size > maxFileSize ) {
136+ core . error ( `File exceeds size limit: ${ relativeFilePath } (${ stats . size } bytes > ${ maxFileSize } bytes)` ) ;
137+ core . setFailed ( "File size validation failed" ) ;
138+ throw new Error ( "File size validation failed" ) ;
139+ }
140+
141+ filesToCopy . push ( {
142+ relativePath : relativeFilePath ,
143+ source : fullPath ,
144+ size : stats . size
145+ } ) ;
117146 }
118-
119- // Validate file size
120- if ( stats . size > maxFileSize ) {
121- core . error ( `File exceeds size limit: ${ fileName } (${ stats . size } bytes > ${ maxFileSize } bytes)` ) ;
122- core . setFailed ( "File size validation failed" ) ;
123- return ;
124- }
125-
126- filesToCopy . push ( { name : fileName , source : sourceFilePath , size : stats . size } ) ;
127147 }
148+ }
149+
150+ try {
151+ scanDirectory ( sourceMemoryPath ) ;
128152 } catch ( error ) {
129- core . setFailed ( `Failed to read artifact directory: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
153+ core . setFailed ( `Failed to scan artifact directory: ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
130154 return ;
131155 }
132156
@@ -143,14 +167,20 @@ async function main() {
143167
144168 core . info ( `Copying ${ filesToCopy . length } validated file(s)...` ) ;
145169
146- // Copy files to destination
170+ // Copy files to destination (preserving directory structure)
147171 for ( const file of filesToCopy ) {
148- const destFilePath = path . join ( destMemoryPath , file . name ) ;
172+ const destFilePath = path . join ( destMemoryPath , file . relativePath ) ;
173+ const destDir = path . dirname ( destFilePath ) ;
174+
149175 try {
176+ // Ensure destination directory exists
177+ fs . mkdirSync ( destDir , { recursive : true } ) ;
178+
179+ // Copy file
150180 fs . copyFileSync ( file . source , destFilePath ) ;
151- core . info ( `Copied: ${ file . name } (${ file . size } bytes)` ) ;
181+ core . info ( `Copied: ${ file . relativePath } (${ file . size } bytes)` ) ;
152182 } catch ( error ) {
153- core . setFailed ( `Failed to copy file ${ file . name } : ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
183+ core . setFailed ( `Failed to copy file ${ file . relativePath } : ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
154184 return ;
155185 }
156186 }
0 commit comments