diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml
new file mode 100644
index 000000000..f072fca98
--- /dev/null
+++ b/.github/workflows/nightly-builds.yml
@@ -0,0 +1,95 @@
+name: Nightly Checks
+
+on:
+ schedule:
+ # Runs at 09Z (3am MDT)
+ - cron: "0 9 * * 2"
+
+ # Allow a manual run
+ workflow_dispatch:
+
+ # Run if we modify the workflow
+ push:
+ branches:
+ - main
+ paths:
+ - .github/workflows/nightly-builds.yml
+ - .github/workflows/unstable-builds.yml
+ pull_request:
+ paths:
+ - .github/workflows/nightly-builds.yml
+ - .github/workflows/unstable-builds.yml
+
+jobs:
+ Builds:
+ uses: ./.github/workflows/unstable-builds.yml
+
+ Report:
+ name: Report
+ needs: Builds
+ if: failure() || github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+
+ steps:
+ - name: Download logs
+ uses: actions/download-artifact@v4
+ with:
+ path: /tmp/workspace/logs
+
+ - name: Grab log files
+ run: |
+ cp /tmp/workspace/logs/log-*/*.log . || true
+ touch tests-nightly.log build.log linkchecker.log
+
+ - name: Report failures
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const title = "Nightly build is failing";
+ const workflow_url = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
+ body = `The [Nightly workflow](${workflow_url}) is failing.\n`;
+
+ if ('${{ needs.Builds.outputs.tests_result }}' === 'failure') {
+ const test_log = fs.readFileSync('tests-nightly.log', 'utf8').substring(0, 21000);
+ body += `The tests failed.\nLog:\n${test_log}
`;
+ }
+ if ('${{ needs.Builds.outputs.docs_result }}' === 'failure') {
+ const build_log = fs.readFileSync('build.log', 'utf8').substring(0, 21000);
+ const linkchecker = fs.readFileSync('linkchecker.log', 'utf8').substring(0, 21000);
+ body += `The documentation build failed.\nLog:\n${build_log}
`;
+ body += `\nLinkchecker output:\n${linkchecker}
`;
+ }
+
+ // See if we have an existing issue
+ const items = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ creator: 'github-actions[bot]'
+ });
+ const existing = items.data.filter(i => i.title === title);
+
+ params = {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: body,
+ title: title,
+ labels: ['Type: Maintenance']
+ };
+
+ // On PRs, avoid actually issuing an API request, since we don't have permission
+ if ( context.eventName === 'pull_request' ) {
+ github.hook.wrap('request', (request, options) => { return {}; })
+ }
+
+ if (existing.length === 0) {
+ console.log('Creating new issue.')
+ github.rest.issues.create(params)
+ } else {
+ params.issue_number = existing[0].number;
+ console.log(`Updating existing issue: ${params.issue_number}`)
+ github.rest.issues.update(params)
+ }
diff --git a/.github/workflows/run-unstable-pr.yml b/.github/workflows/run-unstable-pr.yml
new file mode 100644
index 000000000..859c94a77
--- /dev/null
+++ b/.github/workflows/run-unstable-pr.yml
@@ -0,0 +1,16 @@
+name: PR Unstable Builds
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - labeled
+
+jobs:
+ Builds:
+ if: |
+ ((github.event.action == 'labeled' && github.event.label.name == 'nightly-ci') ||
+ contains(github.event.pull_request.labels.*.name, 'nightly-ci'))
+ uses: ./.github/workflows/unstable-builds.yml
diff --git a/.github/workflows/unstable-builds.yml b/.github/workflows/unstable-builds.yml
new file mode 100644
index 000000000..fb94400a4
--- /dev/null
+++ b/.github/workflows/unstable-builds.yml
@@ -0,0 +1,93 @@
+name: Unstable Builds
+
+on:
+ workflow_call:
+ outputs:
+ tests_result:
+ description: "Result from running tests"
+ value: ${{ jobs.Tests.outputs.result }}
+ docs_result:
+ description: "Result from running docs"
+ value: ${{ jobs.Docs.outputs.result }}
+
+jobs:
+ Tests:
+ runs-on: ubuntu-latest
+ outputs:
+ result: ${{ steps.tests.outcome }}
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 150
+ fetch-tags: true
+
+ - name: Assemble test requirements
+ run: |
+ echo git+https://github.com/pydata/xarray@main#egg=xarray > ci/extra_requirements.txt
+
+ - name: Install using PyPI
+ uses: Unidata/MetPy/.github/actions/install-pypi@main
+ with:
+ need-extras: true
+ type: test
+ version-file: Prerelease
+ python-version: 3.12
+ need-cartopy: 'false'
+
+ - name: Run tests
+ id: tests
+ uses: Unidata/MetPy/.github/actions/run-tests@main
+ with:
+ run-doctests: false
+ key: nightly
+ upload-coverage: false
+ pytest-args: ''
+
+ - name: Upload test log
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: log-nightly-tests
+ path: tests-nightly.log
+ retention-days: 5
+
+ Docs:
+ runs-on: ubuntu-latest
+ outputs:
+ result: ${{ steps.build.outcome }}
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 150
+ fetch-tags: true
+
+ - name: Assemble doc requirements
+ run: |
+ echo git+https://github.com/pydata/xarray@main#egg=xarray > ci/extra_requirements.txt
+
+ - name: Install using PyPI
+ uses: Unidata/MetPy/.github/actions/install-pypi@main
+ with:
+ type: doc
+ version-file: Prerelease
+ python-version: 3.12
+
+ - name: Build docs
+ id: build
+ uses: Unidata/MetPY/.github/actions/build-docs@main
+ with:
+ run-linkchecker: true
+ key: nightly
+ make-targets: ''
+
+ - name: Upload build log
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: log-nightly-docs
+ path: |
+ build.log
+ linkchecker.log
+ retention-days: 5