diff --git a/.github/workflows/evict_caches.yml b/.github/workflows/evict_caches.yml new file mode 100644 index 0000000..d87282c --- /dev/null +++ b/.github/workflows/evict_caches.yml @@ -0,0 +1,129 @@ +# Code generated internal/ci/ci_tool.cue; DO NOT EDIT. + +name: Evict caches +"on": + schedule: + - cron: 0 2 * * * +jobs: + test: + if: ${{github.repository == 'cue-lang/vscode-cue'}} + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Reset git directory modification times + run: touch -t 202211302355 $(find * -type d) + - name: Restore git file modification times + uses: chetan/git-restore-mtime-action@075f9bc9d159805603419d50f794bd9f33252ebe + - id: DispatchTrailer + name: Try to extract Dispatch-Trailer + run: |- + x="$(git log -1 --pretty='%(trailers:key=Dispatch-Trailer,valueonly)')" + if [[ "$x" == "" ]] + then + # Some steps rely on the presence or otherwise of the Dispatch-Trailer. + # We know that we don't have a Dispatch-Trailer in this situation, + # hence we use the JSON value null in order to represent that state. + # This means that GitHub expressions can determine whether a Dispatch-Trailer + # is present or not by checking whether the fromJSON() result of the + # output from this step is the JSON value null or not. + x=null + fi + echo "value<> $GITHUB_OUTPUT + echo "$x" >> $GITHUB_OUTPUT + echo "EOD" >> $GITHUB_OUTPUT + - if: |- + ((github.ref == 'refs/heads/master') && (! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')))) && (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')) + name: Check we don't have Dispatch-Trailer on a protected branch + run: |- + echo "github.event.head_commit.message contains Dispatch-Trailer but we are on a protected branch" + false + - name: Delete caches + run: |- + set -x + + echo ${{ secrets.CUECKOO_GITHUB_PAT }} | gh auth login --with-token + gh extension install actions/gh-actions-cache + for i in https://github.com/cue-lang/vscode-cue https://github.com/cue-lang/vscode-cue-trybot + do + echo "Evicting caches for $i" + cd $(mktemp -d) + git init -b initialbranch + git remote add origin $i + for j in $(gh actions-cache list -L 100 | grep refs/ | awk '{print $1}') + do + gh actions-cache delete --confirm $j + done + done + - name: Trigger workflow runs to repopulate caches + run: |- + # Prepare git for pushes to trybot repo. Note + # because we have already checked out code we don't + # need origin. Fetch origin default branch for later use + git config user.name cueckoo + git config user.email cueckoo@gmail.com + git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n cueckoo:${{ secrets.CUECKOO_GITHUB_PAT }} | base64)" + git remote add trybot https://github.com/cue-lang/vscode-cue-trybot + + # Now trigger the most recent workflow run on each of the default branches. + # We do this by listing all the branches on the main repo and finding those + # which match the protected branch patterns (globs). + for j in $(curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.CUECKOO_GITHUB_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" -f https://api.github.com/repos/cue-lang/vscode-cue/branches | jq -r '.[] | .name') + do + for i in master + do + if [[ "$j" != $i ]]; then + continue + fi + + echo Branch: $j + sha=$(curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.CUECKOO_GITHUB_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/cue-lang/vscode-cue/commits/$j" | jq -r '.sha') + echo Latest commit: $sha + + echo "Trigger workflow on cue-lang/vscode-cue" + curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.CUECKOO_GITHUB_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" --fail-with-body -X POST https://api.github.com/repos/cue-lang/vscode-cue/actions/workflows/trybot.yml/dispatches -d "{\"ref\":\"$j\"}" + + # Ensure that the trybot repo has the latest commit for + # this branch. If the force-push results in a commit + # being pushed, that will trigger the trybot workflows + # so we don't need to do anything, otherwise we need to + # trigger the most recent commit on that branch + git remote -v + git fetch origin refs/heads/$j + git log -1 FETCH_HEAD + + success=false + for try in {1..20}; do + echo "Push to trybot try $try" + exitCode=0; push="$(git push -f trybot FETCH_HEAD:$j 2>&1)" || exitCode=$? + echo "$push" + if [[ $exitCode -eq 0 ]]; then + success=true + break + fi + sleep 1 + done + if ! $success; then + echo "Giving up" + exit 1 + fi + + if echo "$push" | grep up-to-date + then + # We are up-to-date, i.e. the push did nothing, hence we need to trigger a workflow_dispatch + # in the trybot repo. + echo "Trigger workflow on cue-lang/vscode-cue-trybot" + curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.CUECKOO_GITHUB_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" --fail-with-body -X POST https://api.github.com/repos/cue-lang/vscode-cue-trybot/actions/workflows/trybot.yml/dispatches -d "{\"ref\":\"$j\"}" + else + echo "Force-push to cue-lang/vscode-cue-trybot did work; nothing to do" + fi + done + done diff --git a/.github/workflows/push_tip_to_trybot.yml b/.github/workflows/push_tip_to_trybot.yml new file mode 100644 index 0000000..aaa1829 --- /dev/null +++ b/.github/workflows/push_tip_to_trybot.yml @@ -0,0 +1,50 @@ +# Code generated internal/ci/ci_tool.cue; DO NOT EDIT. + +name: Push tip to trybot +"on": + push: + branches: + - master +concurrency: push_tip_to_trybot +jobs: + push: + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + if: ${{github.repository == 'cue-lang/vscode-cue'}} + steps: + - name: Write netrc file for cueckoo Gerrithub + run: |- + cat < ~/.netrc + machine review.gerrithub.io + login cueckoo + password ${{ secrets.CUECKOO_GERRITHUB_PASSWORD }} + EOD + chmod 600 ~/.netrc + - name: Push tip to trybot + run: |- + mkdir tmpgit + cd tmpgit + git init -b initialbranch + git config user.name cueckoo + git config user.email cueckoo@gmail.com + git config http.https://github.com/.extraheader "AUTHORIZATION: basic $(echo -n cueckoo:${{ secrets.CUECKOO_GITHUB_PAT }} | base64)" + git remote add origin https://review.gerrithub.io/a/cue-lang/vscode-cue + git remote add trybot https://github.com/cue-lang/vscode-cue-trybot + + git fetch origin "${{ github.ref }}" + + success=false + for try in {1..20}; do + echo "Push to trybot try $try" + if git push -f trybot "FETCH_HEAD:${{ github.ref }}"; then + success=true + break + fi + sleep 1 + done + if ! $success; then + echo "Giving up" + exit 1 + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index df42fff..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,32 +0,0 @@ -on: - push: - branches: - - master - pull_request: - branches: - - '**' - -name: Test -jobs: - test: - strategy: - fail-fast: false - matrix: - go-version: [1.16.6] - platform: [ubuntu-latest] - runs-on: ${{ matrix.platform }} - steps: - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v2 - - name: Generate - run: go generate ./... - - name: Test - run: go test ./... - - name: Tidy - run: go mod tidy - - name: Ensure commit is clean - run: test -z "$(git status --porcelain)" || (git status; git diff; false) diff --git a/.github/workflows/trybot.yml b/.github/workflows/trybot.yml new file mode 100644 index 0000000..c63b95a --- /dev/null +++ b/.github/workflows/trybot.yml @@ -0,0 +1,111 @@ +# Code generated internal/ci/ci_tool.cue; DO NOT EDIT. + +name: TryBot +"on": + push: + branches: + - ci/test + - master + pull_request: {} + workflow_dispatch: {} +jobs: + test: + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + if: |- + (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"trybot"')) || ! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')) + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Reset git directory modification times + run: touch -t 202211302355 $(find * -type d) + - name: Restore git file modification times + uses: chetan/git-restore-mtime-action@075f9bc9d159805603419d50f794bd9f33252ebe + - id: DispatchTrailer + name: Try to extract Dispatch-Trailer + run: |- + x="$(git log -1 --pretty='%(trailers:key=Dispatch-Trailer,valueonly)')" + if [[ "$x" == "" ]] + then + # Some steps rely on the presence or otherwise of the Dispatch-Trailer. + # We know that we don't have a Dispatch-Trailer in this situation, + # hence we use the JSON value null in order to represent that state. + # This means that GitHub expressions can determine whether a Dispatch-Trailer + # is present or not by checking whether the fromJSON() result of the + # output from this step is the JSON value null or not. + x=null + fi + echo "value<> $GITHUB_OUTPUT + echo "$x" >> $GITHUB_OUTPUT + echo "EOD" >> $GITHUB_OUTPUT + - if: |- + ((github.ref == 'refs/heads/master') && (! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')))) && (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')) + name: Check we don't have Dispatch-Trailer on a protected branch + run: |- + echo "github.event.head_commit.message contains Dispatch-Trailer but we are on a protected branch" + false + - name: Install Go + uses: actions/setup-go@v5 + with: + cache: false + go-version: 1.23.x + - name: Set common go env vars + run: |- + go env -w GOTOOLCHAIN=local + + # Dump env for good measure + go env + - id: go-mod-cache-dir + name: Get go mod cache directory + run: echo "dir=$(go env GOMODCACHE)" >> ${GITHUB_OUTPUT} + - id: go-cache-dir + name: Get go build/test cache directory + run: echo "dir=$(go env GOCACHE)" >> ${GITHUB_OUTPUT} + - if: |- + (((github.ref == 'refs/heads/master') && (! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')))) || (github.ref == 'refs/heads/ci/test')) + uses: actions/cache@v4 + with: + path: |- + ${{ steps.go-mod-cache-dir.outputs.dir }}/cache/download + ${{ steps.go-cache-dir.outputs.dir }} + key: ${{ runner.os }}-1.23.x-${{ github.run_id }} + restore-keys: ${{ runner.os }}-1.23.x + - if: |- + ! (((github.ref == 'refs/heads/master') && (! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')))) || (github.ref == 'refs/heads/ci/test')) + uses: actions/cache/restore@v4 + with: + path: |- + ${{ steps.go-mod-cache-dir.outputs.dir }}/cache/download + ${{ steps.go-cache-dir.outputs.dir }} + key: ${{ runner.os }}-1.23.x-${{ github.run_id }} + restore-keys: ${{ runner.os }}-1.23.x + - if: |- + github.repository == 'cue-lang/vscode-cue' && (((github.ref == 'refs/heads/master') && (! (contains(github.event.head_commit.message, ' + Dispatch-Trailer: {"type":"')))) || github.ref == 'refs/heads/ci/test') + run: go clean -testcache + - name: Verify + run: go mod verify + - name: Generate + run: go generate ./... + - name: Test + run: go test ./... + - name: Race test + run: go test -race ./... + - name: staticcheck + run: go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./... + - name: Tidy + run: go mod tidy + - if: always() + name: Check that git is clean at the end of the job + run: test -z "$(git status --porcelain)" || (git status; git diff; false) diff --git a/.github/workflows/trybot_dispatch.yml b/.github/workflows/trybot_dispatch.yml new file mode 100644 index 0000000..923ae4d --- /dev/null +++ b/.github/workflows/trybot_dispatch.yml @@ -0,0 +1,133 @@ +# Code generated internal/ci/ci_tool.cue; DO NOT EDIT. + +name: Dispatch trybot +"on": + repository_dispatch: {} + push: + branches: + - ci/test +jobs: + trybot: + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + if: ${{ ((github.ref == 'refs/heads/ci/test') && false) || github.event.client_payload.type == 'trybot' }} + steps: + - name: Write netrc file for cueckoo Gerrithub + run: |- + cat < ~/.netrc + machine review.gerrithub.io + login cueckoo + password ${{ secrets.CUECKOO_GERRITHUB_PASSWORD }} + EOD + chmod 600 ~/.netrc + - id: payload + if: github.repository == 'cue-lang/vscode-cue' && (github.ref == 'refs/heads/ci/test') + name: Write fake payload + run: |- + cat <> $GITHUB_OUTPUT + value<.needs + // keyword. + // Each job runs in a fresh instance of the virtual environment + // specified by runs-on. + // You can run an unlimited number of jobs as long as you are + // within the workflow usage limits. For more information, see + // https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#usage-limits. + jobs!: { + {[=~"^[_a-zA-Z][a-zA-Z0-9_-]*$" & !~"^()$"]: #normalJob | #reusableWorkflowCallJob} + } + + // The name for workflow runs generated from the workflow. GitHub + // displays the workflow run name in the list of workflow runs on + // your repository's 'Actions' tab. + "run-name"?: string + permissions?: #permissions + + #architecture: "ARM32" | "x64" | "x86" + + #branch: #globs + + #concurrency: { + // When a concurrent job or workflow is queued, if another job or + // workflow using the same concurrency group in the repository is + // in progress, the queued job or workflow will be pending. Any + // previously pending job or workflow in the concurrency group + // will be canceled. + group!: string + + // To cancel any currently running job or workflow in the same + // concurrency group, specify cancel-in-progress: true. + "cancel-in-progress"?: bool | #expressionSyntax + } + + #configuration: string | number | bool | { + [string]: #configuration + } | [...#configuration] + + #container: { + // The Docker image to use as the container to run the action. The + // value can be the Docker Hub image name or a registry name. + image!: string + + // If the image's container registry requires authentication to + // pull the image, you can use credentials to set a map of the + // username and password. The credentials are the same values + // that you would provide to the `docker login` command. + credentials?: { + username?: string + password?: string + ... + } + + // Sets an array of environment variables in the container. + env?: #env + + // Sets an array of ports to expose on the container. + ports?: [...number | string] & [_, ...] + + // Sets an array of volumes for the container to use. You can use + // volumes to share data between services or other steps in a + // job. You can specify named Docker volumes, anonymous Docker + // volumes, or bind mounts on the host. + // To specify a volume, you specify the source and destination + // path: : + // The is a volume name or an absolute path on the host + // machine, and is an absolute path in the + // container. + volumes?: [...=~"^[^:]+:[^:]+$"] & [_, ...] + + // Additional Docker container resource options. For a list of + // options, see + // https://docs.docker.com/engine/reference/commandline/create/#options. + options?: string + } + + #defaults: run?: { + shell?: #shell + "working-directory"?: #["working-directory"] + } + + #permissions: "read-all" | "write-all" | #["permissions-event"] + + #: "permissions-event": { + actions?: #["permissions-level"] + attestations?: #["permissions-level"] + checks?: #["permissions-level"] + contents?: #["permissions-level"] + deployments?: #["permissions-level"] + discussions?: #["permissions-level"] + "id-token"?: #["permissions-level"] + issues?: #["permissions-level"] + packages?: #["permissions-level"] + pages?: #["permissions-level"] + "pull-requests"?: #["permissions-level"] + "repository-projects"?: #["permissions-level"] + "security-events"?: #["permissions-level"] + statuses?: #["permissions-level"] + } + + #: "permissions-level": "read" | "write" | "none" + + #env: { + [string]: bool | number | string + } | #stringContainingExpressionSyntax + + #environment: { + // The name of the environment configured in the repo. + name!: string + + // A deployment URL + url?: string + } + + #event: "branch_protection_rule" | "check_run" | "check_suite" | "create" | "delete" | "deployment" | "deployment_status" | "discussion" | "discussion_comment" | "fork" | "gollum" | "issue_comment" | "issues" | "label" | "merge_group" | "milestone" | "page_build" | "project" | "project_card" | "project_column" | "public" | "pull_request" | "pull_request_review" | "pull_request_review_comment" | "pull_request_target" | "push" | "registry_package" | "release" | "status" | "watch" | "workflow_call" | "workflow_dispatch" | "workflow_run" | "repository_dispatch" + + #eventObject: null | { + ... + } + + #expressionSyntax: =~""" + ^\\$\\{\\{(.|[\r + ])*\\}\\}$ + """ + + #stringContainingExpressionSyntax: =~""" + ^.*\\$\\{\\{(.|[\r + ])*\\}\\}.*$ + """ + + #globs: [...strings.MinRunes(1)] & [_, ...] + + #machine: "linux" | "macos" | "windows" + + #name: =~"^[_a-zA-Z][a-zA-Z0-9_-]*$" + + #path: #globs + + #ref: null | { + branches?: #branch + "branches-ignore"?: #branch + tags?: #branch + "tags-ignore"?: #branch + paths?: #path + "paths-ignore"?: #path + ... + } + + #shell: string | ("bash" | "pwsh" | "python" | "sh" | "cmd" | "powershell") + + #types: [_, ...] + + #: "working-directory": string + + #jobNeeds: [...#name] & [_, ...] | #name + + #matrix: { + {[=~"^(in|ex)clude$" & !~"^()$"]: #expressionSyntax | [...{ + [string]: #configuration + }] & [_, ...]} + {[!~"^(in|ex)clude$" & !~"^()$"]: [...#configuration] & [_, ...] | #expressionSyntax} + } | #expressionSyntax + + #reusableWorkflowCallJob: { + // The name of the job displayed on GitHub. + name?: string + needs?: #jobNeeds + permissions?: #permissions + + // You can use the if conditional to prevent a job from running + // unless a condition is met. You can use any supported context + // and expression to create a conditional. + // Expressions in an if conditional do not require the ${{ }} + // syntax. For more information, see + // https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions. + if?: bool | number | string + + // The location and version of a reusable workflow file to run as + // a job, of the form './{path/to}/{localfile}.yml' or + // '{owner}/{repo}/{path}/{filename}@{ref}'. {ref} can be a SHA, + // a release tag, or a branch name. Using the commit SHA is the + // safest for stability and security. + uses!: =~"^(.+/)+(.+)\\.(ya?ml)(@.+)?$" + + // A map of inputs that are passed to the called workflow. Any + // inputs that you pass must match the input specifications + // defined in the called workflow. Unlike + // 'jobs..steps[*].with', the inputs you pass with + // 'jobs..with' are not be available as environment + // variables in the called workflow. Instead, you can reference + // the inputs by using the inputs context. + with?: #env + + // When a job is used to call a reusable workflow, you can use + // 'secrets' to provide a map of secrets that are passed to the + // called workflow. Any secrets that you pass must match the + // names defined in the called workflow. + secrets?: #env | "inherit" + + // A strategy creates a build matrix for your jobs. You can define + // different variations of an environment to run each job in. + strategy?: { + matrix!: #matrix + + // When set to true, GitHub cancels all in-progress jobs if any + // matrix job fails. Default: true + "fail-fast"?: bool | string | *true + + // The maximum number of jobs that can run simultaneously when + // using a matrix job strategy. By default, GitHub will maximize + // the number of jobs run in parallel depending on the available + // runners on GitHub-hosted virtual machines. + "max-parallel"?: number | string + } + + // Concurrency ensures that only a single job or workflow using + // the same concurrency group will run at a time. A concurrency + // group can be any string or expression. The expression can use + // any context except for the secrets context. + // You can also specify concurrency at the workflow level. + // When a concurrent job or workflow is queued, if another job or + // workflow using the same concurrency group in the repository is + // in progress, the queued job or workflow will be pending. Any + // previously pending job or workflow in the concurrency group + // will be canceled. To also cancel any currently running job or + // workflow in the same concurrency group, specify + // cancel-in-progress: true. + concurrency?: string | #concurrency + } + + #normalJob: { + // The name of the job displayed on GitHub. + name?: string + needs?: #jobNeeds + permissions?: #permissions + + // The type of machine to run the job on. The machine can be + // either a GitHub-hosted runner, or a self-hosted runner. + "runs-on"!: string | [string] & [_, ...] | { + group?: string + labels?: string | [...string] + ... + } | #stringContainingExpressionSyntax | #expressionSyntax + + // The environment that the job references. + environment?: string | #environment + + // A map of outputs for a job. Job outputs are available to all + // downstream jobs that depend on this job. + outputs?: { + [string]: string + } + + // A map of environment variables that are available to all steps + // in the job. + env?: #env + + // A map of default settings that will apply to all steps in the + // job. + defaults?: #defaults + + // You can use the if conditional to prevent a job from running + // unless a condition is met. You can use any supported context + // and expression to create a conditional. + // Expressions in an if conditional do not require the ${{ }} + // syntax. For more information, see + // https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions. + if?: bool | number | string + + // A job contains a sequence of tasks called steps. Steps can run + // commands, run setup tasks, or run an action in your + // repository, a public repository, or an action published in a + // Docker registry. Not all steps run actions, but all actions + // run as a step. Each step runs in its own process in the + // virtual environment and has access to the workspace and + // filesystem. Because steps run in their own process, changes to + // environment variables are not preserved between steps. GitHub + // provides built-in steps to set up and complete a job. + // Must contain either `uses` or `run` + steps?: [...({ + uses!: string + ... + } | { + run!: string + ... + }) & { + // A unique identifier for the step. You can use the id to + // reference the step in contexts. For more information, see + // https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions. + id?: string + + // You can use the if conditional to prevent a step from running + // unless a condition is met. You can use any supported context + // and expression to create a conditional. + // Expressions in an if conditional do not require the ${{ }} + // syntax. For more information, see + // https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions. + if?: bool | number | string + + // A name for your step to display on GitHub. + name?: string + + // Selects an action to run as part of a step in your job. An + // action is a reusable unit of code. You can use an action + // defined in the same repository as the workflow, a public + // repository, or in a published Docker container image + // (https://hub.docker.com/). + // We strongly recommend that you include the version of the + // action you are using by specifying a Git ref, SHA, or Docker + // tag number. If you don't specify a version, it could break + // your workflows or cause unexpected behavior when the action + // owner publishes an update. + // - Using the commit SHA of a released action version is the + // safest for stability and security. + // - Using the specific major action version allows you to receive + // critical fixes and security patches while still maintaining + // compatibility. It also assures that your workflow should still + // work. + // - Using the master branch of an action may be convenient, but + // if someone releases a new major version with a breaking + // change, your workflow could break. + // Some actions require inputs that you must set using the with + // keyword. Review the action's README file to determine the + // inputs required. + // Actions are either JavaScript files or Docker containers. If + // the action you're using is a Docker container you must run the + // job in a Linux virtual environment. For more details, see + // https://help.github.com/en/articles/virtual-environments-for-github-actions. + uses?: string + + // Runs command-line programs using the operating system's shell. + // If you do not provide a name, the step name will default to + // the text specified in the run command. + // Commands run using non-login shells by default. You can choose + // a different shell and customize the shell used to run + // commands. For more information, see + // https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell. + // Each run keyword represents a new process and shell in the + // virtual environment. When you provide multi-line commands, + // each line runs in the same shell. + run?: string + "working-directory"?: #["working-directory"] + shell?: #shell + + // A map of the input parameters defined by the action. Each input + // parameter is a key/value pair. Input parameters are set as + // environment variables. The variable is prefixed with INPUT_ + // and converted to upper case. + with?: #env & (null | bool | number | string | [...] | { + args?: string + entrypoint?: string + ... + }) + + // Sets environment variables for steps to use in the virtual + // environment. You can also set environment variables for the + // entire workflow or a job. + env?: #env + + // Prevents a job from failing when a step fails. Set to true to + // allow a job to pass when this step fails. + "continue-on-error"?: bool | #expressionSyntax | *false + + // The maximum number of minutes to run the step before killing + // the process. + "timeout-minutes"?: number | #expressionSyntax + }] & [_, ...] + + // The maximum number of minutes to let a workflow run before + // GitHub automatically cancels it. Default: 360 + "timeout-minutes"?: number | #expressionSyntax | *360 + + // A strategy creates a build matrix for your jobs. You can define + // different variations of an environment to run each job in. + strategy?: { + matrix!: #matrix + + // When set to true, GitHub cancels all in-progress jobs if any + // matrix job fails. Default: true + "fail-fast"?: bool | string | *true + + // The maximum number of jobs that can run simultaneously when + // using a matrix job strategy. By default, GitHub will maximize + // the number of jobs run in parallel depending on the available + // runners on GitHub-hosted virtual machines. + "max-parallel"?: number | string + } + + // Prevents a workflow run from failing when a job fails. Set to + // true to allow a workflow run to pass when this job fails. + "continue-on-error"?: bool | #expressionSyntax + + // A container to run any steps in a job that don't already + // specify a container. If you have steps that use both script + // and container actions, the container actions will run as + // sibling containers on the same network with the same volume + // mounts. + // If you do not set a container, all steps will run directly on + // the host specified by runs-on unless a step refers to an + // action configured to run in a container. + container?: string | #container + + // Additional containers to host services for a job in a workflow. + // These are useful for creating databases or cache services like + // redis. The runner on the virtual machine will automatically + // create a network and manage the life cycle of the service + // containers. + // When you use a service container for a job or your step uses + // container actions, you don't need to set port information to + // access the service. Docker automatically exposes all ports + // between containers on the same network. + // When both the job and the action run in a container, you can + // directly reference the container by its hostname. The hostname + // is automatically mapped to the service name. + // When a step does not use a container action, you must access + // the service using localhost and bind the ports. + services?: { + [string]: #container + } + + // Concurrency ensures that only a single job or workflow using + // the same concurrency group will run at a time. A concurrency + // group can be any string or expression. The expression can use + // any context except for the secrets context. + // You can also specify concurrency at the workflow level. + // When a concurrent job or workflow is queued, if another job or + // workflow using the same concurrency group in the repository is + // in progress, the queued job or workflow will be pending. Any + // previously pending job or workflow in the concurrency group + // will be canceled. To also cancel any currently running job or + // workflow in the same concurrency group, specify + // cancel-in-progress: true. + concurrency?: string | #concurrency + } +} diff --git a/cue.mod/usr/github.com/SchemaStore/schemastore/src/schemas/json/workflow.cue b/cue.mod/usr/github.com/SchemaStore/schemastore/src/schemas/json/workflow.cue new file mode 100644 index 0000000..7b0e3c8 --- /dev/null +++ b/cue.mod/usr/github.com/SchemaStore/schemastore/src/schemas/json/workflow.cue @@ -0,0 +1,15 @@ +package json + +#job: ((#Workflow & {jobs: _}).jobs & {x: _}).x +#step: ((#job & {steps: _}).steps & [_])[0] + +// CUE does not properly encode a JSON Schema oneOf; see +// https://cuelang.org/issue/3165. For now, apply a temporary workaround which +// forces the other option to bottom. +#Workflow: jobs?: [string]: steps?: [...( + { + uses?: _|_ + } | { + run?: _|_ + }), +] diff --git a/gen.go b/gen.go index 90b6258..6961045 100644 --- a/gen.go +++ b/gen.go @@ -1,3 +1,3 @@ package vscode -//go:generate go run github.com/cue-sh/vscode-cue/internal/cmd/gen-syntax syntaxes/cue.tmLanguage.json +//go:generate go run ./internal/cmd/gen-syntax syntaxes/cue.tmLanguage.json diff --git a/go.mod b/go.mod index 1a384e4..0165d37 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/cue-sh/vscode-cue +module github.com/cue-lang/vscode-cue -go 1.12 +go 1.23.0 diff --git a/internal/ci/base/base.cue b/internal/ci/base/base.cue new file mode 100644 index 0000000..c0dea7d --- /dev/null +++ b/internal/ci/base/base.cue @@ -0,0 +1,88 @@ +// Copyright 2022 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package base is a collection of workflows, jobs, stes etc that are common to +// CUE projects and the workflows they specify. The package itself needs to be +// instantiated to parameterise many of the exported definitions. +// +// For example a package using base would do something like this: +// +// package workflows +// +// import "cuelang.org/go/internal/ci/base" +// +// // Create an instance of base +// _base: base & core { params: { +// // any values you need to set that are outside of core +// ... +// }} +// +package base + +import ( + "strings" +) + +// Package parameters +githubRepositoryPath: *(URLPath & {#url: githubRepositoryURL, _}) | string +githubRepositoryURL: *("https://github.com/" + githubRepositoryPath) | string +gerritHubHostname: "review.gerrithub.io" +gerritHubRepositoryURL: *("https://\(gerritHubHostname)/a/" + githubRepositoryPath) | string +trybotRepositoryPath: *(githubRepositoryPath + "-" + trybot.key) | string +trybotRepositoryURL: *("https://github.com/" + trybotRepositoryPath) | string + +defaultBranch: *"master" | string +testDefaultBranch: *"ci/test" | _ +protectedBranchPatterns: *[defaultBranch] | [...string] +releaseTagPrefix: *"v" | string +releaseTagPattern: *(releaseTagPrefix + "*") | string + +botGitHubUser: string +botGitHubUserTokenSecretsKey: *(strings.ToUpper(botGitHubUser) + "_GITHUB_PAT") | string +botGitHubUserEmail: string +botGerritHubUser: *botGitHubUser | string +botGerritHubUserPasswordSecretsKey: *(strings.ToUpper(botGitHubUser) + "_GERRITHUB_PASSWORD") | string +botGerritHubUserEmail: *botGitHubUserEmail | string + +workflowFileExtension: ".yml" + +linuxMachine: string + +codeReview: #codeReview & { + github: githubRepositoryURL + gerrit: gerritHubRepositoryURL +} + +// Define some shared keys and human-readable names. +// +// trybot.key and unity.key are shared with +// github.com/cue-lang/contrib-tools/cmd/cueckoo. The keys are used across various CUE +// workflows and their consistency in those various locations is therefore +// crucial. As such, we assert specific values for the keys here rather than +// just deriving values from the human-readable names. +// +// trybot.name is by the trybot GitHub workflow and by gerritstatusupdater as +// an identifier in the status updates that are posted as reviews for this +// workflows, but also as the result label key, e.g. "TryBot-Result" would be +// the result label key for the "TryBot" workflow. This name also shows up in +// the CI badge in the top-level README. +trybot: { + key: "trybot" & strings.ToLower(name) + name: "TryBot" +} + +unity: { + key: "unity" & strings.ToLower(name) + name: "Unity" +} diff --git a/internal/ci/base/codereview.cue b/internal/ci/base/codereview.cue new file mode 100644 index 0000000..134c940 --- /dev/null +++ b/internal/ci/base/codereview.cue @@ -0,0 +1,28 @@ +package base + +// This file contains aspects principally related to git-codereview +// configuration. + +import ( + "strings" +) + +// #codeReview defines the schema of a codereview.cfg file that +// sits at the root of a repository. codereview.cfg is the configuration +// file that drives golang.org/x/review/git-codereview. This config +// file is also used by github.com/cue-lang/contrib-tools/cmd/cueckoo. +#codeReview: { + gerrit?: string + github?: string + "cue-unity"?: string +} + +// #toCodeReviewCfg converts a #codeReview instance to +// the key: value +toCodeReviewCfg: { + #input: #codeReview + let parts = [for k, v in #input {k + ": " + v}] + + // Per https://pkg.go.dev/golang.org/x/review/git-codereview#hdr-Configuration + strings.Join(parts, "\n") +} diff --git a/internal/ci/base/gerrithub.cue b/internal/ci/base/gerrithub.cue new file mode 100644 index 0000000..0105ee0 --- /dev/null +++ b/internal/ci/base/gerrithub.cue @@ -0,0 +1,356 @@ +package base + +// This file contains gerrithub related definitions etc + +import ( + encjson "encoding/json" + "strings" + + "github.com/SchemaStore/schemastore/src/schemas/json" +) + +// trybotWorkflows is a template for trybot-based repos +trybotWorkflows: { + (trybot.key): json.#Workflow & { + on: workflow_dispatch: {} + } + "\(trybot.key)_dispatch": trybotDispatchWorkflow + "push_tip_to_\(trybot.key)": pushTipToTrybotWorkflow + "evict_caches": evictCaches +} + +#dispatch: { + type: string + CL: int + patchset: int + targetBranch: *defaultBranch | string + + let p = strings.Split("\(CL)", "") + let rightMostTwo = p[len(p)-2] + p[len(p)-1] + ref: *"refs/changes/\(rightMostTwo)/\(CL)/\(patchset)" | string +} + +trybotDispatchWorkflow: bashWorkflow & { + #dummyDispatch?: #dispatch + name: "Dispatch \(trybot.key)" + on: { + repository_dispatch: {} + push: { + // To enable testing of the dispatch itself + branches: [testDefaultBranch] + } + } + jobs: [string]: defaults: run: shell: "bash" + jobs: { + (trybot.key): { + "runs-on": linuxMachine + + let goodDummyData = [if encjson.Marshal(#dummyDispatch) != _|_ {true}, false][0] + + // We set the "on" conditions above, but this would otherwise mean we + // run for all dispatch events. + if: "${{ (\(isTestDefaultBranch) && \(goodDummyData)) || github.event.client_payload.type == '\(trybot.key)' }}" + + // See the comment below about the need for cases + let cases = [ + { + condition: "!=" + expr: "fromJSON(steps.payload.outputs.value)" + nameSuffix: "fake data" + }, + { + condition: "==" + expr: "github.event.client_payload" + nameSuffix: "repository_dispatch payload" + }, + ] + + steps: [ + writeNetrcFile, + + json.#step & { + name: "Write fake payload" + id: "payload" + if: "github.repository == '\(githubRepositoryPath)' && \(isTestDefaultBranch)" + + // Use bash heredocs so that JSON's use of double quotes does + // not get interpreted as shell. Both in the running of the + // command itself, which itself is the echo-ing of a command to + // $GITHUB_OUTPUT. + run: #""" + cat <> $GITHUB_OUTPUT + value< ~/.netrc + machine \(gerritHubHostname) + login \(botGerritHubUser) + password ${{ secrets.\(botGerritHubUserPasswordSecretsKey) }} + EOD + chmod 600 ~/.netrc + """ +} diff --git a/internal/ci/base/github.cue b/internal/ci/base/github.cue new file mode 100644 index 0000000..5b1c4ce --- /dev/null +++ b/internal/ci/base/github.cue @@ -0,0 +1,350 @@ +package base + +// This file contains aspects principally related to GitHub workflows + +import ( + encjson "encoding/json" + "list" + "strings" + "strconv" + + "github.com/SchemaStore/schemastore/src/schemas/json" +) + +bashWorkflow: json.#Workflow & { + jobs: [string]: defaults: run: shell: "bash" +} + +installGo: { + #setupGo: json.#step & { + name: "Install Go" + uses: "actions/setup-go@v5" + with: { + // We do our own caching in setupGoActionsCaches. + cache: false + "go-version": string + } + } + + // Why set GOTOOLCHAIN here? As opposed to an environment variable + // elsewhere? No perfect answer to this question but here is the thinking: + // + // Setting the variable here localises it with the installation of Go. Doing + // it elsewhere creates distance between the two steps which are + // intrinsically related. And it's also hard to do: "when we use this step, + // also ensure that we establish an environment variable in the job for + // GOTOOLCHAIN". + // + // Environment variables can only be set at a workflow, job or step level. + // Given we currently use a matrix strategy which varies the Go version, + // that rules out using an environment variable based approach, because the + // Go version is only available at runtime via GitHub actions provided + // context. Whether we should instead be templating multiple workflows (i.e. + // exploding the matrix ourselves) is a different question, but one that + // has performance implications. + // + // So as clumsy as it is to use a step "template" that includes more than + // one step, it's the best option available to us for now. + [ + #setupGo, + + { + json.#step & { + name: "Set common go env vars" + run: """ + go env -w GOTOOLCHAIN=local + + # Dump env for good measure + go env + """ + } + }, + ] +} + +checkoutCode: { + #actionsCheckout: json.#step & { + name: "Checkout code" + uses: "actions/checkout@v4" + + // "pull_request" builds will by default use a merge commit, + // testing the PR's HEAD merged on top of the master branch. + // For consistency with Gerrit, avoid that merge commit entirely. + // This doesn't affect builds by other events like "push", + // since github.event.pull_request is unset so ref remains empty. + with: { + ref: "${{ github.event.pull_request.head.sha }}" + "fetch-depth": 0 // see the docs below + } + } + + [ + #actionsCheckout, + + // Restore modified times to work around https://go.dev/issues/58571, + // as otherwise we would get lots of unnecessary Go test cache misses. + // Note that this action requires actions/checkout to use a fetch-depth of 0. + // Since this is a third-party action which runs arbitrary code, + // we pin a commit hash for v2 to be in control of code updates. + // Also note that git-restore-mtime does not update all directories, + // per the bug report at https://github.com/MestreLion/git-tools/issues/47, + // so we first reset all directory timestamps to a static time as a fallback. + // TODO(mvdan): May be unnecessary once the Go bug above is fixed. + json.#step & { + name: "Reset git directory modification times" + run: "touch -t 202211302355 $(find * -type d)" + }, + json.#step & { + name: "Restore git file modification times" + uses: "chetan/git-restore-mtime-action@075f9bc9d159805603419d50f794bd9f33252ebe" + }, + + { + json.#step & { + name: "Try to extract \(dispatchTrailer)" + id: dispatchTrailerStepID + run: """ + x="$(git log -1 --pretty='%(trailers:key=\(dispatchTrailer),valueonly)')" + if [[ "$x" == "" ]] + then + # Some steps rely on the presence or otherwise of the Dispatch-Trailer. + # We know that we don't have a Dispatch-Trailer in this situation, + # hence we use the JSON value null in order to represent that state. + # This means that GitHub expressions can determine whether a Dispatch-Trailer + # is present or not by checking whether the fromJSON() result of the + # output from this step is the JSON value null or not. + x=null + fi + echo "\(_dispatchTrailerDecodeStepOutputVar)<> $GITHUB_OUTPUT + echo "$x" >> $GITHUB_OUTPUT + echo "EOD" >> $GITHUB_OUTPUT + """ + } + }, + + // Safety nets to flag if we ever have a Dispatch-Trailer slip through the + // net and make it to master + json.#step & { + name: "Check we don't have \(dispatchTrailer) on a protected branch" + if: "\(isProtectedBranch) && \(containsDispatchTrailer)" + run: """ + echo "\(_dispatchTrailerVariable) contains \(dispatchTrailer) but we are on a protected branch" + false + """ + }, + ] +} + +earlyChecks: json.#step & { + name: "Early git and code sanity checks" + run: "go run cuelang.org/go/internal/ci/checks@v0.11.0-0.dev.0.20240903133435-46fb300df650" +} + +curlGitHubAPI: { + #tokenSecretsKey: *botGitHubUserTokenSecretsKey | string + + #""" + curl -s -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.\#(#tokenSecretsKey) }}" -H "X-GitHub-Api-Version: 2022-11-28" + """# +} + +setupGoActionsCaches: { + // #readonly determines whether we ever want to write the cache back. The + // writing of a cache back (for any given cache key) should only happen on a + // protected branch. But running a workflow on a protected branch often + // implies that we want to skip the cache to ensure we catch flakes early. + // Hence the concept of clearing the testcache to ensure we catch flakes + // early can be defaulted based on #readonly. In the general case the two + // concepts are orthogonal, hence they are kept as two parameters, even + // though in our case we could get away with a single parameter that + // encapsulates our needs. + #readonly: *false | bool + #cleanTestCache: *!#readonly | bool + #goVersion: string + #additionalCacheDirs: [...string] + #os: string + + let goModCacheDirID = "go-mod-cache-dir" + let goCacheDirID = "go-cache-dir" + + // cacheDirs is a convenience variable that includes + // GitHub expressions that represent the directories + // that participate in Go caching. + let cacheDirs = list.Concat([[ + "${{ steps.\(goModCacheDirID).outputs.dir }}/cache/download", + "${{ steps.\(goCacheDirID).outputs.dir }}", + ], #additionalCacheDirs]) + + let cacheRestoreKeys = "\(#os)-\(#goVersion)" + + let cacheStep = json.#step & { + with: { + path: strings.Join(cacheDirs, "\n") + + // GitHub actions caches are immutable. Therefore, use a key which is + // unique, but allow the restore to fallback to the most recent cache. + // The result is then saved under the new key which will benefit the + // next build. Restore keys are only set if the step is restore. + key: "\(cacheRestoreKeys)-${{ github.run_id }}" + "restore-keys": cacheRestoreKeys + } + } + + let readWriteCacheExpr = "(\(isProtectedBranch) || \(isTestDefaultBranch))" + + // pre is the list of steps required to establish and initialise the correct + // caches for Go-based workflows. + [ + // TODO: once https://github.com/actions/setup-go/issues/54 is fixed, + // we could use `go env` outputs from the setup-go step. + json.#step & { + name: "Get go mod cache directory" + id: goModCacheDirID + run: #"echo "dir=$(go env GOMODCACHE)" >> ${GITHUB_OUTPUT}"# + }, + json.#step & { + name: "Get go build/test cache directory" + id: goCacheDirID + run: #"echo "dir=$(go env GOCACHE)" >> ${GITHUB_OUTPUT}"# + }, + + // Only if we are not running in readonly mode do we want a step that + // uses actions/cache (read and write). Even then, the use of the write + // step should be predicated on us running on a protected branch. Because + // it's impossible for anything else to write such a cache. + if !#readonly { + cacheStep & { + if: readWriteCacheExpr + uses: "actions/cache@v4" + } + }, + + cacheStep & { + // If we are readonly, there is no condition on when we run this step. + // It should always be run, becase there is no alternative. But if we + // are not readonly, then we need to predicate this step on us not + // being on a protected branch. + if !#readonly { + if: "! \(readWriteCacheExpr)" + } + + uses: "actions/cache/restore@v4" + }, + + if #cleanTestCache { + // All tests on protected branches should skip the test cache. The + // canonical way to do this is with -count=1. However, we want the + // resulting test cache to be valid and current so that subsequent CLs + // in the trybot repo can leverage the updated cache. Therefore, we + // instead perform a clean of the testcache. + // + // Critically we only want to do this in the main repo, not the trybot + // repo. + json.#step & { + if: "github.repository == '\(githubRepositoryPath)' && (\(isProtectedBranch) || github.ref == 'refs/heads/\(testDefaultBranch)')" + run: "go clean -testcache" + } + }, + ] +} + +// isProtectedBranch is an expression that evaluates to true if the +// job is running as a result of pushing to one of protectedBranchPatterns. +// It would be nice to use the "contains" builtin for simplicity, +// but array literals are not yet supported in expressions. +isProtectedBranch: { + #trailers: [...string] + "((" + strings.Join([for branch in protectedBranchPatterns { + (_matchPattern & {variable: "github.ref", pattern: "refs/heads/\(branch)"}).expr + }], " || ") + ") && (! \(containsDispatchTrailer)))" +} + +// isTestDefaultBranch is an expression that evaluates to true if +// the job is running on the testDefaultBranch +isTestDefaultBranch: "(github.ref == 'refs/heads/\(testDefaultBranch)')" + +// #isReleaseTag creates a GitHub expression, based on the given release tag +// pattern, that evaluates to true if called in the context of a workflow that +// is part of a release. +isReleaseTag: { + (_matchPattern & {variable: "github.ref", pattern: "refs/tags/\(releaseTagPattern)"}).expr +} + +checkGitClean: json.#step & { + name: "Check that git is clean at the end of the job" + if: "always()" + run: "test -z \"$(git status --porcelain)\" || (git status; git diff; false)" +} + +repositoryDispatch: json.#step & { + #githubRepositoryPath: *githubRepositoryPath | string + #botGitHubUserTokenSecretsKey: *botGitHubUserTokenSecretsKey | string + #arg: _ + + _curlGitHubAPI: curlGitHubAPI & {#tokenSecretsKey: #botGitHubUserTokenSecretsKey, _} + + name: string + run: #""" + \#(_curlGitHubAPI) --fail --request POST --data-binary \#(strconv.Quote(encjson.Marshal(#arg))) https://api.github.com/repos/\#(#githubRepositoryPath)/dispatches + """# +} + +// dispatchTrailer is the trailer that we use to pass information in a commit +// when triggering workflow events in other GitHub repos. +// +// NOTE: keep this consistent with gerritstatusupdater parsing logic. +dispatchTrailer: "Dispatch-Trailer" + +// dispatchTrailerStepID is the ID of the step that attempts +// to extract a Dispatch-Trailer value from the commit at HEAD +dispatchTrailerStepID: strings.Replace(dispatchTrailer, "-", "", -1) + +// _dispatchTrailerDecodeStepOutputVar is the name of the output +// variable int he dispatchTrailerStepID step +_dispatchTrailerDecodeStepOutputVar: "value" + +// dispatchTrailerExpr is a GitHub expression that can be dereferenced +// to get values from the JSON-decded Dispatch-Trailer value that +// is extracted during the dispatchTrailerStepID step. +dispatchTrailerExpr: "fromJSON(steps.\(dispatchTrailerStepID).outputs.\(_dispatchTrailerDecodeStepOutputVar))" + +// containsDispatchTrailer returns a GitHub expression that looks at the commit +// message of the head commit of the event that triggered the workflow, an +// expression that returns true if the commit message associated with that head +// commit contains dispatchTrailer. +// +// Note that this logic does not 100% match the answer that would be returned by: +// +// git log --pretty=%(trailers:key=Dispatch-Trailer,valueonly) +// +// GitHub expressions are incredibly limited in their capabilities: +// +// https://docs.github.com/en/actions/learn-github-actions/expressions +// +// There is not even a regular expression matcher. Hence the logic is a best-efforts +// approximation of the logic employed by git log. +containsDispatchTrailer: { + #type?: string + + // If we have a value for #type, then match against that value. + // Otherwise the best we can do is match against: + // + // Dispatch-Trailer: {"type:} + // + let _typeCheck = [if #type != _|_ {#type + "\""}, ""][0] + """ + (contains(\(_dispatchTrailerVariable), '\n\(dispatchTrailer): {"type":"\(_typeCheck)')) + """ +} + +containsTrybotTrailer: containsDispatchTrailer & { + #type: trybot.key + _ +} + +containsUnityTrailer: containsDispatchTrailer & { + #type: unity.key + _ +} + +_dispatchTrailerVariable: "github.event.head_commit.message" diff --git a/internal/ci/base/helpers.cue b/internal/ci/base/helpers.cue new file mode 100644 index 0000000..31321a7 --- /dev/null +++ b/internal/ci/base/helpers.cue @@ -0,0 +1,40 @@ +package base + +// This file contains everything else + +import ( + "list" + "strings" +) + +// _matchPattern returns a GitHub Actions expression which evaluates whether a +// variable matches a globbing pattern. For literal patterns it uses "==", +// and for suffix patterns it uses "startsWith". +// See https://docs.github.com/en/actions/learn-github-actions/expressions. +_matchPattern: { + variable: string + pattern: string + expr: [ + if strings.HasSuffix(pattern, "*") { + let prefix = strings.TrimSuffix(pattern, "*") + "startsWith(\(variable), '\(prefix)')" + }, + { + "\(variable) == '\(pattern)'" + }, + ][0] +} + +doNotEditMessage: { + #generatedBy: string + "Code generated \(#generatedBy); DO NOT EDIT." +} + +// #URLPath is a temporary measure to derive the path part of a URL. +// +// TODO: remove when cuelang.org/issue/1433 lands. +URLPath: { + #url: string + let parts = strings.Split(#url, "/") + strings.Join(list.Slice(parts, 3, len(parts)), "/") +} diff --git a/internal/ci/ci_tool.cue b/internal/ci/ci_tool.cue new file mode 100644 index 0000000..4973a41 --- /dev/null +++ b/internal/ci/ci_tool.cue @@ -0,0 +1,68 @@ +// Copyright 2021 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ci + +import ( + "path" + + "encoding/yaml" + + "tool/file" + + "github.com/cue-lang/vscode-cue/internal/ci/repo" + "github.com/cue-lang/vscode-cue/internal/ci/github" +) + +_goos: string @tag(os,var=os) + +// gen.workflows regenerates the GitHub workflow Yaml definitions. +// +// See internal/ci/gen.go for details on how this step fits into the sequence +// of generating our CI workflow definitions, and updating various txtar tests +// with files from that process. +command: gen: { + _dir: path.FromSlash("../../.github/workflows", path.Unix) + + workflows: { + remove: { + glob: file.Glob & { + glob: path.Join([_dir, "*.yml"], _goos) + files: [...string] + } + for _, _filename in glob.files { + "delete \(_filename)": file.RemoveAll & { + path: _filename + } + } + } + for _workflowName, _workflow in github.workflows { + let _filename = _workflowName + repo.workflowFileExtension + "generate \(_filename)": file.Create & { + $after: [ for v in remove {v}] + filename: path.Join([_dir, _filename], _goos) + let donotedit = repo.doNotEditMessage & {#generatedBy: "internal/ci/ci_tool.cue", _} + contents: "# \(donotedit)\n\n\(yaml.Marshal(_workflow))" + } + } + } +} + +command: gen: codereviewcfg: file.Create & { + _dir: path.FromSlash("../../", path.Unix) + filename: path.Join([_dir, "codereview.cfg"], _goos) + let res = repo.toCodeReviewCfg & {#input: repo.codeReview, _} + let donotedit = repo.doNotEditMessage & {#generatedBy: "internal/ci/ci_tool.cue", _} + contents: "# \(donotedit)\n\n\(res)\n" +} diff --git a/internal/ci/gen.go b/internal/ci/gen.go new file mode 100644 index 0000000..d3077a2 --- /dev/null +++ b/internal/ci/gen.go @@ -0,0 +1,18 @@ +// Copyright 2021 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ci + +//go:generate go run cuelang.org/go/cmd/cue@v0.10.0 cmd importjsonschema ./vendor +//go:generate go run cuelang.org/go/cmd/cue@v0.10.0 cmd gen diff --git a/internal/ci/github/repo.cue b/internal/ci/github/repo.cue new file mode 100644 index 0000000..9a87a41 --- /dev/null +++ b/internal/ci/github/repo.cue @@ -0,0 +1,28 @@ +// Copyright 2022 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +// This file exists to provide a single point of importing +// the repo package. The pattern of using base and repo +// is replicated across a number of CUE repos, and as such +// the import path of repo varies between them. This makes +// spotting differences and applying changes between the +// github/*.cue files noisy. Instead, import the repo package +// in a single file, and that keeps the different in import +// path down to a single file. + +import repo "github.com/cue-lang/vscode-cue/internal/ci/repo" + +_repo: repo diff --git a/internal/ci/github/trybot.cue b/internal/ci/github/trybot.cue new file mode 100644 index 0000000..ccc246e --- /dev/null +++ b/internal/ci/github/trybot.cue @@ -0,0 +1,87 @@ +// Copyright 2023 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +import ( + "list" + + "github.com/SchemaStore/schemastore/src/schemas/json" +) + +// The trybot workflow. +workflows: trybot: _repo.bashWorkflow & { + name: _repo.trybot.name + + on: { + push: { + branches: list.Concat([[_repo.testDefaultBranch], _repo.protectedBranchPatterns]) // do not run PR branches + } + pull_request: {} + } + + jobs: test: { + "runs-on": _repo.linuxMachine + + let runnerOSExpr = "runner.os" + let runnerOSVal = "${{ \(runnerOSExpr) }}" + let _setupGoActionsCaches = _repo.setupGoActionsCaches & { + #goVersion: _repo.latestGo + #os: runnerOSVal + _ + } + let installGo = _repo.installGo & { + #setupGo: with: "go-version": _repo.latestGo + _ + } + + // Only run the trybot workflow if we have the trybot trailer, or + // if we have no special trailers. Note this condition applies + // after and in addition to the "on" condition above. + if: "\(_repo.containsTrybotTrailer) || ! \(_repo.containsDispatchTrailer)" + + steps: [ + for v in _repo.checkoutCode {v}, + for v in installGo {v}, + + for v in _setupGoActionsCaches {v}, + + json.#step & { + name: "Verify" + run: "go mod verify" + }, + json.#step & { + name: "Generate" + run: "go generate ./..." + }, + json.#step & { + name: "Test" + run: "go test ./..." + }, + json.#step & { + name: "Race test" + run: "go test -race ./..." + }, + json.#step & { + name: "staticcheck" + run: "go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./..." + }, + json.#step & { + name: "Tidy" + run: "go mod tidy" + }, + _repo.checkGitClean, + ] + } +} diff --git a/internal/ci/github/workflows.cue b/internal/ci/github/workflows.cue new file mode 100644 index 0000000..23d3380 --- /dev/null +++ b/internal/ci/github/workflows.cue @@ -0,0 +1,50 @@ +// Copyright 2021 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// package github declares the workflows for this project. +package github + +// Note: the name of the workflows (and hence the corresponding .yml filenames) +// correspond to the environment variable names for gerritstatusupdater. +// Therefore, this filename must only be change in combination with also +// updating the environment in which gerritstatusupdater is running for this +// repository. +// +// This name is also used by the CI badge in the top-level README. +// +// This name is also used in the evict_caches lookups. +// +// i.e. don't change the names of workflows! +// +// In addition to separately declaring the workflows themselves, we define the +// shape of #workflows here as a cross-check that we don't accidentally change +// the name of workflows without reading this comment. +// +// We explicitly use close() here instead of a definition in order that we can +// cue export the github package as a test. +workflows: close({ + // Adding this constraint here, whilst clear for the reader, + // blows out evaluation time. This will be fixed as part of + // the performance work which is covered under various issues. + // [string]: json.#Workflow + + _repo.trybotWorkflows + + trybot: _ + trybot_dispatch: #dummyDispatch: dummyDispatch +}) + +dummyDispatch: _repo.#dispatch & { + type: _repo.trybot.key +} diff --git a/internal/ci/repo/repo.cue b/internal/ci/repo/repo.cue new file mode 100644 index 0000000..7ee4d77 --- /dev/null +++ b/internal/ci/repo/repo.cue @@ -0,0 +1,19 @@ +// package repo contains data values that are common to all CUE configurations +// in this repo. The list of configurations includes GitHub workflows, but also +// things like gerrit configuration etc. +package repo + +import ( + "github.com/cue-lang/vscode-cue/internal/ci/base" +) + +base + +githubRepositoryPath: "cue-lang/vscode-cue" + +botGitHubUser: "cueckoo" +botGitHubUserEmail: "cueckoo@gmail.com" + +linuxMachine: "ubuntu-22.04" + +latestGo: "1.23.x" diff --git a/internal/ci/vendor/vendor_tool.cue b/internal/ci/vendor/vendor_tool.cue new file mode 100644 index 0000000..74ff3c3 --- /dev/null +++ b/internal/ci/vendor/vendor_tool.cue @@ -0,0 +1,50 @@ +// Copyright 2021 The CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vendor + +import ( + "path" + + "tool/exec" + "tool/http" +) + +// _cueCmd defines the command that is run to run cmd/cue. +// This is factored out in order that the cue-github-actions +// project which "vendors" the various workflow-related +// packages can specify "cue" as the value so that unity +// tests can specify the cmd/cue binary to use. +_cueCmd: string | *"go run cuelang.org/go/cmd/cue@v0.10.0" @tag(cue_cmd) + +// For the commands below, note we use simple yet hacky path resolution, rather +// than anything that might derive the module root using go list or similar, in +// order that we have zero dependencies. + +// importjsonschema vendors a CUE-imported version of the JSONSchema that +// defines GitHub workflows into the main module's cue.mod/pkg. +command: importjsonschema: { + getJSONSchema: http.Get & { + request: body: "" + + // Tip link for humans: + // https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-workflow.json + url: "https://raw.githubusercontent.com/SchemaStore/schemastore/88d26ad0d451cbd5ebc70218062850aa905bdf18/src/schemas/json/github-workflow.json" + } + import: exec.Run & { + _outpath: path.FromSlash("../../cue.mod/pkg/github.com/SchemaStore/schemastore/src/schemas/json/github-workflow.cue", "unix") + stdin: getJSONSchema.response.body + cmd: "\(_cueCmd) import -f -p json -l #Workflow: -o \(_outpath) jsonschema: -" + } +} diff --git a/internal/cmd/gen-syntax/main.go b/internal/cmd/gen-syntax/main.go index 4c62f3e..42bddb7 100644 --- a/internal/cmd/gen-syntax/main.go +++ b/internal/cmd/gen-syntax/main.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "io" - "io/ioutil" "log" "os" ) @@ -16,7 +15,7 @@ func main() { if err := encode(&wb); err != nil { log.Fatal(err) } - if err := ioutil.WriteFile(os.Args[1], wb.Bytes(), 0644); err != nil { + if err := os.WriteFile(os.Args[1], wb.Bytes(), 0644); err != nil { log.Fatal(err) } }