Calq Flow
Calq Flow is a workflow and software delivery engine. Designed for fully automated, error-free versioning and release management of modular software and CLI-tool-driven workflows.
Comparison
Versioning & Release
| Feature | Calq Flow | Commit-Convention Release Tools | Manual-Changeset Release Tools | Tag/History-Based Versioners | Monorepo Version Managers |
|---|---|---|---|---|---|
| Version calculation basis | ✅ IL binary diff | ⚠️ commit message keywords | ⚠️ changeset files | ⚠️ tag distance / commit count | ⚠️ commit messages per package |
| Breaking change detection | ✅ automatic (API surface) | ❌ manual classification | ❌ manual classification | ❌ none | ❌ manual classification |
| Convention-free operation | ✅ no conventions required | ❌ conventional commits | ❌ changeset files per PR | ✅ tag-only | ❌ conventional commits |
| False positive prevention | ✅ compiler attribute filtering | ❌ any "feat:" bumps | ❌ any changeset bumps | ⚠️ height-based increment | ❌ any "feat:" bumps |
| Unnecessary release prevention | ✅ binary no-change = no release | ❌ commit = release | ❌ changeset = release | ⚠️ release decision external | ❌ commit = release |
| Monorepo support | ✅ automatic project discovery | ⚠️ plugin per package | ⚠️ config per package | ⚠️ tag prefix per project | ✅ built-in |
| .NET-native | ✅ IL analysis + .csproj | ❌ Node.js ecosystem | ❌ Node.js ecosystem | ✅ .NET-native | ❌ Node.js ecosystem |
| Reproducible builds | ✅ deterministic + lock file | ❌ not in scope | ❌ not in scope | ❌ not in scope | ❌ not in scope |
| Test enforcement before publish | ✅ automatic discovery + gating | ❌ separate CI step | ❌ separate CI step | ❌ separate CI step | ❌ separate CI step |
| Change metadata | ✅ structured JSON (IL diff) | ✅ markdown changelog | ✅ markdown changelog | ❌ | ✅ markdown changelog |
| Pre-release / alpha channel | ❌ manual version edit | ✅ built-in branch rules | ✅ pre-release mode | ✅ automatic suffix | ✅ built-in |
Build & Publish Orchestration
| Feature | Calq Flow | .NET Build Automation Frameworks | CI/CD Pipeline YAML | Package Manager Publish Commands |
|---|---|---|---|---|
| Zero configuration | ✅ convention-based discovery | ❌ build script required | ❌ YAML per workflow | ❌ manual per package |
| Topological build order | ✅ automatic from project references | ⚠️ task-level only | ⚠️ manual job dependencies | ❌ manual ordering |
| Binary-level change detection | ✅ IL / metadata comparison | ❌ | ❌ | ❌ |
| Build isolation | ✅ shadow copy, source unmodified | ⚠️ user responsibility | ❌ shared workspace | ❌ shared workspace |
| Cross-source republishing | ✅ re-push existing package | ❌ not built-in | ⚠️ manual artifact forwarding | ❌ not built-in |
| Idempotent push | ✅ --skip-duplicate built-in |
⚠️ user responsibility | ⚠️ user responsibility | ⚠️ manual flag |
| Package signing | ✅ single --sign parameter |
⚠️ custom target | ⚠️ manual step | ⚠️ manual step |
| Dry-run mode | ✅ full simulation + JSON output | ⚠️ execution plan only | ❌ | ❌ |
| Machine-readable output | ✅ structured JSON on stdout | ❌ log output | ❌ log output | ❌ log output |
| Scale-to-zero | ✅ CLI tool, no daemon | ✅ CLI tool | ✅ runner-based | ✅ CLI tool |
| Multi-language support | ❌ .NET only | ⚠️ .NET-focused | ✅ any language | ⚠️ per-ecosystem |
Table of Contents
- Comparison
- Usage - Calq Flow
- 1. Setup
- 2. Change Detection
- 3. Binary Comparison
- 4. Versioning
- 5. Build Orchestration
- 6. Build Isolation
- 7. Branch & Tag Management
- 8. Packaging & Distribution
- 9. Output & Machine Integration
- 10. Workflow Distribution
- Quick Start
- License
Usage - Calq Flow
1. Setup
1.1 Configuration
Action inputs:
| Input | Required | Default | Description |
|---|---|---|---|
subcommand |
Yes | — | The flow subcommand to execute (e.g., publish, publish --dry-run) |
nuget-config-repo |
No | .nuget |
Repository to pull NuGet.Config from (under the same GitHub owner) |
cache |
No | true |
Enable caching for the tool binary and NuGet packages |
Global CLI options:
| Option | Type | Default | Description |
|---|---|---|---|
--sources / -s |
List<string> |
["main"] |
NuGet source names to push packages to |
--remote / -r |
string |
origin |
Git remote name for tag resolution and fetch operations |
--tag-prefix / -t |
string |
v |
Prefix for version tags |
calq-flow --sources nuget.org --remote upstream --tag-prefix release/ publish
Publish parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
--dry-run |
bool |
false |
Log actions without modifying filesystem, Git, or NuGet |
--ignore-access-modifiers |
bool |
false |
Include internal member changes (for InternalsVisibleTo) |
--sign |
string |
"" |
Certificate fingerprint for signing .nupkg files before push |
--rolling-branch |
string |
latest |
Branch pointer to force-update on release (empty string disables) |
--api-key |
string |
"" |
API key for authenticated NuGet push operations |
calq-flow publish --dry-run --ignore-access-modifiers
calq-flow publish --sign ABC123 --rolling-branch main
calq-flow publish --rolling-branch "" # Disable rolling branch
Key points:
- If
--sourcesis not provided, it defaults to["main"] - Source names reference entries in the system's
NuGet.Configfor credentials unless--api-keyis provided --tag-prefixaffects both tag resolution (git ls-remote) and tag creation
1.2 NuGet authentication
Calq Flow separates read and write credentials for NuGet operations:
- Read (restore/download): Credentials come from a
NuGet.Configfile, sourced from a dedicated repository (default:.nugetunder the same GitHub owner). The action clones this repo and installsNuGet/NuGet.Configinto the runner's~/.nuget/NuGet/directory, expanding environment variables viaenvsubst. - Write (push): Credentials are passed via the
--api-keyCLI parameter, completely independent ofNuGet.Config.
Setting up the .nuget repository:
Create a repository named .nuget under your GitHub organization with the following structure:
.nuget/
└── NuGet/
└── NuGet.Config
Example NuGet.Config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="main" value="https://nuget.pkg.github.com/your-org/index.json" />
</packageSources>
<packageSourceCredentials>
<main>
<add key="Username" value="your-username" />
<add key="ClearTextPassword" value="${MAIN_NUGET_PAT}" />
</main>
</packageSourceCredentials>
</configuration>
Key points:
${MAIN_NUGET_PAT}is expanded at runtime from your workflow's environment. Pass it viaenv: ${{ secrets }}or explicitly asenv: { MAIN_NUGET_PAT: ${{ secrets.MAIN_NUGET_PAT }} }- The PAT only needs
packages:readscope — it is used exclusively fordotnet restoreoperations (dependency resolution and base DLL downloads) - Push authentication is handled separately by
--api-key, which accepts${{ github.token }}(for GitHub Packages) or a nuget.org API key - The
mainsource key inNuGet.Configcorresponds to the default--sources ["main"]value — these names must match - Override the config repository name with the
nuget-config-repoaction input if needed
See also: 1.1 Configuration
1.3 GitHub Action setup
Because Calq Flow is architected as a C# Composite Action, it eliminates the container image pull and initialization latency associated with conventional Docker-based DevOps actions. It leverages the GitHub Runner's native .NET runtime directly.
Publish workflows should utilize concurrency to prevent race conditions on version tags:
name: Publish
on:
workflow_dispatch:
concurrency:
group: publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Calq Flow (publish)
uses: calq-framework/flow@latest
with:
subcommand: 'publish --api-key ${{ github.token }}'
env: ${{ secrets }}
Pin to a specific version:
- uses: calq-framework/[email protected]
with:
subcommand: 'publish'
Dry-run on pull requests:
- uses: calq-framework/flow@latest
with:
subcommand: 'publish --dry-run'
Publish to nuget.org:
- uses: calq-framework/flow@latest
with:
subcommand: 'publish --sources nuget.org --api-key ${{ secrets.NUGET_API_KEY }}'
Key points:
fetch-depth: 0is required for full Git history (tag resolution and diff)contents: writepermission is required for Git tag creationpackages: writepermission is required for GitHub Packages NuGet push- Dry-run logs exactly which packages would be published and which versions would be bumped, without modifying the filesystem, Git state, or NuGet registries
See also: 1.1 Configuration, 1.2 NuGet authentication
1.4 CLI tool installation
Calq Flow enables local execution of the same deterministic pipeline that runs in CI. The orchestration logic is not confined to YAML.
Install globally:
dotnet tool install --global CalqFramework.Flow.Cli
Run locally:
calq-flow publish
calq-flow publish --dry-run
calq-flow publish --sources nuget.org --api-key <key>
Key points:
- Requires
gitanddotnetCLI on the systemPATH - Must be executed within a Git repository
- Produces identical results to the GitHub Action
See also: 1.1 Configuration, 1.3 GitHub Action setup
2. Change Detection
2.1 Git-based impact analysis
Calq Flow detects exactly which modules changed since the last version tag using git diff --name-only {lastTag}..HEAD. If no tags exist, all committed files are considered changed.
Key points:
- Diff is computed against the most recent version tag matching
--tag-prefix - Only committed changes are considered — unstaged or untracked files are excluded
- The diff operates on file paths, not content, for performance
See also: 1.3 GitHub Action setup
2.2 Project-scoped diff boundaries
A project is marked "changed" only if a file modification occurs within the project's own directory or its subdirectories. Changes to root files (e.g., .gitignore, README.md) do not trigger project changes.
Key points:
- Each project's change boundary is its directory subtree
- Shared files outside any project directory never trigger a version bump
- This scoping prevents unrelated repository-level changes from causing unnecessary releases
See also: 2.1 Git-based impact analysis
3. Binary Comparison
3.1 IL / metadata-level analysis
Assemblies are analyzed at the binary level using System.Reflection.MetadataLoadContext:
| Change Type | Classification | Example |
|---|---|---|
| Deleted member | Breaking | Removing a public method |
| Modified attributes | Breaking | Changing method attributes |
| Added member | Non-breaking | Adding a new public method |
| IL bytecode change | Non-breaking | Modifying method body |
Key points:
- Comparison operates on compiled assemblies, not source code — captures the true public surface
- Both current and base DLLs are loaded into
MetadataLoadContextfor reflection without executing code - Classification drives the version bump decision in the versioning stage
3.2 Byte-level fallback
If no syntactic changes are found but the DLL bytes differ, the diff falls back to byte-level comparison and classifies the change as ILChanged (non-breaking).
Key points:
- Byte-level fallback ensures that changes not captured by metadata reflection (e.g., method body modifications) still trigger a patch bump
ByteLevelFallbackis reported in the JSON output for visibility- This fallback never classifies a change as breaking — only as non-breaking
See also: 3.1 IL / metadata-level analysis
3.3 Filtered compiler attributes
Compiler-generated attributes are filtered out to prevent false positives:
AsyncStateMachineAttributeNullableContextAttributeNullableAttributeCompilerGeneratedAttributeIteratorStateMachineAttribute
Key points:
- These attributes change between compilations without any source-level modification
- Filtering prevents spurious version bumps caused by compiler implementation details
- The filter list is fixed — custom attributes are always included in comparison
See also: 3.1 IL / metadata-level analysis
3.4 Access modifier control
Use --ignore-access-modifiers to include internal member changes in the comparison, relevant for projects using InternalsVisibleTo.
Key points:
- Default behavior compares only
publicandprotectedmembers - When enabled,
internalandprivate protectedmembers are included in breaking change detection - Useful for libraries that expose internals to friend assemblies via
InternalsVisibleTo
See also: 3.1 IL / metadata-level analysis, 1.1 Configuration
4. Versioning
4.1 Version resolution
Key points:
- Reads
<Version>(or<VersionPrefix>) from.csprojusing XML parsing, stripping pre-release suffixes - Resolves the latest version tag from the remote using
git ls-remote --tags --sort -version:refname - Uses
System.Version(3-component: major.minor.build) - Pre-release suffixes (e.g.,
-alpha.1) are stripped before comparison — only the numeric components participate in precedence
See also: 1.1 Configuration, 2.1 Git-based impact analysis
4.2 Version bump rules
Calq Flow algorithmically calculates version bumps based on IL changes:
| Condition | Bump | Example |
|---|---|---|
| Breaking change (deleted/modified member) | Minor | 0.1.0 → 0.2.0 |
| Non-breaking change (added member / IL change) | Patch | 0.1.0 → 0.1.1 |
| Major version | Manual only | Edit <Version> in .csproj |
Key points:
- Follows pre-1.0 SemVer convention: breaking changes bump minor, not major
- Major version bumps are intentional — achieved by manually editing
<Version>in the.csproj - If no changes are detected, no version bump occurs
See also: 3.1 IL / metadata-level analysis, 4.1 Version resolution
4.3 Version precedence
The higher version between the .csproj <Version> and the computed syntactic version always prevails.
Key points:
- Ensures idempotency — re-running the pipeline never produces a lower version than what is already declared
- Enables manual override — setting a higher
<Version>in.csprojtakes priority over algorithmic computation - Lockstep versioning: all projects in the monorepo are packed at the single computed target version
See also: 4.2 Version bump rules, 4.1 Version resolution
5. Build Orchestration
5.1 Monorepo project discovery
Calq Flow recursively scans the working directory for *.*proj files with automatic exclusion logic.
Exclusion rules:
- Projects matching
*Test*,*Example*,*Sample*(and identically named directories) are excluded - If a project file resides in the same directory or a subdirectory of another discovered project, the nested one is ignored
my-monorepo/
├── MyLibrary/
│ └── MyLibrary.csproj ← Discovered
├── MyLibrary.Tests/
│ └── MyLibrary.Tests.csproj ← Associated test project (auto-discovered)
├── Example.App/
│ └── Example.App.csproj ← Excluded (matches *Example*)
└── OtherLib/
└── OtherLib.csproj ← Discovered
Key points:
- Discovery is recursive from the Git repository root
- No configuration file is required — convention-based detection
- Nested projects (project within another project's directory) are automatically excluded to prevent duplicate builds
See also: 2.2 Project-scoped diff boundaries, 4.3 Version precedence
5.2 Topological build order
Projects are built in dependency order using a topological sort with queue-retry fallback.
Key points:
- Transitive dependencies are built before their dependents
- The queue-retry fallback handles circular or unresolvable dependency graphs gracefully
- Changed projects and their transitive dependents are built; unchanged projects are only built when publishing (not during dry-run) to ensure lockstep packing
See also: 5.1 Monorepo project discovery, 2.1 Git-based impact analysis
5.3 Locked-mode restore
Projects with a packages.lock.json are restored in --locked-mode for reproducibility; projects without one proceed normally with a warning.
Key points:
--locked-modeensures the exact dependency graph from the lock file is restored — no floating resolution- Projects without a lock file receive a warning but are not blocked
- Lock file presence is detected per-project, not globally
See also: 5.2 Topological build order, 1.2 NuGet authentication
5.4 Reproducible builds
All builds use deterministic flags:
-p:Deterministic=true -p:ContinuousIntegrationBuild=true -p:PathMap="$(MSBuildProjectDirectory)=/src"
Key points:
Deterministic=trueensures identical source produces identical binary outputContinuousIntegrationBuild=truenormalizes file paths in PDBs for reproducibility across machinesPathMapreplaces local absolute paths with/srcin debug information- Projects with SourceLink are built with embedded debug symbols and source tracking
See also: 5.3 Locked-mode restore, 3.2 Byte-level fallback
5.5 Test association and enforcement
For each library project, Calq Flow searches upward from the project's directory for {ProjectName}Test*.*proj files. Traversal is bounded by the Git repository root.
Key points:
- Test projects are built and tests are strictly enforced before publishing
- If no test project is found, only the library project is built
- Test failure aborts the pipeline — no packages are published if any test fails
- Test projects themselves are never published as packages
See also: 5.1 Monorepo project discovery, 5.2 Topological build order
6. Build Isolation
6.1 Base DLL resolution
For each changed project, Calq Flow resolves the previous version's DLL using a prioritized fallback:
- NuGet Download: Attempts to download the previous version package from configured sources using a temporary project restore into the global NuGet cache
- Shadow Copy Build: If NuGet download fails, creates an isolated shadow copy of the repository and builds the old version there
Key points:
- The shadow copy is created at most once per publish cycle (shared across all projects)
- The shadow copy is automatically purged upon completion
- The primary working directory is never mutated
- NuGet download is preferred for performance — shadow copy is the guaranteed fallback
See also: 1.2 NuGet authentication, 3.1 IL / metadata-level analysis, 4.1 Version resolution
6.2 Shadow copy mechanism
When the previous version's DLL cannot be retrieved from NuGet, Calq Flow creates a temporary workspace reflecting the repository's previous state:
- Copy: Creates a physical copy of the working directory into a system temporary path, excluding
bin,obj, and.vsfolders - Fetch: Resolves the base commit from the last version tag, then fetches it with
git fetch {remote} {baseCommit} --depth 1 - Sanitize: Executes
git reset --hardandgit clean -d -x --forceinside the shadow copy only - Checkout: Executes
git checkout {baseCommit}to synchronize the filesystem with the base version - Build: Builds the old version of changed projects in isolation
- Cleanup: Recursively deletes the temporary directory (resets read-only attributes on Windows for git pack files)
Key points:
- The shadow copy guarantees the pipeline never fails due to an unreachable package registry
- Uses
CD(AsyncLocal) for thread-safe directory switching - Exclusion of
bin,obj, and.vsreduces copy time and avoids stale build artifacts
See also: 6.1 Base DLL resolution, 5.4 Reproducible builds, 2.1 Git-based impact analysis
6.3 Working directory safety
Key points:
- Destructive Git commands (
reset --hard,clean -d -x --force) are never executed on the user's source path - All destructive operations are confined to the temporary shadow copy directory
- The primary working directory remains unmodified throughout the entire pipeline execution
- On pipeline failure, the shadow copy is still cleaned up — no temporary directories are left behind
See also: 6.2 Shadow copy mechanism
7. Branch & Tag Management
7.1 Version tagging
After a successful publish, Calq Flow creates a global tag in the format {prefix}{version} (e.g., v0.2.0) representing the state of the entire repository.
Key points:
- Tag prefix is configurable via
--tag-prefix(default:v) - The tag represents all projects at the computed version — not per-project tags
- Tag creation requires
contents: writepermission in GitHub Actions - Tags are pushed to the configured remote
See also: 1.3 GitHub Action setup, 1.1 Configuration, 2.1 Git-based impact analysis, 4.1 Version resolution
7.2 Rolling branch
Force-updates a configurable branch pointer (default latest) to the release commit.
calq-flow publish --rolling-branch main
calq-flow publish --rolling-branch "" # Disable rolling branch
Key points:
- The rolling branch provides a stable reference (e.g.,
@latest) for consumers who want the most recent release - Force-update means the branch always points to the latest release commit, not a merge
- Set to empty string to disable rolling branch behavior entirely
See also: 7.1 Version tagging, 1.1 Configuration
8. Packaging & Distribution
8.1 Pack and push lifecycle
After version computation and build completion, all projects are packed at the computed version and pushed to configured NuGet sources.
Key points:
- All projects are packed at the single computed target version (lockstep versioning)
- Unchanged projects are built during publish (not during dry-run) to ensure all packages are available for packing
- Push uses
dotnet nuget pushwith--skip-duplicatefor idempotency - Pack produces standard
.nupkgfiles compatible with any NuGet feed
See also: 1.2 NuGet authentication, 4.3 Version precedence, 5.2 Topological build order, 5.5 Test association and enforcement
8.2 Package signing
.nupkg files can be signed with a certificate fingerprint before push using the --sign parameter.
calq-flow publish --sign ABC123
Key points:
- Signing is performed via
dotnet nuget signwith--certificate-fingerprint - Signing occurs after pack and before push — the signed package is what reaches the registry
- If
--signis empty (default), signing is skipped entirely
See also: 8.1 Pack and push lifecycle, 1.1 Configuration
8.3 Cross-source republishing
When no source changes are detected but a tagged version exists, Calq Flow downloads the existing .nupkg from any configured source and re-pushes to the requested targets.
Key points:
- Enables publishing an existing package to additional registries (e.g., GitHub Packages → nuget.org) without rebuilding
- The downloaded package is identical to the originally published artifact
- Requires the package to be available on at least one configured source
See also: 1.2 NuGet authentication, 7.1 Version tagging, 2.1 Git-based impact analysis
8.4 Idempotent push
All push operations use --skip-duplicate to tolerate re-runs.
Key points:
- Re-running the pipeline against an already-published version does not fail
--skip-duplicateis a NuGet CLI flag that returns success when the package version already exists on the feed- Combined with cross-source republishing, this enables safe multi-target delivery across pipeline retries
See also: 8.1 Pack and push lifecycle, 8.3 Cross-source republishing
9. Output & Machine Integration
9.1 JSON result format
publish returns a PublishResult serialized to JSON on stdout:
{
"TargetVersion": "0.2.0",
"PreviousVersion": "0.1.0",
"ChangedProjects": ["MyLibrary"],
"PublishedPackages": ["MyLibrary"],
"Diffs": [
{
"ProjectName": "MyLibrary",
"Changes": [
{
"MemberIdentity": "MyNamespace.MyClass.NewMethod(System.String)",
"Kind": "Added"
}
],
"HasBreakingChanges": false,
"HasNonBreakingChanges": true,
"ByteLevelFallback": false
}
],
"DryRun": false
}
Key points:
- Safe for piping to
jq, scripts, or other tools - Contains version information, project lists, and detailed diff metadata
DryRunfield indicates whether the result represents an actual publish or a simulation
See also: 4.2 Version bump rules, 3.1 IL / metadata-level analysis
9.2 Stream separation
All build/test output is routed to stderr, maintaining a pristine stdout stream for machine consumption.
Key points:
stdoutcontains only the JSON resultstderrcontains all build, test, and diagnostic output- This separation enables
calq-flow publish | jq .TargetVersionwithout interference from build logs
See also: 9.1 JSON result format
9.3 Diff metadata
The Diffs array in the JSON output provides member-level change metadata for each changed project.
Key points:
MemberIdentitycontains the fully qualified member signatureKindindicates the change type (e.g.,Added,Deleted,Modified,ILChanged)HasBreakingChangesandHasNonBreakingChangesprovide project-level classificationByteLevelFallbackindicates whether the diff fell back to byte-level comparison for that project
See also: 9.1 JSON result format, 3.2 Byte-level fallback, 4.2 Version bump rules, 3.1 IL / metadata-level analysis
10. Workflow Distribution
10.1 CLI-first architecture
Calq Flow's pipeline logic is a standalone .NET console application with no CI platform dependencies. The tool requires only git and dotnet on the system PATH, accepts configuration via CLI parameters, and produces structured JSON on stdout.
This design enables two distribution paths:
- Package distribution (Section 8): libraries are pushed to NuGet feeds for programmatic consumption
- Workflow distribution (this section): the CLI tool is wrapped in platform-specific CI/CD integration for declarative pipeline consumption
Any CI/CD platform can invoke the tool identically:
dotnet tool install --global CalqFramework.Flow.Cli
calq-flow publish --api-key <key>
Key points:
- No GitHub API calls — uses
gitCLI for all repository operations - No environment variable detection — all values are passed explicitly via parameters
- Stream separation (stdout/stderr) enables machine consumption on any platform
- Platform-specific concerns (caching, credential injection, step format) belong to the wrapper, not the tool
See also: 1.4 CLI tool installation, 9.1 JSON result format
10.2 GitHub Action packaging
The CLI-first architecture allows any .NET tool to be packaged as a GitHub Action using a composite action wrapper. The wrapper handles tool installation, caching, and credential injection while delegating all logic to the CLI.
1. Add these properties to your .csproj:
<PackAsTool>true</PackAsTool>
<ToolCommandName>your-tool</ToolCommandName>
<PackageId>YourTool.Cli</PackageId>
2. Copy action.yaml to your repository root. Update the four variables in the first step:
TOOL_NAME="Your Tool"
TOOL_REPO_URL="https://github.com/your-org/your-tool.git"
TOOL_PACKAGE="YourTool.Cli"
TOOL_CMD="your-tool"
3. Adapt action.yaml to your requirements — remove steps you do not need, such as the self-install step.
Key points:
- Eliminates container image pull and initialization latency associated with Docker-based actions
- Leverages the GitHub Runner's native .NET runtime directly
- Provides strong typing, IDE support, and testability compared to bash/YAML
- The wrapper is thin — platform-specific concerns are isolated from pipeline logic
See also: 10.1 CLI-first architecture, 1.3 GitHub Action setup
10.3 Workflow templates
Three workflow templates enable both self-publishing and consumer distribution:
| Template | Trigger | Purpose |
|---|---|---|
ci.yaml |
Push / PR | Dry-run validation — verifies versioning and build without publishing |
publish.yaml |
Manual | Publishes packages to GitHub Packages and creates version tags |
publish-nuget-org.yaml |
Manual | Republishes existing packages to nuget.org |
For your tool's own CI — place these in your tool repository's .github/workflows/ directory using uses: ./ (local action reference):
- uses: ./
with:
subcommand: 'publish --dry-run'
For consumer distribution — place these in your organization's .github repository under workflow-templates/ using the full action reference:
- uses: your-org/your-tool@latest
with:
subcommand: 'publish --api-key ${{ github.token }}'
Add a properties.json alongside each template to control visibility in the GitHub "New workflow" UI:
{
"name": "Your Tool Publish",
"description": "Publish workflow with your-tool.",
"iconName": "octicon package",
"categories": ["C#", "F#"],
"filePatterns": [".*\\.csproj$", ".*\\.fsproj$"]
}
Key points:
uses: ./is for development — the tool publishes itself using its own local actionuses: your-org/your-tool@latestis for consumers — references the published actionfilePatternscontrols which repositories see the template in the GitHub Actions UI- The
publishconcurrency group prevents race conditions on version tags across all publish workflows
See also: 10.2 GitHub Action packaging, 7.2 Rolling branch
10.4 Version-pinned consumption
Consumers reference a specific version or track a rolling release:
# Pin to exact version
- uses: your-org/[email protected]
# Track latest within a minor
- uses: your-org/[email protected]
# Always use latest release
- uses: your-org/your-tool@latest
The action wrapper resolves the reference to a NuGet package version:
@v1.2.3→ installs exact version1.2.3@v1.2→ resolves latestv1.2.*tag viagit ls-remote@latestor branch name → resolves latestv*.*.*tag
Key points:
- Version tags created by the publish pipeline directly enable version-pinned consumption
- The rolling branch provides
@latesttracking without requiring consumers to update their workflows - Package binary is cached per resolved version — subsequent runs skip installation
See also: 10.3 Workflow templates, 7.1 Version tagging, 7.2 Rolling branch, 8.1 Pack and push lifecycle
Quick Start
dotnet tool install --global CalqFramework.Flow.Cli
cd your-monorepo
calq-flow publish --dry-run
License
Calq Flow is dual-licensed under PolyForm Noncommercial (with Evaluation Grant) and the Calq Commercial License.