Bicep Shared Modules: Skip-if-Exists Pattern Instead of --force
Bicep Shared Modules: Skip-if-Exists Pattern Instead of --force
At Christie's we have a well-documented setup – the platform team runs 76 shared Bicep modules in an Azure Container Registry and dozens of workload repositories consume them through br:registry/module:tag references. Versions are pinned, semver disciplined, schemas stable. Until the day Russell, the platform team lead, showed me a recent incident.
The Storage module cmn-storage:1.4.2 got overwritten as part of a hotfix. A consumer in the retail subscription saw propertyName Foo of ResourceType Bar is not allowed on the next az deployment. The pinned 1.4.2 had changed under their feet because the publish pipeline used --force.
That event spawned ticket DO-429: replace --force with a skip-if-exists pattern and enforce semver bumps in PR validation. Here is the playbook.
Why --force Existed in the First Place
The historical reason was convenience. The CI pipeline published on every merge to main, and if someone forgot to bump the version, the build failed. The team got used to writing --force as a quick fix instead of fixing semver hygiene. After 18 months it became the default behavior in the main pipeline.
# Before – publish-modules.yml (wrong)
- task: AzureCLI@2
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: bash
inlineScript: |
for module in modules/*/; do
name=$(basename "$module")
az bicep publish \
--file "$module/main.bicep" \
--target "br:$(acrName).azurecr.io/bicep/$name:$(version)" \
--force # <-- the problem
doneThree-bullet summary of the problem: silently overwrites published versions, no audit trail, no checkpoint where the version was supposed to bump.
The Skip-if-Exists Pattern
The principle: check whether the version already exists. If yes, log and skip. If no, publish.
#!/usr/bin/env bash
set -euo pipefail
ACR_NAME="$1"
MODULE_NAME="$2"
VERSION="$3"
# Check whether the tag exists
if az acr repository show-tags \
--name "$ACR_NAME" \
--repository "bicep/$MODULE_NAME" \
--query "[?@=='$VERSION'] | [0]" -o tsv | grep -q "$VERSION"; then
echo "::notice::Module $MODULE_NAME:$VERSION already exists, skipping."
exit 0
fi
# Publish (without --force)
az bicep publish \
--file "modules/$MODULE_NAME/main.bicep" \
--target "br:$ACR_NAME.azurecr.io/bicep/$MODULE_NAME:$VERSION"
echo "::notice::Published $MODULE_NAME:$VERSION"Three properties that make this script enterprise-ready:
set -euo pipefail– any error kills the pipeline, no silent failures- Skip logged via GitHub
::notice::– the job summary shows exactly what was published and what was skipped - No
--force– if the version exists, that is the end; an older version cannot be overwritten
PR Validation: Enforce Version Bumps
Skip-if-exists addresses the production risk. But you still risk a developer changing a module, forgetting to bump the version, and the merge passing – production consumers with pinned versions will never see the change.
The fix: a PR check that watches the diff and requires a semver bump for every changed module.
# .github/workflows/pr-validate-bicep.yml
name: Validate Bicep module changes
on:
pull_request:
paths:
- 'modules/**'
jobs:
semver-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for diff against main
- name: Check semver bump for changed modules
run: |
changed_modules=$(git diff --name-only origin/main...HEAD modules/ | awk -F'/' '{print $2}' | sort -u)
fail=0
for module in $changed_modules; do
metadata_file="modules/$module/metadata.json"
if ! git diff origin/main...HEAD -- "$metadata_file" | grep -q '"version"'; then
echo "::error::Module $module changed but version in $metadata_file not bumped."
fail=1
fi
done
exit $failCombining skip-if-exists at publish time with a PR check on the version means a change cannot be deployed with the old version by accident.
Consumer Side: How to Pin a Version Properly
A consumer repository pins a specific version:
module storage 'br:cmnregistry.azurecr.io/bicep/cmn-storage:1.4.3' = {
name: 'storageDeployment'
params: {
storageAccountName: name
location: location
sku: 'Standard_LRS'
}
}What not to do:
- Do not use the
latesttag – it physically exists in ACR but defeats semver - Do not use ranges – the Bicep registry has no range syntax (
^1.4.0); always a concrete tag - Do not publish modules from feature branches into the production registry – keep a separate
bicep-dev/repository inside ACR
Renovate / Dependabot for Bicep Modules
To keep consumer repositories current I recommend Renovate, which understands Bicep registry references natively since v39:
// renovate.json
{
"extends": ["config:recommended"],
"bicep": { "enabled": true },
"packageRules": [
{
"matchManagers": ["bicep"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "pr"
},
{
"matchManagers": ["bicep"],
"matchUpdateTypes": ["major"],
"labels": ["bicep-major-bump", "needs-review"]
}
]
}Patch and minor bumps merge automatically (the semver contract says they are compatible). Major bumps go to review – the workflow labels and notifies the cloud platform team.
Audit: Who Overwrote a Module and When
Even with the break-glass procedure we want to know when --force was used. At Christie's we wire ACR Activity Log into a Log Analytics alert:
resource forcePublishAlert 'Microsoft.Insights/scheduledQueryRules@2023-03-15-preview' = {
name: 'alert-bicep-module-overwrite'
location: location
properties: {
severity: 1
enabled: true
evaluationFrequency: 'PT15M'
windowSize: 'PT15M'
scopes: [logAnalyticsWorkspaceId]
criteria: {
allOf: [
{
query: '''
ContainerRegistryRepositoryEvents
| where Repository startswith "bicep/"
| where OperationName == "Push"
| summarize PushCount = count() by Repository, Tag = MediaType, Identity
| where PushCount > 1
'''
timeAggregation: 'Count'
operator: 'GreaterThan'
threshold: 0
}
]
}
actions: {
actionGroups: [securityActionGroupId]
}
}
}If a second push arrives at the same tag inside a 15-minute window, the security team is alerted. It is usually a legitimate break-glass operation, but occasionally it catches a pipeline misconfiguration.
Rollout: How to Replace --force in an Existing Environment
At Christie's we did this in four steps:
| Step | Week | Risk |
|---|---|---|
Implement skip-if-exists, keep --force as a fallback flag | 1 | None – behavior unchanged |
| Run both modes in parallel, monitor the skip rate | 2–3 | None – data collection only |
| Communicate the change to all teams, give 1 week's notice | 4 | None |
Remove the --force flag, enforce the semver PR check | 5 | Low – occasional PR fails, dev fixes |
After five weeks we had zero overwrite incidents and no team complained about the publish process being slower (skipping is faster than publishing).
Conclusion
--force on az bicep publish is a landmine in a multi-team environment. The skip-if-exists pattern, semver PR validation, and audit alerting defuse it in a five-week rollout. The cost is zero – just discipline and 50 lines of Bash.
If you are dealing with a similar situation around shared Bicep or Terraform modules at enterprise scale, check out our cloud architecture services or reach out for a review of your IaC supply chain.
About the author

Martin Rylko
Senior Cloud Architect & DevOps Engineer
14+ years in IT – from on-premises datacenters and Hyper-V clustering to cloud infrastructure on Microsoft Azure. I specialize in Landing Zones, IaC automation, Kubernetes and security compliance.
Frequently Asked Questions
Why is --force a problem when publishing Bicep modules?▾
How does semver apply to Bicep module publishing?▾
What if I genuinely need to overwrite a published version?▾
Does the skip-if-exists pattern work in both Azure DevOps and GitHub Actions?▾
You might also like
Bicep CI/CD: GitHub Actions Pipeline for Azure
Build a production Bicep deployment pipeline with GitHub Actions. Covers what-if previews, environment approvals, OIDC authentication, and rollback strategies.
ReadBicep Deployment Stacks: Lifecycle Management Without Manual Cleanup
Deployment Stacks in Bicep are a way to manage Azure resources as a coherent unit with denyAssignments, auto-cleanup, and controlled deletion. Practical guide with a migration plan from classic deployments.
ReadTerraform Azure Modules: Private Registry and Testing
Build reusable Terraform modules for Azure with private registry publishing, automated testing with Terratest, and versioned module consumption in production.
Read