blob: 5f6bd5a5ba2a4dd99db8780c04b84ade17f52e90 [file] [log] [blame]
/*
* 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.
*/
plugins {
id 'base' // Define common lifecycle tasks and artifact types
id 'org.ajoberstar.grgit' // Publish website to asf-git branch.
}
def dockerImageTag = 'beam-website'
def dockerWorkDir = "/opt"
def buildDir = "${project.rootDir}/build/website"
def dockerSourceDir = "$dockerWorkDir/website/www"
def dockerBuildDir = "$dockerWorkDir/build/website"
def commitedChanges = false
def gitboxUrl = project.findProperty('gitPublishRemote') ?: 'https://gitbox.apache.org/repos/asf/beam.git'
def shell = { cmd ->
println cmd
exec {
executable 'sh'
args '-c', cmd
}
}
def envdir = "${buildDir}/gradleenv"
task setupVirtualenv {
doLast {
exec {
commandLine 'virtualenv', "${envdir}"
}
exec {
executable 'sh'
args '-c', ". ${envdir}/bin/activate && pip install beautifulsoup4"
}
}
outputs.dirs(envdir)
}
task buildDockerImage(type: Exec) {
commandLine 'docker', 'build', '-t', dockerImageTag, '.'
}
task createDockerContainer(type: Exec) {
dependsOn buildDockerImage
standardOutput = new ByteArrayOutputStream()
ext.containerId = {
return standardOutput.toString().trim()
}
gradle.taskGraph.whenReady {
def extraOptions = ''
if (gradle.taskGraph.hasTask(":${project.name}:serveWebsite")) {
// Publish port 1313 where Hugo serves website from
extraOptions = "--publish 127.0.0.1:1313:1313"
}
if (System.getenv().getOrDefault('NODE_NAME', '').startsWith('websites')) {
// Jenkins websites node: run as root. Files written to /repo will get
// "jenkins" user ownership. BEAM-9737
} else {
// Otherwise: run as current user, so as to not require sudo to clean up
// build/ directory.
extraOptions += " -u \$(id -u):\$(id -g)"
}
commandLine '/bin/bash', '-c',
"docker create -v $project.rootDir:$dockerWorkDir $extraOptions $dockerImageTag sh -c 'trap \"exit 0\" INT; while true; do sleep 30; done;'"
}
}
task startDockerContainer(type: Exec) {
dependsOn createDockerContainer
ext.containerId = {
return createDockerContainer.containerId()
}
commandLine 'docker', 'start',
"${->createDockerContainer.containerId()}" // Lazily evaluate containerId.
}
// Clone Docsy submodule which is our Hugo theme
task initGitSubmodules(type: Exec) {
commandLine 'docker', 'exec',
"${->startDockerContainer.containerId()}", 'git', 'submodule', 'update', '--init', '--recursive'
}
// Install yarn dependencies
task installDependencies(type: Exec) {
commandLine 'docker', 'exec', '--workdir', "$dockerSourceDir",
"${->startDockerContainer.containerId()}", 'yarn', 'install'
}
// Run build_code_samples.sh to fetch Beam project content
// which is used by code_sample shortcodes to inject snippets into codeblocks
task buildCodeSamples(type: Exec) {
commandLine 'docker', 'exec', '--workdir', "$dockerSourceDir",
"${->startDockerContainer.containerId()}", 'yarn', 'build_code_samples'
}
task setupDockerContainer(type: Exec) {
dependsOn startDockerContainer
finalizedBy initGitSubmodules, installDependencies, buildCodeSamples
ext.containerId = {
return startDockerContainer.containerId()
}
// Create the config to point to a GitHub or Colab blob in the repo, e.g. apache/beam/blob/master
commandLine 'docker', 'exec',
"${->startDockerContainer.containerId()}", '/bin/bash', '-c',
"""echo '[params]\n branch_repo = "${getBranchRepo()}"' > /tmp/_config_branch_repo.toml"""
}
task stopAndRemoveDockerContainer(type: Exec) {
commandLine 'docker', 'rm', '-f', "${->createDockerContainer.containerId()}"
}
task cleanWebsite(type: Delete) {
delete buildDir
}
clean.dependsOn cleanWebsite
class BuildTaskConfiguration {
String name
boolean useTestConfig = false
String baseUrl = ''
String dockerWorkDir = ''
}
def createBuildTask = {
BuildTaskConfiguration config = it as BuildTaskConfiguration
task "build${config.name}Website" (type:Exec) {
dependsOn setupDockerContainer
finalizedBy stopAndRemoveDockerContainer
def configs = "$dockerSourceDir/site/config.toml"
def baseUrlFlag = config.baseUrl ? "--baseURL /${config.baseUrl}" : ""
commandLine 'docker', 'exec',
"${->setupDockerContainer.containerId()}", '/bin/bash', '-c',
"""cd $dockerSourceDir && \
yarn build \
-d $dockerBuildDir/generated-${config.name.toLowerCase()}-content \
--config $configs \
$baseUrlFlag
"""
}
}
// task buildLocalWebsite
createBuildTask(
name:'Local',
)
task buildWebsite(dependsOn:buildLocalWebsite)
build.dependsOn buildWebsite
// task buildGcsWebsite
createBuildTask(
name:'Gcs',
baseUrl: getBaseUrl(),
)
// task buildApacheWebsite
createBuildTask(
name:'Apache',
)
/**
* Use the pull request ID in the URL path, or the git branch when building locally.
* This allows staging multiple work trees without clobbering eachother, i.e.
* for shared GCS staging, or for locally comparing the differences between
* branches.
*/
def getBaseUrl() {
def pullRequestId = System.getenv('ghprbPullId')?.trim()
if (pullRequestId) { return pullRequestId }
// grgit is null if building outside of git (i.e. from source archive)
def buildContext = grgit ? grgit.branch.current().getName() : 'archive'
return "${System.getProperty('user.name')}-${buildContext}"
}
/**
* Gets the branch repository where the new files are located. This is used to point
* all the GitHub and Colab files to the pull request's branch when testing, while
* pointing them to the apache/beam master branch on production. This is done so tests
* and staged versions work even when the files don't exist in master yet.
*/
def getBranchRepo() {
// Jenkins stores the GitHub author in $ghprbPullAuthorLogin
def author = System.env.ghprbPullAuthorLogin
if (author == null) {
// If the author is not defined, it's most probably running locally.
if (grgit != null) {
// If on git, try to infer the author from the remote URL
for (remote in grgit.remote.list()) {
if (remote.getName() == 'origin') {
// remote.url = 'git@github.com:author/beam.git'
author = remote.url.split(':')[1].split('/')[0]
break
}
}
}
}
// Jenkins stores the branch in both of the following environment variables.
def branch = System.env.ghprbSourceBranch ?: System.env.GIT_BRANCH
if (branch == null && grgit) {
branch = grgit.branch.current().getName()
}
// Return the author's branch repo, otherwise default to the master repo.
if (author && branch) {
return "${author}/beam/blob/${branch}"
}
return "apache/beam/blob/master"
}
def buildContentDir(name) {
"${project.rootDir}/build/website/generated-${name.toLowerCase()}-content"
}
task serveWebsite(type: Exec) {
dependsOn setupDockerContainer
finalizedBy stopAndRemoveDockerContainer
commandLine 'docker', 'exec',
"${->setupDockerContainer.containerId()}", '/bin/bash', '-c',
"""cd $dockerSourceDir && \
yarn develop \
--bind="0.0.0.0" \
--config $dockerSourceDir/site/config.toml,/tmp/_config_branch_repo.toml
"""
}
task testWebsite(type: Exec) {
// dependsOn setupDockerContainer, 'buildWebsite'
finalizedBy stopAndRemoveDockerContainer
commandLine 'docker', 'exec',
"${->setupDockerContainer.containerId()}", '/bin/bash', '-c',
"$dockerSourceDir/check-links.sh $dockerBuildDir/generated-local-content"
}
testWebsite.dependsOn 'buildLocalWebsite'
task preCommit {
dependsOn testWebsite
}
// Creates a new commit on asf-site branch
task commitWebsite {
doLast {
assert grgit : "Cannot commit website outside of git repository"
assert file("${buildContentDir('apache')}/index.html").exists()
// Generated javadoc and pydoc content is not built or stored in this repo.
assert !file("${buildContentDir('apache')}/documentation/sdks/javadoc").exists()
assert !file("${buildContentDir('apache')}/documentation/sdks/pydoc").exists()
def git = grgit.open(currentDir: project.rootDir)
// get the latest commit on master
def latestCommit = grgit.log(maxCommits: 1)[0].abbreviatedId
shell "git fetch --force origin +asf-site:asf-site"
shell "git stash"
shell "git checkout asf-site"
git.checkout(branch: 'asf-site')
// Delete the previous content. These are asf-site branch paths.
git.remove(patterns: [ 'website/generated-content' ])
def repoContentDir = "${project.rootDir}/website/generated-content"
assert !file("${repoContentDir}/index.html").exists()
delete repoContentDir
// Copy the built content and add it.
copy {
from buildContentDir('apache')
into repoContentDir
}
assert file("${repoContentDir}/index.html").exists()
git.add(patterns: ['website/generated-content'])
def currentDate = new Date().format('yyyy/MM/dd HH:mm:ss')
String message = "Publishing website ${currentDate} at commit ${latestCommit}"
if (!git.status().staged.getAllChanges()) {
println 'No changes to commit'
} else {
println 'Creating commit for changes'
commitedChanges = true
git.commit(message: message)
}
}
}
/*
* Pushes the asf-site branch commits.
*
* This requires write access to the asf-site branch and can be run on
* Jenkins executors with the git-websites label.
*
* For more details on publishing, see:
* https://www.apache.org/dev/project-site.html
* https://github.com/apache/infrastructure-puppet/blob/deployment/modules/gitwcsub/files/config/gitwcsub.cfg
*
* You can test this locally with a forked repository by manually adding the
* website-publish remote pointing to your forked repository, for example:
* git remote add website-publish git@github.com:${GITUSER}/beam.git
* because the remote is only added if it doesn't exist. The remote needs
* to be added before every execution of the publishing.
*/
task publishWebsite {
doLast {
assert grgit : "Cannot publish website outside of git repository"
def git = grgit.open(currentDir: project.rootDir)
git.checkout(branch: 'asf-site')
if (!commitedChanges) {
println 'No changes to push'
return
}
// Because git.push() fails to authenticate, run git push directly.
shell "git push ${gitboxUrl} asf-site"
}
}
commitWebsite.dependsOn buildApacheWebsite
publishWebsite.dependsOn commitWebsite
/*
* Stages a pull request on GCS
* For example:
* ./gradlew :website:stageWebsite -PwebsiteBucket=foo
*/
task stageWebsite {
doLast {
def baseUrl = getBaseUrl()
assert baseUrl : 'Website staging requires a valid baseUrl'
def gcs_bucket = project.findProperty('websiteBucket') ?: 'apache-beam-website-pull-requests'
def gcs_path = "gs://${gcs_bucket}/${baseUrl}"
// Fixup the links to index.html files
shell ". ${envdir}/bin/activate && python append_index_html_to_internal_links.py ${buildContentDir('gcs')}"
// Copy the build website to GCS
shell "gsutil -m rsync -r -d ${buildContentDir('gcs')} ${gcs_path}"
println "Website published to http://${gcs_bucket}." +
"storage.googleapis.com/${baseUrl}/index.html"
}
}
stageWebsite.dependsOn setupVirtualenv
stageWebsite.dependsOn buildGcsWebsite