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

     1  package base
     2  
     3  // This file contains gerrithub related definitions etc
     4  
     5  import (
     6  	encjson "encoding/json"
     7  	"strings"
     8  
     9  	"github.com/SchemaStore/schemastore/src/schemas/json"
    10  )
    11  
    12  // trybotWorkflows is a template for trybot-based repos
    13  trybotWorkflows: {
    14  	(trybot.key): json.#Workflow & {
    15  		on: workflow_dispatch: {}
    16  	}
    17  	"\(trybot.key)_dispatch":    trybotDispatchWorkflow
    18  	"push_tip_to_\(trybot.key)": pushTipToTrybotWorkflow
    19  	"evict_caches":              evictCaches
    20  }
    21  
    22  #dispatch: {
    23  	type:         string
    24  	CL:           int
    25  	patchset:     int
    26  	targetBranch: *defaultBranch | string
    27  
    28  	let p = strings.Split("\(CL)", "")
    29  	let rightMostTwo = p[len(p)-2] + p[len(p)-1]
    30  	ref: *"refs/changes/\(rightMostTwo)/\(CL)/\(patchset)" | string
    31  }
    32  
    33  trybotDispatchWorkflow: bashWorkflow & {
    34  	#dummyDispatch?: #dispatch
    35  	name:            "Dispatch \(trybot.key)"
    36  	on: {
    37  		repository_dispatch: {}
    38  		push: {
    39  			// To enable testing of the dispatch itself
    40  			branches: [testDefaultBranch]
    41  		}
    42  	}
    43  	jobs: [string]: defaults: run: shell: "bash"
    44  	jobs: {
    45  		(trybot.key): {
    46  			"runs-on": linuxMachine
    47  
    48  			let goodDummyData = [if encjson.Marshal(#dummyDispatch) != _|_ {true}, false][0]
    49  
    50  			// We set the "on" conditions above, but this would otherwise mean we
    51  			// run for all dispatch events.
    52  			if: "${{ (\(isTestDefaultBranch) && \(goodDummyData)) || github.event.client_payload.type == '\(trybot.key)' }}"
    53  
    54  			// See the comment below about the need for cases
    55  			let cases = [
    56  				{
    57  					condition:  "!="
    58  					expr:       "fromJSON(steps.payload.outputs.value)"
    59  					nameSuffix: "fake data"
    60  				},
    61  				{
    62  					condition:  "=="
    63  					expr:       "github.event.client_payload"
    64  					nameSuffix: "repository_dispatch payload"
    65  				},
    66  			]
    67  
    68  			steps: [
    69  				writeNetrcFile,
    70  
    71  				json.#step & {
    72  					name: "Write fake payload"
    73  					id:   "payload"
    74  					if:   "github.repository == '\(githubRepositoryPath)' && \(isTestDefaultBranch)"
    75  
    76  					// Use bash heredocs so that JSON's use of double quotes does
    77  					// not get interpreted as shell.  Both in the running of the
    78  					// command itself, which itself is the echo-ing of a command to
    79  					// $GITHUB_OUTPUT.
    80  					run: #"""
    81  						cat <<EOD >> $GITHUB_OUTPUT
    82  						value<<DOE
    83  						\#(*encjson.Marshal(#dummyDispatch) | "null")
    84  						DOE
    85  						EOD
    86  						"""#
    87  				},
    88  
    89  				// GitHub does not allow steps with the same ID, even if (by virtue
    90  				// of runtime 'if' expressions) both would not actually run. So
    91  				// we have to duplciate the steps that follow with those same
    92  				// runtime expressions
    93  				//
    94  				// Hence we have to create two steps, one to trigger if the
    95  				// repository_dispatch payload is set, and one if not (i.e. we use
    96  				// the fake payload).
    97  				for v in cases {
    98  					let localBranchExpr = "local_${{ \(v.expr).targetBranch }}"
    99  					let targetBranchExpr = "${{ \(v.expr).targetBranch }}"
   100  					json.#step & {
   101  						name: "Trigger \(trybot.name) (\(v.nameSuffix))"
   102  						if:   "github.event.client_payload.type \(v.condition) '\(trybot.key)'"
   103  						run:  """
   104  						mkdir tmpgit
   105  						cd tmpgit
   106  						git init -b initialbranch
   107  						git config user.name \(botGitHubUser)
   108  						git config user.email \(botGitHubUserEmail)
   109  						git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n \(botGitHubUser):${{ secrets.\(botGitHubUserTokenSecretsKey) }} | base64)"
   110  						git remote add origin  \(gerritHubRepositoryURL)
   111  
   112  						git fetch origin ${{ \(v.expr).ref }}
   113  						git checkout -b \(localBranchExpr) FETCH_HEAD
   114  
   115  						# Error if we already have dispatchTrailer according to git log logic.
   116  						# See earlier check for GitHub expression logic check.
   117  						x="$(git log -1 --pretty='%(trailers:key=\(dispatchTrailer),valueonly)')"
   118  						if [[ "$x" != "" ]]
   119  						then
   120  							 echo "Ref ${{ \(v.expr).ref }} already has a \(dispatchTrailer)"
   121  							 exit 1
   122  						fi
   123  
   124  						# Add the trailer because we don't have it yet. GitHub expressions do not have a
   125  						# substitute or quote capability. So we do that in shell. We also strip out the
   126  						# indenting added by toJSON. We ensure that the type field is first in order
   127  						# that we can safely check for specific types of dispatch trailer.
   128  						#
   129  						# Use bash heredoc so that JSON's use of double quotes does
   130  						# not get interpreted as shell.
   131  						trailer="$(cat <<EOD | jq -r -c '{type} + .'
   132  						${{ toJSON(\(v.expr)) }}
   133  						EOD
   134  						)"
   135  						git log -1 --format=%B | git interpret-trailers --trailer "\(dispatchTrailer): $trailer" | git commit --amend -F -
   136  						git log -1
   137  
   138  						success=false
   139  						for try in {1..20}; do
   140  							echo "Push to trybot try $try"
   141  							if git push -f \(trybotRepositoryURL) \(localBranchExpr):\(targetBranchExpr); then
   142  								success=true
   143  								break
   144  							fi
   145  							sleep 1
   146  						done
   147  						if ! $success; then
   148  							echo "Giving up"
   149  							exit 1
   150  						fi
   151  						"""
   152  					}
   153  				},
   154  			]
   155  		}
   156  	}
   157  }
   158  
   159  pushTipToTrybotWorkflow: bashWorkflow & {
   160  	jobs: [string]: defaults: run: shell: "bash"
   161  
   162  	on: {
   163  		push: branches: protectedBranchPatterns
   164  	}
   165  	jobs: push: {
   166  		"runs-on": linuxMachine
   167  		if:        "${{github.repository == '\(githubRepositoryPath)'}}"
   168  	}
   169  
   170  	name: "Push tip to \(trybot.key)"
   171  
   172  	concurrency: "push_tip_to_trybot"
   173  
   174  	jobs: push: {
   175  		steps: [
   176  			writeNetrcFile,
   177  			json.#step & {
   178  				name: "Push tip to trybot"
   179  				run:  """
   180  						mkdir tmpgit
   181  						cd tmpgit
   182  						git init -b initialbranch
   183  						git config user.name \(botGitHubUser)
   184  						git config user.email \(botGitHubUserEmail)
   185  						git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n \(botGitHubUser):${{ secrets.\(botGitHubUserTokenSecretsKey) }} | base64)"
   186  						git remote add origin \(gerritHubRepositoryURL)
   187  						git remote add trybot \(trybotRepositoryURL)
   188  
   189  						git fetch origin "${{ github.ref }}"
   190  
   191  						success=false
   192  						for try in {1..20}; do
   193  							 echo "Push to trybot try $try"
   194  							 if git push -f trybot "FETCH_HEAD:${{ github.ref }}"; then
   195  								  success=true
   196  								  break
   197  							 fi
   198  							 sleep 1
   199  						done
   200  						if ! $success; then
   201  							 echo "Giving up"
   202  							 exit 1
   203  						fi
   204  						"""
   205  			},
   206  		]
   207  	}
   208  
   209  }
   210  
   211  // evictCaches removes "old" GitHub actions caches from the main repo and the
   212  // accompanying trybot  The job is only run in the main repo, because
   213  // that is the only place where the credentials exist.
   214  //
   215  // The GitHub actions caches in the main and trybot repos can get large. So
   216  // large in fact we got the following warning from GitHub:
   217  //
   218  //   "Approaching total cache storage limit (34.5 GB of 10 GB Used)"
   219  //
   220  // Yes, you did read that right.
   221  //
   222  // Not only does this have the effect of causing us to breach "limits" it also
   223  // means that we can't be sure that individual caches are not bloated.
   224  //
   225  // Fix that by purging the actions caches on a daily basis at 0200, followed 15
   226  // mins later by a re-run of the tip trybots to repopulate the caches so they
   227  // are warm and minimal.
   228  //
   229  // In testing with @mvdan, this resulted in cache sizes for Linux dropping from
   230  // ~1GB to ~125MB. This is a considerable saving.
   231  //
   232  // Note this currently removes all cache entries, regardless of whether they
   233  // are go-related or not. We should revisit this later.
   234  evictCaches: bashWorkflow & {
   235  	name: "Evict caches"
   236  
   237  	on: {
   238  		schedule: [
   239  			{cron: "0 2 * * *"},
   240  		]
   241  	}
   242  
   243  	jobs: {
   244  		test: {
   245  			// We only want to run this in the main repo
   246  			if:        "${{github.repository == '\(githubRepositoryPath)'}}"
   247  			"runs-on": linuxMachine
   248  			steps: [
   249  				for v in checkoutCode {v},
   250  
   251  				json.#step & {
   252  					name: "Delete caches"
   253  					run:  """
   254  						set -x
   255  
   256  						echo ${{ secrets.\(botGitHubUserTokenSecretsKey) }} | gh auth login --with-token
   257  						gh extension install actions/gh-actions-cache
   258  						for i in \(githubRepositoryURL) \(trybotRepositoryURL)
   259  						do
   260  							echo "Evicting caches for $i"
   261  							cd $(mktemp -d)
   262  							git init -b initialbranch
   263  							git remote add origin $i
   264  							for j in $(gh actions-cache list -L 100 | grep refs/ | awk '{print $1}')
   265  							do
   266  							   gh actions-cache delete --confirm $j
   267  							done
   268  						done
   269  						"""
   270  				},
   271  
   272  				json.#step & {
   273  					name: "Trigger workflow runs to repopulate caches"
   274  					let branchPatterns = strings.Join(protectedBranchPatterns, " ")
   275  
   276  					run: """
   277  						# Prepare git for pushes to trybot repo. Note
   278  						# because we have already checked out code we don't
   279  						# need origin. Fetch origin default branch for later use
   280  						git config user.name \(botGitHubUser)
   281  						git config user.email \(botGitHubUserEmail)
   282  						git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n \(botGitHubUser):${{ secrets.\(botGitHubUserTokenSecretsKey) }} | base64)"
   283  						git remote add trybot \(trybotRepositoryURL)
   284  
   285  						# Now trigger the most recent workflow run on each of the default branches.
   286  						# We do this by listing all the branches on the main repo and finding those
   287  						# which match the protected branch patterns (globs).
   288  						for j in $(\(curlGitHubAPI) -f https://api.github.com/repos/\(githubRepositoryPath)/branches | jq -r '.[] | .name')
   289  						do
   290  							for i in \(branchPatterns)
   291  							do
   292  								if [[ "$j" != $i ]]; then
   293  									continue
   294  								fi
   295  
   296  								echo Branch: $j
   297  								sha=$(\(curlGitHubAPI) "https://api.github.com/repos/\(githubRepositoryPath)/commits/$j" | jq -r '.sha')
   298  								echo Latest commit: $sha
   299  
   300  								echo "Trigger workflow on \(githubRepositoryPath)"
   301  								\(curlGitHubAPI) --fail-with-body -X POST https://api.github.com/repos/\(githubRepositoryPath)/actions/workflows/\(trybot.key+workflowFileExtension)/dispatches -d "{\\"ref\\":\\"$j\\"}"
   302  
   303  								# Ensure that the trybot repo has the latest commit for
   304  								# this branch.  If the force-push results in a commit
   305  								# being pushed, that will trigger the trybot workflows
   306  								# so we don't need to do anything, otherwise we need to
   307  								# trigger the most recent commit on that branch
   308  								git remote -v
   309  								git fetch origin refs/heads/$j
   310  								git log -1 FETCH_HEAD
   311  
   312  								success=false
   313  								for try in {1..20}; do
   314  									echo "Push to trybot try $try"
   315  									exitCode=0; push="$(git push -f trybot FETCH_HEAD:$j 2>&1)" || exitCode=$?
   316  									echo "$push"
   317  									if [[ $exitCode -eq 0 ]]; then
   318  										success=true
   319  										break
   320  									fi
   321  									sleep 1
   322  								done
   323  								if ! $success; then
   324  									echo "Giving up"
   325  									exit 1
   326  								fi
   327  
   328  								if echo "$push" | grep up-to-date
   329  								then
   330  									# We are up-to-date, i.e. the push did nothing, hence we need to trigger a workflow_dispatch
   331  									# in the trybot repo.
   332  									echo "Trigger workflow on \(trybotRepositoryPath)"
   333  									\(curlGitHubAPI) --fail-with-body -X POST https://api.github.com/repos/\(trybotRepositoryPath)/actions/workflows/\(trybot.key+workflowFileExtension)/dispatches -d "{\\"ref\\":\\"$j\\"}"
   334  								else
   335  									echo "Force-push to \(trybotRepositoryPath) did work; nothing to do"
   336  								fi
   337  							done
   338  						done
   339  						"""
   340  				},
   341  			]
   342  		}
   343  	}
   344  }
   345  
   346  writeNetrcFile: json.#step & {
   347  	name: "Write netrc file for \(botGerritHubUser) Gerrithub"
   348  	run:  """
   349  			cat <<EOD > ~/.netrc
   350  			machine \(gerritHubHostname)
   351  			login \(botGerritHubUser)
   352  			password ${{ secrets.\(botGerritHubUserPasswordSecretsKey) }}
   353  			EOD
   354  			chmod 600 ~/.netrc
   355  			"""
   356  }