diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4eb5c69 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,131 @@ +name: R builds + +on: + push: + paths: + - 'builder/**' + - 'test/**' + - 'Makefile' + - '.github/workflows/test.yml' + workflow_dispatch: + inputs: + platforms: + description: | + Comma-separated list of platforms. Specify "all" to use all platforms (the default). + required: false + default: 'all' + type: string + r_versions: + description: | + Comma-separated list of R versions. Specify "last-N" to use the + last N minor R versions, or "all" to use all minor R versions since R 3.1. + Defaults to "last-5". + required: false + default: 'last-5' + type: string + +permissions: + contents: read + +jobs: + setup-matrix: + runs-on: ubuntu-latest + outputs: + platforms: ${{ steps.setup-matrix.outputs.platforms }} + r_versions: ${{ steps.setup-matrix.outputs.r_versions }} + steps: + - uses: actions/checkout@v3 + + - name: Set up matrix of platforms and R versions + id: setup-matrix + run: | + platforms=$(python test/get_platforms.py ${{ github.event.inputs.platforms }}) + echo "::set-output name=platforms::$platforms" + r_versions=$(python test/get_r_versions.py ${{ github.event.inputs.r_versions }}) + echo "::set-output name=r_versions::$r_versions" + + docker-images: + needs: setup-matrix + strategy: + matrix: + platform: ${{ fromJson(needs.setup-matrix.outputs.platforms) }} + runs-on: ubuntu-latest + name: Docker image (${{ matrix.platform }}) + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + install: true + + # Enable Docker layer caching without having to push to a registry. + # https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#local-cache + # This may eventually be migrated to the GitHub Actions cache backend, + # which is still considered experimental. + # https://github.com/moby/buildkit#github-actions-cache-experimental + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ matrix.platform }}-buildx-${{ github.sha }} + restore-keys: ${{ matrix.platform }}-buildx- + + # Use docker buildx instead of docker-compose here because cache exporting + # does not seem to work as of docker-compose v2.6.0 and buildx v0.8.2, even + # though it works with buildx individually. + - name: Build image + run: | + docker buildx build -t r-builds:${{ matrix.platform }} \ + --file builder/Dockerfile.${{ matrix.platform }} \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache-new,mode=max" \ + builder + + # Temporary workaround for unbounded GHA cache growth with the local cache mode. + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + test: + needs: [setup-matrix, docker-images] + strategy: + fail-fast: false + matrix: + platform: ${{ fromJson(needs.setup-matrix.outputs.platforms) }} + r_version: ${{ fromJson(needs.setup-matrix.outputs.r_versions) }} + runs-on: ubuntu-latest + name: ${{ matrix.platform }} (R ${{ matrix.r_version }}) + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + install: true + + - name: Restore cached Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ matrix.platform }}-buildx-${{ github.sha }} + restore-keys: ${{ matrix.platform }}-buildx- + + - name: Load cached Docker image + run: | + docker buildx build -t r-builds:${{ matrix.platform }} \ + --file builder/Dockerfile.${{ matrix.platform }} \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --load \ + builder + + - name: Build R + run: | + R_VERSION=${{ matrix.r_version }} make build-r-${{ matrix.platform }} + + - name: Test R + run: | + R_VERSION=${{ matrix.r_version }} make test-r-${{ matrix.platform }} diff --git a/Makefile b/Makefile index f0f7492..186b40c 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,9 @@ $(foreach platform,$(PLATFORMS), \ $(eval $(GEN_TARGETS)) \ ) +print-platforms: + @echo $(PLATFORMS) + # Helper for launching a bash session on a docker image of your choice. Defaults # to "ubuntu:xenial". TARGET_IMAGE?=ubuntu:xenial @@ -68,4 +71,4 @@ bash: -w /r-builds \ ${TARGET_IMAGE} /bin/bash -.PHONY: deps docker-build docker-push docker-down docker-build-package docker-shell-package-env ecr-login fetch-serverless-custom-file serverless-deploy +.PHONY: deps docker-build docker-push docker-down docker-build-package docker-shell-package-env ecr-login fetch-serverless-custom-file print-platforms serverless-deploy diff --git a/README.md b/README.md index 2ee37c2..ba42a2f 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,21 @@ environment: In order for the makefile to push these new platforms to ECR, add them to the PLATFORMS variable near the top of the Makefile +### test/docker-compose.yml + +A new service in the `test/docker-compose.yml` file named according to the `platform-version` and containing the proper entries: + +```yaml + ubuntu-2204: + image: ubuntu:jammy + command: /r-builds/test/test-apt.sh + environment: + - OS_IDENTIFIER=ubuntu-2204 + - R_VERSION=${R_VERSION} + volumes: + - ../:/r-builds +``` + ### Submit a Pull Request Once you've followed the steps above, submit a pull request. On successful merge, builds for this platform will begin to be available from the CDN. @@ -244,6 +259,16 @@ serverless invoke stepf -n rBuilds -d '{"force": true, "versions": ["3.6.3", "4. ## Testing +Tests are automatically run on each push that changes a file in `builder/`, `test/`, or the `Makefile`. +These tests validate that R was correctly configured, built, and packaged. By default, the tests run +for the last 5 minor R versions on each platform. + +To run the tests manually, you can navigate to the [GitHub Actions workflow page](https://github.com/rstudio/r-builds/actions/workflows/test.yml) +and use "Run workflow" to run the tests from a custom branch, list of platforms, and list of R versions. + +To skip the tests, add `[skip ci]` to your commit message. See [Skipping workflow runs](https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs) +for more information. + To test the R builds locally, you can use the `build-r-$PLATFORM` and `test-r-$PLATFORM` targets to build R and run the tests. The tests use the quick install script to install R, using a locally built R if present, or otherwise a build from the CDN. diff --git a/builder/docker-compose.yml b/builder/docker-compose.yml index 9eed916..0ab026c 100644 --- a/builder/docker-compose.yml +++ b/builder/docker-compose.yml @@ -1,5 +1,3 @@ -version: '2.0' - services: ubuntu-1804: command: ./build.sh diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 57215bd..2bb5760 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -1,5 +1,3 @@ -version: '2.0' - services: ubuntu-1804: image: ubuntu:bionic diff --git a/test/get_platforms.py b/test/get_platforms.py new file mode 100644 index 0000000..20d7e6d --- /dev/null +++ b/test/get_platforms.py @@ -0,0 +1,31 @@ +import argparse +import json +import subprocess + + +def main(): + parser = argparse.ArgumentParser(description="Print R-builds platforms as JSON.") + parser.add_argument( + 'platforms', + type=str, + nargs='?', + default='all', + help='Comma-separated list of platforms. Specify "all" to use all platforms (the default).' + ) + args = parser.parse_args() + platforms = _get_platforms(which=args.platforms) + print(json.dumps(platforms)) + + +def _get_platforms(which='all'): + supported_platforms = subprocess.check_output(['make', 'print-platforms'], text=True) + supported_platforms = supported_platforms.split() + if which == 'all': + return supported_platforms + platforms = which.split(',') + platforms = [p for p in platforms if p in supported_platforms] + return platforms + + +if __name__ == '__main__': + main() diff --git a/test/get_r_versions.py b/test/get_r_versions.py new file mode 100644 index 0000000..0a2ebd9 --- /dev/null +++ b/test/get_r_versions.py @@ -0,0 +1,67 @@ +import argparse +import json +import re +import urllib.request + +VERSIONS_URL = 'https://cdn.rstudio.com/r/versions.json' + +# Minimum R version for "all" +MIN_ALL_VERSION = '3.1.0' + + +def main(): + parser = argparse.ArgumentParser(description="Print R-builds R versions as JSON.") + parser.add_argument( + 'versions', + type=str, + nargs='?', + default='last-5', + help="""Comma-separated list of R versions. Specify "last-N" to use the + last N minor R versions, or "all" to use all minor R versions since R 3.1. + Defaults to "last-5". + """ + ) + args = parser.parse_args() + versions = _get_versions(which=args.versions) + print(json.dumps(versions)) + + +def _get_versions(which='all'): + supported_versions = sorted(_get_supported_versions(), reverse=True) + + last_n_versions = None + if which.startswith('last-'): + last_n_versions = int(which.replace('last-', '')) + elif which != 'all': + versions = which.split(',') + versions = [v for v in versions if v in supported_versions] + return versions + + versions = {} + for ver in supported_versions: + # Skip unreleased versions (e.g., devel, next) + if not re.match(r'[\d.]', ver): + continue + if ver < MIN_ALL_VERSION: + continue + minor_ver = tuple(ver.split('.')[0:2]) + if minor_ver not in versions: + versions[minor_ver] = ver + versions = sorted(list(versions.values()), reverse=True) + + if last_n_versions: + return versions[0:last_n_versions] + + return versions + + +def _get_supported_versions(): + request = urllib.request.Request(VERSIONS_URL) + response = urllib.request.urlopen(request) + data = response.read() + result = json.loads(data) + return result['r_versions'] + + +if __name__ == '__main__': + main()