diff --git a/.github/workflows/cmake_build.yml b/.github/workflows/cmake_build.yml
index 0d30d7599..3592f95eb 100644
--- a/.github/workflows/cmake_build.yml
+++ b/.github/workflows/cmake_build.yml
@@ -1,92 +1,60 @@
-#
-# CMake based build for Audacity
-#
 name: CMake Build
 
-#
-# Only execute on "git push" actions
-#
 on:
   push:
-    # Remove the "#" from the next 2 lines if you need to disable this action
-    #branches:
-    #  - disable
   pull_request:
-    # Remove the "#" from the next 2 lines if you need to disable this action
-    #branches:
-    #  - disable
 
-#
-# Global environment variables
-#
-env:
-  WXURL: https://github.com/audacity/wxWidgets
-  WXREF: audacity-fixes-3.1.3
-  WXWIN: ${{ github.workspace }}/wxwin
-  # As of 2021/01/01, github is using Xcode 12.2 as the default and
-  # it has a bug in the install_name_tool.  So explicitly use 12.3
-  # instead.
-  DEVELOPER_DIR: /Applications/Xcode_12.3.app/Contents/Developer
-  CONAN_USER_HOME: "${{ github.workspace }}/conan-home/"
-  CONAN_USER_HOME_SHORT: "${{ github.workspace }}/conan-home/short"
-#
-# Define our job(s)
-#
+defaults:
+  run:
+    shell: bash
+
 jobs:
   build:
     name: ${{ matrix.config.name }}
     runs-on: ${{ matrix.config.os }}
+    env:
+      AUDACITY_CMAKE_GENERATOR: ${{ matrix.config.generator }}
+      AUDACITY_ARCH_LABEL: ${{ matrix.config.arch }}
     strategy:
       fail-fast: false
       matrix:
         config:
-        - {
-            name: "Windows_32bit",
-            os: windows-latest,
-            generator: "Visual Studio 16 2019",
-            platform: "Win32"
-          }
-        - {
-            name: "Windows_64bit",
-            os: windows-latest,
-            generator: "Visual Studio 16 2019",
-            platform: "x64"
-          }
-        - {
-            name: "Ubuntu_18.04",
-            os: ubuntu-18.04,
-            generator: "Unix Makefiles"
-          }
-        - {
-            name: "macOS",
-            os: macos-latest,
-            generator: "Xcode"
-          }
+
+        - name: Ubuntu_18.04
+          os: ubuntu-18.04
+          arch: x86_64 # as reported by `arch` or `uname -m`
+          generator: Unix Makefiles
+
+        - name: macOS_Intel
+          os: macos-latest
+          arch: Intel # as reported by Apple menu > About This Mac
+          generator: Xcode
+
+        - name: Windows_32bit
+          os: windows-latest
+          arch: 32bit # as reported by Windows Settings > System > About
+          generator: Visual Studio 16 2019
+
+        - name: Windows_64bit
+          os: windows-latest
+          arch: 64bit # as reported by Windows Settings > System > About
+          generator: Visual Studio 16 2019
 
     steps:
-    # =========================================================================
-    # SHARED: Checkout source
-    # =========================================================================
+
     - name: Checkout
       uses: actions/checkout@v2
-    #  with:
-    #    ref: master   
-    # =========================================================================
-    # SHARED: Checkout source
-    # =========================================================================
-    - name: Calculate short hash
-      shell: bash
-      run: |
-        set -x
-        # Get the short hash
-        shorthash=$(git show -s --format='%h')
-        # Export the short hash for the upload step
-        echo "SHORTHASH=${shorthash}" >> ${GITHUB_ENV}
-        # Export the destination directory name
-        echo "DEST=${{matrix.config.name}}_${shorthash}" >> ${GITHUB_ENV}
 
-    - name: GitHub Action Cache for .conan
-      id: github-cache-conan
+    - name: Dependencies
+      run: |
+        exec bash "scripts/ci/dependencies.sh"
+
+    - name: Environment
+      run: |
+        source "scripts/ci/environment.sh"
+
+    - name: Cache for .conan
+      id: cache-conan
       uses: actions/cache@v2
       env:
         cache-name: cache-conan-modules
@@ -95,146 +63,32 @@ jobs:
         key: host-${{ matrix.config.name }}-${{ hashFiles('cmake-proxies/CMakeLists.txt') }}
         restore-keys: |
           host-${{ matrix.config.name }}-
-    - name: Check Sentry secrets
+
+    - name: Configure
       env:
         SENTRY_DSN_KEY: ${{ secrets.SENTRY_DSN_KEY }}
         SENTRY_HOST: ${{ secrets.SENTRY_HOST }}
         SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
-      if: ${{ env.SENTRY_DSN_KEY != '' && env.SENTRY_HOST != '' && env.SENTRY_PROJECT != '' }}
-      shell: bash
       run: |
-        echo "SENTRY_PARAMETERS<<EOF" >> $GITHUB_ENV
-        echo "-DSENTRY_DSN_KEY=${SENTRY_DSN_KEY} -DSENTRY_HOST=${SENTRY_HOST} -DSENTRY_PROJECT=${SENTRY_PROJECT}" >> $GITHUB_ENV
-        echo "EOF" >> $GITHUB_ENV
+        exec bash "scripts/ci/configure.sh"
 
-    # =========================================================================
-    # WINDOWS: Build (for all versions of Windows)
-    # =========================================================================
-    - name: Build for Windows
-      if: startswith( matrix.config.os, 'windows' )
-      shell: bash
+    - name: Build
       run: |
-        set -x
-        pip install conan
-        conan --version
+        exec bash "scripts/ci/build.sh"
 
-        # Configure Audacity
-        #
-        cmake -S . \
-              -B build \
-              -G "${{matrix.config.generator}}" \
-              -A ${{matrix.config.platform}} \
-              -D audacity_use_pch=no \
-              -D audacity_has_networking=yes ${{ env.SENTRY_PARAMETERS }}
-
-        # Build Audacity
-        cmake --build build --config Release --verbose
-
-        # "Install" Audacity
-        mkdir -p "${DEST}"
-        cp -a build/bin/Release/* "${DEST}"
-        rm -f "${DEST}"/{*.iobj,*.ipdb}
-
-        # Create artifact (zipped as Github actions don't preserve permissions)
-        cmake -E tar c "${GITHUB_SHA}.zip" --format=zip "${DEST}"
-
-    # =========================================================================
-    # MACOS: Build (for all versions of MacOS)
-    # =========================================================================
-    - name: Build for macOS
-      if: startswith( matrix.config.os, 'macos' )
-      shell: bash
+    - name: Install
       run: |
-        set -x
+        exec bash "scripts/ci/install.sh"
 
-        # Setup environment
-        export PATH="/usr/local/bin:${PATH}"
-        export DYLD_LIBRARY_PATH="/usr/local/lib"
-
-        # Install required packages
-        brew install gettext
-        brew link --force gettext
-
-        brew install conan
-        conan --version
-
-        # Configure Audacity
-        cmake -S . \
-              -B build \
-              -T buildsystem=1 \
-              -G "${{matrix.config.generator}}" \
-              -D audacity_use_pch=no \
-              -D audacity_has_networking=yes ${{ env.SENTRY_PARAMETERS }}
-
-        # Build Audacity
-        cmake --build build --config Release
-
-        # "Install" Audacity
-        mkdir -p "${DEST}"
-        cp -a build/bin/Release/ "${DEST}"
-
-        # Create artifact (zipped as Github actions don't preserve permissions)
-        cmake -E tar c "${GITHUB_SHA}.zip" --format=zip "${DEST}"
-
-    # =========================================================================
-    # UBUNTU: Build (for all versions of Ubuntu)
-    # =========================================================================
-    - name: Build for Ubuntu
-      if: startswith( matrix.config.os, 'ubuntu' )
-      shell: bash
+    - name: Package
       run: |
-        set -x
+        exec bash "scripts/ci/package.sh"
 
-        # Setup environment
-        export PATH="/usr/local/bin:${PATH}"
-        export LD_LIBRARY_PATH="/usr/local/lib"
-
-        # Install required packages
-        sudo apt-get update -y
-        sudo apt-get install -y libgtk2.0-dev libasound2-dev gettext python3-pip
-        sudo apt-get remove -y ccache
-
-        pip3 install wheel setuptools
-        pip3 install conan
-
-        conan --version
-
-        # Configure Audacity
-        cmake -S . \
-              -B build \
-              -G "${{matrix.config.generator}}" \
-              -D audacity_use_pch=no \
-              -D audacity_has_networking=yes ${{ env.SENTRY_PARAMETERS }} 
-
-        # Build Audacity
-        cmake --build build --config Release
-
-        # "Install" Audacity
-        cmake --install build --config Release --prefix "${DEST}"
-
-        # Create the lib directory
-        mkdir -p ${DEST}/lib
-
-        # Create wrapper script
-        cat >"${DEST}/audacity" <<"EOF"
-        #!/bin/sh
-        lib="${0%/*}/lib/audacity"
-        export LD_LIBRARY_PATH="${lib}:${LD_LIBRARY_PATH}"
-        export AUDACITY_MODULES_PATH="${lib}/modules"
-        "${0%/*}/bin/audacity"
-
-        EOF
-        chmod +x "${DEST}/audacity"
-
-        # Create artifact (zipped as Github actions don't preserve permissions)
-        cmake -E tar c "${GITHUB_SHA}.zip" --format=zip "${DEST}"
-
-    # =========================================================================
-    # SHARED: Attach the artifact to the workflow results
-    # =========================================================================
     - name: Upload artifact
-      uses: actions/upload-artifact@v1
+      uses: actions/upload-artifact@v2
       with:
-        name: ${{ matrix.config.name }}_${{ env.SHORTHASH }}
-        path: ${{ github.sha }}.zip
-
+        name: Audacity_${{ matrix.config.name }}_${{ github.run_id }}_${{ env.GIT_HASH_SHORT }}
+        path: |
+          build/package/*
+          !build/package/_CPack_Packages
+        if-no-files-found: error
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b5e50bb0f..fee909b3f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -168,8 +168,12 @@ include( CMakePushCheckState )
 include( GNUInstallDirs )
 include( TestBigEndian )
 
+set_from_env(SENTRY_DSN_KEY)
+set_from_env(SENTRY_HOST)
+set_from_env(SENTRY_PROJECT)
+
 cmake_dependent_option(
-   ${_OPT}has_sentry_reporting 
+   ${_OPT}has_sentry_reporting
    "Build support for sending errors to Sentry"
    On
    "${_OPT}has_networking;DEFINED SENTRY_DSN_KEY;DEFINED SENTRY_HOST;DEFINED SENTRY_PROJECT"
@@ -525,3 +529,4 @@ execute_process( COMMAND
    print_properties( TARGET "wxWidgets" )
 #]]
 
+include( Package ) # do this last
diff --git a/cmake-proxies/cmake-modules/AudacityFunctions.cmake b/cmake-proxies/cmake-modules/AudacityFunctions.cmake
index 165101289..199fcb878 100644
--- a/cmake-proxies/cmake-modules/AudacityFunctions.cmake
+++ b/cmake-proxies/cmake-modules/AudacityFunctions.cmake
@@ -74,6 +74,15 @@ macro( set_cache_value var value )
    set_property( CACHE ${var} PROPERTY VALUE "${value}" )
 endmacro()
 
+# Set a CMake variable to the value of the corresponding environment variable
+# if the CMake variable is not already defined. Any addition arguments after
+# the variable name are passed through to set().
+macro( set_from_env var )
+   if( NOT DEFINED ${var} AND NOT "$ENV{${var}}" STREQUAL "" )
+      set( ${var} "$ENV{${var}}" ${ARGN} ) # pass additional args (e.g. CACHE)
+   endif()
+endmacro()
+
 # Set the given property and its config specific brethren to the same value
 function( set_target_property_all target property value )
    set_target_properties( "${target}" PROPERTIES "${property}" "${value}" )
diff --git a/cmake-proxies/cmake-modules/Package.cmake b/cmake-proxies/cmake-modules/Package.cmake
new file mode 100644
index 000000000..84b95e969
--- /dev/null
+++ b/cmake-proxies/cmake-modules/Package.cmake
@@ -0,0 +1,25 @@
+
+set(CPACK_PACKAGE_VERSION_MAJOR "${AUDACITY_VERSION}") # X
+set(CPACK_PACKAGE_VERSION_MINOR "${AUDACITY_RELEASE}") # Y
+set(CPACK_PACKAGE_VERSION_PATCH "${AUDACITY_REVISION}") # Z
+
+# X.Y.Z-alpha-20210615
+set(CPACK_PACKAGE_VERSION "${AUDACITY_VERSION}.${AUDACITY_RELEASE}.${AUDACITY_REVISION}${AUDACITY_SUFFIX}")
+
+if(NOT AUDACITY_BUILD_LEVEL EQUAL 2)
+   # X.Y.Z-alpha-20210615+a1b2c3d
+   set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION}+${GIT_COMMIT_SHORT}")
+endif()
+
+# Audacity-X.Y.Z-alpha-20210615
+set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}")
+
+if(NOT "$ENV{AUDACITY_ARCH_LABEL}" STREQUAL "")
+   # Audacity-X.Y.Z-alpha-20210615-x86_64
+   set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_FILE_NAME}-$ENV{AUDACITY_ARCH_LABEL}")
+endif()
+set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/package")
+
+set(CPACK_GENERATOR ZIP)
+
+include(CPack) # do this last
diff --git a/linux/audacity.sh b/linux/audacity.sh
new file mode 100755
index 000000000..aab7bb102
--- /dev/null
+++ b/linux/audacity.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+lib="${0%/*}/lib/audacity"
+
+export LD_LIBRARY_PATH="${lib}:${LD_LIBRARY_PATH}"
+export AUDACITY_MODULES_PATH="${lib}/modules"
+
+exec "${0%/*}/bin/audacity" "$@"
diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh
new file mode 100755
index 000000000..48262b3f2
--- /dev/null
+++ b/scripts/ci/build.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "$0: Error: Please upgrade Bash."; exit 1; }
+
+set -euxo pipefail
+
+if [[ "${OSTYPE}" == msys* ]]; then # Windows
+
+    cpus="${NUMBER_OF_PROCESSORS}"
+
+elif [[ "${OSTYPE}" == darwin* ]]; then # macOS
+
+    cpus="$(sysctl -n hw.ncpu)"
+
+else # Linux & others
+
+    cpus="$(nproc)"
+
+fi
+
+# Build Audacity
+cmake --build build -j "${cpus}" --config "${AUDACITY_BUILD_TYPE}"
diff --git a/scripts/ci/configure.sh b/scripts/ci/configure.sh
new file mode 100755
index 000000000..a01644f50
--- /dev/null
+++ b/scripts/ci/configure.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "$0: Error: Please upgrade Bash."; exit 1; }
+
+set -euxo pipefail
+
+conan --version # check it works
+
+cmake_args=(
+    -S .
+    -B build
+    -G "${AUDACITY_CMAKE_GENERATOR}"
+    -D audacity_use_pch=no
+    -D audacity_has_networking=yes
+    -D CMAKE_BUILD_TYPE="${AUDACITY_BUILD_TYPE}"
+    -D CMAKE_INSTALL_PREFIX="${AUDACITY_INSTALL_PREFIX}"
+)
+
+if [[ "${AUDACITY_CMAKE_GENERATOR}" == "Visual Studio"* ]]; then
+    case "${AUDACITY_ARCH_LABEL}" in
+    32bit)  cmake_args+=( -A Win32 ) ;;
+    64bit)  cmake_args+=( -A x64 ) ;;
+    *)      echo >&2 "$0: Unrecognised arch label '${AUDACITY_ARCH_LABEL}'" ; exit 1 ;;
+    esac
+elif [[ "${AUDACITY_CMAKE_GENERATOR}" == Xcode* ]]; then
+    cmake_args+=(
+        -T buildsystem=1
+    )
+fi
+
+# Configure Audacity
+cmake "${cmake_args[@]}"
diff --git a/scripts/ci/dependencies.sh b/scripts/ci/dependencies.sh
new file mode 100755
index 000000000..b96eb40eb
--- /dev/null
+++ b/scripts/ci/dependencies.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+((${BASH_VERSION%%.*} >= 4)) || echo >&2 "$0: Warning: Using ancient Bash version ${BASH_VERSION}."
+
+set -euxo pipefail
+
+if [[ "${OSTYPE}" == msys* ]]; then # Windows
+
+    # Python packages
+    pip_packages=(
+        conan
+    )
+    pip3 install "${pip_packages[@]}"
+
+elif [[ "${OSTYPE}" == darwin* ]]; then # macOS
+
+    # Homebrew packages
+    brew_packages=(
+        bash # macOS ships with Bash v3 for licensing reasons so upgrade it now
+        conan
+    )
+    brew install "${brew_packages[@]}"
+
+else # Linux & others
+
+    # Distribution packages
+    if which apt-get; then
+        apt_packages=(
+            libasound2-dev
+            libgtk2.0-dev
+            gettext
+            python3-pip
+        )
+        sudo apt-get update -y
+        sudo apt-get install -y --no-install-recommends "${apt_packages[@]}"
+        sudo apt-get remove -y ccache
+    else
+        echo >&2 "$0: Error: You don't have a recognized package manager installed."
+        exit 1
+    fi
+
+    # Python packages
+    pip_packages=(
+        conan
+    )
+    pip3 install wheel setuptools # need these first to install other packages (e.g. conan)
+    pip3 install "${pip_packages[@]}"
+
+fi
diff --git a/scripts/ci/environment.sh b/scripts/ci/environment.sh
new file mode 100644
index 000000000..fd426eccc
--- /dev/null
+++ b/scripts/ci/environment.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+if [[ "$0" == "${BASH_SOURCE}" ]]; then
+    echo >&2 "$0: Please source this script instead of running it."
+    exit 1
+fi
+
+((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "${BASH_SOURCE}: Error: Please upgrade Bash."; return 1; }
+
+function gh_export()
+{
+    [[ "${GITHUB_ENV-}" ]] || local -r GITHUB_ENV="/dev/null"
+    export -- "$@" && printf "%s\n" "$@" >> "${GITHUB_ENV}"
+}
+
+repository_root="$(cd "$(dirname "${BASH_SOURCE}")/../.."; echo "${PWD}")"
+
+gh_export CONAN_USER_HOME="${repository_root}/conan-home/"
+gh_export CONAN_USER_HOME_SHORT="${repository_root}/conan-home/short"
+
+gh_export GIT_HASH="$(git show -s --format='%H')"
+gh_export GIT_HASH_SHORT="$(git show -s --format='%h')"
+
+gh_export AUDACITY_BUILD_TYPE="Release"
+gh_export AUDACITY_INSTALL_PREFIX="${repository_root}/build/install"
diff --git a/scripts/ci/install.sh b/scripts/ci/install.sh
new file mode 100755
index 000000000..d302b0ef5
--- /dev/null
+++ b/scripts/ci/install.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "$0: Error: Please upgrade Bash."; exit 1; }
+
+set -euxo pipefail
+
+# Install Audacity
+cmake --install build --config "${AUDACITY_BUILD_TYPE}" --verbose
diff --git a/scripts/ci/package.sh b/scripts/ci/package.sh
new file mode 100755
index 000000000..38d6e80a7
--- /dev/null
+++ b/scripts/ci/package.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+((${BASH_VERSION%%.*} >= 4)) || { echo >&2 "$0: Error: Please upgrade Bash."; exit 1; }
+
+set -euxo pipefail
+
+cd build
+cpack -C "${AUDACITY_BUILD_TYPE}" --verbose
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 90a878c61..b35934665 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1339,7 +1339,15 @@ if( CMAKE_VERSION VERSION_GREATER_EQUAL "3.16" AND NOT CCACHE_PROGRAM )
    endif()
 endif()
 
-if( NOT "${CMAKE_GENERATOR}" MATCHES "Xcode|Visual Studio*" )
+if( "${CMAKE_GENERATOR}" MATCHES "Xcode|Visual Studio*" )
+   install(
+      DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$<CONFIG>/"
+      DESTINATION "."
+      USE_SOURCE_PERMISSIONS
+      PATTERN "*.pdb" EXCLUDE
+      PATTERN "*.ilk" EXCLUDE
+   )
+else()
    if( CMAKE_SYSTEM_NAME MATCHES "Darwin" )
       install( TARGETS ${TARGET}
                DESTINATION "."
@@ -1358,6 +1366,9 @@ if( NOT "${CMAKE_GENERATOR}" MATCHES "Xcode|Visual Studio*" )
                DESTINATION "${_DATADIR}/mime/packages" )
       install( FILES "${topdir}/presets/EQDefaultCurves.xml"
                DESTINATION "${_PKGDATA}" )
+      install( PROGRAMS "${PROJECT_SOURCE_DIR}/linux/audacity.sh"
+               DESTINATION "."
+               RENAME "audacity" )
    endif()
 endif()