├── .github └── workflows │ ├── codeql.yml │ └── main.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── __tests__ ├── DeploymentProvider │ ├── AzureSpringAppsDeploymentProvider.test.ts │ └── DeploymentHelper.test.ts └── test_apps │ ├── source-app │ ├── pom.xml │ └── src │ │ └── main │ │ ├── assembly │ │ └── assembly.xml │ │ └── java │ │ └── com │ │ └── azure │ │ └── spring │ │ └── cloud │ │ └── test │ │ └── config │ │ └── client │ │ └── Application.java │ └── target │ └── hello-world.jar ├── action.yml ├── icon.svg ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── DeploymentProvider │ ├── AzureSpringAppsDeploymentProvider.ts │ ├── DeploymentHelper.ts │ └── azure-storage.ts ├── main.ts └── operations │ └── ActionParameters.ts └── tsconfig.json /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '26 7 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | # - name: Autobuild 59 | # uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | - name: Set up JDK 8 67 | if: ${{ matrix.language == 'java' }} 68 | uses: actions/setup-java@v3 69 | with: 70 | distribution: 'adopt' 71 | java-version: '8' 72 | cache: 'maven' 73 | 74 | - name: Build with Maven 75 | if: ${{ matrix.language == 'java' }} 76 | working-directory: ${{ github.workspace }}/__tests__/test_apps/source-app 77 | run: mvn -B package --file pom.xml 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v2 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [workflow_dispatch] 2 | env: 3 | ASC_PACKAGE_PATH: ${{ github.workspace }} 4 | JAR_PATH: __tests__/test_apps/target/hello-world.jar 5 | VAR1: Eastern Standard Time 6 | 7 | jobs: 8 | Standard_tier_deploy_job: 9 | runs-on: ubuntu-latest 10 | name: Standard tier deploy 11 | steps: 12 | - name: Checkout Github Action 13 | uses: actions/checkout@master 14 | 15 | - name: Login via Azure CLI 16 | uses: azure/login@v1 17 | with: 18 | creds: ${{ secrets.AZURE_SUBSCRIPTION_SP }} 19 | 20 | - name: Get current date 21 | id: date 22 | run: echo "::set-output name=date::$(date +'%m%d')" 23 | 24 | - name: Create Staging Deploy 25 | uses: Azure/spring-apps-deploy@v1 26 | with: 27 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 28 | action: deploy 29 | service-name: "github-action-standard-${{ steps.date.outputs.date }}" 30 | app-name: hello 31 | create-new-deployment: true 32 | use-staging-deployment: true 33 | package: ${{ github.workspace }}/${{ env.JAR_PATH }} 34 | jvm-options: -Xms512m -Xmx512m 35 | environment-variables: "-CUSTOMER_NAME Contoso \ 36 | -WEBSITE_TIME_ZONE \"${{ env.VAR1 }}\"" 37 | 38 | - name: Deploy to production (source) 39 | uses: Azure/spring-apps-deploy@v1 40 | with: 41 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 42 | action: deploy 43 | service-name: "github-action-standard-${{ steps.date.outputs.date }}" 44 | app-name: hello 45 | use-staging-deployment: false 46 | package: ${{ github.workspace }}/__tests__/test_apps/source-app 47 | 48 | - name: Set production 49 | uses: Azure/spring-apps-deploy@v1 50 | with: 51 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 52 | action: set-production 53 | service-name: "github-action-standard-${{ steps.date.outputs.date }}" 54 | app-name: hello 55 | use-staging-deployment: true 56 | 57 | - name: Delete staging deployment 58 | uses: Azure/spring-apps-deploy@v1 59 | with: 60 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 61 | action: delete-staging-deployment 62 | service-name: "github-action-standard-${{ steps.date.outputs.date }}" 63 | app-name: hello 64 | use-staging-deployment: true 65 | 66 | Enterprise_tier_deploy_job: 67 | runs-on: ubuntu-latest 68 | name: Enterprise tier deploy 69 | steps: 70 | - name: Checkout Github Action 71 | uses: actions/checkout@master 72 | 73 | - name: Login via Azure CLI 74 | uses: azure/login@v1 75 | with: 76 | creds: ${{ secrets.AZURE_SUBSCRIPTION_SP }} 77 | 78 | - name: Get current date 79 | id: date 80 | run: echo "::set-output name=date::$(date +'%m%d')" 81 | 82 | - name: Create Staging Deploy 83 | uses: Azure/spring-apps-deploy@v1 84 | with: 85 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 86 | action: deploy 87 | service-name: "github-action-enterprise-${{ steps.date.outputs.date }}" 88 | app-name: hello 89 | create-new-deployment: true 90 | use-staging-deployment: true 91 | package: ${{ github.workspace }}/${{ env.JAR_PATH }} 92 | builder: default 93 | 94 | - name: Deploy to production (source) 95 | uses: Azure/spring-apps-deploy@v1 96 | with: 97 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 98 | action: deploy 99 | service-name: "github-action-enterprise-${{ steps.date.outputs.date }}" 100 | app-name: hello 101 | use-staging-deployment: false 102 | package: ${{ github.workspace }}/__tests__/test_apps/source-app 103 | builder: default 104 | 105 | - name: Set production 106 | uses: Azure/spring-apps-deploy@v1 107 | with: 108 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 109 | action: set-production 110 | service-name: "github-action-enterprise-${{ steps.date.outputs.date }}" 111 | app-name: hello 112 | use-staging-deployment: true 113 | 114 | - name: Delete staging deployment 115 | uses: Azure/spring-apps-deploy@v1 116 | with: 117 | azure-subscription: ${{ secrets.AZURE_SUBSCRIPTION }} 118 | action: delete-staging-deployment 119 | service-name: "github-action-enterprise-${{ steps.date.outputs.date }}" 120 | app-name: hello 121 | use-staging-deployment: true 122 | -------------------------------------------------------------------------------- /.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 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | [Ll]ib/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | **/Properties/launchSettings.json 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.iobj 69 | *.pch 70 | *.pdb 71 | *.ipdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp 80 | *.tmp_proj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 deploying to Azure Spring Apps 2 | 3 | GitHub Actions support an automated software development lifecycle workflow. With GitHub Actions for Azure Spring Apps you can create workflows in your repository to manage your deployment of Azure Spring Apps conveniently. 4 | 5 | ## Prerequisites 6 | ### Set up GitHub repository and authenticate 7 | 8 | You need an [Azure service principal credential](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli) to authorize Azure login action. To get an Azure credential, execute the following commands on your local machine: 9 | ```azurecli 10 | az login 11 | az ad sp create-for-rbac --role contributor --scopes /subscriptions/ --sdk-auth 12 | ``` 13 | 14 | To access to a specific resource group, you can reduce the scope: 15 | 16 | ```azurecli 17 | az ad sp create-for-rbac --role contributor --scopes /subscriptions//resourceGroups/ --sdk-auth 18 | ``` 19 | The command should output a JSON object: 20 | 21 | ```json 22 | { 23 | "clientId": "", 24 | "clientSecret": "", 25 | "subscriptionId": "", 26 | "tenantId": "", 27 | ... 28 | } 29 | ``` 30 | 31 | ### Dependencies on other GitHub Actions 32 | 33 | * [Checkout](https://github.com/actions/checkout) Checkout your Git repository content into GitHub Actions agent. 34 | * Authenticate using the [Azure Login Action](https://github.com/Azure/login) with the Azure service principal credential prepared as mentioned above. Examples are given later in this article. 35 | 36 | ## End-to-End Sample Workflows 37 | ### Deploying 38 | #### To production 39 | Azure Spring Apps supports deploying to deployments with built artifacts (e.g., JAR or .NET Core ZIP) or source code archive. 40 | The following example deploys to the default production deployment in Azure Spring Apps using JAR file built by Maven. This is the only possible deployment scenario when using the Basic SKU: 41 | 42 | ```yml 43 | name: AzureSpringApps 44 | on: push 45 | env: 46 | ASC_PACKAGE_PATH: ${{ github.workspace }} 47 | AZURE_SUBSCRIPTION: 48 | 49 | jobs: 50 | deploy_to_production: 51 | runs-on: ubuntu-latest 52 | name: deploy to production with artifact 53 | steps: 54 | - name: Checkout Github Action 55 | uses: actions/checkout@v2 56 | 57 | - name: Set up JDK 1.8 58 | uses: actions/setup-java@v1 59 | with: 60 | java-version: 1.8 61 | 62 | - name: maven build, clean 63 | run: | 64 | mvn clean package 65 | 66 | - name: Login via Azure CLI 67 | uses: azure/login@v1 68 | with: 69 | creds: ${{ secrets.AZURE_CREDENTIALS }} 70 | 71 | - name: deploy to production with artifact 72 | uses: azure/spring-apps-deploy@v1 73 | with: 74 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 75 | action: Deploy 76 | service-name: 77 | app-name: 78 | use-staging-deployment: false 79 | package: ${{ env.ASC_PACKAGE_PATH }}/**/*.jar 80 | ``` 81 | 82 | The following example deploys to the default production deployment in Azure Spring Apps using source code. 83 | 84 | ```yml 85 | name: AzureSpringApps 86 | on: push 87 | env: 88 | ASC_PACKAGE_PATH: ${{ github.workspace }} 89 | AZURE_SUBSCRIPTION: 90 | 91 | jobs: 92 | deploy_to_production: 93 | runs-on: ubuntu-latest 94 | name: deploy to production with soruce code 95 | steps: 96 | - name: Checkout Github Action 97 | uses: actions/checkout@v2 98 | 99 | - name: Login via Azure CLI 100 | uses: azure/login@v1 101 | with: 102 | creds: ${{ secrets.AZURE_CREDENTIALS }} 103 | 104 | - name: deploy to production step with soruce code 105 | uses: azure/spring-apps-deploy@v1 106 | with: 107 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 108 | action: deploy 109 | service-name: 110 | app-name: 111 | use-staging-deployment: false 112 | package: ${{ env.ASC_PACKAGE_PATH }} 113 | ``` 114 | 115 | #### Blue-green 116 | 117 | The following examples deploy to an existing staging deployment. This deployment will not receive production traffic until it is set as a production deployment. You can set use-staging-deployment true to find the staging deployment automatically or just allocate specific deployment-name. We will only focus on the spring-apps-deploy action and leave out the preparatory jobs in the rest of the article. 118 | 119 | ```yml 120 | # environment preparation configurations omitted 121 | steps: 122 | - name: blue green deploy step use-staging-deployment 123 | uses: azure/spring-apps-deploy@v1 124 | with: 125 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 126 | action: deploy 127 | service-name: 128 | app-name: 129 | use-staging-deployment: true 130 | package: ${{ env.ASC_PACKAGE_PATH }}/**/*.jar 131 | ``` 132 | 133 | ```yml 134 | # environment preparation configurations omitted 135 | steps: 136 | - name: blue green deploy step with deployment-name 137 | uses: azure/spring-apps-deploy@v1 138 | with: 139 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 140 | action: deploy 141 | service-name: 142 | app-name: 143 | deployment-name: staging 144 | package: ${{ env.ASC_PACKAGE_PATH }}/**/*.jar 145 | ``` 146 | 147 | For more information on blue-green deployments, including an alternative approach, see [Blue-green deployment strategies](https://docs.microsoft.com/en-us/azure/spring-apps/concepts-blue-green-deployment-strategies). 148 | 149 | #### Creating new deployment 150 | The following example shows how to create a new staing deployment. CPU and memory can be allocated when creating new deployment. 151 | ```yml 152 | # environment preparation configurations omitted 153 | steps: 154 | - name: blue green deploy step with deployment-name 155 | uses: azure/spring-apps-deploy@v1 156 | with: 157 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 158 | action: deploy 159 | service-name: 160 | app-name: 161 | create-new-deployment: true 162 | deployment-name: staging 163 | cpu: 164 | memory: 165 | package: ${{ env.ASC_PACKAGE_PATH }}/**/*.jar 166 | ``` 167 | 168 | 169 | #### Custom container image support 170 | To deploy directly from a existing container image, use the following template. 171 | ```yml 172 | # environment preparation configurations omitted 173 | steps: 174 | - name: Deploy custom container image 175 | uses: Azure/spring-apps-deploy@v1 176 | with: 177 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 178 | action: deploy 179 | service-name: 180 | app-name: 181 | deployment-name: 182 | container-registry: 183 | registry-username: 184 | registry-password: 185 | container-image: 186 | ``` 187 | 188 | ### Setting production deployment 189 | 190 | The following example will set the current staging deployment as production, effectively swapping which deployment will receive production traffic. 191 | 192 | ```yml 193 | # environment preparation configurations omitted 194 | steps: 195 | - name: set production deployment step 196 | uses: azure/spring-apps-deploy@v1 197 | with: 198 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 199 | action: set-production 200 | service-name: 201 | app-name: 202 | use-staging-deployment: true 203 | ``` 204 | ### Deleting a staging deployment 205 | 206 | The "Delete Staging Deployment" action allows you to delete the deployment not receiving production traffic. This frees up resources used by that deployment and makes room for a new staging deployment: 207 | 208 | ```yml 209 | # environment preparation configurations omitted 210 | steps: 211 | - name: Delete staging deployment step 212 | uses: azure/spring-apps-deploy@v1 213 | with: 214 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 215 | action: delete-staging-deployment 216 | service-name: 217 | app-name: 218 | ``` 219 | 220 | ### Create or update build 221 | 222 | The following example will create or update an build resource. 223 | ```yml 224 | # environment preparation configurations omitted 225 | steps: 226 | - name: Create or update build 227 | uses: azure/spring-apps-deploy@v1 228 | with: 229 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 230 | action: build 231 | service-name: 232 | build-name: 233 | package: ${{ env.ASC_PACKAGE_PATH }} 234 | builder: 235 | ``` 236 | 237 | ### Delete build 238 | 239 | The following example will delete an build resource. 240 | ```yml 241 | # environment preparation configurations omitted 242 | steps: 243 | - name: Delete build 244 | uses: azure/spring-apps-deploy@v1 245 | with: 246 | azure-subscription: ${{ env.AZURE_SUBSCRIPTION }} 247 | action: delete-build 248 | service-name: 249 | build-name: 250 | ``` 251 | 252 | ## Arguments 253 | > [!NOTE] 254 | > Some arguments are only applicable for certain settings of the `action` argument. The Action column below specifies the pertinent actions for each argument. Any argument listed as Required is only required for the pertinent Action(s). 255 | 256 | |Argument|
Action
|Required|Description| 257 | |--- |--- |--- |--- | 258 | |`action`|all|Required| The action to be performed by this task.
One of: `deploy`, `set-production`, `delete-staging-deployment`, `build`, `delete-build`
Default value: `deploy`| 259 | |`azure-subscription`|all|Required| The Azure subscription ID for the target Azure Spring Apps instance.| 260 | |`service-name`|all|Required| The name of the Azure Spring Apps service instance.| 261 | |`app-name`|deploy
set-production
delete-staging-deployment|Optional| The name of the Azure Spring Apps app to deploy. The app must exist prior to task execution.| 262 | |`use-staging-deployment`|deploy
set-production|Optional| If set to `true`, apply the task to whichever deployment is set as the staging deployment at time of execution. If set to `false`, apply the task to the production deployment.
Default value: `true`| 263 | |`deployment-name`|deploy
set-production|Optional| The name of the deployment to which the action will apply. It overrides the setting of `use-staging-deployment`.| 264 | |`create-new-deployment`|deploy|Optional| If set to `true` and the deployment specified by `deployment-name` does not exist at execution time, it will be created.
Default value: `false`| 265 | |`package`|deploy
build|Required| The file path to the package containing the application to be deployed (`.jar` file for Java, `.zip` for .NET Core) or to a folder containing the application source to be built.
Default value: ```${{ github.workspace }}/**/*.jar```| 266 | |`target-module`|deploy|Optional| Child module to be deployed, required for multiple jar packages built from source code.| 267 | |`cpu`|deploy|Optional| The CPU resource quantity. It should be 500m or number of CPU cores. It is effective only when creating new deployment.
Default value: 1| 268 | |`memory`|deploy|Optional| The memory resource quantity. It should be 512Mi or #Gi, e.g., 1Gi, 3Gi. It is effective only when creating new deployment.
Default value: 1Gi| 269 | |`runtime-version`|deploy|Optional| The runtime stack for the application.
One of: `Java_8`, `Java_11`, `NetCore_31`,
Default value: `Java_11`| 270 | |`environment-variables`|deploy|Optional| Environment variables to be entered using the syntax '-key value'. Values containing spaces should be enclosed in double quotes.
Example: ```-CUSTOMER_NAME Contoso -WEBSITE_TIME_ZONE "Eastern Standard Time"```| 271 | |`jvm-options`|deploy|Optional| A string containing JVM Options.
Example: `-Dspring.profiles.active=mysql`| 272 | |`dotnetcore-mainentry-path`|deploy|Optional| A string containing the path to the .NET executable relative to zip root.| 273 | |`version`|deploy|Optional| The deployment version. If not set, the version is left unchanged.| 274 | |`build-name`|build
delete-build|Optional| (Enterprise Tier Only) The build name.| 275 | |`builder`|deploy
build|Optional| (Enterprise Tier Only) Build service builder used to build the executable.| 276 | |`build-cpu`|deploy
build|Optional| (Enterprise Tier Only) CPU resource quantity for build container. Should be 500m or number of CPU cores. Default: 1| 277 | |`build-memory`|deploy
build|Optional| (Enterprise Tier Only) Memory resource quantity for build container. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi. Default: 2Gi.| 278 | |`build-env`|deploy
build|Optional| (Enterprise Tier Only) Space-separated environment variables for the build process using the syntax '-key value'.
Example: ```-CUSTOMER_NAME Contoso -WEBSITE_TIME_ZONE "Eastern Standard Time"```| 279 | |`config-file-patterns`|deploy|Optional| (Enterprise Tier Only) Config file patterns separated with ',' to decide which patterns of Application Configuration Service will be used. Use '""' to clear existing configurations.| 280 | |`container-registry`|deploy|Optional| The registry of the container image.
Default value: `docker.io`| 281 | |`registry-username`|deploy|Optional| The username of the container registry.| 282 | |`registry-password`|deploy|Optional| The password of the container registry.| 283 | |`container-image `|deploy|Optional| The container image.| 284 | |`container-command`|deploy|Optional| The command of the container.| 285 | |`container-args`|deploy|Optional| The arguments of the container.| 286 | |`language-framework`|deploy|Optional| The language framework of the container.| 287 | |`enable-liveness-probe`|deploy|Optional| If false, will disable the liveness probe of the app instance. Allowed values: false, true.| 288 | |`enable-readiness-probe`|deploy|Optional| If false, will disable the readiness probe of the app instance. Allowed values: false, true.| 289 | |`enable-startup-probe`|deploy|Optional| If false, will disable the startup probe of the app instance. Allowed values: false, true.| 290 | |`termination-grace-period-seconds`|deploy|Optional| Optional duration in seconds the app instance needs to terminate gracefully.| 291 | |`liveness-probe-config`|deploy|Optional| A json file path indicates the liveness probe config.| 292 | |`readiness-probe-config`|deploy|Optional| A json file path indicates the readiness probe config.| 293 | |`startup-probe-config`|deploy|Optional| A json file path indicates the startup probe config.| 294 | ## Contributing 295 | 296 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 297 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 298 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 299 | 300 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 301 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 302 | provided by the bot. You will only need to do this once across all repos using our CLA. 303 | 304 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 305 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 306 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 307 | 308 | ## Trademarks 309 | 310 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 311 | trademarks or logos is subject to and must follow 312 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 313 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 314 | Any use of third-party trademarks or logos are subject to those third-party's policies. 315 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /__tests__/DeploymentProvider/AzureSpringAppsDeploymentProvider.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import {ActionParameters, ActionParametersUtility} from "../../src/operations/ActionParameters"; 3 | import {DeploymentHelper} from "../../src/DeploymentProvider/DeploymentHelper"; 4 | import {AzureSpringAppsDeploymentProvider} from "../../src/DeploymentProvider/AzureSpringAppsDeploymentProvider"; 5 | 6 | jest.mock('@azure/identity'); 7 | jest.mock('@actions/core'); 8 | jest.mock('@azure/arm-appplatform') 9 | jest.mock('../../src/DeploymentProvider/DeploymentHelper') 10 | 11 | describe('Test azure-spring-apps-deployment-provider', () => { 12 | 13 | afterEach(() => { 14 | jest.restoreAllMocks(); 15 | }) 16 | 17 | test("set active deployment", async () => { 18 | const params: ActionParameters = { 19 | azureSubscription: 'AzureSubscription', 20 | serviceName: 'ServiceName', 21 | action: 'set-production', 22 | appName: 'AppName', 23 | useStagingDeployment: true 24 | } 25 | const actionParamsSpy = jest.spyOn(ActionParametersUtility, 'getParameters').mockReturnValue(params); 26 | const stagingSpy = jest.spyOn(DeploymentHelper, 'getStagingDeploymentName').mockImplementation(async () => 'staging'); 27 | let provider: AzureSpringAppsDeploymentProvider = new AzureSpringAppsDeploymentProvider(); 28 | await provider.deployAppStep(); 29 | expect(DeploymentHelper.setActiveDeployment).toBeCalledTimes(1); 30 | expect(DeploymentHelper.getStagingDeploymentName).toBeCalledTimes(1); 31 | }); 32 | 33 | test("delete staging deployment", async () => { 34 | const params: ActionParameters = { 35 | azureSubscription: 'AzureSubscription', 36 | serviceName: 'ServiceName', 37 | action: 'delete-staging-deployment', 38 | appName: 'AppName' 39 | } 40 | const actionParamsSpy = jest.spyOn(ActionParametersUtility, 'getParameters').mockReturnValue(params); 41 | const stagingSpy = jest.spyOn(DeploymentHelper, 'getStagingDeploymentName').mockImplementation( async () => 'staging'); 42 | let provider: AzureSpringAppsDeploymentProvider = new AzureSpringAppsDeploymentProvider(); 43 | await provider.deployAppStep(); 44 | expect(DeploymentHelper.deleteDeployment).toBeCalledTimes(1); 45 | expect(DeploymentHelper.getStagingDeploymentName).toBeCalledTimes(1); 46 | }); 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/DeploymentProvider/DeploymentHelper.test.ts: -------------------------------------------------------------------------------- 1 | import * as asa from '@azure/arm-appplatform' 2 | import * as identity from '@azure/identity' 3 | import {ActionParameters, ActionParametersUtility} from "../../src/operations/ActionParameters"; 4 | import {DeploymentHelper} from "../../src/DeploymentProvider/DeploymentHelper"; 5 | 6 | jest.mock('@azure/identity'); 7 | jest.mock('@actions/core'); 8 | 9 | describe('Test azure-spring-apps-deployment-helper', () => { 10 | afterEach(() => { 11 | jest.restoreAllMocks(); 12 | }) 13 | let clientMock: jest.Mocked = new asa.AppPlatformManagementClient(new identity.DefaultAzureCredential(), '') as any; 14 | let paramsMock: jest.Mocked = {} as any; 15 | let deploymentListMock: Array = [ 16 | { 17 | properties: { 18 | active: false 19 | }, 20 | name: 'staging' 21 | }, 22 | { 23 | properties: { 24 | active: true 25 | }, 26 | name: 'production' 27 | } 28 | ]; 29 | let responseMock: asa.DeploymentsListResponse = deploymentListMock as asa.DeploymentsListResponse 30 | clientMock.deployments.list = jest.fn().mockReturnValue(responseMock); 31 | test("get staging deployment name", async () => { 32 | const stagingName = await DeploymentHelper.getStagingDeploymentName(clientMock, paramsMock); 33 | expect(stagingName).toBe('staging'); 34 | }); 35 | 36 | test("gets all deployment names", async () => { 37 | const names = await DeploymentHelper.getAllDeploymentsName(clientMock, paramsMock); 38 | expect(names).toMatchObject(['staging', 'production']); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/test_apps/source-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.1.RELEASE 9 | 10 | 11 | com.azure.spring.cloud.test-apps 12 | echo-app 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 1.8 17 | Hoxton.SR8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-web 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-actuator 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-starter-netflix-eureka-client 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.springframework.cloud 39 | spring-cloud-dependencies 40 | ${spring-cloud.version} 41 | pom 42 | import 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 53 | 54 | maven-assembly-plugin 55 | 3.2.0 56 | 57 | 58 | src/main/assembly/assembly.xml 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /__tests__/test_apps/source-app/src/main/assembly/assembly.xml: -------------------------------------------------------------------------------- 1 | 2 | bundle 3 | 4 | tar.gz 5 | 6 | false 7 | 8 | 9 | ${basedir} 10 | 11 | src/** 12 | pom.xml 13 | 14 | . 15 | 16 | 17 | -------------------------------------------------------------------------------- /__tests__/test_apps/source-app/src/main/java/com/azure/spring/cloud/test/config/client/Application.java: -------------------------------------------------------------------------------- 1 | package com.azure.spring.cloud.test.config.client; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @SpringBootApplication 10 | @RestController 11 | public class Application { 12 | 13 | public static void main(String [] args) { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | 17 | @GetMapping("/{message}") 18 | public String echo(@PathVariable("message") String message) { 19 | System.out.println(message + " from echo sample"); 20 | System.out.flush(); 21 | return message; 22 | } 23 | 24 | @GetMapping("/health") 25 | public String health() { 26 | return "GREEN"; 27 | } 28 | 29 | @GetMapping("/javaversion") 30 | public String version() { 31 | return System.getProperty("java.version"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /__tests__/test_apps/target/hello-world.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/spring-apps-deploy/ee95b349bfb9497436d9203b722080459ee35ed6/__tests__/test_apps/target/hello-world.jar -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # azure spirng apps action 2 | name: 'Azure Spring Apps' 3 | description: 'Deploy applications to Azure Spring Apps and manage deployments.' 4 | inputs: 5 | azure-subscription: 6 | description: 'Select the Azure Resource Manager subscription for the deployment.' 7 | required: true 8 | action: 9 | description: 'Action to be performed on Azure Spring Apps.' 10 | required: true 11 | default: 'deploy' 12 | service-name: 13 | description: 'Select the Azure Spring Apps service to which to deploy.' 14 | required: true 15 | app-name: 16 | description: 'Select the Azure Spring Apps app to deploy.' 17 | required: true 18 | use-staging-deployment: 19 | description: "Automatically select the deployment that's set as Staging at the time the task runs." 20 | required: true 21 | default: true 22 | create-new-deployment: 23 | description: "Whether to target the deployment that's set as Staging at the time of execution. If unchecked, the 'Deployment Name' setting must be set." 24 | required: false 25 | default: false 26 | deployment-name: 27 | description: 'The deployment to which this task will apply. Lowercase letters, - and numbers only; must start with a letter.' 28 | required: false 29 | package: 30 | description: "File path to the package or a folder containing the Spring Apps app contents." 31 | required: false 32 | default: '${{ github.workspace }}/**/*.jar' 33 | target-module: 34 | description: "Child module to be deployed, required for multiple jar packages built from source code." 35 | required: false 36 | cpu: 37 | description: "The CPU resource quantity. It should be 500m or number of CPU cores. It is effective only when creating new deployment." 38 | required: false 39 | default: '1' 40 | memory: 41 | description: "The memory resource quantity. It should be 512Mi or #Gi, e.g., 1Gi, 3Gi. It is effective only when creating new deployment." 42 | required: false 43 | default: '1Gi' 44 | environment-variables: 45 | description: "Edit the app's environment variables." 46 | required: false 47 | jvm-options: 48 | description: "Edit the app's JVM options. A String containing JVM Options. Example: `-Xms1024m -Xmx2048m`" 49 | required: false 50 | runtime-version: 51 | description: 'The runtime on which the app will run.' 52 | required: false 53 | dotnetcore-mainentry-path: 54 | description: 'The path to the .NET executable relative to zip root.' 55 | required: false 56 | version: 57 | description: 'The runtime on which the app will run.' 58 | required: false 59 | build-name: 60 | description: '(Enterprise Tier Only) The build name.' 61 | required: false 62 | builder: 63 | description: '(Enterprise Tier Only) Build service builder used to build the executable.' 64 | required: false 65 | build-cpu: 66 | description: '(Enterprise Tier Only) CPU resource quantity for build container. Should be 500m or number of CPU cores. Default: 1' 67 | required: false 68 | build-memory: 69 | description: '(Enterprise Tier Only) Memory resource quantity for build container. Should be 512Mi or #Gi, e.g., 1Gi, 3Gi. Default: 2Gi.' 70 | required: false 71 | build-env: 72 | description: "(Enterprise Tier Only) Space-separated environment variables for the build process in 'key[=value]' format." 73 | required: false 74 | config-file-patterns: 75 | description: "(Enterprise Tier Only) Config file patterns separated with ',' to decide which patterns of Application Configuration Service will be used. Use '\"\"' to clear existing configurations." 76 | required: false 77 | container-registry: 78 | description: "The registry of the container image. Default: docker.io." 79 | required: false 80 | default: "docker.io" 81 | registry-username: 82 | description: "The username of the container registry." 83 | required: false 84 | registry-password: 85 | description: "The password of the container registry." 86 | required: false 87 | container-image: 88 | description: "The container image." 89 | required: false 90 | container-command: 91 | description: "The command of the container." 92 | required: false 93 | container-args: 94 | description: "The arguments of the container." 95 | required: false 96 | language-framework: 97 | description: "The language framework of the container." 98 | required: false 99 | enable-liveness-probe: 100 | description: "If false, will disable the liveness probe of the app instance. Allowed values: false, true." 101 | required: false 102 | enable-readiness-probe: 103 | description: "If false, will disable the readiness probe of the app instance. Allowed values: false, true." 104 | required: false 105 | enable-startup-probe: 106 | description: "If false, will disable the startup probe of the app instance. Allowed values: false, true." 107 | required: false 108 | termination-grace-period-seconds: 109 | description: "Optional duration in seconds the app instance needs to terminate gracefully." 110 | required: false 111 | liveness-probe-config: 112 | description: "A json file path indicates the liveness probe config" 113 | required: false 114 | readiness-probe-config: 115 | description: "A json file path indicates the readiness probe config" 116 | required: false 117 | startup-probe-config: 118 | description: "A json file path indicates the startup probe config" 119 | required: false 120 | 121 | 122 | branding: 123 | icon: 'icon.svg' 124 | runs: 125 | using: 'node12' 126 | main: 'lib/main.js' 127 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 75 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest' 8 | }, 9 | verbose: true 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-apps-deploy", 3 | "version": "1.0.0", 4 | "description": "spring-apps-deploy", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/RuoyuWang-MS/spring-apps-deploy.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/RuoyuWang-MS/spring-apps-deploy/issues" 19 | }, 20 | "homepage": "https://github.com/RuoyuWang-MS/spring-apps-deploy#readme", 21 | "devDependencies": { 22 | "@types/jest": "^27.0.2", 23 | "@types/node": "^16.11.6", 24 | "jest": "^27.3.1", 25 | "ts-jest": "^27.0.7", 26 | "ts-node": "^10.4.0", 27 | "typescript": "^4.4.4" 28 | }, 29 | "dependencies": { 30 | "@actions/core": "^1.6.0", 31 | "@actions/github": "^5.0.0", 32 | "@azure/arm-appplatform": "^3.0.0-beta.1", 33 | "@azure/identity": "^2.1.0", 34 | "@azure/storage-file-share": "^12.8.0", 35 | "azure-actions-utility": "^1.0.3", 36 | "node-fetch": "^2.6.7", 37 | "tar": "^6.1.11", 38 | "uuid": "^8.3.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DeploymentProvider/AzureSpringAppsDeploymentProvider.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { Package, PackageType } from 'azure-actions-utility/packageUtility'; 4 | import { Actions, ActionParameters, ActionParametersUtility } from '../operations/ActionParameters'; 5 | import * as asa from '@azure/arm-appplatform' 6 | import { getDefaultAzureCredential } from '@azure/identity' 7 | import { DeploymentHelper as dh } from "./DeploymentHelper"; 8 | import * as tar from 'tar'; 9 | 10 | export class AzureSpringAppsDeploymentProvider { 11 | 12 | defaultInactiveDeploymentName = 'staging'; 13 | 14 | params: ActionParameters; 15 | client: asa.AppPlatformManagementClient; 16 | logDetail: string; 17 | tier: string; 18 | resourceId: string; 19 | 20 | constructor() { 21 | this.params = ActionParametersUtility.getParameters(); 22 | } 23 | 24 | public async preDeploymentStep() { 25 | const token = getDefaultAzureCredential(); 26 | const option: asa.AppPlatformManagementClientOptionalParams = { 27 | userAgentOptions : { 28 | userAgentPrefix : 'GitHub Action / spring-apps-deploy' 29 | } 30 | } 31 | this.client = new asa.AppPlatformManagementClient(token, this.params.azureSubscription, option); 32 | const serviceList = await this.client.services.listBySubscription(); 33 | let filteredResources: Array = []; 34 | for await (const service of serviceList) { 35 | if (service.name == this.params.serviceName) { 36 | filteredResources.push(service); 37 | } 38 | } 39 | if (!filteredResources || filteredResources.length == 0) { 40 | throw new Error('ResourceDoesntExist: ' + this.params.serviceName); 41 | } else if (filteredResources.length == 1) { 42 | const reg = new RegExp('(?<=/resourceGroups/).*?(?=/providers/Microsoft.AppPlatform/Spring/)', 'i') 43 | const match = filteredResources[0].id.match(reg); 44 | if (!match || match.length != 1) { 45 | throw new Error('ResourceGroupNameParseErrorWithId:' + filteredResources[0].id); 46 | } 47 | this.params.resourceGroupName = match[0]; 48 | console.log('service resource group name: ' + this.params.resourceGroupName); 49 | } else { //Should never happen 50 | throw new Error('DuplicateAzureSpringAppsName: ' + this.params.serviceName); 51 | } 52 | const serviceResponse = await this.client.services.get(this.params.resourceGroupName, this.params.serviceName); 53 | core.debug("service response: " + JSON.stringify(serviceResponse)); 54 | this.logDetail = `service ${this.params.serviceName} app ${this.params.appName}`; 55 | this.tier = serviceResponse.sku.tier; 56 | this.resourceId = serviceResponse.id; 57 | } 58 | 59 | public async deployAppStep() { 60 | switch (this.params.action) { 61 | 62 | case Actions.DEPLOY: { 63 | await this.performDeployAction(); 64 | break; 65 | } 66 | 67 | case Actions.SET_PRODUCTION: { 68 | await this.performSetProductionAction(); 69 | break; 70 | } 71 | 72 | case Actions.DELETE_STAGING_DEPLOYMENT: { 73 | await this.performDeleteStagingDeploymentAction(); 74 | break; 75 | } 76 | 77 | case Actions.BUILD: { 78 | await this.performBuildAction(); 79 | break; 80 | } 81 | 82 | case Actions.DELETE_BUILD: { 83 | await this.performDeleteBuildAction(); 84 | break; 85 | } 86 | 87 | default: 88 | throw Error('UnknownOrUnsupportedAction: ' + this.params.action); 89 | } 90 | } 91 | 92 | private async performDeleteStagingDeploymentAction() { 93 | let deploymentName = this.params.deploymentName; 94 | if (!deploymentName) { 95 | deploymentName = await dh.getStagingDeploymentName(this.client, this.params); 96 | this.params.deploymentName = deploymentName; 97 | } 98 | if (deploymentName) { 99 | console.log(`Delete staging deployment action for ${this.logDetail} to deployment ${deploymentName}.`); 100 | await dh.deleteDeployment(this.client, this.params); 101 | } else { 102 | throw Error(`No staging deployment in ${this.logDetail}`); 103 | } 104 | console.log(`Delete staging deployment action successful for ${this.logDetail} to deployment ${deploymentName}..`); 105 | return deploymentName; 106 | } 107 | 108 | private async performSetProductionAction() { 109 | let deploymentName: string; 110 | if (this.params.deploymentName) { 111 | console.log(`Set production action for ${this.logDetail} to the specific deployment ${this.params.deploymentName}`); 112 | deploymentName = this.params.deploymentName; 113 | let existingStagingDeploymentNames: Array = await dh.getStagingDeploymentNames(this.client, this.params); 114 | if (!existingStagingDeploymentNames.includes(deploymentName)) { 115 | throw Error(`Staging deployment ${deploymentName} not exist in ${this.logDetail}.`); 116 | } 117 | await dh.setActiveDeployment(this.client, this.params); 118 | } else if (this.params.useStagingDeployment) { 119 | console.log(`Set production deployment for ${this.logDetail} to the current inactive deployment.`); 120 | deploymentName = await dh.getStagingDeploymentName(this.client, this.params); 121 | this.params.deploymentName = deploymentName; 122 | if (!deploymentName) { //If no inactive deployment exists, we cannot continue as instructed. 123 | throw Error(`No staging deployment in ${this.logDetail}`); 124 | } 125 | await dh.setActiveDeployment(this.client, this.params); 126 | } else { 127 | throw Error(`Set production deployment action should use-staging-deployment or specify deployment-name`); 128 | } 129 | 130 | console.log(`Set production action successful for ${this.logDetail} to deployment ${deploymentName}.`); 131 | } 132 | 133 | private async performDeployAction() { 134 | let sourceType: string = this.determineSourceType(this.params.package); 135 | //If uploading a source folder, compress to tar.gz file. 136 | let fileToUpload: string 137 | if (sourceType != SourceType.CUSTOM_CONTAINER) { 138 | fileToUpload = sourceType == SourceType.SOURCE_DIRECTORY 139 | ? await this.compressSourceDirectory(this.params.package.getPath()) 140 | : this.params.package.getPath(); 141 | } 142 | let deploymentName: string; 143 | 144 | if (this.params.deploymentName) { 145 | console.log(`Deploying for ${this.logDetail} to deployment ${this.params.deploymentName}.`); 146 | deploymentName = this.params.deploymentName; 147 | let deploymentNames: Array = await dh.getAllDeploymentsName(this.client, this.params); 148 | if (!deploymentNames || !deploymentNames.includes(deploymentName)) { 149 | console.log(`Deployment ${deploymentName} does not exist`); 150 | if (this.params.createNewDeployment) { 151 | if (deploymentNames.length > 1) { 152 | throw Error(`More than 1 deployments already exist in ${this.logDetail}: ${JSON.stringify(deploymentNames)}`); 153 | } else { 154 | console.log(`New Deployment will be created for ${this.logDetail}.`); 155 | } 156 | } else { 157 | throw Error(`Deployment ${deploymentName} doesn\'t exist in ${this.logDetail}`); 158 | } 159 | } 160 | } else if (this.params.useStagingDeployment) { 161 | console.log(`Deploying to the staging deployment of ${this.logDetail}.`); 162 | deploymentName = await dh.getStagingDeploymentName(this.client, this.params); 163 | if (!deploymentName) { //If no inactive deployment exists 164 | console.log(`No staging deployment was found in ${this.logDetail}.`); 165 | if (this.params.createNewDeployment) { 166 | console.log(`New deployment ${this.defaultInactiveDeploymentName} will be created in ${this.logDetail}`); 167 | deploymentName = this.defaultInactiveDeploymentName; //Create a new deployment with the default name. 168 | this.params.deploymentName = deploymentName; 169 | } else 170 | throw Error(`No staging deployment in ${this.logDetail}`); 171 | } 172 | } else { 173 | console.log(`Deploying to the production deployment of ${this.logDetail}.`); 174 | deploymentName = await dh.getProductionDeploymentName(this.client, this.params); 175 | this.params.deploymentName = deploymentName; 176 | if(!deploymentName) { 177 | throw Error(`Production deployment does not exist in ${this.logDetail}.`); 178 | } 179 | } 180 | if (sourceType == SourceType.CUSTOM_CONTAINER) { 181 | await dh.deployCustomContainer(this.client, this.params, sourceType); 182 | } 183 | else if (this.tier == "Standard" || this.tier == "Basic") { 184 | await dh.deploy(this.client, this.params, sourceType, fileToUpload); 185 | } else if (this.tier == "Enterprise") { 186 | await dh.deployEnterprise(this.client, this.params, "BuildResult", fileToUpload, this.resourceId); 187 | } else { 188 | throw Error(`Service tier not recognizable in ${this.logDetail}.`); 189 | } 190 | console.log(`Deploy action successful for ${this.logDetail} to deployment ${deploymentName}.`); 191 | 192 | } 193 | 194 | private async performBuildAction() { 195 | if (this.tier != "Enterprise") { 196 | throw Error(`Build action is only supported in Enterprise tier.`); 197 | } 198 | let sourceType: string = this.determineSourceType(this.params.package); 199 | //If uploading a source folder, compress to tar.gz file. 200 | let fileToUpload: string = sourceType == SourceType.SOURCE_DIRECTORY 201 | ? await this.compressSourceDirectory(this.params.package.getPath()) 202 | : this.params.package.getPath(); 203 | await dh.build(this.client, this.params, fileToUpload, this.resourceId); 204 | console.log(`Build action successful for service ${this.params.serviceName} build ${this.params.buildName}.`); 205 | } 206 | 207 | private async performDeleteBuildAction() { 208 | if (this.tier != "Enterprise") { 209 | throw Error(`Delete build action is only supported in Enterprise tier.`); 210 | } 211 | await dh.deleteBuild(this.client, this.params); 212 | console.log(`Delete build action successful for service ${this.params.serviceName} build ${this.params.buildName}.`); 213 | } 214 | 215 | /** 216 | * Compresses sourceDirectoryPath into a tar.gz 217 | * @param sourceDirectoryPath 218 | */ 219 | //todo pack source code ignore some files 220 | async compressSourceDirectory(sourceDirectoryPath: string): Promise { 221 | const fileName = `${uuidv4()}.tar.gz`; 222 | core.debug(`CompressingSourceDirectory ${sourceDirectoryPath} ${fileName}`); 223 | await tar.c({ 224 | gzip: true, 225 | file: fileName, 226 | sync: true, 227 | cwd: sourceDirectoryPath, 228 | onWarn: warning => { 229 | core.warning(warning); 230 | } 231 | }, ['.']); 232 | return fileName; 233 | } 234 | 235 | private determineSourceType(pkg: Package): string { 236 | if (this.params.containerImage && this.params.containerRegistry) { 237 | return SourceType.CUSTOM_CONTAINER; 238 | } 239 | var sourceType: string; 240 | switch (pkg.getPackageType()) { 241 | case PackageType.folder: 242 | sourceType = SourceType.SOURCE_DIRECTORY; 243 | break; 244 | case PackageType.zip: 245 | sourceType = SourceType.DOT_NET_CORE_ZIP; 246 | break; 247 | case PackageType.jar: 248 | sourceType = SourceType.JAR; 249 | break; 250 | default: 251 | throw Error('UnsupportedSourceType: ' + pkg.getPath()); 252 | } 253 | return sourceType; 254 | } 255 | } 256 | 257 | export const SourceType = { 258 | JAR: "Jar", 259 | SOURCE_DIRECTORY: "Source", 260 | DOT_NET_CORE_ZIP: "NetCoreZip", 261 | BUILD_RESULT: "BuildResult", 262 | CUSTOM_CONTAINER: "Container" 263 | } 264 | -------------------------------------------------------------------------------- /src/DeploymentProvider/DeploymentHelper.ts: -------------------------------------------------------------------------------- 1 | import { Actions, ActionParameters } from '../operations/ActionParameters'; 2 | import * as asa from '@azure/arm-appplatform' 3 | import { uploadFileToSasUrl } from "./azure-storage"; 4 | import * as core from "@actions/core"; 5 | import { parse } from 'azure-actions-utility/parameterParserUtility'; 6 | import {SourceType} from "./AzureSpringAppsDeploymentProvider"; 7 | import fetch from 'node-fetch'; 8 | import * as fs from 'fs'; 9 | 10 | export class DeploymentHelper { 11 | 12 | private static listDeploymentsResult: Array = []; 13 | private static readonly buildServiceName = "default"; 14 | 15 | private static async listDeployments(client: asa.AppPlatformManagementClient, params: ActionParameters): Promise> { 16 | if (this.listDeploymentsResult.length > 0) { 17 | core.debug('list from cache, list deployments response: ' + JSON.stringify(this.listDeploymentsResult)); 18 | return this.listDeploymentsResult; 19 | } 20 | const deployments = await client.deployments.list(params.resourceGroupName, params.serviceName, params.appName); 21 | for await (const deployment of deployments) { 22 | this.listDeploymentsResult.push(deployment); 23 | } 24 | core.debug('list deployments response: ' + JSON.stringify(this.listDeploymentsResult)); 25 | return this.listDeploymentsResult; 26 | } 27 | 28 | private static async getDeployment(client: asa.AppPlatformManagementClient, params: ActionParameters, deploymentName: string): Promise { 29 | if (this.listDeploymentsResult.length > 0) { 30 | core.debug('get from list cache, list deployments response: ' + JSON.stringify(this.listDeploymentsResult)); 31 | let ret: asa.DeploymentResource; 32 | this.listDeploymentsResult.forEach(deployment => { 33 | core.debug('deployment str: ' + JSON.stringify(deployment)); 34 | if (deployment.name == deploymentName) { 35 | ret = deployment; 36 | } 37 | }); 38 | return ret; 39 | } 40 | const getResponse: asa.DeploymentsGetResponse = await client.deployments.get(params.resourceGroupName, params.serviceName, params.appName, deploymentName); 41 | return getResponse; 42 | } 43 | 44 | public static async getStagingDeploymentNames(client: asa.AppPlatformManagementClient, params: ActionParameters): Promise> { 45 | const deployments = await this.listDeployments(client, params); 46 | let ret: Array = []; 47 | deployments.forEach(deployment => { 48 | core.debug('deployment str: ' + JSON.stringify(deployment)); 49 | if (deployment.properties.active == false) { 50 | core.debug("inactive deployment name: " + deployment.name); 51 | ret.push(deployment.name); 52 | } else { 53 | core.debug("active deployment name: " + deployment.name); 54 | } 55 | }); 56 | return ret; 57 | } 58 | 59 | public static async getStagingDeploymentName(client: asa.AppPlatformManagementClient, params: ActionParameters): Promise { 60 | let deploymentNames: Array = await this.getStagingDeploymentNames(client, params); 61 | if (deploymentNames.length >= 2) { 62 | throw Error('More than 1 staging deployments were found: ' + JSON.stringify(deploymentNames)); 63 | } else if (deploymentNames.length == 0) { 64 | return null; 65 | } 66 | return deploymentNames[0]; 67 | } 68 | 69 | public static async getProductionDeploymentName(client: asa.AppPlatformManagementClient, params: ActionParameters): Promise { 70 | const deployments = await this.listDeployments(client, params); 71 | let ret: Array = []; 72 | deployments.forEach(deployment => { 73 | if (deployment.properties.active) { 74 | ret.push(deployment.name); 75 | } 76 | }); 77 | if (ret.length >= 2) { 78 | throw Error('More than 1 production deployments were found: ' + JSON.stringify(ret)); 79 | } else if (ret.length == 0) { 80 | return null; 81 | } 82 | return ret[0]; 83 | } 84 | 85 | public static async getAllDeploymentsName(client: asa.AppPlatformManagementClient, params: ActionParameters): Promise> { 86 | let names: Array = []; 87 | const deployments = await this.listDeployments(client, params); 88 | deployments.forEach(deployment => { 89 | names.push(deployment.name); 90 | }); 91 | return names; 92 | } 93 | 94 | public static async setActiveDeployment(client: asa.AppPlatformManagementClient, params: ActionParameters) { 95 | let activeDeploymentCollection: asa.ActiveDeploymentCollection = { 96 | activeDeploymentNames: [params.deploymentName] 97 | } 98 | const setActiveResponse: asa.AppsUpdateResponse = await client.apps.beginSetActiveDeploymentsAndWait(params.resourceGroupName, params.serviceName, params.appName, activeDeploymentCollection); 99 | core.debug('set active deployment response: ' + JSON.stringify(setActiveResponse)); 100 | return; 101 | } 102 | 103 | public static async deploy(client: asa.AppPlatformManagementClient, params: ActionParameters, sourceType: string, fileToUpload: string) { 104 | let uploadResponse: asa.AppsGetResourceUploadUrlResponse = await client.apps.getResourceUploadUrl(params.resourceGroupName, params.serviceName, params.appName); 105 | core.debug('request upload url response: ' + JSON.stringify(uploadResponse)); 106 | await uploadFileToSasUrl(uploadResponse.uploadUrl, fileToUpload); 107 | let deploymentResource: asa.DeploymentResource = await this.buildDeploymentResource(client, params, sourceType, uploadResponse.relativePath); 108 | core.debug("deploymentResource: " + JSON.stringify(deploymentResource)); 109 | await this.deployWithLog(client, params, deploymentResource); 110 | return; 111 | } 112 | 113 | public static async deployCustomContainer(client: asa.AppPlatformManagementClient, params: ActionParameters, sourceType: string) { 114 | let deploymentResource: asa.DeploymentResource = await this.buildDeploymentResource(client, params, sourceType, null); 115 | core.debug("custom container deploymentResource: " + JSON.stringify(deploymentResource)); 116 | await this.deployWithLog(client, params, deploymentResource); 117 | return; 118 | } 119 | 120 | public static async deployEnterprise(client: asa.AppPlatformManagementClient, params: ActionParameters, sourceType: string, fileToUpload: string, resourceId: string) { 121 | const buildResponse = await this.buildAndGetResult(client, params, fileToUpload, resourceId); 122 | let deploymentResource: asa.DeploymentResource = await this.buildDeploymentResource(client, params, sourceType, buildResponse.properties.triggeredBuildResult.id); 123 | core.debug("deploymentResource: " + JSON.stringify(deploymentResource)); 124 | await this.deployWithLog(client, params, deploymentResource); 125 | return; 126 | } 127 | 128 | public static async deleteDeployment(client: asa.AppPlatformManagementClient, params: ActionParameters) { 129 | const response = await client.deployments.beginDeleteAndWait(params.resourceGroupName, params.serviceName, params.appName, params.deploymentName); 130 | core.debug('delete deployment response: ' + JSON.stringify(response)); 131 | return; 132 | } 133 | 134 | public static async build(client: asa.AppPlatformManagementClient, params: ActionParameters, fileToUpload: string, resourceId: string) { 135 | const response = await this.buildAndGetResult(client, params, fileToUpload, resourceId); 136 | core.debug('build response: ' + JSON.stringify(response)); 137 | return; 138 | } 139 | 140 | public static async deleteBuild(client: asa.AppPlatformManagementClient, params: ActionParameters) { 141 | const response = await client.buildServiceOperations.beginDeleteBuildAndWait(params.resourceGroupName, params.serviceName, this.buildServiceName, params.buildName); 142 | core.debug('delete build response: ' + JSON.stringify(response)); 143 | return; 144 | } 145 | 146 | protected static async buildDeploymentResource(client: asa.AppPlatformManagementClient, params: ActionParameters, sourceType: string, idOrPath: string): Promise { 147 | let getDeploymentName = params.deploymentName; 148 | if (params.createNewDeployment) { 149 | getDeploymentName = await this.getProductionDeploymentName(client, params); 150 | } 151 | let getResponse: asa.DeploymentResource; 152 | if (getDeploymentName) { 153 | getResponse = await this.getDeployment(client, params, getDeploymentName); 154 | } 155 | let deploymentResource: asa.DeploymentResource; 156 | let sourcePart: {}; 157 | if (sourceType == SourceType.CUSTOM_CONTAINER) { 158 | sourcePart = { 159 | type: SourceType.CUSTOM_CONTAINER 160 | } 161 | let customContainer: asa.CustomContainer = {}; 162 | let imageRegistryCredential: asa.ImageRegistryCredential = {}; 163 | if (params.containerRegistry) { 164 | customContainer.server = params.containerRegistry; 165 | } 166 | if (params.registryUsername || params.registryPassword) { 167 | imageRegistryCredential.username = params.registryUsername; 168 | imageRegistryCredential.password = params.registryPassword; 169 | customContainer.imageRegistryCredential = imageRegistryCredential; 170 | } 171 | if (params.containerImage) { 172 | customContainer.containerImage = params.containerImage; 173 | } 174 | if (params.containerCommand) { 175 | customContainer.command = params.containerCommand.split(' '); 176 | } 177 | if (params.containerArgs) { 178 | customContainer.args = params.containerArgs.split(' '); 179 | } 180 | if (params.languageFramework) { 181 | customContainer.languageFramework = params.languageFramework; 182 | } 183 | sourcePart["customContainer"] = customContainer; 184 | } 185 | else if (sourceType == SourceType.BUILD_RESULT) { 186 | sourcePart = { 187 | buildResultId: idOrPath, 188 | type: SourceType.BUILD_RESULT 189 | } 190 | } else { 191 | sourcePart = { 192 | relativePath: idOrPath, 193 | type: sourceType, 194 | } 195 | } 196 | if(params.version) { 197 | sourcePart["version"] = params.version; 198 | } 199 | let deploymentSettingsPart = {}; 200 | let resourceRequests: asa.ResourceRequests = {}; 201 | if (params.action == Actions.DEPLOY && params.createNewDeployment) { 202 | resourceRequests.cpu = params.cpu; 203 | resourceRequests.memory = params.memory; 204 | deploymentSettingsPart["resourceRequests"] = resourceRequests; 205 | } 206 | if (params.jvmOptions) { 207 | sourcePart["jvmOptions"] = params.jvmOptions; 208 | } 209 | if (params.dotNetCoreMainEntryPath) { 210 | deploymentSettingsPart["netCoreMainEntryPath"] = params.dotNetCoreMainEntryPath; 211 | } 212 | if (params.targetModule) { 213 | sourcePart["artifactSelector"] = params.targetModule; 214 | } 215 | if (params.runtimeVersion) { 216 | sourcePart["runtimeVersion"] = params.runtimeVersion; 217 | } 218 | if (params.configFilePatterns) { 219 | deploymentSettingsPart["addonConfigs"] = { 220 | applicationConfigurationService: { 221 | configFilePatterns: params.configFilePatterns 222 | } 223 | } 224 | } 225 | let transformedEnvironmentVariables = {}; 226 | if (params.environmentVariables) { 227 | core.debug("Environment variables modified."); 228 | const parsedEnvVariables = parse(params.environmentVariables); 229 | //Parsed pairs come back as {"key1":{"value":"val1"},"key2":{"value":"val2"}} 230 | Object.keys(parsedEnvVariables).forEach(key => { 231 | transformedEnvironmentVariables[key] = parsedEnvVariables[key]['value']; 232 | }); 233 | core.debug('Environment Variables: ' + JSON.stringify(transformedEnvironmentVariables)); 234 | deploymentSettingsPart["environmentVariables"] = transformedEnvironmentVariables; 235 | } 236 | const disableProbe: asa.Probe = { 237 | disableProbe: true 238 | } 239 | if (params.enableLivenessProbe.length > 0) { 240 | if (params.enableLivenessProbe.toLowerCase() == "true") { 241 | deploymentSettingsPart["livenessProbe"] = this.loadProbeConfig(params.livenessProbeConfig); 242 | } else if (params.enableLivenessProbe.toLowerCase() == "false") { 243 | deploymentSettingsPart["livenessProbe"] = disableProbe; 244 | } else { 245 | throw new Error("Invalid value for enableLivenessProbe. Please provide true/false"); 246 | } 247 | } 248 | if (params.enableReadinessProbe.length > 0) { 249 | if (params.enableReadinessProbe.toLowerCase() == "true") { 250 | deploymentSettingsPart["readinessProbe"] = this.loadProbeConfig(params.readinessProbeConfig); 251 | } else if (params.enableReadinessProbe.toLowerCase() == "false") { 252 | deploymentSettingsPart["readinessProbe"] = disableProbe; 253 | } else { 254 | throw new Error("Invalid value for enableReadinessProbe. Please provide true/false"); 255 | } 256 | } 257 | if (params.enableStartupProbe.length > 0) { 258 | if (params.enableStartupProbe.toLowerCase() == "true") { 259 | deploymentSettingsPart["startupProbe"] = this.loadProbeConfig(params.startupProbeConfig); 260 | } else if (params.enableStartupProbe.toLowerCase() == "false") { 261 | deploymentSettingsPart["startupProbe"] = disableProbe; 262 | } else { 263 | throw new Error("Invalid value for enableStartupProbe. Please provide true/false"); 264 | } 265 | } 266 | if (params.terminationGracePeriodSeconds) { 267 | deploymentSettingsPart["terminationGracePeriodSeconds"] = params.terminationGracePeriodSeconds; 268 | } 269 | if (getResponse) { 270 | let source = {...getResponse.properties.source, ...sourcePart}; 271 | let deploymentSettings = {...getResponse.properties.deploymentSettings, ...deploymentSettingsPart}; 272 | deploymentResource = { 273 | properties: { 274 | source: source as asa.UserSourceInfoUnion, 275 | deploymentSettings: deploymentSettings 276 | }, 277 | sku: getResponse.sku 278 | }; 279 | } else { 280 | deploymentResource = { 281 | properties: { 282 | source: sourcePart as asa.UserSourceInfoUnion, 283 | deploymentSettings: deploymentSettingsPart 284 | } 285 | }; 286 | } 287 | return deploymentResource; 288 | } 289 | 290 | public static async buildAndGetResult(client: asa.AppPlatformManagementClient, params: ActionParameters, fileToUpload: string, resourceId: string): Promise { 291 | const buildName = params.action == Actions.BUILD ? params.buildName : `${params.appName}-${params.deploymentName}`; 292 | const uploadResponse = await client.buildServiceOperations.getResourceUploadUrl(params.resourceGroupName, params.serviceName, this.buildServiceName); 293 | core.debug('request upload url response: ' + JSON.stringify(uploadResponse)); 294 | await uploadFileToSasUrl(uploadResponse.uploadUrl, fileToUpload); 295 | const build: asa.Build = { 296 | properties: { 297 | relativePath: uploadResponse.relativePath, 298 | builder: params.builder ? `${resourceId}/buildServices/${this.buildServiceName}/builders/${params.builder}` : `${resourceId}/buildServices/${this.buildServiceName}/builders/default`, 299 | agentPool: `${resourceId}/buildServices/${this.buildServiceName}/agentPools/default`, 300 | } 301 | }; 302 | if (params.buildCpu) { 303 | build.properties.resourceRequests.cpu = params.buildCpu; 304 | } 305 | if (params.buildMemory) { 306 | build.properties.resourceRequests.memory = params.buildMemory; 307 | } 308 | let transformedBuildEnvironmentVariables = {}; 309 | if (params.buildEnv) { 310 | core.debug("Build environment variables modified."); 311 | const parsedBuildEnvVariables = parse(params.buildEnv); 312 | //Parsed pairs come back as {"key1":{"value":"val1"},"key2":{"value":"val2"}} 313 | Object.keys(parsedBuildEnvVariables).forEach(key => { 314 | transformedBuildEnvironmentVariables[key] = parsedBuildEnvVariables[key]['value']; 315 | }); 316 | build.properties.env = transformedBuildEnvironmentVariables; 317 | } 318 | core.debug('build: ' + JSON.stringify(build)); 319 | const buildResponse = await client.buildServiceOperations.createOrUpdateBuild(params.resourceGroupName, params.serviceName, this.buildServiceName, buildName, build); 320 | core.debug('build response: ' + JSON.stringify(buildResponse)); 321 | const regex = RegExp("[^/]+$"); 322 | const buildResultName = regex.exec(buildResponse.properties.triggeredBuildResult.id)[0]; 323 | let buildProvisioningState = 'Queuing'; 324 | let cnt = 0; 325 | core.debug("wait for build result......"); 326 | //Waiting for build result. Timeout 30 minutes. 327 | const logStream = await this.logStreamConstructor(client, params); 328 | const stagesRecorded = new Set(); 329 | while (buildProvisioningState != 'Succeeded' && buildProvisioningState != 'Failed' && cnt++ < 180) { 330 | await new Promise(resolve => setTimeout(resolve, 10000)); 331 | core.debug("wait for 10 seconds...."); 332 | try { 333 | const waitResponse = await client.buildServiceOperations.getBuildResult(params.resourceGroupName, params.serviceName, this.buildServiceName, buildName, buildResultName); 334 | waitResponse.properties.buildStages.forEach(async stage => { 335 | if (!stagesRecorded.has(stage.name) && stage.status != "NotStarted") { 336 | const url=`https://${logStream["baseUrl"]}/api/logstream/buildpods/${waitResponse.properties.buildPodName}/stages/${stage.name}?follow=true` 337 | const credentials = Buffer.from(`primary:${logStream["primaryKey"]}`).toString('base64'); 338 | const auth = { "Authorization" : `Basic ${credentials}` }; 339 | const response = await fetch(url, {method: 'GET', headers : auth }); 340 | response.body.pipe(process.stdout); 341 | stagesRecorded.add(stage.name); 342 | } 343 | }); 344 | core.debug('build result response: ' + JSON.stringify(waitResponse)); 345 | buildProvisioningState = waitResponse.properties.provisioningState; 346 | } 347 | catch (e:any) { 348 | console.log(e.message); 349 | } 350 | } 351 | if (cnt == 180) { 352 | throw Error("Build result timeout."); 353 | } 354 | if (buildProvisioningState != 'Succeeded') { 355 | throw Error("Build result failed."); 356 | } 357 | return buildResponse; 358 | } 359 | 360 | public static async logStreamConstructor(client: asa.AppPlatformManagementClient, params: ActionParameters) { 361 | const test_keys = await client.services.listTestKeys(params.resourceGroupName, params.serviceName); 362 | let ret = {}; 363 | ret["primaryKey"] = test_keys.primaryKey; 364 | const serviceResponse = await client.services.get(params.resourceGroupName, params.serviceName); 365 | ret["baseUrl"] = serviceResponse.properties.fqdn; 366 | return ret; 367 | } 368 | 369 | private static loadProbeConfig(probeConfig: string): asa.Probe { 370 | if (!probeConfig) { 371 | return null; 372 | } 373 | const data = this.readJsonFile(probeConfig); 374 | if (!data) { 375 | return null; 376 | } 377 | if (!data.probe) { 378 | throw new Error(`Probe must be provided in the json file ${probeConfig}`); 379 | } 380 | 381 | if (!data.probe.probeAction || !data.probe.probeAction.type) { 382 | throw new Error(`ProbeAction, Type mast be provided in the json file ${probeConfig}`); 383 | } 384 | 385 | let probeAction : asa.ProbeActionUnion = null; 386 | if (data.probe.probeAction.type.toLowerCase() === 'httpgetaction') { 387 | probeAction = { 388 | type: 'HTTPGetAction', 389 | path: data.probe.probeAction.path, 390 | scheme: data.probe.probeAction.scheme, 391 | }; 392 | } else if (data.probe.probeAction.type.toLowerCase() === 'tcpsocketaction') { 393 | probeAction = { 394 | type: 'TCPSocketAction', 395 | }; 396 | } else if (data.probe.probeAction.type.toLowerCase() === 'execaction') { 397 | probeAction = { 398 | type: 'ExecAction', 399 | command: data.probe.probeAction.command, 400 | }; 401 | } else { 402 | throw new Error(`ProbeAction.Type is invalid in the json file ${probeConfig}`); 403 | } 404 | 405 | const probeSettings : asa.Probe = { 406 | probeAction: probeAction, 407 | disableProbe: false, 408 | initialDelaySeconds: data.probe.initialDelaySeconds, 409 | periodSeconds: data.probe.periodSeconds, 410 | timeoutSeconds: data.probe.timeoutSeconds, 411 | failureThreshold: data.probe.failureThreshold, 412 | successThreshold: data.probe.successThreshold 413 | }; 414 | return probeSettings; 415 | } 416 | 417 | private static readJsonFile(file: string): any { 418 | const data = fs.readFileSync(file); 419 | return JSON.parse(data.toString()); 420 | } 421 | 422 | private static async printLatestAppInstanceLog(client: asa.AppPlatformManagementClient, params: ActionParameters) { 423 | const logStream = await this.logStreamConstructor(client, params); 424 | const deploymentResource = await client.deployments.get(params.resourceGroupName, params.serviceName, params.appName, params.deploymentName); 425 | const instances = deploymentResource.properties.instances; 426 | let startTime = instances[0].startTime; 427 | let instanceName = instances[0].name; 428 | 429 | // print the newly created instance log 430 | for (const tempInstance of instances) { 431 | if (tempInstance.startTime > startTime) { 432 | startTime = tempInstance.startTime; 433 | instanceName = tempInstance.name; 434 | } 435 | } 436 | let streamingUrl = `https://${logStream["baseUrl"]}/api/logstream/apps/${params.appName}/instances/${instanceName}`; 437 | const logParams: any = {}; 438 | logParams['tailLines'] = 500; 439 | logParams['limitBytes'] = 1024 * 1024; 440 | logParams['sinceSeconds'] = 300; 441 | logParams['follow'] = true; 442 | const credentials = Buffer.from(`primary:${logStream["primaryKey"]}`).toString('base64'); 443 | const auth = { "Authorization" : `Basic ${credentials}` }; 444 | var url = new URL(streamingUrl) 445 | url.search = new URLSearchParams(logParams).toString(); 446 | fetch(url, {method: 'GET', headers : auth }) 447 | .then(response => { 448 | if (response.ok) { 449 | const stream = response.body; 450 | // Pipe the stream to stderr 451 | stream.pipe(process.stderr); 452 | } else { 453 | // Handle error response 454 | console.error('Error:', response.status, response.statusText); 455 | } 456 | }) 457 | .catch(error => { 458 | // Handle network error 459 | console.error('Error:', error.message); 460 | });; 461 | } 462 | 463 | private static async deployWithLog(client: asa.AppPlatformManagementClient, params: ActionParameters, deploymentResource: asa.DeploymentResource) { 464 | try { 465 | const response = await client.deployments.beginCreateOrUpdateAndWait(params.resourceGroupName, params.serviceName, params.appName, params.deploymentName, deploymentResource); 466 | core.debug('deploy response: ' + JSON.stringify(response)); 467 | } catch (e:any) { 468 | core.warning("Deployment failed. Please check the application logs for more details."); 469 | await this.printLatestAppInstanceLog(client, params); 470 | throw e; 471 | } 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /src/DeploymentProvider/azure-storage.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as core from '@actions/core'; 3 | import { ShareFileClient, AnonymousCredential } from '@azure/storage-file-share'; 4 | 5 | export async function uploadFileToSasUrl(uploadUrl: string, localPath: string) { 6 | core.debug('uploading file to URL: ' + uploadUrl); 7 | const shareFileClient = new ShareFileClient(uploadUrl, new AnonymousCredential()); 8 | try { 9 | console.info('StartingUploadOf' + localPath); 10 | await shareFileClient.uploadFile(localPath, { 11 | onProgress: (ev) => console.log(ev) 12 | }); 13 | console.info('CompletedUploadOf' + localPath); 14 | } catch (err) { 15 | core.debug(err); 16 | throw Error('UploadFileError'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import {AzureSpringAppsDeploymentProvider} from "./DeploymentProvider/AzureSpringAppsDeploymentProvider"; 3 | 4 | export async function main() { 5 | 6 | try { 7 | core.debug('Starting deployment task execution'); 8 | let deploymentProvider = new AzureSpringAppsDeploymentProvider(); 9 | core.debug("Pre-deployment Step Started"); 10 | await deploymentProvider.preDeploymentStep(); 11 | core.debug("Deployment Step Started"); 12 | await deploymentProvider.deployAppStep(); 13 | } 14 | catch (error) { 15 | core.setFailed("Action failed with error: " + error.message); 16 | } 17 | } 18 | 19 | main(); 20 | 21 | -------------------------------------------------------------------------------- /src/operations/ActionParameters.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { Package, PackageType } from 'azure-actions-utility/packageUtility'; 3 | 4 | export class Inputs { 5 | public static readonly AZURE_SUBSCRIPTION = 'azure-subscription'; 6 | public static readonly RESOURCE_GROUP_NAME = 'resource-group-name'; 7 | public static readonly SERVICE_NAME = 'service-name'; 8 | public static readonly ACTION = 'action'; 9 | public static readonly APP_NAME = 'app-name'; 10 | public static readonly USE_STAGING_DEPLOYMENT = 'use-staging-deployment'; 11 | public static readonly CREATE_NEW_DEPLOYMENT = 'create-new-deployment'; 12 | public static readonly DEPLOYMENT_NAME = 'deployment-name'; 13 | public static readonly ENVIRONMENT_VARIABLES = 'environment-variables'; 14 | public static readonly JVM_OPTIONS = 'jvm-options' 15 | public static readonly RUNTIME_VERSION = 'runtime-version'; 16 | public static readonly DOTNETCORE_MAINENTRY_PATH = 'dotnetcore-mainentry-path'; 17 | public static readonly VERSION = 'version'; 18 | public static readonly BUILD_NAME = 'build-name'; 19 | public static readonly PACKAGE = 'package'; 20 | public static readonly TARGET_MODULE = 'target-module'; 21 | public static readonly CPU = 'cpu'; 22 | public static readonly MEMORY = 'memory'; 23 | public static readonly BUILDER = 'builder'; 24 | public static readonly BUILD_CPU = 'build-cpu'; 25 | public static readonly BUILD_MEMORY = 'build-memory'; 26 | public static readonly BUILD_ENV = 'build-env'; 27 | public static readonly CONFIG_FILE_PATTERNS = 'config-file-patterns'; 28 | public static readonly CONTAINER_REGISTRY = 'container-registry'; 29 | public static readonly REGISTRY_USERNAME = 'registry-username'; 30 | public static readonly REGISTRY_PASSWORD = 'registry-password'; 31 | public static readonly CONTAINER_IMAGE = 'container-image'; 32 | public static readonly CONTAINER_COMMAND = 'container-command'; 33 | public static readonly CONTAINER_ARGS = 'container-args'; 34 | public static readonly LANGUAGE_FRAMEWORK = 'language-framework'; 35 | public static readonly ENABLE_LIVENESS_PROBE = 'enable-liveness-probe'; 36 | public static readonly ENABLE_READINESS_PROBE = 'enable-readiness-probe'; 37 | public static readonly ENABLE_STARTUP_PROBE = 'enable-startup-probe'; 38 | public static readonly TERMINATION_GRACE_PERIOD_SECONDS = 'termination-grace-period-seconds'; 39 | public static readonly LIVENESS_PROBE_CONFIG = 'liveness-probe-config'; 40 | public static readonly READINESS_PROBE_CONFIG = 'readiness-probe-config'; 41 | public static readonly STARTUP_PROBE_CONFIG = 'startup-probe-config'; 42 | } 43 | 44 | export class Actions { 45 | public static readonly DEPLOY = 'deploy'; 46 | public static readonly SET_PRODUCTION = 'set-production'; 47 | public static readonly DELETE_STAGING_DEPLOYMENT = 'delete-staging-deployment'; 48 | public static readonly BUILD = 'build'; 49 | public static readonly DELETE_BUILD = 'delete-build'; 50 | } 51 | 52 | export class ActionParametersUtility { 53 | public static getParameters(): ActionParameters { 54 | core.debug('Started getParameters'); 55 | var taskParameters: ActionParameters = { 56 | azureSubscription: core.getInput(Inputs.AZURE_SUBSCRIPTION, {"required": true}), 57 | serviceName: core.getInput(Inputs.SERVICE_NAME, {"required": true}), 58 | action: core.getInput(Inputs.ACTION, {"required": true}).toLowerCase(), 59 | appName: core.getInput(Inputs.APP_NAME, {"required": false}), 60 | useStagingDeployment: core.getBooleanInput(Inputs.USE_STAGING_DEPLOYMENT, {"required": true}), 61 | createNewDeployment: core.getBooleanInput(Inputs.CREATE_NEW_DEPLOYMENT, {"required": false}), 62 | deploymentName: core.getInput(Inputs.DEPLOYMENT_NAME, {"required": false}), 63 | targetModule: core.getInput(Inputs.TARGET_MODULE, {"required": false}), 64 | cpu: core.getInput(Inputs.CPU, {"required": false}), 65 | memory: core.getInput(Inputs.MEMORY, {"required": false}), 66 | environmentVariables: core.getInput(Inputs.ENVIRONMENT_VARIABLES, {"required": false}), 67 | jvmOptions: core.getInput(Inputs.JVM_OPTIONS, {"required": false}), 68 | runtimeVersion: core.getInput(Inputs.RUNTIME_VERSION, {"required": false}), 69 | dotNetCoreMainEntryPath: core.getInput(Inputs.DOTNETCORE_MAINENTRY_PATH, {"required": false}), 70 | version: core.getInput(Inputs.VERSION, {"required": false}), 71 | buildName: core.getInput(Inputs.BUILD_NAME, {"required": false}), 72 | builder: core.getInput(Inputs.BUILDER, {"required": false}), 73 | buildCpu: core.getInput(Inputs.BUILD_CPU, {"required": false}), 74 | buildMemory: core.getInput(Inputs.BUILD_MEMORY, {"required": false}), 75 | buildEnv: core.getInput(Inputs.BUILD_ENV, {"required": false}), 76 | configFilePatterns: core.getInput(Inputs.CONFIG_FILE_PATTERNS, {"required": false}), 77 | containerRegistry: core.getInput(Inputs.CONTAINER_REGISTRY, {"required": false}), 78 | registryUsername: core.getInput(Inputs.REGISTRY_USERNAME, {"required": false}), 79 | registryPassword: core.getInput(Inputs.REGISTRY_PASSWORD, {"required": false}), 80 | containerImage: core.getInput(Inputs.CONTAINER_IMAGE, {"required": false}), 81 | containerCommand: core.getInput(Inputs.CONTAINER_COMMAND, {"required": false}), 82 | containerArgs: core.getInput(Inputs.CONTAINER_ARGS, {"required": false}), 83 | languageFramework: core.getInput(Inputs.LANGUAGE_FRAMEWORK, {"required": false}), 84 | enableLivenessProbe: core.getInput(Inputs.ENABLE_LIVENESS_PROBE, {"required": false}), 85 | enableReadinessProbe: core.getInput(Inputs.ENABLE_READINESS_PROBE, {"required": false}), 86 | enableStartupProbe: core.getInput(Inputs.ENABLE_STARTUP_PROBE, {"required": false}), 87 | terminationGracePeriodSeconds: Number(core.getInput(Inputs.TERMINATION_GRACE_PERIOD_SECONDS, {"required": false})), 88 | livenessProbeConfig: core.getInput(Inputs.LIVENESS_PROBE_CONFIG, {"required": false}), 89 | readinessProbeConfig: core.getInput(Inputs.READINESS_PROBE_CONFIG, {"required": false}), 90 | startupProbeConfig: core.getInput(Inputs.STARTUP_PROBE_CONFIG, {"required": false}) 91 | } 92 | 93 | //Do not attempt to parse package in non-deployment steps. This causes variable substitution errors. 94 | if ((taskParameters.action == Actions.DEPLOY && !taskParameters.containerImage) || taskParameters.action == Actions.BUILD) { 95 | taskParameters.package = new Package(core.getInput(Inputs.PACKAGE, {"required": true})); 96 | } 97 | 98 | core.debug('Task parameters: ' + JSON.stringify(taskParameters)); 99 | return taskParameters; 100 | } 101 | } 102 | 103 | 104 | export interface ActionParameters { 105 | azureSubscription: string, 106 | resourceGroupName?: string; 107 | action: string; 108 | serviceName: string; 109 | appName: string; 110 | useStagingDeployment?: boolean; 111 | createNewDeployment?: boolean; 112 | deploymentName?: string; 113 | environmentVariables?: string; 114 | package?: Package; 115 | targetModule?: string; 116 | cpu?: string; 117 | memory?: string; 118 | jvmOptions?: string; 119 | runtimeVersion?: string; 120 | dotNetCoreMainEntryPath?: string; 121 | version?: string; 122 | buildName?: string; 123 | builder?: string; 124 | buildCpu?: string; 125 | buildMemory?: string; 126 | buildEnv?: string; 127 | configFilePatterns?: string; 128 | containerRegistry?: string; 129 | registryUsername?: string; 130 | registryPassword?: string; 131 | containerImage?: string; 132 | containerCommand?: string; 133 | containerArgs?: string; 134 | languageFramework?: string; 135 | enableLivenessProbe?: string; 136 | enableReadinessProbe?: string; 137 | enableStartupProbe?: string; 138 | terminationGracePeriodSeconds?: number; 139 | livenessProbeConfig?: string; 140 | readinessProbeConfig?: string; 141 | startupProbeConfig?: string; 142 | } 143 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": false, /* Enable all strict type-checking options. */ 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "exclude" : [ 61 | "./__tests__" 62 | ] 63 | } 64 | --------------------------------------------------------------------------------