diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..dd73301 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,90 @@ +name: PyPI Release + +on: + push: + tags: + # Release tags are bare version numbers: 1.2.0, 1.2.0.dev0 + - "[0-9]*" + # A `v` prefix is NOT a valid release tag. It is matched here only so the + # job can fail with an explanatory error instead of silently doing nothing. + - "v[0-9]*" + +concurrency: pypi-release-${{ github.ref }} + +jobs: + publish: + name: Build and publish numerai-cli to PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # full history so the master-ancestry check below can run + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install --upgrade build packaging setuptools twine wheel + + - name: Verify tag format and version match + env: + TAG_NAME: ${{ github.ref_name }} + run: | + python - <<'PY' + import os + import pathlib + import re + import sys + from packaging.version import Version + + tag = os.environ["TAG_NAME"] + if tag.startswith("v"): + sys.exit( + f"::error::Release tags must be bare version numbers. " + f"`{tag}` has a `v` prefix; delete it and tag `{tag[1:]}` instead." + ) + pkg = re.search( + r'numerai_cli_version\s*=\s*"([^"]+)"', + pathlib.Path("setup.py").read_text(), + ).group(1) + print(f"tag={tag} setup.py={pkg}") + if Version(tag) != Version(pkg): + sys.exit( + f"::error::Tag {tag} does not match setup.py numerai_cli_version={pkg}" + ) + print(f"::notice::publishing numerai-cli {Version(pkg)}") + PY + + - name: Final releases must come from master + env: + TAG_NAME: ${{ github.ref_name }} + run: | + python - <<'PY' > prerelease.txt + import os + from packaging.version import Version + + print("yes" if Version(os.environ["TAG_NAME"]).is_prerelease else "no") + PY + if [ "$(cat prerelease.txt)" = "no" ]; then + git fetch --no-tags --quiet origin master + if git merge-base --is-ancestor "$GITHUB_SHA" FETCH_HEAD; then + echo "final release from a commit on master: ok" + else + echo "::error::Final release tags must point at a commit on master; $GITHUB_SHA is not one. Cut a pre-release (e.g. 1.1.7.dev0) from your branch instead." + exit 1 + fi + else + echo "pre-release: master-ancestry check skipped" + fi + rm -f prerelease.txt + + - name: Build a source tarball and wheel + run: python -m build + + # No --skip-existing: under tag-triggered releases a duplicate version is + # a mistake worth failing on, not something to silently swallow. + - name: Publish to PyPI + run: | + python -m twine upload \ + -u __token__ \ + -p ${{ secrets.PYPI_API_KEY }} \ + dist/* diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 34e8b3c..2f899da 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -1,24 +1,27 @@ -name: Test & Deploy Numerai CLI +name: Test Numerai CLI +# Publishing lives in pypi.yml and is triggered by version tags, not by pushes +# to this or any other branch. See RELEASING.md. on: workflow_dispatch: push: -concurrency: deploy-numerai-cli-${{ github.ref }} +concurrency: test-numerai-cli-${{ github.ref }} jobs: build-python: name: Build Python ${{ matrix.python-version }} strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install . + - run: python -m unittest discover -s tests - run: numerai copy-example - run: test -e tournament-python3/predict.py @@ -26,27 +29,9 @@ jobs: name: Build Docker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: | cd numerai/examples/tournament-python3 \ && docker build -t example-python \ --build-arg NUMERAI_PUBLIC_ID=test \ --build-arg NUMERAI_SECRET_KEY=test . - - pypi-release: - name: PyPI Release - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/master' - needs: [build-python, build-docker] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v4 - with: - python-version: "3.12" - - run: python -m pip install --upgrade setuptools twine wheel - - run: python setup.py sdist bdist_wheel - - run: | - python -m twine upload \ - -u __token__ \ - -p ${{ secrets.PYPI_API_KEY }} \ - --skip-existing dist/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7786c2e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +Notable changes to numerai-cli. Update this file as part of the version-bump +commit for each release (see [RELEASING.md](RELEASING.md)). + +Releases before 1.2.0 were not tracked here — their notes live in the pull +requests that cut them. + +## 1.2.0 - 2026-08-12 + +### Added + +- Python 3.14 support. The CI test matrix now covers Python 3.10 through 3.14, + and the example prediction nodes build on `python:3.14` Docker images. +- Tag-triggered PyPI release workflow (`.github/workflows/pypi.yml`). Pushing a + bare version tag (e.g. `1.2.0`, `1.2.0.dev0`) is now the only thing that + publishes. The workflow refuses to publish if the tag has a `v` prefix, if it + doesn't match `numerai_cli_version` in `setup.py`, or if a final (non-`.devN`) + release is tagged off a commit that isn't on `master`. +- [RELEASING.md](RELEASING.md), documenting the release flow: beta `.devN` + pre-releases from topic branches, final releases from `master`, and hotfixes. +- Tests for the platform setup scripts (`tests/test_setup_scripts.py`), run by + CI on every push. +- This changelog. + +### Changed + +- Upgraded the pinned dependencies in all three example nodes + (`tournament-python3`, `signals-python3`, `crypto-python3`): + - `pandas` 2.3.3 → 3.0.5 (pandas 3 support) + - `pyarrow` 18.1.0 → 23.0.1 + - `lightgbm` 4.5.0 → 4.7.0 — required for the new scikit-learn pin: the + scikit-learn estimator API in lightgbm ≤ 4.5.0 is incompatible with + scikit-learn ≥ 1.6 (`__sklearn_tags__` change). 4.7.0 also adds official + Python 3.14 and pandas 3 support. + - `scikit-learn` 1.6.1 → 1.8.0 +- The platform setup scripts (`setup-mac.sh`, `setup-win10.ps1`) now install + Python 3.12.10 instead of 3.9.1. The macOS script uses the single universal2 + installer, which supports both Intel and Apple Silicon on macOS 10.13+. +- Branch pushes no longer publish anything. The old `pypi-release` job was + removed from `test-and-deploy.yml`, which now only runs the test matrix and + the example Docker build. +- `DEPLOY.md`'s manual `~/.pypirc` + `twine` instructions were replaced with a + pointer to [RELEASING.md](RELEASING.md). + +### Fixed + +- The macOS setup script's Python and OS-version detection checks, which could + never match (a broken `which` comparison and a malformed version regex), were + rewritten using `command -v` and `sw_vers`. +- Publishing a version that already exists on PyPI now fails loudly instead of + silently succeeding: the publish step no longer passes `--skip-existing` to + twine, so forgetting to bump `setup.py` can't ship nothing on a green build. diff --git a/DEPLOY.md b/DEPLOY.md index b4b08d5..73927e4 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,29 +1,7 @@ # Deployment Instructions -Create a .pypirc file in your home directory like so: -``` -▶ cat ~/.pypirc -[distutils] -index-servers = - pypi -[pypi] -repository: https://upload.pypi.org/legacy/ -username: REPLACE_ME -password: *** -``` +Releasing has moved to a tag-triggered flow. See [RELEASING.md](RELEASING.md). -Install twine: -``` -pip3 install twine -``` - -Build the dist packages: -``` -rm -rf dist -python3 setup.py sdist bdist_wheel -``` - -Deploy: -``` -twine upload dist/* -``` +The manual `~/.pypirc` + `twine upload dist/*` procedure that used to live here +is no longer the release path — publishing is done by +`.github/workflows/pypi.yml` when a bare `X.Y.Z` version tag is pushed. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..9ae4c66 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,224 @@ +# Releasing numerai-cli + +numerai-cli ships to **PyPI only**. The Docker build in CI is a smoke test — no +image is pushed anywhere, and nothing in this repo deploys to a cloud account. +Users get everything by running `pip install numerai-cli`. + +This flow is deliberately close to the one in `numerapi/RELEASING.md`, so +switching between the two repos should not require re-learning anything. The +differences that matter are called out under [Caveats specific to this +repo](#caveats-specific-to-this-repo). + +## The model + +Two rules explain everything else: + +1. **`numerai_cli_version` in `setup.py` is the release.** The git tag is only + the trigger. Whatever that string says is what lands on PyPI. +2. **The version string picks the channel**, not the branch. A + [PEP 440](https://peps.python.org/pep-0440/) pre-release (`1.1.7.dev0`) is + invisible to `pip install numerai-cli`; a final version (`1.1.7`) is what + everyone gets by default. + +| Ref | Role | +| --- | --- | +| `/` | all work; branch off `master` | +| `master` | released state; **final** releases are cut here | +| `X.Y.Z.devN` tag | publishes a pre-release, from **any** branch | +| `X.Y.Z` tag | publishes a final release, **must** be on `master` | + +Nothing publishes on a branch push. Only pushing a tag publishes. + +| What a user runs | What they get | +| --- | --- | +| `pip install numerai-cli` / `pip install -U numerai-cli` | latest **final** version | +| `pip install 'numerai-cli==1.1.7.dev0'` | that exact pre-release | +| `pip install --pre numerai-cli` | latest including pre-releases | + +## Conventions + +- **Tags are bare version numbers and must match `setup.py` exactly:** `1.1.7`, + `1.1.7.dev0`. A `v` prefix is not allowed — CI rejects `v1.1.7` with an + explicit error. +- Use the canonical PEP 440 spelling with the dot: `1.1.7.dev0`, not + `1.1.7dev0`. Both normalize to the same release, but the canonical form avoids + confusion. +- Pre-releases use `.devN`. Increment `N` for each beta on the same version line. +- **A version number can never be reused.** PyPI permanently rejects re-uploading + a version, even one that was deleted. If you burn a number, move to the next. + +## Develop without releasing + +```bash +git checkout master && git pull +git checkout -b josh/some-feature +# ... work ... +git push -u origin josh/some-feature +gh pr create --base master +``` + +The Python matrix (3.10–3.14) and the Docker build run on every push. No tag +means nothing is published. Leave `setup.py` alone until you are actually +cutting something. + +## Cut a beta + +For beta users who need the code before it is stable. There is no integration +branch here — cut it straight from your topic branch, before it merges. + +```bash +git checkout josh/some-feature + +# setup.py: numerai_cli_version = "1.1.7.dev0" +git commit -am "numerai-cli 1.1.7.dev0" +git push origin josh/some-feature # publishes nothing + +# tag and push — this is the release event +git tag 1.1.7.dev0 +git push origin 1.1.7.dev0 +``` + +Verify: + +```bash +gh run list --workflow=pypi.yml --limit 1 # expect success +pip install 'numerai-cli==1.1.7.dev0' # what beta users run +pip install -U numerai-cli # must NOT be the dev version +``` + +Tell beta users to install the exact version. Note that `pip index versions` and +the simple index can lag a few minutes behind a successful publish on CDN cache; +an exact-version install works immediately. + +For the next beta, repeat with `.dev1`, `.dev2`, … + +## Promote to a final release (from `master`) + +Flip the version to final **as the last commit before merging**, so `master` +never holds a pre-release string and picks up the release version atomically at +merge. + +```bash +git checkout josh/some-feature + +# setup.py: numerai_cli_version = "1.1.7" (drop the .devN suffix) +git commit -am "numerai-cli 1.1.7" +git push origin josh/some-feature + +gh pr create --base master --title "numerai-cli 1.1.7" +gh pr merge --squash + +git checkout master && git pull +grep numerai_cli_version setup.py # must read exactly 1.1.7 +git tag 1.1.7 +git push origin 1.1.7 +``` + +Verify with `pip install -U numerai-cli`, then `numerai --help` and +`numerai copy-example` on a clean virtualenv. + +## Hotfix a released version + +`master` is the only long-lived branch, so a hotfix is just the normal flow with +a patch bump: + +```bash +git checkout -b hotfix/1.1.8 master +# fix + setup.py 1.1.8 +gh pr create --base master +# after merge: +git checkout master && git pull +git tag 1.1.8 && git push origin 1.1.8 +``` + +## What CI enforces + +`.github/workflows/pypi.yml` runs on tag pushes that start with a digit (and on +`v`-prefixed tags, solely to reject them). It refuses to publish unless: + +1. **The tag has no `v` prefix.** `v1.1.7` fails with an error telling you to + re-tag as `1.1.7`. +2. **The tag matches `setup.py`.** Compared as normalized PEP 440 versions, so + `1.1.7dev0` and `1.1.7.dev0` are equivalent, but `1.1.7` against a `setup.py` + of `1.1.7.dev0` fails. +3. **Final releases point at a commit on `master`.** Pre-releases skip this + check, so betas can be cut from a topic branch but a final one cannot. + +`.github/workflows/test-and-deploy.yml` runs the Python 3.10–3.14 matrix +(`pip install .`, `python -m unittest discover -s tests`, `numerai copy-example`) +and the example Docker build on every push. + +## Caveats specific to this repo + +**There is no `preview` branch, and that is on purpose.** numerapi has one +because `tournament-monorepo` pins it and needs somewhere unreleased work can +sit indefinitely. numerai-cli is a leaf — no internal service depends on it, so +a second long-lived branch would be pure overhead. The practical consequence is +that betas are cut from topic branches rather than from an integration branch. + +**Tests do not gate the publish.** `pypi.yml` and `test-and-deploy.yml` are +separate workflows, and GitHub Actions cannot express a cross-workflow +dependency. Pushing a tag does start the test matrix against the tagged commit, +but it runs *concurrently* with the publish — a red test suite will not stop the +upload. Tag commits that have already gone green on a branch push. + +**A duplicate version now fails loudly.** The old `pypi-release` job used +`twine upload --skip-existing`, which meant forgetting to bump `setup.py` was a +silent no-op: green build, nothing shipped. That is how `setup.py` reached +`1.1.6` while PyPI's latest was `1.1.5`. The flag is gone; a duplicate version +now errors. See [Troubleshooting](#troubleshooting). + +**A release ships more than Python code.** `MANIFEST.in` is +`recursive-include numerai *`, so the terraform modules under +`numerai/terraform/` and every example under `numerai/examples/` (Dockerfiles, +`requirements.txt`, `predict.py`) go out with the package. A release can +therefore change the infrastructure users apply to their own cloud accounts and +the images their prediction nodes build. Blast radius is wider than for a pure +library — this is the main reason the `.devN` channel is worth using here rather +than shipping straight to final. + +**CHANGELOG.md** Edit `CHANGELOG.md` as part of the version-bump commit when cutting a release. + +**Version is only in `setup.py`.** The package exposes no `__version__` and +nothing else in the tree hardcodes it, so `numerai_cli_version` is the single +place to edit. + +**The PyPI secret is `PYPI_API_KEY`** in this repo, not `PYPI_API_TOKEN` as in +numerapi. Publishing uses `twine` directly rather than +`pypa/gh-action-pypi-publish` — it is the path this repo's token is already set +up for. + +**Do not retro-tag old releases.** Any new tag starting with a digit triggers a +publish attempt that will fail on a duplicate version. The only historical tag is +`v0.1.22`, which predates the bare-number convention — leave it alone. It already +exists on `origin`, so it fires nothing; re-pushing it would only hit the +`v`-prefix rejection. The 99 releases on PyPI predating this flow were published +by branch-push CI and have no corresponding tags at all. Two junk tags (`list`, +`liost`, both typos pointing at the same commit) existed locally and have been +deleted — they were never pushed to `origin`. + +**Pre-releases are not new here.** PyPI already holds 49 of them from the 0.3.x +era (`0.3.0.dev10` … `0.3.4.dev1`), published back when CircleCI released from +every branch. The `.devN` convention above matches what is already there. + +## Troubleshooting + +**`File already exists` on publish.** That version is already on PyPI. Bump to +the next number — you cannot re-upload, and you cannot fix it by deleting the +release on PyPI either. + +**Tag mismatch error.** You tagged without bumping `setup.py`, or vice versa. +Fix `setup.py`, commit, delete the tag locally and on origin +(`git push origin :refs/tags/X.Y.Z`), then re-tag. Deleting a tag never publishes +anything. + +**"Release tags must be bare version numbers."** You tagged `v1.1.7` out of +habit. Delete the tag locally and on origin, then re-tag as `1.1.7`. + +**"Final release tags must point at a commit on master."** You tagged a +suffix-free version on a topic branch. Either merge to `master` first, or cut it +as a `.devN` pre-release instead. + +**A bad version is already public.** You cannot unpublish, but you can +`yank` it on PyPI, which hides it from resolution while leaving existing pins +working. Then ship the fix as the next version. diff --git a/numerai/examples/crypto-python3/Dockerfile b/numerai/examples/crypto-python3/Dockerfile index 23d1a09..5707358 100644 --- a/numerai/examples/crypto-python3/Dockerfile +++ b/numerai/examples/crypto-python3/Dockerfile @@ -1,5 +1,5 @@ # Provides us a working Python 3 environment. -FROM python:3.13 +FROM python:3.14 # These are docker arguments that `numerai node deploy/test` will always pass into docker. # They are then set in your environment so that numerapi can access them when uploading submissions. diff --git a/numerai/examples/crypto-python3/requirements.txt b/numerai/examples/crypto-python3/requirements.txt index da7ba24..684bdc3 100644 --- a/numerai/examples/crypto-python3/requirements.txt +++ b/numerai/examples/crypto-python3/requirements.txt @@ -1,6 +1,6 @@ numerapi==2.22.0 -pandas==2.3.3 -pyarrow==18.1.0 +pandas==3.0.5 +pyarrow==23.0.1 joblib==1.5.3 -lightgbm==4.5.0 -scikit-learn==1.6.1 +lightgbm==4.7.0 +scikit-learn==1.8.0 diff --git a/numerai/examples/signals-python3/Dockerfile b/numerai/examples/signals-python3/Dockerfile index 23d1a09..5707358 100644 --- a/numerai/examples/signals-python3/Dockerfile +++ b/numerai/examples/signals-python3/Dockerfile @@ -1,5 +1,5 @@ # Provides us a working Python 3 environment. -FROM python:3.13 +FROM python:3.14 # These are docker arguments that `numerai node deploy/test` will always pass into docker. # They are then set in your environment so that numerapi can access them when uploading submissions. diff --git a/numerai/examples/signals-python3/requirements.txt b/numerai/examples/signals-python3/requirements.txt index da7ba24..684bdc3 100644 --- a/numerai/examples/signals-python3/requirements.txt +++ b/numerai/examples/signals-python3/requirements.txt @@ -1,6 +1,6 @@ numerapi==2.22.0 -pandas==2.3.3 -pyarrow==18.1.0 +pandas==3.0.5 +pyarrow==23.0.1 joblib==1.5.3 -lightgbm==4.5.0 -scikit-learn==1.6.1 +lightgbm==4.7.0 +scikit-learn==1.8.0 diff --git a/numerai/examples/tournament-python3/Dockerfile b/numerai/examples/tournament-python3/Dockerfile index 23d1a09..5707358 100644 --- a/numerai/examples/tournament-python3/Dockerfile +++ b/numerai/examples/tournament-python3/Dockerfile @@ -1,5 +1,5 @@ # Provides us a working Python 3 environment. -FROM python:3.13 +FROM python:3.14 # These are docker arguments that `numerai node deploy/test` will always pass into docker. # They are then set in your environment so that numerapi can access them when uploading submissions. diff --git a/numerai/examples/tournament-python3/requirements.txt b/numerai/examples/tournament-python3/requirements.txt index da7ba24..684bdc3 100644 --- a/numerai/examples/tournament-python3/requirements.txt +++ b/numerai/examples/tournament-python3/requirements.txt @@ -1,6 +1,6 @@ numerapi==2.22.0 -pandas==2.3.3 -pyarrow==18.1.0 +pandas==3.0.5 +pyarrow==23.0.1 joblib==1.5.3 -lightgbm==4.5.0 -scikit-learn==1.6.1 +lightgbm==4.7.0 +scikit-learn==1.8.0 diff --git a/scripts/setup-mac.sh b/scripts/setup-mac.sh index 7103040..1a94456 100644 --- a/scripts/setup-mac.sh +++ b/scripts/setup-mac.sh @@ -7,30 +7,27 @@ xcode-select --installed fi - # Install Python 3.9.1 if not found, checks for OS X 10.9 or later and Intel vs. Apple Silicon - if [[ $(which python3) = "python3 not installed" ]]; then + # Install Python 3.12.10 if not found. Its universal2 installer supports + # both Intel and Apple Silicon on macOS 10.13 or later. + if ! command -v python3 >/dev/null 2>&1; then echo "Python 3 not found, installing now..." - sys_ver_os=$(system_profiler SPSoftwareDataType | grep "System Version:") - if [[ sys_ver_os =~ (.*[macOS|OS X] [10\.9|10.1\d|11\.]) ]]; then - echo "Mac OS 10.9 or later detected, installing Python 3.9.1" - - if [[ $(system_profiler SPHardwareDataType | grep "Processor Name:" ) =~ .*Intel.* ]]; then - echo "Intel chip detected..." - curl https://www.python.org/ftp/python/3.9.1/python-3.9.1-macosx10.9.pkg --output ~/Downloads/python-3.9.1-installer.pkg - else - echo "Apple Silicon detected..." - curl https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg --output ~/Downloads/python-3.9.1-installer.pkg - fi - sudo installer -pkg ~/Downloads/python-3.9.1-installer.pkg -target / + sys_ver_os=$(sw_vers -productVersion) + sys_ver_major=${sys_ver_os%%.*} + sys_ver_minor=${sys_ver_os#*.} + sys_ver_minor=${sys_ver_minor%%.*} + if (( sys_ver_major > 10 || (sys_ver_major == 10 && sys_ver_minor >= 13) )); then + echo "macOS 10.13 or later detected, installing Python 3.12.10" + curl -fL https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg --output ~/Downloads/python-3.12.10-installer.pkg + sudo installer -pkg ~/Downloads/python-3.12.10-installer.pkg -target / else - echo "Your Mac OS version is too old, consider updating to 10.9 before installing python..." + echo "Your macOS version is too old, consider updating to 10.13 before installing Python..." echo $sys_ver_os exit 1 fi - echo "Python 3.9.1 installed!" + echo "Python 3.12.10 installed!" else echo "Python 3 installed!" fi @@ -60,4 +57,4 @@ echo "Setup script failed, please include the following along with the error if you report this:" system_profiler SPSoftwareDataType system_profiler SPHardwareDataType -} \ No newline at end of file +} diff --git a/scripts/setup-win10.ps1 b/scripts/setup-win10.ps1 index 3f24afd..ef13406 100644 --- a/scripts/setup-win10.ps1 +++ b/scripts/setup-win10.ps1 @@ -2,7 +2,7 @@ [CmdletBinding()] Param( $downloadsDir = "C:\Users\$env:UserName\Downloads", $installsDir = "C:\Program Files", - $pythonVersion = "3.9.1", + $pythonVersion = "3.12.10", $pythonUrl = "https://www.python.org/ftp/python/$pythonVersion/python-$pythonVersion-amd64.exe", $pythonDownloadPath = "$downloadsDir\python-$pythonVersion-amd64-installer.exe", $pythonInstallDir = "$installsDir\Python$pythonVersion", @@ -44,4 +44,3 @@ Catch [Exception] { Write-Host $_.Exception | format-list -force Get-ComputerInfo } - diff --git a/setup.py b/setup.py index 6115e72..393672e 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,10 @@ from setuptools import setup from os import path +# The release version. See RELEASING.md -- this string is the release, the git +# tag is only the trigger, and CI refuses to publish if the two disagree. +numerai_cli_version = "1.2.0" + # read the contents of the README file this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: @@ -9,7 +13,7 @@ setup( name="numerai-cli", - version="1.1.5", + version=numerai_cli_version, description="A library for deploying Numer.ai Prediction Nodes.", url="https://github.com/numerai/numerai-cli", author="Numer.ai", diff --git a/tests/test_setup_scripts.py b/tests/test_setup_scripts.py new file mode 100644 index 0000000..cbeea4c --- /dev/null +++ b/tests/test_setup_scripts.py @@ -0,0 +1,98 @@ +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +SCRIPTS_DIRECTORY = REPOSITORY_ROOT / "scripts" + + +class SetupScriptsTest(unittest.TestCase): + def test_installers_do_not_reference_python_39(self): + for script_path in SCRIPTS_DIRECTORY.iterdir(): + if not script_path.is_file(): + continue + with self.subTest(script=script_path.name): + self.assertNotIn("3.9", script_path.read_text()) + + def test_macos_installs_python_312(self): + script = (SCRIPTS_DIRECTORY / "setup-mac.sh").read_text() + + self.assertIn("Python 3.12.10", script) + self.assertIn( + "https://www.python.org/ftp/python/3.12.10/" + "python-3.12.10-macos11.pkg", + script, + ) + + def test_macos_runs_the_python_312_installer_when_python_is_missing(self): + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + commands_path = temporary_path / "commands" + commands_path.mkdir() + command_log = temporary_path / "commands.log" + + commands = { + "curl": '#!/bin/bash\nprintf "curl %s\\n" "$*" >> "$COMMAND_LOG"\n', + "sudo": '#!/bin/bash\nprintf "sudo %s\\n" "$*" >> "$COMMAND_LOG"\n', + "sw_vers": '#!/bin/bash\necho "13.6.0"\n', + "which": ( + '#!/bin/bash\nif [[ "$1" == "docker" ]]; then ' + 'echo "/usr/local/bin/docker"; fi\n' + ), + } + for command_name, command in commands.items(): + command_path = commands_path / command_name + command_path.write_text(command) + command_path.chmod(0o755) + + environment = os.environ.copy() + environment.update( + { + "COMMAND_LOG": str(command_log), + "HOME": str(temporary_path), + "PATH": str(commands_path), + } + ) + subprocess.run( + ["/bin/bash", SCRIPTS_DIRECTORY / "setup-mac.sh"], + check=True, + env=environment, + capture_output=True, + text=True, + ) + + commands_run = command_log.read_text() + self.assertIn( + "curl -fL https://www.python.org/ftp/python/3.12.10/" + "python-3.12.10-macos11.pkg", + commands_run, + ) + self.assertIn( + "sudo installer -pkg " + f"{temporary_path}/Downloads/python-3.12.10-installer.pkg -target /", + commands_run, + ) + + def test_windows_installs_python_312(self): + script = (SCRIPTS_DIRECTORY / "setup-win10.ps1").read_text() + + self.assertIn('$pythonVersion = "3.12.10"', script) + self.assertIn( + "https://www.python.org/ftp/python/$pythonVersion/" + "python-$pythonVersion-amd64.exe", + script, + ) + + def test_shell_scripts_have_valid_syntax(self): + for script_name in ("setup-mac.sh", "setup-ubu.sh"): + with self.subTest(script=script_name): + subprocess.run( + ["bash", "-n", SCRIPTS_DIRECTORY / script_name], check=True + ) + + +if __name__ == "__main__": + unittest.main()