diff --git a/cli/internal/ci/vendors.go b/cli/internal/ci/vendors.go index 13bce77dc1805..f619140bc8ef4 100644 --- a/cli/internal/ci/vendors.go +++ b/cli/internal/ci/vendors.go @@ -15,6 +15,12 @@ type Vendor struct { Env vendorEnvs // EvalEnv is key/value map of environment variables that can be used to quickly determine the vendor EvalEnv map[string]string + + // The name of the environment variable that contains the current git sha + ShaEnvVar string + + // The name of the environment variable that contains the current checked out branch + BranchEnvVar string } // Vendors is a list of common CI/CD vendors (from https://github.com/watson/ci-info/blob/master/vendors.json) @@ -107,9 +113,11 @@ var Vendors = []Vendor{ Env: vendorEnvs{Any: []string{"EAS_BUILD"}}, }, { - Name: "GitHub Actions", - Constant: "GITHUB_ACTIONS", - Env: vendorEnvs{Any: []string{"GITHUB_ACTIONS"}}, + Name: "GitHub Actions", + Constant: "GITHUB_ACTIONS", + Env: vendorEnvs{Any: []string{"GITHUB_ACTIONS"}}, + ShaEnvVar: "GITHUB_SHA", + BranchEnvVar: "GITHUB_REF_NAME", }, { Name: "GitLab CI", @@ -224,9 +232,11 @@ var Vendors = []Vendor{ Env: vendorEnvs{Any: []string{"TRAVIS"}}, }, { - Name: "Vercel", - Constant: "VERCEL", - Env: vendorEnvs{Any: []string{"NOW_BUILDER", "VERCEL"}}, + Name: "Vercel", + Constant: "VERCEL", + Env: vendorEnvs{Any: []string{"NOW_BUILDER", "VERCEL"}}, + ShaEnvVar: "VERCEL_GIT_COMMIT_SHA", + BranchEnvVar: "VERCEL_GIT_COMMIT_REF", }, { Name: "Visual Studio App Center", diff --git a/cli/internal/runsummary/format_json.go b/cli/internal/runsummary/format_json.go index 76a0a40f7fdcb..b4e6da9e2dd55 100644 --- a/cli/internal/runsummary/format_json.go +++ b/cli/internal/runsummary/format_json.go @@ -63,4 +63,5 @@ type nonMonorepoRunSummary struct { EnvMode util.EnvMode `json:"envMode"` ExecutionSummary *executionSummary `json:"execution,omitempty"` Tasks []*TaskSummary `json:"tasks"` + SCM *scmState `json:"scm"` } diff --git a/cli/internal/runsummary/run_summary.go b/cli/internal/runsummary/run_summary.go index 90b37c8cc3e29..f55c6902e8976 100644 --- a/cli/internal/runsummary/run_summary.go +++ b/cli/internal/runsummary/run_summary.go @@ -64,6 +64,7 @@ type RunSummary struct { EnvMode util.EnvMode `json:"envMode"` ExecutionSummary *executionSummary `json:"execution,omitempty"` Tasks []*TaskSummary `json:"tasks"` + SCM *scmState `json:"scm"` } // NewRunSummary returns a RunSummary instance @@ -105,6 +106,7 @@ func NewRunSummary( EnvMode: globalEnvMode, Tasks: []*TaskSummary{}, GlobalHashSummary: globalHashSummary, + SCM: getSCMState(repoRoot), }, ui: ui, runType: runType, diff --git a/cli/internal/runsummary/scm_summary.go b/cli/internal/runsummary/scm_summary.go new file mode 100644 index 0000000000000..217e05a365ae0 --- /dev/null +++ b/cli/internal/runsummary/scm_summary.go @@ -0,0 +1,41 @@ +package runsummary + +import ( + "github.com/vercel/turbo/cli/internal/ci" + "github.com/vercel/turbo/cli/internal/env" + "github.com/vercel/turbo/cli/internal/scm" + "github.com/vercel/turbo/cli/internal/turbopath" +) + +type scmState struct { + Type string `json:"type"` + Sha string `json:"sha"` + Branch string `json:"branch"` +} + +// getSCMState returns the sha and branch when in a git repo +// Otherwise it should return empty strings right now. +// We my add handling of other scms and non-git tracking in the future. +func getSCMState(dir turbopath.AbsoluteSystemPath) *scmState { + allEnvVars := env.GetEnvMap() + + state := &scmState{Type: "git"} + + // If we're in CI, try to get the values we need from environment variables + if ci.IsCi() { + vendor := ci.Info() + state.Sha = allEnvVars[vendor.ShaEnvVar] + state.Branch = allEnvVars[vendor.BranchEnvVar] + } + + // Otherwise fallback to using `git` + if state.Branch == "" { + state.Branch = scm.GetCurrentBranch(dir) + } + + if state.Sha == "" { + state.Sha = scm.GetCurrentSha(dir) + } + + return state +} diff --git a/cli/internal/runsummary/spaces.go b/cli/internal/runsummary/spaces.go index cc31e301b2760..88298a2a3efc6 100644 --- a/cli/internal/runsummary/spaces.go +++ b/cli/internal/runsummary/spaces.go @@ -26,11 +26,11 @@ type spacesRunPayload struct { RepositoryPath string `json:"repositoryPath,omitempty"` // where the command was invoked from Context string `json:"context,omitempty"` // the host on which this Run was executed (e.g. Github Action, Vercel, etc) Client spacesClientSummary `json:"client"` // Details about the turbo client + GitBranch string `json:"gitBranch"` + GitSha string `json:"gitSha"` // TODO: we need to add these in // originationUser string - // gitBranch string - // gitSha string } // spacesCacheStatus is the same as TaskCacheSummary so we can convert @@ -64,6 +64,7 @@ func (rsm *Meta) newSpacesRunCreatePayload() *spacesRunPayload { if name := ci.Constant(); name != "" { context = name } + return &spacesRunPayload{ StartTime: startTime, Status: "running", @@ -71,6 +72,8 @@ func (rsm *Meta) newSpacesRunCreatePayload() *spacesRunPayload { RepositoryPath: rsm.repoPath.ToString(), Type: "TURBO", Context: context, + GitBranch: rsm.RunSummary.SCM.Branch, + GitSha: rsm.RunSummary.SCM.Sha, Client: spacesClientSummary{ ID: "turbo", Name: "Turbo", diff --git a/cli/internal/scm/scm.go b/cli/internal/scm/scm.go index e7f17c8b7374a..825401b79faf3 100644 --- a/cli/internal/scm/scm.go +++ b/cli/internal/scm/scm.go @@ -7,6 +7,9 @@ package scm import ( + "os/exec" + "strings" + "github.com/pkg/errors" "github.com/vercel/turbo/cli/internal/turbopath" @@ -51,3 +54,27 @@ func FromInRepo(repoRoot turbopath.AbsoluteSystemPath) (SCM, error) { } return newFallback(dotGitDir.Dir()) } + +// GetCurrentBranch returns the current branch +func GetCurrentBranch(dir turbopath.AbsoluteSystemPath) string { + cmd := exec.Command("git", []string{"branch", "--show-current"}...) + cmd.Dir = dir.ToString() + + out, err := cmd.Output() + if err != nil { + return "" + } + return strings.TrimRight(string(out), "\n") +} + +// GetCurrentSha returns the current SHA +func GetCurrentSha(dir turbopath.AbsoluteSystemPath) string { + cmd := exec.Command("git", []string{"rev-parse", "HEAD"}...) + cmd.Dir = dir.ToString() + + out, err := cmd.Output() + if err != nil { + return "" + } + return strings.TrimRight(string(out), "\n") +} diff --git a/cli/internal/scm/scm_test.go b/cli/internal/scm/scm_test.go new file mode 100644 index 0000000000000..e0982aecc259e --- /dev/null +++ b/cli/internal/scm/scm_test.go @@ -0,0 +1,136 @@ +package scm + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/turbopath" +) + +func TestGetCurrentBranchMain(t *testing.T) { + targetbranch := "main" + testDir := getTestDir(t, "myrepo") + originalName, originalEmail := getOriginalConfig(testDir) + + // Setup git + gitCommand(t, testDir, []string{"config", "--global", "user.email", "turbo@vercel.com"}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", "Turbobot"}) + gitCommand(t, testDir, []string{"init"}) + + gitCommand(t, testDir, []string{"checkout", "-B", targetbranch}) + branch := GetCurrentBranch(testDir) + assert.Equal(t, branch, targetbranch) + + // cleanup + gitRm(t, testDir) + gitCommand(t, testDir, []string{"config", "--global", "user.email", originalEmail}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", originalName}) +} + +func TestGetCurrentBranchNonMain(t *testing.T) { + targetbranch := "mybranch" + testDir := getTestDir(t, "myrepo") + + originalName, originalEmail := getOriginalConfig(testDir) + + // Setup git + gitCommand(t, testDir, []string{"config", "--global", "user.email", "turbo@vercel.com"}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", "Turbobot"}) + gitCommand(t, testDir, []string{"init"}) + gitCommand(t, testDir, []string{"checkout", "-B", targetbranch}) + + branch := GetCurrentBranch(testDir) + assert.Equal(t, branch, targetbranch) + + // cleanup + gitRm(t, testDir) + gitCommand(t, testDir, []string{"config", "--global", "user.email", originalEmail}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", originalName}) +} + +func TestGetCurrentSHA(t *testing.T) { + testDir := getTestDir(t, "myrepo") + originalName, originalEmail := getOriginalConfig(testDir) + + // Setup git + gitCommand(t, testDir, []string{"config", "--global", "user.email", "turbo@vercel.com"}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", "Turbobot"}) + gitCommand(t, testDir, []string{"init"}) + + // initial sha is blank because there are no commits + initSha := GetCurrentSha(testDir) + assert.True(t, initSha == "", "initial sha is empty") + + // first commit + gitCommand(t, testDir, []string{"commit", "--allow-empty", "-am", "new commit"}) + sha1 := GetCurrentSha(testDir) + assert.True(t, sha1 != "sha on commit 1 is not empty") + + // second commit + gitCommand(t, testDir, []string{"commit", "--allow-empty", "-am", "new commit"}) + sha2 := GetCurrentSha(testDir) + assert.True(t, sha2 != "", "sha on commit 2 is not empty") + assert.True(t, sha2 != sha1, "sha on commit 2 changes from commit 1") + + // cleanup + gitRm(t, testDir) + gitCommand(t, testDir, []string{"config", "--global", "user.email", originalEmail}) + gitCommand(t, testDir, []string{"config", "--global", "user.name", originalName}) +} + +// Helper functions +func getTestDir(t *testing.T, testName string) turbopath.AbsoluteSystemPath { + defaultCwd, err := os.Getwd() + if err != nil { + t.Errorf("failed to get cwd: %v", err) + } + cwd, err := fs.CheckedToAbsoluteSystemPath(defaultCwd) + if err != nil { + t.Fatalf("cwd is not an absolute directory %v: %v", defaultCwd, err) + } + + return cwd.UntypedJoin("testdata", testName) +} + +func gitRm(t *testing.T, dir turbopath.AbsoluteSystemPath) { + cmd := exec.Command("rm", []string{"-rf", ".git"}...) + cmd.Dir = dir.ToString() + if out, err := cmd.Output(); err != nil { + t.Fatalf("Failed to cleanup git dir: %s\n%v", out, err) + } +} + +func getOriginalConfig(cwd turbopath.AbsoluteSystemPath) (string, string) { + // Ignore errors. If there was an error, it's likely because there was no value for these + // configs (e.g. in CI), so git is returning non-zero status code. This is ok, and we'll use the + // zero-value empty strings. + name, _ := _gitCommand(cwd, []string{"config", "--global", "user.name"}) + email, _ := _gitCommand(cwd, []string{"config", "--global", "user.name"}) + + return name, email +} + +func gitCommand(t *testing.T, cwd turbopath.AbsoluteSystemPath, args []string) string { + out, err := _gitCommand(cwd, args) + + if err != nil { + t.Fatalf("Failed git command %s: %s\n%v", args, out, err) + } + + return string(out) +} + +func _gitCommand(cwd turbopath.AbsoluteSystemPath, args []string) (string, error) { + cmd := exec.Command("git", args...) + cmd.Dir = cwd.ToString() + out, err := cmd.CombinedOutput() + + if err != nil { + return "", err + } + + return string(out), nil +} diff --git a/cli/internal/scm/testdata/myrepo/foo b/cli/internal/scm/testdata/myrepo/foo new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/turborepo-tests/integration/tests/dry_json/monorepo.t b/turborepo-tests/integration/tests/dry_json/monorepo.t index e7c6c1f53882a..b4f6a9da4d667 100644 --- a/turborepo-tests/integration/tests/dry_json/monorepo.t +++ b/turborepo-tests/integration/tests/dry_json/monorepo.t @@ -76,6 +76,7 @@ Setup "globalCacheInputs", "id", "packages", + "scm", "tasks", "turboVersion", "version" diff --git a/turborepo-tests/integration/tests/dry_json/single_package.t b/turborepo-tests/integration/tests/dry_json/single_package.t index 3b746da67a04e..8a21295ec0a8c 100644 --- a/turborepo-tests/integration/tests/dry_json/single_package.t +++ b/turborepo-tests/integration/tests/dry_json/single_package.t @@ -82,5 +82,10 @@ Setup "globalPassthrough": null } } - ] + ], + "scm": { + "type": "git", + "sha": "[a-z0-9]+", (re) + "branch": ".+" (re) + } } diff --git a/turborepo-tests/integration/tests/dry_json/single_package_no_config.t b/turborepo-tests/integration/tests/dry_json/single_package_no_config.t index a3dfe79985c4b..f80e6068a9656 100644 --- a/turborepo-tests/integration/tests/dry_json/single_package_no_config.t +++ b/turborepo-tests/integration/tests/dry_json/single_package_no_config.t @@ -76,5 +76,10 @@ Setup "globalPassthrough": null } } - ] + ], + "scm": { + "type": "git", + "sha": "[a-z0-9]+", (re) + "branch": ".+" (re) + } } diff --git a/turborepo-tests/integration/tests/dry_json/single_package_with_deps.t b/turborepo-tests/integration/tests/dry_json/single_package_with_deps.t index 0664cd02699d3..2d6a8a5ca6a70 100644 --- a/turborepo-tests/integration/tests/dry_json/single_package_with_deps.t +++ b/turborepo-tests/integration/tests/dry_json/single_package_with_deps.t @@ -143,5 +143,10 @@ Setup "globalPassthrough": null } } - ] + ], + "scm": { + "type": "git", + "sha": "[a-z0-9]+", (re) + "branch": ".+" (re) + } } diff --git a/turborepo-tests/integration/tests/run_summary/monorepo.t b/turborepo-tests/integration/tests/run_summary/monorepo.t index 99967d4656243..e083cad4e1548 100644 --- a/turborepo-tests/integration/tests/run_summary/monorepo.t +++ b/turborepo-tests/integration/tests/run_summary/monorepo.t @@ -29,7 +29,27 @@ Setup $ FIRST=$(/bin/ls .turbo/runs/*.json | head -n1) $ SECOND=$(/bin/ls .turbo/runs/*.json | tail -n1) + $ cat $FIRST | jq 'keys' + [ + "envMode", + "execution", + "globalCacheInputs", + "id", + "packages", + "scm", + "tasks", + "turboVersion", + "version" + ] + # some top level run summary validation + $ cat $FIRST | jq '.scm' + { + "type": "git", + "sha": "[a-z0-9]+", (re) + "branch": ".+" (re) + } + $ cat $FIRST | jq '.tasks | length' 2 $ cat $FIRST | jq '.version' diff --git a/turborepo-tests/integration/tests/run_summary/single-package.t b/turborepo-tests/integration/tests/run_summary/single-package.t index fdf3c4206b2dc..03c16cb1a0141 100644 --- a/turborepo-tests/integration/tests/run_summary/single-package.t +++ b/turborepo-tests/integration/tests/run_summary/single-package.t @@ -29,6 +29,25 @@ Check "success" ] + $ cat $SUMMARY | jq 'keys' + [ + "envMode", + "execution", + "globalCacheInputs", + "id", + "scm", + "tasks", + "turboVersion", + "version" + ] + + $ cat $SUMMARY | jq '.scm' + { + "type": "git", + "sha": "[a-z0-9]+", (re) + "branch": ".+" (re) + } + $ cat $SUMMARY | jq '.execution.exitCode' 0 $ cat $SUMMARY | jq '.execution.attempted'