From 72beb091b0f56f133b949c430a1485f8e2d65f95 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Thu, 26 Feb 2026 08:16:34 +0000 Subject: [PATCH] Add download-code-site and upload-code-site GitHub Actions Add new GitHub Actions for Power Pages code site download and upload, following the existing download-paportal/upload-paportal patterns. New actions: - download-code-site: Downloads Power Pages code site content - upload-code-site: Uploads Power Pages code site with root-path, compiled-path, and site-name parameters Includes unit tests for both actions matching the paportal test patterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- download-code-site/action.yml | 53 +++++++++++++++++++++++++ src/actions/download-code-site/index.ts | 27 +++++++++++++ src/actions/upload-code-site/index.ts | 27 +++++++++++++ src/test/download-code-site.test.ts | 46 +++++++++++++++++++++ src/test/upload-code-site.test.ts | 46 +++++++++++++++++++++ upload-code-site/action.yml | 53 +++++++++++++++++++++++++ 6 files changed, 252 insertions(+) create mode 100644 download-code-site/action.yml create mode 100644 src/actions/download-code-site/index.ts create mode 100644 src/actions/upload-code-site/index.ts create mode 100644 src/test/download-code-site.test.ts create mode 100644 src/test/upload-code-site.test.ts create mode 100644 upload-code-site/action.yml diff --git a/download-code-site/action.yml b/download-code-site/action.yml new file mode 100644 index 00000000..438d15c1 --- /dev/null +++ b/download-code-site/action.yml @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +name: 'download-code-site' +description: 'Power Platform Download Code Site' +inputs: + environment-url: + description: 'URL of Power Platform environment to connect with; e.g. "https://test-env.crm.dynamics.com"' + required: true + + user-name: + description: 'Power Platform user name to authenticate with, e.g. myname@my-org.onmicrosoft.com. Setting this input makes user-name and password required; specifying alternate "app-id" credential set of inputs will result in an error.' + required: false + + password-secret: + description: 'Power Platform password, required if authenticating with username. Do NOT checkin password, instead create a secret and reference it here with: see: https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#using-encrypted-secrets-in-a-workflow' + required: false + + app-id: + description: 'The application id to authenticate with. Setting this input makes app-id, tenant-id and client-secret required; specifying alternate "username" credential set of inputs will result in an error.' + required: false + + client-secret: + description: 'The client secret to authenticate with. Required if authenticating with app-id.' + required: false + + tenant-id: + description: 'Tenant id if using app-id & client secret to authenticate.' + required: false + + cloud: + description: 'Cloud instance to authenticate with. Default: Public. See "pac auth create help" for valid cloud instance names' + required: false + default: 'Public' + + download-path: + description: 'Local path to where the Power Pages code site content will be downloaded' + required: true + + website-id: + description: 'Website id of the Power Pages website to be downloaded' + required: true + + overwrite: + description: 'Overwrite if Power Pages code site exists at the given path' + required: false + + working-directory: + description: 'Working directory; default: root of repository' + required: false + +runs: + using: 'node20' + main: '../dist/actions/download-code-site/index.js' diff --git a/src/actions/download-code-site/index.ts b/src/actions/download-code-site/index.ts new file mode 100644 index 00000000..ce245760 --- /dev/null +++ b/src/actions/download-code-site/index.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import * as core from '@actions/core'; +import { downloadCodeSite } from "@microsoft/powerplatform-cli-wrapper/dist/actions"; +import { YamlParser } from '../../lib/parser/YamlParser'; +import { ActionsHost } from '../../lib/host/ActionsHost'; +import getCredentials from "../../lib/auth/getCredentials"; +import getEnvironmentUrl from "../../lib/auth/getEnvironmentUrl"; +import { runnerParameters } from '../../lib/runnerParameters'; + +(async () => { + const taskParser = new YamlParser(); + const parameterMap = taskParser.getHostParameterEntries('download-code-site'); + + await downloadCodeSite({ + credentials: getCredentials(), + environmentUrl: getEnvironmentUrl(), + path: parameterMap['download-path'], + websiteId: parameterMap['website-id'], + overwrite: parameterMap['overwrite'], + }, runnerParameters, new ActionsHost()); + core.endGroup(); +})().catch(error => { + const logger = runnerParameters.logger; + logger.error(`failed: ${error}`); + core.endGroup(); +}); diff --git a/src/actions/upload-code-site/index.ts b/src/actions/upload-code-site/index.ts new file mode 100644 index 00000000..d9b8cf45 --- /dev/null +++ b/src/actions/upload-code-site/index.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import * as core from '@actions/core'; +import { uploadCodeSite } from "@microsoft/powerplatform-cli-wrapper/dist/actions"; +import { YamlParser } from '../../lib/parser/YamlParser'; +import { ActionsHost } from '../../lib/host/ActionsHost'; +import getCredentials from "../../lib/auth/getCredentials"; +import getEnvironmentUrl from "../../lib/auth/getEnvironmentUrl"; +import { runnerParameters } from '../../lib/runnerParameters'; + +(async () => { + const taskParser = new YamlParser(); + const parameterMap = taskParser.getHostParameterEntries('upload-code-site'); + + await uploadCodeSite({ + credentials: getCredentials(), + environmentUrl: getEnvironmentUrl(), + rootPath: parameterMap['root-path'], + compiledPath: parameterMap['compiled-path'], + siteName: parameterMap['site-name'], + }, runnerParameters, new ActionsHost()); + core.endGroup(); +})().catch(error => { + const logger = runnerParameters.logger; + logger.error(`failed: ${error}`); + core.endGroup(); +}); diff --git a/src/test/download-code-site.test.ts b/src/test/download-code-site.test.ts new file mode 100644 index 00000000..e7c1ee65 --- /dev/null +++ b/src/test/download-code-site.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { should, use } from "chai"; +import { stubInterface } from "ts-sinon"; +import * as sinonChai from "sinon-chai"; +import rewiremock from "./rewiremock"; +import { fake, stub } from "sinon"; +import { UsernamePassword } from "@microsoft/powerplatform-cli-wrapper"; +import { runnerParameters } from "../../src/lib/runnerParameters"; +import Sinon = require("sinon"); +import { ActionsHost } from "../lib/host/ActionsHost"; +should(); +use(sinonChai); + +describe("download code-site test", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const downloadCodeSiteStub: Sinon.SinonStub = stub(); + const credentials: UsernamePassword = stubInterface(); + const environmentUrl = "environment url"; + + async function callActionWithMocks(): Promise { + await rewiremock.around( + () => import("../../src/actions/download-code-site/index"), + (mock) => { + mock(() => import("@microsoft/powerplatform-cli-wrapper/dist/actions")).with({ downloadCodeSite: downloadCodeSiteStub }); + mock(() => import("../../src/lib/auth/getCredentials")).withDefault(() => credentials ); + mock(() => import("../../src/lib/auth/getEnvironmentUrl")).withDefault(() => environmentUrl ); + mock(() => import("fs/promises")).with({ chmod: fake() }); + mock(() => import("../../src/lib/runnerParameters")).with({ runnerParameters: runnerParameters }); + }); + } + + it("calls download code-site", async () => { + + await callActionWithMocks(); + + downloadCodeSiteStub.should.have.been.calledOnceWithExactly({ + credentials: credentials, + environmentUrl: environmentUrl, + path: { name: 'download-path', required: true, defaultValue: undefined }, + websiteId: { name: 'website-id', required: true, defaultValue: undefined }, + overwrite: { name: 'overwrite', required: false, defaultValue: undefined }, + }, runnerParameters, new ActionsHost()); + }); +}); diff --git a/src/test/upload-code-site.test.ts b/src/test/upload-code-site.test.ts new file mode 100644 index 00000000..6bd47044 --- /dev/null +++ b/src/test/upload-code-site.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { should, use } from "chai"; +import { stubInterface } from "ts-sinon"; +import * as sinonChai from "sinon-chai"; +import rewiremock from "./rewiremock"; +import { fake, stub } from "sinon"; +import { UsernamePassword } from "@microsoft/powerplatform-cli-wrapper"; +import { runnerParameters } from "../../src/lib/runnerParameters"; +import Sinon = require("sinon"); +import { ActionsHost } from "../lib/host/ActionsHost"; +should(); +use(sinonChai); + +describe("upload code-site test", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const uploadCodeSiteStub: Sinon.SinonStub = stub(); + const credentials: UsernamePassword = stubInterface(); + const environmentUrl = "environment url"; + + async function callActionWithMocks(): Promise { + await rewiremock.around( + () => import("../../src/actions/upload-code-site/index"), + (mock) => { + mock(() => import("@microsoft/powerplatform-cli-wrapper/dist/actions")).with({ uploadCodeSite: uploadCodeSiteStub }); + mock(() => import("../../src/lib/auth/getCredentials")).withDefault(() => credentials ); + mock(() => import("../../src/lib/auth/getEnvironmentUrl")).withDefault(() => environmentUrl ); + mock(() => import("fs/promises")).with({ chmod: fake() }); + mock(() => import("../../src/lib/runnerParameters")).with({ runnerParameters: runnerParameters }); + }); + } + + it("calls upload code-site", async () => { + + await callActionWithMocks(); + + uploadCodeSiteStub.should.have.been.calledOnceWithExactly({ + credentials: credentials, + environmentUrl: environmentUrl, + rootPath: { name: 'root-path', required: true, defaultValue: undefined }, + compiledPath: { name: 'compiled-path', required: false, defaultValue: undefined }, + siteName: { name: 'site-name', required: false, defaultValue: undefined }, + }, runnerParameters, new ActionsHost()); + }); +}); diff --git a/upload-code-site/action.yml b/upload-code-site/action.yml new file mode 100644 index 00000000..9fc09d3a --- /dev/null +++ b/upload-code-site/action.yml @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +name: 'upload-code-site' +description: 'Power Platform Upload Code Site' +inputs: + environment-url: + description: 'URL of Power Platform environment to connect with; e.g. "https://test-env.crm.dynamics.com"' + required: true + + user-name: + description: 'Power Platform user name to authenticate with, e.g. myname@my-org.onmicrosoft.com. Setting this input makes user-name and password required; specifying alternate "app-id" credential set of inputs will result in an error.' + required: false + + password-secret: + description: 'Power Platform password, required if authenticating with username. Do NOT checkin password, instead create a secret and reference it here with: see: https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#using-encrypted-secrets-in-a-workflow' + required: false + + app-id: + description: 'The application id to authenticate with. Setting this input makes app-id, tenant-id and client-secret required; specifying alternate "username" credential set of inputs will result in an error.' + required: false + + client-secret: + description: 'The client secret to authenticate with. Required if authenticating with app-id.' + required: false + + tenant-id: + description: 'Tenant id if using app-id & client secret to authenticate.' + required: false + + cloud: + description: 'Cloud instance to authenticate with. Default: Public. See "pac auth create help" for valid cloud instance names' + required: false + default: 'Public' + + root-path: + description: 'Root path of the Power Pages code site project' + required: true + + compiled-path: + description: 'Path to the compiled output directory' + required: false + + site-name: + description: 'Name of the Power Pages site' + required: false + + working-directory: + description: 'Working directory; default: root of repository' + required: false + +runs: + using: 'node20' + main: '../dist/actions/upload-code-site/index.js'