Grabbing BC Artifacts in Your Pipelines via an Azure Function and How It All Works

In this Areopa Academy webinar, Tobias Fenster (Managing Director at 4PS Hilty, Microsoft MVP for Azure and Business Applications) and Arthur van de Vondervoort (developer at CompanyFUI and active contributor to the Business Central community) walk through the BC Artifact URL Proxy — an Azure Function that caches artifact URL lookups — and the companion Azure DevOps extension that integrates it directly into CI/CD pipelines. Moderator Bert Verneek opens the session and hosts the Q&A.

Why the BC Artifact URL Proxy Exists

Business Central containers require artifact URLs to build images. Fetching those URLs is done through the Get-BCArtifactUrl cmdlet from the BcContainerHelper PowerShell module — and it can take anywhere from 30 seconds to over a minute, particularly for sandbox artifacts.

The cost adds up quickly. A single pipeline run at Arthur’s company calls the cmdlet three times (once to generate a base translation file, once to compile the app, and once in the test stage). Each pull request triggers the pipeline at least twice. For teams running nightly builds or next-minor/next-major checks across dozens of projects, the cumulative wait time can stretch into hours.

There are also scenarios where PowerShell simply isn’t available — for example, when spinning up an Azure Container Instance through the Azure portal. The proxy solves both problems: it provides a plain HTTP endpoint that returns artifact URLs, and it caches results so subsequent requests are served in milliseconds rather than minutes.

Slide: Why BCArtifactUrl-Proxy — lists the problem of repeated artifact URL fetching across pipelines and partners
▶ Watch this segment
📖 Docs: Running a container-based development environment — Microsoft Learn — official guidance on using BC containers for local and pipeline-based development.

How the Proxy Is Built

The proxy is a .NET 8 Azure Function with a single HTTP trigger. When a request arrives, it validates the parameters (type, country, version, and optional flags such as select, before, after, cacheExpiration, and accept_insiderEula), constructs the equivalent Get-BCArtifactUrl command, and checks an in-memory cache before running anything.

If a matching cache entry exists and falls within the allowed expiration window, the URL is returned immediately with response headers indicating a cache hit and the timestamp of the cached entry. If there is no valid cache entry, the function runs the PowerShell command, stores the result, and returns it. The default cache lifetime is one hour; callers can shorten it to a minimum of 15 minutes via the cacheExpiration parameter.

Slide: Architecture flow diagram of the BC Artifact URL Proxy — request comes in, parameters validated, bccontainerhelper command created, cache checked, URL returned
▶ Watch this segment

By default, the function redirects the caller directly to the artifact URL. Setting the doNotRedirect=true query parameter returns the URL as a plain text response body instead — useful when you want to capture the URL as a pipeline variable rather than follow the redirect.

Behind the scenes, the proxy does not include a full BcContainerHelper installation. It downloads only the two scripts it needs — Get-BCArtifactUrl.ps1 and HelperFunctions.ps1 — directly from the navcontainerhelper GitHub repository at container startup. This keeps the image lightweight while ensuring that the exact Microsoft-provided script logic is used without modification.

Source Code Walkthrough

The core logic lives in GetUrl.cs. The HTTP trigger is registered at the route bca-url/{type?}/{country?}/{version?}, which means the type, country, and version can be provided as path segments or as query string parameters. Tobias showed this during the live demo by running the function locally with F5 from inside a dev container.

VS Code showing the GetUrl.cs Azure Function source code with route parameters for type, country, and version
▶ Watch this segment

The response headers expose the BcContainerHelper version in use (X-bccontainerhelper-version), the full PowerShell command that was executed (X-bccontainerhelper-command), whether the result came from the cache (X-bcaup-from-cache), and the cache entry timestamp (X-bcaup-cache-timestamp). These headers make it easy to audit what the proxy did and how fresh the cached data is.

VS Code showing the cache lookup and cache population logic in GetUrl.cs — cache hit returns immediately, cache miss calls BcContainerHelper
▶ Watch this segment

The demo showed a first request taking 40.5 seconds with a cache miss. A second identical request returned in 62 milliseconds from the cache — illustrating the core benefit in a single before-and-after comparison.

VS Code with the test.http file and the HTTP response showing a 40-second initial artifact URL fetch with cache miss headers
▶ Watch this segment

Development Setup with Dev Containers

The repository ships with a fully configured dev container. To contribute or explore the code, install Docker Desktop and the Dev Containers extension for VS Code, then use Clone Repository in Container Volume. The container starts in around five to ten minutes on first run (image pull included) and pre-installs the Azure Functions Core Tools, Azure CLI, Docker-outside-of-Docker, PowerShell, and a set of recommended VS Code extensions.

The Dockerfile downloads the configured version of BcContainerHelper at build time, extracts just the two required scripts, and patches the Export-ModuleMember call so the scripts can be used outside of a module context. Hitting F5 starts the function host locally so you can send test requests immediately.

📖 Docs: Develop Azure Functions using Visual Studio Code — Microsoft Learn — covers local development, debugging, and deployment of Azure Functions from VS Code.

Running the Proxy on Azure

The production instance is hosted as a containerized Azure Function App. Tobias builds the Docker image locally, pushes it to Docker Hub, and then updates the image tag in the Azure Portal’s Deployment Center. Application Insights is configured automatically and provides visibility into request volume, response times, and failures.

Over the 30-day window shown in the demo, the proxy received approximately 14,000 server requests with near-100% availability. Response time peaks coincide with cache misses where the underlying PowerShell command takes 40–60 seconds, but the vast majority of requests are served from cache in milliseconds.

Azure Portal Application Insights dashboard for bca-url-proxy showing ~14,000 server requests over 30 days with near-100% availability
▶ Watch this segment

Tobias noted that CI/CD for the proxy itself is not in place today — releases are infrequent enough that a manual build-push-update cycle is sufficient. He flagged this as something that may change as the project evolves.

📖 Docs: Work with Azure Functions in Containers — Microsoft Learn — explains how to build, push, and deploy custom container images to an Azure Function App.

The Azure DevOps Integration Problem

Arthur described how his company’s pipelines were structured: a build stage (compile app twice for translation file generation, then compile with language files), a test stage (Docker image, container, publish apps), and a publish stage. The build stage called Get-BCArtifactUrl to fetch the daily sandbox artifact URL — and that single call could take up to two minutes.

With three calls per pipeline run and two runs per pull request (once for the PR itself, once after merge to main), the team was spending up to ten minutes per pull request just waiting for artifact URLs. Nightly and weekly next-minor/next-major builds across dozens of projects pushed the total into hours.

Slide: Azure DevOps timing problem — BcContainerHelper can take up to 2 minutes, called 3 times per pipeline run, resulting in up to 10 minutes wasted per pull request
▶ Watch this segment

The fix started as a few lines of PowerShell making an HTTP call to the proxy and setting the result as a pipeline variable. With the proxy’s cross-pipeline cache, a second run that needs the same URL returns in under a second — even if the cache was populated by a completely different pipeline or project earlier in the day.

The Azure DevOps Extension

Arthur packaged the PowerShell approach as an open-source Azure DevOps extension published to the Visual Studio Marketplace: BC Artifact URL Proxy. The extension source is available at github.com/Arthurvdv/bcartifacturl-proxy-vss.

The extension has a single task with a set of familiar inputs: instanceUri (defaults to the community-hosted proxy), type, country, version, select (Latest, Daily, Weekly, NextMinor, NextMajor), accept_insiderEula, and cacheExpiration. Arthur’s team typically sets cacheExpiration to 43200 seconds (12 hours) so the URL is cached for the entire working day.

Slide: Azure DevOps Extension structure diagram showing the vss-extension.json manifest, task modules, task manifest files, and PowerShell scripts
▶ Watch this segment

The task writes the resolved artifact URL to the pipeline variable $(BCARTIFACTURL_PROXY), which subsequent steps can reference directly — for example, when passing it to BcContainerHelper or another tool that accepts the URL as an input parameter.

Pipeline Demo

Arthur ran a live demo using a small pull request against an AL project in Azure DevOps. The Get-BCArtifactUrl step completed in under one second, having retrieved a cached result from the proxy. The build stage finished in about 1 minute 20 seconds total — roughly half the previous duration — before surfacing a compiler warning as the intentional failure point.

Azure DevOps pipeline run showing the Get-BCArtifactUrl step completing in under 1 second using the proxy extension
▶ Watch this segment
📖 Docs: Add a Custom Build or Release Task in an Extension — Microsoft Learn — describes the structure of Azure DevOps extension tasks, including the task manifest and PowerShell handler pattern used in this extension.

Extension Source Code

The extension PowerShell script reads all task inputs via Get-VstsInput, logs them for diagnostics, and applies a few normalisation rules before calling the proxy. One important rule: if accept_insiderEula is true but the selected artifact type is not an insider build, the flag is silently set to false. This maximises the likelihood of a cache hit, since a request with accept_insiderEula=true and one without it are cached as separate entries.

VS Code showing the extension PowerShell fallback logic — if the proxy fails, the script falls back to installing BcContainerHelper directly
▶ Watch this segment

The script includes a fallback: if the proxy call fails or returns no URL, it installs BcContainerHelper from the PowerShell Gallery and calls Get-BCArtifactUrl directly. This means pipeline reliability is not dependent on proxy availability — the fallback adds a few minutes but keeps the pipeline functional.

Lessons Learned Building the Extension

Arthur shared a few takeaways from building his first Azure DevOps extension:

  • The extension task handler (PowerShell3) runs PowerShell 5.1. Upgrading to PowerShell 7 is not straightforward in the current Azure DevOps extension model — Arthur noted this is a known challenge that other extension authors face as well.
  • Automated testing for extension tasks is not yet in place. The PowerShell scripts can be run and validated standalone, but end-to-end automated tests for the extension as a whole are a future goal.
  • The extension structure is simpler than it initially appears: a vss-extension.json manifest at the root, a folder per task, and within each task folder a task.json, a PowerShell script, and a ps_modules folder. Arthur recommended Corne Hoskam’s blog post on creating Azure DevOps extensions as a helpful starting reference.

Q&A Highlights

How long does the cache live? By default, entries expire after one hour. The minimum is 15 minutes. After one hour and one minute, any request will always fetch a fresh URL regardless of what is in the cache.

Why would you host your own instance instead of using the community URL? Tobias identified two reasons: privacy (avoiding any external party knowing which BC versions you are targeting) and resilience (removing a dependency on a community-hosted service that could be discontinued). The extension makes it easy to point at a self-hosted instance by changing a single input value.

How long does it take to add the proxy to an existing pipeline? Arthur estimated around 30 minutes to an hour. The extension adds a step before the BcContainerHelper call, captures the URL as a pipeline variable, and passes it forward. Most BC pipeline tooling already supports accepting a pre-fetched artifact URL as an input.

Resources


This post was drafted with AI assistance based on the webinar transcript and video content.