├── .gitignore ├── .pipelines ├── ci.yaml └── templates │ ├── check.yaml │ ├── detect.yaml │ └── release.yaml ├── .tflint.hcl ├── README.md └── pull_request_template.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 12 | # password, private keys, and other secrets. These should not be part of version 13 | # control as they are data points which are potentially sensitive and subject 14 | # to change depending on the environment. 15 | # 16 | *.tfvars 17 | 18 | # Ignore override files as they are usually used to override resources locally and so 19 | # are not checked in 20 | override.tf 21 | override.tf.json 22 | *_override.tf 23 | *_override.tf.json 24 | 25 | # Include override files you do wish to add to version control using negated pattern 26 | # 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | -------------------------------------------------------------------------------- /.pipelines/ci.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | 6 | pool: 7 | vmImage: ubuntu-latest 8 | 9 | variables: 10 | isMaster: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] 11 | 12 | stages: 13 | - template: templates/detect.yaml 14 | - template: templates/check.yaml 15 | - template: templates/release.yaml 16 | -------------------------------------------------------------------------------- /.pipelines/templates/check.yaml: -------------------------------------------------------------------------------- 1 | stages: 2 | - stage: check 3 | dependsOn: detect 4 | condition: and(succeeded(), ne(dependencies.detect.outputs['detect.check_labels.labelOutput'], 'no-release'), eq(variables.isMaster, false)) 5 | jobs: 6 | - job: lint 7 | strategy: 8 | matrix: $[ stageDependencies.detect.detect.outputs['plan.changedModules'] ] 9 | steps: 10 | - checkout: self 11 | 12 | - bash: | 13 | curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash 14 | tflint --init && \ 15 | tflint --chdir="./$(module)" 16 | displayName: TFLint 🧹 17 | 18 | - job: validate 19 | strategy: 20 | matrix: $[ stageDependencies.detect.detect.outputs['plan.changedModules'] ] 21 | steps: 22 | - checkout: self 23 | 24 | - pwsh: | 25 | terraform -chdir="./$(module)" fmt -write=false -diff -check 26 | displayName: terraform fmt 📄 27 | 28 | - pwsh: | 29 | terraform -chdir="./$(module)" init --backend=false 30 | terraform validate 31 | displayName: terraform validate ✅ 32 | 33 | - job: checkov 34 | strategy: 35 | matrix: $[ stageDependencies.detect.detect.outputs['plan.changedModules'] ] 36 | steps: 37 | - checkout: self 38 | 39 | - bash: | 40 | docker pull bridgecrew/checkov 41 | docker run --volume $(pwd):/$(module) bridgecrew/checkov --directory /$(module) 42 | displayName: 'checkov 🔎' 43 | workingDirectory: $(System.DefaultWorkingDirectory)/$(module) 44 | 45 | - job: tfsec 46 | strategy: 47 | matrix: $[ stageDependencies.detect.detect.outputs['plan.changedModules'] ] 48 | steps: 49 | - checkout: self 50 | 51 | - bash: | 52 | docker pull aquasec/tfsec 53 | docker run --volume $(pwd):/$(module) aquasec/tfsec /$(module) 54 | displayName: 'tfsec 🛡️' 55 | workingDirectory: $(System.DefaultWorkingDirectory)/$(module) 56 | -------------------------------------------------------------------------------- /.pipelines/templates/detect.yaml: -------------------------------------------------------------------------------- 1 | stages: 2 | - stage: detect 3 | jobs: 4 | - job: detect 5 | steps: 6 | - checkout: self 7 | persistCredentials: true 8 | 9 | - pwsh: | 10 | if ([System.Convert]::ToBoolean("$(isMaster)")) 11 | { 12 | $sha = $env:BUILD_SOURCEVERSION 13 | $body = @" 14 | { 15 | "queries": [ 16 | { 17 | "type": 1, 18 | "items": [ 19 | "$sha" 20 | ] 21 | } 22 | ] 23 | } 24 | "@ 25 | $headers = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } 26 | $url = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/PullRequestQuery?api-version=5.1" 27 | 28 | $response = Invoke-RestMethod -Uri $url ` 29 | -Method POST ` 30 | -Headers $headers ` 31 | -Body $Body ` 32 | -ContentType application/json 33 | 34 | $pullRequestId = $response.results."$sha".pullRequestId 35 | } 36 | else { 37 | $pullRequestId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID 38 | } 39 | 40 | Write-Host "##vso[task.setvariable variable=pullRequestId]$pullRequestId" 41 | Write-Host "##vso[task.setvariable variable=prId;isOutput=true]$pullRequestId" 42 | env: 43 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 44 | displayName: Get PR number 🔗 45 | name: get_pr_number 46 | 47 | - pwsh: | 48 | $url = "$($env:SYSTEM_TEAMFOUNDATIONSERVERURI)$env:SYSTEM_TEAMPROJECTID/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$(pullRequestId)/labels?api-version=5.1-preview.1" 49 | 50 | $response = Invoke-RestMethod -Uri $url -Method Get -Headers @{ 51 | Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" 52 | } 53 | 54 | $labels = $response.value.name ` 55 | | Where-Object { 56 | @("major", 57 | "minor", 58 | "patch", 59 | "no-release") -contains $_ 60 | } 61 | try { 62 | if ($labels.count -eq 1) { 63 | Write-Output "Release type: $labels" 64 | Write-Host "##vso[task.setvariable variable=label]$labels" 65 | Write-Host "##vso[task.setvariable variable=labelOutput;isOutput=true]$labels" 66 | } 67 | elseif ($labels.count -eq 0) { 68 | Write-Error -Message "No release type labels found. (patch/minor/major/no-release)." 69 | } 70 | elseif ($labels.count -gt 1) { 71 | Write-Error -Message "Too many release labels set on pull request: $labels" 72 | } 73 | } 74 | catch { 75 | Write-Host "##vso[task.logissue type=error]$_" 76 | Write-Host "##vso[task.complete result=Failed;]$_" 77 | } 78 | env: 79 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 80 | displayName: Check labels 🏷️ 81 | name: check_labels 82 | 83 | - pwsh: | 84 | $headers = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } 85 | $uri = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/commits/$($env:BUILD_SOURCEVERSION)/changes?api-version=5.1" 86 | $changes = Invoke-RestMethod -Method Get ` 87 | -Headers $headers ` 88 | -Uri $uri 89 | 90 | $modules = (($changes.changes.GetEnumerator().item ` 91 | | Where-Object { 92 | ($_.isFolder -eq $true) ` 93 | -and ($_.path -notlike '/.pipelines*') ` 94 | -and ($_.path -ne '/') 95 | }).path).trim('/') 96 | $modules 97 | 98 | $existingModules = (Get-ChildItem -Directory).Name ` 99 | | Where-Object { 100 | $_ -ne '/.pipelines' 101 | } 102 | 103 | $changedModules = New-Object PSObject 104 | $modules | ForEach { 105 | Add-Member -InputObject $changedModules ` 106 | -NotePropertyName $_ ` 107 | -NotePropertyValue @{"module" = $_} 108 | 109 | if ((git tag -l "$_/v*" -n1 | Where-Object {$_ -like "*PR #$(pullRequestId) -*"}).count -ne 0) { 110 | $errorMessage = "Changes from this PR were already published for module: $_" 111 | Write-Error $errorMessage 112 | Write-Host "##vso[task.logissue type=error]$errorMessage" 113 | Write-Host "##vso[task.complete result=Failed;]$errorMessage" 114 | } 115 | } 116 | 117 | $changedModulesJson = $changedModules | ConvertTo-Json -Compress 118 | 119 | $params = @{ 120 | 'Uri' = "$($env:SYSTEM_TEAMFOUNDATIONSERVERURI)$env:SYSTEM_TEAMPROJECTID/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$(pullRequestId)/labels?api-version=5.1-preview.1" 121 | 'Headers' = $headers 122 | 'Method' = 'GET' 123 | 'ContentType' = 'application/json; charset=utf-8' 124 | } 125 | $moduleLabels = (Invoke-restmethod @params).value.name 126 | 127 | $moduleLabels | Where-Object { ($existingModules -contains $_) -and ($modules -notcontains $_) } | ForEach-Object { 128 | $params = @{ 129 | 'Uri' = "$($env:SYSTEM_TEAMFOUNDATIONSERVERURI)$env:SYSTEM_TEAMPROJECTID/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$(pullRequestId)/labels/$($_)?api-version=5.1-preview.1" 130 | 'Headers' = $headers 131 | 'Method' = 'DELETE' 132 | 'ContentType' = 'application/json; charset=utf-8' 133 | } 134 | Invoke-RestMethod @params 135 | } 136 | 137 | $modules | Where-Object { $moduleLabels -notcontains $_ } | ForEach-Object { 138 | $params = @{ 139 | 'Uri' = "$($env:SYSTEM_TEAMFOUNDATIONSERVERURI)$env:SYSTEM_TEAMPROJECTID/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$(pullRequestId)/labels?api-version=5.1-preview.1" 140 | 'Headers' = $headers 141 | 'Method' = 'POST' 142 | 'ContentType' = 'application/json; charset=utf-8' 143 | 'Body' = @{name = "$($_)"} | ConvertTo-Json -Compress 144 | } 145 | Invoke-RestMethod @params 146 | } 147 | 148 | Write-Output "##vso[task.setvariable variable=changedModules;isOutput=true]$changedModulesJson" 149 | 150 | $releases = @() 151 | foreach ($module in $modules) { 152 | $tags = (git tag -l "$module/v*.*.*") 153 | 154 | if ($tags) { 155 | $versions = $tags.trim("$module/v") 156 | $latest = [System.Version[]]$versions | Sort-Object -Descending | select -first 1 157 | } 158 | else { 159 | $latest = New-Object System.Version(0, 0, 0) 160 | } 161 | switch ("$(label)") { 162 | "major" { 163 | $major = $latest.major + 1 164 | $minor = 0 165 | $patch = 0 166 | } 167 | "minor" { 168 | $major = $latest.major 169 | $minor = $latest.minor + 1 170 | $patch = 0 171 | } 172 | "patch" { 173 | $major = $latest.major 174 | $minor = $latest.minor 175 | $patch = $latest.build + 1 176 | } 177 | } 178 | $new = New-Object System.Version($major, $minor, $patch) 179 | $releases += New-Object PSObject –Property @{ 180 | module = $module; 181 | previous = $latest; 182 | new = "v$new" 183 | } 184 | } 185 | $output = New-Object PSObject –Property @{ 186 | sha = "$(git rev-parse HEAD)"; 187 | releases = $releases 188 | } | ConvertTo-Json -Compress 189 | 190 | Write-Output "##vso[task.setvariable variable=releases;isOutput=true]$output" 191 | $plan = $releases | foreach { 192 | "| $($_.module) | v$($_.previous) | **$($_.new)** |`n" 193 | } 194 | $prUrl = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$([uri]::EscapeDataString("$env:SYSTEM_TEAMPROJECT/_git/$($env:BUILD_REPOSITORY_NAME)/pullrequest/$(pullRequestId)"))" 195 | 196 | $prApiUrl = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/git/pullrequests/$(pullRequestId)?api-version=5.1" 197 | $prInfo = Invoke-RestMethod -Method Get ` 198 | -Headers $headers ` 199 | -Uri $prApiUrl 200 | $prTitle = $prInfo.title 201 | 202 | $changelogEntry = ((($($prInfo.description) -split ("#+ ?") | Where-Object { 203 | $_ -like "Changelog*" 204 | }) -split ('```'))[1]).Trim() 205 | 206 | try { 207 | if(!$changelogEntry) { 208 | Write-Error "Changelog section not found in PR description" 209 | } 210 | if($changelogEntry -like "TODO:*") { 211 | Write-Error "Please update change log section in PR description" 212 | } 213 | } 214 | catch { 215 | Write-Host "##vso[task.logissue type=error]$_" 216 | Write-Host "##vso[task.complete result=Failed;]$_" 217 | } 218 | 219 | Write-Output "##vso[task.setvariable variable=prTitle;isOutput=true]$prTitle" 220 | Write-Output "##vso[task.setvariable variable=changelogEntry;isOutput=true]$changelogEntry" 221 | 222 | $content = @" 223 | # Release plan 224 | | Directory | Previous version | New version | 225 | |-----------|------------------|-------------| 226 | $plan 227 |
Changelog preview: 228 | 229 | * PR [#$(pullRequestId)]($($prUrl)) - $prTitle 230 | 231 | `````` 232 | $changelogEntry 233 | `````` 234 |
235 | "@ 236 | 237 | if (![System.Convert]::ToBoolean("$(isMaster)")) { 238 | $body = @" 239 | { 240 | "comments": [ 241 | { 242 | "parentCommentId": 0, 243 | "content": "$content", 244 | "commentType": 1 245 | } 246 | ], 247 | "status": 4 248 | } 249 | "@ 250 | $headers = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } 251 | $url = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/git/repositories/$($env:BUILD_REPOSITORY_NAME)/pullRequests/$(pullRequestId)/threads?api-version=5.1" 252 | ((Invoke-RestMethod -Uri $url ` 253 | -Method GET ` 254 | -Headers $headers ` 255 | -Body $Body ` 256 | -ContentType application/json).value.comments ` 257 | | where-object { 258 | $_.content -like "# Release plan*" 259 | })."_links".self.href | foreach-object { 260 | if (![string]::IsNullOrEmpty($_)) { 261 | Invoke-RestMethod -Uri "$($_)?api-version=5.1" ` 262 | -Method DELETE ` 263 | -Headers $headers ` 264 | -Body $Body ` 265 | -ContentType application/json 266 | } 267 | } 268 | Invoke-RestMethod -Uri $url ` 269 | -Method POST ` 270 | -Headers $headers ` 271 | -Body $Body ` 272 | -ContentType application/json 273 | } 274 | env: 275 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 276 | condition: and(succeeded(), ne(variables['label'], 'no-release')) 277 | displayName: Create release plan 🎯 278 | name: plan 279 | -------------------------------------------------------------------------------- /.pipelines/templates/release.yaml: -------------------------------------------------------------------------------- 1 | stages: 2 | - stage: release 3 | dependsOn: detect 4 | condition: and(succeeded(), ne(dependencies.detect.outputs['detect.check_labels.labelOutput'], 'no-release'), eq(variables.isMaster, true)) 5 | variables: 6 | releases: $[ stageDependencies.detect.detect.outputs['plan.releases'] ] 7 | pullRequestId: $[ stageDependencies.detect.detect.outputs['get_pr_number.prId'] ] 8 | pullRequestTitle: $[ stageDependencies.detect.detect.outputs['plan.prTitle'] ] 9 | changelogEntry: $[ stageDependencies.detect.detect.outputs['plan.changelogEntry'] ] 10 | jobs: 11 | - job: release 12 | steps: 13 | - checkout: self 14 | persistCredentials: true 15 | 16 | - pwsh: | 17 | $payload = '$(releases)' | convertfrom-json 18 | 19 | $date = Get-Date -Format dd-MM-yyyy 20 | 21 | $message = "($date) PR #$(pullRequestId) - $(pullRequestTitle)" 22 | $description = "- $(changelogEntry)" 23 | 24 | git config --global user.email "build@sdncenter.pl" 25 | git config --global user.name "Project Collection Build Service (sdncenterspzoo)" 26 | 27 | $payload.releases | ForEach-Object { 28 | $branchName = "tmp_$($_.module)_$($_.new)" 29 | $tag = "$($_.module)/$($_.new)" 30 | $module = $($_.module) 31 | $previousTag = "$($_.module)/$($_.previous)" 32 | $major = $($_.new).Substring(1).Split('.')[0] 33 | $minor = $($_.new).Substring(1).Split('.')[1] 34 | $majorTag = "$($module)/v$($major)" 35 | $minorTag = "$($module)/v$($major).$($minor)" 36 | 37 | git checkout "$($payload.sha)" 38 | 39 | write-host "module: $module" 40 | 41 | Copy-Item -Path $module ` 42 | -Destination "./_workingtmp/" ` 43 | -Recurse ` 44 | -Force 45 | 46 | if (git tag -l "$previousTag") 47 | { 48 | git checkout "$previousTag" 49 | } 50 | 51 | git checkout -b "$branchName" 52 | 53 | Get-ChildItem -Exclude "_workingtmp" ` 54 | | Remove-Item -Recurse ` 55 | -Force 56 | 57 | Copy-Item -Path "_workingtmp/*" ` 58 | -Destination ./ ` 59 | -Recurse ` 60 | -Force 61 | 62 | Remove-Item -LiteralPath "_workingtmp" ` 63 | -Force ` 64 | -Recurse 65 | 66 | git rm -r ".pipelines" 67 | git rm ".gitignore" 68 | git rm ".tflint.hcl" 69 | 70 | git add . 71 | git commit -a -m "$message" -m "$description" 72 | git tag "$tag" -m "$message" -m "$description" 73 | git tag -fa "$majorTag" -m "$message" -m "$description" 74 | git tag -fa "$minorTag" -m "$message" -m "$description" 75 | git push origin "$tag" 76 | git push origin "$minorTag" -f 77 | git push origin "$majorTag" -f 78 | git checkout $env:BUILD_SOURCEVERSION 79 | } 80 | env: 81 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 82 | displayName: Tag and publish new versions 🚀 83 | 84 | - job: wiki 85 | strategy: 86 | matrix: $[ stageDependencies.detect.detect.outputs['plan.changedModules'] ] 87 | maxParallel: 1 88 | variables: 89 | pullRequestId: $[ stageDependencies.detect.detect.outputs['get_pr_number.prId'] ] 90 | pullRequestTitle: $[ stageDependencies.detect.detect.outputs['plan.prTitle'] ] 91 | changelogEntry: $[ stageDependencies.detect.detect.outputs['plan.changelogEntry'] ] 92 | steps: 93 | - checkout: self 94 | 95 | - bash: | 96 | wget https://github.com/terraform-docs/terraform-docs/releases/download/v0.15.0/terraform-docs-v0.15.0-linux-amd64.tar.gz \ 97 | --output-document - \ 98 | --progress dot:mega \ 99 | | tar -xvz 100 | displayName: Install terraform-docs 🔧 101 | 102 | - pwsh: | 103 | $version = (('$(releases)' | convertfrom-json).releases | where-object {$_.module -eq "$(module)"}).new 104 | mkdir $(module)/output 105 | $intro = @" 106 | [[_TOC_]] 107 | 108 | # Module Location 109 | To use this module in your Terraform, use the below source value. 110 | ``````hcl 111 | module "$(module)" { 112 | source = "git::$($env:BUILD_REPOSITORY_URI)?ref=$(module)/$version" 113 | # also any inputs for the module (see below) 114 | } 115 | `````` 116 | 117 | # Module Attributes 118 | "@ 119 | $intro | Out-File "$(module)/output/documentation.md" 120 | 121 | $currentDir = (Get-Location).Path 122 | 123 | ./terraform-docs markdown table ` 124 | --output-file "$currentDir/$(module)/output/documentation.md" ` 125 | --sort-by required '$(module)' 126 | displayName: Generate docs ✍ 127 | 128 | - pwsh: | 129 | $rootPageName = "$($env:BUILD_REPOSITORY_NAME)".Replace(' ','-') 130 | 131 | $version = (('$(releases)' | convertfrom-json).releases | where-object {$_.module -eq "$(module)"}).new 132 | $docs = get-content -raw "$(module)/output/documentation.md" | Out-String 133 | $wikiName = ('{0}.wiki' -f $($env:SYSTEM_TEAMPROJECT).Replace(' ','-')) 134 | $headers = @{ Authorization = "Bearer $env:SYSTEM_ACCESSTOKEN" } 135 | 136 | try { 137 | $rootPageParams = @{ 138 | 'Uri' = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/wiki/wikis/$wikiName/pages?path=$rootPageName&api-version=5.1" 139 | 'Headers' = $headers 140 | 'Method' = 'PUT' 141 | 'ContentType' = 'application/json; charset=utf-8' 142 | 'body' = @{content = "[[_TOSP_]]"} | ConvertTo-Json -Compress 143 | } 144 | 145 | Invoke-RestMethod @rootPageParams 146 | } 147 | catch {} 148 | 149 | $uri = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$env:SYSTEM_TEAMPROJECT/_apis/wiki/wikis/$wikiName/pages?path=$rootPageName/$(module)&comment=$(module)`@$($version)&api-version=5.1" 150 | 151 | try { 152 | $wikiPage = Invoke-WebRequest "$($uri)&includecontent=true" -Method GET -Headers $headers 153 | $etag = $wikiPage.Headers.ETag 154 | $content = ($wikiPage.content | convertfrom-json).content 155 | $headers.Add("If-Match", "$etag") 156 | } 157 | catch { 158 | if (($_.ErrorDetails.Message | ConvertFrom-Json).typeKey -ne "WikiPageNotFoundException") { 159 | $_ 160 | exit 1 161 | } 162 | } 163 | 164 | $prUrl = "$env:SYSTEM_TEAMFOUNDATIONSERVERURI$([uri]::EscapeDataString("$env:SYSTEM_TEAMPROJECT/_git/$($env:BUILD_REPOSITORY_NAME)/pullrequest/$(pullRequestId)"))" 165 | 166 | $date = Get-Date -Format dd-MM-yyyy 167 | 168 | $changelog = @" 169 | 170 | # Changelog 171 | 172 | ## $version ($date) 173 | * PR [#$(pullRequestId)]($($prUrl)) - $(pullRequestTitle) 174 | `````` 175 | $(changelogEntry) 176 | `````` 177 | "@ 178 | 179 | if ($content -like "**") { 180 | $existingChangelog = ($content -split (""))[1] 181 | 182 | if ($existingChangelog -like "*## $version*") { 183 | $errorMessage = "Changelog for version $version already exists." 184 | Write-Error $errorMessage 185 | Write-Host "##vso[task.logissue type=error]$errorMessage" 186 | Write-Host "##vso[task.complete result=Failed;]$errorMessage" 187 | } 188 | 189 | $changelog = @" 190 | $changelog 191 | $existingChangelog 192 | "@ 193 | } 194 | 195 | $page = $docs -replace "(?s)(?<=!-- END_TF_DOCS -->).+?","$changelog" 196 | $markdown = @{content = $page } | ConvertTo-Json -Compress 197 | 198 | $params = @{ 199 | 'Uri' = $uri 200 | 'Headers' = $headers 201 | 'Method' = 'PUT' 202 | 'ContentType' = 'application/json; charset=utf-8' 203 | 'body' = "$markdown" 204 | } 205 | 206 | $params | ConvertTo-Json 207 | 208 | Invoke-RestMethod @params 209 | env: 210 | SYSTEM_ACCESSTOKEN: $(System.AccessToken) 211 | displayName: Create and publish markdown wiki 📚 212 | -------------------------------------------------------------------------------- /.tflint.hcl: -------------------------------------------------------------------------------- 1 | plugin "terraform" { 2 | enabled = true 3 | preset = "all" 4 | } 5 | 6 | plugin "azurerm" { 7 | enabled = true 8 | version = "0.25.1" 9 | source = "github.com/terraform-linters/tflint-ruleset-azurerm" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Modules monorepo on Azure DevOps 2 | Working Terraform monorepo example fully based on Azure DevOps. :tada: 3 | 4 | Pipelines are setup to execute following steps: tfsec, tflint, checkov and terraform validate, enabling early issue detection and shift left approach. 5 | 6 | For a detailed walkthrough, dive into blog article: [Azure DevOps Terraform Modules Monorepo.](https://cloudchronicles.blog/blog/Azure-DevOps-Terraform-Modules-Monorepo/) 7 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Changelog entry 2 | ``` 3 | TODO: Replace this inner text with a useful message 4 | for users of the affected modules! 5 | ``` 6 | 7 | --------------------------------------------------------------------------------