├── .github ├── CODEOWNERS ├── issue-label-bot.yaml ├── ISSUE_TEMPLATE │ └── bug-report-feature-request.md └── workflows │ ├── auto-triage-issues │ └── defaultLabels.yml ├── tsconfig.dev.json ├── tsconfig.prod.json ├── jest.config.js ├── eng └── ci │ ├── code-mirror.yml │ ├── templates │ └── jobs │ │ └── build-and-test.yml │ ├── public-build.yml │ └── official-build.yml ├── tsconfig.base.json ├── CODE_OF_CONDUCT.md ├── action.yml ├── package.json ├── src ├── Utilities │ ├── AzureAppServiceUtilityExt.ts │ └── ContainerDeploymentUtility.ts ├── taskparameters.ts ├── Tests │ └── main.test.ts └── main.ts ├── LICENSE ├── lib ├── Utilities │ ├── AzureAppServiceUtilityExt.js │ └── ContainerDeploymentUtility.js ├── taskparameters.js ├── Tests │ └── main.test.js └── main.js ├── SECURITY.md ├── README.md └── .gitignore /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @anirudhgarg @eamonoreilly @Hazhzeng 2 | -------------------------------------------------------------------------------- /.github/issue-label-bot.yaml: -------------------------------------------------------------------------------- 1 | label-alias: 2 | bug: 'bug' 3 | feature_request: 'enhancement' 4 | question: 'question' 5 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | } 6 | } -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false 5 | } 6 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report / Feature Request 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: need-to-triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /eng/ci/code-mirror.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | - releases/* 6 | 7 | resources: 8 | repositories: 9 | - repository: eng 10 | type: git 11 | name: engineering 12 | ref: refs/tags/release 13 | 14 | variables: 15 | - template: ci/variables/cfs.yml@eng 16 | 17 | extends: 18 | template: ci/code-mirror.yml@eng -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": "./lib", 7 | "strict": false, 8 | "strictFunctionTypes": true, 9 | "esModuleInterop": true, 10 | "noImplicitAny" : true, 11 | "noUnusedParameters": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "**/*.spec.ts"] 15 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/auto-triage-issues: -------------------------------------------------------------------------------- 1 | #This workflow is used for automatically labelling the respository issues to the nearest possible labels from enhancement,bug, documentation. 2 | 3 | name: "Auto-Labelling Issues" 4 | on: 5 | issues: 6 | types: [opened, edited] 7 | 8 | jobs: 9 | auto_label: 10 | runs-on: ubuntu-latest 11 | name: Auto-Labelling Issues 12 | steps: 13 | - name: Label Step 14 | uses: larrylawl/Auto-Github-Issue-Labeller@v1.0 15 | with: 16 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 17 | REPOSITORY: ${{github.repository}} 18 | DELTA: "7" 19 | CONFIDENCE: "2" 20 | FEATURE: "enhancement" 21 | BUG: "bug" 22 | DOCS: "documentation" 23 | 24 | -------------------------------------------------------------------------------- /eng/ci/templates/jobs/build-and-test.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: pools 3 | type: object 4 | default: [] 5 | 6 | jobs: 7 | - ${{ each pool in parameters.pools }}: 8 | - job: Build_And_Test_${{ pool.type }} 9 | displayName: 'Build and Test ${{ pool.type }}' 10 | 11 | pool: 12 | name: ${{ pool.name }} 13 | image: ${{ pool.image }} 14 | os: ${{ pool.os }} 15 | 16 | steps: 17 | - task: Npm@1 18 | inputs: 19 | command: 'install' 20 | displayName: 'Install dependencies' 21 | 22 | - task: Npm@1 23 | inputs: 24 | command: 'custom' 25 | customCommand: 'run build' 26 | displayName: 'Build' 27 | 28 | - task: Npm@1 29 | inputs: 30 | command: 'custom' 31 | customCommand: 'run test' 32 | displayName: 'Test' -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Azure Functions Container Action' 2 | description: 'Deploy Functions Container to Azure' 3 | inputs: 4 | app-name: 5 | description: 'Name of the Azure Function App' 6 | required: true 7 | image: 8 | description: "Specify the fully qualified container image(s) name. For example, 'myregistry.azurecr.io/nginx:latest' or 'python:3.7.2-alpine/'." 9 | required: true 10 | container-command: 11 | description: "Enter the start up command. For ex. 'dotnet run' or '/azure-functions-host/Microsoft.Azure.WebJobs.Script.WebHost'" 12 | required: false 13 | slot-name: 14 | description: 'Function app slot to be deploy to' 15 | required: false 16 | outputs: 17 | app-url: 18 | description: 'URL to work with your function app' 19 | branding: 20 | icon: 'container-functionapp.svg' 21 | color: 'blue' 22 | runs: 23 | using: 'node20' 24 | main: 'lib/main.js' 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functionapp-container", 3 | "version": "1.0.0", 4 | "description": "Deploy to Azure Functions Container", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "start": "node lib/main.js", 8 | "build": "tsc --project tsconfig.prod.json", 9 | "test": "jest" 10 | }, 11 | "author": { 12 | "name": "Sumiran Aggarwal", 13 | "email": "Sumiran.Aggarwal@microsoft.com" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Hanzhang Zeng", 18 | "email": "Hanzhang.Zeng@microsoft.com" 19 | } 20 | ], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/jest": "^29.5.12", 24 | "@types/node": "^20.11.30", 25 | "jest": "^29.7.0", 26 | "ts-jest": "^29.1.4", 27 | "typescript": "^5.4.3" 28 | }, 29 | "dependencies": { 30 | "@actions/core": "^1.10.1", 31 | "@types/q": "^1.5.8", 32 | "azure-actions-appservice-rest": "^1.3.13", 33 | "azure-actions-webclient": "^1.1.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Utilities/AzureAppServiceUtilityExt.ts: -------------------------------------------------------------------------------- 1 | import core = require('@actions/core'); 2 | import { AzureAppService } from 'azure-actions-appservice-rest/Arm/azure-app-service'; 3 | 4 | export class AzureAppServiceUtilityExt { 5 | private _appService: AzureAppService; 6 | constructor(appService: AzureAppService) { 7 | this._appService = appService; 8 | } 9 | 10 | public async isFunctionAppOnCentauri(): Promise{ 11 | try{ 12 | let details: any = await this._appService.get(); 13 | if (details.properties["managedEnvironmentId"]){ 14 | core.debug("Function Container app is on Centauri."); 15 | return true; 16 | } 17 | else{ 18 | return false; 19 | } 20 | } 21 | catch(error){ 22 | core.debug(`Skipping Centauri check: ${error}`); 23 | return false; 24 | } 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/defaultLabels.yml: -------------------------------------------------------------------------------- 1 | name: setting-default-labels 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | - cron: "0 0/3 * * *" 7 | 8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 9 | jobs: 10 | build: 11 | # The type of runner that the job will run on 12 | runs-on: ubuntu-latest 13 | 14 | # Steps represent a sequence of tasks that will be executed as part of the job 15 | steps: 16 | - uses: actions/stale@v3 17 | name: Setting issue as idle 18 | with: 19 | repo-token: ${{ secrets.GITHUB_TOKEN }} 20 | stale-issue-message: 'This issue is idle because it has been open for 14 days with no activity.' 21 | stale-issue-label: 'idle' 22 | days-before-stale: 14 23 | days-before-close: -1 24 | operations-per-run: 100 25 | exempt-issue-labels: 'backlog' 26 | 27 | - uses: actions/stale@v3 28 | name: Setting PR as idle 29 | with: 30 | repo-token: ${{ secrets.GITHUB_TOKEN }} 31 | stale-pr-message: 'This PR is idle because it has been open for 14 days with no activity.' 32 | stale-pr-label: 'idle' 33 | days-before-stale: 14 34 | days-before-close: -1 35 | operations-per-run: 100 36 | -------------------------------------------------------------------------------- /eng/ci/public-build.yml: -------------------------------------------------------------------------------- 1 | schedules: 2 | - cron: "30 22 * * 2" 3 | displayName: Nightly Build 4 | branches: 5 | include: 6 | - master 7 | always: true 8 | 9 | trigger: 10 | batch: true 11 | branches: 12 | include: 13 | - master 14 | 15 | pr: 16 | branches: 17 | include: 18 | - master 19 | 20 | resources: 21 | repositories: 22 | - repository: 1es 23 | type: git 24 | name: 1ESPipelineTemplates/1ESPipelineTemplates 25 | ref: refs/tags/release 26 | 27 | parameters: 28 | - name: pools 29 | type: object 30 | default: 31 | - type: linux 32 | name: 1es-pool-azfunc-public 33 | image: 1es-ubuntu-22.04 34 | os: linux 35 | - type: windows 36 | name: 1es-pool-azfunc-public 37 | image: 1es-windows-2022 38 | os: windows 39 | - type: macos 40 | name: Azure Pipelines 41 | image: macOS-latest 42 | os: macOS 43 | 44 | extends: 45 | template: v1/1ES.Unofficial.PipelineTemplate.yml@1es 46 | parameters: 47 | pool: 48 | name: 1es-pool-azfunc-public 49 | image: 1es-windows-2022 50 | os: windows 51 | 52 | sdl: 53 | codeql: 54 | compiled: 55 | enabled: true 56 | runSourceLanguagesInSourceAnalysis: true 57 | 58 | stages: 59 | - stage: Build_And_Test 60 | 61 | jobs: 62 | - template: /eng/ci/templates/jobs/build-and-test.yml@self 63 | parameters: 64 | pools: ${{ parameters.pools }} -------------------------------------------------------------------------------- /eng/ci/official-build.yml: -------------------------------------------------------------------------------- 1 | schedules: 2 | - cron: "30 22 * * 2" 3 | displayName: Nightly Build 4 | branches: 5 | include: 6 | - master 7 | always: true 8 | 9 | trigger: 10 | batch: true 11 | branches: 12 | include: 13 | - master 14 | - releases/* 15 | 16 | pr: none 17 | 18 | resources: 19 | repositories: 20 | - repository: 1es 21 | type: git 22 | name: 1ESPipelineTemplates/1ESPipelineTemplates 23 | ref: refs/tags/release 24 | - repository: eng 25 | type: git 26 | name: engineering 27 | ref: refs/tags/release 28 | 29 | variables: 30 | - template: ci/variables/build.yml@eng 31 | - template: ci/variables/cfs.yml@eng 32 | 33 | parameters: 34 | - name: pools 35 | type: object 36 | default: 37 | - type: linux 38 | name: 1es-pool-azfunc 39 | image: 1es-ubuntu-22.04 40 | os: linux 41 | - type: windows 42 | name: 1es-pool-azfunc 43 | image: 1es-windows-2022 44 | os: windows 45 | - type: macos 46 | name: Azure Pipelines 47 | image: macOS-latest 48 | os: macOS 49 | 50 | extends: 51 | template: v1/1ES.Official.PipelineTemplate.yml@1es 52 | parameters: 53 | pool: 54 | name: 1es-pool-azfunc 55 | image: 1es-windows-2022 56 | os: windows 57 | 58 | stages: 59 | - stage: Build_And_Test 60 | 61 | jobs: 62 | - template: /eng/ci/templates/jobs/build-and-test.yml@self 63 | parameters: 64 | pools: ${{ parameters.pools }} -------------------------------------------------------------------------------- /lib/Utilities/AzureAppServiceUtilityExt.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.AzureAppServiceUtilityExt = void 0; 13 | const core = require("@actions/core"); 14 | class AzureAppServiceUtilityExt { 15 | constructor(appService) { 16 | this._appService = appService; 17 | } 18 | isFunctionAppOnCentauri() { 19 | return __awaiter(this, void 0, void 0, function* () { 20 | try { 21 | let details = yield this._appService.get(); 22 | if (details.properties["managedEnvironmentId"]) { 23 | core.debug("Function Container app is on Centauri."); 24 | return true; 25 | } 26 | else { 27 | return false; 28 | } 29 | } 30 | catch (error) { 31 | core.debug(`Skipping Centauri check: ${error}`); 32 | return false; 33 | } 34 | }); 35 | } 36 | } 37 | exports.AzureAppServiceUtilityExt = AzureAppServiceUtilityExt; 38 | -------------------------------------------------------------------------------- /src/taskparameters.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | 3 | import { AzureResourceFilterUtility } from "azure-actions-appservice-rest/Utilities/AzureResourceFilterUtility"; 4 | import { IAuthorizer } from "azure-actions-webclient/Authorizer/IAuthorizer"; 5 | 6 | import fs = require('fs'); 7 | 8 | export class TaskParameters { 9 | private static taskparams: TaskParameters; 10 | private _appName: string; 11 | private _image: string; 12 | private _resourceGroupName?: string; 13 | private _endpoint: IAuthorizer; 14 | private _containerCommand: string; 15 | private _kind: string; 16 | private _slot: string; 17 | private _isLinux: boolean; 18 | 19 | private constructor(endpoint: IAuthorizer) { 20 | this._appName = core.getInput('app-name', { required: true }); 21 | this._image = core.getInput('image'); 22 | this._slot = core.getInput('slot-name'); 23 | this._containerCommand = core.getInput('container-command'); 24 | this._endpoint = endpoint; 25 | } 26 | 27 | public static getTaskParams(endpoint: IAuthorizer) { 28 | if(!this.taskparams) { 29 | this.taskparams = new TaskParameters(endpoint); 30 | } 31 | return this.taskparams; 32 | } 33 | 34 | public get appName() { 35 | return this._appName; 36 | } 37 | 38 | public get image() { 39 | return this._image; 40 | } 41 | 42 | public get resourceGroupName() { 43 | return this._resourceGroupName; 44 | } 45 | 46 | public get endpoint() { 47 | return this._endpoint; 48 | } 49 | 50 | public get isLinux() { 51 | return this._isLinux; 52 | } 53 | 54 | public get containerCommand() { 55 | return this._containerCommand; 56 | } 57 | 58 | public get slot() { 59 | if (this._slot !== undefined && this._slot.trim() === "") { 60 | return undefined; 61 | } 62 | return this._slot; 63 | } 64 | 65 | public async getResourceDetails() { 66 | let appDetails = await AzureResourceFilterUtility.getAppDetails(this.endpoint, this.appName); 67 | this._resourceGroupName = appDetails["resourceGroupName"]; 68 | this._kind = appDetails["kind"]; 69 | this._isLinux = this._kind.indexOf('linux') >= 0; 70 | } 71 | } -------------------------------------------------------------------------------- /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 [many more](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](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, 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.** Instead, please report them to the Microsoft Security Response Center at [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://technet.microsoft.com/en-us/security/dn606155). 12 | 13 | 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). 14 | 15 | 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: 16 | 17 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 18 | * Full paths of source file(s) related to the manifestation of the issue 19 | * The location of the affected source code (tag/branch/commit or direct URL) 20 | * Any special configuration required to reproduce the issue 21 | * Step-by-step instructions to reproduce the issue 22 | * Proof-of-concept or exploit code (if possible) 23 | * Impact of the issue, including how an attacker might exploit the issue 24 | 25 | This information will help us triage your report more quickly. 26 | 27 | ## Preferred Languages 28 | 29 | We prefer all communications to be in English. 30 | 31 | ## Policy 32 | 33 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 34 | 35 | -------------------------------------------------------------------------------- /src/Tests/main.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { main } from "../main"; 3 | import { AuthorizerFactory } from 'azure-actions-webclient/AuthorizerFactory'; 4 | import { AzureAppServiceUtility } from 'azure-actions-appservice-rest/Utilities/AzureAppServiceUtility'; 5 | import { ContainerDeploymentUtility } from 'azure-actions-appservice-rest/Utilities/ContainerDeploymentUtility'; 6 | import { KuduServiceUtility } from 'azure-actions-appservice-rest/Utilities/KuduServiceUtility'; 7 | import { AzureAppService } from 'azure-actions-appservice-rest/Arm/azure-app-service'; 8 | import { TaskParameters } from "../taskparameters"; 9 | 10 | jest.mock('@actions/core'); 11 | jest.mock('../taskparameters'); 12 | jest.mock('azure-actions-webclient/AuthorizerFactory'); 13 | jest.mock('azure-actions-appservice-rest/Arm/azure-app-service'); 14 | jest.mock('azure-actions-appservice-rest/Utilities/AzureAppServiceUtility'); 15 | jest.mock('azure-actions-appservice-rest/Utilities/ContainerDeploymentUtility'); 16 | jest.mock('azure-actions-appservice-rest/Utilities/KuduServiceUtility'); 17 | 18 | describe('main.ts tests', () => { 19 | 20 | afterEach(() => { 21 | jest.restoreAllMocks(); 22 | }) 23 | 24 | //this test checks if all the functions in this action are executing or not 25 | it("gets inputs and executes all the functions", async () => { 26 | 27 | try { 28 | 29 | let getAuthorizerSpy = jest.spyOn(AuthorizerFactory, 'getAuthorizer'); 30 | 31 | let getResourceDetailsSpy = jest.fn(); 32 | let getTaskParamsSpy = jest.spyOn(TaskParameters, 'getTaskParams').mockReturnValue({ 33 | appName: 'mockApp' , 34 | image: 'mockImage' , 35 | slot: 'mockSlotName' , 36 | containerCommand: 'mockContainerCommand' , 37 | getResourceDetails: getResourceDetailsSpy as unknown 38 | } as TaskParameters); 39 | 40 | let getKuduServiceSpy = jest.spyOn(AzureAppServiceUtility.prototype, 'getKuduService'); 41 | let deployWebAppImageSpy = jest.spyOn(ContainerDeploymentUtility.prototype, 'deployWebAppImage'); 42 | let syncFunctionTriggersViaHostruntimeSpy = jest.spyOn(AzureAppService.prototype,'syncFunctionTriggersViaHostruntime'); 43 | let updateDeploymentStatusSpy = jest.spyOn(KuduServiceUtility.prototype, 'updateDeploymentStatus'); 44 | let getApplicationURLSpy = jest.spyOn(AzureAppServiceUtility.prototype, 'getApplicationURL').mockResolvedValue('http://test'); 45 | let setOutputSpy = jest.spyOn(core, 'setOutput'); 46 | let exportVariableSpy = jest.spyOn(core, 'exportVariable'); 47 | 48 | await main(); 49 | 50 | expect(getResourceDetailsSpy).toHaveBeenCalledTimes(1); 51 | expect(getAuthorizerSpy).toHaveBeenCalledTimes(1); 52 | expect(getTaskParamsSpy).toHaveBeenCalledTimes(1); 53 | expect(getKuduServiceSpy).toHaveBeenCalled(); 54 | expect(deployWebAppImageSpy).toHaveBeenCalled(); 55 | expect(syncFunctionTriggersViaHostruntimeSpy).toHaveBeenCalled(); 56 | expect(updateDeploymentStatusSpy).toHaveBeenCalled(); 57 | expect(getApplicationURLSpy).toHaveBeenCalled(); 58 | expect(setOutputSpy).toHaveBeenCalled(); 59 | expect(exportVariableSpy).toHaveBeenCalled(); 60 | 61 | } 62 | catch(e) { 63 | } 64 | 65 | }) 66 | }) -------------------------------------------------------------------------------- /lib/taskparameters.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | exports.TaskParameters = void 0; 36 | const core = __importStar(require("@actions/core")); 37 | const AzureResourceFilterUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureResourceFilterUtility"); 38 | class TaskParameters { 39 | constructor(endpoint) { 40 | this._appName = core.getInput('app-name', { required: true }); 41 | this._image = core.getInput('image'); 42 | this._slot = core.getInput('slot-name'); 43 | this._containerCommand = core.getInput('container-command'); 44 | this._endpoint = endpoint; 45 | } 46 | static getTaskParams(endpoint) { 47 | if (!this.taskparams) { 48 | this.taskparams = new TaskParameters(endpoint); 49 | } 50 | return this.taskparams; 51 | } 52 | get appName() { 53 | return this._appName; 54 | } 55 | get image() { 56 | return this._image; 57 | } 58 | get resourceGroupName() { 59 | return this._resourceGroupName; 60 | } 61 | get endpoint() { 62 | return this._endpoint; 63 | } 64 | get isLinux() { 65 | return this._isLinux; 66 | } 67 | get containerCommand() { 68 | return this._containerCommand; 69 | } 70 | get slot() { 71 | if (this._slot !== undefined && this._slot.trim() === "") { 72 | return undefined; 73 | } 74 | return this._slot; 75 | } 76 | getResourceDetails() { 77 | return __awaiter(this, void 0, void 0, function* () { 78 | let appDetails = yield AzureResourceFilterUtility_1.AzureResourceFilterUtility.getAppDetails(this.endpoint, this.appName); 79 | this._resourceGroupName = appDetails["resourceGroupName"]; 80 | this._kind = appDetails["kind"]; 81 | this._isLinux = this._kind.indexOf('linux') >= 0; 82 | }); 83 | } 84 | } 85 | exports.TaskParameters = TaskParameters; 86 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as crypto from "crypto"; 3 | 4 | import { AuthorizerFactory } from 'azure-actions-webclient/AuthorizerFactory'; 5 | import { AzureAppService } from 'azure-actions-appservice-rest/Arm/azure-app-service'; 6 | import { AzureAppServiceUtility } from 'azure-actions-appservice-rest/Utilities/AzureAppServiceUtility'; 7 | import { AzureAppServiceUtilityExt } from './Utilities/AzureAppServiceUtilityExt'; 8 | import { ContainerDeploymentUtility } from './Utilities/ContainerDeploymentUtility'; 9 | import { KuduServiceUtility } from 'azure-actions-appservice-rest/Utilities/KuduServiceUtility'; 10 | import { TaskParameters } from './taskparameters'; 11 | import { addAnnotation } from 'azure-actions-appservice-rest/Utilities/AnnotationUtility'; 12 | 13 | var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : ""; 14 | let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex'); 15 | let actionName = 'DeployFunctionAppContainerToAzure'; 16 | let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS_${actionName}_${usrAgentRepo}`; 17 | core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); 18 | 19 | export async function main() { 20 | let isDeploymentSuccess: boolean = true; 21 | const responseUrl: string = 'app-url'; 22 | var isCentauri = false; 23 | 24 | try { 25 | let endpoint = await AuthorizerFactory.getAuthorizer(); 26 | var taskParams = TaskParameters.getTaskParams(endpoint); 27 | await taskParams.getResourceDetails(); 28 | 29 | core.debug("Predeployment Step Started"); 30 | var appService = new AzureAppService(taskParams.endpoint, taskParams.resourceGroupName, taskParams.appName, taskParams.slot); 31 | var appServiceUtility = new AzureAppServiceUtility(appService); 32 | var appServiceUtilityExt = new AzureAppServiceUtilityExt(appService); 33 | var isCentauri = await appServiceUtilityExt.isFunctionAppOnCentauri(); 34 | if (!isCentauri){ 35 | var kuduService = await appServiceUtility.getKuduService(); 36 | var kuduServiceUtility = new KuduServiceUtility(kuduService); 37 | } 38 | 39 | core.debug("Deployment Step Started"); 40 | core.debug("Performing container based deployment."); 41 | let containerDeploymentUtility: ContainerDeploymentUtility = new ContainerDeploymentUtility(appService); 42 | if (isCentauri){ 43 | await containerDeploymentUtility.deployWebAppImage(taskParams.image, "", taskParams.isLinux, false, taskParams.containerCommand, false); 44 | } else { 45 | await containerDeploymentUtility.deployWebAppImage(taskParams.image, "", taskParams.isLinux, false, taskParams.containerCommand); 46 | } 47 | 48 | try { 49 | await appService.syncFunctionTriggersViaHostruntime(); 50 | } catch (expt) { 51 | core.warning("Failed to sync function triggers in function app. Trigger listing may be out of date."); 52 | } 53 | } 54 | catch (error) { 55 | core.debug("Deployment Failed with Error: " + error); 56 | isDeploymentSuccess = false; 57 | core.setFailed(error); 58 | } 59 | finally { 60 | if (!isCentauri){ 61 | if(!!kuduServiceUtility) { 62 | await addAnnotation(taskParams.endpoint, appService, isDeploymentSuccess); 63 | let activeDeploymentID = await kuduServiceUtility.updateDeploymentStatus(isDeploymentSuccess, null, {'type': 'Deployment', 'slotName': appService.getSlot()}); 64 | core.debug('Active DeploymentId: '+ activeDeploymentID); 65 | } 66 | 67 | if(!!appServiceUtility) { 68 | let appServiceApplicationUrl: string = await appServiceUtility.getApplicationURL(); 69 | console.log('Azure Function App URL: ' + appServiceApplicationUrl); 70 | core.setOutput(responseUrl, appServiceApplicationUrl); 71 | } 72 | } 73 | core.exportVariable('AZURE_HTTP_USER_AGENT', prefix); 74 | core.debug(isDeploymentSuccess ? "Deployment Succeded" : "Deployment failed"); 75 | } 76 | } 77 | 78 | main(); -------------------------------------------------------------------------------- /src/Utilities/ContainerDeploymentUtility.ts: -------------------------------------------------------------------------------- 1 | import { AzureAppService } from 'azure-actions-appservice-rest/Arm/azure-app-service'; 2 | import { AzureAppServiceUtility } from 'azure-actions-appservice-rest/Utilities/AzureAppServiceUtility'; 3 | 4 | import fs = require('fs'); 5 | import path = require('path'); 6 | import core = require('@actions/core'); 7 | 8 | export class ContainerDeploymentUtility { 9 | private _appService: AzureAppService; 10 | private _appServiceUtility: AzureAppServiceUtility; 11 | 12 | constructor(appService: AzureAppService) { 13 | this._appService = appService; 14 | this._appServiceUtility = new AzureAppServiceUtility(appService); 15 | } 16 | 17 | public async deployWebAppImage(images: string, multiContainerConfigFile: string, isLinux: boolean, 18 | isMultiContainer: boolean, startupCommand: string, restart:boolean=true): Promise { 19 | let updatedMulticontainerConfigFile: string = multiContainerConfigFile; 20 | 21 | if(isMultiContainer) { 22 | core.debug("Deploying Docker-Compose file " + multiContainerConfigFile + " to the webapp " + this._appService.getName()); 23 | if(!!images) { 24 | updatedMulticontainerConfigFile = this._updateImagesInConfigFile(multiContainerConfigFile, images); 25 | } 26 | } 27 | else { 28 | core.debug("Deploying image " + images + " to the webapp " + this._appService.getName()); 29 | } 30 | 31 | core.debug("Updating the webapp configuration."); 32 | await this._updateConfigurationDetails(startupCommand, isLinux, isMultiContainer, images, updatedMulticontainerConfigFile); 33 | 34 | if (restart){ 35 | core.debug('making a restart request to app service'); 36 | await this._appService.restart(); 37 | } 38 | } 39 | 40 | private _updateImagesInConfigFile(multicontainerConfigFile: any, images: any): string { 41 | const tempDirectory = `${process.env.RUNNER_TEMP}`; 42 | var contents = fs.readFileSync(multicontainerConfigFile).toString(); 43 | var imageList = images.split("\n"); 44 | imageList.forEach((image: string) => { 45 | let imageName = image.split(":")[0]; 46 | if (contents.indexOf(imageName) > 0) { 47 | contents = this._tokenizeImages(contents, imageName, image); 48 | } 49 | }); 50 | 51 | let newFilePath = path.join(tempDirectory, path.basename(multicontainerConfigFile)); 52 | fs.writeFileSync( 53 | path.join(newFilePath), 54 | contents 55 | ); 56 | 57 | return newFilePath; 58 | } 59 | 60 | private _tokenizeImages(currentString: string, imageName: string, imageNameWithNewTag: string) { 61 | let i = currentString.indexOf(imageName); 62 | if (i < 0) { 63 | core.debug(`No occurence of replacement token: ${imageName} found`); 64 | return currentString; 65 | } 66 | 67 | let newString = ""; 68 | currentString.split("\n") 69 | .forEach((line) => { 70 | if (line.indexOf(imageName) > 0 && line.toLocaleLowerCase().indexOf("image") > 0) { 71 | let i = line.indexOf(imageName); 72 | newString += line.substring(0, i); 73 | let leftOverString = line.substring(i); 74 | if (leftOverString.endsWith("\"")) { 75 | newString += imageNameWithNewTag + "\"" + "\n"; 76 | } else { 77 | newString += imageNameWithNewTag + "\n"; 78 | } 79 | } 80 | else { 81 | newString += line + "\n"; 82 | } 83 | }); 84 | 85 | return newString; 86 | } 87 | 88 | private async _updateConfigurationDetails(startupCommand: string, isLinuxApp: boolean, isMultiContainer: boolean, imageName?: string, multicontainerConfigFile?: string): Promise { 89 | var appSettingsNewProperties: {[index: string]:any} = !!startupCommand ? { appCommandLine: startupCommand } : {}; 90 | 91 | if(isLinuxApp) { 92 | if(isMultiContainer) { 93 | let fileData = fs.readFileSync(multicontainerConfigFile); 94 | appSettingsNewProperties["linuxFxVersion"] = "COMPOSE|" + (Buffer.from(fileData).toString('base64')); 95 | } 96 | else { 97 | appSettingsNewProperties["linuxFxVersion"] = "DOCKER|" + imageName; 98 | } 99 | } 100 | else { 101 | appSettingsNewProperties["windowsFxVersion"] = "DOCKER|" + imageName; 102 | } 103 | 104 | core.debug(`CONTAINER UPDATE CONFIG VALUES : ${JSON.stringify(appSettingsNewProperties)}`); 105 | await this._appServiceUtility.updateConfigurationSettings(appSettingsNewProperties); 106 | } 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions for deploying customized Azure Functions image 2 | 3 | With the Azure Functions GitHub Action, you can automate your workflow to deploy [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) in a customized image. 4 | 5 | Get started today with a [free Azure account](https://azure.com/free/open-source)! 6 | 7 | The repository contains a GitHub Action to deploy your customized function app image into Azure Functions. If you are looking for a simple GitHub Action to deploy your function app without building a customized image, please consider using [functions-action](https://github.com/Azure/functions-action). 8 | 9 | The definition of this GitHub Action is in [action.yml](https://github.com/Azure/functions-container-action/blob/master/action.yml). 10 | 11 | # End-to-End Sample workflow 12 | 13 | ## Dependencies on other GitHub Actions 14 | * [Checkout](https://github.com/actions/checkout) Checkout your Git repository content into GitHub Actions agent. 15 | * [Azure Login](https://github.com/Azure/login) Login with your Azure credentials for function app deployment authentication. 16 | 17 | ## Azure Service Principle for RBAC 18 | Create an [Azure Service Principal for RBAC](https://docs.microsoft.com/en-us/azure/role-based-access-control/overview) and add it as a [GitHub Secret in your repository](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables). 19 | 1. Download Azure CLI from [here](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest), run `az login` to login with your Azure credentials. 20 | 2. Run Azure CLI command 21 | ``` 22 | az ad sp create-for-rbac --name "myApp" --role contributor \ 23 | --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Web/sites/{app-name} \ 24 | --sdk-auth 25 | 26 | # Replace {subscription-id}, {resource-group}, and {app-name} with the names of your subscription, resource group, and Azure function app. 27 | # The command should output a JSON object similar to this: 28 | 29 | { 30 | "clientId": "", 31 | "clientSecret": "", 32 | "subscriptionId": "", 33 | "tenantId": "", 34 | (...) 35 | } 36 | ``` 37 | 3. Paste the json response from above Azure CLI to your GitHub Repository > Settings > Secrets > Add a new secret > **AZURE_CREDENTIALS** 38 | 39 | ## Setup Container Registry Credentials 40 | 1. Paste your container registry username (e.g. docker hub username) to your GitHub Repository > Settings > Secrets > Add a new secret > **REGISTRY_USERNAME** 41 | 2. Paste your container registry password (e.g. docker hub password) to your GitHub Repository > Settings > Secrets > Add a new secret > **REGISTRY_PASSWORD** 42 | 3. (Optional) Create a new repository under your registry namespace (e.g. docker.io/mynamespace/myrepository) 43 | 44 | ## Create Azure function app and Deploy to function app container using GitHub Actions 45 | 1. Follow the tutorial [Create a function on Linux using a custom image](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-linux-custom-image) 46 | 2. Customize your Dockerfile to ensure the function app dependencies can be resolved properly on runtime (e.g. npm install) 47 | 3. Use the [linux-container-functionapp-on-azure.yml](https://github.com/Azure/actions-workflow-samples/tree/master/FunctionApp/linux-container-functionapp-on-azure.yml) template as a reference, create a new workflow.yml file under your project `./github/workflows/` 48 | 4. Commit and push your project to GitHub repository, you should see a new GitHub Action initiated in **Actions** tab. 49 | 50 | Azure Functions Action for deploying customized Azure Functions image is supported for the Azure public cloud as well as Azure government clouds ('AzureUSGovernment' or 'AzureChinaCloud'). Before running this action, login to the respective Azure Cloud using [Azure Login](https://github.com/Azure/login) by setting appropriate value for the `environment` parameter. 51 | 52 | # Contributing 53 | 54 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 55 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 56 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 57 | 58 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 59 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 60 | provided by the bot. You will only need to do this once across all repos using our CLA. 61 | 62 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 63 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 64 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 65 | -------------------------------------------------------------------------------- /lib/Tests/main.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | const core = __importStar(require("@actions/core")); 36 | const main_1 = require("../main"); 37 | const AuthorizerFactory_1 = require("azure-actions-webclient/AuthorizerFactory"); 38 | const AzureAppServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureAppServiceUtility"); 39 | const ContainerDeploymentUtility_1 = require("azure-actions-appservice-rest/Utilities/ContainerDeploymentUtility"); 40 | const KuduServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/KuduServiceUtility"); 41 | const azure_app_service_1 = require("azure-actions-appservice-rest/Arm/azure-app-service"); 42 | const taskparameters_1 = require("../taskparameters"); 43 | jest.mock('@actions/core'); 44 | jest.mock('../taskparameters'); 45 | jest.mock('azure-actions-webclient/AuthorizerFactory'); 46 | jest.mock('azure-actions-appservice-rest/Arm/azure-app-service'); 47 | jest.mock('azure-actions-appservice-rest/Utilities/AzureAppServiceUtility'); 48 | jest.mock('azure-actions-appservice-rest/Utilities/ContainerDeploymentUtility'); 49 | jest.mock('azure-actions-appservice-rest/Utilities/KuduServiceUtility'); 50 | describe('main.ts tests', () => { 51 | afterEach(() => { 52 | jest.restoreAllMocks(); 53 | }); 54 | //this test checks if all the functions in this action are executing or not 55 | it("gets inputs and executes all the functions", () => __awaiter(void 0, void 0, void 0, function* () { 56 | try { 57 | let getAuthorizerSpy = jest.spyOn(AuthorizerFactory_1.AuthorizerFactory, 'getAuthorizer'); 58 | let getResourceDetailsSpy = jest.fn(); 59 | let getTaskParamsSpy = jest.spyOn(taskparameters_1.TaskParameters, 'getTaskParams').mockReturnValue({ 60 | appName: 'mockApp', 61 | image: 'mockImage', 62 | slot: 'mockSlotName', 63 | containerCommand: 'mockContainerCommand', 64 | getResourceDetails: getResourceDetailsSpy 65 | }); 66 | let getKuduServiceSpy = jest.spyOn(AzureAppServiceUtility_1.AzureAppServiceUtility.prototype, 'getKuduService'); 67 | let deployWebAppImageSpy = jest.spyOn(ContainerDeploymentUtility_1.ContainerDeploymentUtility.prototype, 'deployWebAppImage'); 68 | let syncFunctionTriggersViaHostruntimeSpy = jest.spyOn(azure_app_service_1.AzureAppService.prototype, 'syncFunctionTriggersViaHostruntime'); 69 | let updateDeploymentStatusSpy = jest.spyOn(KuduServiceUtility_1.KuduServiceUtility.prototype, 'updateDeploymentStatus'); 70 | let getApplicationURLSpy = jest.spyOn(AzureAppServiceUtility_1.AzureAppServiceUtility.prototype, 'getApplicationURL').mockResolvedValue('http://test'); 71 | let setOutputSpy = jest.spyOn(core, 'setOutput'); 72 | let exportVariableSpy = jest.spyOn(core, 'exportVariable'); 73 | yield (0, main_1.main)(); 74 | expect(getResourceDetailsSpy).toHaveBeenCalledTimes(1); 75 | expect(getAuthorizerSpy).toHaveBeenCalledTimes(1); 76 | expect(getTaskParamsSpy).toHaveBeenCalledTimes(1); 77 | expect(getKuduServiceSpy).toHaveBeenCalled(); 78 | expect(deployWebAppImageSpy).toHaveBeenCalled(); 79 | expect(syncFunctionTriggersViaHostruntimeSpy).toHaveBeenCalled(); 80 | expect(updateDeploymentStatusSpy).toHaveBeenCalled(); 81 | expect(getApplicationURLSpy).toHaveBeenCalled(); 82 | expect(setOutputSpy).toHaveBeenCalled(); 83 | expect(exportVariableSpy).toHaveBeenCalled(); 84 | } 85 | catch (e) { 86 | } 87 | })); 88 | }); 89 | -------------------------------------------------------------------------------- /lib/Utilities/ContainerDeploymentUtility.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.ContainerDeploymentUtility = void 0; 13 | const AzureAppServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureAppServiceUtility"); 14 | const fs = require("fs"); 15 | const path = require("path"); 16 | const core = require("@actions/core"); 17 | class ContainerDeploymentUtility { 18 | constructor(appService) { 19 | this._appService = appService; 20 | this._appServiceUtility = new AzureAppServiceUtility_1.AzureAppServiceUtility(appService); 21 | } 22 | deployWebAppImage(images_1, multiContainerConfigFile_1, isLinux_1, isMultiContainer_1, startupCommand_1) { 23 | return __awaiter(this, arguments, void 0, function* (images, multiContainerConfigFile, isLinux, isMultiContainer, startupCommand, restart = true) { 24 | let updatedMulticontainerConfigFile = multiContainerConfigFile; 25 | if (isMultiContainer) { 26 | core.debug("Deploying Docker-Compose file " + multiContainerConfigFile + " to the webapp " + this._appService.getName()); 27 | if (!!images) { 28 | updatedMulticontainerConfigFile = this._updateImagesInConfigFile(multiContainerConfigFile, images); 29 | } 30 | } 31 | else { 32 | core.debug("Deploying image " + images + " to the webapp " + this._appService.getName()); 33 | } 34 | core.debug("Updating the webapp configuration."); 35 | yield this._updateConfigurationDetails(startupCommand, isLinux, isMultiContainer, images, updatedMulticontainerConfigFile); 36 | if (restart) { 37 | core.debug('making a restart request to app service'); 38 | yield this._appService.restart(); 39 | } 40 | }); 41 | } 42 | _updateImagesInConfigFile(multicontainerConfigFile, images) { 43 | const tempDirectory = `${process.env.RUNNER_TEMP}`; 44 | var contents = fs.readFileSync(multicontainerConfigFile).toString(); 45 | var imageList = images.split("\n"); 46 | imageList.forEach((image) => { 47 | let imageName = image.split(":")[0]; 48 | if (contents.indexOf(imageName) > 0) { 49 | contents = this._tokenizeImages(contents, imageName, image); 50 | } 51 | }); 52 | let newFilePath = path.join(tempDirectory, path.basename(multicontainerConfigFile)); 53 | fs.writeFileSync(path.join(newFilePath), contents); 54 | return newFilePath; 55 | } 56 | _tokenizeImages(currentString, imageName, imageNameWithNewTag) { 57 | let i = currentString.indexOf(imageName); 58 | if (i < 0) { 59 | core.debug(`No occurence of replacement token: ${imageName} found`); 60 | return currentString; 61 | } 62 | let newString = ""; 63 | currentString.split("\n") 64 | .forEach((line) => { 65 | if (line.indexOf(imageName) > 0 && line.toLocaleLowerCase().indexOf("image") > 0) { 66 | let i = line.indexOf(imageName); 67 | newString += line.substring(0, i); 68 | let leftOverString = line.substring(i); 69 | if (leftOverString.endsWith("\"")) { 70 | newString += imageNameWithNewTag + "\"" + "\n"; 71 | } 72 | else { 73 | newString += imageNameWithNewTag + "\n"; 74 | } 75 | } 76 | else { 77 | newString += line + "\n"; 78 | } 79 | }); 80 | return newString; 81 | } 82 | _updateConfigurationDetails(startupCommand, isLinuxApp, isMultiContainer, imageName, multicontainerConfigFile) { 83 | return __awaiter(this, void 0, void 0, function* () { 84 | var appSettingsNewProperties = !!startupCommand ? { appCommandLine: startupCommand } : {}; 85 | if (isLinuxApp) { 86 | if (isMultiContainer) { 87 | let fileData = fs.readFileSync(multicontainerConfigFile); 88 | appSettingsNewProperties["linuxFxVersion"] = "COMPOSE|" + (Buffer.from(fileData).toString('base64')); 89 | } 90 | else { 91 | appSettingsNewProperties["linuxFxVersion"] = "DOCKER|" + imageName; 92 | } 93 | } 94 | else { 95 | appSettingsNewProperties["windowsFxVersion"] = "DOCKER|" + imageName; 96 | } 97 | core.debug(`CONTAINER UPDATE CONFIG VALUES : ${JSON.stringify(appSettingsNewProperties)}`); 98 | yield this._appServiceUtility.updateConfigurationSettings(appSettingsNewProperties); 99 | }); 100 | } 101 | } 102 | exports.ContainerDeploymentUtility = ContainerDeploymentUtility; 103 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | exports.main = void 0; 36 | const core = __importStar(require("@actions/core")); 37 | const crypto = __importStar(require("crypto")); 38 | const AuthorizerFactory_1 = require("azure-actions-webclient/AuthorizerFactory"); 39 | const azure_app_service_1 = require("azure-actions-appservice-rest/Arm/azure-app-service"); 40 | const AzureAppServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/AzureAppServiceUtility"); 41 | const AzureAppServiceUtilityExt_1 = require("./Utilities/AzureAppServiceUtilityExt"); 42 | const ContainerDeploymentUtility_1 = require("./Utilities/ContainerDeploymentUtility"); 43 | const KuduServiceUtility_1 = require("azure-actions-appservice-rest/Utilities/KuduServiceUtility"); 44 | const taskparameters_1 = require("./taskparameters"); 45 | const AnnotationUtility_1 = require("azure-actions-appservice-rest/Utilities/AnnotationUtility"); 46 | var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : ""; 47 | let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex'); 48 | let actionName = 'DeployFunctionAppContainerToAzure'; 49 | let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS_${actionName}_${usrAgentRepo}`; 50 | core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); 51 | function main() { 52 | return __awaiter(this, void 0, void 0, function* () { 53 | let isDeploymentSuccess = true; 54 | const responseUrl = 'app-url'; 55 | var isCentauri = false; 56 | try { 57 | let endpoint = yield AuthorizerFactory_1.AuthorizerFactory.getAuthorizer(); 58 | var taskParams = taskparameters_1.TaskParameters.getTaskParams(endpoint); 59 | yield taskParams.getResourceDetails(); 60 | core.debug("Predeployment Step Started"); 61 | var appService = new azure_app_service_1.AzureAppService(taskParams.endpoint, taskParams.resourceGroupName, taskParams.appName, taskParams.slot); 62 | var appServiceUtility = new AzureAppServiceUtility_1.AzureAppServiceUtility(appService); 63 | var appServiceUtilityExt = new AzureAppServiceUtilityExt_1.AzureAppServiceUtilityExt(appService); 64 | var isCentauri = yield appServiceUtilityExt.isFunctionAppOnCentauri(); 65 | if (!isCentauri) { 66 | var kuduService = yield appServiceUtility.getKuduService(); 67 | var kuduServiceUtility = new KuduServiceUtility_1.KuduServiceUtility(kuduService); 68 | } 69 | core.debug("Deployment Step Started"); 70 | core.debug("Performing container based deployment."); 71 | let containerDeploymentUtility = new ContainerDeploymentUtility_1.ContainerDeploymentUtility(appService); 72 | if (isCentauri) { 73 | yield containerDeploymentUtility.deployWebAppImage(taskParams.image, "", taskParams.isLinux, false, taskParams.containerCommand, false); 74 | } 75 | else { 76 | yield containerDeploymentUtility.deployWebAppImage(taskParams.image, "", taskParams.isLinux, false, taskParams.containerCommand); 77 | } 78 | try { 79 | yield appService.syncFunctionTriggersViaHostruntime(); 80 | } 81 | catch (expt) { 82 | core.warning("Failed to sync function triggers in function app. Trigger listing may be out of date."); 83 | } 84 | } 85 | catch (error) { 86 | core.debug("Deployment Failed with Error: " + error); 87 | isDeploymentSuccess = false; 88 | core.setFailed(error); 89 | } 90 | finally { 91 | if (!isCentauri) { 92 | if (!!kuduServiceUtility) { 93 | yield (0, AnnotationUtility_1.addAnnotation)(taskParams.endpoint, appService, isDeploymentSuccess); 94 | let activeDeploymentID = yield kuduServiceUtility.updateDeploymentStatus(isDeploymentSuccess, null, { 'type': 'Deployment', 'slotName': appService.getSlot() }); 95 | core.debug('Active DeploymentId: ' + activeDeploymentID); 96 | } 97 | if (!!appServiceUtility) { 98 | let appServiceApplicationUrl = yield appServiceUtility.getApplicationURL(); 99 | console.log('Azure Function App URL: ' + appServiceApplicationUrl); 100 | core.setOutput(responseUrl, appServiceApplicationUrl); 101 | } 102 | } 103 | core.exportVariable('AZURE_HTTP_USER_AGENT', prefix); 104 | core.debug(isDeploymentSuccess ? "Deployment Succeded" : "Deployment failed"); 105 | } 106 | }); 107 | } 108 | exports.main = main; 109 | main(); 110 | -------------------------------------------------------------------------------- /.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 | # Github Actions master branch will not have node_modules (use v1 instead) 7 | node_modules/ 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | **/Properties/launchSettings.json 59 | 60 | # StyleCop 61 | StyleCopReport.xml 62 | 63 | # Files built by Visual Studio 64 | *_i.c 65 | *_p.c 66 | *_i.h 67 | *.ilk 68 | *.meta 69 | *.obj 70 | *.iobj 71 | *.pch 72 | *.pdb 73 | *.ipdb 74 | *.pgc 75 | *.pgd 76 | *.rsp 77 | *.sbr 78 | *.tlb 79 | *.tli 80 | *.tlh 81 | *.tmp 82 | *.tmp_proj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | 259 | # Microsoft Fakes 260 | FakesAssemblies/ 261 | 262 | # GhostDoc plugin setting file 263 | *.GhostDoc.xml 264 | 265 | # Node.js Tools for Visual Studio 266 | .ntvs_analysis.dat 267 | # Github Action does not have node module resolution yet 268 | # node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush 299 | .cr/ 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | --------------------------------------------------------------------------------