├── .eslintrc.js ├── .github ├── dependabot.yml ├── policies │ ├── issueMgmt.recentActivity.yml │ ├── issueMgmt.triageNew.yml │ └── prMgmt.dependabot.yml └── workflows │ ├── check-dist.yml │ └── ci-workflow.yml ├── .gitignore ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── HowToDebug.md ├── LICENSE ├── README.md ├── ReleaseProcess.md ├── SECURITY.md ├── action.yml ├── dist ├── index.js └── licenses.txt ├── examples ├── Advanced.md ├── advanced-example.yaml ├── exampleGuide.md └── template │ ├── parameters.json │ └── template.json ├── img └── wd-githubaction-arm.png ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── deploy │ ├── scope_managementgroup.ts │ ├── scope_resourcegroup.ts │ ├── scope_subscription.ts │ └── scope_tenant.ts ├── main.ts ├── run.ts └── utils │ ├── azhelper.ts │ └── utils.ts ├── test ├── bicep │ ├── inputs-outputs.bicep │ ├── inputs-outputs.bicepparam │ └── inputs-outputs.parameters.json ├── json │ ├── inputs-outputs.json │ └── inputs-outputs.parameters.json ├── main.test.ts ├── mocks.ts ├── parameters.json ├── resourceGroup-Negative │ ├── main.bicep │ └── main.bicepparam ├── subscription-Negative │ └── template.json ├── template.json └── tenant │ ├── negative │ ├── parameters.json │ └── template.json │ ├── parameters.json │ └── template.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | module.exports = { 4 | root: true, 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | ecmaVersion: 2020, 8 | sourceType: "module", 9 | project: './tsconfig.json', 10 | }, 11 | plugins: ["header"], 12 | extends: ["eslint:recommended", "plugin:prettier/recommended"], 13 | ignorePatterns: ["/out/**/*"], 14 | rules: { 15 | "header/header": [ 16 | 2, 17 | "line", 18 | [ 19 | " Copyright (c) Microsoft Corporation.", 20 | " Licensed under the MIT License.", 21 | ], 22 | ], 23 | }, 24 | overrides: [ 25 | { 26 | files: ["*.ts"], 27 | extends: [ 28 | "plugin:@typescript-eslint/recommended", 29 | "plugin:jest/all", 30 | "plugin:prettier/recommended", 31 | ], 32 | rules: { 33 | "jest/no-hooks": "off", 34 | "jest/prefer-expect-assertions": "off", 35 | "jest/expect-expect": [ 36 | "error", 37 | { 38 | assertFunctionNames: ["expect*", "invokingBicepCommand"], 39 | }, 40 | ], 41 | }, 42 | }, 43 | { 44 | files: ["*.js"], 45 | env: { node: true }, 46 | }, 47 | ], 48 | }; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "sunday" 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | day: "sunday" -------------------------------------------------------------------------------- /.github/policies/issueMgmt.recentActivity.yml: -------------------------------------------------------------------------------- 1 | id: issueMgmt.recentActivity 2 | name: GitOps.PullRequestIssueManagement 3 | description: Manages recent activity on issues - closes stale issues, adds updated issues to triage 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | scheduledSearches: 11 | - description: "Close issues with 'Status: No Recent Activity'" 12 | frequencies: 13 | - hourly: 14 | hour: 12 15 | filters: 16 | - isIssue 17 | - isOpen 18 | - hasLabel: 19 | label: "Needs: Author Feedback" 20 | - hasLabel: 21 | label: "Status: No Recent Activity" 22 | - noActivitySince: 23 | days: 3 24 | actions: 25 | - closeIssue 26 | 27 | - description: "Add 'Status: No Recent Activity' label to issues with 'Needs: Author Feedback' and inactive" 28 | frequencies: 29 | - hourly: 30 | hour: 12 31 | filters: 32 | - isIssue 33 | - isOpen 34 | - hasLabel: 35 | label: "Needs: Author Feedback" 36 | - noActivitySince: 37 | days: 7 38 | - isNotLabeledWith: 39 | label: "Status: No Recent Activity" 40 | actions: 41 | - addLabel: 42 | label: "Status: No Recent Activity" 43 | - addReply: 44 | reply: "Hi @${issueAuthor}, this issue has been marked as stale because it was labeled as requiring author feedback but has not had any activity for **7 days**. It will be closed if no further activity occurs **within 3 days of this comment**. Thanks for contributing! :smile: :mechanical_arm:" 45 | 46 | eventResponderTasks: 47 | - description: "Replace 'Needs: Author Feedback' with 'Needs: Triage' label when author comments" 48 | if: 49 | - payloadType: Issue_Comment 50 | - isAction: 51 | action: Created 52 | - isActivitySender: 53 | issueAuthor: True 54 | - hasLabel: 55 | label: "Needs: Author Feedback" 56 | - isOpen 57 | then: 58 | - addLabel: 59 | label: "Needs: Triage :mag:" 60 | - removeLabel: 61 | label: "Needs: Author Feedback" 62 | 63 | - description: "Remove 'Status: No Recent Activity' label when an issue is commented on" 64 | if: 65 | - payloadType: Issue_Comment 66 | - hasLabel: 67 | label: "Status: No Recent Activity" 68 | then: 69 | - removeLabel: 70 | label: "Status: No Recent Activity" 71 | onFailure: 72 | onSuccess: 73 | -------------------------------------------------------------------------------- /.github/policies/issueMgmt.triageNew.yml: -------------------------------------------------------------------------------- 1 | id: issueMgmt.triageNew 2 | name: GitOps.PullRequestIssueManagement 3 | description: Adds a triage tag to new issues 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | eventResponderTasks: 11 | - description: "Adds 'Needs: Triage' label for new issues" 12 | if: 13 | - payloadType: Issues 14 | - isAction: 15 | action: Opened 16 | then: 17 | - addLabel: 18 | label: "Needs: Triage :mag:" 19 | onFailure: 20 | onSuccess: 21 | -------------------------------------------------------------------------------- /.github/policies/prMgmt.dependabot.yml: -------------------------------------------------------------------------------- 1 | id: prMgmt.dependabot 2 | name: GitOps.PullRequestIssueManagement 3 | description: Approves and merges Dependabot PRs 4 | owner: 5 | resource: repository 6 | disabled: false 7 | where: 8 | configuration: 9 | resourceManagementConfiguration: 10 | eventResponderTasks: 11 | - description: Approve PRs submitted by dependabot with the "dependencies" label 12 | if: 13 | - payloadType: Pull_Request 14 | - hasLabel: 15 | label: dependencies 16 | - not: 17 | hasLabel: 18 | label: auto-merge 19 | - isActivitySender: 20 | user: dependabot[bot] 21 | issueAuthor: False 22 | then: 23 | - approvePullRequest: 24 | comment: ":shipit:" 25 | - addLabel: 26 | label: auto-merge 27 | - enableAutoMerge: 28 | mergeMethod: Squash 29 | onFailure: 30 | onSuccess: 31 | -------------------------------------------------------------------------------- /.github/workflows/check-dist.yml: -------------------------------------------------------------------------------- 1 | name: Check dist 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check-dist: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set Node.js 20.x 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20.x 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Rebuild the dist/ directory 26 | run: npm run package 27 | 28 | - name: Compare the expected and actual dist/ directories 29 | run: | 30 | if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 31 | echo "Detected uncommitted changes after build. See status below:" 32 | git diff 33 | exit 1 34 | fi 35 | id: diff -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | id-token: write 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | jobs: 17 | run-tests: 18 | name: Run Tests 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Check out source code 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v3 27 | with: 28 | languages: javascript 29 | 30 | - uses: azure/login@v2 31 | with: 32 | client-id: ${{ secrets.CLIENT_ID }} 33 | tenant-id: ${{ secrets.TENANT_ID }} 34 | subscription-id: ${{ secrets.SUBSCRIPTION_ID }} 35 | 36 | - name: Setup Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: "20.x" 40 | 41 | - run: npm ci 42 | 43 | - name: Run Tests 44 | env: 45 | SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }} 46 | run: npm test 47 | 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@v3 50 | 51 | run-tests-edge: 52 | name: Run Tests (Edge) 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: Check out source code 57 | uses: actions/checkout@v4 58 | 59 | - name: Initialize CodeQL 60 | uses: github/codeql-action/init@v3 61 | with: 62 | languages: javascript 63 | 64 | - name: Installing Az CLI Edge build 65 | run: | 66 | cd ../.. 67 | CWD="$(pwd)" 68 | python3 -m venv oidc-venv 69 | . oidc-venv/bin/activate 70 | echo "***********activated virual environment**********" 71 | python3 -m pip install --upgrade pip 72 | echo "***************started installing cli edge build******************" 73 | pip3 install -q --upgrade --pre azure-cli --extra-index-url https://azurecliprod.blob.core.windows.net/edge --no-cache-dir --upgrade-strategy=eager 74 | echo "***************installed cli Edge build*******************" 75 | echo "$CWD/oidc-venv/bin" >> $GITHUB_PATH 76 | az --version 77 | 78 | - uses: azure/login@v2 79 | with: 80 | client-id: ${{ secrets.CLIENT_ID }} 81 | tenant-id: ${{ secrets.TENANT_ID }} 82 | subscription-id: ${{ secrets.SUBSCRIPTION_ID }} 83 | 84 | - name: Setup Node.js 85 | uses: actions/setup-node@v4 86 | with: 87 | node-version: "20.x" 88 | 89 | - run: npm ci 90 | 91 | - name: Run Tests 92 | env: 93 | SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }} 94 | run: npm test 95 | 96 | - name: Perform CodeQL Analysis 97 | uses: github/codeql-action/analyze@v3 98 | 99 | execute-action: 100 | name: Execute Action 101 | runs-on: ${{ matrix.os }} 102 | strategy: 103 | fail-fast: false 104 | matrix: 105 | os: 106 | - windows-latest 107 | - ubuntu-latest 108 | - macos-latest 109 | 110 | steps: 111 | - name: Git Checkout 112 | uses: actions/checkout@v4 113 | 114 | - uses: azure/login@v2 115 | with: 116 | client-id: ${{ secrets.CLIENT_ID }} 117 | tenant-id: ${{ secrets.TENANT_ID }} 118 | subscription-id: ${{ secrets.SUBSCRIPTION_ID }} 119 | 120 | - name: Run Action 121 | id: deploy 122 | uses: ./ 123 | with: 124 | scope: resourcegroup 125 | subscriptionId: ${{ secrets.SUBSCRIPTION_ID }} 126 | resourceGroupName: arm-deploy-e2e 127 | parameters: test/bicep/inputs-outputs.bicepparam 128 | deploymentName: e2e-test-${{ matrix.os }} 129 | maskedOutputs: | 130 | myServerIP 131 | 132 | - name: Print Result 133 | run: | 134 | echo "StringOutput=${{ steps.deploy.outputs.stringOutput }}" 135 | echo "intOutput=${{ steps.deploy.outputs.intOutput }}" 136 | echo "objectOutput=${{ steps.deploy.outputs.objectOutput }}" 137 | echo "myServerIP=${{ steps.deploy.outputs.myServerIP }}" 138 | # myServerIP is register as secret, it can still be accessed, just won't print in raw form. 139 | echo "myServerIPEncoded=$(echo ${{ steps.deploy.outputs.myServerIP }} | base64)" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # local built files 7 | _build 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/run.ts", 15 | "outFiles": [ 16 | "${workspaceFolder}/_build/*.js" 17 | ], 18 | "env": { 19 | "INPUT_SCOPE": "resourcegroup", 20 | "INPUT_SUBSCRIPTIONID": "c00d16c7-6c1f-4c03-9be1-6934a4c49682", 21 | "INPUT_RESOURCEGROUPNAME": "kptest", 22 | "INPUT_TEMPLATE": "./test/template.json", 23 | "INPUT_PARAMETERS": "./test/parameters.json", 24 | "INPUT_DEPLOYMENTNAME": "github-test", 25 | "INPUT_DEPLOYMENTMODE": "Incremental" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /HowToDebug.md: -------------------------------------------------------------------------------- 1 | **Debugging the ARM action in local machine** 2 | 3 | Open PowerShell, go to the directory where the repo is stored (.../arm-deploy/) and execute the following commands. 4 | 5 | **1.npm install** \ 6 | npm install downloads dependencies defined in a package. json file and generates a node_modules folder with the installed modules. \ 7 | **2.npm install -g @vercel/ncc** \ 8 | **3.ncc build src/run.ts -s -o _build** \ 9 | ncc is a simple CLI for compiling a Node.js module into a single file, together with all its dependencies, gcc-style. \ 10 | **4. az login** \ 11 | This will open the browser, where you can do the Azure login which gives you proper access required for the action. 12 | 13 | Open the arm-deploy repository in VSCode, attach debugging points at required places _(flow begins from run.ts)_ and press F5. The debugger gets attached. 14 | 15 | Also, for various input values required while testing, you can specify those as environment variables in launch.json that gets created. \ 16 | _Happy debugging!_ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action for Azure Resource Manager (ARM) deployments 2 | 3 | > 🚀 **New Release Alert!** 4 | > We are excited to announce a new implementation of our GitHub Action for Azure Resource Manager (ARM) deployments! To improve the deployment and management of Azure resources, we’ve launched [azure/bicep-deploy](https://github.com/azure/bicep-deploy), which supports both Bicep and ARM templates, along with first-party Deployment Stacks support, making it easier than ever to manage your infrastructure directly from GitHub workflows. 5 | > 6 | > ⚠️ **Deprecation Notice:** This repository and action (azure/arm-deploy) will be **deprecated in the future**. We recommend switching to [azure/bicep-deploy](https://github.com/azure/bicep-deploy) for ongoing support and new features. 7 | > 8 | 9 | A GitHub Action to deploy ARM templates. With this action you can automate your workflow to deploy ARM templates and manage Azure resources. 10 | 11 | This action can be used to deploy Azure Resource Manager templates at different [deployment scopes](https://docs.microsoft.com/bs-latn-ba/Azure/azure-resource-manager/resource-group-template-deploy-rest#deployment-scope) - resource group deployment scope, subscription deployment scope and management group deployment scopes. 12 | 13 | By default, the action only parses the output and does not print them out. In order to get the values of ```outputs```use [this](https://github.com/Azure/arm-deploy#another-example-on-how-to-use-this-action-to-get-the-output-of-arm-template). 14 | 15 | ## Dependencies 16 | 17 | * [Azure Login](https://github.com/Azure/login) Login with your Azure credentials 18 | * [Checkout](https://github.com/actions/checkout) To checks-out your repository so the workflow can access any specified ARM template. 19 | 20 | ## Inputs 21 | 22 | * `scope`: Provide the scope of the deployment. Valid values are: `resourcegroup`(default) , `tenant`, `subscription`, `managementgroup`. 23 | * `resourceGroupName`: **Conditional** Provide the name of a resource group. Only required for Resource Group Scope 24 | * `subscriptionId`: **Conditional** Provide a value to override the subscription ID set by [Azure Login](https://github.com/Azure/login). 25 | * `managementGroupId`: **Conditional** Specify the Management Group ID, only required for Management Group Deployments. 26 | * `region`: **Conditional** Provide the target region, only required for Tenant, Management Group or Subscription deployments. 27 | * `template`: **Required** Specify the path or URL to the Azure Resource Manager template. 28 | * `parameters`: Specify the path or URL to the Azure Resource Manager deployment parameter values file (local / remote) and/or specify local overrides. 29 | * `deploymentMode`: `Incremental`(default) (only add resources to resource group) or `Complete` (remove extra resources from resource group) or `Validate` (only validates the template). 30 | * `deploymentName`: Specifies the name of the resource group deployment to create. 31 | * `failOnStdErr`: Specify whether to fail the action if some data is written to stderr stream of az cli. Valid values are: true, false. Default value set to true. 32 | * `additionalArguments`: Specify any additional arguments for the deployment. These arguments will be ignored while `validating` the template. 33 | * `maskedOutputs`: Specify list of output keys that its value need to be register to github action as secret. (checkout https://github.com/actions/toolkit/issues/184#issuecomment-1198653452 for valid multiline string) 34 | 35 | A good way to use additionalArguments would be to send optional parameters like `--what-if` or `--what-if-exclude-change-types`. [Read more about this here](https://docs.microsoft.com/en-us/cli/azure/deployment?view=azure-cli-latest#az-deployment-create-optional-parameters) 36 | ## Outputs 37 | Every template output will either be exported as output if output is a json object else will be consoled out where output is not a json object. 38 | 39 | ## Usage 40 | 41 | ```yml 42 | - uses: azure/arm-deploy@v2 43 | with: 44 | subscriptionId: 45 | resourceGroupName: 46 | template: 47 | ``` 48 | 49 | ## Example 50 | 51 | ```yaml 52 | on: [push] 53 | name: AzureARMSample 54 | 55 | jobs: 56 | build-and-deploy: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@master 60 | - uses: azure/login@v2 61 | with: 62 | creds: ${{ secrets.AZURE_CREDENTIALS }} 63 | - uses: azure/arm-deploy@v2 64 | with: 65 | resourceGroupName: github-action-arm-rg 66 | template: ./azuredeploy.json 67 | parameters: examples/template/parameters.json storageAccountType=Standard_LRS sqlServerPassword=${{ secrets.SQL_SERVER }} 68 | additionalArguments: "--what-if --rollback-on-error --what-if-exclude-change-types Create Ignore" 69 | ``` 70 | 71 | ## Another example which ensures the Azure Resource Group exists before ARM deployment 72 | In the preceeding example there is a pre-requisite that an existing Azure Resource Group named ```github-action-arm-rg``` must already exist. 73 | 74 | The below example makes use of the [Azure CLI Action](https://github.com/marketplace/actions/azure-cli-action) to ensure the resource group is created before doing an ARM deployment. Note that the command `az group create` is idempotent, so it will run sucessfully even if the group already exists. 75 | 76 | ## Steps 77 | When generating your credentials (in this example we store in a secret named ```AZURE_CREDENTIALS```) you will need to specify a scope at the subscription level. 78 | 79 | ```azurecli 80 | az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor --scopes /subscriptions/{subscription-id} 81 | ``` 82 | 83 | See [Configure deployment credentials](https://github.com/marketplace/actions/azure-login#configure-deployment-credentials). 84 | 85 | ## Example 86 | ```yaml 87 | on: [push] 88 | name: AzureARMSample 89 | 90 | jobs: 91 | build-and-deploy: 92 | runs-on: ubuntu-latest 93 | env: 94 | ResourceGroupName: github-action-arm-rg 95 | ResourceGroupLocation: "australiaeast" 96 | steps: 97 | - uses: actions/checkout@master 98 | - uses: azure/login@v2 99 | with: 100 | creds: ${{ secrets.AZURE_CREDENTIALS }} 101 | - uses: Azure/CLI@v2 102 | with: 103 | inlineScript: | 104 | #!/bin/bash 105 | az group create --name ${{ env.ResourceGroupName }} --location ${{ env.ResourceGroupLocation }} 106 | echo "Azure resource group created" 107 | - uses: azure/arm-deploy@v2 108 | with: 109 | resourceGroupName: ${{ env.ResourceGroupName }} 110 | template: ./azuredeploy.json 111 | parameters: storageAccountType=Standard_LRS 112 | ``` 113 | 114 | ## Another example on how to use this Action to get the output of ARM template 115 | In this example, our template outputs `containerName`. 116 | 117 | ## Steps 118 | ```yaml 119 | - uses: azure/arm-deploy@v2 120 | id: deploy 121 | with: 122 | resourceGroupName: azurearmaction 123 | template: examples/template/template.json 124 | parameters: examples/template/parameters.json 125 | deploymentName: github-advanced-test 126 | ``` 127 | Here we see a normal use of the Action, we pass the template as json file as well as the parameters. If we look into the `template.json` File we can see at the very bottom the defined outputs: 128 | ```json 129 | { 130 | ... 131 | "outputs": { 132 | ... 133 | "containerName": { 134 | "type": "string", 135 | "value": "[parameters('containerName')]" 136 | } 137 | } 138 | } 139 | ``` 140 | And we know our Action writes this output(s) to an action output variable with the same name, we can access it using `${{ steps.deploy.outputs.containerName }}` (Note: `deploy` comes from the `id` field from above.) 141 | 142 | If we now add a Shell script with a simple echo from that value, we can see that on the console the containername to be printed. 143 | 144 | ```yaml 145 | - run: echo ${{ steps.deploy.outputs.containerName }} 146 | ``` 147 | 148 | ARM Deploy Actions is supported for the Azure public cloud as well as Azure government clouds ('AzureUSGovernment' or 'AzureChinaCloud') and Azure Stack ('AzureStack') Hub. Before running this action, login to the respective Azure Cloud using [Azure Login](https://github.com/Azure/login) by setting appropriate value for the `environment` parameter. 149 | 150 | ## Example on how to use failOnStdErr 151 | In this example, we are setting `failOnStdErr` to false. 152 | 153 | ```yaml 154 | - uses: azure/arm-deploy@v2 155 | id: deploy 156 | with: 157 | resourceGroupName: azurearmaction 158 | template: examples/template/template.json 159 | parameters: examples/template/parameters.json 160 | deploymentName: github-advanced-test 161 | failOnStdErr: false 162 | ``` 163 | `failOnStdErr` equals true implied that if some data is written to stdErr and exit code from az-cli is 0, then action execution will fail. 164 | 165 | `failOnStdErr` equals false implies that if some data is written to stdErr and return code from az-cli is 0, then action will continue execution. This input is added to support cases where stdErr is being used to stream warning or progress info. 166 | 167 | Non zero Exit code will always lead to failure of action irrespective the value of `failOnStdErr`. 168 | 169 | For more examples, refer : [Example Guide](https://github.com/Azure/arm-deploy/blob/main/examples/exampleGuide.md) 170 | 171 | ## Az CLI dependency 172 | Internally in this action, we use azure CLI and execute `az login` with the credentials provided through secrets. In order to validate the new az CLI releases for this action, [canary test workflow](.github/workflows/azure-login-canary.yml) is written which will execute the action on [az CLI's edge build](https://github.com/Azure/azure-cli#edge-builds) which will fail incase of any breaking change is being introduced in the new upcoming release. The test results can be posted on a slack or teams channel using the corresponding integrations. Incase of a failure, the concern will be raised to [azure-cli](https://github.com/Azure/azure-cli) for taking a necessary action and also the latest CLI installation will be postponed in [Runner VMs](https://github.com/actions/virtual-environments) as well for hosted runner to prevent the workflows failing due to the new CLI changes. 173 | 174 | # Contributing 175 | 176 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 177 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 178 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 179 | 180 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 181 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 182 | provided by the bot. You will only need to do this once across all repos using our CLA. 183 | 184 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 185 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 186 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 187 | -------------------------------------------------------------------------------- /ReleaseProcess.md: -------------------------------------------------------------------------------- 1 | **Releasing a new version** 2 | 3 | Semanting versioning is used to release different versions of the action. Following steps are to be followed : 4 | 5 | 1. Create a new branch for every major version. \ 6 | Example, releases/v1, releases/v2. 7 | 2. For every minor and patch release for a major version, update the corresponding release branch. \ 8 | Example, for releasing v1.1.1, update releases/v1. 9 | 3. Create tags for every new release (major/minor/patch). \ 10 | Example,v1.0.0. , v1.0.1, v2.0.1, etc. and also have tags like v1, v2 for every major version release. 11 | 4. On releasing minor and patch versions, update the tag of the corresponding major version. \ 12 | Example, for releasing v1.0.1, update the v1 tag to point to the ref of the current release. \ 13 | The following commands are to be run on the release\v1 branch so that it picks the latest commit and updates the v1 tag accordingly : 14 | (Ensure that you are on same commit locally as you want to release.) 15 | * `git tag -fa v1 -m "Update v1 tag"` 16 | * `git push origin v1 --force` 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy Azure Resource Manager (ARM) Template" 2 | description: "Use this GitHub Action task to deploy Azure Resource Manager (ARM) template" 3 | inputs: 4 | scope: 5 | description: "Provide the scope of the deployment. Valid values are: 'resourcegroup', 'tenant', 'managementgroup', 'subscription'" 6 | required: true 7 | subscriptionId: 8 | description: "Override the Subscription Id set by Azure Login." 9 | required: false 10 | managementGroupId: 11 | description: "Specify the Id for the Management Group, only required for Management Group Deployments." 12 | required: false 13 | region: 14 | description: "Provide the target region, only required for tenant, management Group or Subscription deployments." 15 | required: false 16 | resourceGroupName: 17 | description: "Provide the name of a resource group, only required for resource Group deployments." 18 | required: false 19 | template: 20 | description: "Specify the path or URL to the Azure Resource Manager template." 21 | required: true 22 | deploymentMode: 23 | description: "Incremental (only add resources to resource group) or Complete (remove extra resources from resource group) or Validate (only validates the template)." 24 | required: false 25 | deploymentName: 26 | description: "Specifies the name of the resource group deployment to create." 27 | required: false 28 | parameters: 29 | description: "Supply deployment parameter values." 30 | required: false 31 | failOnStdErr: 32 | description: "Specify whether to fail the action if some data is written to stderr stream of az cli. Valid values are: true, false" 33 | required: false 34 | default: true 35 | additionalArguments: 36 | description: "Specify any additional arguments for the deployment." 37 | required: false 38 | maskedOutputs: 39 | description: "Specify list of output keys that its value need to be register to github action as secret. (checkout https://github.com/actions/toolkit/issues/184#issuecomment-1198653452 for valid multiline string)" 40 | required: false 41 | branding: 42 | color: orange 43 | icon: package 44 | runs: 45 | using: "node20" 46 | main: "dist/index.js" 47 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/http-client 26 | MIT 27 | Actions Http Client for Node.js 28 | 29 | Copyright (c) GitHub, Inc. 30 | 31 | All rights reserved. 32 | 33 | MIT License 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 36 | associated documentation files (the "Software"), to deal in the Software without restriction, 37 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 38 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 39 | subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 44 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 45 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 46 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 47 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | 50 | @actions/io 51 | MIT 52 | The MIT License (MIT) 53 | 54 | Copyright 2019 GitHub 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 61 | 62 | tunnel 63 | MIT 64 | The MIT License (MIT) 65 | 66 | Copyright (c) 2012 Koichi Kobayashi 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy 69 | of this software and associated documentation files (the "Software"), to deal 70 | in the Software without restriction, including without limitation the rights 71 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 72 | copies of the Software, and to permit persons to whom the Software is 73 | furnished to do so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in 76 | all copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 84 | THE SOFTWARE. 85 | 86 | 87 | uuid 88 | MIT 89 | The MIT License (MIT) 90 | 91 | Copyright (c) 2010-2020 Robert Kieffer and other contributors 92 | 93 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 94 | 95 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 98 | -------------------------------------------------------------------------------- /examples/Advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced example on how to use this Action 2 | In this example we deploy 2 templates (for the sake of the example the same template) but the second one depends on the first one as we need first the output of first one and second we need to override a parameter in the second template. 3 | Our template has two outputs `location` and `containerName`. But we are only interested in `containerName`, the first template will output that one and the second one requires that and appends `-overriden` so we can see it got overriden. 4 | 5 | ## Steps 6 | ```yaml 7 | - uses: azure/arm-deploy@v1 8 | id: deploy 9 | with: 10 | scope: resourcegroup 11 | resourceGroupName: azurearmaction 12 | template: examples/template/template.json 13 | parameters: examples/template/parameters.json 14 | deploymentName: github-advanced-test 15 | ``` 16 | Here we see a normal use of the Action, we pass the template as json file as well as the parameters. If we look into the `template.json` File we can see at the very bottom the defined outputs: 17 | ```json 18 | { 19 | ... 20 | "outputs": { 21 | ... 22 | "containerName": { 23 | "type": "string", 24 | "value": "[parameters('containerName')]" 25 | } 26 | } 27 | } 28 | ``` 29 | And we know our Action writes this output(s) to an action output variable with the same name, we can access it using `${{ steps.deploy.outputs.containerName }}` (Note: `deploy` comes from the `id` field from above.) 30 | 31 | If we now add a Shell script with a simple echo from that value, 32 | ```yaml 33 | - run: echo ${{ steps.deploy.outputs.containerName }} 34 | ``` 35 | we can see that on the console will be `github-action` printed. 36 | 37 | Now we add our second deployment which relies on that value and modfies the `containerName` parameter, 38 | ```yaml 39 | - uses: azure/arm-deploy@v1 40 | id: deploy2 41 | with: 42 | scope: resourcegroup 43 | resourceGroupName: azurearmaction 44 | template: examples/template/template.json 45 | parameters: examples/template/parameters.json containerName=${{ steps.deploy.outputs.containerName }}-overriden 46 | deploymentName: github-advanced-test 47 | ``` 48 | Look at the `parameters` section, where we plug in another `parameter.json` File and we pass in line seperated key-value pairs as overrides. If we now add again a shell script to see our ouput, 49 | ```yaml 50 | - run: echo ${{ steps.deploy2.outputs.containerName }} 51 | ``` 52 | we can see that on the console will be `github-action-overriden` printed. 53 | -------------------------------------------------------------------------------- /examples/advanced-example.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | test_action_job: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: azure/login@v1 15 | with: 16 | creds: ${{ secrets.AZURE_CREDENTIALS }} 17 | 18 | - uses: azure/arm-deploy@v1 19 | id: deploy 20 | with: 21 | scope: resourcegroup 22 | resourceGroupName: azurearmaction 23 | template: examples/template/template.json 24 | parameters: examples/template/parameters.json 25 | deploymentName: github-advanced-test 26 | 27 | - run: echo ${{ steps.deploy.outputs.containerName }} 28 | 29 | - uses: azure/arm-deploy@v1 30 | id: deploy2 31 | with: 32 | scope: resourcegroup 33 | resourceGroupName: azurearmaction 34 | template: examples/template/template.json 35 | parameters: examples/template/parameters.json containerName=${{ steps.deploy.outputs.containerName }}-overriden 36 | deploymentName: github-advanced-test 37 | 38 | - run: echo ${{ steps.deploy2.outputs.containerName }} 39 | -------------------------------------------------------------------------------- /examples/exampleGuide.md: -------------------------------------------------------------------------------- 1 | Following are some examples in which you can use the ARM action. 2 | 3 | 4 | Different scopes: 5 | 6 | **Resource group (Default group)** 7 | 8 | scope: resourcegroup 9 | subscriptionId: c01d26c9-****-****-****-************ 10 | resourceGroupName**: demoGroup 11 | template: examples/template/template.json 12 | 13 | There are a lot of [sample templates](https://github.com/Azure/azure-quickstart-templates) available which can be used for deployment. 14 | 15 | **Subscription** 16 | 17 | scope: subscription 18 | subscriptionId: c01d26c9-****-****-****-************ 19 | region: centralus 20 | template: https://raw.githubusercontent.com/Azure/azure-docs-json-samples/master/azure-resource-manager/emptyRG.json 21 | parameters: rgName=demoResourceGroup rgLocation=centralus 22 | 23 | More [Sample templates](https://github.com/Azure/azure-quickstart-templates/tree/master/subscription-deployments) can be found here. 24 | 25 | **Management group** 26 | 27 | scope: managementgroup 28 | managementGroupId: demoId 29 | region: centralus 30 | template: https://teststorage.blob/template.json 31 | parameters: https://teststorage.blob/parameters.json 32 | 33 | [Sample templates](https://github.com/Azure/azure-quickstart-templates/tree/master/managementgroup-deployments) can be found here. 34 | **Note:** Parameter value specified in parameter file can be overridden by specifying it along with parameter file name. 35 | Example: 36 | parameters: https://teststorage.blob/parameters.json parameterName=parameterValue 37 | 38 | **Things to keep in mind:** 39 | 40 | * For all scenarios, you can either paste the template in your repo and provide its location or can use the URI of the template directly. Same thing applies to providing parameters. 41 | * Authorization issue can be due to lack of proper access to the secret AZURE_CREDENTIALS. So, ensure that the secret has proper rights. 42 | * DeploymentMode is a parameter required only in case of Resource group deployments. It can either be incremental(default) or complete. 43 | -------------------------------------------------------------------------------- /examples/template/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "containerName": { 6 | "value": "github-action" 7 | }, 8 | "region": { 9 | "value": "westeurope" 10 | }, 11 | "imageType": { 12 | "value": "Public" 13 | }, 14 | "imageName": { 15 | "value": "whiteduck/sample-mvc" 16 | }, 17 | "osType": { 18 | "value": "Linux" 19 | }, 20 | "numberCpuCores": { 21 | "value": "1" 22 | }, 23 | "memory": { 24 | "value": "0.5" 25 | }, 26 | "restartPolicy": { 27 | "value": "OnFailure" 28 | }, 29 | "ipAddressType": { 30 | "value": "Public" 31 | }, 32 | "ports": { 33 | "value": [ 34 | { 35 | "port": "8080", 36 | "protocol": "TCP" 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/template/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "region": { 6 | "type": "string" 7 | }, 8 | "containerName": { 9 | "type": "string" 10 | }, 11 | "imageType": { 12 | "type": "string", 13 | "allowedValues": [ 14 | "Public", 15 | "Private" 16 | ] 17 | }, 18 | "imageName": { 19 | "type": "string" 20 | }, 21 | "osType": { 22 | "type": "string", 23 | "allowedValues": [ 24 | "Linux", 25 | "Windows" 26 | ] 27 | }, 28 | "numberCpuCores": { 29 | "type": "string" 30 | }, 31 | "memory": { 32 | "type": "string" 33 | }, 34 | "restartPolicy": { 35 | "type": "string", 36 | "allowedValues": [ 37 | "OnFailure", 38 | "Always", 39 | "Never" 40 | ] 41 | }, 42 | "ipAddressType": { 43 | "type": "string" 44 | }, 45 | "ports": { 46 | "type": "array" 47 | } 48 | }, 49 | "resources": [ 50 | { 51 | "region": "[parameters('region')]", 52 | "name": "[parameters('containerName')]", 53 | "type": "Microsoft.ContainerInstance/containerGroups", 54 | "apiVersion": "2018-10-01", 55 | "properties": { 56 | "containers": [ 57 | { 58 | "name": "[parameters('containerName')]", 59 | "properties": { 60 | "image": "[parameters('imageName')]", 61 | "resources": { 62 | "requests": { 63 | "cpu": "[int(parameters('numberCpuCores'))]", 64 | "memoryInGB": "[float(parameters('memory'))]" 65 | } 66 | }, 67 | "ports": "[parameters('ports')]" 68 | } 69 | } 70 | ], 71 | "restartPolicy": "[parameters('restartPolicy')]", 72 | "osType": "[parameters('osType')]", 73 | "ipAddress": { 74 | "type": "[parameters('ipAddressType')]", 75 | "ports": "[parameters('ports')]" 76 | } 77 | }, 78 | "tags": {} 79 | } 80 | ], 81 | "outputs": { 82 | "region": { 83 | "type": "string", 84 | "value": "[parameters('region')]" 85 | }, 86 | "containerName": { 87 | "type": "string", 88 | "value": "[parameters('containerName')]" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /img/wd-githubaction-arm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/arm-deploy/5e8a0b7801014be112ee2f99d5e43491a037feea/img/wd-githubaction-arm.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | moduleFileExtensions: [ 4 | "ts", 5 | "js" 6 | ], 7 | transform: { 8 | '^.+\\.(ts|tsx)$': 'ts-jest', 9 | }, 10 | testMatch: [ 11 | '**/test/**/*.test.(ts)' 12 | ], 13 | testEnvironment: 'node', 14 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arm-deploy", 3 | "version": "1.0.0", 4 | "description": "GitHub Action task to deploy an Azure Resource Manager (ARM) template", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest", 9 | "package": "ncc build --license licenses.txt src/run.ts", 10 | "lint": "eslint src --ext ts", 11 | "lint:fix": "eslint src --ext ts --fix" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Azure/arm-deploy.git" 16 | }, 17 | "keywords": [ 18 | "arm templates", 19 | "bicep" 20 | ], 21 | "author": "Microsoft", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@types/jest": "^29.5.13", 25 | "@typescript-eslint/eslint-plugin": "^8.6.0", 26 | "@typescript-eslint/parser": "^8.6.0", 27 | "@vercel/ncc": "^0.38.1", 28 | "eslint": "^9.10.0", 29 | "eslint-config-prettier": "^9.1.0", 30 | "eslint-plugin-header": "^3.1.1", 31 | "eslint-plugin-jest": "^28.8.3", 32 | "eslint-plugin-prettier": "^5.2.1", 33 | "jest": "^29.7.0", 34 | "ts-jest": "^29.2.5", 35 | "ts-node": "^10.9.2", 36 | "typescript": "^5.6.2" 37 | }, 38 | "dependencies": { 39 | "@actions/core": "^1.10.1", 40 | "@actions/exec": "^1.1.1", 41 | "@actions/io": "^1.1.3", 42 | "@types/assert": "^1.5.10", 43 | "@types/node": "^22.5.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/deploy/scope_managementgroup.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import * as core from "@actions/core"; 4 | import { DeploymentResult, joinCliArguments } from "../utils/utils"; 5 | import { AzCliHelper } from "../utils/azhelper"; 6 | 7 | export async function deployManagementGroupScope( 8 | azCli: AzCliHelper, 9 | region: string, 10 | template: string | undefined, 11 | deploymentMode: string | undefined, 12 | deploymentName: string, 13 | parameters: string | undefined, 14 | managementGroupId: string, 15 | failOnStdErr: boolean, 16 | maskedOutputs: string[] | undefined, 17 | additionalArguments: string | undefined, 18 | ): Promise { 19 | // Check if region is set 20 | if (!region) { 21 | throw Error("Region must be set."); 22 | } 23 | 24 | // check if mode is set as this will be ignored 25 | if (deploymentMode && deploymentMode != "validate") { 26 | core.warning( 27 | "This deployment mode is not supported for management group scoped deployments, this parameter will be ignored!", 28 | ); 29 | } 30 | 31 | // create the parameter list 32 | const validateParameters = joinCliArguments( 33 | region ? `--location "${region}"` : undefined, 34 | template 35 | ? template.startsWith("http") 36 | ? `--template-uri ${template}` 37 | : `--template-file ${template}` 38 | : undefined, 39 | managementGroupId 40 | ? `--management-group-id "${managementGroupId}"` 41 | : undefined, 42 | deploymentName ? `--name "${deploymentName}"` : undefined, 43 | parameters ? `--parameters ${parameters}` : undefined, 44 | ); 45 | 46 | let azDeployParameters = validateParameters; 47 | if (additionalArguments) { 48 | azDeployParameters += ` ${additionalArguments}`; 49 | } 50 | 51 | // validate the deployment 52 | core.info("Validating template..."); 53 | await azCli.validate( 54 | `deployment mg validate ${validateParameters} -o json`, 55 | deploymentMode === "validate", 56 | ); 57 | 58 | if (deploymentMode != "validate") { 59 | // execute the deployment 60 | core.info("Creating deployment..."); 61 | return await azCli.deploy( 62 | `deployment mg create ${azDeployParameters} -o json`, 63 | maskedOutputs, 64 | failOnStdErr, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/deploy/scope_resourcegroup.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import * as core from "@actions/core"; 4 | import { DeploymentResult, joinCliArguments } from "../utils/utils"; 5 | import { AzCliHelper } from "../utils/azhelper"; 6 | 7 | export async function deployResourceGroupScope( 8 | azCli: AzCliHelper, 9 | resourceGroupName: string, 10 | template: string | undefined, 11 | deploymentMode: string | undefined, 12 | deploymentName: string, 13 | parameters: string | undefined, 14 | failOnStdErr: boolean, 15 | maskedOutputs: string[] | undefined, 16 | additionalArguments: string | undefined, 17 | ): Promise { 18 | // Check if resourceGroupName is set 19 | if (!resourceGroupName) { 20 | throw Error("ResourceGroup name must be set."); 21 | } 22 | 23 | // Check if the resourceGroup exists 24 | const rgExists = await azCli.resourceGroupExists(resourceGroupName); 25 | if (!rgExists) { 26 | throw Error(`Resource Group ${resourceGroupName} could not be found.`); 27 | } 28 | 29 | // create the parameter list 30 | const validateParameters = joinCliArguments( 31 | resourceGroupName ? `--resource-group ${resourceGroupName}` : undefined, 32 | template 33 | ? template.startsWith("http") 34 | ? `--template-uri ${template}` 35 | : `--template-file ${template}` 36 | : undefined, 37 | deploymentMode && deploymentMode != "validate" 38 | ? `--mode ${deploymentMode}` 39 | : "--mode Incremental", 40 | deploymentName ? `--name "${deploymentName}"` : undefined, 41 | parameters ? `--parameters ${parameters}` : undefined, 42 | ); 43 | 44 | let azDeployParameters = validateParameters; 45 | if (additionalArguments) { 46 | azDeployParameters += ` ${additionalArguments}`; 47 | } 48 | 49 | // validate the deployment 50 | core.info("Validating template..."); 51 | await azCli.validate( 52 | `deployment group validate ${validateParameters} -o json`, 53 | deploymentMode === "validate", 54 | ); 55 | 56 | if (deploymentMode != "validate") { 57 | // execute the deployment 58 | core.info("Creating deployment..."); 59 | return await azCli.deploy( 60 | `deployment group create ${azDeployParameters} -o json`, 61 | maskedOutputs, 62 | failOnStdErr, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/deploy/scope_subscription.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import * as core from "@actions/core"; 4 | import { DeploymentResult, joinCliArguments } from "../utils/utils"; 5 | import { AzCliHelper } from "../utils/azhelper"; 6 | 7 | export async function deploySubscriptionScope( 8 | azCli: AzCliHelper, 9 | region: string, 10 | template: string | undefined, 11 | deploymentMode: string | undefined, 12 | deploymentName: string, 13 | parameters: string | undefined, 14 | failOnStdErr: boolean, 15 | maskedOutputs: string[] | undefined, 16 | additionalArguments: string | undefined, 17 | ): Promise { 18 | // Check if region is set 19 | if (!region) { 20 | throw Error("Region must be set."); 21 | } 22 | 23 | // check if mode is set as this will be ignored 24 | if (deploymentMode && deploymentMode != "validate") { 25 | core.warning( 26 | "This deployment mode is not supported for subscription scoped deployments, this parameter will be ignored!", 27 | ); 28 | } 29 | 30 | // create the parameter list 31 | const validateParameters = joinCliArguments( 32 | region ? `--location "${region}"` : undefined, 33 | template 34 | ? template.startsWith("http") 35 | ? `--template-uri ${template}` 36 | : `--template-file ${template}` 37 | : undefined, 38 | deploymentName ? `--name "${deploymentName}"` : undefined, 39 | parameters ? `--parameters ${parameters}` : undefined, 40 | ); 41 | 42 | let azDeployParameters = validateParameters; 43 | if (additionalArguments) { 44 | azDeployParameters += ` ${additionalArguments}`; 45 | } 46 | 47 | // validate the deployment 48 | core.info("Validating template..."); 49 | await azCli.validate( 50 | `deployment sub validate ${validateParameters} -o json`, 51 | deploymentMode === "validate", 52 | ); 53 | 54 | if (deploymentMode != "validate") { 55 | // execute the deployment 56 | core.info("Creating deployment..."); 57 | return await azCli.deploy( 58 | `deployment sub create ${azDeployParameters} -o json`, 59 | maskedOutputs, 60 | failOnStdErr, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/deploy/scope_tenant.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import * as core from "@actions/core"; 4 | import { DeploymentResult, joinCliArguments } from "../utils/utils"; 5 | import { AzCliHelper } from "../utils/azhelper"; 6 | 7 | export async function deployTenantScope( 8 | azCli: AzCliHelper, 9 | region: string, 10 | template: string | undefined, 11 | deploymentMode: string | undefined, 12 | deploymentName: string, 13 | parameters: string | undefined, 14 | failOnStdErr: boolean, 15 | maskedOutputs: string[] | undefined, 16 | additionalArguments: string | undefined, 17 | ): Promise { 18 | // Check if region is set 19 | if (!region) { 20 | throw Error("Region must be set."); 21 | } 22 | 23 | // check if mode is set as this will be ignored 24 | if (deploymentMode && deploymentMode != "validate") { 25 | core.warning( 26 | "This deployment mode is not supported for tenant scoped deployments, this parameter will be ignored!", 27 | ); 28 | } 29 | // create the parameter list 30 | const validateParameters = joinCliArguments( 31 | region ? `--location "${region}"` : undefined, 32 | template 33 | ? template.startsWith("http") 34 | ? `--template-uri ${template}` 35 | : `--template-file ${template}` 36 | : undefined, 37 | deploymentName ? `--name "${deploymentName}"` : undefined, 38 | parameters ? `--parameters ${parameters}` : undefined, 39 | ); 40 | 41 | let azDeployParameters = validateParameters; 42 | if (additionalArguments) { 43 | azDeployParameters += ` ${additionalArguments}`; 44 | } 45 | 46 | // validate the deployment 47 | core.info("Validating template..."); 48 | await azCli.validate( 49 | `deployment tenant validate ${validateParameters} -o json`, 50 | deploymentMode === "validate", 51 | ); 52 | 53 | if (deploymentMode != "validate") { 54 | // execute the deployment 55 | core.info("Creating deployment..."); 56 | return await azCli.deploy( 57 | `deployment tenant create ${azDeployParameters} -o json`, 58 | maskedOutputs, 59 | failOnStdErr, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import { 4 | getBooleanInput, 5 | info, 6 | getInput, 7 | getMultilineInput, 8 | setFailed, 9 | setOutput, 10 | } from "@actions/core"; 11 | import { deployResourceGroupScope } from "./deploy/scope_resourcegroup"; 12 | import { deployTenantScope } from "./deploy/scope_tenant"; 13 | import { deployManagementGroupScope } from "./deploy/scope_managementgroup"; 14 | import { deploySubscriptionScope } from "./deploy/scope_subscription"; 15 | import { DeploymentResult } from "./utils/utils"; 16 | import { getAzCliHelper } from "./utils/azhelper"; 17 | 18 | export type Options = { 19 | scope: string; 20 | managementGroupId: string; 21 | subscriptionId: string; 22 | resourceGroupName: string; 23 | region: string; 24 | template?: string; 25 | deploymentMode?: string; 26 | deploymentName: string; 27 | parameters?: string; 28 | additionalArguments?: string; 29 | maskedOutputs?: string[]; 30 | failOnStdErr: boolean; 31 | }; 32 | 33 | async function populateOptions(): Promise { 34 | const scope = getInput("scope") || "resourcegroup"; 35 | const subscriptionId = getInput("subscriptionId"); 36 | const region = getInput("region"); 37 | const resourceGroupName = getInput("resourceGroupName"); 38 | const template = getInput("template"); 39 | const deploymentMode = getInput("deploymentMode").toLowerCase(); 40 | const deploymentName = getInput("deploymentName"); 41 | const parameters = getInput("parameters"); 42 | const managementGroupId = getInput("managementGroupId"); 43 | const additionalArguments = getInput("additionalArguments"); 44 | const maskedOutputs = getMultilineInput("maskedOutputs"); 45 | let failOnStdErr; 46 | try { 47 | failOnStdErr = getBooleanInput("failOnStdErr"); 48 | } catch (err) { 49 | failOnStdErr = true; 50 | } 51 | 52 | return { 53 | scope, 54 | subscriptionId, 55 | region: region, 56 | resourceGroupName, 57 | template, 58 | deploymentMode, 59 | deploymentName, 60 | parameters, 61 | managementGroupId, 62 | additionalArguments, 63 | maskedOutputs, 64 | failOnStdErr, 65 | }; 66 | } 67 | 68 | export async function deploy( 69 | options: Options, 70 | ): Promise { 71 | // determine az path 72 | const azCli = await getAzCliHelper(); 73 | 74 | // retrieve action variables 75 | const { 76 | scope, 77 | subscriptionId, 78 | region: region, 79 | resourceGroupName, 80 | template, 81 | deploymentMode, 82 | deploymentName, 83 | parameters, 84 | managementGroupId, 85 | additionalArguments, 86 | maskedOutputs, 87 | failOnStdErr, 88 | } = options; 89 | 90 | // change the subscription context 91 | if ( 92 | scope !== "tenant" && 93 | scope !== "managementgroup" && 94 | subscriptionId !== "" 95 | ) { 96 | info("Changing subscription context..."); 97 | await azCli.setSubscriptionContext(subscriptionId); 98 | } 99 | 100 | // Run the Deployment 101 | switch (scope) { 102 | case "resourcegroup": 103 | return await deployResourceGroupScope( 104 | azCli, 105 | resourceGroupName, 106 | template, 107 | deploymentMode, 108 | deploymentName, 109 | parameters, 110 | failOnStdErr, 111 | maskedOutputs, 112 | additionalArguments, 113 | ); 114 | 115 | case "tenant": 116 | return await deployTenantScope( 117 | azCli, 118 | region, 119 | template, 120 | deploymentMode, 121 | deploymentName, 122 | parameters, 123 | failOnStdErr, 124 | maskedOutputs, 125 | additionalArguments, 126 | ); 127 | 128 | case "managementgroup": 129 | return await deployManagementGroupScope( 130 | azCli, 131 | region, 132 | template, 133 | deploymentMode, 134 | deploymentName, 135 | parameters, 136 | managementGroupId, 137 | failOnStdErr, 138 | maskedOutputs, 139 | additionalArguments, 140 | ); 141 | 142 | case "subscription": 143 | return await deploySubscriptionScope( 144 | azCli, 145 | region, 146 | template, 147 | deploymentMode, 148 | deploymentName, 149 | parameters, 150 | failOnStdErr, 151 | maskedOutputs, 152 | additionalArguments, 153 | ); 154 | 155 | default: 156 | throw new Error( 157 | "Invalid scope. Valid values are: 'resourcegroup', 'tenant', 'managementgroup', 'subscription'", 158 | ); 159 | } 160 | } 161 | 162 | // Action Main code 163 | export async function main() { 164 | try { 165 | const options = await populateOptions(); 166 | const result = await deploy(options); 167 | 168 | if (result) { 169 | for (const outputName in result.outputs) { 170 | setOutput(outputName, result.outputs[outputName]); 171 | } 172 | } 173 | } catch (err) { 174 | setFailed(`${err}`); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/run.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import { main } from "./main"; 4 | 5 | main(); 6 | -------------------------------------------------------------------------------- /src/utils/azhelper.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | import { ExecOptions, exec } from "@actions/exec"; 4 | import * as core from "@actions/core"; 5 | import { getDeploymentResult } from "./utils"; 6 | import { which } from "@actions/io"; 7 | 8 | export class AzCliHelper { 9 | constructor(private azPath: string) {} 10 | 11 | setSubscriptionContext = setSubscriptionContext.bind(null, this.azPath); 12 | resourceGroupExists = resourceGroupExists.bind(null, this.azPath); 13 | deploy = deploy.bind(null, this.azPath); 14 | validate = validate.bind(null, this.azPath); 15 | } 16 | 17 | export async function getAzCliHelper() { 18 | const azPath = await which("az", true); 19 | 20 | return new AzCliHelper(azPath); 21 | } 22 | 23 | async function setSubscriptionContext(azPath: string, subscriptionId: string) { 24 | await callAzCli(azPath, `account set --subscription ${subscriptionId}`, { 25 | silent: true, 26 | }); 27 | } 28 | 29 | async function resourceGroupExists(azPath: string, resourceGroupName: string) { 30 | const exitCode = await callAzCli( 31 | azPath, 32 | `group show --resource-group ${resourceGroupName}`, 33 | { silent: true, ignoreReturnCode: true }, 34 | ); 35 | 36 | return exitCode === 0; 37 | } 38 | 39 | async function deploy(azPath: string, command: string, maskedOutputs: string[]|undefined, failOnStdErr: boolean) { 40 | let hasStdErr = false; 41 | let stdOut = ""; 42 | const options: ExecOptions = { 43 | silent: true, 44 | ignoreReturnCode: true, 45 | failOnStdErr: false, 46 | listeners: { 47 | stderr: (data: BufferSource) => { 48 | const error = data.toString(); 49 | if (error && error.trim().length !== 0) { 50 | hasStdErr = true; 51 | core.error(error); 52 | } 53 | }, 54 | stdout: (data: BufferSource) => { 55 | stdOut += data.toString(); 56 | }, 57 | debug: (data: string) => { 58 | core.debug(data); 59 | }, 60 | }, 61 | }; 62 | 63 | const exitCode = await callAzCli(azPath, command, options); 64 | 65 | if (exitCode != 0) { 66 | throw new Error("Deployment failed."); 67 | } 68 | 69 | if (hasStdErr && failOnStdErr) { 70 | throw new Error( 71 | "Deployment process failed as some lines were written to stderr", 72 | ); 73 | } 74 | 75 | core.info("Parsing outputs..."); 76 | // getDeploymentResult handles the secret masking 77 | const result = getDeploymentResult(stdOut, maskedOutputs); 78 | // print stdOut only after secret masking 79 | core.debug(stdOut); 80 | return result 81 | } 82 | 83 | async function validate( 84 | azPath: string, 85 | command: string, 86 | failOnNonZeroExit: boolean, 87 | ) { 88 | const options: ExecOptions = { 89 | silent: true, 90 | ignoreReturnCode: true, 91 | listeners: { 92 | stderr: (data: BufferSource) => { 93 | core.warning(data.toString()); 94 | }, 95 | }, 96 | }; 97 | 98 | const exitCode = await callAzCli(azPath, command, options); 99 | 100 | if (failOnNonZeroExit && exitCode != 0) { 101 | throw new Error("Template validation failed."); 102 | } else if (exitCode != 0) { 103 | core.warning("Template validation failed."); 104 | } 105 | } 106 | 107 | async function callAzCli( 108 | azPath: string, 109 | command: string, 110 | options: ExecOptions, 111 | ) { 112 | return await exec(`"${azPath}" ${command}`, [], options); 113 | } 114 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import { debug, setSecret } from "@actions/core"; 5 | 6 | export type DeploymentResult = { 7 | outputs: Record; 8 | }; 9 | 10 | export function getDeploymentResult(commandOutput: string, maskedOutputs: string[]|undefined): DeploymentResult { 11 | // parse the result and save the outputs 12 | const outputs: Record = {}; 13 | try { 14 | const parsed = JSON.parse(commandOutput) as { 15 | properties: { 16 | outputs: { 17 | [index: string]: { 18 | value: any; 19 | }; 20 | }; 21 | }; 22 | }; 23 | 24 | debug("registering secrets for keys: " + maskedOutputs); 25 | for (const key in parsed.properties.outputs) { 26 | const maskedValue = parsed.properties.outputs[key].value; 27 | if (maskedOutputs && maskedOutputs.some(maskedKey => maskedKey === key)) { 28 | setSecret(JSON.stringify(maskedValue)); 29 | debug("registered output value as secret for key: " + key); 30 | } 31 | outputs[key] = maskedValue; 32 | } 33 | } catch (err) { 34 | console.error(commandOutput); 35 | } 36 | 37 | return { 38 | outputs, 39 | }; 40 | } 41 | 42 | export function joinCliArguments(...args: (string | undefined)[]) { 43 | return args.filter(Boolean).join(" "); 44 | } 45 | -------------------------------------------------------------------------------- /test/bicep/inputs-outputs.bicep: -------------------------------------------------------------------------------- 1 | param stringParam string 2 | param intParam int 3 | param objectParam object 4 | 5 | output stringOutput string = stringParam 6 | output intOutput int = intParam 7 | output objectOutput object = objectParam 8 | output myServerIP string = '192.168.0.1' 9 | -------------------------------------------------------------------------------- /test/bicep/inputs-outputs.bicepparam: -------------------------------------------------------------------------------- 1 | using 'inputs-outputs.bicep' 2 | 3 | param intParam = 42 4 | param stringParam = 'hello world' 5 | param objectParam = { 6 | prop1: 'value1' 7 | prop2: 'value2' 8 | } 9 | -------------------------------------------------------------------------------- /test/bicep/inputs-outputs.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "intParam": { 6 | "value": 42 7 | }, 8 | "stringParam": { 9 | "value": "hello world" 10 | }, 11 | "objectParam": { 12 | "value": { 13 | "prop1": "value1", 14 | "prop2": "value2" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/json/inputs-outputs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "metadata": { 5 | "_generator": { 6 | "name": "bicep", 7 | "version": "0.25.3.34343", 8 | "templateHash": "11866388564313615137" 9 | } 10 | }, 11 | "parameters": { 12 | "stringParam": { 13 | "type": "string" 14 | }, 15 | "intParam": { 16 | "type": "int" 17 | }, 18 | "objectParam": { 19 | "type": "object" 20 | } 21 | }, 22 | "resources": [], 23 | "outputs": { 24 | "stringOutput": { 25 | "type": "string", 26 | "value": "[parameters('stringParam')]" 27 | }, 28 | "intOutput": { 29 | "type": "int", 30 | "value": "[parameters('intParam')]" 31 | }, 32 | "objectOutput": { 33 | "type": "object", 34 | "value": "[parameters('objectParam')]" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/json/inputs-outputs.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "intParam": { 6 | "value": 42 7 | }, 8 | "stringParam": { 9 | "value": "hello world" 10 | }, 11 | "objectParam": { 12 | "value": { 13 | "prop1": "value1", 14 | "prop2": "value2" 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/main.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import path from "path"; 5 | import { coreMock, mockInputs } from "./mocks"; 6 | import { main } from "../src/main"; 7 | import { randomBytes } from "crypto"; 8 | 9 | const liveTestTimeout = 5 * 60 * 1000; // 5 minutes 10 | 11 | function getRandomDeploymentName(prefix: string) { 12 | const suffix = randomBytes(5).toString("hex"); 13 | 14 | return `${prefix}-${suffix}`; 15 | } 16 | 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | describe("live tests", () => { 22 | it("resource group scope - positive", async () => { 23 | mockInputs({ 24 | scope: 'resourcegroup', 25 | managementGroupId: '', 26 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 27 | resourceGroupName: 'arm-deploy-e2e', 28 | region: '', 29 | template: path.resolve(__dirname, './json/inputs-outputs.json'), 30 | parameters: path.resolve(__dirname, './json/inputs-outputs.parameters.json'), 31 | deploymentMode: 'Complete', 32 | deploymentName: getRandomDeploymentName('test-rg'), 33 | additionalArguments: '', 34 | failOnStdErr: false, 35 | }); 36 | 37 | await main(); 38 | expect(coreMock.setFailed).not.toHaveBeenCalled(); 39 | }, liveTestTimeout); 40 | 41 | it("resource group scope - negative", async () => { 42 | mockInputs({ 43 | scope: 'resourcegroup', 44 | managementGroupId: '', 45 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 46 | resourceGroupName: 'arm-deploy-e2e', 47 | region: '', 48 | parameters: path.resolve(__dirname, './resourceGroup-Negative/main.bicepparam'), 49 | deploymentMode: 'Complete', 50 | deploymentName: getRandomDeploymentName('test-rg'), 51 | additionalArguments: '', 52 | failOnStdErr: false, 53 | }); 54 | 55 | await main(); 56 | expect(coreMock.setFailed).toHaveBeenCalled(); 57 | }, liveTestTimeout); 58 | 59 | // subscription scope auth not currently supported in live tests 60 | xit("subscription scope - positive", async () => { 61 | mockInputs({ 62 | scope: 'subscription', 63 | managementGroupId: '', 64 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 65 | resourceGroupName: '', 66 | region: 'centralus', 67 | template: 'https://raw.githubusercontent.com/Azure/azure-docs-json-samples/master/azure-resource-manager/emptyrg.json', 68 | parameters: 'rgName=demoResourceGroup rgLocation=centralus', 69 | deploymentMode: '', 70 | deploymentName: getRandomDeploymentName('test-sub'), 71 | additionalArguments: '', 72 | failOnStdErr: false, 73 | }); 74 | 75 | await main(); 76 | expect(coreMock.setFailed).not.toHaveBeenCalled(); 77 | }, liveTestTimeout); 78 | 79 | // subscription scope auth not currently supported in live tests 80 | xit("subscription scope - negative", async () => { 81 | mockInputs({ 82 | scope: 'subscription', 83 | managementGroupId: '', 84 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 85 | resourceGroupName: '', 86 | region: 'centralus', 87 | template: path.resolve(__dirname, './subscription-Negative/template.json'), 88 | parameters: 'rgName=demoResourceGroup rgLocation=centralus', 89 | deploymentMode: '', 90 | deploymentName: getRandomDeploymentName('test-sub'), 91 | additionalArguments: '', 92 | failOnStdErr: false, 93 | }); 94 | 95 | await main(); 96 | expect(coreMock.setFailed).toHaveBeenCalled(); 97 | }, liveTestTimeout); 98 | 99 | it("validate mode - positive", async () => { 100 | mockInputs({ 101 | scope: 'resourcegroup', 102 | managementGroupId: '', 103 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 104 | resourceGroupName: 'arm-deploy-e2e', 105 | region: '', 106 | template: path.resolve(__dirname, './json/inputs-outputs.json'), 107 | parameters: path.resolve(__dirname, './json/inputs-outputs.parameters.json'), 108 | deploymentMode: 'Validate', 109 | deploymentName: getRandomDeploymentName('test-rg'), 110 | additionalArguments: '', 111 | failOnStdErr: false, 112 | }); 113 | 114 | await main(); 115 | expect(coreMock.setFailed).not.toHaveBeenCalled(); 116 | }, liveTestTimeout); 117 | 118 | it("can deploy .bicep files", async () => { 119 | mockInputs({ 120 | scope: 'resourcegroup', 121 | managementGroupId: '', 122 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 123 | resourceGroupName: 'arm-deploy-e2e', 124 | region: '', 125 | template: path.resolve(__dirname, './bicep/inputs-outputs.bicep'), 126 | parameters: path.resolve(__dirname, './bicep/inputs-outputs.parameters.json'), 127 | deploymentName: getRandomDeploymentName('test-rg'), 128 | failOnStdErr: false, 129 | }); 130 | 131 | await main(); 132 | 133 | expect(coreMock.setFailed).not.toHaveBeenCalled(); 134 | expect(coreMock.setOutput).toHaveBeenCalledWith('stringOutput', 'hello world'); 135 | expect(coreMock.setOutput).toHaveBeenCalledWith('intOutput', 42); 136 | expect(coreMock.setOutput).toHaveBeenCalledWith('objectOutput', { 137 | prop1: 'value1', 138 | prop2: 'value2', 139 | }); 140 | }, liveTestTimeout); 141 | 142 | it("can deploy .bicepparam files", async () => { 143 | mockInputs({ 144 | scope: 'resourcegroup', 145 | managementGroupId: '', 146 | subscriptionId: process.env.SUBSCRIPTION_ID ?? '', 147 | resourceGroupName: 'arm-deploy-e2e', 148 | region: '', 149 | parameters: path.resolve(__dirname, './bicep/inputs-outputs.bicepparam'), 150 | deploymentName: getRandomDeploymentName('test-rg'), 151 | failOnStdErr: false, 152 | }); 153 | 154 | await main(); 155 | 156 | expect(coreMock.setFailed).not.toHaveBeenCalled(); 157 | expect(coreMock.setOutput).toHaveBeenCalledWith('stringOutput', 'hello world'); 158 | expect(coreMock.setOutput).toHaveBeenCalledWith('intOutput', 42); 159 | expect(coreMock.setOutput).toHaveBeenCalledWith('objectOutput', { 160 | prop1: 'value1', 161 | prop2: 'value2', 162 | }); 163 | }, liveTestTimeout); 164 | }); -------------------------------------------------------------------------------- /test/mocks.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@actions/core', () => coreMock); 2 | import { Options } from "../src/main"; 3 | 4 | export const coreMock = { 5 | setFailed: jest.fn(), 6 | setOutput: jest.fn(), 7 | getBooleanInput: jest.fn(), 8 | getInput: jest.fn(), 9 | getMultilineInput: jest.fn(), 10 | error: jest.fn(), 11 | warning: jest.fn(), 12 | info: jest.fn(), 13 | debug: jest.fn(), 14 | }; 15 | 16 | export function mockInputs(options: Options) { 17 | coreMock.getInput.mockImplementation(name => { 18 | return options[name as keyof Options] ?? ''; 19 | }); 20 | coreMock.getBooleanInput.mockImplementation(name => { 21 | return options[name as keyof Options] ?? false; 22 | }); 23 | } -------------------------------------------------------------------------------- /test/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "targetMG": { 6 | "value": "E2eTestGroupForArmAction" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /test/resourceGroup-Negative/main.bicep: -------------------------------------------------------------------------------- 1 | param storageAccountName string 2 | 3 | param location string = resourceGroup().location 4 | 5 | resource sa 'Microsoft.Storage/storageAccounts@2019-06-01' = { 6 | name: storageAccountName 7 | location: location 8 | sku: { 9 | name: 'Premium_ZRS' 10 | } 11 | kind: 'StorageV2' 12 | } 13 | -------------------------------------------------------------------------------- /test/resourceGroup-Negative/main.bicepparam: -------------------------------------------------------------------------------- 1 | using './main.bicep' 2 | 3 | param storageAccountName = 'foo' 4 | 5 | -------------------------------------------------------------------------------- /test/subscription-Negative/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.1", 4 | "parameters": { 5 | "rgName": { 6 | "type": "string" 7 | }, 8 | "rgLocation": { 9 | "type": "string" 10 | }, 11 | "tags": { 12 | "type": "object", 13 | "defaultValue": {} 14 | } 15 | }, 16 | "variables": {}, 17 | "resources": [ 18 | { 19 | "type": "Microsoft.Resources/resourceGroups", 20 | "apiVersion": "2018-05-01", 21 | "location": "wrongLocation", 22 | "name": "[parameters('rgName')]", 23 | "properties": {}, 24 | "tags": "[parameters('tags')]" 25 | } 26 | ], 27 | "outputs": {} 28 | } -------------------------------------------------------------------------------- /test/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-08-01/managementGroupDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "targetMG": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Target Management Group" 9 | } 10 | }, 11 | "allowedLocations": { 12 | "type": "array", 13 | "defaultValue": [ 14 | "australiaeast", 15 | "australiasoutheast", 16 | "australiacentral" 17 | ], 18 | "metadata": { 19 | "description": "An array of the allowed locations, all other locations will be denied by the created policy." 20 | } 21 | } 22 | }, 23 | "variables": { 24 | "mgScope": "[tenantResourceId('Microsoft.Management/managementGroups', parameters('targetMG'))]", 25 | "policyDefinition": "LocationRestriction" 26 | }, 27 | "resources": [ 28 | { 29 | "type": "Microsoft.Authorization/policyDefinitions", 30 | "name": "[variables('policyDefinition')]", 31 | "apiVersion": "2019-09-01", 32 | "properties": { 33 | "policyType": "Custom", 34 | "mode": "All", 35 | "parameters": { 36 | }, 37 | "policyRule": { 38 | "if": { 39 | "not": { 40 | "field": "location", 41 | "in": "[parameters('allowedLocations')]" 42 | } 43 | }, 44 | "then": { 45 | "effect": "deny" 46 | } 47 | } 48 | } 49 | }, 50 | { 51 | "type": "Microsoft.Authorization/policyAssignments", 52 | "name": "location-lock", 53 | "apiVersion": "2019-09-01", 54 | "dependsOn": [ 55 | "[variables('policyDefinition')]" 56 | ], 57 | "properties": { 58 | "scope": "[variables('mgScope')]", 59 | "policyDefinitionId": "[extensionResourceId(variables('mgScope'), 'Microsoft.Authorization/policyDefinitions', variables('policyDefinition'))]" 60 | } 61 | } 62 | ] 63 | } -------------------------------------------------------------------------------- /test/tenant/negative/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "mgName": { 6 | "value": "mg-test01" 7 | }, 8 | "mgDisplayName": { 9 | "value": "Test Management Group" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/tenant/negative/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "mgName": { 6 | "type": "int", 7 | "defaultValue": "[concat('mg-', uniqueString(newGuid()))]" 8 | }, 9 | "mgDisplayName": { 10 | "type": "int" 11 | } 12 | }, 13 | "resources": [ 14 | { 15 | "type": "Microsoft.Management/managementGroups", 16 | "apiVersion": "2021-04-01", 17 | "name": "[parameters('mgName')]", 18 | "properties": { 19 | "displayName": "[parameters('mgDisplayName')]" 20 | } 21 | } 22 | ], 23 | "outputs": {} 24 | } -------------------------------------------------------------------------------- /test/tenant/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "mgName": { 6 | "value": "mg-test01" 7 | }, 8 | "mgDisplayName": { 9 | "value": "Test Management Group" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/tenant/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "mgName": { 6 | "type": "string", 7 | "defaultValue": "[concat('mg-', uniqueString(newGuid()))]" 8 | }, 9 | "mgDisplayName": { 10 | "type": "string" 11 | } 12 | }, 13 | "resources": [ 14 | { 15 | "type": "Microsoft.Management/managementGroups", 16 | "apiVersion": "2021-04-01", 17 | "name": "[parameters('mgName')]", 18 | "properties": { 19 | "displayName": "[parameters('mgDisplayName')]" 20 | } 21 | } 22 | ], 23 | "outputs": {} 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } --------------------------------------------------------------------------------