CI/CD and Releases#

CI/CD stands for continuous integration/continuous development. It is a practice that is meant to allow code to be release as frequently as possible. The continuous integration encourages code to be frequently merged into main and continuous deployment encourages stable builds to be made frequently. Currently, the deployment process for WISER is run manually, but we are looking into running it nightly or more frequently.

Continuous Integration#

In WISER we will be practicing continuous integration. Read this to learn more. That article has a lot of useful information on branching strategies if you want to read more.

WISER currently has two main github actions to increase code quality: linting/formatting and development tests. We use ruff to do our linting and formatting because it is fast. For testing, we use pytest. Read on for more information on how the these are set up.

Linting / Formatting#

As previously said, we use ruff to do linting and formatting. We control the rules for this in pyproject.toml. You will see that there are a few rules that we ignore for linting. Read the comments in this PR for more information.

Linting can be done with the command ruff check .. If there are any safe fixes that can be done do ruff check --fix .. You should not do ruff check --fix --unsafe-fixes . unless you are very confident.

Formatting can be done with ruff format. Note that the command ruff format will change files in order to make the formatting changes. If you do not want to do this and you just want to check to see if things are formatted correctly, do ruff format --check.

There is a pre-commit hook to perform the linting and formatting. However, you have to install it the pre-commit first. Pre-commit hooks that are defined in .pre-commit-config are automatically installed if you run the command make install-dev-env in the etc/ folder. You can also run the commands pre-commit clean then pre-commit install while you’re in the dev environment.

The linting / formatting is also done on push and pull-request to main and release branches rel/**.

Dev Testing#

Testing is only done on push and pull-request to main and release branches rel/**. We use micromamba to make the conda environment to run our tests. Micromamba is great because it is fast to make the environment and can be quickly pulled from cache. The cache is made from the file etc/dev-conda-lock.yml and will update when this file is changed. The tests use the cache and also update the cache, so there are two rules that update the micromamba environment cache. When running tests, we simply cd into src/tests and run the command pytest -s ..

Testing happens after linting.

Continuous Deployment#

We are still figuring out what type of continuous deployment framework that we want for WISER. Currently, the build process takes a pretty long time (around 24 minutes for the longest build). This seems a bit infeasible for developers to run on each push to a pull request. We need to look into a way to speed up our builds (<10 minutes would be good).

The deployment code is in the GitHub Actions workflow prod-deploy.yml. It builds WISER on every platform (Windows, macOS arm/intel, and the six Linux distro/arch targets). The Linux legs run the full --test_mode suite inside each distributable (a hard gate — a failing suite produces no tarball); Windows and macOS run a fast --smoke launch check in CI and the full --test_mode locally. Running these checks ensures WISER actually starts up and that core functionality was packaged correctly.

It is important to note that these build artifacts still need some more work done to them in order to become our distributables. The Windows artifact needs to be code-signed then packaged into an installer. The MacOS artifacts need to be packaged into a .dmg and code-signed.

“Production” Testing#

Currently, we test on our distributables in the github runners by running WISER with the --test_mode flag. We do this because we want to make sure everything in our distributables was packaged up correctly. For example, to test that JP2OpenJPEG.dll was properly packaged into the build, we would have a test that would try opening up a very small JP2 file that we can run with a command like ./WISER_Bin --mode test. So you can see how it is important to run tests on the build artifact.

Note that “smoke test” means two different things in our pipeline. The Linux builds run the full --test_mode suite in-container via install-linux/run_smoke.sh (despite the name) — a failing suite fails the build and no tarball is produced. Windows/macOS run only a lightweight --smoke launch check in CI (start Qt, show the splash, exit) and rely on the full --test_mode being run locally before signing. Don’t “standardize” the Linux path onto --smoke — that would silently drop the Linux test gate.

We want to build these artifacts and run these tests on main and rel/** banches because these branches are meant to contain very stable code that can be deployed at a moments notice. However, currently we are still deciding whether we should run deployment tests on every PR to main and rel/** or nightly.

Deployment Tests#

We currently have a github action that builds WISER on the github runners then runs our smoke tests then uploads distribution files to github. Currently, this only happens by workflow_dispatch, which means a developer must manually make it run.

There are also simple make recipes to build and test wiser locally. The make commands smoke-test-win-build and smoke-test-mac-build when run from the root directory build WISER and run the --test_mode locally. This is useful to find build bugs as well.

This github action is under .github\workflows\prod-deploy.yml .

Deployment Test Signing#

The good thing about the pipeline in the Deployment Tests section is that we can deploy directly from it. Since the artifacts from these tests were built on a fresh machine (the github runner), we know its more reproducible than building it locally! All we have to do is pull the artifact down to our local machine and sign it. I have made code to do this.

You can run this code by going into the root directory and doing

make sign-mac LINK=<link-address-of-artifact> MAC_DIST_GITHUB_NAME=<artifact-github-name>

or simply

make sign-windows LINK=<link-address-of-artifact>

for windows. This step requires you to have Github’s CLI tool installed which lets you use the command gh. The logic for this step is in the files /src/devtools/sign_mac.py and /src/devtools/sign_windows.py. It was originally introduced on this branch.

Releases#

WISER releases should always be made from a release branch and tagged through the GitHub Releases feature. Release notes should accompany every release.

The installers are attached to the GitHub release as release assets (not linked from an Actions run — Actions artifacts expire, require a login, and are not counted in a release’s download totals). Release notes should point users at the assets on the release page, never at a …/actions/runs/<id> URL.

Asset naming convention#

Asset filenames are normalized at build time so a platform + architecture is always identifiable from the name (the public downloads page groups assets by these tokens):

WISER-<version>-windows-x64-setup.exe
WISER-<version>-macos-arm64.dmg
WISER-<version>-macos-x64.dmg
WISER-<version>-linux-<distro>-x64.tar.gz     # distro: ubuntu2004 | debian11 | fedora39
WISER-<version>-linux-<distro>-arm64.tar.gz

x64/arm64 are the canonical arch tokens (Intel/amd64 → x64). The names are set by win-install.nsi (Windows), src/devtools/sign_mac.py (macOS), and the Linux build step in prod-deploy.yml — not by hand on upload.

How assets reach the release#

Assets are built once and promoted — the release event never triggers a build, so the bits you ship are the exact bits you tested.

  • Linux builds are unsigned, so a CI tarball is the final asset. prod-deploy.yml (run via workflow_dispatch) builds and tests each distro and uploads the tarballs as run artifacts. When you are ready to release, publish-release-assets.yml attaches the tarballs from that specific run to the release — it verifies the run succeeded and that the tag points at the built commit, so nothing is rebuilt at release time.

  • Windows / macOS are signed locally with the maintainer’s certificates, then uploaded with the canonical name via the sign scripts’ --release-tag <tag> option (see the Release Process below).

Backup to Nexus#

Release assets exist in exactly one place — the GitHub release — and that is a single point of failure for every binary WISER has ever shipped. Back up release assets to Nexus (backup-releases-to-nexus.yml) copies each release into the LASP Nexus raw repository at https://lasp.colorado.edu/repository/wiser-raw/, so the installers survive loss of the GitHub repository. GitHub stays the place users download from; Nexus is the archive behind it.

Each release is stored under its tag, alongside the metadata needed to make the copy usable on its own:

releases/<tag>/<asset name>                    # exactly as attached to the release
releases/<tag>/source/WISER-<tag>-source.tar.gz
releases/<tag>/source/WISER-<tag>-source.zip
releases/<tag>/manifest.json                   # commit sha, dates, sizes, per-asset SHA-256
releases/<tag>/SHA256SUMS                      # verify with `sha256sum -c SHA256SUMS`
releases/<tag>/release-notes.md

The workflow runs on three triggers:

  • released — when a release’s pre-release flag is cleared. This is deliberately not published: the process below publishes a release before attaching installers, so published would fire while the release is still empty.

  • Weekly (Mondays 07:00 UTC) — re-checks the whole release history and uploads only what is missing, so a failed or skipped run repairs itself instead of leaving a silent gap.

  • workflow_dispatch — for backfilling a tag or the full history, with dry run and force options.

Uploads are idempotent: an asset already in Nexus at the right size is skipped, so re-running is cheap. Every upload is verified against the SHA-256 GitHub records for the asset and the size and SHA-1 Nexus reports back; a mismatch fails the run rather than logging a warning.

The same logic runs by hand for a backfill or a spot check — see make backup-releases:

make backup-releases TAG=v3.0b0
make backup-releases ALL=1 DRY_RUN=1     # report what is missing, upload nothing

Credentials come from NEXUS_USERNAME / NEXUS_PASSWORD in Secret.mk or the environment. Configuring the Nexus side is covered in The Nexus release archive.

Pre-release flag#

The web …/releases/latest and the API latest endpoint both exclude releases flagged prerelease. Our beta tags (e.g. v2.2b1) are not automatically treated as pre-releases — it is purely the checkbox at publish time. When publishing the release the public should download, leave “Set as a pre-release” unchecked (and keep “Set as the latest release” on) so latest resolves to it.

Release Process#

Assets are built once, tested, then promoted to the release — nothing is rebuilt at release time, so what ships is exactly what you tested.

  1. Bump the version in src/wiser/version.py (and RELEASE_DATE) and commit it. The Linux asset name is read from version.py at build time, so it must be correct before you build.

  2. Verify app metadata is current:

    • Check the year. Ctrl+F for whatever the current year is throughout the app and go through each hit to make sure the year is correct — some may not need to be changed. (We should turn this into a single variable in the future.)

    • Check the feature flags. Search for FLAGS. and check feature_flags.py, confirming the FEATURE_GATES dictionary is correct. Then check app_config.py and confirm everything referencing feature_flags. is correct.

  3. Build and test — run Build and Smoke Test WISER (prod-deploy.yml) via workflow_dispatch on the commit you intend to release. Every platform builds; each Linux distro runs the full --test_mode suite in-container (a leg only produces a tarball if its tests pass), and Windows/macOS run a --smoke launch check. Note the run ID — you will promote this exact run.

  4. Verify locally — download the Linux tarballs from the run and test if needed. Build, --test_mode, code-sign, and clean-install-test the Windows/macOS installers locally (CI does not produce the signed installers). See the signing certificate requirements below.

  5. Create the release on the same commit you built. Publish it as a pre-release (see the Pre-release flag section) so it stays out of …/releases/latest while you attach and verify assets. A published pre-release also lets the publish workflow confirm the tag matches the built commit.

  6. Attach the Linux assets — run Publish release assets (publish-release-assets.yml) with the build run ID and the release tag. It checks the run succeeded and that the tag points at the built commit, then attaches the six Linux tarballs.

  7. Upload the signed Windows/macOS installers with RELEASE_TAG=<tag> so each is uploaded to the release with its canonical name in one step:

    make sign-mac LINK=<artifact-url> MAC_DIST_GITHUB_NAME=<artifact-name> RELEASE_TAG=<tag> or make sign-windows LINK=<artifact-url> RELEASE_TAG=<tag>.

    For signing on macOS, you must have a valid Apple Developer signing certificate tied to a paid Apple Developer Program account. For signing on Windows, you must have a Windows code signing certificate, tied to an individual or a legal entity.

    a. If you code-sign your own distribution, do not present it to others as an official WISER release unless you have been explicitly allowed to for a specific release.

    b. WISER has no code-signing mechanism for Linux; the Linux assets are attached unsigned.

  8. Finalize the release notes on the GitHub release. Point users at the attached release assets — never a …/actions/runs/<id> URL.

  9. Go live — once every asset is present with its canonical name, uncheck “Set as a pre-release” (and keep “Set as the latest release” on) so latest resolves to it.

  10. Confirm the Nexus backup — clearing the pre-release flag triggers Back up release assets to Nexus. Check that the run succeeded and that every asset shows as uploaded in its job summary. If it did not fire, dispatch it manually with the tag. See Backup to Nexus.

  11. Update the plugin API documentation in doc/sphinx-general-wiser-docs/source/extending-wiser/ to reflect any changes to plugin interfaces or dependencies in the latest version.

  12. Announce — if you have permission, email wiser-announcements@lists.lasp.colorado.edu with a summary of the release. If not, reach out to someone who deals with the email you want sent.

Note: This process can only be done by a maintainer with access to all of these resources. This intentionally limits who can do official WISER releases. If the community thinks that an official WISER release should be done, please reach out to the maintainers.

The Nexus release archive#

The archive is a raw hosted repository named wiser-raw on the LASP Nexus, holding a copy of every release asset WISER has published. Raw is the right format because these are opaque binaries — .dmg, .exe, .tar, .zip — with no package semantics for Nexus to interpret; a format like maven or npm would force a coordinate scheme that means nothing here.

This section is for whoever administers the Nexus side. The workflow that fills the archive is described in Backup to Nexus.

Two hostnames, and why it matters#

The server answers to two names, and CI can only use one of them:

reachable from

serves

artifacts.pdmz.lasp.colorado.edu

LASP network / VPN only

the web UI and the full REST API

lasp.colorado.edu/repository/…

the public internet

repository paths only

Despite the pdmz in its name, the internal host is not in public DNS and resolves to a private (RFC1918) address, so a GitHub-hosted runner cannot reach it. Everything automated must go through https://lasp.colorado.edu/repository/wiser-raw/.

The public proxy exposes /repository/ but not /service/rest/, so the Nexus REST API — search, component listing, repository administration — is unavailable from CI. This is why the backup tooling uses only PUT, GET, and HEAD against the raw repository. Anything you write against this archive should assume the same constraint.

Creating the repository#

In the Nexus UI (⚙ → Repository → Repositories → Create repository → raw (hosted)):

  • Name: wiser-raw

  • Blob store: the default, or a dedicated store if you want the archive’s usage reported separately. Budget roughly 12 GiB for the existing history and about 4 GiB per future release.

  • Deployment policy: Allow redeploy. The workflow refreshes manifest.json and SHA256SUMS on every run, which Disable redeploy would reject. Immutability comes from the CI account having no delete privilege (below) plus the workflow skipping assets that are already present, rather than from the deployment policy.

  • Cleanup policies: none. Cleanup policies delete components by age or by last-download date, which is precisely wrong for an archive whose purpose is to retain old releases that nobody downloads. Attaching one silently defeats the backup.

The CI account#

Create a dedicated local user (Security → Users → Create local user), for example wiser-ci, and a role (Security → Roles) granting exactly these privileges:

nx-repository-view-raw-wiser-raw-add
nx-repository-view-raw-wiser-raw-browse
nx-repository-view-raw-wiser-raw-edit
nx-repository-view-raw-wiser-raw-read

Note what is absent: no -delete. CI can add and update, never remove. A leaked CI credential then cannot destroy the archive, which is the failure mode that matters most for a backup.

This Nexus is licensed as Pro, so prefer a user token over the account password. Enable Security → Realms → User Token Realm, sign in as the service account, and generate its token under the account menu. The token’s name and passcode are used exactly like a username and password in HTTP Basic auth. If your Nexus expires user tokens, put a reminder on the calendar — an expired token surfaces as a 401 in the weekly backup run.

GitHub configuration#

In Settings → Secrets and variables → Actions on the WISER repository:

kind

name

value

secret

NEXUS_USERNAME

the user-token name code (or the account username)

secret

NEXUS_PASSWORD

the user-token passcode (or the account password)

variable (optional)

NEXUS_RAW_URL

overrides the repository URL if it ever moves

Secrets are not exposed to workflows triggered by pull requests from forks, and this workflow has no pull_request trigger, so a fork cannot reach these credentials.

Verifying the setup#

A read needs no credentials; a write does. From anywhere on the internet:

# read back a file the backup wrote
curl -I https://lasp.colorado.edu/repository/wiser-raw/releases/v3.0b0/SHA256SUMS

# confirm the CI account can write
printf 'ok\n' | curl -u "$NEXUS_USERNAME:$NEXUS_PASSWORD" --upload-file - \
  https://lasp.colorado.edu/repository/wiser-raw/.probe/hello.txt

An unauthenticated PUT returns 401 with a WWW-Authenticate: BASIC challenge — that is the expected response, and confirms the proxy forwards writes rather than blocking them.

Decide deliberately whether anonymous read stays enabled. The assets are already public on GitHub, so anonymous read costs nothing and keeps restores simple. Disabling it means every restore, and every dry run of the backup tool, needs credentials.

Restoring from the archive#

Each tag directory is self-describing, so a restore needs no tooling beyond curl:

tag=v3.0b0
base=https://lasp.colorado.edu/repository/wiser-raw/releases/$tag

curl -sO "$base/SHA256SUMS"
curl -s "$base/manifest.json" | python3 -m json.tool   # commit sha, dates, sizes
awk '{print $2}' SHA256SUMS | while read -r name; do curl -sO "$base/$name"; done
sha256sum -c SHA256SUMS

manifest.json records the commit each release was built from, so an archived binary can always be traced back to its source revision.

GitHub Actions Environment Setup#

Creating the conda environment on each CI run is the main performance bottleneck. Approaches ranked slowest to fastest:

1. Create from scratch every run (~7.5 min)#

- uses: conda-incubator/setup-miniconda@v3
  with:
    auto-update-conda: true
    activate-environment: wiser-source
    environment-file: etc/testing.yml
    channels: conda-forge
    auto-activate-base: false

2. Cache conda packages, then create env (~4.5 min)#

- name: Setup Cached Conda Environment
  uses: actions/cache@v4
  env:
    CACHE_NUMBER: 0
  id: cache
  with:
    path: ~/conda_pkgs_dir
    key: ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-${{ hashFiles('etc/testing.yml') }}
    restore-keys: |
      ${{ runner.os }}-conda-${{ env.CACHE_NUMBER }}-

- uses: conda-incubator/setup-miniconda@v3
  with:
    auto-update-conda: true
    activate-environment: wiser-source
    environment-file: etc/testing.yml
    channels: conda-forge
    auto-activate-base: false
    use-only-tar-bz2: true  # required for caching to work

- name: Update environment
  run: conda env update -n wiser-source -f etc/testing.yml
  if: steps.cache.outputs.cache-hit != 'true'

3. Preloaded Docker image (fast, harder to maintain)#

Run tests inside a Docker image that already has the conda environment installed. The tradeoff is that the Docker image must be rebuilt and pushed whenever dependencies change.

Useful references:

4. Micromamba with conda-lock and caching (fastest, and relatively easy to maintain)#

Using conda-lock, micromamba doesn’t need to solve and micromamba itself is very fast and barebones. I believe when I tested this in the past it took about 1.5 minutes to make an environment from a cold start.

Docker Container Reference#

Running Qt (headless) inside Docker#

PySide6/PySide2 requires a display. Use xvfb inside a container:

Xvfb :1 -screen 0 1024x768x16 &
export DISPLAY=:1
pytest test_rasterpane

See also: Stack Overflow — headless Qt

Note: pytest-qt currently only works with PySide6, not PySide2. See pytest-qt docs.

Known Docker / CI Issues#

NetCDF segfault on Linux: Opening a .nc file inside a Linux Docker container (tested with GDAL 3.10.1 and 3.9.3) causes a fatal segmentation fault during get_band_data_normalized. The stack trace originates in numpy.ma.core.masked_values called from dataset.py. This is a known issue with no current fix.

Common conda error: does not exist (perhaps a typo or a missing channel) — usually caused by a missing conda-forge channel or a typo in the environment file.

Maintaining the Plugin API Documentation#

The Plugin API documentation is part of this Sphinx docs site (doc/sphinx-general-wiser-docs/), under the Extending WISER section. It is built and deployed alongside the rest of the WISER documentation via the existing GitHub Actions workflow (see docs-preview.yml / deploy-docs.yml).

The community plugin examples and third-party plugins themselves live in a separate repository: WISER-Plugin-Repository.

This step is part of the release process (see the Release Process section above).


Release Checklist#

This page documents the process for creating a new WISER release.

Overview#

Note

This section is not yet written. It will cover the end-to-end release workflow: version bumping, building distributables, code signing, CI/CD pipeline, and publishing releases.