This is an automated email from the ASF dual-hosted git repository.

rantunes pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/incubator-kie-tools-temporary-rnd-do-not-use.git


The following commit(s) were added to refs/heads/main by this push:
     new e0ddf0386c CI - Jenkins shared scripts (#21)
e0ddf0386c is described below

commit e0ddf0386ca5bac2ff3eafbd6f84fed58f6b5315
Author: Rodrigo Antunes <[email protected]>
AuthorDate: Thu Nov 23 16:29:57 2023 -0300

    CI - Jenkins shared scripts (#21)
    
    * CI - Jenkins shared scripts
    
    * Update openshift get route utility
---
 .ci/jenkins/shared-scripts/buildUtils.groovy       | 110 +++++++++++++
 .ci/jenkins/shared-scripts/chromeStoreUtils.groovy |  52 ++++++
 .ci/jenkins/shared-scripts/dockerUtils.groovy      |  32 ++++
 .ci/jenkins/shared-scripts/githubUtils.groovy      | 180 +++++++++++++++++++++
 .ci/jenkins/shared-scripts/openShiftUtils.groovy   |  77 +++++++++
 .ci/jenkins/shared-scripts/pipelineVars.groovy     |  20 +++
 .ci/jenkins/shared-scripts/zipUtils.groovy         |  37 +++++
 7 files changed, 508 insertions(+)

diff --git a/.ci/jenkins/shared-scripts/buildUtils.groovy 
b/.ci/jenkins/shared-scripts/buildUtils.groovy
new file mode 100644
index 0000000000..9a01314af9
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/buildUtils.groovy
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Start Docker in Docker (DinD)
+*/
+def startDockerInDocker() {
+    sh '''#!/bin/bash -el
+    sudo entrypoint.sh
+    sudo service dbus start
+    '''.trim()
+}
+
+/**
+* Start Xvfb X server required for KIE-Tools E2E tests
+*/
+def startXvfb() {
+    sh '''#!/bin/bash -el
+    Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 &
+    '''.trim()
+}
+
+/**
+* Start Fluxbox window manager required for KIE-Tools E2E tests
+*/
+def startFluxbox() {
+    sh '''#!/bin/bash -el
+    fluxbox -display :99 > /dev/null 2>&1 &
+    '''.trim()
+}
+
+/**
+* Setup PNPM parameters for building KIE-Tools
+*/
+def setupPnpm() {
+    sh """#!/bin/bash -el
+    pnpm config set network-timeout 1000000
+    pnpm -r exec 'bash' '-c' 'mkdir .mvn'
+    pnpm -r exec 'bash' '-c' 'echo -B > .mvn/maven.config'
+    pnpm -r exec 'bash' '-c' 'echo -ntp >> .mvn/maven.config'
+    pnpm -r exec 'bash' '-c' 'echo -Xmx2g > .mvn/jvm.config'
+    pnpm -F *-image exec sed -i 
's/\\("build:prod.*".*\\)podman:build\\(.*\\)/\\1docker:build\\2/g' package.json
+    """.trim()
+}
+
+/**
+* PNPM Bootsrap
+*/
+def pnpmBootstrap(String filters = '') {
+    sh """#!/bin/bash -el
+    pnpm bootstrap ${filters}
+    """.trim()
+}
+
+/**
+* PNPM build all packages
+*/
+def pnpmBuildFull(Integer workspaceConcurrency = 1) {
+    sh """#!/bin/bash -el
+    pnpm -r --workspace-concurrency=${workspaceConcurrency} build:prod
+    """.trim()
+}
+
+/**
+* PNPM build a set of packages
+*/
+def pnpmBuild(String filters, Integer workspaceConcurrency = 1) {
+    sh """#!/bin/bash -el
+    pnpm ${filters} --workspace-concurrency=${workspaceConcurrency} build:prod
+    """.trim()
+}
+
+/**
+* Start KIE-Tools required services for build and test
+*/
+def startRequiredServices() {
+    startDockerInDocker()
+    startXvfb()
+    startFluxbox()
+}
+
+/**
+* @return String build datetime - format (%Y-%m-%d %T)
+*/
+def buildDateTime() {
+    return sh(script: "echo `date +'%Y-%m-%d %T'`", returnStdout: true).trim()
+}
+
+/**
+* @return String the Apache Jenkins agent nodes with higher capacity (builds22 
to builds40)
+**/
+def apacheAgentLabels() {
+    return (22..40).collect{"builds$it"}.join(' || ')
+}
+
+return this;
diff --git a/.ci/jenkins/shared-scripts/chromeStoreUtils.groovy 
b/.ci/jenkins/shared-scripts/chromeStoreUtils.groovy
new file mode 100644
index 0000000000..610df262af
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/chromeStoreUtils.groovy
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Upload an extension to Chrome Store
+*
+* @return String status
+*/
+def uploadExtension(String chromeStoreCredentialsId, String 
chromeStoreRefreshTokenCredentialsId, String file, String extensionId) {
+    withCredentials([usernamePassword(credentialsId: chromeStoreCredentialsId, 
usernameVariable: 'CLIENT_ID', passwordVariable: 'CLIENT_SECRET')]) {
+        withCredentials([string(credentialsId: 
"${pipelineVars.chromeStoreRefreshTokenCredentialsId}", variable: 
'REFRESH_TOKEN')]) {
+            accessToken = sh(returnStdout: true, script: "curl -X POST -fsS 
\"https://oauth2.googleapis.com/token\"; -d 
\"client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token\"
 | jq -r '.access_token'").trim()
+            uploadResponse = sh(returnStdout: true, script: "curl -X PUT -sS 
\"https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extensionId}\"; 
-H \"Authorization: Bearer ${accessToken}\" -H \"x-goog-api-version:2\" -T 
${file}").trim()
+
+            return sh(returnStdout: true, script: "echo \"${uploadResponse}\" 
| jq -r '.uploadState'").trim()
+        }
+    }
+}
+
+/**
+* Publish an extension to Chrome Store
+*
+* @return String status
+*/
+def publishExtension(String chromeStoreCredentialsId, String 
chromeStoreRefreshTokenCredentialsId, String extensionId) {
+   withCredentials([usernamePassword(credentialsId: 
"${pipelineVars.chromeStoreCredentialsId}", usernameVariable: 'CLIENT_ID', 
passwordVariable: 'CLIENT_SECRET')]) {
+        withCredentials([string(credentialsId: 
"${pipelineVars.chromeStoreRefreshTokenCredentialsId}", variable: 
'REFRESH_TOKEN')]) {
+            script {
+                accessToken = sh(returnStdout: true, script: "curl -X POST 
-fsS \"https://oauth2.googleapis.com/token\"; -d 
\"client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&refresh_token=${REFRESH_TOKEN}&grant_type=refresh_token\"
 | jq -r '.access_token'").trim()
+                publishResponse = sh(returnStdout: true, script: "curl -X POST 
-sS 
\"https://www.googleapis.com/chromewebstore/v1.1/items/${extensionId}/publish\"; 
-H \"Authorization: Bearer ${accessToken}\" -H \"x-goog-api-version:2\" -H 
\"Content-Length:\"").trim()
+
+                return sh(returnStdout: true, script: "echo 
\"${publishResponse}\" | jq -r '.status | .[0]'").trim()
+            }
+        }
+    }
+}
+
+return this;
diff --git a/.ci/jenkins/shared-scripts/dockerUtils.groovy 
b/.ci/jenkins/shared-scripts/dockerUtils.groovy
new file mode 100644
index 0000000000..9f78c06dac
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/dockerUtils.groovy
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Push an image to a given registry
+*/
+def pushImageToRegistry(String registry, String image, String tags, String 
credentialsId) {
+    withCredentials([usernamePassword(credentialsId: credentialsId, 
usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PWD')]) {
+        sh "set +x && docker login -u $REGISTRY_USER -p $REGISTRY_PWD 
$registry"
+        tagList = tags.split(' ')
+        for (tag in tagList) {
+            sh "docker  push $registry/$image:$tag"
+        }
+        sh 'docker logout'
+    }
+}
+
+return this;
diff --git a/.ci/jenkins/shared-scripts/githubUtils.groovy 
b/.ci/jenkins/shared-scripts/githubUtils.groovy
new file mode 100644
index 0000000000..b5378ab021
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/githubUtils.groovy
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Create a Github release
+*
+* @return String with the release information
+*/
+def createRelease(String repository, String name, String tag, String commit, 
Boolean draft, Boolean preRelease, String credentialsId) {
+    withCredentials([string(credentialsId: credentialsId, variable: 
'GITHUB_TOKEN')]) {
+        response = sh returnStdout: true, script: """
+        set +x
+        curl -L \
+        -X POST \
+        -H "Accept: application/vnd.github+json" \
+        -H "Authorization: Bearer ${GITHUB_TOKEN}" \
+        -H "X-GitHub-Api-Version: 2022-11-28" \
+        https://api.github.com/repos/${repository}/releases \
+        -d '{"tag_name": "${tag}", "target_commitish": "${commit}", "name": 
"${name}", "draft": ${draft}, "prerelease": ${preRelease}'
+        """.trim()
+
+        return response
+    }
+}
+
+/**
+* Fetch a Github Release by tag
+*
+* @return String with the release information
+*/
+def fetchRelease(String repository, String tag, String credentialsId) {
+    withCredentials([string(credentialsId: credentialsId, variable: 
'GITHUB_TOKEN')]) {
+        response = sh returnStdout: true, script: """
+        set +x
+        curl -L \
+        -H "Accept: application/vnd.github+json" \
+        -H "Authorization: Bearer ${GITHUB_TOKEN}" \
+        -H "X-GitHub-Api-Version: 2022-11-28" \
+        https://api.github.com/repos/${repository}/releases/tags/${tag}
+        """.trim()
+
+        return response
+    }
+}
+
+/**
+* Upload an asset to a GitHub release
+*
+* @return String with the release asset information
+*/
+def uploadReleaseAsset(String uploadUrl, String assetPath, String assetName, 
String assetContentType, String credentialsId) {
+    withCredentials([string(credentialsId: credentialsId, variable: 
'GITHUB_TOKEN')]) {
+        response = sh returnStdout: true, script: """
+        set +x
+        curl -L \
+        -X POST \
+        -H "Accept: application/vnd.github+json" \
+        -H "Authorization: Bearer ${GITHUB_TOKEN}" \
+        -H "X-GitHub-Api-Version: 2022-11-28" \
+        -H "Content-Type: ${assetContentType}" \
+        "${uploadUrl}?name=${assetName}" \
+        --data-binary "@${assetPath}"
+        """.trim()
+
+        return response
+    }
+}
+
+/**
+* Set build status
+*/
+def commitStatus(String repository, String commit, String context, String 
state, String jobUrl) {
+    withCredentials([string(credentialsId: credentialsId, variable: 
'GITHUB_TOKEN')]) {
+        response = sh returnStdout: true, script: """
+        set +x
+        curl -L \
+        -X POST \
+        -H "Accept: application/vnd.github+json" \
+        -H "Authorization: Bearer ${GITHUB_TOKEN}" \
+        -H "X-GitHub-Api-Version: 2022-11-28" \
+        https://api.github.com/repos/${repository}/statuses/${commit} \
+        -d 
'{"state":"${state}","target_url":"${jobUrl}","description":"${message}","context":"${context}"}'
+        """.trim()
+
+        return response
+    }
+}
+
+/**
+* Parse an release upload asset url to remove unecessary strings
+*/
+def parseReleaseAssetUploadUrl(String uploadUrl) {
+    return uploadUrl.replace('{?name,label}', '')
+}
+
+/**
+* Checkout a github repository using GitSCM class
+*/
+def checkoutRepo(String url, String branch, String credentialsId) {
+    checkout([$class: 'GitSCM',
+        branches: [[name: "${branch}"]],
+        doGenerateSubmoduleConfigurations: false,
+        extensions: [[$class: 'CleanCheckout']],
+        submoduleCfg: [],
+        userRemoteConfigs: [[credentialsId: credentialsId, url: "${url}"]]
+    ])
+}
+
+/**
+* Perform a squashed merge on a local repository
+*/
+def squashedMerge(String author, String branch, String url) {
+    sh """#!/bin/bash -el
+    git config --global user.email "[email protected]"
+    git config --global user.name "KIE Tools Bot (kiegroup)"
+    git remote add ${author} ${url}
+    git fetch ${author} ${branch}
+    git merge --squash ${author}/${branch}
+    git commit --no-edit
+    """.trim()
+}
+
+/**
+* Checkout a github repository and perform a squashed merge on a local 
repository
+*/
+def checkoutRepoSquashedMerge(String author, String branch, String url, String 
targetBranch, String targetUrl, String credentialsId) {
+    checkoutRepo(targetUrl, targetBranch, credentialsId)
+    if (author && branch && url) {
+        squashedMerge(author, branch, url)
+    } else {
+        echo 'Skip squashed merge, not a pull request'
+    }
+}
+
+/**
+* @return the Github repository slug (org/repo) from an URL
+*/
+def getRepoSlug(String url) {
+    tokens = url.tokenize('/')
+    org = tokens[tokens.size()-4]
+    repo = tokens[tokens.size()-3]
+
+    return "${org}/${repo}"
+}
+
+/**
+* @return the files changed in the last commit
+*/
+def getChangesetLastCommit() {
+    changeset = sh returnStdout: true, script: '''
+    git diff --name-only HEAD HEAD~1
+    '''.trim()
+
+    return changeset
+}
+
+/**
+* @return if a given file is in the changeset of the last commit
+*/
+def fileIsInChangeset(String file) {
+    changeset = getChangesetLastCommit()
+
+    return changeset.contains(file)
+}
+
+return this;
diff --git a/.ci/jenkins/shared-scripts/openShiftUtils.groovy 
b/.ci/jenkins/shared-scripts/openShiftUtils.groovy
new file mode 100644
index 0000000000..729c4d7671
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/openShiftUtils.groovy
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Create or update an existing application in a Openshift cluster
+*/
+def createOrUpdateApp(String project, String appName, String imageTag, String 
imageUrl, String partOf, String deploymentIcon, String credentialsId, String 
deploymentEnvVarsPath='./deployment.env') {
+    withCredentials([usernamePassword(credentialsId: credentialsId, 
usernameVariable: 'OS_SERVER', passwordVariable: 'OS_TOKEN')]) {
+        sh 'set +x && oc login --token=$OS_TOKEN --server=$OS_SERVER 
--insecure-skip-tls-verify'
+        sh """#!/bin/bash -el
+        oc project ${project}
+
+        if [ ! -f ${deploymentEnvVarsPath} ]; then
+          echo "ENV file does not exist"
+          touch ${deploymentEnvVarsPath}
+        fi
+
+        if ! oc get deploy ${appName} > /dev/null 2>&1; then
+          echo "Create the app '${appName}'"
+
+          oc create imagestream ${appName}
+          oc import-image ${appName}:${imageTag} --from=${imageUrl} --confirm
+          oc tag ${appName}:${imageTag} ${appName}:latest
+
+          oc label imagestreams/${appName} app=${appName}
+          oc label imagestreams/${appName} 
app.kubernetes.io/component=${appName}
+          oc label imagestreams/${appName} 
app.kubernetes.io/instance=${appName}
+          oc label imagestreams/${appName} app.kubernetes.io/part-of=${partOf}
+
+          oc new-app ${appName}:latest --name=${appName} 
--env-file=${deploymentEnvVarsPath}
+          oc create route edge --service=${appName}
+
+          oc label services/${appName} app.kubernetes.io/part-of=${partOf}
+          oc label routes/${appName} app.kubernetes.io/part-of=${partOf}
+          oc label deployments/${appName} app.kubernetes.io/part-of=${partOf}
+          oc label deployments/${appName} 
app.openshift.io/runtime=${deploymentIcon}
+        else
+          echo "App '${appName}' already exists. Update the ImageStream 
instead."
+          oc tag -d ${appName}:latest
+          oc import-image ${appName}:${imageTag} --from=${imageUrl} --confirm
+          oc tag ${appName}:${imageTag} ${appName}:latest
+          cat ${deploymentEnvVarsPath} | oc set env deploy/${appName} -
+        fi
+        """.trim()
+        sh 'oc logout'
+    }
+}
+
+/**
+* @return String route to the OpenShift application
+*/
+def getAppRoute(String project, String appName, String credentialsId) {
+    withCredentials([usernamePassword(credentialsId: credentialsId, 
usernameVariable: 'OS_SERVER', passwordVariable: 'OS_TOKEN')]) {
+        sh 'set +x && oc login --token=$OS_TOKEN --server=$OS_SERVER 
--insecure-skip-tls-verify'
+        sh "oc project ${project}"
+        route = sh(returnStdout: true, script: "oc get route ${appName} -o 
jsonpath='{.spec.host}'").trim()
+        sh 'oc logout'
+
+        return "https://${route}";
+    }
+}
+
+return this;
diff --git a/.ci/jenkins/shared-scripts/pipelineVars.groovy 
b/.ci/jenkins/shared-scripts/pipelineVars.groovy
new file mode 100644
index 0000000000..c278e882aa
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/pipelineVars.groovy
@@ -0,0 +1,20 @@
+class PipelineVars implements Serializable {
+
+    String githubRepositoryOrg = 'apache';
+    String githubRepositoryName = 
'incubator-kie-tools-temporary-rnd-do-not-use';
+    String githubRepositorySlug = 
'apache/incubator-kie-tools-temporary-rnd-do-not-use';
+
+    String quayPushCredentialsId = 'quay-io-kie-tools-token';
+    String openshiftCredentialsId = 'openshift-kie-tools-token';
+    String kieToolsBotGithubCredentialsId = 'kie-tools-bot-gh';
+    String kieToolsBotGithubTokenCredentialsId = 'kie-tools-bot-gh-token';
+    String kieToolsGithubCodeQLTokenCredentialsId = 
'kie-tools-gh-codeql-token';
+    String chromeStoreCredentialsId = 'kie-tools-chome-store';
+    String chromeStoreRefreshTokenCredentialsId = 
'kie-tools-chome-store-refresh-token';
+    String npmTokenCredentialsId = 'kie-tools-npm-token';
+
+    String defaultArtifactsTempDir = 'artifacts-tmp';
+
+}
+
+return new PipelineVars();
diff --git a/.ci/jenkins/shared-scripts/zipUtils.groovy 
b/.ci/jenkins/shared-scripts/zipUtils.groovy
new file mode 100644
index 0000000000..1521ce5312
--- /dev/null
+++ b/.ci/jenkins/shared-scripts/zipUtils.groovy
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/**
+* Compress a build artifact to a zip file
+*/
+def zipArtifact(String filePath, String patterns) {
+    sh """#!/bin/bash -el
+    output_empty_zip () { echo UEsFBgAAAAAAAAAAAAAAAAAAAAAAAA== | base64 -d; }
+    zip -r ${filePath} ${patterns} || output_empty_zip > ${filePath}
+    """.trim()
+}
+
+/**
+* Unzip an build artifact
+*/
+def unzipArtifact(String filePath, String targetDir) {
+    sh """#!/bin/bash -el
+    unzip ${filePath} -d ${targetDir}
+    """.trim()
+}
+
+return this;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to