@@ -8,12 +8,15 @@ import (
88 "fmt"
99 "io"
1010 "os"
11+ "os/exec"
1112 "strings"
1213
1314 "github.com/spf13/cobra"
1415 "golang.org/x/term"
1516
1617 "github.com/CircleCI-Public/chunk-cli/internal/config"
18+ "github.com/CircleCI-Public/chunk-cli/internal/gitremote"
19+ "github.com/CircleCI-Public/chunk-cli/internal/gitutil"
1720 "github.com/CircleCI-Public/chunk-cli/internal/iostream"
1821 "github.com/CircleCI-Public/chunk-cli/internal/sidecar"
1922 "github.com/CircleCI-Public/chunk-cli/internal/tui"
@@ -141,7 +144,13 @@ func newValidateCmd() *cobra.Command {
141144 }
142145 }
143146
144- execErr := runValidate (cmd .Context (), workDir , name , inlineCmd , save , sidecarID , identityFile , workdir , cfg , statusFn , streams )
147+ results , execErr := runValidate (cmd .Context (), workDir , name , inlineCmd , save , sidecarID , identityFile , workdir , cfg , statusFn , streams )
148+
149+ if results != nil {
150+ if treeSHA , treeErr := gitutil .ComputeTreeSHA (workDir ); treeErr == nil {
151+ _ = validate .SaveResults (treeSHA , results )
152+ }
153+ }
145154
146155 if hook != nil {
147156 maxAttempts := cfg .StopHookMaxAttempts
@@ -164,12 +173,15 @@ func newValidateCmd() *cobra.Command {
164173 cmd .Flags ().BoolVar (& save , "save" , false , "Save --cmd to .chunk/config.json" )
165174 cmd .Flags ().StringVar (& projectDir , "project" , "" , "Override project directory" )
166175
176+ cmd .AddCommand (newPostCommitCmd ())
177+
167178 return cmd
168179}
169180
170181// runValidate dispatches to the appropriate Run* function based on the
171182// provided options. It is shared by both direct and hook invocations.
172- func runValidate (ctx context.Context , workDir , name , inlineCmd string , save bool , sidecarID , identityFile , workdir string , cfg * config.ProjectConfig , statusFn iostream.StatusFunc , streams iostream.Streams ) error {
183+ // Returns nil results for remote/sidecar runs (no result recording on those paths).
184+ func runValidate (ctx context.Context , workDir , name , inlineCmd string , save bool , sidecarID , identityFile , workdir string , cfg * config.ProjectConfig , statusFn iostream.StatusFunc , streams iostream.Streams ) ([]validate.CommandResult , error ) {
173185 // --cmd: inline command
174186 if inlineCmd != "" {
175187 cmdName := name
@@ -178,16 +190,16 @@ func runValidate(ctx context.Context, workDir, name, inlineCmd string, save bool
178190 }
179191 if save {
180192 if err := config .SaveCommand (workDir , cmdName , inlineCmd ); err != nil {
181- return & userError {msg : "Could not save command to .chunk/config.json." , err : err }
193+ return nil , & userError {msg : "Could not save command to .chunk/config.json." , err : err }
182194 }
183195 streams .ErrPrintf ("%s\n " , ui .Success (fmt .Sprintf ("Saved %s to .chunk/config.json" , cmdName )))
184196 }
185197 if sidecarID != "" {
186198 execFn , dest , err := openSSHSession (ctx , sidecarID , identityFile , workdir , streams )
187199 if err != nil {
188- return err
200+ return nil , err
189201 }
190- return validate .RunRemoteInline (ctx , execFn , cmdName , inlineCmd , dest , streams )
202+ return nil , validate .RunRemoteInline (ctx , execFn , cmdName , inlineCmd , dest , streams )
191203 }
192204 return validate .RunInline (ctx , workDir , cmdName , inlineCmd , statusFn , streams )
193205 }
@@ -196,16 +208,16 @@ func runValidate(ctx context.Context, workDir, name, inlineCmd string, save bool
196208 if sidecarID != "" {
197209 execFn , dest , err := openSSHSession (ctx , sidecarID , identityFile , workdir , streams )
198210 if err != nil {
199- return err
211+ return nil , err
200212 }
201- return validate .RunRemote (ctx , execFn , cfg , name , dest , streams )
213+ return nil , validate .RunRemote (ctx , execFn , cfg , name , dest , streams )
202214 }
203215
204216 // Named command
205217 if name != "" {
206218 if cfg .FindCommand (name ) == nil {
207219 if ! term .IsTerminal (int (os .Stdin .Fd ())) {
208- return & userError {
220+ return nil , & userError {
209221 msg : fmt .Sprintf ("Command %q is not configured." , name ),
210222 suggestion : "Add it to .chunk/config.json." ,
211223 errMsg : fmt .Sprintf ("command %q is not configured" , name ),
@@ -216,28 +228,99 @@ func runValidate(ctx context.Context, workDir, name, inlineCmd string, save bool
216228 streams .ErrPrintf ("What command should %s run? " , ui .Bold (name ))
217229 scanner := bufio .NewScanner (os .Stdin )
218230 if ! scanner .Scan () {
219- return & userError {msg : "No command entered." , errMsg : "no input received" }
231+ return nil , & userError {msg : "No command entered." , errMsg : "no input received" }
220232 }
221233 input := strings .TrimSpace (scanner .Text ())
222234 if input == "" {
223235 streams .ErrPrintln (ui .Dim ("No command entered, aborting." ))
224- return & userError {msg : "No command entered." , errMsg : "no command entered" }
236+ return nil , & userError {msg : "No command entered." , errMsg : "no command entered" }
225237 }
226238 if err := config .SaveCommand (workDir , name , input ); err != nil {
227- return & userError {msg : "Could not save command to .chunk/config.json." , err : err }
239+ return nil , & userError {msg : "Could not save command to .chunk/config.json." , err : err }
228240 }
229241 streams .ErrPrintf ("%s\n " , ui .Success (fmt .Sprintf ("Saved %s to .chunk/config.json" , name )))
230242 var err error
231243 cfg , err = config .LoadProjectConfig (workDir )
232244 if err != nil {
233- return err
245+ return nil , err
234246 }
235247 }
236- return mapValidateError (validate .RunNamed (ctx , workDir , name , cfg , statusFn , streams ))
248+ results , err := validate .RunNamed (ctx , workDir , name , cfg , statusFn , streams )
249+ return results , mapValidateError (err )
237250 }
238251
239252 // Run all
240- return mapValidateError (validate .RunAll (ctx , workDir , cfg , statusFn , streams ))
253+ results , err := validate .RunAll (ctx , workDir , cfg , statusFn , streams )
254+ return results , mapValidateError (err )
255+ }
256+
257+ func newPostCommitCmd () * cobra.Command {
258+ var projectDir string
259+
260+ cmd := & cobra.Command {
261+ Use : "post-commit" ,
262+ Short : "Report validate results as GitHub commit statuses" ,
263+ SilenceUsage : true ,
264+ RunE : func (cmd * cobra.Command , _ []string ) error {
265+ streams := iostream .FromCmd (cmd )
266+ ctx := cmd .Context ()
267+
268+ workDir := projectDir
269+ if workDir == "" {
270+ var err error
271+ workDir , err = os .Getwd ()
272+ if err != nil {
273+ return err
274+ }
275+ }
276+
277+ treeOut , err := exec .Command ("git" , "-C" , workDir , "rev-parse" , "HEAD^{tree}" ).Output ()
278+ if err != nil {
279+ return fmt .Errorf ("resolve HEAD tree: %w" , err )
280+ }
281+ treeSHA := strings .TrimSpace (string (treeOut ))
282+
283+ results , found , err := validate .LoadResults (treeSHA )
284+ if err != nil {
285+ return fmt .Errorf ("load validate results: %w" , err )
286+ }
287+ if ! found {
288+ return nil
289+ }
290+
291+ commitOut , err := exec .Command ("git" , "-C" , workDir , "rev-parse" , "HEAD" ).Output ()
292+ if err != nil {
293+ return fmt .Errorf ("resolve HEAD: %w" , err )
294+ }
295+ commitSHA := strings .TrimSpace (string (commitOut ))
296+
297+ org , repo , err := gitremote .DetectOrgAndRepo (workDir )
298+ if err != nil {
299+ return fmt .Errorf ("detect repo: %w" , err )
300+ }
301+
302+ ghClient , err := ensureGitHubClient (ctx , streams , tui .PromptHidden )
303+ if err != nil {
304+ return err
305+ }
306+
307+ for _ , r := range results {
308+ state := "success"
309+ if ! r .Passed {
310+ state = "failure"
311+ }
312+ if postErr := ghClient .CreateCommitStatus (ctx , org , repo , commitSHA , state , "chunk/" + r .Name , "chunk validate: " + r .Name ); postErr != nil {
313+ streams .ErrPrintf (" %s\n " , ui .Warning (fmt .Sprintf ("could not post status for %s: %v" , r .Name , postErr )))
314+ continue
315+ }
316+ streams .ErrPrintf (" %s %s/%s → %s\n " , ui .Success ("posted" ), org , repo , r .Name )
317+ }
318+ return nil
319+ },
320+ }
321+
322+ cmd .Flags ().StringVar (& projectDir , "project" , "" , "Override project directory" )
323+ return cmd
241324}
242325
243326// openSSHSession establishes an SSH session to the sidecar and returns an
0 commit comments