Migrate from Azure Pipelines to Github Actions (#474)

This commit migrates wasmtime's CI infrastructure from Azure Pipelines
to Github Actions. Using Github Actions has a few benefits over other
offerings:

* Being natively integrated with Github means that there's no degree of
  user account configuration or access control management, it's all
  inherent via already existing Github permissions.

* Github Actions gives 20 parallel builders instead of Azure's 10 by
  default, which is a nice boost to have!

Overall I've found Github Actions to feel a bit cleaner than Azure
Pipelines as well. Subjectively I've found the configuration to be more
readable and more pleasant to work with, although they're both just as
"powerful" I think. Additionally Github Actions has been pretty solid in
my own personal testing for a number of other projects.

The main trickiness with wasmtime's CI is the rolling `dev` release of
the master branch as well as binary releases for tags. Github Actions
doesn't have quite as much built in functionality as Azure Pipelines,
but Github Actions does have a nice feature where you can define the
code for an action locally rather than only using built-in actions.

This migration adds three local actions with some associated JS code to
run the action (currently it looks like it basically requires JS)

* An `install-rust` action papers over the gotchas about installing
  Rust, allowing Rust installation to be a one-liner in the configuration.

* A `binary-compatible-builds` action allows easily configuring the
  wheels and the binaries to be "more binary compatible" and handles
  things like compilation flags on OSX and Windows while handling the
  `centos:6` container on Linux.

* The `github-release` action is the logic using the `@actions/github`
  JS package to orchestrate the custom way we manage rolling releases,
  ensuring that a new release is made for the master branch under `dev`
  (deleting the previous tag/release ahead of time) and then also
  manages tagged releases by uploading them there.

I'm hoping that most of the inline actions here will largely go away.
For example `install-rust` should be simply `rustup update $toolchain`
once various environment issues are fixed on Github Actions runner
images. Additionally `github-release` will ideally migrate to something
like https://github.com/actions/create-release or similar once it has
enough functionality. I'm also hoping that the maintenance in the
meantime of these actions is pretty low-cost, but if it becomes an issue
we can look into other solutions!
This commit is contained in:
Alex Crichton
2019-11-05 19:21:52 -06:00
committed by Dan Gohman
parent c0c7851cb6
commit 10f27197b5
18 changed files with 675 additions and 545 deletions

View File

@@ -0,0 +1,9 @@
# binary-compatible-builds
A small (ish) action which is intended to be used and will configure builds of
Rust projects to be "more binary compatible". On Windows and macOS this
involves setting a few env vars, and on Linux this involves spinning up a CentOS
6 container which is running in the background.
All subsequent build commands need to be wrapped in `$CENTOS` to optionally run
on `$CENTOS` on Linux to ensure builds happen inside the container.

View File

@@ -0,0 +1,6 @@
name: 'Set up a CentOS 6 container to build releases in'
description: 'Set up a CentOS 6 container to build releases in'
runs:
using: node12
main: 'main.js'

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
const child_process = require('child_process');
const stdio = { stdio: 'inherit' };
// On OSX all we need to do is configure our deployment target as old as
// possible. For now 10.9 is the limit.
if (process.platform == 'darwin') {
console.log("::set-env name=MACOSX_DEPLOYMENT_TARGET::10.9");
console.log("::set-env name=python::python3");
return;
}
// On Windows we build against the static CRT to reduce dll dependencies
if (process.platform == 'win32') {
console.log("::set-env name=RUSTFLAGS::-Ctarget-feature=+crt-static");
console.log("::set-env name=python::python");
return;
}
// ... and on Linux we do fancy things with containers. We'll spawn an old
// CentOS container in the background with a super old glibc, and then we'll run
// commands in there with the `$CENTOS` env var.
if (process.env.CENTOS !== undefined) {
const args = ['exec', '-w', process.cwd(), '-i', 'centos'];
for (const arg of process.argv.slice(2)) {
args.push(arg);
}
child_process.execFileSync('docker', args, stdio);
return;
}
// Add our rust mount onto PATH, but also add some stuff to PATH from
// the packages that we install.
let path = process.env.PATH;
path = `${path}:/rust/bin`;
path = `/opt/rh/devtoolset-8/root/usr/bin:${path}`;
path = `/opt/rh/rh-python36/root/usr/bin:${path}`;
// Spawn a container daemonized in the background which we'll connect to via
// `docker exec`. This'll have access to the current directory.
child_process.execFileSync('docker', [
'run',
'-di',
'--name', 'centos',
'-v', `${process.cwd()}:${process.cwd()}`,
'-v', `${child_process.execSync('rustc --print sysroot').toString().trim()}:/rust:ro`,
'--env', `PATH=${path}`,
'centos:6',
], stdio);
// Use ourselves to run future commands
console.log(`::set-env name=CENTOS::${__filename}`)
// See https://edwards.sdsu.edu/research/c11-on-centos-6/ for where these
const exec = s => {
child_process.execSync(`docker exec centos ${s}`, stdio);
};
exec('yum install -y centos-release-scl cmake xz epel-release');
exec('yum install -y rh-python36 patchelf unzip');
exec('yum install -y devtoolset-8-gcc devtoolset-8-binutils devtoolset-8-gcc-c++');
// Delete `libstdc++.so` to force gcc to link against `libstdc++.a` instead.
// This is a hack and not the right way to do this, but it ends up doing the
// right thing for now.
exec('rm -f /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.so');
console.log("::set-env name=python::python3");

View File

@@ -0,0 +1,8 @@
FROM node:slim
COPY . /action
WORKDIR /action
RUN npm install --production
ENTRYPOINT ["node", "/action/main.js"]

View File

@@ -0,0 +1,18 @@
# github-release
An action used to publish GitHub releases for `wasmtime`.
As of the time of this writing there's a few actions floating around which
perform github releases but they all tend to have their set of drawbacks.
Additionally nothing handles deleting releases which we need for our rolling
`dev` release.
To handle all this this action rolls-its-own implementation using the
actions/toolkit repository and packages published there. These run in a Docker
container and take various inputs to orchestrate the release from the build.
More comments can be found in `main.js`.
Testing this is really hard. If you want to try though run `npm install` and
then `node main.js`. You'll have to configure a bunch of env vars though to get
anything reasonably working.

View File

@@ -0,0 +1,15 @@
name: 'wasmtime github releases'
description: 'wasmtime github releases'
inputs:
token:
description: ''
required: true
name:
description: ''
required: true
files:
description: ''
required: true
runs:
using: 'docker'
image: 'Dockerfile'

91
.github/actions/github-release/main.js vendored Normal file
View File

@@ -0,0 +1,91 @@
const core = require('@actions/core');
const path = require("path");
const fs = require("fs");
const github = require('@actions/github');
const glob = require('glob');
async function run() {
// Load all our inputs and env vars. Note that `getInput` reads from `INPUT_*`
const files = core.getInput('files');
const name = core.getInput('name');
const token = core.getInput('token');
const slug = process.env.GITHUB_REPOSITORY;
const owner = slug.split('/')[0];
const repo = slug.split('/')[1];
const sha = process.env.GITHUB_SHA;
core.info(`files: ${files}`);
core.info(`name: ${name}`);
core.info(`token: ${token}`);
const octokit = new github.GitHub(token);
// If this is a `dev` release then we need to actually delete the previous
// release since we can't overwrite a new one. We also need to update the
// `dev` tag while we're at it. So here you'll see:
//
// * Look for the `dev` release, then delete it if it exists
// * Update the `dev` release to our current sha, or create one if it doesn't
// exist
if (name == 'dev') {
const releases = await octokit.paginate("GET /repos/:owner/:repo/releases", { owner, repo });
for (const release of releases) {
if (release.tag_name !== 'dev') {
continue;
}
const release_id = release.id;
core.info(`deleting release ${release_id}`);
await octokit.repos.deleteRelease({ owner, repo, release_id });
}
try {
core.info(`updating dev tag`);
await octokit.git.updateRef({
owner,
repo,
ref: 'tags/dev',
sha,
force: true,
});
} catch (e) {
console.log("ERROR: ", JSON.stringify(e, null, 2));
core.info(`creating dev tag`);
await octokit.git.createTag({
owner,
repo,
tag: 'dev',
message: 'dev release',
object: sha,
type: 'commit',
});
}
}
// Creates an official GitHub release for this `tag`, and if this is `dev`
// then we know that from the previous block this should be a fresh release.
core.info(`creating a release`);
const release = await octokit.repos.createRelease({
owner,
repo,
tag_name: name,
prerelease: name === 'dev',
});
// Upload all the relevant assets for this release as just general blobs.
for (const file of glob.sync(files)) {
const size = fs.statSync(file).size;
core.info(`upload ${file}`);
await octokit.repos.uploadReleaseAsset({
file: fs.createReadStream(file),
headers: { 'content-length': size, 'content-type': 'application/octet-stream' },
name: path.basename(file),
url: release.data.upload_url,
})
}
}
run().catch(err => {
console.log("ERROR: ", JSON.stringify(err, null, 2));
core.setFailed(err.message);
console.log(err.stack);
});

View File

@@ -0,0 +1,10 @@
{
"name": "wasmtime-github-release",
"version": "0.0.0",
"main": "main.js",
"dependencies": {
"@actions/core": "^1.0.0",
"@actions/github": "^1.0.0",
"glob": "^7.1.5"
}
}

18
.github/actions/install-rust/README.md vendored Normal file
View File

@@ -0,0 +1,18 @@
# install-rust
A small github action to install `rustup` and a Rust toolchain. This is
generally expressed inline, but it was repeated enough in this repository it
seemed worthwhile to extract.
Some gotchas:
* Can't `--self-update` on Windows due to permission errors (a bug in Github
Actions)
* `rustup` isn't installed on macOS (a bug in Github Actions)
When the above are fixed we should delete this action and just use this inline:
```yml
- run: rustup update $toolchain && rustup default $toolchain
shell: bash
```

12
.github/actions/install-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: 'Install Rust toolchain'
description: 'Install both `rustup` and a Rust toolchain'
inputs:
toolchain:
description: 'Default toolchan to install'
required: false
default: 'stable'
runs:
using: node12
main: 'main.js'

17
.github/actions/install-rust/main.js vendored Normal file
View File

@@ -0,0 +1,17 @@
const child_process = require('child_process');
const toolchain = process.env.INPUT_TOOLCHAIN;
for (var i = 0, keys = Object.keys(process.env), ii = keys.length; i < ii; i++) {
console.log(keys[i] + '=' + process.env[keys[i]]);
}
if (process.platform === 'darwin') {
child_process.execSync(`curl https://sh.rustup.rs | sh -s -- -y --default-toolchain=none --profile=minimal`);
const bindir = `${process.env.HOME}/.cargo/bin`;
console.log(`::add-path::${bindir}`);
process.env.PATH = `${process.env.PATH}:${bindir}`;
child_process.execFileSync('rustup', ['set', 'profile', 'minimal']);
}
child_process.execFileSync('rustup', ['update', toolchain, '--no-self-update']);
child_process.execFileSync('rustup', ['default', toolchain]);

348
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,348 @@
name: CI
on:
push:
branches: [master]
tags-ignore: [dev]
pull_request:
branches: [master]
jobs:
# Check Code style quickly by running `rustfmt` over all code
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: ./.github/actions/install-rust
- run: cargo fmt --all -- --check
# Build `mdBook` documentation for `wasmtime`, and upload it as a temporary
# build artifact
doc_book:
name: Doc - build the book
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- run: |
set -e
curl -L https://github.com/rust-lang-nursery/mdBook/releases/download/v0.3.1/mdbook-v0.3.1-x86_64-unknown-linux-gnu.tar.gz | tar xzf -
echo ::add-path::`pwd`
- run: (cd docs && mdbook build)
- uses: actions/upload-artifact@v1
with:
name: doc-book
path: docs/book
# Build rustdoc API documentation for `wasmtime*` crates. Note that we don't
# want to document all our transitive dependencies, hence `--no-deps`. This is
# a temporary build artifact we upload to consume later.
doc_api:
name: Doc - build the API documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: ./.github/actions/install-rust
- run: cargo doc --no-deps -p wasmtime
- run: cargo doc --no-deps -p wasmtime-api
- run: cargo doc --no-deps -p wasmtime-debug
- run: cargo doc --no-deps -p wasmtime-environ
- run: cargo doc --no-deps -p wasmtime-interface-types
- run: cargo doc --no-deps -p wasmtime-jit
- run: cargo doc --no-deps -p wasmtime-obj
- run: cargo doc --no-deps -p wasmtime-runtime
- run: cargo doc --no-deps -p wasmtime-wasi
- run: cargo doc --no-deps -p wasmtime-wast
- uses: actions/upload-artifact@v1
with:
name: doc-api
path: target/doc
# Perform all tests (debug mode) for `wasmtime`. This runs stable/beta/nightly
# channels of Rust as well as macOS/Linux/Windows.
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [stable, beta, nightly, windows, linux]
include:
- build: stable
os: macos-latest
rust: stable
- build: beta
os: macos-latest
rust: beta
- build: nightly
os: macos-latest
rust: nightly
- build: linux
os: ubuntu-latest
rust: stable
- build: windows
os: windows-latest
rust: stable
steps:
- uses: actions/checkout@master
with:
submodules: true
- uses: ./.github/actions/install-rust
with:
toolchain: ${{ matrix.rust }}
- run: cargo fetch
# Build and test all features except for lightbeam
- run: cargo test --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py -- --nocapture
env:
RUST_BACKTRACE: 1
# Build and test lightbeam if we're using the nightly toolchain. Note that
# Lightbeam tests fail right now, but we don't want to block on that.
- run: cargo build --package lightbeam
if: matrix.rust == 'nightly'
- run: cargo test --package lightbeam -- --nocapture
if: matrix.rust == 'nightly'
continue-on-error: true
env:
RUST_BACKTRACE: 1
# Builds a Python wheel (package) for Windows/Mac/Linux. Note that we're
# careful to create binary-compatible releases here to old releases of
# Windows/Mac/Linux. This will also build wheels for Python 3.6 and 3.7.
wheels:
name: Python Wheel
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@master
- uses: ./.github/actions/install-rust
with:
toolchain: nightly-2019-08-15
- uses: ./.github/actions/binary-compatible-builds
- run: mkdir misc/wasmtime-py/wheelhouse
shell: bash
# Install Python & dependencies needed for our `setup.py` scripts
- name: Setup Python 3.6
uses: actions/setup-python@v1
with:
python-version: '3.6'
architecture: x64
- run: $CENTOS pip3 install setuptools wheel==0.31.1 setuptools-rust
shell: bash
- run: (cd misc/wasmtime-py && $CENTOS $python setup.py bdist_wheel)
shell: bash
# Clear the build directory between building different wheels for different
# Python versions to ensure that we don't package dynamic libraries twice by
# accident.
- run: $CENTOS rm -rf misc/wasmtime-py/build
shell: bash
# Set up Python 3.7 (and build it on Linux), reinstall dependencies, then
# rebuild our wheels
- name: Setup Python 3.7
uses: actions/setup-python@v1
with:
python-version: '3.7'
architecture: x64
if: matrix.os != 'ubuntu-latest'
- name: Build Python 3.7
run: $CENTOS sh ci/setup_centos6_python37.sh
if: matrix.os == 'ubuntu-latest'
- run: $CENTOS pip3 install setuptools wheel==0.31.1 setuptools-rust auditwheel
shell: bash
- run: (cd misc/wasmtime-py && $CENTOS $python setup.py bdist_wheel)
shell: bash
# Move `dist/*.whl` into `wheelhouse/` so we can deploy them, but on Linux we
# need to run an `auditwheel` command as well to turn these into "manylinux"
# wheels to run across a number of distributions.
- run: cp misc/wasmtime-py/dist/*.whl misc/wasmtime-py/wheelhouse/
shell: bash
if: matrix.os != 'ubuntu-latest'
- run: |
set -e
cd misc/wasmtime-py
for whl in dist/*.whl; do
$CENTOS auditwheel repair "$whl" -w wheelhouse/
done
shell: bash
if: matrix.os == 'ubuntu-latest'
# Upload this for the publishing stage of pipelines
- uses: actions/upload-artifact@v1
with:
name: wheels-${{ matrix.os }}
path: misc/wasmtime-py/wheelhouse
# Perform release builds of `wasmtime` and `libwasmtime_api.so`. Builds on
# Windows/Mac/Linux, and artifacts are uploaded after the build is finished.
# Note that we also run tests here to test exactly what we're deploying.
build:
name: Build wasmtime
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@master
with:
submodules: true
- uses: ./.github/actions/install-rust
- uses: ./.github/actions/binary-compatible-builds
# Build `wasmtime` and executables
- run: $CENTOS cargo build --release --bin wasmtime --bin wasm2obj
shell: bash
# Build `libwasmtime_api.so`
- run: $CENTOS cargo build --release --features wasm-c-api --manifest-path wasmtime-api/Cargo.toml
shell: bash
# Test what we just built
- run: $CENTOS cargo test --release --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py --exclude wasmtime-api
shell: bash
env:
RUST_BACKTRACE: 1
# ... and now perform some goop to move all the relevant artifacts into
# something that we'll upload from this action.
- run: mkdir dist
shell: bash
# Move binaries to dist folder
- run: cp target/release/{wasmtime,wasm2obj} dist
if: matrix.os != 'windows-latest'
- run: cp target/release/{wasmtime,wasm2obj}.exe dist
shell: bash
if: matrix.os == 'windows-latest'
# Move libwasmtime_api dylib to dist folder
- run: cp target/release/libwasmtime_api.{so,a} dist
if: matrix.os == 'ubuntu-latest'
- run: cp target/release/libwasmtime_api.{dylib,a} dist
if: matrix.os == 'macos-latest'
- run: cp target/release/wasmtime_api.{dll,lib} dist
shell: bash
if: matrix.os == 'windows-latest'
# Make a Windows MSI installer if we're on Windows
- run: |
export WT_VERSION=`cat Cargo.toml | sed -n 's/^version = "\([^"]*\)".*/\1/p'`
"$WIX/bin/candle" -arch x64 -out target/wasmtime.wixobj installer/msi/wasmtime.wxs
"$WIX/bin/light" -out dist/installer.msi target/wasmtime.wixobj -ext WixUtilExtension
rm dist/installer.wixpdb
shell: bash
if: matrix.os == 'windows-latest'
- uses: actions/upload-artifact@v1
with:
name: bins-${{ matrix.os }}
path: dist
# Consumes all published artifacts from all the previous build steps, creates
# a bunch of tarballs for all of them, and then publishes the tarballs
# themselves as an artifact (for inspection) and then optionally creates
# github releases and/or tags for pushes.
publish:
name: Publish
needs: [doc_book, doc_api, wheels, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
with:
submodules: true
- run: rustup update stable && rustup default stable
# Download all the artifacts that we'll be publishing. Should keep an eye on
# the `download-artifact` repository to see if we can ever get something
# like "download all artifacts" or "download this list of artifacts"
- name: Download book
uses: actions/download-artifact@v1
with:
name: doc-book
- name: Download API docs
uses: actions/download-artifact@v1
with:
name: doc-api
- name: Download macOS Wheel
uses: actions/download-artifact@v1
with:
name: wheels-macos-latest
- name: Download macOS binaries
uses: actions/download-artifact@v1
with:
name: bins-macos-latest
- name: Download Linux Wheel
uses: actions/download-artifact@v1
with:
name: wheels-ubuntu-latest
- name: Download Linux binaries
uses: actions/download-artifact@v1
with:
name: bins-ubuntu-latest
- name: Download Windows Wheel
uses: actions/download-artifact@v1
with:
name: wheels-windows-latest
- name: Download Windows binaries
uses: actions/download-artifact@v1
with:
name: bins-windows-latest
- name: Assemble gh-pages
run: |
mv doc-book gh-pages
mv doc-api gh-pages/api
# If this is a push to the master branch push to the `gh-pages` using a
# deploy key. Note that a deploy key is necessary for now because otherwise
# using the default token for github actions doesn't actually trigger a page
# rebuild.
- name: Push to gh-pages
run: curl -LsSf https://git.io/fhJ8n | rustc - && (cd gh-pages && ../rust_out)
env:
GITHUB_DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
BUILD_REPOSITORY_ID: ${{ github.repository }}
BUILD_SOURCEVERSION: ${{ github.sha }}
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
- name: Calculate tag name
run: |
name=dev
if [[ $GITHUB_REF == refs/tags* ]]; then
name=${GITHUB_REF:10}
fi
echo ::set-output name=val::$name
echo ::set-env name=TAG::$name
id: tagname
# Assemble all the build artifacts into tarballs and zip archives.
- name: Assemble tarballs
run: |
./ci/build-tarballs.sh x86_64-linux ubuntu-latest
./ci/build-tarballs.sh x86_64-windows windows-latest .exe
./ci/build-tarballs.sh x86_64-macos macos-latest
# Upload all assembled tarballs as an artifact of the github action run, so
# that way even PRs can inspect the output.
- uses: actions/upload-artifact@v1
with:
name: tarballs
path: dist
# ... and if this was an actual push (tag or `master`) then we publish a
# new release. This'll automatically publish a tag release or update `dev`
# with this `sha`
- name: Publish Release
uses: ./.github/actions/github-release
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags'))
with:
files: "dist/*"
name: ${{ steps.tagname.outputs.val }}
token: ${{ secrets.GITHUB_TOKEN }}