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"