Repository: geoschem/gcpy (language: python)

Issue:
Make GCPy a conventional Python package
### Name and Institution (Required)

Name: Sebastian Eastham
Institution: Imperial College London

### Confirm you have reviewed the following documentation

- [x] [Contributing guidelines](https://gcpy.readthedocs.io/en/stable/reference/CONTRIBUTING.html)

### New GCPy feature or discussion

Requiring users to directly manipulate `PYTHONPATH` ([as is currently recommended](https://gcpy.readthedocs.io/en/stable/Getting-Started-with-GCPy.html#gcpy-install)) is awkward, non-standard, and makes it difficult to include GCPy as a dependency for other packages. Being able to install GCPy using `pip` (or, better yet, having it as a package on `conda-forge`) would significantly increase its accessibility and usability.


Discussion / hints:
Thanks for the suggestion! GCPy is actually already available on conda-forge as [geoschem-gcpy](https://anaconda.org/conda-forge/geoschem-gcpy). However, the version there is v1.3.2 and we have had a few releases since (1.3.3, 1.4.0). We should update that since it looks like there is demand for this. The conda-forge page says there have been >40k downloads.
Thanks @lizziel and @sdeastham.  It may be a while since we can get to this.  Also, I think the issue that we had was that a package update broke the GCPy package (i.e. they de-listed older ESMF versions).  For the time being we have been specifying an environment file with packages that we know work.

One tricky thing: when you make a package it is expected that it should work with a number of Python versions.  We haven't really tested with anything other than Python 3.9 (since in Python 3.10 there were some updates to the language that may have broken legacy features).  Then you go down the rabbit hole of Python Package Hell.  

We of course will gladly accept any assistance from the community in restoring the package (or giving us best practices):
@msulprizio @kilicomu @laestrada @lizziel @yidant 
So with a little bit of fiddling, I was able to push the 1.4.2 release of GCPy to PyPi.  See: https://pypi.org/project/geoschem-gcpy/1.4.2/

There is also a way of setting up a GitHub action so that this will be done automatically at each release.  I need to figure out how to do that.
Actually there are instructions: https://medium.com/@VersuS_/automate-pypi-releases-with-github-actions-4c5a9cfe947d.  I'll check this out.

Fix patch:
diff --git a/.github/workflows/build-gcpy-environment.yml b/.github/workflows/build-gcpy-environment.yml
index f3953c4..e57f735 100644
--- a/.github/workflows/build-gcpy-environment.yml
+++ b/.github/workflows/build-gcpy-environment.yml
@@ -20,7 +20,7 @@ jobs:
         python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
     steps:
       - name: Checkout the GCPy repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Create "gcpy_env" environment
         uses: mamba-org/setup-micromamba@v1
         with:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 7564906..a6164e8 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -80,3 +80,8 @@ jobs:
       uses: github/codeql-action/analyze@v2
       with:
         category: "/language:${{matrix.language}}"
+
+# Use Node.js version 20 to avoid deprecation warnings
+runs:
+  using: 'node20'
+  main: 'main.js'
diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml
new file mode 100644
index 0000000..2b88a04
--- /dev/null
+++ b/.github/workflows/publish-python.yml
@@ -0,0 +1,39 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Upload GCPy to PyPI
+
+on:
+  release:
+    types: [published]
+
+permissions:
+  contents: read
+
+jobs:
+  deploy:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.x'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install build
+    - name: Build package
+      run: python -m build
+    - name: Publish package
+      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+      with:
+        user: __token__
+        password: ${{ secrets.PYPI_KEY }}
diff --git a/.release/changeVersionNumbers.sh b/.release/changeVersionNumbers.sh
new file mode 100755
index 0000000..7bc694b
--- /dev/null
+++ b/.release/changeVersionNumbers.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+#EOC
+#------------------------------------------------------------------------------
+#                  GEOS-Chem Global Chemical Transport Model                  !
+#------------------------------------------------------------------------------
+#BOP
+#
+# !MODULE: changeVersionNumbers.sh
+#
+# !DESCRIPTION: Bash script to change the version numbers in the appropriate
+#  files in the GCPy directory structure.  Run this before releasing a new
+#  GCPy version.
+#\\
+#\\
+# !CALLING SEQUENCE:
+#  $ ./changeVersionNumbers.sh X.Y.Z        # X.Y.Z = GCClassic version number
+#EOP
+#------------------------------------------------------------------------------
+#BOC
+
+function replace() {
+
+    #========================================================================
+    # Function to replace text in a file via sed.
+    # 
+    # 1st argument: Search pattern
+    # 2nd argument: Replacement text
+    # 3rd argument: File in which to search and replace
+    #========================================================================
+
+    sed -i -e "s/${1}/${2}/" "${3}"
+}
+
+ 
+function exitWithError() {
+
+    #========================================================================
+    # Display and error message and exit
+    #========================================================================
+
+    echo "Could not update version numbers in ${1}... Exiting!"
+    exit 1
+}
+
+
+function main() {
+
+    #========================================================================
+    # Replaces the version number in the files listed.
+    #
+    # 1st argument: New version number to use
+    #========================================================================
+
+    # New version number
+    version="${1}"
+
+    # Save this directory path and change to root directory
+    thisDir=$(pwd -P)
+    cd ..
+
+    #========================================================================
+    # Update version numbers in various files
+    #========================================================================
+
+    # Pattern to match: X.Y.Z
+    pattern='[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*'
+
+    # List of files to replace
+    files=(                                                    \
+        "docs/source/conf.py"                                  \
+	"gcpy/_version.py"                                     \
+	"gcpy/benchmark/run_benchmark.py"                      \
+	"gcpy/benchmark/modules/run_1yr_fullchem_benchmark.py" \
+	"gcpy/benchmark/modules/run_1yr_tt_benchmark.py"       \
+    )
+
+    # Replace version numbers in files
+    for file in ${files[@]}; do
+        replace "${pattern}" "${version}" "${file}"
+        [[ $? -ne 0 ]] && exitWithError "${file}"
+        echo "GCPy version updated to ${version} in ${file}"
+    done
+
+    #========================================================================
+    # Update version number and date in CHANGELOG.md
+    #========================================================================
+
+    # Pattern to match: "[Unreleased] - TBD"
+    pattern='\[.*Unreleased.*\].*'
+    date=$(date -Idate)
+
+    # List of files to replace
+    file="CHANGELOG.md"
+
+    # Replace version numbers in files
+    replace "${pattern}" "\[${version}\] - ${date}" "${file}"
+    [[ $? -ne 0 ]] && exitWithError "${file}"
+    echo "GCClassic version updated to ${version} in ${file}"
+
+    #========================================================================
+    # Update version number in setup.py
+    #========================================================================
+
+    # Split version number into an array
+    IFS='.' read -r -a version_numbers <<< "${version}"
+
+    # File to replace
+    file="setup.py"
+
+    # Replace version numbers
+    replace "MAJOR =.." "MAJOR = ${version_numbers[0]}" "${file}"
+    [[ $? -ne 0 ]] && exitWithError "${file}"
+    replace "MINOR =.." "MINOR = ${version_numbers[1]}" "${file}"
+    [[ $? -ne 0 ]] && exitWithError "${file}"
+    replace "MICRO =.." "MICRO = ${version_numbers[2]}" "${file}"
+    [[ $? -ne 0 ]] && exitWithError "${file}"
+
+    echo "GCPy version updated to ${version} in ${file}"
+    
+    # Return to the starting directory
+    cd "${thisDir}"
+}
+
+# ---------------------------------------------------------------------------
+
+# Expect 1 argument, or exit with error
+if [[ $# -ne 1 ]]; then
+    echo "Usage: ./changeVersionNumbers.sh VERSION"
+    exit 1
+fi
+
+# Replace version numbers
+main "${1}"
+
+# Return status
+exit $?
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68426cc..cd0a102 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to GCPy will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased] - TBD
+### Added
+- Badge for `build-gcpy-environment` GitHub Action in `README.md`
+- Badges in `docs/source/index.rst`
+- GitHub action to push GCPy releases to PyPi
+- Script `./release/changeVersionNumbers.sh`, used to update version numbers in various files befor release
+
 ### Changed
 - Bump pip from 23.2.1 to 23.3 (dependabot suggested this)
 - Bump pypdf from 3.16.1 to 3.17.0 (dependabot suggested this)
diff --git a/README.md b/README.md
index 010e309..ae54a75 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,14 @@
 <p>
   <a href="https://github.com/geoschem/gcpy/releases"><img src="https://img.shields.io/github/v/release/geoschem/gcpy?include_prereleases&label=Latest%20Pre-Release"></a>
   <a href="https://github.com/geoschem/gcpy/releases"><img src="https://img.shields.io/github/v/release/geoschem/gcpy?label=Latest%20Stable%20Release"></a>
-  <a href="https://github.com/geoschem/gcpy/releases/"><img src="https://img.shields.io/github/release-date/geoschem/gcpy"></a>
-  <br />
   <a href="https://doi.org/10.5281/zenodo.3689589"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3689589.svg" alt="DOI"></a>
+ <br />
   <a href="https://github.com/geoschem/gcpy/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
+  <a href="https://github.com/geoschem/gcpy/releases/"><img src="https://img.shields.io/github/release-date/geoschem/gcpy"></a>
   <a href="https://gcpy.readthedocs.io/en/latest/"><img src="https://img.shields.io/readthedocs/gcpy?label=ReadTheDocs"></a>
+  <a href="https://github.com/geoschem/gcpy/actions/workflows/build-gcpy-environment.yml"><img src="https://github.com/geoschem/gcpy/actions/workflows/build-gcpy-environment.yml/badge.svg"></a>
 </p>
 
-
 **GCPy** is a Python-based toolkit containing useful functions for working specifically with the GEOS-Chem model of atmospheric chemistry and composition.
 
 **GCPy** aims to build on the well-established scientific Python technical stack, leveraging tools like **cartopy**, **numpy**, and **xarray** to simplify the task of working with GEOS-Chem model output and performing atmospheric chemistry analyses.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 1c38079..ce51ff9 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -2,6 +2,19 @@
 GCPy: GEOS-Chem Python toolkit
 ##############################
 
+.. raw:: html
+
+   <p>
+   <a href="https://github.com/geoschem/gcpy/releases"><img src="https://img.shields.io/github/v/release/geoschem/gcpy?include_prereleases&label=Latest%20Pre-Release"></a>
+   <a href="https://github.com/geoschem/gcpy/releases"><img src="https://img.shields.io/github/v/release/geoschem/gcpy?label=Latest%20Stable%20Release"></a>
+   <a href="https://doi.org/10.5281/zenodo.3689589"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.3689589.svg" alt="DOI"></a>
+   <br />
+   <a href="https://github.com/geoschem/gcpy/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
+   <a href="https://github.com/geoschem/gcpy/releases/"><img src="https://img.shields.io/github/release-date/geoschem/gcpy"></a>
+   <a href="https://gcpy.readthedocs.io/en/latest/"><img src="https://img.shields.io/readthedocs/gcpy?label=ReadTheDocs"></a>
+   <a href="https://github.com/geoschem/gcpy/actions/workflows/build-gcpy-environment.yml"><img src="https://github.com/geoschem/gcpy/actions/workflows/build-gcpy-environment.yml/badge.svg"></a>
+   </p>
+
 Welcome to the GCPy ReadTheDocs documentation! This site provides documentation
 on the functionality of GCPy and instructions for common use cases.
 


Test patch:
diff --git a/.github/workflows/build-test-environment.yml b/.github/workflows/build-test-environment.yml
index 42fdb1c..0941c28 100644
--- a/.github/workflows/build-test-environment.yml
+++ b/.github/workflows/build-test-environment.yml
@@ -20,7 +20,7 @@ jobs:
         python-version: ["3.9"]
     steps:
       - name: Checkout the GCPy repository
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
       - name: Create "testing" environment
         uses: mamba-org/setup-micromamba@v1
         with: