diff --git a/.editorconfig b/.editorconfig index 978fdae1..66fda4b6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.md] +[*.{md,yml,yaml}] trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7158ca64..a98ac839 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,6 @@ -# https://help.github.com/articles/about-codeowners/ +.github/workflows @atlassianps/ci-managers +JiraPS.build.ps1 @atlassianps/ci-managers +PPSScriptAnalyzerSettings.psd1 @atlassianps/ci-managers +Tools/ @atlassianps/ci-managers -* @atlassianps/maintainers @atlassianps/reviewers +* @atlassianps/maintainers diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b9747b99..98082eec 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,6 +4,9 @@ on: pull_request: branches: - master + push: + branches: + - master jobs: build_module: @@ -11,6 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: pwsh run: | @@ -20,7 +29,7 @@ jobs: shell: pwsh run: | Invoke-Build -Task Clean, Build - - name: Upload + - name: Upload Artifact uses: actions/upload-artifact@v3 with: name: Release @@ -37,6 +46,12 @@ jobs: with: name: Release path: ./Release/ + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: powershell run: | @@ -46,6 +61,12 @@ jobs: shell: powershell run: | Invoke-Build -Task Test + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-Unit-Tests + path: Test*.xml + if: ${{ always() }} test_on_windows_v7: name: Test Module on Windows (PS v7) @@ -58,6 +79,12 @@ jobs: with: name: Release path: ./Release/ + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: pwsh run: | @@ -67,6 +94,12 @@ jobs: shell: pwsh run: | Invoke-Build -Task Test + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-Unit-Tests + path: Test*.xml + if: ${{ always() }} test_on_ubuntu: name: Test Module on Ubuntu @@ -79,6 +112,12 @@ jobs: with: name: Release path: ./Release/ + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: pwsh run: | @@ -88,6 +127,12 @@ jobs: shell: pwsh run: | Invoke-Build -Task Test + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-Unit-Tests + path: Test*.xml + if: ${{ always() }} test_on_macos: name: Test Module on macOS @@ -100,6 +145,12 @@ jobs: with: name: Release path: ./Release/ + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: pwsh run: | @@ -109,6 +160,12 @@ jobs: shell: pwsh run: | Invoke-Build -Task Test + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-Unit-Tests + path: Test*.xml + if: ${{ always() }} test_against_cloud: env: @@ -125,6 +182,12 @@ jobs: with: name: Release path: ./Release/ + - name: Setup PowerShell module cache + id: cacher + uses: actions/cache@v3 + with: + path: "~/.local/share/powershell/Modules" + key: ${{ runner.os }}-PSModules - name: Setup shell: pwsh run: | @@ -135,3 +198,9 @@ jobs: run: | Invoke-Build -Task Test -Tag "Integration" -ExcludeTag "" if: ${{ env.JiraURI != '' }} + - name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: ${{ runner.os }}-Unit-Tests + path: Test*.xml + if: ${{ always() }} diff --git a/.github/workflows/pr_review.yml b/.github/workflows/pr_review.yml new file mode 100644 index 00000000..ac6f1beb --- /dev/null +++ b/.github/workflows/pr_review.yml @@ -0,0 +1,16 @@ +name: Refine PRs + +on: + pull_request: + types: [opened] + +jobs: + label_issue: + runs-on: ubuntu-latest + steps: + - name: "Add default reviewers to PR" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + gh pr edit $PR_URL --add-reviewer @atlassianps/reviewers diff --git a/JiraPS/Public/Get-JiraIssueWorklog.ps1 b/JiraPS/Public/Get-JiraIssueWorklog.ps1 new file mode 100644 index 00000000..899a9625 --- /dev/null +++ b/JiraPS/Public/Get-JiraIssueWorklog.ps1 @@ -0,0 +1,66 @@ +function Get-JiraIssueWorklog { + # .ExternalHelp ..\JiraPS-help.xml + [CmdletBinding()] + param( + [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] + [ValidateNotNullOrEmpty()] + [ValidateScript( + { + if (("JiraPS.Issue" -notin $_.PSObject.TypeNames) -and (($_ -isnot [String]))) { + $exception = ([System.ArgumentException]"Invalid Type for Parameter") #fix code highlighting] + $errorId = 'ParameterType.NotJiraIssue' + $errorCategory = 'InvalidArgument' + $errorTarget = $_ + $errorItem = New-Object -TypeName System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $errorTarget + $errorItem.ErrorDetails = "Wrong object type provided for Issue. Expected [JiraPS.Issue] or [String], but was $($_.GetType().Name)" + $PSCmdlet.ThrowTerminatingError($errorItem) + <# + #ToDo:CustomClass + Once we have custom classes, this check can be done with Type declaration + #> + } + else { + return $true + } + } + )] + [Alias('Key')] + [Object] + $Issue, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + begin { + Write-Verbose "[$($MyInvocation.MyCommand.Name)] Function started" + } + + process { + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] ParameterSetName: $($PsCmdlet.ParameterSetName)" + Write-DebugMessage "[$($MyInvocation.MyCommand.Name)] PSBoundParameters: $($PSBoundParameters | Out-String)" + + # Find the proper object for the Issue + $issueObj = Resolve-JiraIssueObject -InputObject $Issue -Credential $Credential + + $parameter = @{ + URI = "{0}/worklog" -f $issueObj.RestURL + Method = "GET" + GetParameter = @{ + maxResults = $PageSize + } + OutputType = "JiraWorklogItem" + Paging = $true + Credential = $Credential + } + + Write-Debug "[$($MyInvocation.MyCommand.Name)] Invoking JiraMethod with `$parameter" + Invoke-JiraMethod @parameter + } + + end { + Write-Verbose "[$($MyInvocation.MyCommand.Name)] Complete" + } +} diff --git a/JiraPS/Public/Invoke-JiraMethod.ps1 b/JiraPS/Public/Invoke-JiraMethod.ps1 index f3db9a5e..1735e0e5 100644 --- a/JiraPS/Public/Invoke-JiraMethod.ps1 +++ b/JiraPS/Public/Invoke-JiraMethod.ps1 @@ -37,7 +37,8 @@ function Invoke-JiraMethod { "JiraComment", "JiraIssue", "JiraUser", - "JiraVersion" + "JiraVersion", + "JiraWorklogItem" )] [String] $OutputType, diff --git a/JiraPS/Public/New-JiraSession.ps1 b/JiraPS/Public/New-JiraSession.ps1 index 1c07b2e7..3688f0be 100644 --- a/JiraPS/Public/New-JiraSession.ps1 +++ b/JiraPS/Public/New-JiraSession.ps1 @@ -3,7 +3,7 @@ function New-JiraSession { [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseShouldProcessForStateChangingFunctions', '')] param( - [Parameter( Mandatory )] + [Parameter( )] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, @@ -29,8 +29,8 @@ function New-JiraSession { Method = "GET" Headers = $Headers StoreSession = $true - Credential = $Credential } + if ($Credential) { $parameter.Add('Credential',$Credential) } Write-Debug "[$($MyInvocation.MyCommand.Name)] Invoking JiraMethod with `$parameter" $result = Invoke-JiraMethod @parameter diff --git a/Tests/Functions/Get-JiraIssueWorklog.Unit.Tests.ps1 b/Tests/Functions/Get-JiraIssueWorklog.Unit.Tests.ps1 new file mode 100644 index 00000000..42a1aeca --- /dev/null +++ b/Tests/Functions/Get-JiraIssueWorklog.Unit.Tests.ps1 @@ -0,0 +1,141 @@ +#requires -modules BuildHelpers +#requires -modules @{ ModuleName = "Pester"; ModuleVersion = "4.4.0" } + +Describe "Get-JiraIssueWorklog" -Tag 'Unit' { + + BeforeAll { + Remove-Item -Path Env:\BH* + $projectRoot = (Resolve-Path "$PSScriptRoot/../..").Path + if ($projectRoot -like "*Release") { + $projectRoot = (Resolve-Path "$projectRoot/..").Path + } + + Import-Module BuildHelpers + Set-BuildEnvironment -BuildOutput '$ProjectPath/Release' -Path $projectRoot -ErrorAction SilentlyContinue + + $env:BHManifestToTest = $env:BHPSModuleManifest + $script:isBuild = $PSScriptRoot -like "$env:BHBuildOutput*" + if ($script:isBuild) { + $Pattern = [regex]::Escape($env:BHProjectPath) + + $env:BHBuildModuleManifest = $env:BHPSModuleManifest -replace $Pattern, $env:BHBuildOutput + $env:BHManifestToTest = $env:BHBuildModuleManifest + } + + Import-Module "$env:BHProjectPath/Tools/BuildTools.psm1" + + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module $env:BHManifestToTest + } + AfterAll { + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Remove-Module BuildHelpers -ErrorAction SilentlyContinue + Remove-Item -Path Env:\BH* + } + + InModuleScope JiraPS { + + . "$PSScriptRoot/../Shared.ps1" + + + $jiraServer = 'http://jiraserver.example.com' + $issueID = 41701 + $issueKey = 'IT-3676' + + $restResult = @" +{ + "startAt": 0, + "maxResults": 1, + "total": 1, + "worklogs": [ + { + "self": "$jiraServer/rest/api/2/issue/$issueID/worklog/90730", + "id": "90730", + "comment": "Test comment", + "created": "2015-05-01T16:24:38.000-0500", + "updated": "2015-05-01T16:24:38.000-0500", + "visibility": { + "type": "role", + "value": "Developers" + }, + "timeSpent": "3m", + "timeSpentSeconds": 180 + } + ] +} +"@ + + #region Mocks + Mock Get-JiraConfigServer -ModuleName JiraPS { + Write-Output $jiraServer + } + + Mock Get-JiraIssue -ModuleName JiraPS { + $object = [PSCustomObject] @{ + ID = $issueID + Key = $issueKey + RestUrl = "$jiraServer/rest/api/2/issue/$issueID" + } + $object.PSObject.TypeNames.Insert(0, 'JiraPS.Issue') + return $object + } + + Mock Resolve-JiraIssueObject -ModuleName JiraPS { + Get-JiraIssue -Key $Issue + } + + # Obtaining worklog from an issue...this is IT-3676 in the test environment + Mock Invoke-JiraMethod -ModuleName JiraPS -ParameterFilter {$Method -eq 'Get' -and $URI -eq "$jiraServer/rest/api/2/issue/$issueID/worklog"} { + ShowMockInfo 'Invoke-JiraMethod' 'Method', 'Uri' + (ConvertFrom-Json -InputObject $restResult).worklogs + } + + # Generic catch-all. This will throw an exception if we forgot to mock something. + Mock Invoke-JiraMethod -ModuleName JiraPS { + ShowMockInfo 'Invoke-JiraMethod' 'Method', 'Uri' + throw "Unidentified call to Invoke-JiraMethod" + } + #endregion Mocks + + ############# + # Tests + ############# + + It "Obtains all Jira worklogs from a Jira issue if the issue key is provided" { + $worklogs = Get-JiraIssueWorklog -Issue $issueKey + + $worklogs | Should Not BeNullOrEmpty + @($worklogs).Count | Should Be 1 + $worklogs.ID | Should Be 90730 + $worklogs.Comment | Should Be 'Test comment' + $worklogs.TimeSpent | Should Be '3m' + $worklogs.TimeSpentSeconds | Should Be 180 + + # Get-JiraIssue should be called to identify the -Issue parameter + Assert-MockCalled -CommandName Get-JiraIssue -ModuleName JiraPS -Exactly -Times 1 -Scope It + + # Normally, this would be called once in Get-JiraIssue and a second time in Get-JiraIssueComment, but + # since we've mocked Get-JiraIssue out, it will only be called once. + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName JiraPS -Exactly -Times 1 -Scope It + } + + It "Obtains all Jira worklogs from a Jira issue if the Jira object is provided" { + $issue = Get-JiraIssue -Key $issueKey + $worklogs = Get-JiraIssueWorklog -Issue $issue + + $worklogs | Should Not BeNullOrEmpty + $worklogs.ID | Should Be 90730 + + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName JiraPS -Exactly -Times 1 -Scope It + } + + It "Handles pipeline input from Get-JiraIssue" { + $worklogs = Get-JiraIssue -Key $issueKey | Get-JiraIssueWorklog + + $worklogs | Should Not BeNullOrEmpty + $worklogs.ID | Should Be 90730 + + Assert-MockCalled -CommandName Invoke-JiraMethod -ModuleName JiraPS -Exactly -Times 1 -Scope It + } + } +} diff --git a/docs/en-US/about_JiraPS_Authentication.md b/docs/en-US/about_JiraPS_Authentication.md index a5fb7ba9..cea262f4 100644 --- a/docs/en-US/about_JiraPS_Authentication.md +++ b/docs/en-US/about_JiraPS_Authentication.md @@ -56,6 +56,8 @@ the email address and the API token must be used. > with **Cloud Servers** to **always** use API Tokens. > More information in the [Deprecation notice](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). +Some implementations of Jira Server (on-premise) might not be able to use HTTP Basic authentication method noted above when using API tokens. For this, you may need to create a new Jira session using `New-JiraSession` and pass a custom Authorization header. + _More information on the API tokens and how to create one can be found at:_ __ @@ -85,6 +87,19 @@ The session is stored in the module's runtime. This means that it will not be available in a new Powershell session or if the module is reloaded. +### Creating a Session Using Custom Authorization Headers + +Some implementations of Jira Server (on-premise) might not be able to use the methods listed above of using HTTP Basic authentication by passing an email address and token to authenticate. In this case, you will need to create a session by passing the API Token as a bearer token in a custom Authorization header. + +To create a session using the API Token as the bearer token, you can use the New-JiraSession function: + +```powershell +$personalAccessToken = "" +$headers = @{ Authorization = "Bearer $($personalAccessToken)" } + +New-JiraSession -Headers $headers +``` + ## What About OAuth Jira does support use of OAuth, but JiraPS does not - yet. diff --git a/docs/en-US/commands/Add-JiraIssueWorklog.md b/docs/en-US/commands/Add-JiraIssueWorklog.md index 89c125b0..cb08d36d 100644 --- a/docs/en-US/commands/Add-JiraIssueWorklog.md +++ b/docs/en-US/commands/Add-JiraIssueWorklog.md @@ -226,6 +226,8 @@ If neither are supplied, this function will run with anonymous access to JIRA. ## RELATED LINKS +[Get-JiraIssueWorklog](../Get-JiraIssueWorklog) + [Get-JiraIssue](../Get-JiraIssue/) [Format-Jira](../Format-Jira/) diff --git a/docs/en-US/commands/Get-JiraIssueWorklog.md b/docs/en-US/commands/Get-JiraIssueWorklog.md new file mode 100644 index 00000000..f6043e54 --- /dev/null +++ b/docs/en-US/commands/Get-JiraIssueWorklog.md @@ -0,0 +1,105 @@ +--- +external help file: JiraPS-help.xml +Module Name: JiraPS +online version: https://atlassianps.org/docs/JiraPS/commands/Get-JiraIssueWorklog/ +locale: en-US +schema: 2.0.0 +layout: documentation +permalink: /docs/JiraPS/commands/Get-JiraIssueWorklog/ +--- +# Get-JiraIssueWorklog + +## SYNOPSIS + +Returns worklogs from an issue in JIRA. +**** + +## SYNTAX + +```powershell +Get-JiraIssueWorklog [-Issue] [[-Credential] ] [] +``` + +## DESCRIPTION + +This function obtains worklogs from existing issues in JIRA. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-JiraIssueWorklog -Key TEST-001 +``` + +This example returns all worklogs from issue TEST-001. + +### EXAMPLE 2 + +```powershell +Get-JiraIssue TEST-002 | Get-JiraIssueWorklog +``` + +This example illustrates use of the pipeline to return all worklogs from issue TEST-002. + +## PARAMETERS + +### -Issue + +JIRA issue to check for worklogs. + +Can be a `JiraPS.Issue` object, issue key, or internal issue ID. + +```yaml +Type: Object +Parameter Sets: (All) +Aliases: Key + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -Credential + +Credentials to use to connect to JIRA. +If not specified, this function will use anonymous access. + +```yaml +Type: PSCredential +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +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 + +### [JiraPS.Issue] / [String] + +## OUTPUTS + +### [JiraPS.WorklogItem] + +## NOTES + +This function requires either the `-Credential` parameter to be passed or a persistent JIRA session. +See `New-JiraSession` for more details. +If neither are supplied, this function will run with anonymous access to JIRA. + +## RELATED LINKS + +[Add-JiraIssueWorklog](../Add-JiraIssueWorklog/) + +[Get-JiraIssue](../Get-JiraIssue/) diff --git a/docs/en-US/commands/New-JiraSession.md b/docs/en-US/commands/New-JiraSession.md index 87f63a55..90792af2 100644 --- a/docs/en-US/commands/New-JiraSession.md +++ b/docs/en-US/commands/New-JiraSession.md @@ -41,6 +41,19 @@ Get-JiraIssue TEST-01 Creates a Jira session for jiraUsername. The following `Get-JiraIssue` is run using the saved session for jiraUsername. +### EXAMPLE 2 + +```powershell +$personalAccessToken = "" +$headers = @{ Authorization = "Bearer $($personalAccessToken)" } + +New-JiraSession -Headers $headers +Get-JiraIssue TEST-01 +``` + +Creates a Jira session using a Personal Access Token (PAT) as a bearer token in a custom Authorization header. +The following `Get-JiraIssue` is run using the saved session created using the PAT. + ## PARAMETERS ### -Credential @@ -52,7 +65,7 @@ Type: PSCredential Parameter Sets: (All) Aliases: -Required: True +Required: False Position: 1 Default value: None Accept pipeline input: False