From 47de162baf0a814cea608e1efe34fdf04eb0e069 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Tue, 3 Dec 2024 11:47:12 +0000 Subject: [PATCH 01/18] Initial module --- .editorconfig | 11 + .github/CODE_OF_CONDUCT.md | 44 + .github/CONTRIBUTING.md | 50 + .github/ISSUE_TEMPLATE/bug_report.yaml | 81 + .github/ISSUE_TEMPLATE/config.yml | 7 + .github/ISSUE_TEMPLATE/feature_request.yaml | 20 + .github/PULL_REQUEST_TEMPLATE/adr_template.md | 23 + .../pull_request_template.md | 30 + .github/SECURITY.md | 45 + .github/SUPPORT.md | 7 + .github/release-drafter.yml | 61 + .github/workflows/wf_Windows.yml | 95 ++ .vscode/extensions.json | 10 + .vscode/settings.json | 23 + .vscode/tasks.json | 269 ++++ CHANGELOG.txt | 1 + COPYING.Lesser | 165 ++ README.md | 35 + actions_bootstrap.ps1 | 70 + docs/Convert-ADTDeployment.mdx | 146 ++ docs/Test-ADTCompatibility.mdx | 117 ++ src/PSAppDeployToolkit.Tools.build.ps1 | 567 +++++++ src/PSAppDeployToolkit.Tools/ImportsFirst.ps1 | 64 + src/PSAppDeployToolkit.Tools/ImportsLast.ps1 | 11 + .../PSAppDeployToolkit.Tools.psd1 | 121 ++ .../PSAppDeployToolkit.Tools.psm1 | 21 + .../Measure-ADTCompatibility.psm1 | 1392 +++++++++++++++++ .../Public/Convert-ADTDeployment.ps1 | 365 +++++ .../Public/Test-ADTCompatibility.ps1 | 126 ++ .../SampleIntegrationTest.Tests.ps1 | 17 + src/Tests/Unit/ExportedFunctions.Tests.ps1 | 61 + ...loyToolkit.ModuleScaffold-Module.Tests.ps1 | 49 + src/Tools/MarkdownRepair.ps1 | 135 ++ 33 files changed, 4239 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE/adr_template.md create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/SECURITY.md create mode 100644 .github/SUPPORT.md create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/wf_Windows.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CHANGELOG.txt create mode 100644 COPYING.Lesser create mode 100644 README.md create mode 100644 actions_bootstrap.ps1 create mode 100644 docs/Convert-ADTDeployment.mdx create mode 100644 docs/Test-ADTCompatibility.mdx create mode 100644 src/PSAppDeployToolkit.Tools.build.ps1 create mode 100644 src/PSAppDeployToolkit.Tools/ImportsFirst.ps1 create mode 100644 src/PSAppDeployToolkit.Tools/ImportsLast.ps1 create mode 100644 src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 create mode 100644 src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 create mode 100644 src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 create mode 100644 src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 create mode 100644 src/PSAppDeployToolkit.Tools/Public/Test-ADTCompatibility.ps1 create mode 100644 src/Tests/Integration/SampleIntegrationTest.Tests.ps1 create mode 100644 src/Tests/Unit/ExportedFunctions.Tests.ps1 create mode 100644 src/Tests/Unit/PSAppDeployToolkit.ModuleScaffold-Module.Tests.ps1 create mode 100644 src/Tools/MarkdownRepair.ps1 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..2fd7a50 --- /dev/null +++ b/.github/workflows/wf_Windows.yml @@ -0,0 +1,95 @@ +# 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-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.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 + path: .\src\Artifacts\Module + if-no-files-found: error + overwrite: true + - name: Upload v3 module template + uses: actions/upload-artifact@v4 + with: + name: PSAppDeployToolkit_Template_v3 + path: .\src\Artifacts\Template_v3 + if-no-files-found: error + overwrite: true + - name: Upload v4 module template + uses: actions/upload-artifact@v4 + with: + name: PSAppDeployToolkit_Template_v4 + path: .\src\Artifacts\Template_v4 + 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..8867c84 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# ![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 your PSAppDeployToolkit v3 scripts to get a full report on which functions and variables have changed in v4. +- Convert a PSAppDeployToolkit v3 script or an entire package folder to v4 standards. + +## Getting Started + +-> [System Requirements](https://psappdeploytoolkit.com/docs/getting-started/requirements) +-> [Downloading](https://psappdeploytoolkit.com/docs/getting-started/download) + +### 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..d5e4e31 --- /dev/null +++ b/actions_bootstrap.ps1 @@ -0,0 +1,70 @@ +# 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/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 + } + 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..8944ac9 --- /dev/null +++ b/docs/Convert-ADTDeployment.mdx @@ -0,0 +1,146 @@ +--- +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] ] [-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 +``` + +### -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..e931cb3 --- /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)) + { + # 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. + # if (($canSign = ($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..8568ca6 --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -0,0 +1,121 @@ +# +# 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.0.1' + + # 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 = @('PSAppDeployToolkit', 'PSScriptAnalyzer') # 'PSAppDeployToolkit >= 3.93.0', 'PSScriptAnalyzer >= 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 = @{ + + # 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/latest' + + } # 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..09a5f7a --- /dev/null +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 @@ -0,0 +1,21 @@ +#----------------------------------------------------------------------------- +# +# 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 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..f62a094 --- /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 = $null + configBalloonTextError = $null + configBalloonTextFastRetry = $null + configBalloonTextRestartRequired = $null + configBalloonTextStart = $null + configBannerIconBannerName = $null + configBannerIconFileName = $null + configBannerLogoImageFileName = $null + configBlockExecutionMessage = $null + configClosePromptButtonClose = $null + configClosePromptButtonContinue = $null + configClosePromptButtonContinueTooltip = $null + configClosePromptButtonDefer = $null + configClosePromptCountdownMessage = $null + configClosePromptMessage = $null + configConfigDate = $null + configConfigDetails = $null + configConfigVersion = $null + configDeferPromptDeadline = $null + configDeferPromptExpiryMessage = $null + configDeferPromptRemainingDeferrals = $null + configDeferPromptWarningMessage = $null + configDeferPromptWelcomeMessage = $null + configDeploymentTypeInstall = $null + configDeploymentTypeRepair = $null + configDeploymentTypeUnInstall = $null + configDiskSpaceMessage = $null + configInstallationDeferExitCode = $null + configInstallationPersistInterval = $null + configInstallationPromptToSave = $null + configInstallationRestartPersistInterval = $null + configInstallationUIExitCode = $null + configInstallationUILanguageOverride = $null + configInstallationUITimeout = $null + configInstallationWelcomePromptDynamicRunningProcessEvaluation = $null + configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval = $null + configMSIInstallParams = $null + configMSILogDir = $null + configMSILoggingOptions = $null + configMSIMutexWaitTime = $null + configMSISilentParams = $null + configMSIUninstallParams = $null + configProgressMessageInstall = $null + configProgressMessageRepair = $null + configProgressMessageUninstall = $null + configRestartPromptButtonRestartLater = $null + configRestartPromptButtonRestartNow = $null + configRestartPromptMessage = $null + configRestartPromptMessageRestart = $null + configRestartPromptMessageTime = $null + configRestartPromptTimeRemaining = $null + configRestartPromptTitle = $null + configShowBalloonNotifications = $null + configToastAppName = $null + configToastDisable = $null + configToolkitCachePath = $null + configToolkitCompressLogs = $null + configToolkitLogAppend = $null + configToolkitLogDebugMessage = $null + configToolkitLogDir = $null + configToolkitLogMaxHistory = $null + configToolkitLogMaxSize = $null + configToolkitLogStyle = $null + configToolkitLogWriteToHost = $null + configToolkitRegPath = $null + configToolkitRequireAdmin = $null + configToolkitTempPath = $null + configToolkitUseRobocopy = $null + configWelcomePromptCountdownMessage = $null + configWelcomePromptCustomMessage = $null + 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 + Matches = $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)] + [string]$Destination = (Split-Path -Path $Path -Parent), + + [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($MyInvocation.MyCommand.Module.ModuleBase, 'Frontend\v4\Invoke-AppDeployToolkit.ps1') + } + + process + { + try + { + try + { + $tempFolderName = "Convert-ADTDeployment_$([System.IO.Path]::GetRandomFileName().Replace('.', ''))" + $tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFolderName) + + if ([System.IO.File]::Exists($Path)) + { + Write-Verbose -Message "Input path is a 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') } + + # 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 + New-Item -Path $tempPath -ItemType Directory -Force | Out-Null + # Create a temp copy of the script to run ScriptAnalyzer fixes on - prefix filename with _ in case it's named Invoke-AppDeployToolkit.ps1 + $inputScriptPath = [System.IO.Path]::Combine(([System.IO.Path]::GetDirectoryName($fullPath)), "_$([System.IO.Path]::GetFileName($fullPath))") + Copy-Item -LiteralPath $Path -Destination $inputScriptPath -Force -PassThru + # Copy over our template v4 script + $tempScriptPath = (Copy-Item -LiteralPath $templateScriptPath -Destination $tempPath -Force -PassThru).FullName + } + elseif ([System.IO.Directory]::Exists($Path)) + { + 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 + $Destination = [System.IO.Path]::Combine($Destination, $folderName) + + # 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) + } + + # Use New-ADTTemplate to generate our temp folder + New-ADTTemplate -Destination ([System.IO.Path]::GetTempPath()) -Name $tempFolderName + + # Create a temp copy of Deploy-Application.ps1 to run ScriptAnalyzer fixes on + $inputScriptPath = (Copy-Item -LiteralPath ([System.IO.Path]::Combine($Path, 'Deploy-Application.ps1')) -Destination $tempPath -Force -PassThru).FullName + + # Set the path of our v4 template script + $tempScriptPath = [System.IO.Path]::Combine($tempPath, 'Invoke-AppDeployToolkit.ps1') + } + + # First run the fixes on the input script to update function names and variables + 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) + { + # Re-read and parse the v4 template script after each replacement so that the offsets will still be valid + $tempScriptContent = Get-Content -Path $tempScriptPath -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) + { + # 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 $tempScriptPath -Value $newScriptContent -Encoding UTF8 + } + } + } + + # Re-read and parse the script one more time + $tempScriptContent = Get-Content -Path $tempScriptPath -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) + { + # Get the text form of the hashtable definition + $hashtableContent = $hashtableAst.Right.Extent.Text + + # Update the appScriptDate value to the current date + $hashtableContent = $hashtableContent -replace "appScriptDate\s*=\s*'[^']+'", "appScriptDate = '$(Get-Date -Format "yyyy-MM-dd")'" + + # 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 "^\[[^\]]+\]?\`$$variableReplacement$" + }, $true) + + if ($assignmentAst) + { + $variableValue = $assignmentAst.Right.Extent.Text + $hashtableContent = $hashtableContent -replace "$variableReplacement\s*=\s*'[^']*'", "$variableReplacement = $variableValue" + } + } + + # 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 $tempScriptPath -Value $newScriptContent -Encoding UTF8 + } + + # Delete the temporary copy of the v3 script used for processing + Remove-Item -LiteralPath $inputScriptPath -Force + + if ($Path -like '*.ps1') + { + # Move the updated script to the destination + Move-Item -LiteralPath $tempScriptPath -Destination $Destination -Force -PassThru:$PassThru + } + 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)) + { + Copy-Item -LiteralPath $subFolderPath -Destination $tempPath -Recurse -Force + } + } + + # Remove the Destination if it already exists (Force checks were done earlier) + if (Test-Path -LiteralPath $Destination) + { + Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue + } + + # 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 + { + Move-Item -Path $tempPath -Destination $Destination -Force -PassThru:$PassThru + Write-Information -MessageData "Conversion successful: $Destination" + break + } + catch + { + Write-Verbose -Message "Failed to move [$tempPath] to [$Destination]. Trying again in 500ms." + [System.Threading.Thread]::Sleep(500) + if ($i -eq 4) + { + throw + } + } + } + + } + } + catch + { + # Re-writing the ErrorRecord with Write-Error ensures the correct PositionMessage is used. + Write-Error -ErrorRecord $_ + } + finally + { + if (Test-Path -LiteralPath $tempPath) + { + Write-Verbose -Message "Removing temp path [$tempPath]" + Remove-Item -Path $tempPath -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 +} From 9a2f87ce0dccb1895bf232b77bc821fcf5170097 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Tue, 3 Dec 2024 12:22:22 +0000 Subject: [PATCH 02/18] Update workflow to work with this module --- .github/workflows/wf_Windows.yml | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/wf_Windows.yml b/.github/workflows/wf_Windows.yml index 2fd7a50..f3035ae 100644 --- a/.github/workflows/wf_Windows.yml +++ b/.github/workflows/wf_Windows.yml @@ -5,7 +5,7 @@ # 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-Windows-PowerShell +name: PSAppDeployToolkit.Tools-Windows-PowerShell on: pull_request: paths-ignore: @@ -57,7 +57,7 @@ jobs: subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Test and Build shell: powershell - run: Invoke-Build -File .\src\PSAppDeployToolkit.build.ps1 + run: Invoke-Build -File .\src\PSAppDeployToolkit.Tools.build.ps1 - name: Upload pester results uses: actions/upload-artifact@v4 with: @@ -75,21 +75,7 @@ jobs: - name: Upload module uses: actions/upload-artifact@v4 with: - name: PSAppDeployToolkit + name: PSAppDeployToolkit.Tools path: .\src\Artifacts\Module if-no-files-found: error overwrite: true - - name: Upload v3 module template - uses: actions/upload-artifact@v4 - with: - name: PSAppDeployToolkit_Template_v3 - path: .\src\Artifacts\Template_v3 - if-no-files-found: error - overwrite: true - - name: Upload v4 module template - uses: actions/upload-artifact@v4 - with: - name: PSAppDeployToolkit_Template_v4 - path: .\src\Artifacts\Template_v4 - if-no-files-found: error - overwrite: true From 0ab18f1a7da984aea6ef929c2a312b42ff2780d9 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 00:01:03 +0000 Subject: [PATCH 03/18] Set version to 0.1.0, add Prerelease = 'beta' tag --- src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 index 8568ca6..6925f64 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -9,7 +9,7 @@ RootModule = 'PSAppDeployToolkit.Tools.psm1' # Version number of this module. - ModuleVersion = '0.0.1' + ModuleVersion = '0.1.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -94,6 +94,9 @@ 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' @@ -107,7 +110,7 @@ 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/latest' + ReleaseNotes = 'https://github.com/PSAppDeployToolkit/PSAppDeployToolkit.Tools/releases' } # End of PSData hashtable From 707607c0847c4fb3e87e40da5f471aa92d0a956c Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 00:09:58 +0000 Subject: [PATCH 04/18] Re-enable code signing --- src/PSAppDeployToolkit.Tools.build.ps1 | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools.build.ps1 b/src/PSAppDeployToolkit.Tools.build.ps1 index e931cb3..34a38a0 100644 --- a/src/PSAppDeployToolkit.Tools.build.ps1 +++ b/src/PSAppDeployToolkit.Tools.build.ps1 @@ -507,23 +507,23 @@ Add-BuildTask Build { Write-Build Gray ' ...Docs output completed.' } - # Sign our files if we're running on main. - # if (($canSign = ($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...' - # } + # Sign our files if we're running on main or develop. + if (($canSign = ($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!' } From 789ae821e7d850317845fbb91c184dd8b6c84d03 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 00:31:31 +0000 Subject: [PATCH 05/18] Update module dependencies --- actions_bootstrap.ps1 | 5 +++++ src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index d5e4e31..7e9c5e8 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -8,6 +8,11 @@ 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 = '3.93.0' + })) # https://github.com/pester/Pester [void]$modulesToInstall.Add(([PSCustomObject]@{ ModuleName = 'Pester' diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 index 6925f64..e49ddeb 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -48,7 +48,10 @@ ProcessorArchitecture = 'None' # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('PSAppDeployToolkit', 'PSScriptAnalyzer') # 'PSAppDeployToolkit >= 3.93.0', 'PSScriptAnalyzer >= 1.23.0' + RequiredModules = @( + @{ModuleName = 'PSAppDeployToolkit'; GUID = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '3.93.0'; } + @{ModuleName = 'PSScriptAnalyzer'; GUID = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0'; } + ) # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() From e978c8e88fd2090286c36969db0727f744a5f143 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 00:39:12 +0000 Subject: [PATCH 06/18] Add -AllowPreRelease to installation of dependent PSAppDeployToolkit module so that we can test this before release --- actions_bootstrap.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index 7e9c5e8..9b7fb0c 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -60,6 +60,11 @@ foreach ($module in $modulesToInstall) { # 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 } From a98e75cf4e99a635558bf008ba6d701ac04ecafd Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 23:38:02 +0000 Subject: [PATCH 07/18] RegEx fixes --- .../Public/Convert-ADTDeployment.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 b/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 index 00c107c..a16dd52 100644 --- a/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 +++ b/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 @@ -263,20 +263,20 @@ function Convert-ADTDeployment $hashtableContent = $hashtableAst.Right.Extent.Text # Update the appScriptDate value to the current date - $hashtableContent = $hashtableContent -replace "appScriptDate\s*=\s*'[^']+'", "appScriptDate = '$(Get-Date -Format "yyyy-MM-dd")'" + $hashtableContent = $hashtableContent -replace "(?m)(^\s*appScriptDate\s*=)\s*'[^']+'", "`$1 '$(Get-Date -Format "yyyy-MM-dd")'" # 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 "^\[[^\]]+\]?\`$$variableReplacement$" + $ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and $ast.Left.Extent.Text -match "^(\[[^\]]+\])?\`$(adtSession\.)?$variableReplacement$" }, $true) if ($assignmentAst) { $variableValue = $assignmentAst.Right.Extent.Text - $hashtableContent = $hashtableContent -replace "$variableReplacement\s*=\s*'[^']*'", "$variableReplacement = $variableValue" + $hashtableContent = $hashtableContent -replace "(?m)(^\s*$variableReplacement\s*=)\s*'[^']*'", "`$1 $variableValue" } } From 1ac45551347a485b56fc273e6c8fbaa2e7d1a345 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 23:38:57 +0000 Subject: [PATCH 08/18] Bump dependency version of PSAppDeployToolkit to 4.0.1 --- src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 index e49ddeb..69b3259 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -49,7 +49,7 @@ # Modules that must be imported into the global environment prior to importing this module RequiredModules = @( - @{ModuleName = 'PSAppDeployToolkit'; GUID = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '3.93.0'; } + @{ModuleName = 'PSAppDeployToolkit'; GUID = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.1'; } @{ModuleName = 'PSScriptAnalyzer'; GUID = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0'; } ) From 9e547c01aa49b7392c4acc77cff5e8feeebf8888 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Wed, 4 Dec 2024 23:55:27 +0000 Subject: [PATCH 09/18] Update PSADT version in actions_bootstrap.ps1 --- actions_bootstrap.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index 9b7fb0c..518820d 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -11,7 +11,7 @@ $modulesToInstall = New-Object System.Collections.Generic.List[object] # https://github.com/PSAppDeployToolkit/PSAppDeployToolkit [void]$modulesToInstall.Add(([PSCustomObject]@{ ModuleName = 'PSAppDeployToolkit' - ModuleVersion = '3.93.0' + ModuleVersion = '4.0.1' })) # https://github.com/pester/Pester [void]$modulesToInstall.Add(([PSCustomObject]@{ From e73ed582e1ff83d35e3c4e1df200ce30690e9039 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 00:16:59 +0000 Subject: [PATCH 10/18] Remove unused $canSign variable --- src/PSAppDeployToolkit.Tools.build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools.build.ps1 b/src/PSAppDeployToolkit.Tools.build.ps1 index 34a38a0..ba6ce13 100644 --- a/src/PSAppDeployToolkit.Tools.build.ps1 +++ b/src/PSAppDeployToolkit.Tools.build.ps1 @@ -409,7 +409,7 @@ Add-BuildTask Build { # 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)) + $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() @@ -508,7 +508,7 @@ Add-BuildTask Build { } # Sign our files if we're running on main or develop. - if (($canSign = ($env:GITHUB_ACTIONS -eq 'true') -and ($env:GITHUB_REF_NAME -match '^(main|develop)$'))) + if ($env:GITHUB_ACTIONS -eq 'true' -and $env:GITHUB_REF_NAME -match '^(main|develop)$') { if (!(Get-Command -Name 'azuresigntool' -ErrorAction Ignore)) { From c3ee50b4cc1ba68a084079c26a73d5bdd6253102 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 10:59:40 +0000 Subject: [PATCH 11/18] Add Show switch, plus various fixes and verbose logging added --- .../Public/Convert-ADTDeployment.ps1 | 134 +++++++++++++----- 1 file changed, 98 insertions(+), 36 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 b/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 index a16dd52..59812ec 100644 --- a/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 +++ b/src/PSAppDeployToolkit.Tools/Public/Convert-ADTDeployment.ps1 @@ -18,6 +18,9 @@ function Convert-ADTDeployment .PARAMETER 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. + .PARAMETER Show + Opens the newly created output in Windows Explorer. + .PARAMETER Force Overwrite the output path if it already exists. @@ -91,8 +94,12 @@ function Convert-ADTDeployment [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, @@ -123,7 +130,7 @@ function Convert-ADTDeployment $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($MyInvocation.MyCommand.Module.ModuleBase, 'Frontend\v4\Invoke-AppDeployToolkit.ps1') + $templateScriptPath = [System.IO.Path]::Combine((Get-Module PSAppDeployToolkit).ModuleBase, 'Frontend\v4\Invoke-AppDeployToolkit.ps1') } process @@ -133,14 +140,15 @@ function Convert-ADTDeployment try { $tempFolderName = "Convert-ADTDeployment_$([System.IO.Path]::GetRandomFileName().Replace('.', ''))" - $tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFolderName) + $tempFolderPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), $tempFolderName) - if ([System.IO.File]::Exists($Path)) + if ($Path -like '*.ps1') { - Write-Verbose -Message "Input path is a file: $Path" + 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)) @@ -155,28 +163,49 @@ function Convert-ADTDeployment Write-Error -ErrorRecord (New-ADTErrorRecord @naerParams) } - if ($Path -notmatch '(Deploy-Application\.ps1|Invoke-AppDeployToolkit\.ps1)$') + 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 - New-Item -Path $tempPath -ItemType Directory -Force | Out-Null - # Create a temp copy of the script to run ScriptAnalyzer fixes on - prefix filename with _ in case it's named Invoke-AppDeployToolkit.ps1 - $inputScriptPath = [System.IO.Path]::Combine(([System.IO.Path]::GetDirectoryName($fullPath)), "_$([System.IO.Path]::GetFileName($fullPath))") - Copy-Item -LiteralPath $Path -Destination $inputScriptPath -Force -PassThru + 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 - $tempScriptPath = (Copy-Item -LiteralPath $templateScriptPath -Destination $tempPath -Force -PassThru).FullName + Write-Verbose -Message "Copying template script to [$tempFolderPath\Invoke-AppDeployToolkit.ps1]" + $outputScriptPath = (Copy-Item -LiteralPath $templateScriptPath -Destination $tempFolderPath -Force -PassThru).FullName } - elseif ([System.IO.Directory]::Exists($Path)) + else { - Write-Verbose -Message "Input path is a folder: $Path" + 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 - $Destination = [System.IO.Path]::Combine($Destination, $folderName) + # 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))) @@ -191,17 +220,18 @@ function Convert-ADTDeployment Write-Error -ErrorRecord (New-ADTErrorRecord @naerParams) } - # Use New-ADTTemplate to generate our temp folder + Write-Verbose -Message "Creating ADT Template in [$tempFolderPath]" New-ADTTemplate -Destination ([System.IO.Path]::GetTempPath()) -Name $tempFolderName - # Create a temp copy of Deploy-Application.ps1 to run ScriptAnalyzer fixes on - $inputScriptPath = (Copy-Item -LiteralPath ([System.IO.Path]::Combine($Path, 'Deploy-Application.ps1')) -Destination $tempPath -Force -PassThru).FullName + 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 - $tempScriptPath = [System.IO.Path]::Combine($tempPath, 'Invoke-AppDeployToolkit.ps1') + $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 @@ -225,8 +255,10 @@ function Convert-ADTDeployment 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 $tempScriptPath -Raw + $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 @@ -237,18 +269,24 @@ function Convert-ADTDeployment 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 $tempScriptPath -Value $newScriptContent -Encoding UTF8 + 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 $tempScriptPath -Raw + $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 @@ -259,12 +297,11 @@ function Convert-ADTDeployment if ($hashtableAst) { + Write-Verbose -Message 'Processing $adtSession hashtable' + # Get the text form of the hashtable definition $hashtableContent = $hashtableAst.Right.Extent.Text - # Update the appScriptDate value to the current date - $hashtableContent = $hashtableContent -replace "(?m)(^\s*appScriptDate\s*=)\s*'[^']+'", "`$1 '$(Get-Date -Format "yyyy-MM-dd")'" - # Copy each variable value from the input script to the hashtable foreach ($variableReplacement in $variableReplacements) { @@ -275,26 +312,41 @@ function Convert-ADTDeployment 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 $tempScriptPath -Value $newScriptContent -Encoding UTF8 + Set-Content -Path $outputScriptPath -Value $newScriptContent -Encoding UTF8 + } + else + { + Write-Warning -Message 'Could not find [$adtSession] hashtable' } - # Delete the temporary copy of the v3 script used for processing + Write-Verbose -Message "Removing temp script [$inputScriptPath]" Remove-Item -LiteralPath $inputScriptPath -Force if ($Path -like '*.ps1') { - # Move the updated script to the destination - Move-Item -LiteralPath $tempScriptPath -Destination $Destination -Force -PassThru:$PassThru + 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 { @@ -304,14 +356,16 @@ function Convert-ADTDeployment $subFolderPath = [System.IO.Path]::Combine($Path, $subFolder) if ([System.IO.Directory]::Exists($subFolderPath)) { - Copy-Item -LiteralPath $subFolderPath -Destination $tempPath -Recurse -Force + Write-Verbose -Message "Copying $subFolder content" + Copy-Item -LiteralPath $subFolderPath -Destination $tempFolderPath -Recurse -Force } } - # Remove the Destination if it already exists (Force checks were done earlier) + # 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) { - Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue + 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 @@ -319,13 +373,14 @@ function Convert-ADTDeployment { try { - Move-Item -Path $tempPath -Destination $Destination -Force -PassThru:$PassThru + 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 [$tempPath] to [$Destination]. Trying again in 500ms." + Write-Verbose -Message "Failed to move folder. Trying again in 500ms." [System.Threading.Thread]::Sleep(500) if ($i -eq 4) { @@ -334,6 +389,13 @@ function Convert-ADTDeployment } } + # 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 @@ -343,10 +405,10 @@ function Convert-ADTDeployment } finally { - if (Test-Path -LiteralPath $tempPath) + if (Test-Path -LiteralPath $tempFolderPath) { - Write-Verbose -Message "Removing temp path [$tempPath]" - Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Verbose -Message "Removing temp folder [$tempFolderPath]" + Remove-Item -Path $tempFolderPath -Recurse -Force -ErrorAction SilentlyContinue } } } From fbcd01fb07c862f9082196c6c4dce1f7feb4b185 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 11:00:43 +0000 Subject: [PATCH 12/18] Update help --- docs/Convert-ADTDeployment.mdx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/Convert-ADTDeployment.mdx b/docs/Convert-ADTDeployment.mdx index 8944ac9..6dfe374 100644 --- a/docs/Convert-ADTDeployment.mdx +++ b/docs/Convert-ADTDeployment.mdx @@ -12,7 +12,8 @@ Converts either a Deploy-Application.ps1 script, or a full application package t ## SYNTAX ```powershell -Convert-ADTDeployment [-Path] [[-Destination] ] [-Force] [-PassThru] [] +Convert-ADTDeployment [-Path] [[-Destination] ] [-Show] [-Force] [-PassThru] + [] ``` ## DESCRIPTION @@ -81,6 +82,22 @@ 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. From 970d1878d4cb9614e98eadac69032e5e468f60c3 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 11:21:27 +0000 Subject: [PATCH 13/18] Update dependency --- actions_bootstrap.ps1 | 2 +- src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index 518820d..a7ece92 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -11,7 +11,7 @@ $modulesToInstall = New-Object System.Collections.Generic.List[object] # https://github.com/PSAppDeployToolkit/PSAppDeployToolkit [void]$modulesToInstall.Add(([PSCustomObject]@{ ModuleName = 'PSAppDeployToolkit' - ModuleVersion = '4.0.1' + ModuleVersion = '4.0.2' })) # https://github.com/pester/Pester [void]$modulesToInstall.Add(([PSCustomObject]@{ diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 index 69b3259..66a5435 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -49,7 +49,7 @@ # 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.1'; } + @{ModuleName = 'PSAppDeployToolkit'; GUID = '8c3c366b-8606-4576-9f2d-4051144f7ca2'; ModuleVersion = '4.0.2'; } @{ModuleName = 'PSScriptAnalyzer'; GUID = 'd6245802-193d-4068-a631-8863a4342a18'; ModuleVersion = '1.23.0'; } ) From c06229c65c2bec383d6d4e678e9e51c72e4102b9 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 11:21:54 +0000 Subject: [PATCH 14/18] Stop psm1 processing empty Private dir to resolve build issue --- src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 index 09a5f7a..a49015b 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psm1 @@ -10,7 +10,8 @@ # 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\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 From cee0cef06a766cbafc054b36b8d4f9d63edcbd53 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 23:30:21 +0000 Subject: [PATCH 15/18] Add Config + String variable mappings --- .../Measure-ADTCompatibility.psm1 | 135 +++++++++--------- 1 file changed, 67 insertions(+), 68 deletions(-) diff --git a/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 index f62a094..15e44c8 100644 --- a/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 +++ b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 @@ -85,76 +85,76 @@ function Measure-ADTCompatibility ButtonRightText = $null CleanupBlockedApps = $null closeAppsCountdownGlobal = $null - configBalloonTextComplete = $null - configBalloonTextError = $null - configBalloonTextFastRetry = $null - configBalloonTextRestartRequired = $null - configBalloonTextStart = $null - configBannerIconBannerName = $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 = $null - configBlockExecutionMessage = $null - configClosePromptButtonClose = $null - configClosePromptButtonContinue = $null - configClosePromptButtonContinueTooltip = $null - configClosePromptButtonDefer = $null - configClosePromptCountdownMessage = $null - configClosePromptMessage = $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 = $null - configDeferPromptExpiryMessage = $null - configDeferPromptRemainingDeferrals = $null - configDeferPromptWarningMessage = $null - configDeferPromptWelcomeMessage = $null - configDeploymentTypeInstall = $null - configDeploymentTypeRepair = $null - configDeploymentTypeUnInstall = $null - configDiskSpaceMessage = $null - configInstallationDeferExitCode = $null - configInstallationPersistInterval = $null - configInstallationPromptToSave = $null - configInstallationRestartPersistInterval = $null - configInstallationUIExitCode = $null - configInstallationUILanguageOverride = $null - configInstallationUITimeout = $null - configInstallationWelcomePromptDynamicRunningProcessEvaluation = $null - configInstallationWelcomePromptDynamicRunningProcessEvaluationInterval = $null - configMSIInstallParams = $null - configMSILogDir = $null - configMSILoggingOptions = $null - configMSIMutexWaitTime = $null - configMSISilentParams = $null - configMSIUninstallParams = $null - configProgressMessageInstall = $null - configProgressMessageRepair = $null - configProgressMessageUninstall = $null - configRestartPromptButtonRestartLater = $null - configRestartPromptButtonRestartNow = $null - configRestartPromptMessage = $null - configRestartPromptMessageRestart = $null - configRestartPromptMessageTime = $null - configRestartPromptTimeRemaining = $null - configRestartPromptTitle = $null - configShowBalloonNotifications = $null - configToastAppName = $null - configToastDisable = $null - configToolkitCachePath = $null - configToolkitCompressLogs = $null - configToolkitLogAppend = $null - configToolkitLogDebugMessage = $null - configToolkitLogDir = $null - configToolkitLogMaxHistory = $null - configToolkitLogMaxSize = $null - configToolkitLogStyle = $null - configToolkitLogWriteToHost = $null - configToolkitRegPath = $null - configToolkitRequireAdmin = $null - configToolkitTempPath = $null - configToolkitUseRobocopy = $null - configWelcomePromptCountdownMessage = $null - configWelcomePromptCustomMessage = $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 @@ -186,7 +186,7 @@ function Measure-ADTCompatibility installationStarted = $null InvocationInfo = $null invokingScript = $null - IsOOBEComplete = '(Test-ADTOobeCompleted)' + IsOOBEComplete = 'Test-ADTOobeCompleted' IsTaskSchedulerHealthy = $null LocalPowerUsersGroup = $null LogFileInitialized = $null @@ -194,7 +194,6 @@ function Measure-ADTCompatibility LogicalScreenHeight = $null LogTimeZoneBias = $null mainExitCode = $null - Matches = $null Message = $null MessageAlignment = $null MinimizeWindows = $null From 079589cd9680816d2576687e97810677436f10fe Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Thu, 5 Dec 2024 23:56:03 +0000 Subject: [PATCH 16/18] Add parameter transforms for Path and Transform with Execute-MSI --- .../PSScriptAnalyzer/Measure-ADTCompatibility.psm1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 index 15e44c8..fa2d663 100644 --- a/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 +++ b/src/PSAppDeployToolkit.Tools/PSScriptAnalyzer/Measure-ADTCompatibility.psm1 @@ -723,11 +723,12 @@ function Measure-ADTCompatibility 'Execute-MSI' = @{ 'NewFunction' = 'Start-ADTMsiProcess' 'TransformParameters' = @{ - 'Path' = { "-FilePath $_" } + 'Path' = { if ($_ -match '^[''"]?\{?([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}?[''"]?$') { "-ProductCode $_" } else { "-FilePath $_" } } 'Arguments' = { "-ArgumentList $_" } 'Parameters' = { "-ArgumentList $_" } 'AddParameters' = { "-AdditionalArgumentList $_" } 'SecureParameters' = '-SecureArgumentList' # Should inspect switch values here in case of -Switch:$false + 'Transform' = { "-Transforms $(if ($_ -match "^'") { $_ -replace ';', "','" } elseif ($_ -match '^"') { $_ -replace ';', '","' } else { $_ })" } 'LogName' = { "-LogFileName $_" } 'IgnoreExitCodes' = { "-IgnoreExitCodes $($_.Trim('"').Trim("'") -split ',' -join ',')" } 'ExitOnProcessFailure' = { From 30cef365500bd6e026655ae4c429fb73c4a7bf55 Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Fri, 6 Dec 2024 13:18:41 +0000 Subject: [PATCH 17/18] Update README --- README.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8867c84..a0be79d 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,36 @@ PSAppDeployToolkit.Tools is a companion module for [PSAppDeployToolkit](https:// ### Features -- Test your PSAppDeployToolkit v3 scripts to get a full report on which functions and variables have changed in v4. -- Convert a PSAppDeployToolkit v3 script or an entire package folder to v4 standards. +- **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 --> [System Requirements](https://psappdeploytoolkit.com/docs/getting-started/requirements) --> [Downloading](https://psappdeploytoolkit.com/docs/getting-started/download) +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 From 9fb396e1bd46d68f2535b4ff828540f3d3ba4c2e Mon Sep 17 00:00:00 2001 From: Dan Gough Date: Fri, 6 Dec 2024 13:18:50 +0000 Subject: [PATCH 18/18] This is PSAppDeployToolkit.Tools 0.2.0 --- src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 index 66a5435..8cead80 100644 --- a/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 +++ b/src/PSAppDeployToolkit.Tools/PSAppDeployToolkit.Tools.psd1 @@ -9,7 +9,7 @@ RootModule = 'PSAppDeployToolkit.Tools.psm1' # Version number of this module. - ModuleVersion = '0.1.0' + ModuleVersion = '0.2.0' # Supported PSEditions # CompatiblePSEditions = @()