Continuous Integration - James Montemagno https://montemagno.com/tag/continuous-integration/ https://montemagno.com/content/images/2017/08/MonogramIcon_Orange_NoBuffer.png Continuous Integration - James Montemagno https://montemagno.com/tag/continuous-integration/ Hugo -- gohugo.io Sun, 01 Nov 2026 09:22:20 GMT 60 More AI Agents, More CI: Reducing GitHub Actions Costs & Build Time https://montemagno.com/reducing-github-actions-costs-ai-agents/ https://montemagno.com/reducing-github-actions-costs-ai-agents/ Sat, 24 Oct 2026 19:00:00 GMT

AI coding agents are pretty awesome at helping me move projects forward. They can investigate an issue, make a change, and get a pull request ready while I am working on something else. But there is another part of that workflow that has to keep up: CI.

More agents means more branches, more pull requests, more updates to those pull requests, and more GitHub Actions jobs. Add Windows, Android, iOS, macOS, x64, and ARM64 into the mix, and one small change can turn into a whole lot of build machines doing work.

I could see it in my own usage throughout the year. The chart for my top repositories kept climbing, with a particularly noticeable jump toward the fall. That is the value of the Actions usage, not what I actually paid: much of it is included or covered by GitHub's free usage rules. Still, the growing compute footprint was a good reason to take a closer look.

Qualitative sketch of the supplied January-to-October repository usage chart. Several anonymized repository lines rise over the year, with the largest increases toward August through October. Dollar values and exact proportions are intentionally omitted; these are gross usage trends, not net billed charges.
The trend that prompted the audit. Qualitative redrawing of my top-repository usage chart, with amounts, exact proportions, and repository identities omitted. Gross usage value is not the amount billed; October is a partial month.

That was the problem I wanted to dig into across my apps. Not "how do I stop testing things?" Instead: how do I stop paying for, and waiting on, work that doesn't need to happen?

I used the GitHub Copilot app to analyze my GitHub usage export, inspect the workflows, and work through changes across repositories. Some wins were straightforward. One of the most useful discoveries was deleting caches we had just tried adding. That is right: a cache hit is not automatically a performance improvement!

The build matrix is a multiplier

Let's say an agent pushes three updates to a pull request, and each update starts four platform jobs. That is twelve jobs, before retries, release validation, or another agent working on another feature.

Parallel execution makes this easy to miss. Four five-minute jobs might finish in roughly five minutes if they start together, but they still consume about twenty runner-minutes. Queueing can make the wait longer without adding the same amount of execution time. A release waiting for approval is another thing entirely.

Those are different measurements:

Measurement What I want to learn
Runner execution time How much compute did the jobs actually use?
Pull request feedback time How long until I can act on a useful result?
Billed usage What remains chargeable after GitHub's billing rules and allowances?

The goal is to improve the right number, not just make a workflow page look shorter.

Start with the export, then follow it into the logs

My first prompt in the GitHub Copilot app was:

can you do an analysis of my actions across my repos running on windows and mac runners and see where we can optimize and cut costs

Then I attached the GitHub summarized usage CSV. My next message was simply:

Herei sm y usage report

Yes, typos and all. The useful part was supplying the data, not writing a magical prompt.

We looked at an October 1-24, 2026 snapshot containing 1,100 export records, reconciled it with GitHub's live billing API, and inspected 66 workflows across 14 repositories associated with Windows, macOS, or paid Actions usage. We also fetched 748 job records, not 748 independent workflow runs. That was complete for two paid hotspots, with selected representative runs elsewhere, not a complete performance census of every repository.

The export told us where to look. Workflow files and job logs told us what to change.

We separated setup, restore, compilation, tests, packaging, and cache transfer. We also checked whether a job really needed that operating system, whether superseded runs were still working, and whether "nothing changed" checks were allocating a native runner just to exit.

If your export includes gross usage, discounts, and net charges, keep those separate. An avoided dollar of nominal compute is not necessarily a dollar off your bill. I am deliberately not sharing my billing totals or repository-level billing breakdown here.

One more important limitation: a busy partial month is not a reliable annual forecast. It is an excellent place to find waste, though.

Put each job on the runner it actually needs

First things first: an Android build does not automatically need a Mac just because the same application also targets iOS.

For my FIRE calculator Android workflow, we moved from macos-15 to ubuntu-24.04, installed the Android-specific .NET MAUI workload, and configured the required JDK and Android SDK dependencies. We kept the required check name, tests, signing configuration, artifacts, and gates.

The hosted Android job completed in 2 minutes 35 seconds with all 447 tests passing. That confirms the Linux path worked; it is not a matched before-and-after speed benchmark. The signed AAB credential path was not exercised, and we did not dispatch a release to prove it.

Why move it? Here are the relevant GitHub standard runner list rates, checked October 24, 2026:

Runner billing SKU USD per minute Relative to Linux x64
Linux x64, 2-core $0.006 1.00x
Windows x64, 2-core $0.010 1.67x
macOS, M1 or Intel $0.062 10.33x
Published per-minute rates normalized to Linux x64 at 1: Windows x64 is 1.67 and macOS is 10.33.
Published list-rate comparison, not my bill or a hardware-performance comparison. Runner capabilities and runtimes differ.

At equal billed runtime, moving eligible work from that macOS SKU to Linux x64 reduces its nominal compute price by about 90.3%. That is a rate comparison, not a measured reduction in my total bill.

In fact, this example is a public repository: standard GitHub-hosted runners are free for public repositories. For private repositories, included allowances and discounts affect marginal savings. Larger runners have different rules and are not free just because a repository is public. GitHub also rounds each job's partial minutes up to whole minutes.

The practical lesson is still useful: keep Apple builds on a compatible Mac, Windows-specific packaging on Windows, and move portable work only after proving it works elsewhere.

Don't start an expensive machine to say "nothing changed"

Another discovery was in My Stream Timer. Its required Core test and x64/ARM64 matrix checks still needed to report results, including when a change did not affect the app.

Simply skipping an entire workflow is not a safe substitute for understanding your branch rules. Required checks can end up waiting for a result that never arrives.

Instead, we kept the check names and matrix shape. No-change checks run on Ubuntu; actual Windows work still runs on Windows.

A change detector routes unchanged app inputs to a Linux no-op required check and changed inputs to Windows builds and tests. Detection errors fail rather than reporting success.
Keep the required-check contract. Route the work, not the truth of the result.

There is a gotcha here: a failed change detector must not become a successful no-op. We checked invalid outputs, upstream failures, and cancellation as well as the happy path. Hosted Windows packages for both architectures, tests, and package validation passed. Seven live routing fixtures exercised the no-change and failure/cancellation paths.

This proves the routing behavior. We did not collect a controlled per-run latency benchmark, so I am not attaching a made-up number to it.

Cancel superseded CI, not releases

When an agent pushes a fix while the previous build is still running, do you need both ordinary CI runs to finish?

Usually, I want feedback for the newest commit. Workflow- and pull-request-scoped concurrency lets us stop spending time on an obsolete revision without having different workflows cancel each other.

Here is a small illustrative example for ordinary pull request CI, not a drop-in release policy:

on:
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
  cancel-in-progress: true

We added scoped cancellation to ordinary build workflows in Tiny Clips and OutroKit. Release publishing and deployment workflows were not the target.

Cancellation cannot refund work that already ran. Its payoff depends on how often updates overlap and how much execution remains when the new commit arrives. Measure that instead of assuming every canceled run saves its full duration.

Measure the cache, including moving the cache

This was my favorite part of the investigation.

In Tiny Clips, we tried separate NuGet download caches for direct x64 builds, direct ARM64 builds, Store builds, and tests. That sounds pretty reasonable. However, downloading and unpacking a cache takes time too.

For the direct builds, warm cache-plus-restore paths were 16 seconds slower for x64 and 10 seconds slower for ARM64 than direct restore without the cache. Even the Store cache, which improved over its cold-fill path, was about seven seconds slower warm than restore alone.

We removed all three build caches.

The test cache was different:

Test dependency setup path Observed time
Restore without the cache 32 seconds
Cold cache path, including its save 44 seconds
Warm cache path, including cache overhead 21 seconds
Tiny Clips test setup took 32 seconds without a cache, 44 seconds on the cold cache path, and 21 seconds warm. Warm saved 11 seconds versus no cache.
Observed setup timings, not total workflow durations. One cold/warm pair is useful evidence, not a repeated controlled benchmark.

That is an 11-second warm setup improvement, about 34% of this setup phase. Not a 34% faster application build! The retained test cache was about 0.45 GiB, compared with roughly 3.80 GiB across the initial cache experiment.

The decision rule is pretty simple:

Compare cache lookup, download, extraction, remaining restore, and post-job save against the no-cache restore. A green "cache hit" message is not the finish line.

We kept full platform coverage. Tiny Clips validation included Windows packages for both architectures and the Store flavor, 2,388 Core test cases with zero failures and four skips, 218 passing App tests, plus 398 Mac tests and a Mac App Store build.

In OutroKit, we retained fresh restores in each job and scoped NuGet download caches by OS, architecture, and dependencies. The full Linux, Windows, Intel Mac, and Apple Silicon Mac test matrix remained. All six jobs passed, and the pack job reused the exact build cache. That establishes correctness and reuse, not a controlled speedup measurement.

Follow-up: where does the Windows cache live?

After that experiment, Awake Coding's post on Windows .NET builds and NuGet cache placement gave us another thing to measure: where NuGet reads and writes locally, not whether to download an archive from actions/cache. Those are separate decisions!

We ran four fresh GitHub-hosted Windows x64 jobs each for Tiny Clips and my file converter: two default-location jobs and two with NuGet's global packages and HTTP caches under RUNNER_TEMP. Both caches started empty; remote Actions cache transfer was off. Within each app, source, SDK, workspace, toolchain, and commands stayed the same. We tested both cache paths together, not each variable's contribution independently.

Default caches were on C:, while temporary files and the workspace were on D:. All jobs used the same Windows image, but one temporary-location job in each pilot landed on newer EPYC 9V45/NVMe hardware, versus EPYC 7763/SAS for the other three. The faster-hardware jobs are excluded below. These are independent machines in the same broad CPU/storage class, not repeated paired lab measurements.

Cold-cache pilot Default restore, two jobs Temporary-location restore, one comparable-class job Whole experimental job, default → temporary
Tiny Clips 51-56 seconds 13 seconds 227-237 → 183 seconds
File converter 53-61 seconds 10 seconds 357-368 → 259 seconds

That is a promising cold-restore signal across WinUI and MAUI! Other phases barely changed: Tiny Clips' build/package step went from 138-144 seconds to 137; the converter's compilation from 48-52 to 46. Converter workload installation stayed around 20-22 seconds and tests around 41-43. This is separate from the isolated-SDK improvement described below; that setup was unchanged between the converter arms.

All four Tiny Clips jobs passed direct x64 build and MSIX inspection; this pilot did not run tests or ARM64 builds. All four converter jobs passed the Windows app build, native x64/ARM64 checks, and 15 Bridge plus 297 Core tests, with three expected Apple-only skips per job. Neither pilot exercised Store packaging or release/signing paths. Whole-job times include provisioning and diagnostics, not a promise of production CI duration. The Tiny Clips report and converter report preserve the experiment details.

Here is an illustrative GitHub-hosted Windows step, before SDK setup, remote cache restore, or any NuGet-consuming command:

- name: Put NuGet caches under runner temp
  if: runner.os == 'Windows' && runner.environment == 'github-hosted'
  shell: pwsh
  run: |
    $packages = Join-Path $env:RUNNER_TEMP 'nuget-packages'
    $http = Join-Path $env:RUNNER_TEMP 'nuget-http'
    New-Item -ItemType Directory -Force -Path $packages, $http | Out-Null
    "NUGET_PACKAGES=$packages" >> $env:GITHUB_ENV
    "NUGET_HTTP_CACHE_PATH=$http" >> $env:GITHUB_ENV

Verify your actual paths and disks first: RUNNER_TEMP does not guarantee a faster D: drive, especially on self-hosted machines. Preserve existing deliberate local/NVMe cache placement. If you also use actions/cache, point its package path at ${{ env.NUGET_PACKAGES }} and version the key when changing the cache layout, retaining OS, architecture, and dependency scoping. Do not remotely persist the HTTP cache or credential paths.

Faster feedback is not automatically a smaller bill. The comparable Tiny Clips jobs all round up to four runner-minutes; the converter observations round from six/seven to five. These public repositories use free standard hosted compute, so neither is a measured dollar saving. These pilots did not change production workflows. Tiny Clips' retained test cache was already on D:, and the converter uses a remote package cache in production: neither warm-path benefit was measured here.

My takeaway: try local placement for cold restores, then measure the whole job and any remote transfer separately. It does not overturn the decision to remove slower remote build caches, and these pilots are not included in the cost estimate below.

Fix the environment, not just the cache

Across other cross-platform apps, Windows MAUI workload installation was an obvious hotspot: roughly six and a half to seven minutes in representative jobs.

Tempting solution? Cache the workload folders and skip installation.

Not so fast. The logs showed MSI-managed components and machine-wide Windows Installer state. Restoring a directory is not equivalent to installing that toolchain correctly. We deferred workload-folder caching rather than trade a slow reliable build for a fast mysterious one.

But that did not mean giving up on setup. In my file converter, the preinstalled SDK was reconciling unrelated Visual Studio Android, Apple, and WebAssembly workloads when we only wanted maui-windows. Instead of caching that machine-wide state, we used actions/setup-dotnet with DOTNET_INSTALL_DIR pointing to a fresh directory under runner.temp. The isolated SDK uses file-based workload installation; we still install and verify the pinned workload explicitly. No registry edits, deleting installer markers, or pretending a folder restore is an installation.

The observed Windows workload-install step went from 5 minutes 55 seconds to 20 seconds, and the whole job from 10 minutes 39 seconds to 3 minutes 48 seconds. All seven hosted checks passed, including the Windows app build and native shell checks. These are observations across workflow revisions, not a controlled benchmark attributing every second to SDK isolation; Store packaging was not dispatched.

Observed Windows timings across workflow revisions: workload installation fell from 5 minutes 55 seconds to 20 seconds; whole job duration fell from 10 minutes 39 seconds to 3 minutes 48 seconds. Installation is part of the job, not an additional saving.
Observed hosted runs, not a controlled benchmark. Workload installation is included in total job time; do not add the two reductions together. Store packaging was not exercised.

Some optimizations should stay on the drawing board

Apple pipelines had a similar lesson: a small test body can sit inside a much longer compile, install, simulator startup, and test operation. Profile the whole operation before deciding that deleting tests is the answer.

In one iOS cache experiment, the warm run avoided recompiling unchanged Swift files and cut a portable test step from about two minutes to 17 seconds. Awesome, right? Except the whole workflow went from 11 minutes 15 seconds cold to 14 minutes 53 seconds warm. Setup and native-test overhead outweighed the compilation win. One pair is not a controlled benchmark, but it certainly does not justify claiming overall savings.

We rejected that iOS cache experiment rather than keep the overhead and complexity. The final proposal keeps earlier simulator startup and disables Homebrew's automatic update/cleanup, with all 52 tests preserved. Its uncached run took 11 minutes 27 seconds, near the audit median, so I am counting zero overall savings from it. Source-built media caching in my file converter also has no verified hosted warm-hit benefit yet.

For the converter, I also explicitly chose faster automatic PR checks with full Mac proof on demand. The initial full Mac job took 9 minutes 17 seconds; compile-only checks took 3 minutes 10 seconds in one run and 4 minutes 15 seconds in the latest. Those are observational timings on differing runner images, and this is an intentional change in coverage frequency, not a cache speedup or identical work running faster. Automatic Mac compilation/static guards remain; the manual media, sandbox, and runtime proof must run for the exact release candidate before Store approval. Store signing and approval boundaries stay separate.

The media cache also correctly missed when the runner image changed. That is a safety check doing its job, not evidence of a warm-cache speedup. A reusable media cache must contain verified unsigned source-built outputs, never signing credentials or signed app bundles.

The prompts were a starting point, not the quality gate

After the analysis, my next actual prompt was:

Let's send PRs for all of these please with optimizatons

In the GitHub Copilot app, that became separate, isolated coding sessions for the repositories. Each could inspect its own workflow, make changes, and bring back a pull request and hosted CI evidence. The useful part was being able to coordinate the results: keep the good cache, remove the slower ones, preserve the required checks, and challenge optimizations that were not safe.

For a repeatable version of this process, here are suggested follow-up prompts, not quotations from my original conversation:

Analyze this GitHub Actions usage export alongside the workflows
and representative job logs. Separate runner execution time,
feedback latency, gross usage, discounts, and net billed charges.
Identify the highest-impact changes and the evidence for each.
Do not publish the raw export or private billing details.
For each proposed optimization, preserve required check names,
platform coverage, signing boundaries, and release gates unless
I explicitly approve a behavior change. Explain what can run on
Linux, what must stay native, and how failures propagate.
Measure no-cache, cold-cache, and warm-cache setup, including
lookup, transfer, extraction, restore, and post-job save time.
Remove caches that do not beat the no-cache path. Report sample
size and limitations; do not claim overall workflow speedups
from setup-only measurements.
Validate with hosted CI and report exactly what passed, what was
not exercised, and whether the PR is open or merged. Do not
dispatch a signed release just to test an optimization.

I like this approach because the agent is doing the investigation and implementation, but the acceptance criteria are still concrete. "It should be faster" is not enough.

So, how much time and money does this save?

Putting the changes together, a reasonable planning scenario is roughly 45-51% less nominal compute cost and 10-20% fewer runner-minutes for the example workload below. That combines cheaper runners, less redundant execution, and the test cache that actually helped. It is an estimate informed by our tests, not a measured reduction across my account.

Illustrative combined estimate, with the original workload indexed to 100: nominal compute cost falls to about 49-55, and runner execution time falls to about 80-90. This models runner migration, avoided redundant execution, and test setup caching; it is not measured account savings or pull request latency.
Combined planning scenario, not my actual workload or bill. Lower is better; baseline = 100. Simulator/media-cache gains, Windows SDK isolation, and on-demand Mac proof are excluded from this model.

The assumptions: 100 five-minute jobs split 40 Linux, 20 Windows, and 40 Mac; move 20 eligible Mac jobs to Linux at equal runtime, then avoid 10-20% of execution proportionally through cancellation/no-change routing, net of routing overhead. Twenty of the original Linux jobs also get the observed test-setup cache benefit, assuming 80% warm hits and including cold-cache penalties, scaled down with the avoided work. That works out to about 52-102 runner-minutes saved per 100 jobs. The job mix and avoidance rate are illustrative assumptions, not measurements of my fleet.

Actual billed savings depend on free usage, allowances, and per-job rounding, which this approximate model does not calculate. Less total execution also does not mean every PR finishes 10-20% sooner. Still, it gives us a useful target to compare with the next usage report, without counting unproven optimizations as wins!

Make the next build smarter

The big takeaway for me is not to use fewer agents or stop building cross-platform apps. It is to make the CI graph as intentional as the code those agents are changing.

Start with your usage export. Follow the expensive work into job logs. Put portable work on the right runner, stop obsolete builds, keep required checks truthful, and make caches earn their place.

Then let the agents help with the cleanup, and ask them to bring back the evidence. More pull requests do not have to mean proportionally more wasted CI.

]]>
Continuous Integration & Delivery for Xamarin & .NET Libraries with Azure DevOps (VSTS) https://montemagno.com/continuous-integration-delivery-for-net-libraries-with-vsts/ 5af5cc81dc592200bfc9400b Mon, 21 May 2018 19:20:00 GMT I build a lot of .NET libraries that include simple .NET Standard libraries to fully cross-platform plugins for Xamarin and Windows that target up to 10 different platforms. When I got started building libraries and shipping them to NuGet I went down a simple path of just building everything locally on my machine and manually uploading them to NuGet. This worked great for a single library, but as soon as I took on a second... third... forth... and so on, things got out of hand and I knew I had to put some sort of CI/CD in place. I went down many paths over the years and have now settled on using Visual Studio Team Services to fully automate the build of every library I create and a full distribution pipe line that goes to MyGet then to NuGet preview, and finally NuGet public release with a click of a button. Now I can develop and let VSTS handle everything for me so I can be productive:

If you look at this screenshot from VSTS it is everything that I wanted for so long in my development cycle. I push code, things are built, and automatically packages are pushed. In this post I am going to show you how I set this up for my projects and how you can too in just a few minutes.

.NET Standard and Multi-Target Projects

Before we start to setup our CI/CD for libraries I highly recommend you move your projects to SDK style projects. You can read my full guide on how to convert a PCL to .NET Standard or how to convert a Xamarin.iOS or Xamarin.Android project to SDK Style.

Everything that I develop now is based on a single .NET Standard 2.0 library or a Multi-Targeted project for plugins. I have previously written on how to convert your PCLs to .NET Standard and also have templates for Visual Studio 2017 to create Multi-Target projects for plugins.

SDKStyle

The reason to use these types of projects is that they include full NuGet packaging including all metadata and dependencies. This also means you can fully delete the .nuspec file that always gets out of sync. It is worth your time to update any and all projects to these types of projects to simplify your life.

If you need to learn more about multi-targeting be sure to check out Oren's amazing MSBuildSdkExtras package which is the core of my templates and all of my projects.

If you are building only an iOS or Android library do not fear as you can still just convert your existing library over to an SDK Style. Take a look at my MonoDroid.Toolkit csproj on GitHub.

Build Definition in VSTS

My builds inside of VSTS are actually really simplistic and only contain 6 steps to fully build my libraries. You can use the Hosted Visual Studio 2017 machines, however I have a small Intel NUC that runs a local VSTS agent so all my NuGets are cached which reduces my build times to seconds instead of minutes.

Empty Definition

Select New and pick your source code that you would like to build. I then like to select Empty process because we can easily fill it in.

Empty

NuGet Steps

Under Phase 1 we are going to add two NUGet steps.

First is the NuGet Tool Installer task and change the Version to 4.5.1

NuGetInstaller

Second will be the regular NuGet task which we will use the restore command on our solution that we are going to build. I like to pick the specific .sln or folder where it lives (no need to build other stuff).

NuGetRestore

Build Steps

I build my libraries twice! That is right twice! Once for a "beta" NuGet and once again for "public" NuGet. They are super quick and all it is really doing is changing the version flags. The reason I do this is for my release definition that will allow me to have a single release that goes from beta to public easily. We will be building the .csproj specifically using the MSBuild task:

MSBuild

We will add it twice so we can build the beta and public releases. Name the first Build Beta and the second Build Public. For each of them set the following:

  • Project: Select the .csproj specifically
  • Configuration: $(BuildConfiguration)

For MSBuild Arguments user the following for Beta:

/t:restore;build;pack /p:PackageVersion=$(Build.BuildNumber)-beta /p:PackageOutputPath=$(build.artifactstagingdirectory)/beta /p:AssemblyFileVersion=$(Build.BuildNumber)

For Public use:

/t:restore;build;pack /p:PackageVersion=$(Build.BuildNumber) /p:PackageOutputPath=$(build.artifactstagingdirectory)/public  /p:AssemblyFileVersion=$(Build.BuildNumber)

This will ensure that the build numbers are configured correctly and copy the artifacts to different folders.

Optionally, you may have some additional unit tests or device runners that may need to be run here.

Copy and Publish

The next thing to add is the Copy Files task to stage the NuGet packages for release.

Set these properties:

  • Source Folder: $(build.sourcesdirectory)
  • Contents: **\bin\$(BuildConfiguration)\**\*.nupkg
  • Target Folder: $(build.artifactstagingdirectory)

Finally, add the Publish Build Artifacts task and set the following:

  • Path to publish: $(build.artifactstagingdirectory)
  • Artifact name: drop
  • Artifact public location: Visual Studio Team Services/TFS

This will pull only .nupkg files that we will distribute into a folder that release can pick up.

All in all the definition should look like:

AllDone

Configure Version Number

Under the Options tab in the build definition I like to format the Build number format such as: 4.5.0$(rev:.r)

This means that the next version would be: 4.5.0.1 and then 4.5.0.2.

Now if you change this to: 5.0.0$(rev:.r) it will reset the revision so it will be 5.0.0.1.

Other things!

You will probably want to setup *Triggers so CI and PR validation are configured. This means that every time master is pushed to a package is generated.

Release Definition in VSTS

Now we have our full build complete it is time to setup our release. Our goal for me was to automatically have every build of master go to MyGet and then manually approve the next release to go to NuGet (beta) and finally another approval to go to NuGet with the public non-beta package.

Battery-CD-1

Head over to the Releases section and create a new release definition with an Empty process. Think of Environments as the stages of release. We can create three of them to define each release of the artifacts. Inside of each environment are a sequence of tasks, just like our build definition, that will be run. For my libraries it is really simple as each has just one task. However, before the NuGet beta and production environments I turn on pre-deployment approval steps.

Push to MyGet

In our first environment we will want to push to MyGet. Add the NuGet task with the following settings:

  • Command: push
  • Path to NuGet package: $(System.DefaultWorkingDirectory)/**/drop/beta/*.nupkg
  • Target Feed: External NuGet server

For this step you will want to configure the feed information from MyGet:

MyGet

Push NuGet Beta

For this step you can select Clone or if you already create the new environment simply add the NuGet task again. The settings will be the same as above:

  • Command: push
  • Path to NuGet package: $(System.DefaultWorkingDirectory)/**/drop/beta/*.nupkg
  • Target Feed: External NuGet server

However, we will now want to select or configure NuGet as our destination:

NuGet

Head back to Pipeline and for this environment we don't want it to automatically trigger, we want to approve this release. You will see a lightning bolt and a person shape on the left side of the environment to configure pre-deployment conditions. I turn on pre-deployment approvals and select myself and turn off the policies since I am a solo developer:

predeploy

There is all sorts of rad stuff you can do here on pre and post-deployment so you should totally look into that for larger projects.

Push NuGet Production

Add or clone another environment for after the Beta environment. This is going to be almost the same as the previous step but we are going to change our Path to NuGet package:

  • Command: push
  • Path to NuGet package: $(System.DefaultWorkingDirectory)/**/drop/public/*.nupkg
  • Target Feed: External NuGet server

Notice I am using public here :)

Add Artifact

Finally, click the big Add artifact button on the left side and select the build definition that you created earlier. Also tap on that little lightning bolt and turn on the Continuous deployment trigger so this flow automatically happens!

CI/CD Pro!

You did it! You are totally a Continuous integration and delivery pro!! Enjoy a seamless deployment of every push to master to MyGet and then approval steps to push to NuGet for any and all of your .NET Libraries. <3 Feel free to ask questions below.

]]>
Building Xamarin.Android Apps in Azure DevOps (VSTS) with Hosted Agents! https://montemagno.com/building-xamarin-android-apps-in-team-services-vsts-with-hosted-vs-2017-agents/ 5a76277cda32ce0022cbd5ef Sat, 03 Feb 2018 21:42:30 GMT I recently wrote about how to build any Xamarin.iOS application inside of Visual Studio Team Services using Hosted macOS Agents for free! I have updated the guide a little bit if you are leveraging .NET Standard libraries to ensure that you build with MSBuild, which is pretty important seeing that all of the new templates will soon use these. That had me thinking "I wonder if there are any small tweaks that are needed for Xamarin.Android when building on the Hosted Agents?"... Which led me to finding a few tweaks that I figured I would blog about.

If you want to understand my passion for VSTS then make sure to listen to Merge Conflict Episode 71.

Xamarin.Android Template

When you head into VSTS and create a new build definition you will find the Xamarin.iOS and Xamarin.Android templates. Go ahead and select that Xamarin.Android template!

NewTemplatesAndroid

For now go ahead and:

  1. Delete the Xamarin Test Cloud Step
  2. Disable the MSBuild step for Test. If you have tests go ahead and leave it on.

Select Build Host

Under Process we can fill in some additional information for the build such as what Solution to do a NuGet restore on and what build host to use. For now I will select the Hosted VS2017 agent.

HostedVS2017

Use Nuget 4.5.1

Since the templates in VS 2017 for Xamarin.Forms apps include a UWP (Fall Creators Update app) we will need to update NuGet to use the latest version, which happens to be 4.5.1. This seems necessary due to changes with .NET Standard.

NuGet

Tweak Android Build Task

We recently changed how the default Android templates are generated on disk and create a csproj that is titled: .Android.csproj Previously it was .Droid.csproj. Regardless the default csproj the Android task tries to build is a regex that looks like:

**/*Droid*.csproj

I am not a fan of using a regex regardless and always pick the specific project, but you should make sure it matches your actual projects. So for me it would be:

**/*Android*.csproj

Like I said, be sure to check your project names or pick the specific one to build, which is what I recommend.

Finally, there are multiple versions of the JDK and Java that are installed. As of today Xamarin.Android apps should be built against JDK 8 x64, which you can select under Advanced.

Final-Build

Queue a Build!

That is it! You can queue up a build and will have a full Android app built up. The final thing that you probably want to do is upload your keystore and ensure you Sign and do a Zip Align in that task step.

How about Hosted macOS?

I knew you would ask! The only difference is that you DO NOT need to modify the Java JDK version to use. It looks like macOS has a specific version installed and you can just leave it to default for now.

Want More VSTS?

We use VSTS for all sorts of different applciations and I recently sat down with Damian Brady on his show The DevOps Lab to talk about how we use VSTS to build the Xamarin Live Player.

]]>
Introducing Azure DevOps (VSTS) Mobile App Tasks Extension for iOS and Android https://montemagno.com/introducing-vsts-mobile-build-tasks-extension/ 5a3816bb538da90021c4b262 Mon, 18 Dec 2017 19:43:13 GMT

This year I have made a big push towards making Visual Studio Team Services my new continuous everything for my mobile apps, libraries, and any other project that I work on. While Azure DevOps (VSTS) has a lot of built in tasks for mobile applications, there are still a few essential things missing that would really make it a joy to use. The top that stuck out was having the ability to easily bump the version/build numbers for iOS and Android. The other was to change the app's label and package name, which can be nifty when doing different build types for QA/Production. This is where my brand new and open source Azure DevOps (VSTS) Mobile App Tasks Extension comes in!

Mobile-Tasks

Simply install the extension and four brand new tasks are ready to take building mobile apps in Azure DevOps (VSTS) to the next level.

MobileTasks2

Here is a full breakdown of each task:

Android

Multiple tasks to update the AndroidManifest.xml file

Bump Version

Change app's version name and code at build time.

Inputs:

  • sourcePath: Path to android manifest
  • versionCode: code number that must be an integer
  • versionCodeOffset: a specific number to increment the version code
  • versionName: user visible name
  • printFile: output the file before and after changing variables

Package Identifiers

Change app's package name.

Inputs:

  • sourcePath: Path to android manifest
  • packageName: name to replace (com.company.app)
  • appLabel: application name to replace (App Name)
  • printFile: output the file before and after changing variables

iOS

Multiple tasks to update the info.plist file, must be run on macOS

Bump Version

Change app's version name and code at build time.

Inputs:

  • sourcePath: Path to info.plist
  • versionCode: code number that must be an integer
  • versionCodeOffset: a specific number to increment the version code
  • versionName: user visible name (short code)
  • printFile: output the file before and after changing variables

Bundle Identifiers

Change app's bundle name and identifier

Inputs:

  • sourcePath: Path to android manifest
  • bundleIdenifier: identiier to replace (com.company.app)
  • bundleName: name to replace (App Name)
  • printFile: output the file before and after changing variable

Open Source

I have to say that none of this would have been possible without the help of Andrew Hoefling who helped not only convert the bash scripts I had to powershell core, but then again to TypeScript!. He is awesome and really made this all possible. That being said, if you go over to project's GitHub page you will be able to find all the tasks written in Bash, PowerShell Core, and TypeScript. Have ideas for more great mobile specific tasks? Open up an issue on the GitHub page or send down a PR!

]]>
Building Xamarin.iOS Apps in Azure DevOps (VSTS) with Hosted macOS Agents! https://montemagno.com/building-xamarin-ios-apps-in-team-services-vsts-with-hosted-macos-agents/ 5a204fd19f3eff0022eb6594 Thu, 30 Nov 2017 20:38:16 GMT

Azure DevOps (VSTS) is quickly becoming one of my favorite products that Microsoft creates for developers. It helps any developer writing apps in any language build, test, and deploy their applications. On top of that it has free unlimited private Git repos, work item tracking and kanban board, test, release management, and so much more! What is cool is you can use some of it or all of it, up to you. So if you have your code in GitHub, no problem, want to use Trello, no problem, because they all work great together.

IQsRzovW

Since I am a cross platform developer, Azure DevOps (VSTS) is awesome because I can build any of my iOS, Android, Windows, and ASP.NET backends all in one place. On top of that I can do complete release management straight to AppCenter Test/Distribute, Azure, or directly into the App Store. You can hear about my love and passion for Azure DevOps (VSTS) on Merge Conflict Episode 71.

Azure DevOps (VSTS) has always offered hosted Windows VMs for use and recently Linux VMs that could build apps in the "cloud", which is pretty awesome. This is nice becuase all of the tooling is installed and ready to go on demand. There was always one operating system missing, which was macOS... which is required for iOS and macOS development. This wasn't too bad because you can actually download the Azure DevOps (VSTS) build agent on your own macOS machine and point VSTS to it. This is what I normally like to do, but it is also conveinent to run stuff in the cloud, which is why I am excited that they are now offering a hosted macOS agent! It is super easy to use and has some nice scripts installed for Xamarin developers.

Setting up an iOS CI Build

It couldn't be easier to get started with Azure DevOps (VSTS) for Xamarin.iOS. In fact there is a pre-built template with everything setup that you need.

Xamarin-Templates

Hosted macOS Agent

Once the build is setup it is easy to use the hosted macOS agent. Under Process tap on the Agent and hosted macOS will show up!

HostedMacOS

Build with MSBuild (VS for Mac)

One little gotcha seems to be that if you are using new project structures or .NET Standard you will need to use MSBuild (this is the default now in VS for Mac anyways). So make sure you flip the switch under advanced:

BuildwithMSBuild

At this point you are ready to go, and just like that you are off to the races building your iOS apps inside of Azure DevOps (VSTS) on a hosted macOS agent!

]]>
Version Bumping iOS & Android apps in Azure DevOps (VSTS) https://montemagno.com/version-bumping-ios-and-android-apps-in-visual-studio-te/ 598df8f2c121b60019bf2fd4 Wed, 22 Feb 2017 17:46:37 GMT I love Continuous Integration and Deployment for mobile applications and have given several talks on it in the past. It streamlines your entire development process and makes it drop dead simple to simply commit code and have everything you don’t want to do manually be taken care of for you. This means build, test, bump versions, and ship off to testers with integrations like HockeyApp. Notice I mention “bump versions”, which is actually really important. Every build should be assigned a unique number so developers and testers know exactly what is out there. There is nothing worse then seeing 1.0 on every single build and it makes it impossible to ship out to the app store. Today, I will walk you through bumping version numbers on your iOS and Android apps. 

If you are looking to get your Xamarin apps setup in VSTS then checkout my blogs for Android and iOS that offer a full walk through. This blog will work with any iOS or Android app regardless of how you build it :)

ALM Build & Release Tools

There unfortunately is no out of the box support from what I have seen to actually bump versions, but we can leverage a marketplace extension from Colin called ALM Build & Release, specifically the “Version Assemblies” step.

Android Version Bumping

In my Android manifest my name and code look like this:

android:versionCode="1" android:versionName="1.0.0"

These are important, as we will use a regex to update them.

We will want to add the “Version Assemblies” task TWICE, once for the version name and once for the version code.

Once added, we will bump the name.

First insert the following (with examples):

  • Source Path: src/MobileApps/MyDriving/MyDriving.Android/Properties
  • File Pattern: AndroidManifest.xml
  • Build Regex Pattern: (?:\d+.\d+.\d+.)(\d+)

Under Advanced:

  • Build Regex Group Index: 0

  • Regex Replace Pattern: versionName=“\d+.\d+.\d+

  • Prefix for Replacements: versionName=”

What this will do is update the version name to the Build number format found under “General”, which mine is set to 1.0.0$(rev:.r)

Now for the next one, which is the version code:

  • Source Path: src/MobileApps/MyDriving/MyDriving.Android/Properties
  • File Pattern: AndroidManifest.xml
  • Build Regex Pattern: (?:\d+.\d+.\d+.)(\d+)

Under Advanced:

  • Build Regex Group Index: 1

  • Regex Replace Pattern: versionCode=“\d+

  • Prefix for Replacements: versionCode\

And just like that you are good to go. This will simply update it with the current version revision :)

iOS Version Bumping

This one is a bit trickier and requires some specific things set in the Info.plist. We will need to ensure that the following are set to these very very specific version numbers in the actual Info.plist:

  • CFBundleShortVersionString: 1.0.1

  • CFBundleVersion: 1.1.1 

Don’t ask me why, just trust me!

Under General I have set my build number format to 1.2.$(rev:.r):

I think we just happened to be on 1.2 which is why it is like this, your mileage may vary here. Now we will add the Version Assemblies task TWICE, just like we did for Android.

Let’s first bump the CFBundleShortVersionString:

  • Source Path: src/MobileApps/MyDriving/MyDriving.iOS
  • File Pattern: /Info.plist
  • Build Regex Pattern: (?:\d+.\d+.)(\d+)

Under Advanced:

  • Build Regex Group Index: 1

  • Regex Replace Pattern: 1.0.\d+

  • Prefix for Replacements: 1.2.

That will bump it to 1.2.BuildNumber. 

Let’s then bump the CFBundleVersion:

  • Source Path: src/MobileApps/MyDriving/MyDriving.iOS
  • File Pattern: /Info.plist
  • Build Regex Pattern: (?:\d+.\d+.)(\d+)

Under Advanced:

  • Build Regex Group Index: 1

  • Regex Replace Pattern: 1.1.\d+

  • Prefix for Replacements: 1.2.

As you can see I have to execute these in a specific order to not bump versions differently. It is tricky, but so are regex. And there you have it, bumping versions for both iOS and Android inside of Visual Studio Team Services.

]]>