diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
new file mode 100644
index 0000000000..44a88e11d0
--- /dev/null
+++ b/.github/workflows/core.yml
@@ -0,0 +1,537 @@
+# Main Firedrake CI workflow
+
+on:
+ workflow_call:
+ inputs:
+ target_branch:
+ description: The target branch (usually 'master' or 'release')
+ type: string
+ required: true
+ run_tests:
+ description: Whether to run the test suite
+ type: boolean
+ default: true
+ test_macos:
+ description: Whether to test using macOS
+ type: boolean
+ default: false
+ deploy_website:
+ description: Whether to deploy the website
+ type: boolean
+ default: false
+ upload_pypi:
+ description: Whether to upload an sdist to PyPI
+ type: boolean
+ default: false
+ upload_testpypi:
+ description: Whether to upload an sdist to TestPyPI
+ type: boolean
+ default: false
+
+ workflow_dispatch:
+ inputs:
+ target_branch:
+ description: The target branch (usually 'master' or 'release')
+ type: string
+ required: true
+ run_tests:
+ description: Whether to run the test suite
+ type: boolean
+ default: true
+ test_macos:
+ description: Whether to test using macOS
+ type: boolean
+ default: false
+ deploy_website:
+ description: Whether to deploy the website
+ type: boolean
+ default: false
+ upload_pypi:
+ description: Whether to upload an sdist to PyPI
+ type: boolean
+ default: false
+ upload_testpypi:
+ description: Whether to upload an sdist to TestPyPI
+ type: boolean
+ default: false
+
+concurrency:
+ # Cancel running jobs if new commits are pushed
+ group: >
+ ${{ github.workflow }}-
+ ${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ test_linux:
+ name: Build and test Firedrake (Linux)
+ strategy:
+ # We want to know all of the tests which fail, so don't kill real if
+ # complex fails and vice-versa
+ fail-fast: false
+ matrix:
+ arch: [default, complex]
+ runs-on: [self-hosted, Linux]
+ container:
+ image: ubuntu:latest
+ env:
+ OMPI_ALLOW_RUN_AS_ROOT: 1
+ OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
+ OMP_NUM_THREADS: 1
+ OPENBLAS_NUM_THREADS: 1
+ FIREDRAKE_CI: 1
+ PYOP2_CI_TESTS: 1
+ PYOP2_SPMD_STRICT: 1
+ EXTRA_PYTEST_ARGS: --splitting-algorithm least_duration --timeout=600 --timeout-method=thread -o faulthandler_timeout=660
+ PYTEST_MPI_MAX_NPROCS: 8
+ steps:
+ - name: Fix HOME
+ # For unknown reasons GitHub actions overwrite HOME to /github/home
+ # which will break everything unless fixed
+ # (https://github.com/actions/runner/issues/863)
+ run: echo "HOME=/root" >> "$GITHUB_ENV"
+
+ - name: Pre-run cleanup
+ # Make sure the current directory is empty
+ run: find . -delete
+
+ - uses: actions/checkout@v4
+ with:
+ path: firedrake-repo
+
+ - name: Install system dependencies
+ run: |
+ apt-get update
+ apt-get -y install python3 python3-venv
+ apt-get -y install \
+ $(python3 ./firedrake-repo/scripts/firedrake-configure --arch ${{ matrix.arch }} --show-system-packages)
+ : # Dependencies needed to run the test suite
+ apt-get -y install parallel
+ : # Dependencies needed to build the documentation
+ apt-get -y install inkscape texlive-full
+
+ # Raise an error if any 'TODO RELEASE' comments remain
+ - name: Check no 'TODO RELEASE' comments (release only)
+ if: inputs.target_branch == 'release'
+ run: |
+ cd firedrake-repo
+ if [ -z "$( git grep 'TODO RELEASE' )" ]; then
+ exit 0
+ else
+ exit 1
+ fi
+
+ # TODO: is there a 'blessed commit' or do we just point to 'main'?
+ # I prefer the latter because then failures are everyone's problem, not just mine!
+ - name: Install PETSc
+ run: |
+ if [ ${{ inputs.target_branch }} = 'release' ]; then
+ git clone --depth 1 \
+ --branch $(python3 ./firedrake-repo/scripts/firedrake-configure --show-petsc-version) \
+ https://gitlab.com/petsc/petsc.git
+ else
+ git clone --depth 1 https://gitlab.com/petsc/petsc.git
+ fi
+ cd petsc
+ python3 ../firedrake-repo/scripts/firedrake-configure \
+ --arch ${{ matrix.arch }} --show-petsc-configure-options | \
+ xargs -L1 ./configure --with-make-np=8 --download-slepc
+ make PETSC_DIR=/__w/firedrake/firedrake/petsc PETSC_ARCH=arch-firedrake-${{ matrix.arch }}
+ make check
+ {
+ echo "PETSC_DIR=/__w/firedrake/firedrake/petsc"
+ echo "PETSC_ARCH=arch-firedrake-${{ matrix.arch }}"
+ echo "SLEPC_DIR=/__w/firedrake/firedrake/petsc/arch-firedrake-${{ matrix.arch }}"
+ } >> "$GITHUB_ENV"
+
+ - name: Install Firedrake
+ id: install
+ run: |
+ export $(python3 ./firedrake-repo/scripts/firedrake-configure --arch "${{ matrix.arch }}" --show-env)
+ python3 -m venv venv
+ . venv/bin/activate
+
+ : # Force a rebuild of petsc4py as the cached one will not link to the fresh
+ : # install of PETSc.
+ pip cache remove petsc4py
+ pip cache remove slepc4py
+
+ if [ ${{ inputs.target_branch }} = 'release' ]; then
+ EXTRA_PIP_FLAGS=''
+ else
+ : # Install build dependencies
+ pip install "$PETSC_DIR"/src/binding/petsc4py
+ pip install -r ./firedrake-repo/requirements-build.txt
+
+ : # Install runtime dependencies that have been removed from the pyproject.toml
+ : # because they rely on non-PyPI versions of petsc4py.
+ pip install --no-build-isolation --no-deps \
+ "$PETSC_DIR"/"$PETSC_ARCH"/externalpackages/git.slepc/src/binding/slepc4py
+ pip install --no-deps ngsPETSc netgen-mesher netgen-occt
+
+ : # We have to pass '--no-build-isolation' to use a custom petsc4py
+ EXTRA_PIP_FLAGS='--no-build-isolation'
+ fi
+
+ pip install --verbose $EXTRA_PIP_FLAGS \
+ --no-binary h5py \
+ --extra-index-url https://download.pytorch.org/whl/cpu \
+ './firedrake-repo[ci,docs]'
+
+ firedrake-clean
+ pip list
+
+ - name: Run firedrake-check
+ run: |
+ . venv/bin/activate
+ firedrake-check
+ timeout-minutes: 5
+
+ - name: Run TSFC tests
+ # Run even if earlier tests failed
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ : # Use pytest-xdist here so we can have a single collated output (not possible
+ : # for parallel tests)
+ firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/tsfc
+ timeout-minutes: 60
+
+ - name: Run PyOP2 tests
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ : # Use pytest-xdist here so we can have a single collated output (not possible
+ : # for parallel tests)
+ firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
+ firedrake-run-split-tests 2 4 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
+ firedrake-run-split-tests 3 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
+ firedrake-run-split-tests 4 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
+ timeout-minutes: 15
+
+
+ - name: Run Firedrake tests (nprocs = 1)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ : # Use pytest-xdist here so we can have a single collated output (not possible
+ : # for parallel tests)
+ firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 60
+
+ - name: Run tests (nprocs = 2)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 2 4 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 30
+
+ - name: Run tests (nprocs = 3)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 3 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 60
+
+ - name: Run tests (nprocs = 4)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 4 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 15
+
+ - name: Run tests (nprocs = 5)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 5 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 15
+
+ - name: Run tests (nprocs = 6)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 6 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 15
+
+ - name: Run tests (nprocs = 7)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 7 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 15
+
+ - name: Run tests (nprocs = 8)
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ firedrake-run-split-tests 8 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
+ timeout-minutes: 15
+
+ - name: Run Gusto smoke tests
+ # Only test Gusto in real mode
+ if: |
+ inputs.run_tests &&
+ (success() || steps.install.conclusion == 'success') &&
+ matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ git clone --depth 1 https://github.com/firedrakeproject/gusto.git gusto-repo
+ pip install --verbose ./gusto-repo
+ python -m pytest -n 8 --verbose \
+ gusto-repo/integration-tests/balance/test_saturated_balance.py \
+ gusto-repo/integration-tests/equations/test_thermal_sw.py \
+ gusto-repo/integration-tests/transport/test_embedded_dg_advection.py
+ timeout-minutes: 10
+
+ - name: Run Thetis smoke tests
+ if: |
+ inputs.run_tests &&
+ (success() || steps.install.conclusion == 'success') &&
+ matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ git clone --depth 1 https://github.com/thetisproject/thetis.git thetis-repo
+ pip install --verbose ./thetis-repo
+ python -m pytest -n 8 --verbose thetis-repo/test_adjoint/test_swe_adjoint.py
+ timeout-minutes: 10
+
+ - name: Run spyro smoke tests
+ if: |
+ inputs.run_tests &&
+ (success() || steps.install.conclusion == 'success') &&
+ matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ git clone --depth 1 https://github.com/NDF-Poli-USP/spyro.git spyro-repo
+ pip install --verbose ./spyro-repo
+ mpiexec -n 6 python -m pytest spyro-repo/test_integration/ -m parallel[6]
+ timeout-minutes: 5
+
+ - name: Run G-ADOPT smoke tests
+ if: |
+ inputs.run_tests &&
+ (success() || steps.install.conclusion == 'success') &&
+ matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ git clone --depth 1 https://github.com/g-adopt/g-adopt.git g-adopt-repo
+ pip install --verbose ./g-adopt-repo
+ make -C g-adopt-repo/demos/mantle_convection/base_case check
+ timeout-minutes: 5
+
+ - name: Upload log files
+ uses: actions/upload-artifact@v4
+ if: inputs.run_tests && (success() || steps.install.conclusion == 'success')
+ with:
+ name: firedrake-logs-${{ matrix.arch }}
+ path: pytest_*.log
+
+ - name: Build sdist (release only)
+ if: |
+ inputs.target_branch == 'release' &&
+ matrix.arch == 'default' &&
+ (success() || steps.install.conclusion == 'success')
+ run: |
+ . venv/bin/activate
+ pip install build
+ python -m build ./firedrake-repo --sdist
+
+ - name: Upload sdist (release only)
+ if: |
+ inputs.target_branch == 'release' &&
+ matrix.arch == 'default' &&
+ (success() || steps.install.conclusion == 'success')
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: firedrake-repo/dist/*
+
+ - name: Check bibtex
+ if: always() && matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ make -C firedrake-repo/docs validate-bibtex
+
+ - name: Check documentation links
+ if: always() && matrix.arch == 'default'
+ run: |
+ . venv/bin/activate
+ make -C firedrake-repo/docs linkcheck
+
+ - name: Build documentation
+ if: (success() || steps.install.conclusion == 'success') && matrix.arch == 'default'
+ id: build_docs
+ run: |
+ . venv/bin/activate
+ cd firedrake-repo/docs
+ make SPHINXOPTS="-t ${{ inputs.target_branch }}" html
+ make latex
+ make latexpdf
+ # : Copy manual to HTML tree
+ cp build/latex/Firedrake.pdf build/html/_static/manual.pdf
+
+ - name: Upload documentation
+ uses: actions/upload-pages-artifact@v3
+ if: (success() || steps.build_docs.conclusion == 'success') && matrix.arch == 'default'
+ with:
+ name: github-pages
+ path: firedrake-repo/docs/build/html
+ retention-days: 1
+
+ - name: Post-run cleanup
+ if: always()
+ run: find . -delete
+
+ test_macos:
+ name: Build and test Firedrake (macOS)
+ runs-on: [self-hosted, macOS]
+ if: inputs.test_macos
+ env:
+ FIREDRAKE_CI: 1
+ OMP_NUM_THREADS: 1
+ OPENBLAS_NUM_THREADS: 1
+ steps:
+ - name: Add homebrew to PATH
+ # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#adding-a-system-path
+ run: echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
+
+ - name: Pre-run cleanup
+ # Make sure the current directory is empty
+ run: find . -delete
+
+ - uses: actions/checkout@v4
+ with:
+ path: firedrake-repo
+
+ - name: Install system dependencies
+ run: |
+ brew install $(python3 ./firedrake-repo/scripts/firedrake-configure --arch default --show-system-packages)
+
+ - name: Install PETSc
+ run: |
+ if [ ${{ inputs.target_branch }} = 'release' ]; then
+ git clone --depth 1 \
+ --branch $(python3 ./firedrake-repo/scripts/firedrake-configure --show-petsc-version) \
+ https://gitlab.com/petsc/petsc.git
+ else
+ git clone --depth 1 https://gitlab.com/petsc/petsc.git
+ fi
+ cd petsc
+ python3 ../firedrake-repo/scripts/firedrake-configure \
+ --arch default --show-petsc-configure-options | \
+ xargs -L1 ./configure --with-make-np=4
+ make
+ make check
+ {
+ echo "PETSC_DIR=/Users/github/actions-runner/_work/firedrake/firedrake/petsc"
+ echo "PETSC_ARCH=arch-firedrake-default"
+ echo "SLEPC_DIR=/Users/github/actions-runner/_work/firedrake/firedrake/petsc/arch-firedrake-default"
+ } >> "$GITHUB_ENV"
+
+ - name: Install Firedrake
+ id: install
+ run: |
+ export $(python3 ./firedrake-repo/scripts/firedrake-configure --arch default --show-env)
+ python3 -m venv venv
+ . venv/bin/activate
+
+ : # Force a rebuild of petsc4py as the cached one will not link to the fresh
+ : # install of PETSc. A similar trick may be needed for compiled dependencies
+ : # like h5py or mpi4py if changing HDF5/MPI libraries.
+ pip cache remove petsc4py
+
+ if [ ${{ inputs.target_branch }} = 'release' ]; then
+ EXTRA_PIP_FLAGS=''
+ else
+ : # Install build dependencies
+ pip install "$PETSC_DIR"/src/binding/petsc4py
+ pip install -r ./firedrake-repo/requirements-build.txt
+
+ : # We have to pass '--no-build-isolation' to use a custom petsc4py
+ EXTRA_PIP_FLAGS='--no-build-isolation'
+ fi
+
+ pip install --verbose $EXTRA_PIP_FLAGS \
+ --no-binary h5py \
+ './firedrake-repo[check]'
+
+ firedrake-clean
+ pip list
+
+ - name: Run smoke tests
+ run: |
+ . venv/bin/activate
+ firedrake-check
+ timeout-minutes: 10
+
+ - name: Post-run cleanup
+ if: always()
+ run: |
+ find . -delete
+
+ lint:
+ name: Lint codebase
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+ - name: Setup flake8 annotations
+ uses: rbialon/flake8-annotations@v1
+ - name: Install linting packages
+ run: pip install flake8 pylint
+ - name: Lint codebase
+ run: make lint GITHUB_ACTIONS_FORMATTING=1
+
+ deploy:
+ name: Deploy GitHub pages
+ needs: test_linux
+ if: inputs.deploy_website
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: http://firedrakeproject.github.io/firedrake/${{ inputs.target_branch }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Deploy to GitHub Pages
+ uses: actions/deploy-pages@v4
+
+ upload_pypi:
+ name: Upload to PyPI (optional)
+ if: inputs.upload_pypi && inputs.target_branch == 'release'
+ needs: test_linux
+ runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist
+ - name: Push to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+
+ upload_testpypi:
+ name: Upload to TestPyPI (optional)
+ if: inputs.upload_testpypi && inputs.target_branch == 'release'
+ needs: test_linux
+ runs-on: ubuntu-latest
+ environment:
+ name: testpypi
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist
+ - name: Push to TestPyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 45e74f56b9..b26812f672 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -4,13 +4,7 @@ on:
workflow_call:
inputs:
tag:
- description: 'Optional tag (defaults to `latest`)'
- required: false
- default: 'latest'
- type: string
- status:
- description: 'Firedrake test status'
- required: true
+ description: Docker image tag
type: string
secrets:
# Docker login information
@@ -22,8 +16,6 @@ on:
jobs:
# Firedrake container (just Firedrake)
docker_build_vanilla:
- # Only run if Firedrake tests pass
- if: inputs.status == 'success'
strategy:
fail-fast: false
matrix:
@@ -58,8 +50,6 @@ jobs:
# # Firedrake container (Firedrake and friends)
docker_build_firedrake:
- # Only run if "Build Firedrake" succeeds
- if: inputs.status == 'success'
needs: docker_merge_vanilla
uses: ./.github/workflows/docker_build.yml
# Only build the 'firedrake' container for 'linux/amd64' because
@@ -84,8 +74,6 @@ jobs:
# # Firedrake container with TeX
docker_build_docdeps:
- # Only run if "Build Firedrake" succeeds
- if: inputs.status == 'success'
needs: docker_merge_vanilla
uses: ./.github/workflows/docker_build.yml
with:
@@ -106,8 +94,6 @@ jobs:
# # Firedrake container with Jupyter notebooks
docker_build_jupyter:
- # Only run if "Build Firedrake" succeeds
- if: inputs.status == 'success'
needs: docker_merge_firedrake
uses: ./.github/workflows/docker_build.yml
with:
diff --git a/.github/workflows/docker_merge.yml b/.github/workflows/docker_merge.yml
index c0b3f2c3a0..b7221fb10c 100644
--- a/.github/workflows/docker_merge.yml
+++ b/.github/workflows/docker_merge.yml
@@ -7,13 +7,12 @@ on:
workflow_call:
inputs:
target:
- description: 'Target docker image name to upload to'
+ description: Docker image name
required: true
type: string
tag:
- description: 'Optional tag (defaults to `latest`)'
- required: false
- default: 'latest'
+ description: Docker image tag
+ required: true
type: string
secrets:
# Docker login information
@@ -28,8 +27,7 @@ jobs:
steps:
- name: Pre-cleanup
if: always()
- run: |
- rm -rf ${{ runner.temp }}/digests
+ run: rm -rf ${{ runner.temp }}/digests
- name: Download digests
uses: actions/download-artifact@v4
@@ -47,10 +45,15 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ # NOTE: This action pushes a new image with the given tag but also updates
+ # the 'latest' tag.
- name: Merge and push the per-platform images
working-directory: ${{ runner.temp }}/digests
run: |
- docker buildx imagetools create -t firedrakeproject/${{ inputs.target }}:${{ inputs.tag }} $(printf 'firedrakeproject/${{ inputs.target }}@sha256:%s ' *)
+ docker buildx imagetools create \
+ -t firedrakeproject/${{ inputs.target }}:${{ inputs.tag }} \
+ -t firedrakeproject/${{ inputs.target }}:latest \
+ $(printf 'firedrakeproject/${{ inputs.target }}@sha256:%s ' *)
- name: Inspect image
run: |
@@ -58,5 +61,4 @@ jobs:
- name: Post-cleanup
if: always()
- run: |
- rm -rf ${{ runner.temp }}/digests
+ run: rm -rf ${{ runner.temp }}/digests
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
new file mode 100644
index 0000000000..eea463bacc
--- /dev/null
+++ b/.github/workflows/pr.yml
@@ -0,0 +1,13 @@
+name: Test pull request
+
+on:
+ pull_request:
+
+jobs:
+ test:
+ uses: ./.github/workflows/core.yml
+ with:
+ target_branch: ${{ github.base_ref }}
+ # Only run macOS tests if the PR is labelled 'macOS'
+ test_macos: ${{ contains(github.event.pull_request.labels.*.name, 'macOS') }}
+ secrets: inherit
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
new file mode 100644
index 0000000000..06b0e316fc
--- /dev/null
+++ b/.github/workflows/push.yml
@@ -0,0 +1,16 @@
+name: Deploy after push
+
+on:
+ push:
+ branches:
+ - master
+ - release
+
+jobs:
+ test:
+ uses: ./.github/workflows/core.yml
+ with:
+ target_branch: ${{ github.ref_name }}
+ test_macos: true
+ deploy_website: true
+ secrets: inherit
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000000..4fa26d2c40
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,23 @@
+name: Publish release
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ deploy:
+ if: ! startsWith( github.ref_name, 'Zenodo' )
+ uses: ./.github/workflows/core.yml
+ with:
+ target_branch: release
+ run_tests: false
+ upload_pypi: true
+ secrets: inherit
+
+ docker:
+ if: ! startsWith( github.ref_name, 'Zenodo' )
+ name: Build Docker containers
+ uses: ./.github/workflows/docker.yml
+ with:
+ tag: ${{ github.ref }}
+ secrets: inherit
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 02a258e593..0000000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,376 +0,0 @@
-name: CI
-
-on:
- push:
- branches:
- - master
- pull_request:
- schedule:
- - cron: '0 0 * * 0'
- - cron: '0 0 1 * *' # Monthly release
-
-concurrency:
- # Cancel running jobs if new commits are pushed
- group: >
- ${{ github.workflow }}-
- ${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-env:
- RELEASE_TAG: latest
-
-jobs:
- test:
- name: Install and test Firedrake (Linux)
- strategy:
- # We want to know all of the tests which fail, so don't kill real if
- # complex fails and vice-versa
- fail-fast: false
- matrix:
- arch: [default, complex]
- runs-on: [self-hosted, Linux]
- container:
- image: ubuntu:latest
- env:
- OMPI_ALLOW_RUN_AS_ROOT: 1
- OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1
- OMP_NUM_THREADS: 1
- OPENBLAS_NUM_THREADS: 1
- FIREDRAKE_CI: 1
- PYOP2_CI_TESTS: 1
- PYOP2_SPMD_STRICT: 1
- EXTRA_PYTEST_ARGS: --splitting-algorithm least_duration --timeout=600 --timeout-method=thread -o faulthandler_timeout=660
- PYTEST_MPI_MAX_NPROCS: 8
- steps:
- - name: Fix HOME
- # For unknown reasons GitHub actions overwrite HOME to /github/home
- # which will break everything unless fixed
- # (https://github.com/actions/runner/issues/863)
- run: echo "HOME=/root" >> "$GITHUB_ENV"
-
- - name: Pre-run cleanup
- # Make sure the current directory is empty
- run: find . -delete
-
- - uses: actions/checkout@v4
- with:
- path: firedrake-repo
-
- - name: Install system dependencies
- run: |
- apt-get update
- apt-get -y install python3
- apt-get -y install \
- $(python3 ./firedrake-repo/scripts/firedrake-configure --arch ${{ matrix.arch }} --show-system-packages) python3-venv parallel
-
- - name: Install PETSc
- run: |
- git clone --depth 1 --branch $(python3 ./firedrake-repo/scripts/firedrake-configure --show-petsc-version) https://gitlab.com/petsc/petsc.git
- cd petsc
- python3 ../firedrake-repo/scripts/firedrake-configure \
- --arch ${{ matrix.arch }} --show-petsc-configure-options | \
- xargs -L1 ./configure --with-make-np=8 --download-slepc
- make PETSC_DIR=/__w/firedrake/firedrake/petsc PETSC_ARCH=arch-firedrake-${{ matrix.arch }}
- make check
- {
- echo "PETSC_DIR=/__w/firedrake/firedrake/petsc"
- echo "PETSC_ARCH=arch-firedrake-${{ matrix.arch }}"
- echo "SLEPC_DIR=/__w/firedrake/firedrake/petsc/arch-firedrake-${{ matrix.arch }}"
- } >> "$GITHUB_ENV"
-
- - name: Install Firedrake
- id: install
- run: |
- export $(python3 ./firedrake-repo/scripts/firedrake-configure --arch ${{ matrix.arch }} --show-env)
- python3 -m venv venv
- . venv/bin/activate
- : # Force a rebuild of petsc4py as the cached one will not link to the fresh
- : # install of PETSc.
- pip cache remove petsc4py
- pip cache remove slepc4py
- pip install --verbose \
- --no-binary h5py \
- --extra-index-url https://download.pytorch.org/whl/cpu \
- './firedrake-repo[ci]'
- firedrake-clean
- pip list
-
- - name: Run firedrake-check
- run: |
- . venv/bin/activate
- firedrake-check
- timeout-minutes: 5
-
- - name: Run TSFC tests
- # Run even if earlier tests failed
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- : # Use pytest-xdist here so we can have a single collated output (not possible
- : # for parallel tests)
- firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/tsfc
- timeout-minutes: 60
-
- - name: Run PyOP2 tests
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- : # Use pytest-xdist here so we can have a single collated output (not possible
- : # for parallel tests)
- firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
- firedrake-run-split-tests 2 4 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
- firedrake-run-split-tests 3 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
- firedrake-run-split-tests 4 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/pyop2
- timeout-minutes: 15
-
-
- - name: Run Firedrake tests (nprocs = 1)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- : # Use pytest-xdist here so we can have a single collated output (not possible
- : # for parallel tests)
- firedrake-run-split-tests 1 1 -n 8 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 60
-
- - name: Run tests (nprocs = 2)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 2 4 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 30
-
- - name: Run tests (nprocs = 3)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 3 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 60
-
- - name: Run tests (nprocs = 4)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 4 2 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 15
-
- - name: Run tests (nprocs = 5)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 5 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 15
-
- - name: Run tests (nprocs = 6)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 6 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 15
-
- - name: Run tests (nprocs = 7)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 7 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 15
-
- - name: Run tests (nprocs = 8)
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- firedrake-run-split-tests 8 1 "$EXTRA_PYTEST_ARGS" firedrake-repo/tests/firedrake
- timeout-minutes: 15
-
- - name: Run Gusto smoke tests
- # Only test Gusto in real mode
- if: (success() || steps.install.conclusion == 'success') && matrix.arch == 'default'
- run: |
- . venv/bin/activate
- git clone --depth 1 https://github.com/firedrakeproject/gusto.git gusto-repo
- pip install --verbose ./gusto-repo
- python -m pytest -n 8 --verbose \
- gusto-repo/integration-tests/balance/test_saturated_balance.py \
- gusto-repo/integration-tests/equations/test_thermal_sw.py \
- gusto-repo/integration-tests/transport/test_embedded_dg_advection.py
- timeout-minutes: 10
-
- - name: Run Thetis smoke tests
- if: (success() || steps.install.conclusion == 'success') && matrix.arch == 'default'
- run: |
- . venv/bin/activate
- git clone --depth 1 https://github.com/thetisproject/thetis.git thetis-repo
- pip install --verbose ./thetis-repo
- python -m pytest -n 8 --verbose thetis-repo/test_adjoint/test_swe_adjoint.py
- timeout-minutes: 10
-
- - name: Run spyro smoke tests
- if: (success() || steps.install.conclusion == 'success') && matrix.arch == 'default'
- run: |
- . venv/bin/activate
- git clone --depth 1 https://github.com/NDF-Poli-USP/spyro.git spyro-repo
- pip install --verbose ./spyro-repo
- mpiexec -n 6 python -m pytest spyro-repo/test_integration/ -m parallel[6]
- timeout-minutes: 5
-
- - name: Run G-ADOPT smoke tests
- if: (success() || steps.install.conclusion == 'success') && matrix.arch == 'default'
- run: |
- . venv/bin/activate
- git clone --depth 1 https://github.com/g-adopt/g-adopt.git g-adopt-repo
- pip install --verbose ./g-adopt-repo
- make -C g-adopt-repo/demos/mantle_convection/base_case check
- timeout-minutes: 5
-
- - name: Upload log files
- uses: actions/upload-artifact@v4
- if: success() || steps.install.conclusion == 'success'
- with:
- name: firedrake-logs-${{ matrix.arch }}
- path: pytest_*.log
-
- - name: Post-run cleanup
- if: always()
- run: find . -delete
-
- docker_tag:
- name: "Set the Docker release tag"
- runs-on: [self-hosted, Linux]
- if: github.ref == 'refs/heads/master'
- steps:
- - name: Set release tag
- # Set a release tag if triggered by monthly CRON job
- if: github.event.schedule == '0 0 1 * *'
- run: |
- DATE_TAG="$(date +%Y-%m)"
- echo "RELEASE_TAG=$DATE_TAG" >> "$GITHUB_ENV"
- - name: Print release tag being used
- run: |
- echo The release tag is "$RELEASE_TAG"
- outputs:
- tag: ${{ env.RELEASE_TAG }}
-
- docker:
- name: Build Docker containers
- if: github.ref == 'refs/heads/master'
- needs: [test, docker_tag]
- uses: ./.github/workflows/docker.yml
- with:
- tag: ${{ needs.docker_tag.outputs.tag }}
- status: ${{ needs.test.result }}
- secrets: inherit
-
- lint:
- name: Lint codebase
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: '3.10'
- - name: Setup flake8 annotations
- uses: rbialon/flake8-annotations@v1
- - name: Install linting packages
- run: pip install flake8 pylint
- - name: Lint codebase
- run: make lint GITHUB_ACTIONS_FORMATTING=1
-
- zenodo_canary:
- name: Run zenodo canary
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 1
- - name: Setup Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.12"
- - name: Install deps
- run: pip install requests packaging
- - name: Zenodo API canary
- run: python scripts/firedrake-install --test-doi-resolution
- - name: Upload log
- uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: zenodo-canary
- path: firedrake-install.log
-
- build_docs:
- name: Build documentation
- runs-on: ubuntu-latest
- container:
- image: firedrakeproject/firedrake-docdeps:latest
- outputs:
- conclusion: ${{ steps.report.outputs.conclusion }}
- steps:
- - uses: actions/checkout@v4
- with:
- path: firedrake-repo
-
- - name: Install Firedrake
- id: install
- run: |
- : # Pass '--system-site-packages' so already installed packages can be found
- python3 -m venv --system-site-packages venv
- . venv/bin/activate
- pip uninstall -y firedrake
- pip install --verbose './firedrake-repo[docs]'
-
- - name: Check bibtex
- run: |
- . venv/bin/activate
- make -C firedrake-repo/docs validate-bibtex
-
- - name: Check documentation links
- run: |
- . venv/bin/activate
- make -C firedrake-repo/docs linkcheck
-
- - name: Build docs
- id: build
- if: success() || steps.install.conclusion == 'success'
- run: |
- . venv/bin/activate
- cd firedrake-repo/docs
- make html
- make latex
- make latexpdf
-
- - name: Copy manual to HTML tree
- id: copy
- if: success() || steps.build.conclusion == 'success'
- run: |
- cd firedrake-repo/docs
- cp build/latex/Firedrake.pdf build/html/_static/manual.pdf
-
- - name: Upload artifact
- id: upload
- if: success() || steps.copy.conclusion == 'success'
- uses: actions/upload-pages-artifact@v3
- with:
- name: github-pages
- path: /__w/firedrake/firedrake/firedrake-repo/docs/build/html
- retention-days: 1
-
- - name: Report status
- id: report
- if: success() || steps.upload.conclusion == 'success'
- run: echo "conclusion=success" >> "$GITHUB_OUTPUT"
-
- deploy:
- name: Deploy Github pages
- needs: build_docs
- # Always run this workflow on master, even if linkcheck fails
- if: always() && github.ref == 'refs/heads/master' && needs.build_docs.outputs.conclusion == 'success'
- permissions:
- pages: write
- id-token: write
- environment:
- name: github-pages
- url: http://firedrakeproject.github.io/firedrake
- runs-on: ubuntu-latest
- steps:
- - name: Deploy to GitHub Pages
- uses: actions/deploy-pages@v4
diff --git a/.github/workflows/test_macos.yml b/.github/workflows/test_macos.yml
deleted file mode 100644
index dd987e4a80..0000000000
--- a/.github/workflows/test_macos.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-name: Install and test Firedrake (macOS)
-
-on:
- push:
- branches:
- - master
- pull_request:
- # By default this workflow is run on the "opened", "synchronize" and
- # "reopened" events. We add "labelled" so it will run if the PR is given a label.
- types: [opened, synchronize, reopened, labeled]
-
-concurrency:
- # Cancels jobs running if new commits are pushed
- group: >
- ${{ github.workflow }}-
- ${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- build:
- name: Build Firedrake (macOS)
- runs-on: [self-hosted, macOS]
- # Only run this action if we are pushing to master or the PR is labelled "macOS"
- if: ${{ (github.ref == 'refs/heads/master') || contains(github.event.pull_request.labels.*.name, 'macOS') }}
- env:
- FIREDRAKE_CI: 1
- OMP_NUM_THREADS: 1
- OPENBLAS_NUM_THREADS: 1
- steps:
- - name: Add homebrew to PATH
- # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#adding-a-system-path
- run: echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
-
- - name: Pre-run cleanup
- # Make sure the current directory is empty
- run: find . -delete
-
- - uses: actions/checkout@v4
- with:
- path: firedrake-repo
-
- - name: Install system dependencies
- run: |
- brew install $(python3 ./firedrake-repo/scripts/firedrake-configure --arch default --show-system-packages)
-
- - name: Install PETSc
- run: |
- git clone --depth 1 --branch $(python3 ./firedrake-repo/scripts/firedrake-configure --show-petsc-version) https://gitlab.com/petsc/petsc.git
- cd petsc
- python3 ../firedrake-repo/scripts/firedrake-configure \
- --arch default --show-petsc-configure-options | \
- xargs -L1 ./configure --with-make-np=4
- make
- make check
-
- - name: Install Firedrake
- id: install
- run: |
- export $(python3 ./firedrake-repo/scripts/firedrake-configure --arch default --show-env)
- python3 -m venv venv
- . venv/bin/activate
- : # Force a rebuild of petsc4py as the cached one will not link to the fresh
- : # install of PETSc. A similar trick may be needed for compiled dependencies
- : # like h5py or mpi4py if changing HDF5/MPI libraries.
- pip cache remove petsc4py
- pip install --verbose --no-binary h5py './firedrake-repo[test]'
- firedrake-clean
- : # Extra test dependencies
- pip install pytest-timeout
- pip list
-
- - name: Run smoke tests
- run: |
- . venv/bin/activate
- firedrake-check
- timeout-minutes: 10
-
- - name: Post-run cleanup
- if: always()
- run: |
- find . -delete
diff --git a/Makefile b/Makefile
index 2d5007d4d4..ef491f3efe 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@ actionlint:
@docker pull rhysd/actionlint:latest
@# Exclude SC2046 so it doesn't complain about unquoted $ characters (the
@# quoting can prevent proper parsing)
- @docker run -e SHELLCHECK_OPTS='--exclude=SC2046' --rm -v $$(pwd):/repo --workdir /repo rhysd/actionlint -color
+ @docker run -e SHELLCHECK_OPTS='--exclude=SC2046,SC2078' --rm -v $$(pwd):/repo --workdir /repo rhysd/actionlint -color
.PHONY: dockerlint
dockerlint:
diff --git a/docker/Dockerfile.docdeps b/docker/Dockerfile.docdeps
deleted file mode 100644
index 832b422b5f..0000000000
--- a/docker/Dockerfile.docdeps
+++ /dev/null
@@ -1,7 +0,0 @@
-# Dockerfile for Firedrake plus packages needed to build the documentation
-
-FROM firedrakeproject/firedrake-vanilla-default:latest
-
-RUN apt-get update \
- && apt-get -y install inkscape texlive-full \
- && rm -rf /var/lib/apt/lists/*
diff --git a/docker/Dockerfile.vanilla b/docker/Dockerfile.vanilla
index 09d30b6820..3dce406f03 100644
--- a/docker/Dockerfile.vanilla
+++ b/docker/Dockerfile.vanilla
@@ -26,7 +26,7 @@ ENV PIP_BREAK_SYSTEM_PACKAGES=1
ENV OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1
# Download firedrake-configure
-RUN curl -O --output-dir /opt https://raw.githubusercontent.com/firedrakeproject/firedrake/master/scripts/firedrake-configure
+RUN curl -O --output-dir /opt https://raw.githubusercontent.com/firedrakeproject/firedrake/release/scripts/firedrake-configure
# Install system dependencies
RUN apt-get update \
@@ -63,4 +63,4 @@ ENV MPICC=$CC
# Install Firedrake
RUN pip install --verbose --no-binary h5py --src /opt \
- --editable git+https://github.com/firedrakeproject/firedrake.git#egg=firedrake[docker]
+ --editable git+https://github.com/firedrakeproject/firedrake.git@release#egg=firedrake[docker]
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 29f5e979a4..695b014a64 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -244,6 +244,13 @@
# Output file base name for HTML help builder.
htmlhelp_basename = 'Firedrakedoc'
+# Optional header warning about docs version
+rst_prolog = """
+.. only:: main
+
+ .. warning::
+ You are reading a version of the website built against the unstable ``main`` branch. This content is liable to change without notice and may be inappropriate for your use case.
+"""
# -- Options for LaTeX output --------------------------------------------
diff --git a/docs/source/documentation.rst b/docs/source/documentation.rst
index 2da93622b7..1a90527867 100644
--- a/docs/source/documentation.rst
+++ b/docs/source/documentation.rst
@@ -6,21 +6,17 @@
.. sidebar:: Current development information.
- Firedrake and PyOP2 are continually tested using `GitHub actions
-You can install Firedrake using exactly this set of component versions using: -
firedrake-install --doi {doi}- -
-See firedrakeproject.org/install.html for more information. -
- """.format(title=title, links=links, doi=doi) + """.format(title=title, links=links) if additional_dois: data = """{data} @@ -458,10 +415,7 @@ See firedrakeproject.org-You will have to download and install them separately in your Firedrake installation, since we cannot automate it for you. -
""".format(data=data, additional_links=additional_links) +""".format(data=data, additional_links=additional_links) return data @@ -526,16 +480,12 @@ def create_metarecord(tag, title, components, info_file=None, matching_records = match(all_records, possible_tags) if len(matching_records) != len(possible_tags): missing = set(repo for repo, _ in possible_tags).difference(repo for repo, _ in matching_records) - if args.skip_missing: - log.warning("Did not find a Zenodo record for repositories: '{}'. Continuing.".format( - ", ".join(repo.full_name for repo in missing))) - else: - log.error("Did not find a Zenodo record for the following repositories") - for repo in missing: - log.error("{}".format(repo.full_name)) - log.error("") - log.error("If you want to continue anyway use --skip-missing") - sys.exit(1) + log.error("Did not find a Zenodo record for the following repositories") + for repo in missing: + log.error("{}".format(repo.full_name)) + log.error("") + sys.exit(1) + base_url = f"{ZENODO_URL}/deposit/depositions" if os.getenv("FIREDRAKE_ZENODO_TOKEN"): authentication_params = {"access_token": os.getenv("FIREDRAKE_ZENODO_TOKEN")} @@ -580,10 +530,10 @@ with deposit:write scope.""") metadata = { "metadata": { - "title": "Software used in `{}'".format(title), + "title": f"Software used in `{title}'", "upload_type": "software", "creators": [{"name": "firedrake-zenodo"}], - "version": "{}".format(tag), + "version": tag, "access_right": "open", "license": "cc-by", "related_identifiers": ([{"relation": "cites", "identifier": record["doi"]} @@ -599,11 +549,11 @@ with deposit:write scope.""") if depo.status_code >= 400: raise ValueError("Unable to add metadata to deposition {}".format(depo.json())) - components = create_json(matching_records, title, additional_dois=additional_dois) + components_json = create_json(matching_records, title, additional_dois=additional_dois) # This is where files live bucket_url = depo.json()["links"]["bucket"] upload = requests.put("{url}/{filename}".format(url=bucket_url, filename="components.json"), - data=components, + data=components_json, params=authentication_params) if upload.status_code >= 400: raise ValueError("Unable to upload file {}".format(upload.json())) @@ -617,7 +567,7 @@ with deposit:write scope.""") f"Actual: {actual}") checksum(upload.json()["checksum"], - hashlib.md5(components).hexdigest(), + hashlib.md5(components_json).hexdigest(), "components.json") if info_file is not None: @@ -672,7 +622,6 @@ def format_bibtex(record): def create_bibtex(tag): - check_tag(tag) response = requests.get(f"{ZENODO_URL}/records", params={"q": "creators.name:firedrake-zenodo AND version:%s" % tag}) if response.status_code >= 400: @@ -728,25 +677,30 @@ if args.list_meta_records: sys.exit(0) -if args.meta_release: - with open(args.meta_release, "r") as f: +def create_zenodo_meta_release(meta_release: str) -> None: + with open(meta_release, "r") as f: data = json.loads(f.read()) tag = data["tag"] - check_tag(tag) - title = data["title"] - components = data["components"] - info_file = decode_info_file(data["info_file"]) additional_dois = data["additional_dois"] new_version_of = data["new_version_of"] - record = create_metarecord(tag, title, components, info_file=info_file, - additional_dois=additional_dois, - update_record=new_version_of) + record = create_metarecord( + tag, + data["title"], + data["components"], + info_file=decode_info_file(data["info_file"]), + additional_dois=additional_dois, + update_record=new_version_of, + ) record = record.json() log.info("Created Zenodo meta-release.") - log.info("Tag is `{}`".format(tag)) - log.info("DOI is {}".format(record["doi"])) - log.info("Zenodo URL is {}".format(record["links"]["record_html"])) - log.info("BibTeX\n\n```bibtex\n{}\n```".format(format_bibtex(record))) + log.info(f"Tag is `{tag}`") + log.info(f"DOI is {record[doi]}") + log.info(f"Zenodo URL is {record["links"]["record_html"]}") + log.info(f"BibTeX\n\n```bibtex\n{format_bibtex(record)}\n```") + + +if args.meta_release: + create_zenodo_meta_release(args.meta_release) sys.exit(0) @@ -755,8 +709,7 @@ if args.release or not args.input_file: log.error("You must provide a title using the --title option") sys.exit(1) - # Collect hashes from the current repo. - shas = collect_repo_shas() + shas = {c: dataclasses.asdict(get_component_version(c)) for c in components} if args.info_file: shas["metarelease_info_file"] = encode_info_file(args.info_file[0]) else: @@ -783,7 +736,7 @@ if args.additional_dois: for component in components: new_sha = getattr(args, component) if new_sha: - shas[component] = new_sha[0] + shas[component] = {"commit_id": new_sha[0]} if not (args.release or args.input_file): # Dump json and exit. @@ -795,12 +748,13 @@ if not (args.release or args.input_file): try: import github3 - # Shut up the github module - github3.session.__logs__.setLevel(logging.WARNING) except ImportError: log.error("Publishing releases requires the github3 module. Please pip install github3.py") sys.exit(1) +# Shut up the github module +github3.session.__logs__.setLevel(logging.WARNING) + # Github authentication. token = os.getenv("FIREDRAKE_GITHUB_TOKEN") if token: @@ -820,85 +774,126 @@ variable FIREDRAKE_GITHUB_TOKEN to a Github personal access token with public_repo scope.""") sys.exit(1) -fd = gh.repository("firedrakeproject", "firedrake") -tag = time.strftime("Firedrake_%Y%m%d", time.localtime()) -index = -1 +def get_component_repository(component: str) -> github3.Repository: + return gh.repository("firedrakeproject", component) -for r in fd.tags(): - if r.name.startswith(tag): - newindex = int(r.name.split(".")[1]) - index = max(index, newindex) -tag += "." + str(index + 1) -# Verify commits. This ensures that an invalid sha will cause us to fail before we release any component. -# This step also replaces short shas with long ones. This seems to be necessary for release creation. +def generate_unique_release_tag() -> str: + tag = time.strftime("Zenodo_%Y%m%d", time.localtime()) + index = -1 + firedrake_repo = get_component_repository("firedrake") + for release in firedrake_repo.tags(): + if release.name.startswith(tag): + newindex = int(release.name.split(".")[1]) + index = max(index, newindex) + tag += "." + str(index + 1) + return tag -for component in components: - repo = gh.repository(projects[component], component) - try: - commit = repo.commit(shas[component]) - if not commit: - log.error("Failed to find specified commit for %s" % component) +def check_ref_exists(component): + if "version" in shas[component]: + # component is versioned, a GitHub release should already exist + if not release_already_exists(component, shas[component]["version"]): + log.error(f"A release of {component} is referenced but no corresponding release on GitHub can be found, aborting") + sys.exit(1) + else: + repo = get_component_repository(component) + repo.commit(shas[component]["commit_id"]) - shas[component] = commit.sha - except KeyError: - log.warning("No commit specified for %s. No release will be created for this component." % component) - # Also check that the tag name does not already exist. - if any(t.name == tag for t in repo.tags()): - log.warning(f"Repository {component} already has tag {tag}. This should not have happened.") - sys.exit(1) +def make_github_release_or_tag_existing(component: str) -> None: + repo = get_component_repository(component) + + if "version" in shas[component]: + # component is versioned, a GitHub release should already exist + release = get_matching_release(component, shas[component]["version"]) + tag_existing_release = True + commit_id = release.target_commitish + else: + # referencing a specific commit, may need to make a new release -# Now create releases. -message = """This release is specifically created to document the version of -{component} used in a particular set of experiments using -Firedrake. Please do not cite this as a general source for Firedrake -or any of its dependencies. Instead, refer to -https://www.firedrakeproject.org/citing.html""" - - -for component in (set(shas) & set(components)): - log.info("Releasing %s" % component) - repo = gh.repository(projects[component], component) - releases = repo.releases() - just_tag = False - date = datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat() - tagger = {"name": "firedrake-zenodo", - "email": "firedrake@imperial.ac.uk", - "date": date} - - for release in releases: - if release.target_commitish == shas[component]: - just_tag = True - break - if just_tag: - repo.create_tag(tag, - message=descriptions[component], - sha=shas[component], + # make sure that 'commit_id' is not truncated + commit_id = repo.commit(shas[component]["commit_id"]) + + if commit_already_exists_in_release(component, commit_id): + tag_existing_release = True + else: + tag_existing_release = False + + if tag_existing_release: + log.info(f"Commit '{commit_id}' found in a pre-existing release for " + f"'{component}', adding tag '{release_tag}' to it") + date = datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat() + tagger = {"name": "firedrake-zenodo", + "email": "firedrake@imperial.ac.uk", + "date": date} + repo.create_tag(release_tag, + message=DESCRIPTIONS[component], + sha=commit_id, obj_type="tree", tagger=tagger) else: + log.info(f"Pre-existing release for component '{component}' for commit '{commit_id}' not found, creating a new release '{release_tag}' for it") + body = ( + "This release is specifically created to document the version of " + f"{component} used in a particular set of experiments using " + "Firedrake. Please do not cite this as a general source for Firedrake " + "or any of its dependencies. Instead, refer to " + "https://www.firedrakeproject.org/citing.html" + ) repo.create_release( - tag_name=tag, - target_commitish=shas[component], - name=descriptions[component], - body=message.format(component=component), + tag_name=release_tag, + target_commitish=commit_id, + name=DESCRIPTIONS[component], + body=body, draft=False, prerelease=False) -meta_file = "firedrake-meta-release.json" -with open(meta_file, "w") as f: - data = {"tag": tag, - "title": shas["title"], - "components": sorted(set(shas) & set(components)), - "info_file": shas.get("metarelease_info_file", None), - "new_version_of": shas.get("new_version_of", None), - "additional_dois": shas.get("additional_dois", None)} - f.write(json.dumps(data)) - -log.info("Releases complete. The release tag is %s" % tag) -log.info("Now, you need to create the meta-release") -log.info("Run 'firedrake-zenodo --create-meta-release %s' to do this" % os.path.abspath(meta_file)) -log.info("It is best to wait a short while to ensure that Zenodo is up to date") + +def make_releases() -> None: + for component in components: + check_ref_exists(component) + + for component in components: + make_github_release_or_tag_existing(component) + + meta_file = "firedrake-meta-release.json" + with open(meta_file, "w") as f: + data = {"tag": release_tag, + "title": shas["title"], + "components": components, + "info_file": shas.get("metarelease_info_file", None), + "new_version_of": shas.get("new_version_of", None), + "additional_dois": shas.get("additional_dois", None)} + f.write(json.dumps(data)) + + log.info("Releases complete.") + log.info(f"Now you should create the meta-release with 'firedrake-zenodo --create-meta-release {os.path.abspath(meta_file)}'.") + log.info("It is best to wait a short while to ensure that the new releases are detected by Zenodo.") + + +def release_already_exists(component: str, release_name: str) -> bool: + try: + get_matching_release(component, release_name) + return True + except KeyError: + return False + + +def get_matching_release(component: str, release_name: str) -> github3.Release: + for release in get_component_repository(component).releases(): + if release.tag_name == release_name: + return release + raise KeyError + + +def commit_already_exists_in_release(component: str, commit_id: str) -> bool: + for release in get_component_repository(component).releases(): + if release.target_commitish == commit_id: + return True + return False + + +release_tag = generate_unique_release_tag() +make_releases() diff --git a/pyproject.toml b/pyproject.toml index 55189638ed..cd3e384e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "firedrake" -version = "0.14_dev" +#