cuelang.org/go@v0.10.1/internal/ci/base/github.cue (about)

     1  package base
     2  
     3  // This file contains aspects principally related to GitHub workflows
     4  
     5  import (
     6  	encjson "encoding/json"
     7  	"list"
     8  	"strings"
     9  	"strconv"
    10  
    11  	"github.com/SchemaStore/schemastore/src/schemas/json"
    12  )
    13  
    14  bashWorkflow: json.#Workflow & {
    15  	jobs: [string]: defaults: run: shell: "bash"
    16  }
    17  
    18  installGo: json.#step & {
    19  	name: "Install Go"
    20  	uses: "actions/setup-go@v5"
    21  	with: {
    22  		// We do our own caching in setupGoActionsCaches.
    23  		cache:        false
    24  		"go-version": string
    25  	}
    26  }
    27  
    28  checkoutCode: {
    29  	#actionsCheckout: json.#step & {
    30  		name: "Checkout code"
    31  		uses: "actions/checkout@v4"
    32  
    33  		// "pull_request" builds will by default use a merge commit,
    34  		// testing the PR's HEAD merged on top of the master branch.
    35  		// For consistency with Gerrit, avoid that merge commit entirely.
    36  		// This doesn't affect builds by other events like "push",
    37  		// since github.event.pull_request is unset so ref remains empty.
    38  		with: {
    39  			ref:           "${{ github.event.pull_request.head.sha }}"
    40  			"fetch-depth": 0 // see the docs below
    41  		}
    42  	}
    43  
    44  	[
    45  		#actionsCheckout,
    46  
    47  		// Restore modified times to work around https://go.dev/issues/58571,
    48  		// as otherwise we would get lots of unnecessary Go test cache misses.
    49  		// Note that this action requires actions/checkout to use a fetch-depth of 0.
    50  		// Since this is a third-party action which runs arbitrary code,
    51  		// we pin a commit hash for v2 to be in control of code updates.
    52  		// Also note that git-restore-mtime does not update all directories,
    53  		// per the bug report at https://github.com/MestreLion/git-tools/issues/47,
    54  		// so we first reset all directory timestamps to a static time as a fallback.
    55  		// TODO(mvdan): May be unnecessary once the Go bug above is fixed.
    56  		json.#step & {
    57  			name: "Reset git directory modification times"
    58  			run:  "touch -t 202211302355 $(find * -type d)"
    59  		},
    60  		json.#step & {
    61  			name: "Restore git file modification times"
    62  			uses: "chetan/git-restore-mtime-action@075f9bc9d159805603419d50f794bd9f33252ebe"
    63  		},
    64  
    65  		{
    66  			json.#step & {
    67  				name: "Try to extract \(dispatchTrailer)"
    68  				id:   dispatchTrailerStepID
    69  				run:  """
    70  					x="$(git log -1 --pretty='%(trailers:key=\(dispatchTrailer),valueonly)')"
    71  					if [[ "$x" == "" ]]
    72  					then
    73  					   # Some steps rely on the presence or otherwise of the Dispatch-Trailer.
    74  					   # We know that we don't have a Dispatch-Trailer in this situation,
    75  					   # hence we use the JSON value null in order to represent that state.
    76  					   # This means that GitHub expressions can determine whether a Dispatch-Trailer
    77  					   # is present or not by checking whether the fromJSON() result of the
    78  					   # output from this step is the JSON value null or not.
    79  					   x=null
    80  					fi
    81  					echo "\(_dispatchTrailerDecodeStepOutputVar)<<EOD" >> $GITHUB_OUTPUT
    82  					echo "$x" >> $GITHUB_OUTPUT
    83  					echo "EOD" >> $GITHUB_OUTPUT
    84  					"""
    85  			}
    86  		},
    87  
    88  		// Safety nets to flag if we ever have a Dispatch-Trailer slip through the
    89  		// net and make it to master
    90  		json.#step & {
    91  			name: "Check we don't have \(dispatchTrailer) on a protected branch"
    92  			if:   "\(isProtectedBranch) && \(containsDispatchTrailer)"
    93  			run:  """
    94  				echo "\(_dispatchTrailerVariable) contains \(dispatchTrailer) but we are on a protected branch"
    95  				false
    96  				"""
    97  		},
    98  	]
    99  }
   100  
   101  earlyChecks: json.#step & {
   102  	name: "Early git and code sanity checks"
   103  	run: "go run ./internal/ci/checks"
   104  }
   105  
   106  curlGitHubAPI: {
   107  	#tokenSecretsKey: *botGitHubUserTokenSecretsKey | string
   108  
   109  	#"""
   110  	curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.\#(#tokenSecretsKey) }}" -H "X-GitHub-Api-Version: 2022-11-28"
   111  	"""#
   112  }
   113  
   114  setupGoActionsCaches: {
   115  	// #readonly determines whether we ever want to write the cache back. The
   116  	// writing of a cache back (for any given cache key) should only happen on a
   117  	// protected branch. But running a workflow on a protected branch often
   118  	// implies that we want to skip the cache to ensure we catch flakes early.
   119  	// Hence the concept of clearing the testcache to ensure we catch flakes
   120  	// early can be defaulted based on #readonly. In the general case the two
   121  	// concepts are orthogonal, hence they are kept as two parameters, even
   122  	// though in our case we could get away with a single parameter that
   123  	// encapsulates our needs.
   124  	#readonly:       *false | bool
   125  	#cleanTestCache: *!#readonly | bool
   126  	#goVersion:      string
   127  	#additionalCacheDirs: [...string]
   128  	#os: string
   129  
   130  	let goModCacheDirID = "go-mod-cache-dir"
   131  	let goCacheDirID = "go-cache-dir"
   132  
   133  	// cacheDirs is a convenience variable that includes
   134  	// GitHub expressions that represent the directories
   135  	// that participate in Go caching.
   136  	let cacheDirs = list.Concat([[
   137  		"${{ steps.\(goModCacheDirID).outputs.dir }}/cache/download",
   138  		"${{ steps.\(goCacheDirID).outputs.dir }}",
   139  	], #additionalCacheDirs])
   140  
   141  	let cacheRestoreKeys = "\(#os)-\(#goVersion)"
   142  
   143  	let cacheStep = json.#step & {
   144  		with: {
   145  			path: strings.Join(cacheDirs, "\n")
   146  
   147  			// GitHub actions caches are immutable. Therefore, use a key which is
   148  			// unique, but allow the restore to fallback to the most recent cache.
   149  			// The result is then saved under the new key which will benefit the
   150  			// next build. Restore keys are only set if the step is restore.
   151  			key:            "\(cacheRestoreKeys)-${{ github.run_id }}"
   152  			"restore-keys": cacheRestoreKeys
   153  		}
   154  	}
   155  
   156  	let readWriteCacheExpr = "(\(isProtectedBranch) || \(isTestDefaultBranch))"
   157  
   158  	// pre is the list of steps required to establish and initialise the correct
   159  	// caches for Go-based workflows.
   160  	[
   161  		// TODO: once https://github.com/actions/setup-go/issues/54 is fixed,
   162  		// we could use `go env` outputs from the setup-go step.
   163  		json.#step & {
   164  			name: "Get go mod cache directory"
   165  			id:   goModCacheDirID
   166  			run:  #"echo "dir=$(go env GOMODCACHE)" >> ${GITHUB_OUTPUT}"#
   167  		},
   168  		json.#step & {
   169  			name: "Get go build/test cache directory"
   170  			id:   goCacheDirID
   171  			run:  #"echo "dir=$(go env GOCACHE)" >> ${GITHUB_OUTPUT}"#
   172  		},
   173  
   174  		// Only if we are not running in readonly mode do we want a step that
   175  		// uses actions/cache (read and write). Even then, the use of the write
   176  		// step should be predicated on us running on a protected branch. Because
   177  		// it's impossible for anything else to write such a cache.
   178  		if !#readonly {
   179  			cacheStep & {
   180  				if:   readWriteCacheExpr
   181  				uses: "actions/cache@v4"
   182  			}
   183  		},
   184  
   185  		cacheStep & {
   186  			// If we are readonly, there is no condition on when we run this step.
   187  			// It should always be run, becase there is no alternative. But if we
   188  			// are not readonly, then we need to predicate this step on us not
   189  			// being on a protected branch.
   190  			if !#readonly {
   191  				if: "! \(readWriteCacheExpr)"
   192  			}
   193  
   194  			uses: "actions/cache/restore@v4"
   195  		},
   196  
   197  		if #cleanTestCache {
   198  			// All tests on protected branches should skip the test cache.  The
   199  			// canonical way to do this is with -count=1. However, we want the
   200  			// resulting test cache to be valid and current so that subsequent CLs
   201  			// in the trybot repo can leverage the updated cache. Therefore, we
   202  			// instead perform a clean of the testcache.
   203  			//
   204  			// Critically we only want to do this in the main repo, not the trybot
   205  			// repo.
   206  			json.#step & {
   207  				if:  "github.repository == '\(githubRepositoryPath)' && (\(isProtectedBranch) || github.ref == 'refs/heads/\(testDefaultBranch)')"
   208  				run: "go clean -testcache"
   209  			}
   210  		},
   211  	]
   212  }
   213  
   214  // isProtectedBranch is an expression that evaluates to true if the
   215  // job is running as a result of pushing to one of protectedBranchPatterns.
   216  // It would be nice to use the "contains" builtin for simplicity,
   217  // but array literals are not yet supported in expressions.
   218  isProtectedBranch: {
   219  	#trailers: [...string]
   220  	"((" + strings.Join([for branch in protectedBranchPatterns {
   221  		(_matchPattern & {variable: "github.ref", pattern: "refs/heads/\(branch)"}).expr
   222  	}], " || ") + ") && (! \(containsDispatchTrailer)))"
   223  }
   224  
   225  // isTestDefaultBranch is an expression that evaluates to true if
   226  // the job is running on the testDefaultBranch
   227  isTestDefaultBranch: "(github.ref == 'refs/heads/\(testDefaultBranch)')"
   228  
   229  // #isReleaseTag creates a GitHub expression, based on the given release tag
   230  // pattern, that evaluates to true if called in the context of a workflow that
   231  // is part of a release.
   232  isReleaseTag: {
   233  	(_matchPattern & {variable: "github.ref", pattern: "refs/tags/\(releaseTagPattern)"}).expr
   234  }
   235  
   236  checkGitClean: json.#step & {
   237  	name: "Check that git is clean at the end of the job"
   238  	if:   "always()"
   239  	run:  "test -z \"$(git status --porcelain)\" || (git status; git diff; false)"
   240  }
   241  
   242  repositoryDispatch: json.#step & {
   243  	#githubRepositoryPath:         *githubRepositoryPath | string
   244  	#botGitHubUserTokenSecretsKey: *botGitHubUserTokenSecretsKey | string
   245  	#arg:                          _
   246  
   247  	_curlGitHubAPI: curlGitHubAPI & {#tokenSecretsKey: #botGitHubUserTokenSecretsKey, _}
   248  
   249  	name: string
   250  	run:  #"""
   251  			\#(_curlGitHubAPI) --fail --request POST --data-binary \#(strconv.Quote(encjson.Marshal(#arg))) https://api.github.com/repos/\#(#githubRepositoryPath)/dispatches
   252  			"""#
   253  }
   254  
   255  // dispatchTrailer is the trailer that we use to pass information in a commit
   256  // when triggering workflow events in other GitHub repos.
   257  //
   258  // NOTE: keep this consistent with gerritstatusupdater parsing logic.
   259  dispatchTrailer: "Dispatch-Trailer"
   260  
   261  // dispatchTrailerStepID is the ID of the step that attempts
   262  // to extract a Dispatch-Trailer value from the commit at HEAD
   263  dispatchTrailerStepID: strings.Replace(dispatchTrailer, "-", "", -1)
   264  
   265  // _dispatchTrailerDecodeStepOutputVar is the name of the output
   266  // variable int he dispatchTrailerStepID step
   267  _dispatchTrailerDecodeStepOutputVar: "value"
   268  
   269  // dispatchTrailerExpr is a GitHub expression that can be dereferenced
   270  // to get values from the JSON-decded Dispatch-Trailer value that
   271  // is extracted during the dispatchTrailerStepID step.
   272  dispatchTrailerExpr: "fromJSON(steps.\(dispatchTrailerStepID).outputs.\(_dispatchTrailerDecodeStepOutputVar))"
   273  
   274  // containsDispatchTrailer returns a GitHub expression that looks at the commit
   275  // message of the head commit of the event that triggered the workflow, an
   276  // expression that returns true if the commit message associated with that head
   277  // commit contains dispatchTrailer.
   278  //
   279  // Note that this logic does not 100% match the answer that would be returned by:
   280  //
   281  //      git log --pretty=%(trailers:key=Dispatch-Trailer,valueonly)
   282  //
   283  // GitHub expressions are incredibly limited in their capabilities:
   284  //
   285  //     https://docs.github.com/en/actions/learn-github-actions/expressions
   286  //
   287  // There is not even a regular expression matcher. Hence the logic is a best-efforts
   288  // approximation of the logic employed by git log.
   289  containsDispatchTrailer: {
   290  	#type?: string
   291  
   292  	// If we have a value for #type, then match against that value.
   293  	// Otherwise the best we can do is match against:
   294  	//
   295  	//     Dispatch-Trailer: {"type:}
   296  	//
   297  	let _typeCheck = [if #type != _|_ {#type + "\""}, ""][0]
   298  	"""
   299  	(contains(\(_dispatchTrailerVariable), '\n\(dispatchTrailer): {"type":"\(_typeCheck)'))
   300  	"""
   301  }
   302  
   303  containsTrybotTrailer: containsDispatchTrailer & {
   304  	#type: trybot.key
   305  	_
   306  }
   307  
   308  containsUnityTrailer: containsDispatchTrailer & {
   309  	#type: unity.key
   310  	_
   311  }
   312  
   313  _dispatchTrailerVariable: "github.event.head_commit.message"