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