Automating Version Control for D365FO ISVs: A DevOps Engineer’s Approach
We needed a reliable and transparent way to keep track of the versioning of our own ISVs—not just for internal housekeeping, but because it was absolutely critical for our development workflow and for our clients. Without consistent versioning, it becomes almost impossible to maintain clarity about what exactly is deployed where, which features are included in which build, or whether a client is running the most recent, stable version. Having a clear, always-up-to-date version wasn’t just “nice to have”; it was essential. It would allowed us to quickly identify issues, trace changes back to specific builds, avoid mismatches between environments, and ensure that the client side was never left running outdated or incompatible components. In short, accurate version tracking was the backbone of predictable delivery.
In theory, the solution seemed almost trivial:
“After each build, automatically update the ISV version and commit that change back into version control.”
Clean. Simple. Obvious.
At least, that’s what I thought at the beginning.
But once I started working through the details, it quickly became clear that this wasn’t just a small tweak—it was an entire automation puzzle living inside the Azure DevOps ecosystem. Each step came with its own quirks, dependencies, and hidden complexity, turning what looked like a one-liner into a much larger challenge.
The Challenge
Our setup was fully based on Azure DevOps, TFVC and PowerShell scripts — both for builds and deliveries.
The requirements were clear and specific:
- Update the ISV descriptor file automatically with a new version number.
- Use the build pipeline number format from build pipeline: $(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r) Example: 2025.11.12.1
- Сommit the updated descriptor file back into version control after every successful build.
So far, so good.
But here came the cornerstone problem — how to avoid an infinite build loop.
Every time the pipeline commits a change, the trigger picks it up and starts another build… which commits again… and so on. A DevOps version of Groundhog Day.
The Insight
While exploring ways to reduce unnecessary pipeline noise, I stumbled onto a small but surprisingly powerful detail: adding ***NO_CI*** (or markers like [skip ci]) to a commit message completely suppresses CI triggers. It’s one of those features that feels almost hidden in plain sight — simple, unobtrusive, yet incredibly effective.
That realization became the real “Aha!” moment.
If a single keyword can prevent a pipeline from firing, then it can also stop automation from accidentally triggering itself. And if that mechanism is applied deliberately during automated check-ins, it creates a clean, controlled workflow where updates happen quietly in the background without setting off a chain reaction of pipeline executions.
This opened the door to an elegant idea: use ***NO_CI*** strategically to prevent redundant or self-generated pipeline runs altogether, cutting out the loop before it even begins.
The Solution — Step by Step
I designed a PowerShell-based solution that tied everything together.
1️⃣ Step 1. Detect Changes
Using the tf.exe module, the script checks whether there were any modifications in the ISV models.
The list of ISVs to check is stored in pipeline variables, so it’s flexible and easy to maintain.
2️⃣ Step 2. Update the Descriptor
If changes are found, the script automatically updates the version section of the ISV descriptor file.
The version format follows the built-in pipeline variable $(Build.SourceVersion)
3️⃣ Step 3. Check-In with “NO_CI”
Here’s where the real trick comes in.
When the script commit the updated files, it appends ***NO_CI*** to the commit message. This small but powerful marker tells Azure DevOps to ignore CI triggers for that commit, ensuring the pipeline doesn’t accidentally retrigger itself. It’s a simple safeguard that keeps the whole process from falling into an endless automation loop.
4️⃣ Step 4. Skip the Loop
Because the commit message includes ***NO_CI***, Azure DevOps automatically recognizes it and suppresses new pipeline run that would normally be triggered. No extra logic needed — the trigger itself does the work. This prevents the DevOps version of Groundhog Day, where the pipeline repeatedly restarts itself.
As a result:
- The pipeline sees the ***NO_CI*** marker in the commit message and understands that this run should be skipped.
- Instead of kicking off another build, it quietly steps aside — no redundant executions, no self-triggered loops.
- The build queue stays clean and uncluttered, with only meaningful runs making it through.
This small check eliminates delays for other developers’ builds and keeps our environment clean and responsive.
Maintenance and Scalability
To keep everything maintainable, we adopted a unified scripts repository.
Each build downloads the latest PowerShell scripts from that repo at the start of the run.
After the build finishes — whether successful or failed — the scripts are deleted from the build agent.
This ensures:
- Always up-to-date automation scripts
- No manual updates on build agents
- Consistent behavior across all pipelines
The Results
After implementing this approach, we immediately saw several benefits:
1) No more manual versioning
Developers no longer need to remember to update version numbers — it’s done automatically and reliably after every build.
2) Clear visibility for customers
Each ISV build version is precise, timestamped, and traceable. Customers can easily see which version they are using.
3) No performance impact
The new steps didn’t increase build duration or queue time.
4) Modular and customizable
The solution can be applied selectively to specific models and easily adapted to other pipelines or projects.
Lessons Learned
Sometimes in DevOps, the hardest problems aren’t about using new tools — they’re about using existing ones creatively.
Azure DevOps offers a surprising amount of flexibility once you understand the tools built into the platform. In our case, a bit of creativity — and the smart use of built-in tools— turned what could have been an ongoing headache into a simple, elegant solution.
It’s also a reminder that good DevOps isn’t just about automation — it’s about building systems that think for themselves, so your teams can focus on delivering value instead of fixing process issues.
Enter your contacts to download the information about us immediately
Table of Contents:
Thank you for the request!
We will get back to you soon.
