This is an old revision of the document!
SHIFT-WIKI - Sjoerd Hooft's InFormation Technology
This WIKI is my personal documentation blog. Please enjoy it and feel free to reach out through blue sky if you have a question, remark, improvement or observation.
Azure DevOps Extension: Send email through Graph
Summary: How to get started with the Azure DevOps Extension for sending email through Microsoft Graph.
Date: 9 December 2025
This is a support page for the Azure DevOps Extension: Send email through Graph.
Marketplace link: https://marketplace.visualstudio.com/items?itemName=GetShifting.GraphEmail
GitHub Repository: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail
Overview
The marketplace extension page provides a brief overview on how to use the extension, and how to configure the requirements. This page provides more examlples and also some background on how the extension creation is done and tested.
Yaml Examples
The examples below are also provided in the marketplace extension page:
This is an example of a task to send a multiline email:
- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" BCC: "$(BUILD.REQUESTEDFOREMAIL)" From: "sjoerd@getshifting.com" Subject: "Test mail from $(BUILD.DEFINITIONNAME)" Body: | <h1>This is a testmail</h1> <p>You can use various variables within the body of the email, for example:</p> <ul> <li> Build ID: $(build.buildid) </li> <li> Build Directory: $(agent.builddirectory) </li> <li> Build Queued By: $(BUILD.QUEUEDBY) </li> <li> Agent Name: $(AGENT.NAME) </li> <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li> <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li> <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li> <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li> </ul> Kind regards, <br> $(BUILD.REQUESTEDFOR) ClientID: "e5e6ce84-d241-4faf-97e0-d71a171f1adf" ClientSecret: "$(ClientSecret)" ShowClientSecret: false TenantDomain: getshifting.com
This is an example of a task to send a single line email:
- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" From: "sjoerd@getshifting.com" Subject: "Testmail from $(BUILD.DEFINITIONNAME)" Body: This is a short testmail ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" TenantDomain: getshifting.com
Classic Pipeline Example
Even though classic pipelines are not used that much anymore, when I originally created the extension I still mostly used classic pipelines. Below is an example I used in my pipeline to test the extension:
The Build and Release Pipeline
Below is the almost full build and release pipeline I use to build, test and release the extension to the marketplace. Some notes have been removed, and some of the guids are changed. The tokenized files that are used in the replacetokens task can be reviewed on the public repository of the extension: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail:
name: $(Build.DefinitionName)-$(Build.BuildId) appendCommitMessageToRunName: false variables: - group: ExtensionGraph - name: ClientID value: "42ff8d60-02e0-43dd-9b57-008cbdd86dd2" - name: tfxcli value: "v0.9.x" - name: extensionid value: "graphemail" - name: publisherprod value: "getshifting" - name: publisher value: "getshifting-private" - name: public value: "false" - name: currentDate value: $[format('{0:yyyyMMdd}', pipeline.startTime)] parameters: - name: action displayName: Action type: string default: "Build and Release Privately only" values: - "Build Only" - "Build and Release Privately only" - "Build and Release Public" pool: vmImage: windows-latest trigger: none resources: repositories: - repository: self stages: - stage: build displayName: "Stage: Build" jobs: - job: build displayName: "Job: Build & Package" steps: - task: PowerShell@2 displayName: "Get System Variables" condition: eq(variables['System.debug'], true) inputs: pwsh: true targetType: "inline" script: | Write-Host "`n##[section]Get System Variables`n" Get-ChildItem -path env:* | Sort-Object Name - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: qetza.replacetokens.replacetokens-task.replacetokens@6 displayName: 'Replace tokens in vss-extension.json graphEmail\task.json' inputs: sources: | vss-extension.json graphEmail\task.json tokenPattern: doubleunderscores telemetryOptout: true root: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail' verbosity: "debug" - task: PowerShell@2 displayName: "Add VstsTaskSdk PowerShell Module to the extension package" inputs: pwsh: true targetType: "inline" workingDirectory: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail' script: | # Set the verbose flag based on pipeline variable if ($env:SYSTEM_DEBUG -eq "True"){ Write-Host "##[debug]Verbose logging is enabled" $verboseFlag = $true } else { $verboseFlag = $false } Write-Host "##[section]Set Variables" $vstsTaskSdkDir = ".\graphEmail\ps_modules\VstsTaskSdk" $vstsTempDir = ".\vststemp" Write-Host "VstsTaskSdk Directory : $vstsTaskSdkDir" Write-Host "VstsTaskSdk Temp Directory: $vstsTempDir" if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } Write-Host "##[section]Save VstsTaskSdk to $vstsTempDir and copy to $vstsTaskSdkDir" Write-Host "`nCreate required directory structure" New-Item -Path $vstsTaskSdkDir -ItemType "Directory" -Verbose:$verboseFlag New-Item -Path $vstsTempDir -ItemType "Directory" -Verbose:$verboseFlag Write-Host "`nSave VstsTaskSdk Module to temporary directory" Save-Module -Name VstsTaskSdk -Path $vstsTempDir -Verbose:$verboseFlag Write-Host "`nCopy all required files to VstsTaskSdk directory" Get-ChildItem -Path $vstsTempDir -Recurse -File -Depth 2 | ForEach-Object { Copy-Item -Path $_.FullName -Destination $vstsTaskSdkDir -Verbose:$verboseFlag } if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } Write-Host "##[section]Remove temporary directory" Remove-Item -Path $vstsTempDir -Recurse -Force -Verbose:$verboseFlag if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } - task: ms-devlabs.vsts-developer-tools-build-tasks.package-extension-build-task.PackageAzureDevOpsExtension@5 displayName: "Package Extension" inputs: rootFolder: "$(Build.SourcesDirectory)/public/adoExtensionGraphEmail" outputPath: "$(build.artifactstagingdirectory)" extensionVersion: "$(currentDate).$(Build.BuildId).$(System.StageAttempt)" - task: PublishBuildArtifacts@1 displayName: "Publish Artifact: extension" inputs: ArtifactName: extension - stage: releasePrivate displayName: "Stage: Release Private" condition: and(succeeded(), or( contains('${{ parameters.action }}', 'Build and Release Privately only'), contains('${{ parameters.action }}', 'Build and Release Public'))) jobs: - job: releasePrivate displayName: "Job: Release Privately" steps: - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: DownloadPipelineArtifact@2 inputs: artifactName: "extension" targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension" - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5 displayName: "Publish Extension" inputs: connectTo: "VsTeam" connectedServiceName: "AzureDevOpsMarketPlace" fileType: vsix vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix" updateTasksVersion: false extensionVisibility: private extensionPricing: free - job: waitForInstall displayName: "Job: Wait for the Private Extension to be installed" dependsOn: releasePrivate pool: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 displayName: "Wait for the private extension is installed" timeoutInMinutes: 120 inputs: notifyUsers: | sjoerd@getshifting.com instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting-Private" and wait for the extension to be automatically updated to the just released version.' onTimeout: "reject" - job: testPublic displayName: "Job: Test Privately" dependsOn: waitForInstall steps: - task: GetShifting-Private.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" BCC: "$(BUILD.REQUESTEDFOREMAIL)" From: "sjoerd@getshifting.com" Subject: "Test mail from $(BUILD.DEFINITIONNAME)" Body: | <h1>This is a testmail</h1> <p>You can use various variables within the body of the email, for example:</p> <ul> <li> Build ID: $(build.buildid) </li> <li> Build Directory: $(agent.builddirectory) </li> <li> Build Queued By: $(BUILD.QUEUEDBY) </li> <li> Agent Name: $(AGENT.NAME) </li> <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li> <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li> <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li> <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li> </ul> Kind regards, <br> $(BUILD.REQUESTEDFOR) ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" ShowClientSecret: true TenantDomain: getshifting.com - stage: releasePublic displayName: "Stage: Release Public" condition: and(succeeded(), eq('${{ parameters.action }}', 'Build and Release Public')) jobs: - job: releasePublic displayName: "Job: Release Publicly" steps: - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: DownloadPipelineArtifact@2 inputs: artifactName: "extension" targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension" - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5 displayName: "Publish Extension" inputs: connectTo: "VsTeam" connectedServiceName: "AzureDevOpsMarketPlace" fileType: vsix vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix" publisherId: "$(publisherprod)" updateTasksVersion: false updateTasksId: true extensionVisibility: public extensionPricing: free - job: waitForInstall displayName: "Job: Wait for the Public Extension to be installed" dependsOn: releasePublic pool: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 displayName: "Wait for the public extension is installed" timeoutInMinutes: 120 inputs: notifyUsers: | sjoerd@getshifting.com instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting" and wait for the extension to be automatically updated to the just released version.' onTimeout: "reject" - job: testPublic displayName: "Job: Test Publicly" dependsOn: waitForInstall steps: - task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" From: "sjoerd@getshifting.com" Subject: "Testmail from $(BUILD.DEFINITIONNAME)" Body: Test from public ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" TenantDomain: getshifting.com
Useful Resources
Copilot Prompt to Get All Unattached Disks in an Azure Subscription
Summary: This wiki page shows how I used copilot to get all unattached disks in an Azure subscription. It's part of the 'copilot' tag series in which I showcase the use of copilot in infrastructure related tasks.
Date: 2 December 2025
I've been using copilot for a while now, and I mostly use it without using a prompt. I let it finish my sentences, or create a small comment and wait for a few seconds to let copilot to give me some hints on how to proceed. But sometimes I also use a prompt, and on this page I show you how I used some prompts to create a script.
The hard facts:
- IDE: VS Code, 1.106.3
- Copilot Extension Version: 1.388.0
- Model: Claude Sonnet 4
The Prompt
Actually, I used two prompts. The first prompt was:
I need an azure cli script that will get all disks from an Azure subscription that have the disk state unattached. In the output I want to see the name, the resourcegroup, the last ownership update time, the location, the time created, the disk size, and the following tags: k8s-azure-created-by, kubernetes.io-created-for-pvc-namespace, kubernetes.io-created-for-pvc-name
This created a script, which with some small modifications worked, except that it didn't return the tags. I found that using the azure cli query option didn't retrieve the tags, so I added another prompt:
The output doesn't display the tags. I think the az disk list can't resolve the tag information. I think you need to retrieve the tags separately for each disk
This created a new script, which again needed some small modifications.
The Final Script
Note that this is the script after my modifications. For the modifications, see below. Copilot also provided the possible ways to start the script:
# Display the output for a subscription .\get-unattached-disks.ps1 -SubscriptionId "your-subscription-id" # Export to CSV .\get-unattached-disks.ps1 -SubscriptionId "your-subscription-id" -OutputToCSV -OutputPath "my-disks.csv"
# Get all unattached disks from Azure subscription with detailed information # This script retrieves disks with state "Unattached" and displays relevant metadata param( [string]$SubscriptionId = $null, [switch]$OutputToCSV = $false, [string]$OutputPath = "unattached-disks.csv" ) # Set subscription if provided if ($SubscriptionId) { Write-Host "Setting subscription to: $SubscriptionId" -ForegroundColor Yellow az account set --subscription $SubscriptionId } # Get current subscription info $currentSub = az account show --query "{name:name, id:id}" -o json | ConvertFrom-Json Write-Host "Current subscription: $($currentSub.name) ($($currentSub.id))" -ForegroundColor Green # First get basic disk information for unattached disks $basicQuery = @" [?diskState=='Unattached'].{Name: name, ResourceGroup: resourceGroup, LastOwnershipUpdateTime: LastOwnershipUpdateTime, Location: location, TimeCreated: timeCreated, DiskSizeGB: diskSizeGB} "@ Write-Host "Retrieving basic disk information..." -ForegroundColor Yellow $basicDisks = az disk list --query $basicQuery -o json | ConvertFrom-Json if ($basicDisks.Count -eq 0) { Write-Host "No unattached disks found in the subscription." -ForegroundColor Green exit 0 } Write-Host "Found $($basicDisks.Count) unattached disk(s). Retrieving tag information..." -ForegroundColor Yellow # Now retrieve tags for each disk individually $disks = @() $counter = 0 foreach ($disk in $basicDisks) { $counter++ Write-Progress -Activity "Retrieving disk tags" -Status "Processing disk $counter of $($basicDisks.Count): $($disk.Name)" -PercentComplete (($counter / $basicDisks.Count) * 100) # Get tags for this specific disk $tags = az disk show --name $disk.Name --resource-group $disk.ResourceGroup --query "tags" -o json | ConvertFrom-Json # Create enriched disk object $enrichedDisk = [PSCustomObject]@{ Name = $disk.Name ResourceGroup = $disk.ResourceGroup LastOwnershipUpdateTime = $disk.LastOwnershipUpdateTime Location = $disk.Location TimeCreated = $disk.TimeCreated DiskSizeGB = $disk.DiskSizeGB CreatedBy = $tags.'k8s-azure-created-by' PVCNamespace = $tags.'kubernetes.io-created-for-pvc-namespace' PVCName = $tags.'kubernetes.io-created-for-pvc-name' #AllTags = $tags } $disks += $enrichedDisk } Write-Progress -Activity "Retrieving disk tags" -Completed Write-Host "Completed retrieving tag information for all disks." -ForegroundColor Green Write-Host "" if ($OutputToCSV) { # Output to CSV $disks | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 Write-Host "Results exported to: $OutputPath" -ForegroundColor Green } else { # Display in table format $disks | Format-Table -AutoSize -Property @( 'Name', 'ResourceGroup', @{Name='LastOwnershipUpdate'; Expression={$_.LastOwnershipUpdateTime}; Width=20}, 'Location', @{Name='TimeCreated'; Expression={$_.TimeCreated}; Width=20}, @{Name='SizeGB'; Expression={$_.DiskSizeGB}; Width=8}, @{Name='CreatedBy'; Expression={$_.CreatedBy}; Width=20}, @{Name='PVCNamespace'; Expression={$_.PVCNamespace}; Width=15}, @{Name='PVCName'; Expression={$_.PVCName}; Width=15} ) } # Summary Write-Host "" Write-Host "=== SUMMARY ===" -ForegroundColor Cyan Write-Host "Total unattached disks: $($disks.Count)" -ForegroundColor White $totalSizeGB = ($disks | Measure-Object -Property DiskSizeGB -Sum).Sum Write-Host "Total size: $totalSizeGB GB" -ForegroundColor White # Group by resource group $byResourceGroup = $disks | Group-Object -Property ResourceGroup | Sort-Object Count -Descending Write-Host "" Write-Host "By Resource Group:" -ForegroundColor Cyan foreach ($group in $byResourceGroup) { $groupSizeGB = ($group.Group | Measure-Object -Property DiskSizeGB -Sum).Sum Write-Host " $($group.Name): $($group.Count) disks ($groupSizeGB GB)" -ForegroundColor White } # Show Kubernetes-related disks $k8sDisks = $disks | Where-Object { $_.CreatedBy -or $_.PVCNamespace -or $_.PVCName } if ($k8sDisks.Count -gt 0) { Write-Host "" Write-Host "Kubernetes-related disks: $($k8sDisks.Count)" -ForegroundColor Magenta # Show detailed Kubernetes disk information Write-Host "" Write-Host "Kubernetes Disk Details:" -ForegroundColor Magenta $k8sDisks | Format-Table -AutoSize -Property @( 'Name', @{Name='CreatedBy'; Expression={$_.CreatedBy}; Width=20}, @{Name='PVCNamespace'; Expression={$_.PVCNamespace}; Width=15}, @{Name='PVCName'; Expression={$_.PVCName}; Width=20}, @{Name='SizeGB'; Expression={$_.DiskSizeGB}; Width=8} ) }
Modifications Made
Tags
In the first version of the script, the query also tried to retrive the tags. This was not allowed by the query syntax, so I had to remove that from the query.
Basic query
Originally, copilot had a very easy to read query, with every field in a separate line. However, that gave the following error:
ERROR: argument --query: invalid jmespath_type value: "[?diskState=='Unattached'].{"
To learn more about --query, please visit: 'https://learn.microsoft.com/cli/azure/query-azure-cli'
I updated the query to be just on one line, which solved the issue.
Basic query fields
Two of the fields the query would ask for did not work. I had to check the actual field names in the output of `az disk list`. The fields `LastOwnershipUpdateTime` and `diskSizeGB` had wrong casing. I updated them to the correct casing.
All Tags
Even though I didin't ask for it, copilot also added the field `AllTags`, which contained all tags as a hashtable. It worked, but it made the csv output messy, so I commented the field out.
This wiki has been made possible by:
Thoughts on Upgrading Terraform Providers
Summary: On this wiki page I will cover my experience in upgrading the AzureRM Terraform provider from version 3.112 to 4.51.
Date: 8 November 2025
Recently I had to upgrade the AzureRM terraform provider. We hadn't upgraded it in a while due to other priorities, but once these were addressed, I set about the task of upgrading the provider. On this page I will try to give some background on the process and the provider itself, as well as some of the issues I encountered.
Background
The AzureRM provider is the official Terraform provider for managing Microsoft Azure resources. It is maintained by HashiCorp and Microsoft, and it allows users to define and manage Azure infrastructure using Terraform's declarative configuration language. The provider is regularly updated to add new features, fix bugs, and improve performance.
The latest version can be seen on the terraform registry. It allows you to check available resources and the available options for these resources.
Upgrade Process
Hashicorp, the company behind terraform, provides a general guide on upgrading providers. The process basically comes down to:
- Get your current deployment stable and working
- Make sure you can do a `terraform init`, `terraform plan` and `terraform apply` without any issues
- Create a backup of your terraform state file
- Also install the latest version of the terraform CLI. Note that if you're running terraform from a pipeline on which you cannot control the terraform CLI version, you should install the same version as installed on the pipeline to avoid any discrepancies.
- Update provider configuration
- Update the version in your `provider` block to the desired version
- Create a plan
- Run `terraform init` to download the new provider version
- Use the `-upgrade` flag to ensure that the latest version is downloaded
- Run `terraform plan` to see what changes will be made
- Identify and document errors, warnings, or actions
- Tackle one issue at a time. Start with errors, and only proceed to warnings once all errors are resolved. I had several occurences that after removing certain errors, also warnings dissapeared.
- Refactor your configuration as needed
- Apply your plan
- Run `terraform apply` to implement the changes
And an additional note, if you're working with a pipeline to deploy your terraform code, try to be able to perform `terraform plan` locally first. This will save you a lot of time.
Update the Provider Version
This was our old provider block:
terraform {
required_version = ">= 0.14.9"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.112"
}
}
}
And this is our new provider block. As you can see, only the version number has changed:
terraform {
required_version = ">= 0.14.9"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.51.0"
}
}
}
Run the Plan Locally
The following commands were run in a PowerShell terminal on my local machine:
az login az account set --subscription 30b3c71d-a123-a123-a123-abcd12345678 cd "C:\Repos\tf\applications\main" terraform version # Do a normal terraform init. It will tell you to use -upgrade to get the latest provider version terraform init ` -backend-config=storage_account_name="saeuwdevtf" ` -backend-config=container_name="terraform" ` -backend-config=key="dev.tfstate" ` -backend-config=resource_group_name="rg-euw-dev-tf" ` -backend-config=subscription_id="30b3c71d-a123-a123-a123-abcd12345678" # So do a terraform init with -upgrade terraform init ` -backend-config=storage_account_name="saeuwdevtf" ` -backend-config=container_name="terraform" ` -backend-config=key="dev.tfstate" ` -backend-config=resource_group_name="rg-euw-dev-tf" ` -backend-config=subscription_id="30b3c71d-a123-a123-a123-abcd12345678" ` -upgrade # Then do a plan, and make sure to provide your tfvars file terraform plan ` -var-file="env/dev.tfvars" ` -input="false"
Issues
When running the `terraform plan` command, several issues are to be expected. I'll address some of the issues I encountered below.
Deprecated Attributes
We had some resources with deprecated attributes. Especially the Kubernetes cluster resource had many attributes that were no longer supported. The way to tackle them is like this:
- Lookup the resource in the terraform registry
- Try to find the deprecated attribute. Sometimes it's only renamed, but then the description stayed the same. Other times it is completely removed. If that's the case it's isually mentioned in the old version of the documentation.
- To find the old version of the documentation, you can use the version selector (it's almost at the top) on the terraform registry page of the resource.
- Rename or remove the attribute in your terraform code
New Default Values
Both on the kubernetes resource as well as on storage account we had some attributes that had new default values. This can happen because the new provider suddenly has support for an attribute and sets a default value different from the Azure default value. These settings are usually new, so you need to check them thoroughly. For example, on the storage account the options to allow for cross tenant copy got disabled. And for the kubernetes cluster the node upgrade channel got changed. This actually caused an node image upgrade during the night because I missed that during the plan.
Provider Changes
We also had some errors and warning for the provider itself. It suddenly required a subscription id and also some of the attributes of the provider got renamed.
This was our old provider configuration:
provider "azurerm" {
skip_provider_registration = true
features {
virtual_machine {
skip_shutdown_and_force_delete = true
delete_os_disk_on_deletion = true
}
}
}
And this is our new provider configuration:
provider "azurerm" {
subscription_id = var.env_subscription_id
resource_provider_registrations = "none"
features {
virtual_machine {
skip_shutdown_and_force_delete = true
delete_os_disk_on_deletion = true
}
}
}
Note that adding the subscription id solved a lot of decoding errors:
│ Warning: Failed to decode resource from state │ │ Error decoding "module.restore_storage_account.module.diagnostic_settings.azurerm_monitor_diagnostic_setting.diagnostic_setting[0]" │ from prior state: unsupported attribute "log"
Conclusion
Upgrading terraform providers can be a tedious task, especially when there are many breaking changes. However, by following a systematic approach and addressing issues one at a time, the process can be managed effectively. Always ensure to back up your state file before making any changes, and test thoroughly after the upgrade to ensure everything is functioning as expected. The whole process took me two working days with time for other issues and tasks as well in between.
This wiki has been made possible by:
How to Add Text to Each File in a Directory in a Repository
Summary: This wiki page shows how I added google ads to each wiki page in a Repository using powershell, and use a VS Code extension to make sure new files get the same treatment.
Date: 25 October 2025
Background
When using Google Auto Adsense on my wiki, I get ads like everywhere, which makes the pages look cluttered and hard to read. My solution is to add an ad at the bottom of each page. However, I don not want to do that manually.
Using a VS Code extension to run a script on file save
I originally wanted to use git hooks to run a script. However, git hooks have a notourious bad reputation when running them on Windows. So instead I opted to use a VS Code extension that runs a script when saving a file.
I previously used this Run on Save extension for work, which worked very well, so I decided to use it here again.
After installing the extension, you need to add some configuration to your settings.json file. I've used the vscode settings extensively before, so I just needed to add the following settings:
"emeraldwalk.runonsave": {
// Messages to show before & after all commands
"message": "*** All Start ***",
"messageAfter": "*** All Complete ***",
// Show elappsed time for all commands
"showElapsed": true,
"commands": [
{
"match": "\\.txt$",
"notMatch": "\\drafts/.*$",
"cmd": "pwsh.exe -ExecutionPolicy Bypass -File ${workspaceFolder}\\ads\\Add-AdsToWikiPages.ps1"
}
]
}
This will run the powershell script located in the ads folder every time a .txt file is saved in the dokuwiki/pages folder.
The Powershell script
This is the powershell script me and copilot created together and I tested very thoroughly before using it on my actual wiki pages:
- It starts with defining the function that can later be called with parameters.
- It has an option to exclude certain files from being modified.
- It checks if the string to be added is already present in the file, and only adds it if it's not there yet.
- It appends the string at the end of the file, ensuring proper formatting with new lines.
function Add-StringToFiles { param( [Parameter(Mandatory)] [string]$FolderPath, [Parameter(Mandatory)] [string]$StringToAdd, [string[]]$ExcludeList = @() ) Get-ChildItem -Path $FolderPath -File -Filter "*.txt" | ForEach-Object { $filePath = $_.FullName $fileName = $_.Name # Skip if file is in exclude list if ($ExcludeList -contains $fileName) { Write-Host "Skipping excluded file: $fileName" return } $content = Get-Content -Path $filePath -Raw if ($content -notmatch [regex]::Escape("//This wiki has been made possible by://")) { $newContent = "$content`r`n$StringToAdd`r`n" Set-Content -Path $filePath -Value $newContent } } } $folder = "C:\Repos\GetShifting\knowledge\dokuwiki\pages" $excludeFiles = @("overview.txt", "alltags.txt", "sidebar.txt") $adString = '//This wiki has been made possible by:// <HTML> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8613096447910897" crossorigin="anonymous"></script> <!-- Wiki End of Page --> <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-8613096447910897" data-ad-slot="6221699236" data-ad-format="auto" data-full-width-responsive="true"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </HTML> ' Add-StringToFiles -FolderPath $folder -StringToAdd $adString -ExcludeList $excludeFiles
This wiki has been made possible by:

