@@ -89,6 +89,12 @@ const MULTI_RESULT_ORDER = ["failed", "drift", "partial", "skipped", "applied",
8989/** The visibility filter values that narrow the pool; any other value keeps every repo, as the action does. */
9090const VISIBILITY_FILTERS : ReadonlySet < string > = new Set ( [ "public" , "private" , "internal" ] ) ;
9191
92+ /** Every character an owner or repository name holds, "-" first so a rewritten class reads it as a literal. */
93+ const NAME_CHARACTERS = "-._0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ;
94+
95+ /** A glob class, its opener and members apart, or one letter outside a class. */
96+ const CLASS_OR_LETTER = / \[ ( [ ! ^ ] ? ) ( [ ^ \] ] * ) \] | [ a - z ] / gi;
97+
9298/** Verbatim: the fold for every key GitHub matches exactly. */
9399const same = ( name : string ) : string => name ;
94100
@@ -736,53 +742,35 @@ export function predictMulti(meta: MultiScenarioMeta): MultiPrediction {
736742 return { repos, allowedExitCodes : multiExitCodes ( repos ) , forbidden } ;
737743}
738744
739- /** A pattern with "/" matches the full slug, otherwise the name portion, mirroring the repos-dir <name>.yml vs <owner>/<name>.yml split. */
740- function globTarget ( pattern : string , slug : string ) : string {
741- return ( pattern . includes ( "/" ) ? slug : ( slug . split ( "/" ) [ 1 ] ?? slug ) ) . toLowerCase ( ) ;
742- }
743-
744- /** Whether one pattern unit takes the target character under it: a literal match, or a wildcard. */
745- function globUnitMatches ( unit : string | undefined , char : string | undefined ) : boolean {
746- return unit !== undefined && ( unit === char || unit === "*" || unit === "?" ) ;
745+ /**
746+ * GitHub compares names case-insensitively and Bun.Glob has no mode for it, so the pattern is widened to admit
747+ * either case the way the action's nocase regex does: a letter outside a class becomes "[xX]", and a class
748+ * becomes the name characters it admits in either case. Folding the target instead would misread a range that
749+ * spans the cases: lowercased, "[0-Z]" is "[0-z]" and gains "_".
750+ */
751+ function eitherCase ( pattern : string ) : string {
752+ return pattern . replace ( CLASS_OR_LETTER , ( unit , opener : string , members : string | undefined ) => {
753+ if ( members === undefined ) {
754+ return `[${ unit . toLowerCase ( ) } ${ unit . toUpperCase ( ) } ]` ;
755+ }
756+ const admits = new Bun . Glob ( `[${ members } ]` ) ;
757+ const twin = ( char : string ) : string =>
758+ char === char . toLowerCase ( ) ? char . toUpperCase ( ) : char . toLowerCase ( ) ;
759+ const admitted = [ ...NAME_CHARACTERS ] . filter (
760+ ( char ) => admits . match ( char ) || admits . match ( twin ( char ) ) ,
761+ ) ;
762+ return `[${ opener } ${ admitted . join ( "" ) } ]` ;
763+ } ) ;
747764}
748765
749766/**
750- * An INDEPENDENT glob matcher, deliberately not src's compileExcludePattern (a picomatch compile), so a bug in
751- * either surfaces as a disagreement; it covers the forms the scenarios write ("*", "?", a leading "!"), not
752- * the whole picomatch grammar .
767+ * The runtime's glob, not src's compileExcludePattern (a picomatch compile), so a bug in either surfaces as a
768+ * disagreement. A pattern with "/" matches the full slug, otherwise the name portion, mirroring the repos-dir
769+ * <name>.yml vs <owner>/<name>.yml split .
753770 */
754771function globMatches ( pattern : string , slug : string ) : boolean {
755- if ( pattern . startsWith ( "!" ) ) {
756- return ! globMatches ( pattern . slice ( 1 ) , slug ) ;
757- }
758- const target = globTarget ( pattern , slug ) ;
759- const pat = pattern . toLowerCase ( ) ;
760- let p = 0 ;
761- let t = 0 ;
762- let star = - 1 ;
763- let mark = 0 ;
764- while ( t < target . length ) {
765- if ( globUnitMatches ( pat [ p ] , target [ t ] ) ) {
766- if ( pat [ p ] === "*" ) {
767- star = p ;
768- mark = t ;
769- p ++ ;
770- } else {
771- p ++ ;
772- t ++ ;
773- }
774- } else if ( star === - 1 ) {
775- return false ;
776- } else {
777- p = star + 1 ;
778- mark ++ ;
779- t = mark ;
780- }
781- }
782- while ( p < pat . length && pat [ p ] === "*" ) {
783- p ++ ;
784- }
785- return p === pat . length ;
772+ const target = pattern . includes ( "/" ) ? slug : ( slug . split ( "/" ) [ 1 ] ?? slug ) ;
773+ return new Bun . Glob ( eitherCase ( pattern ) ) . match ( target ) ;
786774}
787775
788776/** The action's documented filter rules over one pool repo, in the engine's attribution order. */
0 commit comments