Repository: niconoe/pyinaturalist (python)

Issue:
Migrate from Travis CI to GitHub Actions
[RIP Travis CI](https://blog.travis-ci.com/2020-11-02-travis-ci-new-billing) 😢
Travis-ci.org is going read-only next month, and travis-ci.com will no longer have free plans for open-source public repos. Moving to GitHub Actions seems to be the popular choice.

Specific features to reproduce:

- [x] Test multiple python versions
- [x] Run additional style checks + build tests (only run for python 3.8, not all python versions)
- [x] Send test coverage results to Coveralls if all tests pass
- [ ] Deploy to pypi using an API token stored in GitHub secrets
- [x] Deploy only if all tests for all python versions passed
- [x] Deploy stable builds on git tags only
- [x] Deploy pre-release builds versioned in the format `<major>.<minor>.<patch>.dev<build_number>`

Pull request:
Migrate from Travis CI to GitHub Actions
Closes #75 

**Note:** I timed the builds with and without tox, and running without tox roughly cuts the build time in half (2:50 -> 1:19). The tox environments are mostly redundant with the build matrix environments. I'm keeping the tox config, though, because it's still useful for local testing.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..5ef367e
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,88 @@
+name: Build & test python package
+
+on:
+  push:
+    branches: [master, dev]
+    tags: ['v*']
+  pull_request:
+    branches: [master, dev]
+
+jobs:
+  test:
+    runs-on: ubuntu-18.04
+    strategy:
+      matrix:
+        python-version: [3.6, 3.7, 3.8, 3.9]
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      # Cache packages per python version, and reuse until setup.py changes
+      - name: Cache pip packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
+          restore-keys: ${{ runner.os }}-pip-${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: pip install ".[dev]"
+      - name: Run unit tests
+        run: pytest
+
+  analyze:
+    runs-on: ubuntu-18.04
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: '3.8'
+      - name: Cache pip packages
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-3.8-${{ hashFiles('setup.py') }}
+          restore-keys: ${{ runner.os }}-pip-3.8
+
+      - name: Install dependencies
+        run: pip install ".[dev]"
+      - name: Run style checks
+        run: black --check .
+      - name: Run type checks
+        run: mypy .
+      - name: Generate code coverage report
+        run: pytest --cov --cov-report=term --cov-report=html
+      - name: Send code coverage report to Coveralls
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: coveralls
+      - name: Test Sphinx documentation build
+        run: make -C docs all
+      - name: Test package build
+        run: |
+          python setup.py sdist bdist_wheel
+          twine check dist/*
+
+  # Deploy pre-release builds from dev branch, and stable builds on tags only
+  release:
+    needs: [test, analyze]
+    if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/dev'
+    runs-on: ubuntu-18.04
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: '3.8'
+
+      - name: Install dependencies
+        run: pip install -U ".[build]"
+      - name: Build wheel
+        run: python setup.py sdist bdist_wheel
+      - name: Deploy to pypi
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
+        run: twine upload dist/*
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 4287fbd..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-# Config file for automatic testing at travis-ci.org
-
-sudo: required
-language: python
-dist: xenial
-
-matrix:
-  include:
-  - python: "3.6"
-  - python: "3.7"
-  # Run a separate job for combined code quality checks & build tests
-  - python: "3.8"
-    env: TOXENV=coverage,mypy,style,docs,dist-test
-  # Only this job will contain an encrypted PYPI_TOKEN for deployment
-  - python: "3.8"
-    env:
-      - secure: QFRSBbtAPfFxO2GONfI2KCrq7gdSI6zj4wI5QEunjz8xVjvb60TD2Q4ULmoMPBqgFkqV6NZr6bB49MpNcpfw9aeBkUqjYmWWkLfZZsFWvJeRnkPV4EcfmMKJ9pph9QUYQ1BUcqWPZbPYk0VQdDxMVwDKETW2nzVNgRFaJm4O9kM6qV462/3dN53Q0NNTBfSju80nV8YQycP7onEQQILFityDOThKqR3FctDiwrfMo3ALWSfghkyvCgvrvpf6uAWncM1hwDwga+1Luo3vDG6reDE+M3AitFwo5XsPXy8hER8BwuUh8MRrr/HbakH66e83vvBX593DoGZr7UMqwEg+Xzpa2yUvSszW2U9ksh9BaYPbZG3nzsJJlTg61k6Sh7OU4AXglsDBPVX5aoLVz6XSAL97p+Ec85yo8dzpxzBc31ow8rI6JS9yycRnHXGATKqTIQ7Z3QiVaNKrIJVKhMdqvng1zb8RFR49MbPFT5QQuxOJLe/S1qkzOjclp2scZV17TPa6hkfTuDz01+rPXKh3US0WbR689TtNCBB/UwuoX5vnFqcvmqi0sZtl0G4eqXPvik4r7JnuX3d81LIGzF6e45FyO8sSvicZxCu8KmQQ7xI0hXjP6+vfEOnnz4xpAKmHmf2xTIStpuaCm7xqdt3M070P/A4kC0xBGcbcNCq1xU4=
-  - python: "3.9"
-
-# Install dependencies, run tests, and run coveralls after 'coverage' job
-install: pip install ".[build]"
-script: tox
-after_success: if [[ $TOXENV == *"coverage"* ]]; then coveralls; fi
-
-# Note: These two release types could be combined, but requires a long, ugly custom condition
-# See: https://docs.travis-ci.com/user/deployment#conditional-releases-with-on
-deploy:
-  # Stable releases: only git tags on master branch
-  - provider: pypi
-    user: __token__
-    password: $PYPI_TOKEN
-    distributions: sdist bdist_wheel
-    skip_cleanup: true          # Don't delete build artifacts
-    skip_existing: true         # Don't overwrite an existing package with the same name
-    on:
-      tags: true
-      condition: $PYPI_TOKEN    # Only run in the job containing an encrypted PYPI_TOKEN
-  # Pre-releases: dev branch only; pre-release version number is set in __init__.py
-  - provider: pypi
-    user: __token__
-    password: $PYPI_TOKEN
-    distributions: sdist bdist_wheel
-    skip_cleanup: true
-    skip_existing: true
-    on:
-      branch: dev
-      condition: $PYPI_TOKEN
diff --git a/pyinaturalist/__init__.py b/pyinaturalist/__init__.py
index c7c5d63..673a08f 100644
--- a/pyinaturalist/__init__.py
+++ b/pyinaturalist/__init__.py
@@ -13,18 +13,15 @@ user_agent = DEFAULT_USER_AGENT
 
 
 def get_prerelease_version(version: str) -> str:
-    """If we're running in a Travis CI job on the dev branch, get a prerelease version using the
+    """If we're running in a GitHub Action job on the dev branch, get a prerelease version using the
     current build number. For example: ``1.0.0 -> 1.0.0-dev.123``
-
-    This could also be done in ``.travis.yml``, but it's a bit cleaner to do in python.
     """
-    if not (getenv("TRAVIS") == "true" and getenv("TRAVIS_BRANCH") == "dev"):
-        return version
-    build_number = getenv("TRAVIS_BUILD_NUMBER", "0")
-    new_version = f"{version}-dev.{build_number}"
-    getLogger(__name__).info(f"Using pre-release version: {new_version}")
-    return new_version
+    if getenv("GITHUB_REF") == "refs/heads/dev":
+        build_number = getenv("GITHUB_RUN_NUMBER", "0")
+        version = f"{version}-dev.{build_number}"
+        getLogger(__name__).info(f"Using pre-release version: {version}")
+    return version
 
 
-# This won't modify the version outside of Travis
+# This won't modify the version outside of a GitHub Action
 __version__ = get_prerelease_version(__version__)
diff --git a/setup.py b/setup.py
index 723b72f..a2571de 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ from pyinaturalist import __version__
 # These package categories allow tox and build environments to install only what they need
 extras_require = {
     # Packages used for CI jobs
-    "build": ["coveralls", "tox-travis"],
+    "build": ["coveralls", "twine", "wheel"],
     # Packages used for documentation builds
     "docs": [
         "m2r2",


Test patch:
diff --git a/test/test_version.py b/test/test_version.py
index 1ed5fdc..20418b4 100644
--- a/test/test_version.py
+++ b/test/test_version.py
@@ -1,17 +1,17 @@
-# A couple tests to make sure that versioning works as expected within Travis
+# A couple tests to make sure that versioning works as expected within GitHub Actions
 # So, for example, the build would fail before accidentally publishing a bad version
 from unittest.mock import patch
 
 
 # Mocking out getenv() instead of actually setting envars so this doesn't affect other tests
-@patch("pyinaturalist.getenv", side_effect=["true", "master", "123"])
+@patch("pyinaturalist.getenv", side_effect=["refs/heads/master", "123"])
 def test_version__stable_release(mock_getenv):
     import pyinaturalist
 
     assert "dev" not in pyinaturalist.__version__
 
 
-@patch("pyinaturalist.getenv", side_effect=["true", "dev", "123"])
+@patch("pyinaturalist.getenv", side_effect=["refs/heads/dev", "123"])
 def test_version__pre_release(mock_getenv):
     import pyinaturalist