11import json
22import os
33from datetime import datetime , timezone
4- from typing import Any
5- from zoneinfo import ZoneInfo
4+ from pathlib import Path
5+ from typing import Any , Optional
66
77
8- class CICDInputError (Exception ): pass
8+ class CICDInputError (Exception ):
9+ pass
910
1011
1112Unset = object ()
1213
1314
15+ def _escape_data (s ) -> str :
16+ return str (s ).replace ("%" , "%25" ).replace ("\r " , "%0D" ).replace ("\n " , "%0A" )
17+
18+
19+ def _escape_property (s ) -> str :
20+ return (
21+ str (s )
22+ .replace ("%" , "%25" )
23+ .replace ("\r " , "%0D" )
24+ .replace ("\n " , "%0A" )
25+ .replace (":" , "%3A" )
26+ .replace ("," , "%2C" )
27+ )
28+
29+
1430class Input :
1531 def get (self , name : str , default = Unset ):
1632 env = os .getenv (name , default )
1733
1834 if env is Unset :
1935 raise CICDInputError (f"Cannot find input { name } . Previous jobs have probably failed." )
20-
21- return env
36+
37+ return env
2238
2339 def get_timestamp (self , name : str ) -> datetime :
2440 env = os .environ [name ]
@@ -28,12 +44,11 @@ def get_timestamp(self, name: str) -> datetime:
2844 except :
2945 return datetime .strptime (env , "%Y-%m-%dT%H:%M:%SZ" ).replace (tzinfo = timezone .utc )
3046
31-
32- def get_json (self , name : str , default = Unset ):
33- env = os .getenv (name ,)
34-
35- if not env :
36- if default is Unset :
47+ def get_json (self , name : str , default = Unset ):
48+ env = os .getenv (name )
49+
50+ if not env :
51+ if default is Unset :
3752 raise CICDInputError (f"Cannot find input { name } . Previous jobs have probably failed." )
3853 else :
3954 return default
@@ -52,5 +67,63 @@ def save_json(self, name: str, value: Any):
5267 f .write (f"{ name } ={ data } \n " )
5368
5469
70+ class Annotation :
71+ def error (
72+ self ,
73+ message : str ,
74+ file : Optional [Path | str ] = None ,
75+ line : Optional [int ] = None ,
76+ col : Optional [int ] = None ,
77+ ):
78+ self ._print ("error" , message , file , line , col )
79+
80+ def warning (
81+ self ,
82+ message : str ,
83+ file : Optional [Path | str ] = None ,
84+ line : Optional [int ] = None ,
85+ col : Optional [int ] = None ,
86+ ):
87+ self ._print ("warning" , message , file , line , col )
88+
89+ def notice (
90+ self ,
91+ message : str ,
92+ file : Optional [Path | str ] = None ,
93+ line : Optional [int ] = None ,
94+ col : Optional [int ] = None ,
95+ ):
96+ self ._print ("notice" , message , file , line , col )
97+
98+ def _print (
99+ self ,
100+ level : str ,
101+ message : str ,
102+ file : Optional [Path | str ] = None ,
103+ line : Optional [int ] = None ,
104+ col : Optional [int ] = None ,
105+ ):
106+ if file is not None :
107+ print (f"In file { file } :{ line } :{ col } :" )
108+
109+ props = []
110+
111+ if file is not None :
112+ props .append (f"file={ _escape_property (file )} " )
113+
114+ if line is not None :
115+ props .append (f"line={ line } " )
116+
117+ if col is not None :
118+ props .append (f"col={ col } " )
119+
120+ props_str = "," .join (props )
121+ if props_str :
122+ props_str = " " + props_str
123+
124+ print (f"::{ level } { props_str } ::{ _escape_data (message )} " )
125+
126+
55127input = Input ()
56128output = Output ()
129+ annotation = Annotation ()
0 commit comments