diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d15b33a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +[*] +charset = utf-8-bom +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a00bbb2 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,44 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..405fa32 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +Thanks for your interest in contributing to PSAppDeployToolkit + +Whether it's a bug report, new feature, correction, or additional documentation, your feedback and contributions are appreciated. + +Please read through this document before submitting any issues or pull requests to ensure all the necessary information is provided to effectively respond to your bug report or contribution. + +Please note there is a code of conduct, please follow it in all your interactions with the project. + +## Reporting Bugs / Submitting Feature Requests + +When filing an issue, please check [existing open](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues), or [recently closed](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues?q=is%3Aissue+is%3Aclosed), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of PSADT that is being used (found in the AppDeployToolkitMain.ps1) +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + +## Contributing via Pull Requests + +Contributions via pull requests are much appreciated. Before sending a pull request, please ensure that: + +1. You are working against the latest source on the *develop* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - I'd hate for your time to be wasted. + +To send a pull request, please: + +1. Fork the repository. +2. Checkout the *develop* branch +3. Modify the source; please focus on the specific change you are contributing. Please refrain from code styling changes, it will be harder to focus on your change. +4. Ensure local tests pass. +5. Commit to your fork using clear commit messages. +6. Send a pull request, answering any default questions in the pull request interface. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + +## Finding contributions to work on + +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) issues is a great place to start. + +## Code of Conduct + +This project has a [Code of Conduct](CODE_OF_CONDUCT.md). + +## Licensing + +See the [LICENSE](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/blob/main/COPYING.Lesser) file for our project's licensing. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..32f97cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,81 @@ +name: "🕷️ Bug report" +description: Report errors or unexpected behavior +labels: [ bug, needs-triage ] +title: "[Bug] " + + +body: +- type: checkboxes + attributes: + label: Prerequisites + options: + - label: Ensure you write a short, descriptive title after [Bug] above. + required: true + - label: Make sure to [search for any existing issues](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues) before filing a new one. + required: true + - label: Verify you are able to reproduce the issue with the [latest released version](https://www.github.com/psappdeploytoolkit/psappdeploytoolkit.tools/releases/latest) + required: true + +- type: input + id: psadttools-version + attributes: + label: PSAppDeployToolkit.Tools version + placeholder: 0.0.1 + description: The version of PSAppDeployToolkit you are using + validations: + required: true + +- type: input + id: psadt-version + attributes: + label: PSAppDeployToolkit version + placeholder: 4.0.0 + description: The version of PSAppDeployToolkit you are using + validations: + required: false + +- type: textarea + id: description + attributes: + label: Describe the bug + description: Please enter a detailed description of the bug you are seeing. Include any error messages, screenshots, or other relevant information. If a PSADT log file was created, please also attach it below. + validations: + required: true + +- type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: Please provide any required setup and steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + value: | + 1. + 2. + 3. + validations: + required: true + +- type: textarea + id: environment-data + attributes: + label: Environment data + description: | + The following script will gather environment details that will help with triage and investigation of the issue. + Please run the script in the PowerShell session where you ran into the issue, and paste the verbatim output below. + ```powershell + Get-ComputerInfo -Property @('OsName','OSDisplayVersion','OsOperatingSystemSKU','OSArchitecture','WindowsVersion','WindowsProductName','WindowsBuildLabEx','OsLanguage','OsMuiLanguages','KeyboardLayout','TimeZone','HyperVisorPresent','CsPartOfDomain','CsPCSystemType'); dotnet --info + ``` + render: console + placeholder: | + OsName ... + OSDisplayVersion ... + OsOperatingSystemSKU ... + OsArchitecture ... + WindowsVersion .... + WindowsProductName ... + WindowsBuildLabEx ... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..dacec47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: Chat with the community + url: https://discord.com/channels/618712310185197588/627204361545842688 + about: PSAppDeployToolkit channel on Discord (WinAdmins) + - name: Join in the discussion + url: https://discourse.psappdeploytoolkit.com + about: PSAppDeployToolkit Discourse Forums diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..ca3745b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,20 @@ +name: 🚀 Feature Request +description: Suggest a new feature or improvement (this does not mean you have to implement it) +labels: ["feature-request", "needs-triage"] +title: "[Feature] " + +body: +- type: textarea + attributes: + label: Summary of the new feature / enhancement + description: > + A clear and concise description of what the problem is that the new feature would solve. Try formulating it in a user story style (if applicable). + placeholder: "'As a user I want X so that Y...' with X being the being the action and Y being the value of the action." + validations: + required: true + +- type: textarea + attributes: + label: Proposed technical implementation details (optional) + placeholder: > + A clear and concise description of what you want to happen. Consider providing an example PowerShell experience with expected result. diff --git a/.github/PULL_REQUEST_TEMPLATE/adr_template.md b/.github/PULL_REQUEST_TEMPLATE/adr_template.md new file mode 100644 index 0000000..e424723 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/adr_template.md @@ -0,0 +1,23 @@ +# [ADR] - Architectural Decision Record + +Please include a summary of any changes. Please also include relevant motivation and context. + +## Status + +What is the status, such as proposed, accepted, rejected, deprecated, superseded, etc.? + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Consequences + +What becomes easier or more difficult to do because of this change? + +## Notes + +Any additional notes? diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..e0ef5ef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,30 @@ +# Pull Request + +## Description + +Please include a summary of any changes. Please also include relevant motivation and context. + +Fixes: #12345 + +Fixes: + +## Type of change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] General code cleanup (non-breaking change which improves readability) + +## Checklist + +- [ ] I am pulling to the **develop** branch +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] I have tested my changes to prove my fix is effective +- [ ] I have tested that the module can build following my changes +- [ ] I have made sure that any script file-encoding is set to UTF8 with BOM, i.e. unchanged. + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..ccfeeaa --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,45 @@ +# Responsible Security Disclosure + +## Introduction + +Thank you for your interest in PSAppDeployToolkit. We take the security of our software seriously and appreciate the efforts of security researchers in identifying and responsibly disclosing vulnerabilities. This document outlines our responsible disclosure policy and provides guidelines for reporting security vulnerabilities. + +## Reporting a Vulnerability + +If you believe you have discovered a security vulnerability in PSAppDeployToolkit, we encourage you to report it to us as soon as possible. To report a vulnerability, please follow these steps: + +1. Send an email to [security@psappdeploytoolkit.com](mailto:security@psappdeploytoolkit.com) with a detailed description of the vulnerability. +2. Include any relevant information, such as the affected version(s) of the software, steps to reproduce the vulnerability, and any proof-of-concept code or screenshots. +3. Provide your contact information (name, email address) so that we can acknowledge your report and keep you updated on the progress of the fix. + +## Responsible Disclosure Guidelines + +To ensure the safety and privacy of our users, we kindly request that you adhere to the following guidelines when reporting a vulnerability: + +- Do not exploit the vulnerability beyond what is necessary to demonstrate the security issue. +- Do not disclose the vulnerability to others until it has been resolved by the project maintainers. +- Do not perform any actions that could negatively impact the availability or integrity of the software or its users' data. + +## Our Commitment + +Upon receiving a vulnerability report, we will: + +- Acknowledge the receipt of your report within 3 business days. +- Investigate and validate the reported vulnerability. +- Work towards addressing the vulnerability in a timely manner. +- Keep you informed of the progress and resolution of the vulnerability. + +## Recognition + +We value the contributions of security researchers and may recognize their efforts, subject to their consent and our discretion. If you would like to be acknowledged for your responsible disclosure, please let us know in your initial report. + +## Legal Considerations + +We will not take any legal action against security researchers who act in good faith and adhere to this responsible disclosure policy. We request that you do not violate any laws or breach any agreements in your research activities. + +## Conclusion + +By following these guidelines, you are helping us ensure the security and privacy of our software and its users. We appreciate your cooperation and responsible approach to vulnerability disclosure. + +Thank you, +The PSAppDeployToolkit Team diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..a3e2daf --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,7 @@ +# PSAppDeployToolkit Support + +If you have any problems, please consult the [PSAppDeployToolkit GitHub Issues](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues) page. + +If you do not see your problem captured, please file a [new issue](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/issues/new/choose) and follow the provided template. + +If you know how to fix the issue, feel free to send a pull request our way. (The [Contribution Guide](https://github.com/psappdeploytoolkit/psappdeploytoolkit.tools/tree/main/.github/CONTRIBUTING.md) apply to that pull request, you may want to give it a read!) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..dde2bcd --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,61 @@ +# Configuration for Release Drafter: https://github.com/toolmantim/release-drafter +name-template: $NEXT_PATCH_VERSION +tag-template: $NEXT_PATCH_VERSION + +# Emoji reference: https://gitmoji.carloscuesta.me/ +categories: + - title: 💥 Breaking changes + labels: + - breaking + - title: 🚀 New Features and enhancements + labels: + - feature + - title: 🐛 Bug fixes + labels: + - bug + - title: 📦 Dependencies + labels: + - dependencies + collapse-after: 15 + - title: 📝 Documentation + labels: + - documentation + - title: 🌐 Localization + labels: + - localization + - title: 👻 Maintenance + labels: + - chore + - maintenance + - title: 🚦 Tests + labels: + - test + - title: ✍ Other changes + +exclude-labels: + - skip-changelog + - invalid + +autolabeler: + - label: "documentation" + files: + - "*.md" + branch: + - '/docs{0,1}\/.+/' + - label: "bug" + title: + - '/bug\/.+/' + branch: + - '/fix\/.+/' + - label: "feature" + title: + - '/feature\/.+/' + branch: + - '/feature\/.+/' + +template: | + ## What's Changed + + + + $CHANGES diff --git a/.github/workflows/wf_Windows.yml b/.github/workflows/wf_Windows.yml new file mode 100644 index 0000000..f3035ae --- /dev/null +++ b/.github/workflows/wf_Windows.yml @@ -0,0 +1,81 @@ +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell +# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions +# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-powershell +# https://github.com/actions/upload-artifact#where-does-the-upload-go +name: PSAppDeployToolkit.Tools-Windows-PowerShell +on: + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + push: + paths-ignore: + - '**.md' + - 'docs/**' +permissions: + id-token: write # Require write permission to Fetch an OIDC token. + contents: read +jobs: + test: + name: Run Tests + runs-on: windows-latest + strategy: + fail-fast: false + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Display the path + shell: powershell + run: echo ${env:PATH} + - name: Version Display + shell: powershell + run: $PSVersionTable + # uncomment below to explore what modules/variables/env variables are available in the build image + # - name: Modules and Variables Display + # shell: powershell + # run: Get-Module -ListAvailable; (Get-Variable).GetEnumerator() | Sort-Object Name | Out-String; (Get-ChildItem env:*).GetEnumerator() | Sort-Object Name | Out-String + - name: NuGet Latest + shell: powershell + run: Install-PackageProvider -Name "NuGet" -Confirm:$false -Force -Verbose + - name: PowerShellGet Latest + shell: powershell + run: Install-Module -Name PowerShellGet -Repository PSGallery -Force + - name: Bootstrap + shell: powershell + run: ./actions_bootstrap.ps1 + - name: Install AzureSignTool + run: dotnet tool install --global azuresigntool + - name: Azure Login + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Test and Build + shell: powershell + run: Invoke-Build -File .\src\PSAppDeployToolkit.Tools.build.ps1 + - name: Upload pester results + uses: actions/upload-artifact@v4 + with: + name: pester-results + path: .\src\Artifacts\testOutput + if-no-files-found: error + overwrite: true + - name: Upload code coverage results + uses: actions/upload-artifact@v4 + with: + name: cc-results + path: .\src\Artifacts\ccReport + if-no-files-found: error + overwrite: true + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: PSAppDeployToolkit.Tools + path: .\src\Artifacts\Module + if-no-files-found: error + overwrite: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..def684f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +// PSAppDeployToolkit default extension recommendations +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-vscode.PowerShell", + "ryanluker.vscode-coverage-gutters", + "DavidAnson.vscode-markdownlint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..56e7f1d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +// PSAppDeployToolkit default settings to ensure consistent code formatting and file encoding +{ + //-------- Indentation configuration -------- + "editor.detectIndentation": true, + "editor.formatOnPaste": true, + "editor.insertSpaces": false, + "editor.tabSize": 4, + + //-------- Files configuration -------- + "files.autoGuessEncoding": true, + "files.encoding": "utf8bom", + "files.insertFinalNewline": true, + + // When enabled, will trim trailing whitespace when you save a file. + "files.trimTrailingWhitespace": true, + // specifies the location of the explicitly ScriptAnalyzer settings file + "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", + // specifies the PowerShell coding style used in this project (https://github.com/PoshCode/PowerShellPracticeAndStyle/issues/81) + "powershell.codeFormatting.preset": "Allman", + "powershell.codeFormatting.alignPropertyValuePairs": false, + "powershell.powerShellDefaultVersion": "Windows PowerShell (x64)", + "dotnet.preferCSharpExtension": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0040c07 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,269 @@ +// Portions of this file were generated using New-VSCodeTask.ps1 +// Modify the build script or tasks-merge.json and recreate. +// https://code.visualstudio.com/docs/editor/tasks +// Available variables which can be used inside of strings. +// https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables +// ${workspaceFolder}: path of the folder opened in VS Code +// ${workspaceFolderBasename} - name of the folder opened in VS Code without any slashes (/) +// ${file}: current opened file +// ${fileWorkspaceFolder} - current opened file's workspace folder +// ${relativeFile}: the current opened file relative to workspaceFolder +// ${relativeFileDirname}: current opened file's dirname relative to workspaceFolder +// ${fileBasename}: current opened file's basename +// ${fileBasenameNoExtension} - current opened file's basename with no file extension +// ${fileDirname}: current opened file's dirname +// ${fileExtname}: current opened file's extension +// ${cwd}: the current working directory of the spawned process +// ${lineNumber} - current selected line number in the active file +// ${selectedText} - current selected text in the active file +// ${execPath} - path to the running VS Code executable +// ${defaultBuildTask} - name of the default build task +// ${pathSeparator} - character used by the operating system to separate components in file paths +{ + "version": "2.0.0", + "windows": { + "options": { + "shell": { + // "executable": "powershell.exe", + "executable": "pwsh.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] + } + } + }, + "linux": { + "options": { + "shell": { + "executable": "/usr/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "osx": { + "options": { + "shell": { + "executable": "/usr/local/bin/pwsh", + "args": [ + "-NoProfile", + "-Command" + ] + } + } + }, + "tasks": [ + { + "label": ".", + "type": "shell", + "command": "$PSVersionTable;Invoke-Build -Task . -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "TestLocal", + "type": "shell", + "command": "Invoke-Build -Task TestLocal -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "HelpLocal", + "type": "shell", + "command": "Invoke-Build -Task HelpLocal -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "Clean", + "type": "shell", + "command": "Invoke-Build -Task Clean -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "ValidateRequirements", + "type": "shell", + "command": "Invoke-Build -Task ValidateRequirements -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "Analyze", + "type": "shell", + "problemMatcher": "$msCompile", + "command": "Invoke-Build -Task Analyze -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "AnalyzeTests", + "type": "shell", + "command": "Invoke-Build -Task AnalyzeTests -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "FormattingCheck", + "type": "shell", + "command": "Invoke-Build -Task FormattingCheck -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "Test", + "type": "shell", + "command": "Invoke-Build -Task Test -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "DevCC", + "type": "shell", + "command": "Invoke-Build -Task DevCC -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "Build", + "type": "shell", + "command": "Invoke-Build -Task Build -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "none" + }, + { + "label": "BuildNoIntegration", + "type": "shell", + "command": "Invoke-Build -Task BuildNoIntegration -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "build" + }, + { + "label": "IntegrationTest", + "type": "shell", + "command": "Invoke-Build -Task IntegrationTest -File '${workspaceFolder}/src/${workspaceFolderBasename}.build.ps1'", + "problemMatcher": "$msCompile", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "PesterTest", + "type": "shell", + "command": "Invoke-Pester '${workspaceFolder}/src/Tests/Unit' -Output Detailed", + "problemMatcher": "$pester", + "group": "test" + }, + { + "label": "Pester-Single-Coverage", + "type": "shell", + "command": "Import-Module -Name '${workspaceFolder}/src/${workspaceFolderBasename}/${workspaceFolderBasename}.psm1';$pesterConfig = New-PesterConfiguration;$pesterConfig.run.Path = '${workspaceFolder}/src/Tests/Unit/*/${input:functionName}.Tests.ps1';$pesterConfig.CodeCoverage.Enabled = $true;$pesterConfig.CodeCoverage.Path = '${workspaceFolder}/src/${workspaceFolderBasename}/*/${input:functionName}.ps1';Invoke-Pester -Configuration $pesterConfig", + "problemMatcher": "$pester", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "Pester-Single-Detailed", + "type": "shell", + "command": "Import-Module -Name '${workspaceFolder}/src/${workspaceFolderBasename}/${workspaceFolderBasename}.psm1';Invoke-Pester '${workspaceFolder}/src/Tests/Unit/*/${input:functionName}.Tests.ps1' -Output Detailed", + "problemMatcher": "$pester", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "DevCC-Single", + "type": "shell", + "command": "Import-Module -Name '${workspaceFolder}/src/${workspaceFolderBasename}/${workspaceFolderBasename}.psm1';$pesterConfig = New-PesterConfiguration;$pesterConfig.run.Path = '${workspaceFolder}/src/Tests/Unit/*/${input:functionName}.Tests.ps1';$pesterConfig.CodeCoverage.Enabled = $true;$pesterConfig.CodeCoverage.Path = '${workspaceFolder}/src/${workspaceFolderBasename}/*/${input:functionName}.ps1';$pesterConfig.CodeCoverage.OutputPath = '${workspaceFolder}/cov.xml';$pesterConfig.CodeCoverage.OutputFormat = 'CoverageGutters';Invoke-Pester -Configuration $pesterConfig", + "problemMatcher": "$pester", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + }, + { + "label": "Integration-Single-Detailed", + "type": "shell", + "command": "Import-Module -Name '${workspaceFolder}/src/${workspaceFolderBasename}/${workspaceFolderBasename}.psm1';Invoke-Pester '${workspaceFolder}/src/Tests/Integration/*/${input:functionName}.Tests.ps1' -Output Detailed", + "problemMatcher": "$pester", + "presentation": { + "echo": false, + "showReuseMessage": false + }, + "group": "test" + } + ], + "inputs": [ + { + "type": "promptString", + "id": "functionName", + "description": "Name of PowerShell function you want to test" + } + ] +} diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/COPYING.Lesser b/COPYING.Lesser new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/COPYING.Lesser @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0be79d --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# ![AppDeployToolkitLogo](https://github.com/user-attachments/assets/7766bcb3-fa87-496e-9b19-7a8e194dcd97) + +## Enterprise App Packaging, Simplified. + +PSAppDeployToolkit.Tools is a companion module for [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit) that provides tools and functions useful during the application packaging process. Having this separate allows for a separate release schedule and also reduces the file size of the module that is required to be delivered to endpoints to handle software deployments. + +### Features + +- **Test-ADTCompatibility** - Test your PSAppDeployToolkit v3 scripts to get a full report on which functions and variables have changed in v4. +- **Convert-ADTDeployment** - Convert a PSAppDeployToolkit v3 script or an entire package folder to v4 standards. + +## Getting Started + +Install the module from the PowerShell Gallery: + +```powershell +Install-Module PSAppDeployToolkit.Tools -Scope CurrentUser +``` + +Example command usage: + +```powershell +Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Grid +``` + +This example analyzes Deploy-Application.ps1 and outputs the results as a grid view. + +```powershell +Convert-ADTDeployment -Path .\Deploy-Application.ps1 +``` + +This example converts Deploy-Application.ps1 into Invoke-AppDeployToolkit.ps1 in the same folder. + +```powershell +Convert-ADTDeployment -Path .\PackageFolder +``` + +This example converts PackageFolder into PackageFolder_Converted in the same folder. + +### PSAppDeployToolkit Links + +-> [Homepage](https://psappdeploytoolkit.com) +-> [Documentation](https://psappdeploytoolkit.com/docs) +-> [Function & Variable References](https://psappdeploytoolkit.com/docs/reference) +-> [Download Latest Release](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit/releases) +-> [News](https://psappdeploytoolkit.com/blog) + +### Community Links + +-> [Discourse Forum](https://discourse.psappdeploytoolkit.com/) +-> [Discord Chat](https://discord.com/channels/618712310185197588/627204361545842688) +-> [Reddit](https://reddit.com/r/psadt) + +## License + +The PowerShell App Deployment Tool is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 new file mode 100644 index 0000000..a7ece92 --- /dev/null +++ b/actions_bootstrap.ps1 @@ -0,0 +1,80 @@ +# Bootstrap dependencies + +# https://docs.microsoft.com/powershell/module/packagemanagement/get-packageprovider +Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null + +# https://docs.microsoft.com/powershell/module/powershellget/set-psrepository +Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + +# List of PowerShell Modules required for the build +$modulesToInstall = New-Object System.Collections.Generic.List[object] +# https://github.com/PSAppDeployToolkit/PSAppDeployToolkit +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'PSAppDeployToolkit' + ModuleVersion = '4.0.2' + })) +# https://github.com/pester/Pester +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'Pester' + ModuleVersion = '5.6.1' + })) +# https://github.com/nightroman/Invoke-Build +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'InvokeBuild' + ModuleVersion = '5.11.3' + })) +# https://github.com/PowerShell/PSScriptAnalyzer +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'PSScriptAnalyzer' + ModuleVersion = '1.23.0' + })) +# https://github.com/PowerShell/platyPS +# older version used due to: https://github.com/PowerShell/platyPS/issues/457 +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'platyPS' + ModuleVersion = '0.12.0' + })) +# https://github.com/alt3/Docusaurus.Powershell +# Needed for our website documentation +[void]$modulesToInstall.Add(([PSCustomObject]@{ + ModuleName = 'Alt3.Docusaurus.Powershell' + ModuleVersion = '1.0.37' + })) + + + +'Installing PowerShell Modules' +foreach ($module in $modulesToInstall) { + $installSplat = @{ + Name = $module.ModuleName + RequiredVersion = $module.ModuleVersion + Repository = 'PSGallery' + SkipPublisherCheck = $true + Force = $true + Scope = 'CurrentUser' + ErrorAction = 'Stop' + } + try { + if ($module.ModuleName -eq 'Pester' -and ($IsWindows -or $PSVersionTable.PSVersion -le [version]'5.1')) { + # special case for Pester certificate mismatch with older Pester versions - https://github.com/pester/Pester/issues/2389 + # this only affects windows builds + Install-Module @installSplat -SkipPublisherCheck + } + elseif ($module.ModuleName -eq 'PSAppDeployToolkit' ) { + # special case for Pester certificate mismatch with older Pester versions - https://github.com/pester/Pester/issues/2389 + # this only affects windows builds + Install-Module @installSplat -AllowPreRelease + } + else { + Install-Module @installSplat + } + Import-Module -Name $module.ModuleName -ErrorAction Stop + ' - Successfully installed {0}' -f $module.ModuleName + } + catch { + $message = 'Failed to install {0}' -f $module.ModuleName + " - $message" + throw + } +} + diff --git a/docs/Convert-ADTDeployment.mdx b/docs/Convert-ADTDeployment.mdx new file mode 100644 index 0000000..6dfe374 --- /dev/null +++ b/docs/Convert-ADTDeployment.mdx @@ -0,0 +1,163 @@ +--- +id: Convert-ADTDeployment +title: Convert-ADTDeployment +hide_title: false +hide_table_of_contents: false +--- + +## SYNOPSIS + +Converts either a Deploy-Application.ps1 script, or a full application package to use the new folder structure and syntax required by PSAppDeployToolkit v4. + +## SYNTAX + +```powershell +Convert-ADTDeployment [-Path] [[-Destination] ] [-Show] [-Force] [-PassThru] + [] +``` + +## DESCRIPTION + +The variables and main code blocks are updated to the new syntax via PSScriptAnalyzer, then transferred to a fresh Invoke-AppDeployToolkit.ps1 script. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Convert-ADTDeployment -Path .\Deploy-Application.ps1 +``` + +This example converts Deploy-Application.ps1 into Invoke-AppDeployToolkit.ps1 in the same folder. + +### EXAMPLE 2 + +```powershell +Convert-ADTDeployment -Path .\PackageFolder +``` + +This example converts PackageFolder into PackageFolder_Converted in the same folder. + +### EXAMPLE 3 + +```powershell +$ConvertedPackages = Get-ChildItem -Directory | Convert-ADTDeployment -Destination C:\Temp\ConvertedPackages -Force -PassThru +``` + +Get all folders in the current directory and convert them to v4 packages in C:\Temp\ConvertedPackages, overwriting existing files and storing the new package info in $ConvertedPackages. + +## PARAMETERS + +### -Path + +Path to the Deploy-Application.ps1 file or folder to analyze. +If a folder is specified, it must contain the Deploy-Application.ps1 script and the AppDeployToolkit folder. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: FullName + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Destination + +Path to the output file or folder. +If not specified it will default to creating either a Invoke-AppDeployToolkit.ps1 file or FolderName_Converted folder under the parent folder of the supplied Path value. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: (Split-Path -Path $Path -Parent) +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Show + +Opens the newly created output in Windows Explorer. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrite the output path if it already exists. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru + +Pass the output file or folder to the pipeline. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. +For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +### You can pipe script files or folders to this function. +## OUTPUTS + +### System.IO.FileInfo / System.IO.DirectoryInfo + +### Returns the file or folder to the pipeline if -PassThru is specified. +## NOTES +An active ADT session is NOT required to use this function. +Requires PSScriptAnalyzer module 1.23.0 or later. +To install: + +Install-Module -Name PSScriptAnalyzer -Scope CurrentUser +Install-Module -Name PSScriptAnalyzer -Scope AllUsers + +Tags: psadt +Website: https://psappdeploytoolkit.com +Copyright: (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). +License: https://opensource.org/license/lgpl-3-0 + +## RELATED LINKS + +[https://psappdeploytoolkit.com](https://psappdeploytoolkit.com) diff --git a/docs/Test-ADTCompatibility.mdx b/docs/Test-ADTCompatibility.mdx new file mode 100644 index 0000000..2f6318f --- /dev/null +++ b/docs/Test-ADTCompatibility.mdx @@ -0,0 +1,117 @@ +--- +id: Test-ADTCompatibility +title: Test-ADTCompatibility +hide_title: false +hide_table_of_contents: false +--- + +## SYNOPSIS + +Tests a PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1 for any deprecated v3.x command or variable usage. + +## SYNTAX + +```powershell +Test-ADTCompatibility [-FilePath] [[-Format] ] [] +``` + +## DESCRIPTION + +The Test-ADTCompatibility function run custom PSScriptAnalyzer rules against the input file and output any detected issues. +The results can be output in a variety of formats. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 +``` + +This example analyzes Deploy-Application.ps1 and outputs the results. + +### EXAMPLE 2 + +```powershell +Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Table +``` + +This example analyzes Deploy-Application.ps1 and outputs the results as a table. + +### EXAMPLE 3 + +```powershell +Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Grid +``` + +This example analyzes Deploy-Application.ps1 and outputs the results as a grid view. + +## PARAMETERS + +### -FilePath + +Path to the .ps1 file to analyze. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: FullName + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Format + +Specifies the output format. +The acceptable values for this parameter are: Raw, Table, Grid. +The default value is Raw, which outputs the raw DiagnosticRecord objects from PSScriptAnalyzer. +Table outputs just the line numbers and messages as a table. +Grid outputs the line numbers and messages in a graphical window. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: Raw +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. +For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +### You can pipe script files to this function. +## OUTPUTS + +### Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord + +### Returns the standard output from Invoke-ScriptAnalyzer. +## NOTES +An active ADT session is NOT required to use this function. +Requires PSScriptAnalyzer module 1.23.0 or later. +To install: + +Install-Module -Name PSScriptAnalyzer -Scope CurrentUser +Install-Module -Name PSScriptAnalyzer -Scope AllUsers + +Tags: psadt +Website: https://psappdeploytoolkit.com +Copyright: (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). +License: https://opensource.org/license/lgpl-3-0 + +## RELATED LINKS + +[https://psappdeploytoolkit.com](https://psappdeploytoolkit.com) diff --git a/src/PSAppDeployToolkit.Tools.build.ps1 b/src/PSAppDeployToolkit.Tools.build.ps1 new file mode 100644 index 0000000..ba6ce13 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools.build.ps1 @@ -0,0 +1,567 @@ +<# +.SYNOPSIS + An Invoke-Build Build file. + +.DESCRIPTION + Build steps can include: + - ValidateRequirements + - ImportModuleManifest + - Clean + - Analyze + - FormattingCheck + - Test + - DevCC + - CreateHelpStart + - Build + - IntegrationTest + +.EXAMPLE + Invoke-Build + + This will perform the default build Add-BuildTasks: see below for the default Add-BuildTask execution. + +.EXAMPLE + Invoke-Build -Add-BuildTask Analyze,Test + + This will perform only the Analyze and Test Add-BuildTasks. + +.NOTES + https://github.com/nightroman/Invoke-Build + https://github.com/nightroman/Invoke-Build/wiki/Build-Scripts-Guidelines + If using VSCode you can use the generated tasks.json to execute the various tasks in this build file. + Ctrl + P | then type task (add space) - you will then be presented with a list of available tasks to run + The 'InstallDependencies' Add-BuildTask isn't present here. + Module dependencies are installed at a previous step in the pipeline. + If your manifest has module dependencies include all required modules in your CI/CD bootstrap file: + AWS - install_modules.ps1 + Azure - actions_bootstrap.ps1 + GitHub Actions - actions_bootstrap.ps1 + AppVeyor - actions_bootstrap.ps1 +#> + +# Default variables. +$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop +$ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue +Set-StrictMode -Version 3 +$ModuleName = [System.Text.RegularExpressions.Regex]::Match((Get-Item $BuildFile).Name, '^(.*)\.build\.ps1$').Groups[1].Value +$BuildScriptPath = $MyInvocation.MyCommand.Path +[System.Version]$requiredPSVersion = '5.1.0' + +# Default build. +$str = @() +$str = 'Clean', 'ValidateRequirements', 'ImportModuleManifest' +$str += 'FormattingCheck' +$str += 'Analyze', 'Test' +$str += 'CreateHelpStart' +$str2 = $str +$str2 += 'Build' +$str += 'Build', 'IntegrationTest' +Add-BuildTask -Name . -Jobs $str + +# Local testing build process. +Add-BuildTask TestLocal Clean, ImportModuleManifest, Analyze, Test + +# Local help file creation process. +Add-BuildTask HelpLocal Clean, ImportModuleManifest, CreateHelpStart + +# Full build sans integration tests. +Add-BuildTask BuildNoIntegration -Jobs $str2 + +# Pre-build variables to be used by other portions of the script. +Enter-Build { + # Set up required paths. + $Script:RepoRootPath = Split-Path -Path $BuildRoot -Parent + $Script:ModuleSourcePath = Join-Path -Path $BuildRoot -ChildPath $Script:ModuleName + $Script:ModuleFiles = Join-Path -Path $Script:ModuleSourcePath -ChildPath '*' + $Script:ModuleManifestFile = Join-Path -Path $Script:ModuleSourcePath -ChildPath "$($Script:ModuleName).psd1" + $Script:TestsPath = Join-Path -Path $BuildRoot -ChildPath 'Tests' + $Script:UnitTestsPath = Join-Path -Path $Script:TestsPath -ChildPath 'Unit' + $Script:IntegrationTestsPath = Join-Path -Path $Script:TestsPath -ChildPath 'Integration' + $Script:ArtifactsPath = Join-Path -Path $BuildRoot -ChildPath 'Artifacts' + $Script:MarkdownExportPath = "$Script:ArtifactsPath\platyPS\" + $Script:DocusaurusExportPath = "$Script:ArtifactsPath\Docusaurus\" + $Script:BuildModuleRoot = Join-Path -Path $Script:ArtifactsPath -ChildPath "Module\$Script:ModuleName" + $Script:BuildModuleRootFile = Join-Path -Path $Script:BuildModuleRoot -ChildPath "$($Script:ModuleName).psm1" + + # Import this module's manifest via the language parser. This allows us to test with potential extra variables that are permitted in manifests. + # https://github.com/PowerShell/PowerShell/blob/7ca7aae1d13d19e38c7c26260758f474cb9bef7f/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs#L509-L512 + $manifestInfo = [System.Management.Automation.Language.Parser]::ParseFile($Script:ModuleManifestFile, [ref]$null, [ref]$null).GetScriptBlock() + $manifestInfo.CheckRestrictedLanguage([System.String[]]$null, [System.String[]]('PSEdition'), $true); $manifestInfo = $manifestInfo.InvokeReturnAsIs() + $Script:ModuleVersion = $manifestInfo.ModuleVersion + $Script:ModuleDescription = $manifestInfo.Description + $Script:FunctionsToExport = $manifestInfo.FunctionsToExport + + # Ensure our builds fail until if below a minimum defined code test coverage threshold. + $Script:coverageThreshold = 0 + [System.Version]$Script:MinPesterVersion = '5.2.2' + [System.Version]$Script:MaxPesterVersion = '5.99.99' + $Script:testOutputFormat = 'NUnitXML' +} + +# Define headers as separator, task path, synopsis, and location, e.g. for Ctrl+Click in VSCode. +# Also change the default color to Green. If you need task start times, use `$Task.Started`. +Set-BuildHeader { + param + ( + $Path + ) + + Write-Build DarkMagenta ('=' * 79) + Write-Build DarkGray "Task $Path : $(Get-BuildSynopsis $Task)" + Write-Build DarkGray "At $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" + Write-Build Yellow "Manifest File: $Script:ModuleManifestFile" + Write-Build Yellow "Manifest Version: $($manifestInfo.ModuleVersion)" +} + +# Define footers similar to default but change the color to DarkGray. +Set-BuildFooter { + param + ( + $Path + ) + + Write-Build DarkGray "Done $Path, $($Task.Elapsed)" +} + +# Synopsis: Validate system requirements are met. +Add-BuildTask ValidateRequirements { + Write-Build White " Verifying at least PowerShell $Script:requiredPSVersion..." + Assert-Build ($PSVersionTable.PSVersion -ge $Script:requiredPSVersion) "At least Powershell $Script:requiredPSVersion is required for this build to function properly" + Write-Build Green ' ...Verification Complete!' +} + +# Synopsis: Import the current module manifest file for processing. +Add-BuildTask TestModuleManifest -Before ImportModuleManifest { + Write-Build White ' Running module manifest tests...' + Assert-Build (Test-Path $Script:ModuleManifestFile) 'Unable to locate the module manifest file.' + Assert-Build (Get-ChildItem $Script:ModuleManifestFile | Test-ModuleManifest -ErrorAction Ignore) 'Module Manifest test did not pass verification.' + Write-Build Green ' ...Module Manifest Verification Complete!' +} + +# Synopsis: Load the module project. +Add-BuildTask ImportModuleManifest { + Write-Build White ' Attempting to load the project module.' + $Script:moduleCommandTable = & (Import-Module $Script:ModuleManifestFile -Force -PassThru) { $CommandTable } + Write-Build Green " ...$Script:ModuleName imported successfully" +} + +# Synopsis: Clean and reset Artifacts directory. +Add-BuildTask Clean { + Write-Build White ' Clean up our Artifacts directory...' + $null = Remove-Item $Script:ArtifactsPath -Force -Recurse -ErrorAction Ignore + $null = New-Item $Script:ArtifactsPath -ItemType Directory + Write-Build Green ' ...Clean Complete!' +} + +# Synopsis: Analyze scripts to verify if they adhere to desired coding format (Stroustrup / OTBS / Allman). +Add-BuildTask FormattingCheck { + Write-Build White ' Performing script formatting checks...' + if (($scriptAnalyzerResults = $Script:BuildScriptPath, $Script:ModuleSourcePath | Invoke-ScriptAnalyzer -Setting CodeFormattingAllman -ExcludeRule PSAlignAssignmentStatement -Recurse -Fix:($env:GITHUB_ACTIONS -ne 'true') -Verbose:$false)) + { + $scriptAnalyzerResults | Format-Table + throw ' PSScriptAnalyzer code formatting check did not adhere to {0} standards' -f $scriptAnalyzerParams.Setting + } + Write-Build Green ' ...Formatting Analyze Complete!' +} + +# Synopsis: Invokes PSScriptAnalyzer against the Module source path. +Add-BuildTask Analyze { + Write-Build White ' Performing Module ScriptAnalyzer checks...' + if (($scriptAnalyzerResults = $Script:BuildScriptPath, $Script:ModuleSourcePath | Invoke-ScriptAnalyzer -ExcludeRule PSUseShouldProcessForStateChangingFunctions -Recurse -Verbose:$false)) + { + $scriptAnalyzerResults | Format-Table + throw ' One or more PSScriptAnalyzer errors/warnings where found.' + } + Write-Build Green ' ...Module Analyze Complete!' +} + +# Synopsis: Invokes Script Analyzer against the Tests path if it exists. +Add-BuildTask AnalyzeTests -After Analyze { + if (Test-Path -Path $Script:TestsPath) + { + Write-Build White ' Performing Test ScriptAnalyzer checks...' + if (($scriptAnalyzerResults = Invoke-ScriptAnalyzer -Path $Script:TestsPath -ExcludeRule PSUseDeclaredVarsMoreThanAssignments -Recurse -Verbose:$false)) + { + $scriptAnalyzerResults | Format-Table + throw ' One or more PSScriptAnalyzer errors/warnings where found.' + } + Write-Build Green ' ...Test Analyze Complete!' + } +} + +# Synopsis: Invokes all Pester Unit Tests in the Tests\Unit folder (if it exists). +Add-BuildTask Test { + # (Re-)Import Pester module. + Write-Build White " Importing desired Pester version. Min: $Script:MinPesterVersion Max: $Script:MaxPesterVersion" + Remove-Module -Name Pester -Force -ErrorAction Ignore # there are instances where some containers have Pester already in the session + Import-Module -Name Pester -MinimumVersion $Script:MinPesterVersion -MaximumVersion $Script:MaxPesterVersion + + # Set up required paths. + $codeCovPath = "$Script:ArtifactsPath\ccReport\" + if (!(Test-Path $codeCovPath)) + { + New-Item -Path $codeCovPath -ItemType Directory | Out-Null + } + $testOutPutPath = "$Script:ArtifactsPath\testOutput\" + if (!(Test-Path $testOutPutPath)) + { + New-Item -Path $testOutPutPath -ItemType Directory | Out-Null + } + + # Perform unit testing. + if (Test-Path -Path $Script:UnitTestsPath) + { + # Perform tests. + Write-Build White ' Performing Pester Unit Tests...' + $pesterConfiguration = New-PesterConfiguration + $pesterConfiguration.run.Path = $Script:UnitTestsPath + $pesterConfiguration.Run.PassThru = $true + $pesterConfiguration.Run.Exit = $false + $pesterConfiguration.CodeCoverage.Enabled = $true + $pesterConfiguration.CodeCoverage.Path = "..\..\..\src\$Script:ModuleName\*\*.ps1" + $pesterConfiguration.CodeCoverage.CoveragePercentTarget = $Script:coverageThreshold + $pesterConfiguration.CodeCoverage.OutputPath = "$codeCovPath\CodeCoverage.xml" + $pesterConfiguration.CodeCoverage.OutputFormat = 'JaCoCo' + $pesterConfiguration.TestResult.Enabled = $true + $pesterConfiguration.TestResult.OutputPath = "$testOutPutPath\PesterTests.xml" + $pesterConfiguration.TestResult.OutputFormat = $Script:testOutputFormat + $pesterConfiguration.Output.Verbosity = 'Detailed' + $testResults = Invoke-Pester -Configuration $pesterConfiguration + + # This will output a nice json for each failed test (if running in CodeBuild) + if ($env:CODEBUILD_BUILD_ARN) + { + $testResults.TestResult | ForEach-Object + { + if ($_.Result -ne 'Passed') + { + ConvertTo-Json -InputObject $_ -Compress + } + } + } + + # Publish results. + Assert-Build (($numberFails = $testResults.FailedCount) -eq 0) ('Failed "{0}" unit tests.' -f $numberFails) + Write-Build Gray (' ...CODE COVERAGE - CommandsExecutedCount: {0}' -f $testResults.CodeCoverage.CommandsExecutedCount) + Write-Build Gray (' ...CODE COVERAGE - CommandsAnalyzedCount: {0}' -f $testResults.CodeCoverage.CommandsAnalyzedCount) + if ($testResults.CodeCoverage.CommandsExecutedCount -ne 0) + { + # Report on coverage percentage. + [System.UInt32]$coveragePercent = '{0:N2}' -f ($testResults.CodeCoverage.CommandsExecutedCount / $testResults.CodeCoverage.CommandsAnalyzedCount * 100) + if ($coveragePercent -lt $coverageThreshold) + { + throw ('Failed to meet code coverage threshold of {0}% with only {1}% coverage' -f $coverageThreshold, $coveragePercent) + } + Write-Build Cyan " $('Covered {0}% of {1} analyzed commands in {2} files.' -f $coveragePercent,$testResults.CodeCoverage.CommandsAnalyzedCount,$testResults.CodeCoverage.FilesAnalyzedCount)" + Write-Build Green ' ...Pester Unit Tests Complete!' + } + } +} + +# Synopsis: Used primarily during active development to generate xml file to graphically display code coverage in VSCode using Coverage Gutters. +Add-BuildTask DevCC { + Write-Build White ' Generating code coverage report at root...' + Write-Build White " Importing desired Pester version. Min: $Script:MinPesterVersion Max: $Script:MaxPesterVersion" + Remove-Module -Name Pester -Force -ErrorAction Ignore # there are instances where some containers have Pester already in the session + Import-Module -Name Pester -MinimumVersion $Script:MinPesterVersion -MaximumVersion $Script:MaxPesterVersion -ErrorAction 'Stop' + $pesterConfiguration = New-PesterConfiguration + $pesterConfiguration.run.Path = $Script:UnitTestsPath + $pesterConfiguration.CodeCoverage.Enabled = $true + $pesterConfiguration.CodeCoverage.Path = "$PSScriptRoot\$Script:ModuleName\*\*.ps1" + $pesterConfiguration.CodeCoverage.CoveragePercentTarget = $Script:coverageThreshold + $pesterConfiguration.CodeCoverage.OutputPath = '..\..\..\cov.xml' + $pesterConfiguration.CodeCoverage.OutputFormat = 'CoverageGutters' + Invoke-Pester -Configuration $pesterConfiguration + Write-Build Green ' ...Code Coverage report generated!' +} + +# Synopsis: Build help for module. +Add-BuildTask CreateHelpStart { + Write-Build White ' Performing all help related actions.' + Write-Build Gray ' Importing platyPS v0.12.0 ...' + Import-Module platyPS -RequiredVersion 0.12.0 + Write-Build Gray ' ...platyPS imported successfully.' +} + +# Synopsis: Build markdown help files for module and fail if help information is missing. +Add-BuildTask CreateMarkdownHelp -After CreateHelpStart { + # Generate markdown files. + Write-Build Gray ' Generating markdown files...' + $null = New-MarkdownHelp -Module $Script:ModuleName -OutputFolder $Script:MarkdownExportPath -Locale en-US -FwLink NA -HelpVersion $Script:ModuleVersion -Force + Write-Build Gray ' ...Markdown generation completed.' + + # Post-process the exported markdown files. + Write-Build Gray ' Replacing markdown elements...' + $Script:MarkdownExportPath | Get-ChildItem -File | ForEach-Object { + # Read the file as a string, not an array. + $content = [System.IO.File]::ReadAllText($_.FullName) + + # Trim the file, fix multi-line EXAMPLES, and unescape tilde characters. + $newContent = ($content.Trim() -replace '(## EXAMPLE [^`]+?```\r\n[^`\r\n]+?\r\n)(```\r\n\r\n)([^#]+?\r\n)(\r\n)([^#]+)(#)', '$1$3$2$4$5$6').Replace('PS C:\\\>', $null).Replace('\`', '`') + if ($newContent -ne $content) + { + [System.IO.File]::WriteAllLines($_.FullName, $newContent.Split("`n").TrimEnd()) + } + } + Write-Build Gray ' ...Markdown replacements complete.' + + # Validate Guid of export is correct. + Write-Build Gray ' Verifying GUID...' + if (Select-String -Path "$Script:MarkdownExportPath*.md" -Pattern "(00000000-0000-0000-0000-000000000000)") + { + Write-Build Yellow ' The documentation that got generated resulted in a generic GUID. Check the GUID entry of your module manifest.' + throw 'Missing GUID. Please review and rebuild.' + } + + # Perform amendments for PowerShell 7.4.x or higher targets. + # https://github.com/PowerShell/platyPS/issues/595 + Write-Build Gray ' Evaluating if running 7.4.0 or higher...' + if ($PSVersionTable.PSVersion -ge [version]'7.4.0') + { + Write-Build Gray ' Performing Markdown repair' + . $BuildRoot\Tools\MarkdownRepair.ps1 + $Script:MarkdownExportPath | Get-ChildItem -File | ForEach-Object { + Repair-PlatyPSMarkdown -Path $_.FullName + } + } + + # Validate nothing is missing. + Write-Build Gray ' Checking for missing documentation in md files...' + if ((($MissingDocumentation = Select-String -Path "$Script:MarkdownExportPath*.md" -Pattern "({{.*}})") | Measure-Object).Count -gt 0) + { + Write-Build Yellow ' The documentation that got generated resulted in missing sections which should be filled out.' + Write-Build Yellow ' Please review the following sections in your comment based help, fill out missing information and rerun this build:' + Write-Build Yellow ' (Note: This can happen if the .EXTERNALHELP CBH is defined for a function before running this build.)' + Write-Build Yellow " Path of files with issues: $Script:MarkdownExportPath" + $MissingDocumentation | Select-Object FileName, LineNumber, Line | Format-Table -AutoSize + throw 'Missing documentation. Please review and rebuild.' + } + + # Validate all exports have a synopsis. + Write-Build Gray ' Checking for missing SYNOPSIS in md files...' + $fSynopsisOutput = Select-String -Path "$Script:MarkdownExportPath*.md" -Pattern "^## SYNOPSIS$" -Context 0, 1 | ForEach-Object { + if ($null -eq $_.Context.DisplayPostContext.ToCharArray()) + { + $_.FileName + } + } + if ($fSynopsisOutput) + { + Write-Build Yellow " The following files are missing SYNOPSIS:" + $fSynopsisOutput + throw 'SYNOPSIS information missing. Please review.' + } + Write-Build Gray ' ...Markdown generation complete.' +} + +# Synopsis: Build the external xml help file from markdown help files with PlatyPS. +Add-BuildTask CreateExternalHelp -After CreateMarkdownHelp $null; $null = { + Write-Build Gray ' Creating external xml help file...' + $null = New-ExternalHelp $Script:MarkdownExportPath -OutputPath "$Script:ArtifactsPath\en-US\" -Force + Write-Build Gray ' ...External xml help file created!' +} + +# Synopsis: Build docusaurus help files from our markdown exports. +Add-BuildTask CreateDocusaurusHelp -After CreateMarkdownHelp { + Write-Build Gray ' Generating docusaurus files...' + New-DocusaurusHelp -PlatyPSMarkdownPath $Script:MarkdownExportPath -DocsFolder $Script:DocusaurusExportPath -NoPlaceHolderExamples | Where-Object { $_ -isnot [System.IO.DirectoryInfo] } + Write-Build Gray ' ...Docusaurus generation complete.' +} + +Add-BuildTask CreateHelpComplete -After CreateExternalHelp { + Write-Build Green ' ...CreateHelp Complete!' +} + +# Synopsis: Replace comment based help (CBH) with external help in all public functions for this project. +Add-BuildTask UpdateCBH -After AssetCopy $null; $null = { + # Define replacements. + $CBHPattern = "(?ms)(\<#.*\.SYNOPSIS.*?#>)" + $ExternalHelp = @" +<# + .EXTERNALHELP $($Script:ModuleName)-help.xml + #> +"@ + + # Perform replacements as required. + Get-ChildItem -Path "$Script:ArtifactsPath\Public\*.ps1" -File | ForEach-Object { + $FormattedOutFile = $_.FullName + Write-Output " Replacing CBH in file: $($FormattedOutFile)" + $UpdatedFile = (Get-Content $FormattedOutFile -Raw) -replace $CBHPattern, $ExternalHelp + $UpdatedFile | Out-File -FilePath $FormattedOutFile -Force -Encoding:utf8 + } +} + +# Synopsis: Copies module assets to Artifacts folder. +Add-BuildTask AssetCopy -Before Build { + Write-Build White ' Copying assets to Artifacts...' + New-Item -Path $Script:BuildModuleRoot -ItemType Directory -Force | Out-Null + Copy-Item -Path "$Script:ModuleSourcePath\*" -Destination $Script:BuildModuleRoot -Exclude "$($Script:ModuleName).ps*1" -Recurse + Write-Build Green ' ...Assets Copy Complete!' +} + +# Synopsis: Builds the Module to the Artifacts folder. +Add-BuildTask Build { + # Perform initial module manifest copy. + Write-Build White ' Performing Module Build' + Write-Build Gray ' Copying manifest file to Artifacts...' + Copy-Item -Path $Script:ModuleManifestFile -Destination $Script:BuildModuleRoot -Recurse + Write-Build Gray ' ...manifest copy complete.' + + # Compile the project into a singular psm1 file. + Write-Build Gray ' Merging Public and Private functions to one module file...' + $scriptContent = foreach ($file in (Get-ChildItem -Path $Script:BuildModuleRoot\ImportsFirst.ps1, $Script:BuildModuleRoot\Private\*.ps1, $Script:BuildModuleRoot\Public\*.ps1, $Script:BuildModuleRoot\ImportsLast.ps1 -Recurse -ErrorAction Ignore)) + { + # Import the script file as a string for substring replacement. + $text = [System.IO.File]::ReadAllText($file.FullName).Trim() + + # If our file isn't internal, redefine its command calls to be via the module's CommandTable. + if (!$file.BaseName.EndsWith('Internal') -and !$file.BaseName.StartsWith('Imports')) + { + # Parse the ps1 file and store its AST. + $tokens = $null + $errors = $null + $scrAst = [System.Management.Automation.Language.Parser]::ParseInput($text, [ref]$tokens, [ref]$errors) + + # Throw if we had any parsing errors. + if ($errors) + { + throw "Received $(($errCount = ($errors | Measure-Object).Count)) error$(if (!$errCount.Equals(1)) {'s'}) while parsing [$($file.Name)]." + } + + # Throw if we don't have exactly one statement. + if (!$scrAst.EndBlock.Statements.Count.Equals(1)) + { + throw "More than one statement is defined in [$($file.Name)]." + } + + # Recursively get all CommandAst objects that have an unknown InvocationOperator (bare word within a script). + $commandAsts = $scrAst.FindAll({ ($args[0] -is [System.Management.Automation.Language.CommandAst]) -and $args[0].InvocationOperator.Equals([System.Management.Automation.Language.TokenKind]::Unknown) }, $true) + + # Throw if there's a found CommandAst object where the first command element isn't a bare word (something unknowh has happened here). + if ($commandAsts.GetEnumerator().ForEach({ if (($_.CommandElements[0] -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) -or !$_.CommandElements[0].StringConstantType.Equals([System.Management.Automation.Language.StringConstantType]::BareWord)) { return $_ } }).Count) + { + throw "One or more found CommandAst objects within [$($file.Name)] were invalid." + } + + # Get all bare-word constants and process in reverse. We reverse the list so that we + # do the last found items first so the substring values in the AST are always correct. + $commandAsts | & { process { $_.CommandElements[0].Extent } } | Sort-Object -Property EndOffset -Descending | . { + process + { + # Don't replace the calls to any internally defined functions. + if (!$_.Text.Equals($file.BaseName) -and $scrAst.FindAll({ ($args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]) -and $args[0].Name.Equals($_.Text) }, $true).Count) + { + return + } + + # Throw if the CommandTable doesn't contain the command. + if (!$Script:moduleCommandTable.Contains($_.Text)) + { + throw "Unable to find the command [$($_.Text)] from [$($file.Name)] within the module's CommandTable." + } + + # Remove the offending text and replace with a CommandTable access. + $text = $text.Remove($_.StartOffset, $_.EndOffset - $_.StartOffset) + $text = $text.Insert($_.StartOffset, "& `$Script:CommandTable.'$($_.Text)'") + } + } + } + + # Write out the processed file back to disk. + $text; [System.String]::Empty; [System.String]::Empty + } + [System.IO.File]::WriteAllLines($Script:BuildModuleRootFile, $scriptContent) + Write-Build Gray ' ...Module creation complete.' + + # Clean up artifacts that are no longer required. + Write-Build Gray ' Cleaning up leftover artifacts...' + if (Test-Path "$Script:BuildModuleRoot\Public") + { + Remove-Item "$Script:BuildModuleRoot\Public" -Recurse -Force + } + if (Test-Path "$Script:BuildModuleRoot\Private") + { + Remove-Item "$Script:BuildModuleRoot\Private" -Recurse -Force + } + if (Test-Path "$Script:BuildModuleRoot\ImportsFirst.ps1") + { + Remove-Item "$Script:BuildModuleRoot\ImportsFirst.ps1" -Force -ErrorAction Ignore + } + if (Test-Path "$Script:BuildModuleRoot\ImportsLast.ps1") + { + Remove-Item "$Script:BuildModuleRoot\ImportsLast.ps1" -Force -ErrorAction Ignore + } + + # Update the parent level docs. + if (Test-Path $Script:MarkdownExportPath) + { + Write-Build Gray ' Overwriting docs output...' + if (!(Test-Path '..\docs\')) + { + New-Item -Path '..\docs\' -ItemType Directory -Force | Out-Null + } + Get-ChildItem -LiteralPath '..\docs\' -File | Remove-Item -Force -Confirm:$false + Move-Item "$($Script:DocusaurusExportPath)Commands\*.mdx" -Destination '..\docs\' -Force + Remove-Item $Script:DocusaurusExportPath -Recurse -Force + Remove-Item $Script:MarkdownExportPath -Recurse -Force + Write-Build Gray ' ...Docs output completed.' + } + + # Sign our files if we're running on main or develop. + if ($env:GITHUB_ACTIONS -eq 'true' -and $env:GITHUB_REF_NAME -match '^(main|develop)$') + { + if (!(Get-Command -Name 'azuresigntool' -ErrorAction Ignore)) + { + throw 'AzureSignTool not found.' + } + Write-Build Gray ' Signing module...' + Get-ChildItem -Path $Script:BuildModuleRoot -Include '*.ps*1' -Recurse | ForEach-Object { + & azuresigntool sign -s -kvu https://psadt-kv-prod-codesign.vault.azure.net -kvc PSADT -kvm -tr http://timestamp.digicert.com -td sha256 "$_" + if ($LASTEXITCODE -ne 0) { throw "Failed to sign file `"$_`". Exit code: $LASTEXITCODE" } + } + } + else + { + Write-Build Yellow ' Not running main or develop branch in GitHub Actions, skipping code signing...' + } + + Write-Build Green ' ...Build Complete!' +} + +# Synopsis: Invokes all Pester Integration Tests in the Tests\Integration folder (if it exists). +Add-BuildTask IntegrationTest { + if (Test-Path -Path $Script:IntegrationTestsPath) + { + # (Re-)Import Pester module. + Write-Build White " Importing desired Pester version. Min: $Script:MinPesterVersion Max: $Script:MaxPesterVersion" + Remove-Module -Name Pester -Force -ErrorAction Ignore # there are instances where some containers have Pester already in the session + Import-Module -Name Pester -MinimumVersion $Script:MinPesterVersion -MaximumVersion $Script:MaxPesterVersion -ErrorAction 'Stop' + + # Perform integration testing. + Write-Build White " Performing Pester Integration Tests..." + $pesterConfiguration = New-PesterConfiguration + $pesterConfiguration.run.Path = $Script:IntegrationTestsPath + $pesterConfiguration.Run.PassThru = $true + $pesterConfiguration.Run.Exit = $false + $pesterConfiguration.CodeCoverage.Enabled = $false + $pesterConfiguration.TestResult.Enabled = $false + $pesterConfiguration.Output.Verbosity = 'Detailed' + $testResults = Invoke-Pester -Configuration $pesterConfiguration + + # This will output a nice json for each failed test (if running in CodeBuild). + if ($env:CODEBUILD_BUILD_ARN) + { + $testResults.TestResult | ForEach-Object { + if ($_.Result -ne 'Passed') + { + ConvertTo-Json -InputObject $_ -Compress + } + } + } + + # Report on failures. + $numberFails = $testResults.FailedCount + Assert-Build($numberFails -eq 0) ('Failed "{0}" unit tests.' -f $numberFails) + Write-Build Green ' ...Pester Integration Tests Complete!' + } +} diff --git a/src/PSAppDeployToolkit.Tools/ImportsFirst.ps1 b/src/PSAppDeployToolkit.Tools/ImportsFirst.ps1 new file mode 100644 index 0000000..b72b354 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/ImportsFirst.ps1 @@ -0,0 +1,64 @@ +<# + +.SYNOPSIS +PSAppDeployToolkit.Tools - companion module for PSAppDeployToolkit. + +.DESCRIPTION +This module script contains functions to aid enterprise application packaging and the creation of PSAppDeployToolkit deployment scripts. + +PSAppDeployToolkit is licensed under the GNU LGPLv3 License - (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the +Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . + +.LINK +https://psappdeploytoolkit.com + +#> + +#----------------------------------------------------------------------------- +# +# MARK: Module Initialization Code +# +#----------------------------------------------------------------------------- + +# Define modules needed to build out CommandTable. +$RequiredModules = [System.Collections.ObjectModel.ReadOnlyCollection[Microsoft.PowerShell.Commands.ModuleSpecification]]$( + @{ ModuleName = 'Microsoft.PowerShell.Management'; Guid = 'eefcb906-b326-4e99-9f54-8b4bb6ef3c6d'; ModuleVersion = '1.0' } + @{ ModuleName = 'Microsoft.PowerShell.Utility'; Guid = '1da87e53-152b-403e-98dc-74d7b4d63d59'; ModuleVersion = '1.0' } + @{ ModuleName = 'PSAppDeployToolkit'; Guid = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '3.93.0' } + @{ ModuleName = 'PSScriptAnalyzer'; Guid = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0' } +) + +# Build out lookup table for all cmdlets used within module, starting with the core cmdlets. +$CommandTable = [ordered]@{}; $ExecutionContext.SessionState.InvokeCommand.GetCmdlets() | & { process { if ($_.PSSnapIn -and $_.PSSnapIn.Name.Equals('Microsoft.PowerShell.Core') -and $_.PSSnapIn.IsDefault) { $CommandTable.Add($_.Name, $_) } } } +(& $CommandTable.'Import-Module' -FullyQualifiedName $RequiredModules -Global -Force -PassThru -ErrorAction Stop).ExportedCommands.Values | & { process { $CommandTable.Add($_.Name, $_) } } + +# Set required variables to ensure module functionality. +& $CommandTable.'New-Variable' -Name ErrorActionPreference -Value ([System.Management.Automation.ActionPreference]::Stop) -Option Constant -Force +& $CommandTable.'New-Variable' -Name InformationPreference -Value ([System.Management.Automation.ActionPreference]::Continue) -Option Constant -Force +& $CommandTable.'New-Variable' -Name ProgressPreference -Value ([System.Management.Automation.ActionPreference]::SilentlyContinue) -Option Constant -Force + +# Ensure module operates under the strictest of conditions. +& $CommandTable.'Set-StrictMode' -Version 3 + +# Import this module's manifest via the language parser. This allows us to test with potential extra variables that are permitted in manifests. +# https://github.com/PowerShell/PowerShell/blob/7ca7aae1d13d19e38c7c26260758f474cb9bef7f/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs#L509-L512 +$Module = [System.Management.Automation.Language.Parser]::ParseFile("$PSScriptRoot\PSAppDeployToolkit.Tools.psd1", [ref]$null, [ref]$null).GetScriptBlock() +$Module.CheckRestrictedLanguage([System.String[]]$null, [System.String[]]('PSEdition'), $true); $Module = & $Module + +# Store build information pertaining to this module's state. +& $CommandTable.'New-Variable' -Name Module -Option Constant -Force -Value ([ordered]@{ + Manifest = $Module + Compiled = $MyInvocation.MyCommand.Name.Equals('PSAppDeployToolkit.Tools.psm1') + }).AsReadOnly() + +# Remove any previous functions that may have been defined. +if ($Module.Compiled) +{ + & $CommandTable.'New-Variable' -Name FunctionNames -Option Constant -Value ($MyInvocation.MyCommand.ScriptBlock.Ast.EndBlock.Statements | & { process { if ($_ -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $_.Name } } }) + & $CommandTable.'New-Variable' -Name FunctionPaths -Option Constant -Value ($FunctionNames -replace '^', 'Microsoft.PowerShell.Core\Function::') + & $CommandTable.'Remove-Item' -LiteralPath $FunctionPaths -Force -ErrorAction Ignore +} diff --git a/src/PSAppDeployToolkit.Tools/ImportsLast.ps1 b/src/PSAppDeployToolkit.Tools/ImportsLast.ps1 new file mode 100644 index 0000000..691a001 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/ImportsLast.ps1 @@ -0,0 +1,11 @@ +#----------------------------------------------------------------------------- +# +# MARK: Module Constants and Function Exports +# +#----------------------------------------------------------------------------- + +# Set all functions as read-only, export all public definitions and finalise the CommandTable. +& $CommandTable.'Set-Item' -LiteralPath $FunctionPaths -Options ReadOnly +& $CommandTable.'Get-Item' -LiteralPath $FunctionPaths | & { process { $CommandTable.Add($_.Name, $_) } } +& $CommandTable.'New-Variable' -Name CommandTable -Value $CommandTable.AsReadOnly() -Option Constant -Force -Confirm:$false +& $CommandTable.'Export-ModuleMember' -Function $Module.Manifest.FunctionsToExport diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 new file mode 100644 index 0000000..8cead80 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -0,0 +1,127 @@ +# +# Module manifest for module 'PSAppDeployToolkit.Tools' +# +# Generated on: 2024-12-03 +# + +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'PSAppDeployToolkit.Tools.psm1' + + # Version number of this module. + ModuleVersion = '0.2.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'b20417ce-57d3-40c2-923f-71dad3b7edd9' + + # Author of this module + Author = 'PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough)' + + # Company or vendor of this module + CompanyName = 'PSAppDeployToolkit Team' + + # Copyright statement for this module + Copyright = 'Copyright © 2024 PSAppDeployToolkit Team. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Enterprise App Packaging, Simplified.' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1.14393.0' + + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the Windows PowerShell host required by this module + PowerShellHostVersion = '5.1.14393.0' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + DotNetFrameworkVersion = '4.6.2.0' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + CLRVersion = '4.0.30319.42000' + + # Processor architecture (None, X86, Amd64) required by this module + ProcessorArchitecture = 'None' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + @{ModuleName = 'PSAppDeployToolkit'; GUID = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.2'; } + @{ModuleName = 'PSScriptAnalyzer'; GUID = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0'; } + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Convert-ADTDeployment' + 'Test-ADTCompatibility' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + # VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tag to indicate pre-release status + Prerelease = 'beta' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = 'psappdeploytoolkit', 'adt', 'psadt', 'appdeployment', 'appdeploytoolkit', 'appdeploy', 'deployment', 'toolkit' + + # A URL to the license for this module. + LicenseUri = 'https://raw.githubusercontent.com/PSAppDeployToolkit/PSAppDeployToolkit.Tools/refs/heads/main/COPYING.Lesser' + + # A URL to the main website for this project. + ProjectUri = 'https://psappdeploytoolkit.com' + + # A URL to an icon representing this module. + IconUri = 'https://raw.githubusercontent.com/PSAppDeployToolkit/PSAppDeployToolkit/refs/heads/main/src/PSAppDeployToolkit/Assets/AppIcon.png' + + # ReleaseNotes of this module + ReleaseNotes = 'https://github.com/PSAppDeployToolkit/PSAppDeployToolkit.Tools/releases' + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + HelpInfoURI = 'https://psappdeploytoolkit.com/docs' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 new file mode 100644 index 0000000..a49015b --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 @@ -0,0 +1,22 @@ +#----------------------------------------------------------------------------- +# +# Local psm1 file for testing the module without having to build it. +# +#----------------------------------------------------------------------------- + +# Dot-source our initial imports. +. "$PSScriptRoot\ImportsFirst.ps1" + +# Dot-source our imports. +if (!$Module.Compiled) +{ + #& $CommandTable.'New-Variable' -Name ModuleFiles -Option Constant -Value ([System.IO.FileInfo[]]$([System.IO.Directory]::GetFiles("$PSScriptRoot\Private"); [System.IO.Directory]::GetFiles("$PSScriptRoot\Public"))) + & $CommandTable.'New-Variable' -Name ModuleFiles -Option Constant -Value ([System.IO.FileInfo[]]$([System.IO.Directory]::GetFiles("$PSScriptRoot\Public"))) + & $CommandTable.'New-Variable' -Name FunctionNames -Option Constant -Value ($ModuleFiles | & { process { return $_.BaseName } }) + & $CommandTable.'New-Variable' -Name FunctionPaths -Option Constant -Value ($FunctionNames -replace '^', 'Microsoft.PowerShell.Core\Function::') + & $CommandTable.'Remove-Item' -LiteralPath $FunctionPaths -Force -ErrorAction Ignore + $ModuleFiles.FullName | . { process { . $_ } } +} + +# Dot-source our final imports. +. "$PSScriptRoot\ImportsLast.ps1" diff --git a/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 new file mode 100644 index 0000000..fa2d663 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 @@ -0,0 +1,1392 @@ +<# + .SYNOPSIS + PSSCriptAnalyzer rules to check for usage of legacy PSAppDeployToolkit v3 commands or variables. + .DESCRIPTION + Can be used directly with PSSCriptAnalyzer or via Test-ADTCompatibility and Convert-ADTDeployment functions. + .EXAMPLE + Measure-ADTCompatibility -ScriptBlockAst $ScriptBlockAst + .INPUTS + [System.Management.Automation.Language.ScriptBlockAst] + .OUTPUTS + [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]] + .NOTES + None +#> +function Measure-ADTCompatibility +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord])] + Param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.ScriptBlockAst] + $ScriptBlockAst + ) + + Begin + { + $variableMappings = @{ + AllowRebootPassThru = '$adtSession.AllowRebootPassThru' + appArch = '$adtSession.AppArch' + appLang = '$adtSession.AppLang' + appName = '$adtSession.AppName' + appRevision = '$adtSession.AppRevision' + appScriptAuthor = '$adtSession.AppScriptAuthor' + appScriptDate = '$adtSession.AppScriptDate' + appScriptVersion = '$adtSession.AppScriptVersion' + appVendor = '$adtSession.AppVendor' + appVersion = '$adtSession.AppVersion' + currentDate = '$adtSession.CurrentDate' + currentDateTime = '$adtSession.CurrentDateTime' + defaultMsiFile = '$adtSession.DefaultMsiFile' + deployAppScriptDate = '$adtSession.DeployAppScriptDate' + deployAppScriptFriendlyName = '$adtSession.DeployAppScriptFriendlyName' + deployAppScriptParameters = '$adtSession.DeployAppScriptParameters' + deployAppScriptVersion = '$adtSession.DeployAppScriptVersion' + DeploymentType = '$adtSession.DeploymentType' + deploymentTypeName = '$adtSession.DeploymentTypeName' + DeployMode = '$adtSession.DeployMode' + dirFiles = '$adtSession.DirFiles' + dirSupportFiles = '$adtSession.DirSupportFiles' + DisableScriptLogging = '$adtSession.DisableLogging' + installName = '$adtSession.InstallName' + installPhase = '$adtSession.InstallPhase' + installTitle = '$adtSession.InstallTitle' + logName = '$adtSession.LogName' + logTempFolder = '$adtSession.LogTempFolder' + scriptDirectory = '$adtSession.ScriptDirectory' + TerminalServerMode = '$adtSession.TerminalServerMode' + useDefaultMsi = '$adtSession.UseDefaultMsi' + appDeployConfigFile = $null + appDeployCustomTypesSourceCode = $null + appDeployExtScriptDate = $null + appDeployExtScriptFriendlyName = $null + appDeployExtScriptParameters = $null + appDeployExtScriptVersion = $null + appDeployLogoBanner = $null + appDeployLogoBannerHeight = $null + appDeployLogoBannerMaxHeight = $null + appDeployLogoBannerObject = $null + appDeployLogoIcon = $null + appDeployLogoImage = $null + appDeployMainScriptAsyncParameters = $null + appDeployMainScriptDate = $null + appDeployMainScriptFriendlyName = $null + appDeployMainScriptMinimumConfigVersion = $null + appDeployMainScriptParameters = $null + appDeployRunHiddenVbsFile = $null + appDeployToolkitDotSourceExtensions = $null + appDeployToolkitExtName = $null + AsyncToolkitLaunch = $null + BlockExecution = $null + ButtonLeftText = $null + ButtonMiddleText = $null + ButtonRightText = $null + CleanupBlockedApps = $null + closeAppsCountdownGlobal = $null + configBalloonTextComplete = '(Get-ADTStringTable).BalloonText.Complete' + configBalloonTextError = '(Get-ADTStringTable).BalloonText.Error' + configBalloonTextFastRetry = '(Get-ADTStringTable).BalloonText.FastRetry' + configBalloonTextRestartRequired = '(Get-ADTStringTable).BalloonText.RestartRequired' + configBalloonTextStart = '(Get-ADTStringTable).BalloonText.Start' + configBannerIconBannerName = '(Get-ADTConfig).Assets.Banner' + configBannerIconFileName = $null + configBannerLogoImageFileName = '(Get-ADTConfig).Assets.Logo' + configBlockExecutionMessage = '(Get-ADTStringTable).BlockExecution.Message' + configClosePromptButtonClose = '(Get-ADTStringTable).ClosePrompt.ButtonClose' + configClosePromptButtonContinue = '(Get-ADTStringTable).ClosePrompt.ButtonContinue' + configClosePromptButtonContinueTooltip = '(Get-ADTStringTable).ClosePrompt.ButtonContinueTooltip' + configClosePromptButtonDefer = '(Get-ADTStringTable).ClosePrompt.ButtonDefer' + configClosePromptCountdownMessage = '(Get-ADTStringTable).ClosePrompt.CountdownMessage' + configClosePromptMessage = '(Get-ADTStringTable).ClosePrompt.Message' + configConfigDate = $null + configConfigDetails = $null + configConfigVersion = $null + configDeferPromptDeadline = '(Get-ADTStringTable).DeferPrompt.Deadline' + configDeferPromptExpiryMessage = '(Get-ADTStringTable).DeferPrompt.ExpiryMessage' + configDeferPromptRemainingDeferrals = '(Get-ADTStringTable).DeferPrompt.RemainingDeferrals' + configDeferPromptWarningMessage = '(Get-ADTStringTable).DeferPrompt.WarningMessage' + configDeferPromptWelcomeMessage = '(Get-ADTStringTable).DeferPrompt.WelcomeMessage' + configDeploymentTypeInstall = '(Get-ADTStringTable).DeploymentType.Install' + configDeploymentTypeRepair = '(Get-ADTStringTable).DeploymentType.Repair' + configDeploymentTypeUnInstall = '(Get-ADTStringTable).DeploymentType.Uninstall' + configDiskSpaceMessage = '(Get-ADTStringTable).DiskSpace.Message' + configInstallationDeferExitCode = '(Get-ADTConfig).UI.DeferExitCode' + configInstallationPersistInterval = '(Get-ADTConfig).UI.DefaultPromptPersistInterval' + configInstallationPromptToSave = '(Get-ADTConfig).UI.PromptToSaveTimeout' + configInstallationRestartPersistInterval = '(Get-ADTConfig).UI.RestartPromptPersistInterval' + configInstallationUIExitCode = '(Get-ADTConfig).UI.DefaultExitCode' + configInstallationUILanguageOverride = '(Get-ADTConfig).UI.LanguageOverride' + configInstallationUITimeout = '(Get-ADTConfig).UI.DefaultTimeout' + configInstallationWelcomePromptDynamicRunningProcessEvaluation = '(Get-ADTConfig).UI.DynamicProcessEvaluation' + configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval = '(Get-ADTConfig).UI.DynamicProcessEvaluationInterval' + configMSIInstallParams = '(Get-ADTConfig).MSI.InstallParams' + configMSILogDir = 'if ($isAdmin) { (Get-ADTConfig).MSI.LogPath } else { (Get-ADTConfig).MSI.LogPathNoAdminRights }' + configMSILoggingOptions = '(Get-ADTConfig).MSI.LoggingOptions' + configMSIMutexWaitTime = '(Get-ADTConfig).MSI.MutexWaitTime' + configMSISilentParams = '(Get-ADTConfig).MSI.SilentParams' + configMSIUninstallParams = '(Get-ADTConfig).MSI.UninstallParams' + configProgressMessageInstall = '(Get-ADTStringTable).Progress.MessageInstall' + configProgressMessageRepair = '(Get-ADTStringTable).Progress.MessageRepair' + configProgressMessageUninstall = '(Get-ADTStringTable).Progress.MessageUninstall' + configRestartPromptButtonRestartLater = '(Get-ADTStringTable).RestartPrompt.ButtonRestartLater' + configRestartPromptButtonRestartNow = '(Get-ADTStringTable).RestartPrompt.ButtonRestartNow' + configRestartPromptMessage = '(Get-ADTStringTable).RestartPrompt.Message' + configRestartPromptMessageRestart = '(Get-ADTStringTable).RestartPrompt.MessageRestart' + configRestartPromptMessageTime = '(Get-ADTStringTable).RestartPrompt.MessageTime' + configRestartPromptTimeRemaining = '(Get-ADTStringTable).RestartPrompt.TimeRemaining' + configRestartPromptTitle = '(Get-ADTStringTable).RestartPrompt.Title' + configShowBalloonNotifications = '(Get-ADTConfig).UI.BalloonNotifications' + configToastAppName = '(Get-ADTConfig).UI.BalloonTitle' + configToastDisable = '(Get-ADTConfig).UI.BalloonNotifications' + configToolkitCachePath = '(Get-ADTConfig).Toolkit.CachePath' + configToolkitCompressLogs = '(Get-ADTConfig).Toolkit.CompressLogs' + configToolkitLogAppend = '(Get-ADTConfig).Toolkit.LogAppend' + configToolkitLogDebugMessage = '(Get-ADTConfig).Toolkit.LogDebugMessage' + configToolkitLogDir = 'if ($isAdmin) { (Get-ADTConfig).Toolkit.LogPath } else { (Get-ADTConfig).Toolkit.LogPathNoAdminRights }' + configToolkitLogMaxHistory = '(Get-ADTConfig).Toolkit.LogMaxHistory' + configToolkitLogMaxSize = '(Get-ADTConfig).Toolkit.LogMaxSize' + configToolkitLogStyle = '(Get-ADTConfig).Toolkit.LogStyle' + configToolkitLogWriteToHost = '(Get-ADTConfig).Toolkit.LogWriteToHost' + configToolkitRegPath = '(Get-ADTConfig).Toolkit.RegPath' + configToolkitRequireAdmin = '(Get-ADTConfig).Toolkit.RequireAdmin' + configToolkitTempPath = 'if ($isAdmin) { (Get-ADTConfig).Toolkit.TempPath } else { (Get-ADTConfig).Toolkit.TempPathNoAdminRights }' + configToolkitUseRobocopy = '(Get-ADTConfig).Toolkit.FileCopyMode -eq ''Robocopy''' + configWelcomePromptCountdownMessage = '(Get-ADTStringTable).WelcomePrompt.Classic.CountdownMessage' + configWelcomePromptCustomMessage = '(Get-ADTStringTable).WelcomePrompt.Classic.CustomMessage' + CountdownNoHideSeconds = $null + CountdownSeconds = $null + currentTime = $null + currentTimeZoneBias = $null + defaultFont = $null + deployModeNonInteractive = $null + deployModeSilent = $null + DeviceContextHandle = $null + dirAppDeployTemp = $null + dpiPixels = $null + dpiScale = $null + envOfficeChannelProperty = $null + envShellFolders = $null + exeMsiexec = $null + exeSchTasks = $null + exeWusa = $null + ExitOnTimeout = $null + formattedOSArch = $null + formWelcomeStartPosition = $null + GetAccountNameUsingSid = $null + GetDisplayScaleFactor = $null + GetLoggedOnUserDetails = $null + GetLoggedOnUserTempPath = $null + GraphicsObject = $null + HKULanguages = $null + HKUPrimaryLanguageShort = $null + hr = $null + Icon = $null + installationStarted = $null + InvocationInfo = $null + invokingScript = $null + IsOOBEComplete = 'Test-ADTOobeCompleted' + IsTaskSchedulerHealthy = $null + LocalPowerUsersGroup = $null + LogFileInitialized = $null + loggedOnUserTempPath = $null + LogicalScreenHeight = $null + LogTimeZoneBias = $null + mainExitCode = $null + Message = $null + MessageAlignment = $null + MinimizeWindows = $null + moduleAppDeployToolkitMain = $null + msiRebootDetected = $null + NoCountdown = $null + notifyIcon = $null + OldDisableLoggingValue = $null + oldPSWindowTitle = $null + PersistPrompt = $null + PhysicalScreenHeight = $null + PrimaryWindowsUILanguage = $null + ProgressRunspace = $null + ProgressSyncHash = $null + ReferencedAssemblies = $null + ReferredInstallName = $null + ReferredInstallTitle = $null + ReferredLogName = $null + regKeyAppExecution = $null + regKeyApplications = $null + regKeyDeferHistory = $null + regKeyLotusNotes = $null + RevertScriptLogging = $null + runningProcessDescriptions = $null + scriptFileName = $null + scriptName = $null + scriptParentPath = $null + scriptPath = $null + scriptRoot = $null + scriptSeparator = $null + ShowBlockedAppDialog = $null + ShowInstallationPrompt = $null + ShowInstallationRestartPrompt = $null + switch = $null + Timeout = $null + Title = $null + TopMost = $null + TypeDef = $null + UserDisplayScaleFactor = $null + welcomeTimer = $null + xmlBannerIconOptions = $null + xmlConfig = $null + xmlConfigFile = $null + xmlConfigMSIOptions = $null + xmlConfigUIOptions = $null + xmlLoadLocalizedUIMessages = $null + xmlToastOptions = $null + xmlToolkitOptions = $null + xmlUIMessageLanguage = $null + xmlUIMessages = $null + } + + $functionMappings = @{ + 'Write-Log' = @{ + 'NewFunction' = 'Write-ADTLogEntry' + 'TransformParameters' = @{ + 'Text' = { "-Message $_" } + 'ContinueOnError' = { if ($_ -eq '$true') { '-ErrorAction SilentlyContinue' } else { '-ErrorAction Stop' } } + } + 'RemoveParameters' = @( + 'AppendToLogFile' + 'LogDebugMessage' + 'MaxLogHistory' + 'MaxLogFileSizeMB' + 'WriteHost' + ) + } + 'Exit-Script' = @{ + 'NewFunction' = 'Exit-ADTScript' + } + 'Invoke-HKCURegistrySettingsForAllUsers' = @{ + 'NewFunction' = 'Invoke-ADTAllUsersRegistryAction' + 'TransformParameters' = @{ + 'RegistrySettings' = { "-ScriptBlock $($_.Replace('$UserProfile', '$_'))" } + } + } + 'Get-HardwarePlatform' = @{ + 'NewFunction' = '$envHardwareType' + 'RemoveParameters' = @( + 'ContinueOnError' + ) + } + 'Get-FreeDiskSpace' = @{ + 'NewFunction' = 'Get-ADTFreeDiskSpace' + 'TransformParameters' = @{ + 'ContinueOnError' = { if ($_ -eq '$true') { '-ErrorAction SilentlyContinue' } else { '-ErrorAction Stop' } } + } + } + 'Remove-InvalidFileNameChars' = @{ + 'NewFunction' = 'Remove-ADTInvalidFileNameChars' + } + 'Get-InstalledApplication' = @{ + 'NewFunction' = 'Get-ADTApplication' + 'TransformParameters' = @{ + 'ContinueOnError' = { if ($_ -eq '$true') { '-ErrorAction SilentlyContinue' } else { '-ErrorAction Stop' } } + 'Exact' = '-NameMatch Exact' # Should inspect switch values here in case of -Switch:$false + 'WildCard' = '-NameMatch WildCard' # Should inspect switch values here in case of -Switch:$false + 'RegEx' = '-NameMatch RegEx' # Should inspect switch values here in case of -Switch:$false + } + } + 'Remove-MSIApplications' = @{ + 'NewFunction' = 'Uninstall-ADTApplication' + 'TransformParameters' = @{ + 'ContinueOnError' = { if ($_ -eq '$true') { '-ErrorAction SilentlyContinue' } else { '-ErrorAction Stop' } } + 'Exact' = '-NameMatch Exact' # Should inspect switch values here in case of -Switch:$false + 'WildCard' = '-NameMatch WildCard' # Should inspect switch values here in case of -Switch:$false + 'Arguments' = { "-ArgumentList $_" } + 'Parameters' = { "-ArgumentList $_" } + 'AddParameters' = { "-AdditionalArgumentList $_" } + 'LogName' = { "-LogFileName $_" } + 'FilterApplication' = { + $filterApplication = @(if ($null -eq $boundParameters.FilterApplication.Value.Extent) { $null } else { $boundParameters.FilterApplication.Value.SafeGetValue() }) + $excludeFromUninstall = @(if ($null -eq $boundParameters.ExcludeFromUninstall.Value.Extent) { $null } else { $boundParameters.ExcludeFromUninstall.Value.SafeGetValue() }) + + $filterArray = $( + foreach ($item in $filterApplication) + { + if ($null -ne $item) + { + if ($item.Count -eq 1 -and $item[0].Count -eq 3) { $item = $item[0] } # Handle the case where input is of the form @(, @('Prop', 'Value', 'Exact'), @('Prop', 'Value', 'Exact')) + if ($item[2] -eq 'RegEx') + { + "`$_.$($item[0]) -match '$($item[1] -replace "'","''")'" + } + elseif ($item[2] -eq 'Contains') + { + $regEx = [System.Text.RegularExpressions.Regex]::Escape(($item[1] -replace "'", "''")) -replace '(? + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('FullName')] + [ValidateScript({ + if (!(Test-Path -LiteralPath $_)) + { + $PSCmdlet.ThrowTerminatingError((New-ADTValidateScriptErrorRecord -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'The specified path does not exist.')) + } + elseif ([System.IO.File]::Exists($_) -and [System.IO.Path]::GetExtension($_) -ne '.ps1') + { + $PSCmdlet.ThrowTerminatingError((New-ADTValidateScriptErrorRecord -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'The specified file is not a PowerShell script.')) + } + elseif ([System.IO.Directory]::Exists($_) -and -not [System.IO.File]::Exists([System.IO.Path]::Combine($_, 'Deploy-Application.ps1'))) + { + $PSCmdlet.ThrowTerminatingError((New-ADTValidateScriptErrorRecord -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'Deploy-Application.ps1 not found in the specified path.')) + } + elseif ([System.IO.Directory]::Exists($_) -and -not [System.IO.Directory]::Exists([System.IO.Path]::Combine($_, 'AppDeployToolkit'))) + { + $PSCmdlet.ThrowTerminatingError((New-ADTValidateScriptErrorRecord -ParameterName Path -ProvidedValue $_ -ExceptionMessage 'AppDeployToolkit folder not found in the specified path.')) + } + return ![System.String]::IsNullOrWhiteSpace($_) + })] + [System.String]$Path, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string]$Destination = (Split-Path -Path $Path -Parent), + + [Parameter(Mandatory = $false)] + [System.Management.Automation.SwitchParameter]$Show, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.SwitchParameter]$Force, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.SwitchParameter]$PassThru + ) + + begin + { + # Initialize function. + Initialize-ADTFunction -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState + + $scriptReplacements = @( + @{ + v4FunctionName = 'Install-ADTDeployment' + v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ine ''Uninstall''' + }, + @{ + v4FunctionName = 'Uninstall-ADTDeployment' + v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ieq ''Uninstall''' + }, + @{ + v4FunctionName = 'Repair-ADTDeployment' + v3IfConditionMatch = '^\$(adtSession\.)?deploymentType -ieq ''Repair''' + } + ) + + $variableReplacements = @('appVendor', 'appName', 'appVersion', 'appArch', 'appLang', 'appRevision', 'appScriptVersion', 'appScriptAuthor', 'installName', 'installTitle') + + $customRulePath = [System.IO.Path]::Combine($MyInvocation.MyCommand.Module.ModuleBase, 'PSScriptAnalyzer\Measure-ADTCompatibility.psm1') + $templateScriptPath = [System.IO.Path]::Combine((Get-Module PSAppDeployToolkit).ModuleBase, 'Frontend\v4\Invoke-AppDeployToolkit.ps1') + } + + process + { + try + { + try + { + $tempFolderName = "Convert-ADTDeployment_$([System.IO.Path]::GetRandomFileName().Replace('.', ''))" + $tempFolderPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFolderName) + + if ($Path -like '*.ps1') + { + Write-Verbose -Message "Input path is a .ps1 file: [$Path]" + + # Update destination path with a specific filename if current value does not end in .ps1 + $Destination = if ($Destination -like '*.ps1') { $Destination } else { [System.IO.Path]::Combine($Destination, 'Invoke-AppDeployToolkit.ps1') } + Write-Verbose -Message "Destination path: [$Destination]" + + # Halt if the destination file already exists and -Force is not specified + if (!$Force -and [System.IO.File]::Exists($Destination)) + { + $naerParams = @{ + Exception = [System.IO.IOException]::new("File [$Destination] already exists.") + Category = [System.Management.Automation.ErrorCategory]::ResourceExists + ErrorId = 'FileAlreadyExistsError' + TargetObject = $Path + RecommendedAction = 'Use the -Force parameter to overwrite the existing file.' + } + Write-Error -ErrorRecord (New-ADTErrorRecord @naerParams) + } + + if ($Path -notmatch '(?<=^|\\)(Deploy-Application\.ps1|Invoke-AppDeployToolkit\.ps1)$') + { + Write-Warning -Message "This function is designed to convert PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1." + } + + # Create the temp folder + Write-Verbose -Message "Creating temp folder [$tempFolderPath]" + New-Item -Path $tempFolderPath -ItemType Directory -Force | Out-Null + + # Create a temp copy of the script to run ScriptAnalyzer fixes on - prefix filename with _ if it's named Invoke-AppDeployToolkit.ps1 + $inputScriptPath = if ($Path -match '(?<=^|\\)Invoke-AppDeployToolkit.ps1$') + { + [System.IO.Path]::Combine(([System.IO.Path]::GetDirectoryName($Path)), "_$([System.IO.Path]::GetFileName($Path))") + } + else + { + $Path + } + + Write-Verbose -Message "Creating copy of [$Path] as [$inputScriptPath]" + Copy-Item -LiteralPath $Path -Destination $inputScriptPath -Force + + # Copy over our template v4 script + Write-Verbose -Message "Copying template script to [$tempFolderPath\Invoke-AppDeployToolkit.ps1]" + $outputScriptPath = (Copy-Item -LiteralPath $templateScriptPath -Destination $tempFolderPath -Force -PassThru).FullName + } + else + { + Write-Verbose -Message "Input path is a folder: [$Path]" + + # Re-use the same folder name with _Converted suffix for the new folder + $folderName = "$(Split-Path -Path $Path -Leaf)_Converted" + + # Update destination path to append this new folder name. If Destination is empty, it would mean that Path was something like C:\ with no parent, so just append the folder name to Path instead. + $Destination = if ([string]::IsNullOrEmpty($Destination)) + { + [System.IO.Path]::Combine($Path, $folderName) + } + else + { + [System.IO.Path]::Combine($Destination, $folderName) + } + Write-Verbose -Message "Destination path: [$Destination]" + + # Halt if the destination folder already exists and is not empty and -Force is not specified + if (!$Force -and [System.IO.Directory]::Exists($Destination) -and ([System.IO.Directory]::GetFiles($Destination) -or [System.IO.Directory]::GetDirectories($Destination))) + { + $naerParams = @{ + Exception = [System.IO.IOException]::new("Folder [$finalDestination] already exists and is not empty.") + Category = [System.Management.Automation.ErrorCategory]::ResourceExists + ErrorId = 'FolderAlreadyExistsError' + TargetObject = $Path + RecommendedAction = 'Use the -Force parameter to overwrite the existing folder.' + } + Write-Error -ErrorRecord (New-ADTErrorRecord @naerParams) + } + + Write-Verbose -Message "Creating ADT Template in [$tempFolderPath]" + New-ADTTemplate -Destination ([System.IO.Path]::GetTempPath()) -Name $tempFolderName + + Write-Verbose -Message "Creating copy of [$Path\Deploy-Application.ps1] as [$tempFolderPath\Deploy-Application.ps1]" + $inputScriptPath = (Copy-Item -LiteralPath ([System.IO.Path]::Combine($Path, 'Deploy-Application.ps1')) -Destination $tempFolderPath -Force -PassThru).FullName + + # Set the path of our v4 template script + $outputScriptPath = [System.IO.Path]::Combine($tempFolderPath, 'Invoke-AppDeployToolkit.ps1') + } + + # First run the fixes on the input script to update function names and variables + Write-Verbose -Message "Running ScriptAnalyzer fixes on [$inputScriptPath]" + Invoke-ScriptAnalyzer -Path $inputScriptPath -CustomRulePath $customRulePath -Fix | Out-Null + + # Parse the input script and find the if statement that contains the deployment code + $inputScriptContent = Get-Content -Path $inputScriptPath -Raw + $inputScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($inputScriptContent, [ref]$null, [ref]$null) + + $ifStatement = $inputScriptAst.Find({ + param ($ast) + $ast -is [System.Management.Automation.Language.IfStatementAst] -and $ast.Clauses[0].Item1.Extent.Text -match $scriptReplacements[0].v3IfConditionMatch + }, $true) + + if (-not $ifStatement) + { + throw "The expected if statement was not found in the input script." + } + + foreach ($scriptReplacement in $scriptReplacements) + { + # Find the if clause to process from the v3 deployment script + $ifClause = $ifStatement.Clauses | Where-Object { $_.Item1.Extent.Text -match $scriptReplacement.v3IfConditionMatch } + + if ($ifClause) + { + Write-Verbose -Message "Found statement: if ($($ifClause.Item1.Extent.Text))" + + # Re-read and parse the v4 template script after each replacement so that the offsets will still be valid + $tempScriptContent = Get-Content -Path $outputScriptPath -Raw + $tempScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($tempScriptContent, [ref]$null, [ref]$null) + + # Find the function definition in the v4 template script to fill in + $functionAst = $tempScriptAst.Find({ + param ($ast) + $ast -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $ast.Name -eq $scriptReplacement.v4FunctionName + }, $true) + + if ($functionAst) + { + Write-Verbose -Message "Updating function [$($scriptReplacement.v4FunctionName)]" + + # Update the content of the v4 template script + $start = $functionAst.Body.Extent.StartOffset + $end = $functionAst.Body.Extent.EndOffset + $scriptContent = $tempScriptAst.Extent.Text + $newScriptContent = ($scriptContent.Substring(0, $start) + $ifClause.Item2.Extent.Text + $scriptContent.Substring($end)).Trim() + Set-Content -Path $outputScriptPath -Value $newScriptContent -Encoding UTF8 + } + } + else + { + Write-Warning -Message "The if statement for [$($scriptReplacement.v4FunctionName)] was not found in the input script." + } + } + + # Re-read and parse the script one more time + $tempScriptContent = Get-Content -Path $outputScriptPath -Raw + $tempScriptAst = [System.Management.Automation.Language.Parser]::ParseInput($tempScriptContent, [ref]$null, [ref]$null) + + # Find the hashtable in the v4 template script that holds the adtSession splat + $hashtableAst = $tempScriptAst.Find({ + param ($ast) + $ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $ast.Left.VariablePath.UserPath -eq 'adtSession' + }, $true) + + if ($hashtableAst) + { + Write-Verbose -Message 'Processing $adtSession hashtable' + + # Get the text form of the hashtable definition + $hashtableContent = $hashtableAst.Right.Extent.Text + + # Copy each variable value from the input script to the hashtable + foreach ($variableReplacement in $variableReplacements) + { + $assignmentAst = $inputScriptAst.Find({ + param ($ast) + $ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $ast.Left.Extent.Text -match "^(\[[^\]]+\])?\`$(adtSession\.)?$variableReplacement$" + }, $true) + + if ($assignmentAst) + { + Write-Verbose -Message "Updating variable [$variableReplacement]" + $variableValue = $assignmentAst.Right.Extent.Text + $hashtableContent = $hashtableContent -replace "(?m)(^\s*$variableReplacement\s*=)\s*'[^']*'", "`$1 $variableValue" + } + } + + Write-Verbose -Message 'Updating variable [appScriptDate]' + $hashtableContent = $hashtableContent -replace "(?m)(^\s*appScriptDate\s*=)\s*'[^']+'", "`$1 '$(Get-Date -Format "yyyy-MM-dd")'" + + # Update the content of the v4 template script + $start = $hashtableAst.Right.Extent.StartOffset + $end = $hashtableAst.Right.Extent.EndOffset + $scriptContent = $tempScriptAst.Extent.Text + $newScriptContent = ($scriptContent.Substring(0, $start) + $hashtableContent + $scriptContent.Substring($end)).Trim() + Set-Content -Path $outputScriptPath -Value $newScriptContent -Encoding UTF8 + } + else + { + Write-Warning -Message 'Could not find [$adtSession] hashtable' + } + + Write-Verbose -Message "Removing temp script [$inputScriptPath]" + Remove-Item -LiteralPath $inputScriptPath -Force + + if ($Path -like '*.ps1') + { + Write-Verbose -Message "Moving file [$outputScriptPath] to [$Destination]" + Move-Item -LiteralPath $outputScriptPath -Destination $Destination -Force -PassThru:$PassThru + + # Display the newly created file in Windows Explorer (/select highlights the file in the folder). + if ($Show) + { + Write-Verbose -Message "Selecting [$Destination] in Windows Explorer" + & ([System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows), 'explorer.exe')) /select, $Destination + } + } + else + { + # If processing a folder, also copy the Files and SupportFiles subfolders + foreach ($subFolder in 'Files', 'SupportFiles') + { + $subFolderPath = [System.IO.Path]::Combine($Path, $subFolder) + if ([System.IO.Directory]::Exists($subFolderPath)) + { + Write-Verbose -Message "Copying $subFolder content" + Copy-Item -LiteralPath $subFolderPath -Destination $tempFolderPath -Recurse -Force + } + } + + # Remove the Destination if it already exists (we should have already exited by this point if folder exists and Force not specified) + if (Test-Path -LiteralPath $Destination) + { + Write-Verbose -Message "Removing existing destination folder [$Destination]" + Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue -WhatIf + } + + # Sometimes previous actions were leaving a lock on the temp folder, so set up a retry loop + for ($i = 0; $i -lt 5; $i++) + { + try + { + Write-Verbose -Message "Moving folder [$tempFolderPath] to [$Destination]" + Move-Item -Path $tempFolderPath -Destination $Destination -Force -PassThru:$PassThru + Write-Information -MessageData "Conversion successful: $Destination" + break + } + catch + { + Write-Verbose -Message "Failed to move folder. Trying again in 500ms." + [System.Threading.Thread]::Sleep(500) + if ($i -eq 4) + { + throw + } + } + } + + # Display the newly created folder in Windows Explorer. + if ($Show) + { + Write-Verbose -Message "Opening [$Destination] in Windows Explorer" + & ([System.IO.Path]::Combine([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows), 'explorer.exe')) $Destination + } + + } + } + catch + { + # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. + Write-Error -ErrorRecord $_ + } + finally + { + if (Test-Path -LiteralPath $tempFolderPath) + { + Write-Verbose -Message "Removing temp folder [$tempFolderPath]" + Remove-Item -Path $tempFolderPath -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + catch + { + # Process the caught error, log it and throw depending on the specified ErrorAction. + Invoke-ADTFunctionErrorHandler -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ + } + } + + end + { + # Finalize function. + Complete-ADTFunction -Cmdlet $PSCmdlet + } +} diff --git a/src/PSAppDeployToolkit.Tools/Public/Test-ADTCompatibility.ps1 b/src/PSAppDeployToolkit.Tools/Public/Test-ADTCompatibility.ps1 new file mode 100644 index 0000000..e698624 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/Public/Test-ADTCompatibility.ps1 @@ -0,0 +1,126 @@ +#----------------------------------------------------------------------------- +# +# MARK: Test-ADTCompatibility +# +#----------------------------------------------------------------------------- + +function Test-ADTCompatibility +{ + <# + .SYNOPSIS + Tests a PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1 for any deprecated v3.x command or variable usage. + + .DESCRIPTION + The Test-ADTCompatibility function run custom PSScriptAnalyzer rules against the input file and output any detected issues. The results can be output in a variety of formats. + + .PARAMETER FilePath + Path to the .ps1 file to analyze. + + .PARAMETER Format + Specifies the output format. The acceptable values for this parameter are: Raw, Table, Grid. The default value is Raw, which outputs the raw DiagnosticRecord objects from PSScriptAnalyzer. Table outputs just the line numbers and messages as a table. Grid outputs the line numbers and messages in a graphical window. + + .INPUTS + System.String + + You can pipe script files to this function. + + .OUTPUTS + Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord + + Returns the standard output from Invoke-ScriptAnalyzer. + + .EXAMPLE + Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 + + This example analyzes Deploy-Application.ps1 and outputs the results. + + .EXAMPLE + Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Table + + This example analyzes Deploy-Application.ps1 and outputs the results as a table. + + .EXAMPLE + Test-ADTCompatibility -FilePath .\Deploy-Application.ps1 -Format Grid + + This example analyzes Deploy-Application.ps1 and outputs the results as a grid view. + + .NOTES + An active ADT session is NOT required to use this function. + Requires PSScriptAnalyzer module 1.23.0 or later. To install: + + Install-Module -Name PSScriptAnalyzer -Scope CurrentUser + Install-Module -Name PSScriptAnalyzer -Scope AllUsers + + Tags: psadt + Website: https://psappdeploytoolkit.com + Copyright: (C) 2024 PSAppDeployToolkit Team (Sean Lillis, Dan Cunningham, Muhammad Mashwani, Mitch Richters, Dan Gough). + License: https://opensource.org/license/lgpl-3-0 + + .LINK + https://psappdeploytoolkit.com + #> + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('FullName')] + [ValidateScript({ + if (![System.IO.File]::Exists($_)) + { + $PSCmdlet.ThrowTerminatingError((New-ADTValidateScriptErrorRecord -ParameterName FilePath -ProvidedValue $_ -ExceptionMessage 'The specified file does not exist.')) + } + return ![System.String]::IsNullOrWhiteSpace($_) + })] + [System.String]$FilePath, + + [Parameter(Mandatory = $false)] + [ValidateSet('Raw', 'Table', 'Grid')] + [System.String]$Format = 'Raw' + ) + + begin + { + # Initialize function. + Initialize-ADTFunction -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState + + $customRulePath = [System.IO.Path]::Combine($MyInvocation.MyCommand.Module.ModuleBase, 'PSScriptAnalyzer\Measure-ADTCompatibility.psm1') + } + + process + { + try + { + try + { + if ($FilePath -notmatch '(Deploy-Application\.ps1|Invoke-AppDeployToolkit\.ps1)$') + { + Write-Warning -Message "This function is designed to test PSAppDeployToolkit deployment scripts such as Deploy-Application.ps1 or Invoke-AppDeployToolkit.ps1." + } + $results = Invoke-ScriptAnalyzer -Path $FilePath -CustomRulePath $customRulePath + + switch ($Format) + { + 'Table' { $results | Format-Table -AutoSize -Wrap -Property Line, Message } + 'Grid' { $results | Select-Object Line, Message | Out-GridView -Title "Test-ADTCompatibility: $FilePath" -OutputMode None } + 'Raw' { $results } + } + } + catch + { + # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. + Write-Error -ErrorRecord $_ + } + } + catch + { + # Process the caught error, log it and throw depending on the specified ErrorAction. + Invoke-ADTFunctionErrorHandler -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -ErrorRecord $_ + } + } + + end + { + # Finalize function. + Complete-ADTFunction -Cmdlet $PSCmdlet + } +} diff --git a/src/Tests/Integration/SampleIntegrationTest.Tests.ps1 b/src/Tests/Integration/SampleIntegrationTest.Tests.ps1 new file mode 100644 index 0000000..917d586 --- /dev/null +++ b/src/Tests/Integration/SampleIntegrationTest.Tests.ps1 @@ -0,0 +1,17 @@ +# BeforeAll { +# Set-Location -Path $PSScriptRoot +# $ModuleName = 'PSAppDeployToolkit.Tools' +# $PathToManifest = [System.IO.Path]::Combine('..', '..', 'Artifacts', "$ModuleName.psd1") +# #if the module is already in memory, remove it +# Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force +# Import-Module $PathToManifest -Force +# } + +# Describe 'Integration Tests' -Tag Integration { +# Context 'First Integration Tests' { +# It 'should pass the first integration test' { +# # test logic +# } #it +# } +# } + diff --git a/src/Tests/Unit/ExportedFunctions.Tests.ps1 b/src/Tests/Unit/ExportedFunctions.Tests.ps1 new file mode 100644 index 0000000..c315568 --- /dev/null +++ b/src/Tests/Unit/ExportedFunctions.Tests.ps1 @@ -0,0 +1,61 @@ +BeforeAll { + Set-Location -Path $PSScriptRoot + $ModuleName = 'PSAppDeployToolkit.Tools' + $PathToManifest = [System.IO.Path]::Combine('..', '..', $ModuleName, "$ModuleName.psd1") + Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force + Import-Module $PathToManifest -Force + $manifestContent = Test-ModuleManifest -Path $PathToManifest + $moduleExported = Get-Command -Module $ModuleName | Select-Object -ExpandProperty Name + $manifestExported = ($manifestContent.ExportedFunctions).Keys +} +BeforeDiscovery { + Set-Location -Path $PSScriptRoot + $ModuleName = 'PSAppDeployToolkit.Tools' + $PathToManifest = [System.IO.Path]::Combine('..', '..', $ModuleName, "$ModuleName.psd1") + $manifestContent = Test-ModuleManifest -Path $PathToManifest + $moduleExported = Get-Command -Module $ModuleName | Select-Object -ExpandProperty Name + $manifestExported = ($manifestContent.ExportedFunctions).Keys +} +Describe $ModuleName { + + Context 'Exported Commands' -Fixture { + + Context 'Number of commands' -Fixture { + + It 'Exports the same number of public functions as what is listed in the Module Manifest' { + ($manifestExported | Measure-Object).Count | Should -BeExactly ($moduleExported | Measure-Object).Count + } + + } + + Context 'Explicitly exported commands' { + + It 'Includes <_> in the Module Manifest ExportedFunctions' -ForEach $moduleExported { + $manifestExported -contains $_ | Should -BeTrue + } + + } + } #context_ExportedCommands + + Context 'Command Help' -Fixture { + Context '<_>' -Foreach $moduleExported { + + BeforeEach { + $help = Get-Help -Name $_ -Full + } + + It -Name 'Includes a Synopsis' -Test { + $help.Synopsis | Should -Not -BeNullOrEmpty + } + + It -Name 'Includes a Description' -Test { + $help.description.Text | Should -Not -BeNullOrEmpty + } + + It -Name 'Includes an Example' -Test { + $help.examples.example | Should -Not -BeNullOrEmpty + } + } + } #context_CommandHelp +} + diff --git a/src/Tests/Unit/PSAppDeployToolkit.ModuleScaffold-Module.Tests.ps1 b/src/Tests/Unit/PSAppDeployToolkit.ModuleScaffold-Module.Tests.ps1 new file mode 100644 index 0000000..f4569a8 --- /dev/null +++ b/src/Tests/Unit/PSAppDeployToolkit.ModuleScaffold-Module.Tests.ps1 @@ -0,0 +1,49 @@ +BeforeAll { + #------------------------------------------------------------------------- + Set-Location -Path $PSScriptRoot + #------------------------------------------------------------------------- + $ModuleName = 'PSAppDeployToolkit.Tools' + $PathToManifest = [System.IO.Path]::Combine('..', '..', $ModuleName, "$ModuleName.psd1") + $PathToModule = [System.IO.Path]::Combine('..', '..', $ModuleName, "$ModuleName.psm1") + #------------------------------------------------------------------------- +} +Describe 'Module Tests' -Tag Unit { + Context "Module Tests" { + $script:manifestEval = $null + It 'Passes Test-ModuleManifest' { + { $script:manifestEval = Test-ModuleManifest -Path $PathToManifest } | Should -Not -Throw + $? | Should -BeTrue + } #manifestTest + It 'root module PSAppDeployToolkit.Tools.psm1 should exist' { + $PathToModule | Should -Exist + $? | Should -BeTrue + } #psm1Exists + It 'manifest should contain PSAppDeployToolkit.Tools.psm1' { + $PathToManifest | + Should -FileContentMatchExactly "PSAppDeployToolkit.Tools.psm1" + } #validPSM1 + It 'should have a matching module name in the manifest' { + $script:manifestEval.Name | Should -BeExactly $ModuleName + } #name + It 'should have a valid description in the manifest' { + $script:manifestEval.Description | Should -Not -BeNullOrEmpty + } #description + It 'should have a valid author in the manifest' { + $script:manifestEval.Author | Should -Not -BeNullOrEmpty + } #author + It 'should have a valid version in the manifest' { + $script:manifestEval.Version -as [Version] | Should -Not -BeNullOrEmpty + } #version + It 'should have a valid guid in the manifest' { + { [guid]::Parse($script:manifestEval.Guid) } | Should -Not -Throw + } #guid + It 'should not have any spaces in the tags' { + foreach ($tag in $script:manifestEval.Tags) { + $tag | Should -Not -Match '\s' + } + } #tagSpaces + It 'should have a valid project Uri' { + $script:manifestEval.ProjectUri | Should -Not -BeNullOrEmpty + } #uri + } #context_ModuleTests +} #describe_ModuleTests diff --git a/src/Tools/MarkdownRepair.ps1 b/src/Tools/MarkdownRepair.ps1 new file mode 100644 index 0000000..aa011ec --- /dev/null +++ b/src/Tools/MarkdownRepair.ps1 @@ -0,0 +1,135 @@ +<# +.SYNOPSIS + Repair PlatyPS generated markdown files. +.NOTES + This file is temporarily required to handle platyPS help generation. + https://github.com/PowerShell/platyPS/issues/595 + This is a result of a breaking change introduced in PowerShell 7.4.0: + https://learn.microsoft.com/en-us/powershell/scripting/whats-new/what-s-new-in-powershell-74?view=powershell-7.4 + Breaking Changes: Added the ProgressAction parameter to the Common Parameters + modified from source: https://github.com/PowerShell/platyPS/issues/595#issuecomment-1820971702 +#> + +function Remove-CommonParameterFromMarkdown { + <# + .SYNOPSIS + Remove a PlatyPS generated parameter block. + .DESCRIPTION + Removes parameter block for the provided parameter name from the markdown file provided. + #> + param( + [Parameter(Mandatory)] + [string[]] + $Path, + + [Parameter(Mandatory = $false)] + [string[]] + $ParameterName = @('ProgressAction') + ) + $ErrorActionPreference = 'Stop' + foreach ($p in $Path) { + $content = (Get-Content -Path $p -Raw).TrimEnd() + $updateFile = $false + foreach ($param in $ParameterName) { + if (-not ($Param.StartsWith('-'))) { + $param = "-$($param)" + } + # Remove the parameter block + $pattern = "(?m)^### $param\r?\n[\S\s]*?(?=#{2,3}?)" + $newContent = $content -replace $pattern, '' + # Remove the parameter from the syntax block + $pattern = " \[$param\s?.*?]" + $newContent = $newContent -replace $pattern, '' + if ($null -ne (Compare-Object -ReferenceObject $content -DifferenceObject $newContent)) { + Write-Verbose "Added $param to $p" + # Update file content + $content = $newContent + $updateFile = $true + } + } + # Save file if content has changed + if ($updateFile) { + $newContent | Out-File -Encoding utf8 -FilePath $p + Write-Verbose "Updated file: $p" + } + } + return +} + +function Add-MissingCommonParameterToMarkdown { + param( + [Parameter(Mandatory)] + [string[]] + $Path, + + [Parameter(Mandatory = $false)] + [string[]] + $ParameterName = @('ProgressAction') + ) + $ErrorActionPreference = 'Stop' + foreach ($p in $Path) { + $content = (Get-Content -Path $p -Raw).TrimEnd() + $updateFile = $false + foreach ($NewParameter in $ParameterName) { + if (-not ($NewParameter.StartsWith('-'))) { + $NewParameter = "-$($NewParameter)" + } + $pattern = '(?m)^This cmdlet supports the common parameters:(.+?)\.' + $replacement = { + $Params = $_.Groups[1].Captures[0].ToString() -split ' ' + $CommonParameters = @() + foreach ($CommonParameter in $Params) { + if ($CommonParameter.StartsWith('-')) { + if ($CommonParameter.EndsWith(',')) { + $CleanParam = $CommonParameter.Substring(0, $CommonParameter.Length - 1) + } + elseif ($p.EndsWith('.')) { + $CleanParam = $CommonParameter.Substring(0, $CommonParameter.Length - 1) + } + else { + $CleanParam = $CommonParameter + } + $CommonParameters += $CleanParam + } + } + if ($NewParameter -notin $CommonParameters) { + $CommonParameters += $NewParameter + } + $CommonParameters[-1] = "and $($CommonParameters[-1]). " + return "This cmdlet supports the common parameters: " + (($CommonParameters | Sort-Object) -join ', ') + } + $newContent = $content -replace $pattern, $replacement + if ($null -ne (Compare-Object -ReferenceObject $content -DifferenceObject $newContent)) { + Write-Verbose "Added $NewParameter to $p" + $updateFile = $true + $content = $newContent + } + } + # Save file if content has changed + if ($updateFile) { + $newContent | Out-File -Encoding utf8 -FilePath $p + Write-Verbose "Updated file: $p" + } + } + return +} + +function Repair-PlatyPSMarkdown { + param( + [Parameter(Mandatory)] + [string[]] + $Path, + + [Parameter()] + [string[]] + $ParameterName = @('ProgressAction') + ) + $ErrorActionPreference = 'Stop' + $Parameters = @{ + Path = $Path + ParameterName = $ParameterName + } + $null = Remove-CommonParameterFromMarkdown @Parameters + $null = Add-MissingCommonParameterToMarkdown @Parameters + return +}