@@ -7,9 +7,12 @@ import (
77 "maps"
88 "os"
99 "os/exec"
10+ "path/filepath"
11+ "runtime"
1012 "strings"
1113
1214 "github.com/actionforge/actrun-cli/utils"
15+ "github.com/go-git/go-git/v5"
1316 "github.com/google/shlex"
1417)
1518
@@ -249,3 +252,201 @@ func decodeJsonFromEnvValue[T any](envValue string) (map[string]T, error) {
249252 }
250253 return envMap , nil
251254}
255+
256+ func getRunnerOS () string {
257+ switch runtime .GOOS {
258+ case "darwin" :
259+ return "macOS"
260+ case "linux" :
261+ return "Linux"
262+ case "windows" :
263+ return "Windows"
264+ default :
265+ return runtime .GOOS
266+ }
267+ }
268+
269+ func getRunnerArch () string {
270+ switch runtime .GOARCH {
271+ case "arm64" , "aarch64" :
272+ return "ARM64"
273+ case "amd64" :
274+ return "X64"
275+ default :
276+ return runtime .GOARCH
277+ }
278+ }
279+
280+ // Extracts owner/repo from a git remote URL. Supports http and ssh formats.
281+ func parseRepoFromRemoteURL (remoteURL string ) (string , error ) {
282+ remoteURL = strings .TrimSpace (remoteURL )
283+
284+ // handle ssh format
285+ if strings .HasPrefix (remoteURL , "git@" ) {
286+ // git@github.com:user/repo.git -> user/repo
287+ colonIdx := strings .Index (remoteURL , ":" )
288+ if colonIdx == - 1 {
289+ return "" , fmt .Errorf ("invalid SSH remote URL format: %s" , remoteURL )
290+ }
291+ path := remoteURL [colonIdx + 1 :]
292+ path = strings .TrimSuffix (path , ".git" )
293+ return path , nil
294+ }
295+
296+ // handle https format
297+ if strings .HasPrefix (remoteURL , "https://" ) || strings .HasPrefix (remoteURL , "http://" ) {
298+ path := remoteURL
299+ path = strings .TrimPrefix (path , "https://" )
300+ path = strings .TrimPrefix (path , "http://" )
301+
302+ // remove the host, eg github.com
303+ slashIdx := strings .Index (path , "/" )
304+ if slashIdx == - 1 {
305+ return "" , fmt .Errorf ("invalid HTTPS remote URL format: %s" , remoteURL )
306+ }
307+ path = path [slashIdx + 1 :]
308+ path = strings .TrimSuffix (path , ".git" )
309+ return path , nil
310+ }
311+
312+ return "" , fmt .Errorf ("unsupported remote URL format: %s" , remoteURL )
313+ }
314+
315+ func SetupGitHubActionsEnv (finalEnv map [string ]string ) error {
316+ sourceWorkspace := finalEnv ["GITHUB_WORKSPACE" ]
317+ if sourceWorkspace == "" {
318+ return CreateErr (nil , nil , "GITHUB_WORKSPACE environment variable is required" ).
319+ SetHint ("Set GITHUB_WORKSPACE to the path of a git repository." )
320+ }
321+
322+ eventName := finalEnv ["GITHUB_EVENT_NAME" ]
323+ if eventName == "" {
324+ return CreateErr (nil , nil , "GITHUB_EVENT_NAME environment variable is required" ).
325+ SetHint ("Set GITHUB_EVENT_NAME to the event that triggered the workflow (e.g., push, pull_request)." )
326+ }
327+
328+ repo , err := git .PlainOpenWithOptions (sourceWorkspace , & git.PlainOpenOptions {
329+ DetectDotGit : true ,
330+ })
331+ if err != nil {
332+ return CreateErr (nil , err , "unable to open git repository at GITHUB_WORKSPACE" ).
333+ SetHint ("Ensure GITHUB_WORKSPACE points to a valid git repository." )
334+ }
335+
336+ remote , err := repo .Remote ("origin" )
337+ if err != nil {
338+ return CreateErr (nil , err , "remote \" origin\" not found in git repository" ).
339+ SetHint ("Your repository must have a GitHub remote named \" origin\" ." )
340+ }
341+
342+ remoteURLs := remote .Config ().URLs
343+ if len (remoteURLs ) == 0 {
344+ return CreateErr (nil , nil , "remote \" origin\" has no URLs configured" ).
345+ SetHint ("Set the origin URL with: git remote set-url origin <url>" )
346+ }
347+
348+ repoName , err := parseRepoFromRemoteURL (remoteURLs [0 ])
349+ if err != nil {
350+ return CreateErr (nil , err , "unable to parse repository from remote URL" ).
351+ SetHint ("Ensure the origin remote URL is a valid GitHub repository URL." )
352+ }
353+
354+ head , err := repo .Head ()
355+ if err != nil {
356+ return CreateErr (nil , err , "failed to get git HEAD" ).
357+ SetHint ("Ensure you have at least one commit in the repository." )
358+ }
359+
360+ // here we default to main if we are not in a branch
361+ branch := "main"
362+ if head .Name ().IsBranch () {
363+ branch = head .Name ().Short ()
364+ }
365+
366+ sha := head .Hash ().String ()
367+
368+ // create RUNNER_WORKSPACE with an empty directory for the actual GITHUB_WORKSPACE
369+ runnerWorkspace , err := os .MkdirTemp ("" , "actrun-runner-" )
370+ if err != nil {
371+ return CreateErr (nil , err , "failed to create runner workspace directory" ).
372+ SetHint ("Check that you have write permissions to the system temp directory." )
373+ }
374+
375+ // extract repo name for the workspace dir name
376+ repoParts := strings .Split (repoName , "/" )
377+ repoBaseName := repoParts [len (repoParts )- 1 ]
378+
379+ // here create the actual GITHUB_WORKSPACE inside the runner workspace
380+ githubWorkspace := filepath .Join (runnerWorkspace , repoBaseName )
381+ if err := os .MkdirAll (githubWorkspace , 0755 ); err != nil {
382+ return CreateErr (nil , err , "failed to create github workspace directory" ).
383+ SetHint ("Check that you have write permissions to the system temp directory." )
384+ }
385+
386+ // create temp dir for runner files
387+ tempDir , err := os .MkdirTemp ("" , "actrun-" )
388+ if err != nil {
389+ return CreateErr (nil , err , "failed to create temp directory" ).
390+ SetHint ("Check that you have write permissions to the system temp directory." )
391+ }
392+
393+ homeDir , err := os .UserHomeDir ()
394+ if err != nil {
395+ return CreateErr (nil , err , "failed to get home directory" ).
396+ SetHint ("Ensure the HOME environment variable is set correctly." )
397+ }
398+ toolCacheDir := filepath .Join (homeDir , ".actrun" , "tool-cache" )
399+
400+ setIfNotSet := func (key , value string ) {
401+ if finalEnv [key ] == "" {
402+ finalEnv [key ] = value
403+ }
404+ }
405+
406+ setIfNotSet ("CI" , "true" )
407+ setIfNotSet ("GITHUB_ACTIONS" , "true" )
408+ setIfNotSet ("GITHUB_REPOSITORY" , repoName )
409+ setIfNotSet ("GITHUB_REF" , "refs/heads/" + branch )
410+ setIfNotSet ("GITHUB_REF_NAME" , branch )
411+ setIfNotSet ("GITHUB_SHA" , sha )
412+ setIfNotSet ("RUNNER_OS" , getRunnerOS ())
413+ setIfNotSet ("RUNNER_ARCH" , getRunnerArch ())
414+ setIfNotSet ("RUNNER_TOOL_CACHE" , toolCacheDir )
415+ setIfNotSet ("GITHUB_OUTPUT" , filepath .Join (tempDir , "output" ))
416+ setIfNotSet ("GITHUB_ENV" , filepath .Join (tempDir , "env" ))
417+ setIfNotSet ("GITHUB_PATH" , filepath .Join (tempDir , "path" ))
418+ setIfNotSet ("GITHUB_STATE" , filepath .Join (tempDir , "state" ))
419+ setIfNotSet ("GITHUB_STEP_SUMMARY" , filepath .Join (tempDir , "summary" ))
420+ setIfNotSet ("RUNNER_TEMP" , tempDir )
421+
422+ // override a few envs here no matter if they were set or not
423+ finalEnv ["GITHUB_WORKSPACE" ] = githubWorkspace
424+ finalEnv ["RUNNER_WORKSPACE" ] = runnerWorkspace
425+
426+ err = os .MkdirAll (toolCacheDir , 0755 )
427+ if err != nil {
428+ return CreateErr (nil , err , "failed to create tool cache directory" ).
429+ SetHint ("Check that you have write permissions to %s." , toolCacheDir )
430+ }
431+
432+ fileCommandFiles := []string {
433+ finalEnv ["GITHUB_OUTPUT" ],
434+ finalEnv ["GITHUB_ENV" ],
435+ finalEnv ["GITHUB_PATH" ],
436+ finalEnv ["GITHUB_STATE" ],
437+ finalEnv ["GITHUB_STEP_SUMMARY" ],
438+ }
439+
440+ for _ , filePath := range fileCommandFiles {
441+ if filePath != "" {
442+ f , err := os .Create (filePath )
443+ if err != nil {
444+ return CreateErr (nil , err , "failed to create file command file %s" , filePath ).
445+ SetHint ("Check that you have write permissions to the runner temp directory." )
446+ }
447+ f .Close ()
448+ }
449+ }
450+
451+ return nil
452+ }
0 commit comments