1"use strict";(self.webpackChunkpester_docs=self.webpackChunkpester_docs||[]).push([["408"],{5889(e,s,t){t.r(s),t.d(s,{metadata:()=>n,default:()=>c,frontMatter:()=>o,contentTitle:()=>l,toc:()=>h,assets:()=>a});var n=JSON.parse('{"id":"ci/github-actions","title":"Running on every push","description":"Wire the test script into a GitHub Actions workflow that installs Pester, runs the suite across operating systems and publishes the results","source":"@site/tutorial/7-ci/2-github-actions.mdx","sourceDirName":"7-ci","slug":"/ci/github-actions","permalink":"/tutorial/ci/github-actions","draft":false,"unlisted":false,"editUrl":"https://github.com/pester/docs/edit/main/tutorial/7-ci/2-github-actions.mdx","tags":[],"version":"current","sidebarPosition":2,"frontMatter":{"id":"github-actions","title":"Running on every push","description":"Wire the test script into a GitHub Actions workflow that installs Pester, runs the suite across operating systems and publishes the results"},"sidebar":"tutorial","previous":{"title":"A test script that fails the build","permalink":"/tutorial/ci/test-script"}}'),i=t(1987),r=t(7008);let o={id:"github-actions",title:"Running on every push",description:"Wire the test script into a GitHub Actions workflow that installs Pester, runs the suite across operating systems and publishes the results"},l,a={},h=[{value:"The workflow",id:"the-workflow",level:2},{value:"Testing on more than one platform",id:"testing-on-more-than-one-platform",level:2},{value:"Where to go from here",id:"where-to-go-from-here",level:2},{value:"You are done",id:"you-are-done",level:2}];function d(e){let s={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components},{TutorialChecklist:t}=s;return t||function(e,s){throw Error("Expected "+(s?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("TutorialChecklist",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(s.p,{children:["The test script is the hard part and it is already done. A CI workflow is mostly plumbing: check out the code, install Pester, run ",(0,i.jsx)(s.code,{children:"./test.ps1"}),", keep the artifacts."]}),"\n",(0,i.jsx)(s.h2,{id:"the-workflow",children:"The workflow"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",metastring:'title=".github/workflows/test.yml"',children:"name: Test\n\non:\n push:\n branches: [main]\n pull_request:\n\njobs:\n test:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Pester\n shell: pwsh\n run: Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser\n\n - name: Run tests\n shell: pwsh\n run: ./test.ps1\n\n - name: Upload results\n if: always()\n uses: actions/upload-artifact@v7\n with:\n name: test-results\n path: |\n testResults.xml\n coverage.xml\n"})}),"\n",(0,i.jsxs)(s.admonition,{title:"This part requires git and GitHub",type:"note",children:[(0,i.jsx)(s.p,{children:"Everything so far ran locally; running in CI means publishing the code to GitHub. If your working folder is not a git repository yet, turn it into one and commit your files:"}),(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:'git init\ngit add .\ngit commit -m "Planetarium module with tests"\n'})}),(0,i.jsxs)(s.p,{children:["Then ",(0,i.jsx)(s.a,{href:"https://docs.github.com/en/get-started/quickstart/create-a-repo",children:"create a GitHub repository and push to it"}),". If you have never used git, that guide covers everything this module needs."]})]}),"\n",(0,i.jsxs)(s.p,{children:["Commit that and push. Every push to ",(0,i.jsx)(s.code,{children:"main"})," and every pull request now runs your 26 tests."]}),"\n",(0,i.jsx)(s.p,{children:"Three details are doing real work."}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"shell: pwsh"})})," on every PowerShell step. The default shell on ",(0,i.jsx)(s.code,{children:"ubuntu-latest"})," is bash, and PowerShell 7 is preinstalled but not the default. Omit this and the step tries to run PowerShell as bash. On Windows runners the default is ",(0,i.jsx)(s.code,{children:"powershell"})," \u2014 Windows PowerShell 5.1 \u2014 which is a ",(0,i.jsx)(s.em,{children:"different"})," shell from ",(0,i.jsx)(s.code,{children:"pwsh"}),", so being explicit avoids a second, subtler version of the same problem."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"-Scope CurrentUser"})})," on the install. The runner user is not an administrator, and an all-users install needs elevation. ",(0,i.jsx)(s.code,{children:"-SkipPublisherCheck"})," is there for the Windows runner, which ships the Microsoft-signed Pester 3.4.0 you met in the prerequisites \u2014 without it, installing over a module signed by a different publisher is refused."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.code,{children:"if: always()"})})," on the upload. Without it, the artifact step is skipped whenever a previous step fails \u2014 which is precisely when you most want the test results. This one line is the difference between a red build you can diagnose from the artifact and one you have to reproduce locally."]}),"\n",(0,i.jsx)(s.h2,{id:"testing-on-more-than-one-platform",children:"Testing on more than one platform"}),"\n",(0,i.jsxs)(s.p,{children:["The single job above tests less than the module claims to
1support. The tutorial has worked on Windows, Linux and macOS all along, and the manifest says ",(0,i.jsx)(s.code,{children:"PowerShellVersion = '5.1'"})," \u2014 Windows PowerShell 5.1 is a genuinely different engine from PowerShell 7, and it is where the surprises live. A matrix runs the same job once per combination, so extend the workflow to test all of it:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",metastring:'title=".github/workflows/test.yml"',children:"jobs:\n test:\n # diff-add-start\n name: ${{ matrix.name }} on ${{ matrix.os }}\n strategy:\n fail-fast: false\n matrix:\n os: [ubuntu-latest, windows-latest, macos-latest]\n shell: [pwsh]\n name: [PowerShell 7]\n include:\n - os: windows-latest\n shell: powershell\n name: Windows PowerShell 5.1\n # diff-add-end\n\n # diff-remove\n runs-on: ubuntu-latest\n # diff-add\n runs-on: ${{ matrix.os }}\n\n steps:\n - uses: actions/checkout@v6\n\n - name: Install Pester\n # diff-remove\n shell: pwsh\n # diff-add\n shell: ${{ matrix.shell }}\n run: Install-Module Pester -MinimumVersion 6.0.0 -Force -SkipPublisherCheck -Scope CurrentUser\n\n - name: Run tests\n # diff-remove\n shell: pwsh\n # diff-add\n shell: ${{ matrix.shell }}\n run: ./test.ps1\n\n - name: Upload results\n if: always()\n uses: actions/upload-artifact@v7\n with:\n # diff-remove\n name: test-results\n # diff-add\n name: test-results-${{ matrix.os }}-${{ matrix.shell }}\n path: |\n testResults.xml\n coverage.xml\n"})}),"\n",(0,i.jsxs)(s.p,{children:["This produces four jobs. The ",(0,i.jsx)(s.code,{children:"os"})," list crossed with ",(0,i.jsx)(s.code,{children:"shell: [pwsh]"})," gives three \u2014 PowerShell 7 on each operating system. The ",(0,i.jsx)(s.code,{children:"include"})," adds a fourth: ",(0,i.jsx)(s.code,{children:"windows-latest"})," running under ",(0,i.jsx)(s.code,{children:"powershell"}),", the built-in Windows PowerShell 5.1 shell. That fourth job is the one that tests what the manifest promises. (For a module of your own that does not support 5.1, drop the ",(0,i.jsx)(s.code,{children:"include"})," and raise ",(0,i.jsx)(s.code,{children:"PowerShellVersion"})," in the manifest instead.)"]}),"\n",(0,i.jsxs)(s.p,{children:["The job ",(0,i.jsx)(s.code,{children:"name"})," is customized to identify all combinations in the CI report, e.g. ",(0,i.jsx)(s.code,{children:"PowerShell 7 on ubuntu-latest"})," and ",(0,i.jsx)(s.code,{children:"Windows PowerShell 5.1 on windows-latest"}),"."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"fail-fast: false"})," matters here. The default cancels every other job the moment one fails, so a Windows-only bug would abort the Linux and macOS runs and hide whether the failure is platform-specific. Turning it off costs a few runner minutes and tells you far more."]}),"\n",(0,i.jsxs)(s.p,{children:["The artifact name now includes both matrix values. Jobs uploading to the same artifact name is an error, and ",(0,i.jsx)(s.code,{children:"${{ matrix.os }}"})," alone would still collide for the two Windows jobs."]}),"\n",(0,i.jsx)(s.admonition,{title:"This is where the cross-platform bugs show up",type:"note",children:(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"Join-Path"})," and ",(0,i.jsx)(s.code,{children:"$TestDrive"})," have been quietly protecting you. Hard-coded ",(0,i.jsx)(s.code,{children:"\\"})," separators, case-sensitivity assumptions, and ",(0,i.jsx)(s.code,{children:"C:\\temp"})," paths all work on your machine and fail on Linux. A matrix is how you find them, and it is the main reason this module is worth doing even for a small module."]})}),"\n",(0,i.jsx)(s.h2,{id:"where-to-go-from-here",children:"Where to go from here"}),"\n",(0,i.jsx)(s.p,{children:"The workflow above is a complete, working setup. Natural next steps, in rough order of value:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Publish the test results"})," as a check run so failures annotate the pull request directly, using something like ",(0,i.jsx)(s.code,{children:"dorny/test-reporter"}),", which reads the NUnit file you are already producing."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsxs)(s.strong,{children:["Send ",(0,i.jsx)(s.code,{children:"coverage.xml"})," to a coverage service"]})," \u2014 the JaCoCo format is widely supported."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Require the check"})," in branch protection, so a red build actually blocks the merge. Until you do this, CI is advisory."]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:"Cache the Pester install"})," if the install step becomes a meaningful share of the run time."]}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"you-are-done",children:"You are done"}),"\n",(0,i.jsx)(s.p,{children:"Starting from an empty folder, you have built a PowerShell module with a manifest, a loader, public and private functions, an external data file and a function that writes reports \u2014 and tested all of it:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"26 tests across six test files"}),"\n",(0,i.jsx)(s.li,{children:"Public functions tested through the module's real front door, which proves they are exported"}),"\n",(0,i.jsxs)(s.li,{children:["Private helpers reached with ",(0,i.jsx)(s.code,{children:"InModuleScope"}),", sparingly"]}),"\n",(0,i.jsx)(s.li,{children:"A data source replaced by mocks so tests own their data"}),"\n",(0,i.jsxs)(s.li,{children:["File operations isolated in ",(0,i.jsx)(s.code,{children:"TestDrive"}),", leaving nothing behind"]}),"\n",(0,i.jsxs)(s.li,{children:["100% coverage, arrived at by reading ",(0,i.jsx)(s.code,{children:"CommandsMissed"})," rather than chasing a number"]}),"\n",(0,i.jsx)(s.li,{children:"A test script that fails correctly, running on three operating systems \u2014 and on Windows PowerShell 5.1 \u2014 on every push"}),"\n"]}),"\n",(0,i.jsxs)(s.p,{children:["The reference documentation goes deeper on everything here: ",(0,i.jsx)(s.a,{href:"../../docs/usage/mocking",children:"mocking"}),", ",(0,i.jsx)(s.a,{href:"../../docs/usage/testdrive",children:"TestDrive"}),", ",(0,i.jsx)(s.a,{href:"../../docs/usage/code-coverage",children:"code coverage"}),", ",(0,i.jsx)(s.a,{href:"../../docs/usage/configuration",children:"configuration"})," and ",(0,i.jsx)(s.a,{href:"../../docs/usage/result-object",children:"the result object"}),". If you write assertions you wish existed, ",(0,i.jsx)(s.a,{href:"../../docs/assertions/custom-assertions",children:"custom assertions"})," is the next thing worth reading."]}),"\n",(0,i.jsx)(t,{items:[{id:"workflow",label:"I added .github/workflows/test.yml and pushed it"},{id:"shell-pwsh",label:"I can explain why every PowerShell step needs shell: pwsh"}
1,{id:"always",label:"I understand why the upload step needs if: always()"},{id:"matrix",label:"I extended the matrix to three operating systems plus Windows PowerShell 5.1, and know why fail-fast: false helps"},{id:"finished",label:"My suite runs green in CI"}]})]})}function c(e={}){let{wrapper:s}={...(0,r.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},7008(e,s,t){t.d(s,{R:()=>o,x:()=>l});var n=t(1763);let i={},r=n.createContext(i);function o(e){let s=n.useContext(r);return n.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(r.Provider,{value:s},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.