CI/CD and release flow
This guide explains how GitHub Actions and the C# ModularPipelines project in build/ work together: integrating work on develop (WIP builds), shipping from master via signed Git tags (releases), how versions are bumped, and the manual maintainer ritual that drives a release.
The legacy Python CLI (pipenv run pyrevit ...) in dev/pyrevit.py remains available for local/manual workflows; CI/CD is driven by dotnet run in build/.
Branches and roles
| Branch | Role |
|---|---|
develop |
Day-to-day integration. Pushes produce signed WIP installers and tester notifications. |
master |
Release line. Holds the version-stamped commit that gets tagged v* to drive a release. |
Feature work branches from develop. Changes reach develop and master through pull requests; releases are cut by pushing a v<version> tag from a clean clone of master.
Workflow architecture
pyRevit's pipeline is split across workflows in .github/workflows/, each invoking the ModularPipelines console project via dotnet run:
| Workflow | File | What it does |
|---|---|---|
pyRevit CI |
ci.yml |
Runs dotnet run -- ci to build unsigned DLLs, runs dotnet test on the build project, uploads unsigned-bin-<sha> (Actions artifact for WIP/release), and publishes the same zip to the public ci-binaries GitHub Release (for pyrevit clone). Runs on every push to develop / master / v* tag (with a path filter), on PRs to those branches, and on manual dispatch. |
pyRevit WIP |
wip.yml |
Downloads CI artifacts, runs dotnet run -- pack sign under the production environment, and uploads signed WIP installers. |
pyRevit Release |
release.yml |
On v* tag pushes, waits for CI, runs dotnet run -- release pack sign publish under production, then notifies linked issues. |
Update Winget manifests |
winget.yml |
After a GitHub release is published, runs dotnet run -- winget to submit WinGet manifest PRs. Strips ElevationRequirement: elevationProhibited from generated user-scope installers before submit (see Troubleshooting). |
The CI notify job (develop pushes only) runs dotnet run -- notify inline in ci.yml with issues: write and does not use the production environment.
This split guarantees that:
- every DLL shipped inside an installer carries an Authenticode signature,
- the installer
.exe/.msithemselves are Authenticode-signed, - the
.nupkg's embedded checksum matches the signed installer users actually download, and - the
.nupkgitself carries a NuGet author signature so Chocolatey clients can verify it.
ci.yml triggers and path filter
ci.yml runs when changes touch build-related paths:
build/,dev/,extensions/,pyrevitlib/,release/,site-packages/
It is triggered by:
- Push to
develop,master, or anyv*tag (with the path filter above). - Pull request (
opened,reopened) targetingdevelopormaster(with the path filter). workflow_dispatchfor manual runs.
Doc-only or other out-of-scope changes skip CI entirely.
PR pushes do not re-run CI
The PR trigger is restricted to opened and reopened. New commits pushed to an open PR do not trigger another run. Close and reopen the PR, or push to the head branch after closing and reopening, if you need a fresh CI run after fixes.
Official repository vs forks
The stamping steps (set year, set build wip|release, set products) only run when Build__Channel is wip or release and GITHUB_REPOSITORY is the main repo (pyrevitlabs/pyRevit). The downstream wip.yml and release.yml jobs are similarly gated on the main repo so secrets are never exposed to forks. Forks still get checkout and an unsigned product build via ci.yml (useful for PR validation). Unsigned builds (Channel=none) still seed bin/pyrevit-products.json from release/ before the labs build so fork PR validation succeeds.
Prebuilt binaries for clone
User workflows (full commands for run-only vs C# contributor): Developer Guide — Clone workflows.
End users and contributors who only need to run pyRevit (not build C#) get bin/ via pyrevit clone or pyrevit clones update on develop or master — no GitHub token on the public repo when Release assets are available. C# contributors use git clone, local dotnet run -- ci, and pyrevit clones update --skip-bin instead — see Profile 2 in the dev guide.
| Consumer | Source | Auth |
|---|---|---|
pyrevit clone / clones update |
GitHub Release ci-binaries assets (fork → upstream SHA fallback) |
None (anonymous HTTPS) |
pyrevit clone / clones update (token fallback) |
GitHub Packages PyRevit.UnsignedBin NuGet mirror |
GITHUBTOKEN (read:packages) |
pyrevit clone / clones update (token fallback) |
Actions artifact unsigned-bin-<sha> |
GITHUBTOKEN (actions:read) |
| WIP / release pack pipelines | Actions artifact unsigned-bin-<sha> |
GITHUB_TOKEN in CI |
After each successful CI push to develop or master on the main repo:
- CI zips
bin/→unsigned-bin-{fullSha}.zip - Uploads to Release tag
ci-binaries(pre-release), plus rollingunsigned-bin-{branch}-latest.zip - Pushes
PyRevit.UnsignedBinNuGet package to GitHub Packages (token-authenticated CLI mirror) - Prunes per-SHA release assets older than the last 3 successful CI builds per branch (
develop,master); branch-latest zips are always kept - Prunes
PyRevit.UnsignedBinNuGet versions older than the last 2 successful CI builds per branch (develop,master)
Anonymous download URL pattern:
https://github.com/pyrevitlabs/pyRevit/releases/download/ci-binaries/unsigned-bin-{sha}.zip
https://github.com/pyrevitlabs/pyRevit/releases/download/ci-binaries/unsigned-bin-develop-latest.zip
CLI download order:
- Release asset for clone remote + commit SHA
- Release asset for upstream (
pyrevitlabs/pyRevit) + same SHA (synced forks) - Release branch-latest on clone remote, then upstream
- NuGet
PyRevit.UnsignedBin(whenGITHUBTOKENis set) - Actions artifacts (when
GITHUBTOKENis set)
See also build/README.md and the developer guide.
Feature or fix → develop (WIP)
- Create a branch from
develop, implement the change, open a PR intodevelop(touch paths under the filter if you need CI). -
After the PR is merged into
develop,ci.ymlrunsdotnet run -- cion the push event:- Stamps copyright/year, applies WIP versioning, refreshes product metadata, builds products, verifies LibGit2, and stages release metadata.
- Uploads the unsigned
bin/tree asunsigned-bin-<sha>.
-
wip.ymlis triggered automatically when that CI run finishes successfully ondevelop. On the main repo:- Downloads CI artifacts and runs
dotnet run -- pack signto sign DLLs, build/sign installers and the Chocolatey.nupkg, and uploadpyrevit-wip-installers-<install-version>. - The
notifyjob inci.ymlrunsdotnet run -- notifywith a link to the WIP workflow run (where signed installers are published).
- Downloads CI artifacts and runs
Push to develop ⇒ signed WIP installers and notification, not a public GitHub Release.
Cutting a release (tag-driven)
Releases are no longer auto-triggered by merging into master. A maintainer runs through the ritual below, and pushing the v<version> tag triggers ci.yml (rebuild on the tagged SHA) and release.yml (waits for CI, then signs and publishes) in parallel.
Pre-flight
- Confirm
developis green: the latest CI run ondevelopsucceeded andwip.ymlproduced the signed artifact. - Confirm
pyrevitlib/pyrevit/versionandrelease/versionreflect the version you intend to publish.release.ymlhard-fails if the tag name does not matchpyrevitlib/pyrevit/version. - On tag pushes, CI preserves the committed build version (including the
+HHMMsuffix) from git.developandmasterbranch pushes still re-stamp the build number as before. -
Make sure the required secrets are configured in the
productionGitHub environment:AZURE_TENANT_ID,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_ENDPOINT,AZURE_CODE_SIGNING_NAME,AZURE_CERT_PROFILE_NAMECHOCO_TOKEN
Cut a release
-
From a clean local clone on
develop, stamp the build as a release:This updates
pyrevitlib/pyrevit/versionand related build info files. -
Commit the version changes and merge them into
mastervia your normal PR flow (or push directly if your branch protection allows it): -
Tag the release commit on
master. The tag name must exactly matchpyrevitlib/pyrevit/versionprefixed withv: -
Pushing the tag triggers two workflows in parallel:
ci.ymlrebuilds DLLs on the tagged commit and uploadsunsigned-bin-<sha>(DLLs only; installers are no longer built in CI).release.ymlstarts immediately and polls for the matching CI run (viagh run watch). Thereleasejob downloads CI artifacts, runsdotnet run -- release pack sign publishunderproduction(sign viasign code trusted-signing, draft GitHub Release, Chocolatey push).notifythen posts to linked issues. After you publish the draft release,winget.ymlsubmits WinGet manifest PRs.
-
Open the draft release on GitHub, review the auto-generated notes, then publish it.
Manual re-run
Running workflow_dispatch on release.yml is supported but the if guard still requires github.ref_type == 'tag', so the dispatch must be invoked against an existing v* tag — not a branch. Use it to retry a failed release without re-pushing the tag.
Post-release
Bump develop to the next development version so subsequent WIP builds carry the right number:
git checkout develop
git pull --rebase origin develop
pipenv run pyrevit set next-version
git add -A
git commit -m "chore: bump next version"
git push origin develop
Hotfix flow
Same as above, but cut the release commit from master (or a hotfix/* branch off master) instead of develop. After tagging and publishing, cherry-pick the version bump back into develop.
Refreshing vendored dependencies (maintainer-only)
The DLLs under dev/libs/netfx/ and dev/libs/netcore/ (pyRevitLabs.MahAppsMetro.dll, pyRevitLabs.NLog.dll, pyRevitLabs.Json.dll, pyRevitLabs.PythonNet.dll, ControlzEx.dll, ...) are vendored: projects that consume these DLLs reference them via HintPath="$(PyRevitDevLibsDir)\...", and the files are committed to git. CI does not rebuild them; the ModularPipelines build invokes labs, engines, runtime, and autocomplete builds.
When you bump a submodule under dev/modules/ (MahApps.Metro, NLog, Newtonsoft.Json, Python.Net, IronPython2/3), you need to refresh the vendored output locally and commit the result:
# one-time setup: install the .NET Core 3.1 SDK (MahApps.Metro netcore TFM
# targets netcoreapp3.1; it's EOL but still publicly available)
winget install Microsoft.DotNet.SDK.3_1
# refresh dev/libs/{netfx,netcore} from the submodule sources
pipenv run pyrevit build deps
# review and commit the diff
git add dev/libs
git commit -m "chore(libs): refresh vendored deps for <submodule> bump"
This keeps the CI hot path on the SDKs preinstalled on windows-2025 (.NET 4.8 + .NET 8 + .NET 10) and avoids depending on the EOL 3.1 archives in a hosted runner. If a submodule ever ships only via NuGet (e.g. modern MahApps.Metro), retire the local build from _labs.build_deps and switch the .csproj to a PackageReference instead of HintPath.
Version files and commands
| File | Purpose |
|---|---|
pyrevitlib/pyrevit/version |
Full build version string used across the product (drives the v* tag name). |
release/version |
Install / marketing version used for installers and the release title. |
CI invokes the ModularPipelines project from build/ via dotnet run; the legacy pyrevit CLI commands remain for local use:
| Command | When / purpose |
|---|---|
dotnet run -- ci (in build/) |
CI build path (replaces pyrevit check, set year, set build, set products, build products) |
dotnet test tests/Build.Tests.csproj |
Build-project unit tests (also run in CI) |
dotnet run -- pack sign |
WIP/release pack path after artifact restore |
dotnet run -- publish |
Draft GitHub release + Chocolatey push |
dotnet run -- notify |
Post WIP/release URL to linked issues |
pipenv run pyrevit set year |
Updates copyright year (local/manual) |
pipenv run pyrevit set build wip |
After push to develop (CI runs this automatically). |
pipenv run pyrevit set build release |
Release build on master (CI runs this on master / v* pushes; maintainer runs it locally before tagging). |
pipenv run pyrevit set products |
Refreshes product metadata before build products. |
pipenv run pyrevit set version <ver> |
Manual bump, e.g. 4.8.0; tooling adds build/time segments (and WIP suffix when used in that mode). |
pipenv run pyrevit set next-version |
Patch bump on develop after a release (run manually by the maintainer). |
pipenv run pyrevit build commit |
Available but not used by CI any more; commits metadata, tags v<build-version> / cli-v<build-version>, and pushes. Kept for local convenience. |
pipenv run pyrevit report releasenotes |
Generates the release notes used by the draft GitHub Release. |
pipenv run pyrevit notify wip <url> / notify release <url> |
Posts the WIP run URL or release URL back to linked issue threads. |
Quick reference
| Goal | Action |
|---|---|
| Validate a change in CI | PR to develop; ensure changed paths match the workflow filter. |
| WIP installers + issue ping | Merge PR → develop (push triggers ci.yml → wip.yml). |
| Ship a release | Stamp release on develop, merge to master, tag v<version> on master, push the tag. |
| Publish the release | Open the draft release on GitHub and publish when ready. |
| Next dev version after release | pipenv run pyrevit set next-version on develop and push. |
Troubleshooting
- Release fails on
Validate tag matches version: the tag (e.g.v4.8.16) doesn't matchpyrevitlib/pyrevit/version. Delete and recreate the tag with the right name, or update the version file and re-tag. If the tag and checkout match but the error shows different+HHMMsuffixes (e.g. tag+1406vs file+1212), CI re-stamped the build number on a tag push — tag CI must preserve the committed version; move the tag to a commit that includes that fix and re-run. - Release fails on
Wait for CI to complete on tagged commit: CI either failed or didn't start within 10 minutes of the tag push. Investigate the CI run for the tagged SHA; once it is green, re-runrelease.yml. - Release fails on
Download unsigned bin artifact: the CI run exists but the expectedunsigned-bin-<sha>artifact is missing (most often because CI failed before the upload step). Fix CI and re-runrelease.yml. - Release fails on
Build Installers: Inno Setup (ISCC.exe), MSBuild, or the legacy WiX v3.x CLI MSI project failed.windows-2025preinstalls Inno Setup 6 and WiX Toolset v3.x; MSBuild is resolved fromPATHor Visual Studio'svswhere.exe. Local installer builds need the same tools installed. - Release fails on
Build Choco Package:choco packfailed, or the upstream signed installer was missing when the SHA was computed. ConfirmSign installersproduced the expecteddist/*.exe/.msioutputs before this step ran. - Release fails on
Sign Choco Package: this step uses thedotnet signCLI (installed viadotnet tool install --global sign --prerelease) and authenticates to Azure Trusted Signing via theAZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRETenv vars (DefaultAzureCredential chain). Common causes: (a) the certificate profile lacks the1.3.6.1.5.5.7.3.3Code Signing EKU required for NuGet author signing; (b) the App Registration is missing theTrusted Signing Certificate Profile Signerrole on the Signing Account; (c) the--prereleaseflag was removed andsignis no longer marked prerelease (drop--prereleaseonce the tool has a stable GA release). The previous attempt usedAzure/artifact-signing-action, but its v2.0.0 PowerShell module routes.nupkgtosigntool.exe, which doesn't recognize the format. Don't switch back without verifying upstream support for NuGet via that action. - Signing step fails (DLLs or installers): verify the
productionenvironment secrets above are present and not expired. - Choco push fails: check
CHOCO_TOKENand thatdist/pyrevit-cli.<version>.nupkgwas produced byBuild Choco Packagein thereleasejob. Re-run the workflow without re-pushing the tag. - Draft release exists but issues were not notified: check the
notifyjob log. Ifnotifysucceeded but no comments appeared, commits since the previous tag must include#<issue>in the message. Ifnotifyfailed with 403, confirm the job hasissues: writeand is not assigned to theproductionenvironment (environment deployment tokens can block issue comments). - Notify hit GitHub secondary rate limit: large merges can reference many issues; GitHub may throttle rapid comment creation (
SecondaryRateLimitExceededException). The notify step usescontinue-on-error: truesobuild/wip/releaseare unaffected.NotifyIssuesModulethrottles comments and stops gracefully when rate-limited — check logs forPosted X of Yand re-runnotifylater if needed. - Notify failed on empty
release_url: thereleasejob did not writedist/github-release-url.txtduring publish (checkRun release pipelinelogs forDeployment GitHub). Re-runrelease, then re-runnotify. The notify job receives the URL from the release job output; it no longer looks up releases by tag (tags containing+breakgh release view). - Draft release exists but
notifydid not run: thereleasejob must finish successfully (including Choco push) beforenotifystarts. Fix or re-runrelease, then re-runnotifyif the draft release URL is already available. - WinGet validation fails with
0x8A150056/elevationProhibited: WinGet's validation VM uses an administrator-capable account. Inno user installers built withPrivilegesRequired=loweststill block installation there even after removingElevationRequirement: elevationProhibitedfrom the manifest (WinGet reads the restriction from the installer). Thewingetpipeline publishes machine-scope admin installers only and stripselevationProhibitedifwingetcreateadds it. Per-user installers remain on GitHub Releases. Pushing a manifest update to the winget-pkgs PR re-triggers validation automatically (@wingetbot runrequires Moderator).
Related reading
- Developer Guide — local setup and building.
- Architecture — how pyRevit is structured at runtime.