@@ -784,7 +784,9 @@ def has_obfuscated_control(value: str) -> bool:
784784 )
785785
786786 direct = unicodedata .normalize ("NFKC" , body )
787- variants = [direct ]
787+ variants = [body ]
788+ if direct != body :
789+ variants .append (direct )
788790 control_obfuscation = has_obfuscated_control (direct )
789791 converged = False
790792 for _ in range (len (direct ) + 1 ):
@@ -805,67 +807,74 @@ def has_obfuscated_control(value: str) -> bool:
805807 break
806808 variants .append (decoded )
807809
808- normalized = variants [- 1 ]
809- normalized_paths = list (_ACTIONS_PATH .finditer (normalized ))
810810 try :
811811 from markdown_it import MarkdownIt
812812 except ImportError as exc :
813813 raise CheckError ("release-evidence Markdown validation requires markdown-it-py" ) from exc
814814
815- bindings : list [int ] = []
816- used_sentinels = {path .group ("run" ) for path in normalized_paths }
817- for index , path in enumerate (normalized_paths ):
818- run_start , run_end = path .span ("run" )
819- width = run_end - run_start
820- sentinel_value = 10 ** (width - 1 ) + index
821- sentinel = str (sentinel_value )
822- while len (sentinel ) == width and sentinel in used_sentinels :
823- sentinel_value += 1
815+ def rendered_bindings (
816+ source : str ,
817+ ) -> tuple [list [re .Match [str ]], list [tuple [str , str | None ]], list [int ]]:
818+ paths = list (_ACTIONS_PATH .finditer (source ))
819+ identities = [(path .group ("run" ), path .group ("attempt" )) for path in paths ]
820+ bindings : list [int ] = []
821+ used_sentinels = {path .group ("run" ) for path in paths }
822+ for index , path in enumerate (paths ):
823+ run_start , run_end = path .span ("run" )
824+ width = run_end - run_start
825+ sentinel_value = 10 ** (width - 1 ) + index
824826 sentinel = str (sentinel_value )
825- if len (sentinel ) != width :
826- raise CheckError ("too many Actions URL occurrences to bind unambiguously" )
827- used_sentinels .add (sentinel )
828- mutated = normalized [:run_start ] + sentinel + normalized [run_end :]
829- identity = (sentinel , path .group ("attempt" ) or None )
830- destinations : Counter [tuple [str , str | None ]] = Counter ()
831- parser = MarkdownIt ("commonmark" , {"html" : True })
832- for token in parser .parse (mutated ):
833- if token .type == "inline" :
834- link_depth = 0
835- html_parser = _ActionsAnchorParser ()
836- for child in token .children or []:
837- if child .type == "link_open" :
838- link_depth += 1
839- target = child .attrGet ("href" )
840- if isinstance (target , str ):
841- destination = _CANONICAL_ACTIONS_DESTINATION .fullmatch (target )
842- if destination is not None :
843- destinations [
844- (destination .group ("run" ), destination .group ("attempt" ))
845- ] += 1
846- elif child .type == "link_close" :
847- link_depth = max (0 , link_depth - 1 )
848- elif child .type == "html_inline" :
849- html_parser .feed (child .content )
850- elif child .type == "text" and link_depth == 0 and html_parser .anchor_depth == 0 :
851- destinations .update (_bare_actions_destinations (child .content ))
852- html_parser .close ()
853- destinations .update (html_parser .destinations )
854- elif token .type == "html_block" :
855- html_parser = _ActionsAnchorParser ()
856- html_parser .feed (token .content )
857- html_parser .close ()
858- destinations .update (html_parser .destinations )
859- bindings .append (destinations [identity ])
827+ while len (sentinel ) == width and sentinel in used_sentinels :
828+ sentinel_value += 1
829+ sentinel = str (sentinel_value )
830+ if len (sentinel ) != width :
831+ raise CheckError ("too many Actions URL occurrences to bind unambiguously" )
832+ used_sentinels .add (sentinel )
833+ mutated = source [:run_start ] + sentinel + source [run_end :]
834+ identity = (sentinel , path .group ("attempt" ) or None )
835+ destinations : Counter [tuple [str , str | None ]] = Counter ()
836+ parser = MarkdownIt ("commonmark" , {"html" : True })
837+ for token in parser .parse (mutated ):
838+ if token .type == "inline" :
839+ link_depth = 0
840+ html_parser = _ActionsAnchorParser ()
841+ for child in token .children or []:
842+ if child .type == "link_open" :
843+ link_depth += 1
844+ target = child .attrGet ("href" )
845+ if isinstance (target , str ):
846+ destination = _CANONICAL_ACTIONS_DESTINATION .fullmatch (target )
847+ if destination is not None :
848+ destinations [
849+ (destination .group ("run" ), destination .group ("attempt" ))
850+ ] += 1
851+ elif child .type == "link_close" :
852+ link_depth = max (0 , link_depth - 1 )
853+ elif child .type == "html_inline" :
854+ html_parser .feed (child .content )
855+ elif (
856+ child .type == "text"
857+ and link_depth == 0
858+ and html_parser .anchor_depth == 0
859+ ):
860+ destinations .update (_bare_actions_destinations (child .content ))
861+ html_parser .close ()
862+ destinations .update (html_parser .destinations )
863+ elif token .type == "html_block" :
864+ html_parser = _ActionsAnchorParser ()
865+ html_parser .feed (token .content )
866+ html_parser .close ()
867+ destinations .update (html_parser .destinations )
868+ bindings .append (destinations [identity ])
869+ return paths , identities , bindings
870+
871+ rendered_variants = [rendered_bindings (variant ) for variant in variants ]
872+ normalized_paths , normalized_identities , bindings = rendered_variants [- 1 ]
860873
861874 malformed = control_obfuscation or not converged or any (count != 1 for count in bindings )
862- normalized_identities = [
863- (match .group ("run" ), match .group ("attempt" )) for match in normalized_paths
864- ]
865875 malformed = malformed or any (
866- [(match .group ("run" ), match .group ("attempt" )) for match in _ACTIONS_PATH .finditer (variant )]
867- != normalized_identities
868- for variant in variants [:- 1 ]
876+ identities != normalized_identities or variant_bindings != bindings
877+ for _ , identities , variant_bindings in rendered_variants [:- 1 ]
869878 )
870879
871880 if malformed :
@@ -874,8 +883,7 @@ def has_obfuscated_control(value: str) -> bool:
874883 line ,
875884 f"release-evidence block for { version } contains a non-canonical Actions URL; "
876885 "use the exact repository /actions/runs/<positive-id> URL as plain Markdown "
877- "text, an autolink, or a link destination, with an optional "
878- "/attempts/<positive-id> suffix" ,
886+ "text, an autolink, or a link destination without an attempt suffix" ,
879887 )
880888 )
881889 run_values = {match .group ("run" ) for match in normalized_paths }
@@ -888,10 +896,24 @@ def has_obfuscated_control(value: str) -> bool:
888896 "outside the immutable evidence block" ,
889897 )
890898 )
891- canonical_workflow_count = sum (
892- count == 1 and path .group ("run" ) == expected_run
899+ matching_base_count = sum (
900+ count == 1 and path .group ("run" ) == expected_run and path . group ( "attempt" ) is None
893901 for path , count in zip (normalized_paths , bindings , strict = True )
894902 )
903+ canonical_workflow_count = (
904+ 1
905+ if normalized_identities == [(expected_run , None )] and bindings == [1 ] and not malformed
906+ else 0
907+ )
908+ if len (normalized_paths ) != 1 or matching_base_count != 1 :
909+ findings .append (
910+ (
911+ line ,
912+ f"release-evidence block for { version } must contain exactly one canonical "
913+ "release workflow URL without an attempt suffix and no other Actions URL; "
914+ "record the attempt number as plain provenance text" ,
915+ )
916+ )
895917 return findings , canonical_workflow_count
896918
897919
@@ -1049,10 +1071,9 @@ def exact_release_version_violations(
10491071 if document == "CHANGELOG.md" :
10501072 return []
10511073 if document in RELEASE_EVIDENCE_DOCUMENTS :
1052- structural = unicodedata .normalize ("NFKC" , text )
10531074 searchable , marker_findings , _ = _evidence_block_violations (
10541075 document ,
1055- structural ,
1076+ text ,
10561077 )
10571078 normalized_searchable = _normalized_document_text (searchable )
10581079 searchable = normalized_searchable
@@ -1074,8 +1095,7 @@ def release_evidence_identities(
10741095 """Return internally validated immutable identities from an evidence document."""
10751096 if document not in RELEASE_EVIDENCE_DOCUMENTS :
10761097 return {}
1077- structural = unicodedata .normalize ("NFKC" , text )
1078- _ , findings , identities = _evidence_block_violations (document , structural )
1098+ _ , findings , identities = _evidence_block_violations (document , text )
10791099 if findings :
10801100 rendered = "; " .join (f"{ document } :{ line } : { label } " for line , label in findings )
10811101 raise CheckError (rendered )
0 commit comments