Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for YouTrack in TodoByTicketRule #51

Merged
merged 7 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ jobs:
test-dir: tests-e2e/jira/
script: |
diff <(vendor/bin/phpstan analyse --error-format=raw --no-progress | sed "s|$(pwd)/||") expected-errors.txt
- os: ubuntu-latest
php-version: '8.2'
test-dir: tests-e2e/youtrack/
script: |
diff <(vendor/bin/phpstan analyse --error-format=raw --no-progress | sed "s|$(pwd)/||") expected-errors.txt

steps:
- name: Checkout
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Reference these virtual packages like any other package in your todo-comments:
Optionally you can configure this extension to analyze your comments with issue tracker ticket keys.
The extension fetches issue tracker API for issue status. If the remote issue is resolved, the comment will be reported.

Currently, Jira and GitHub are supported.
Currently, Jira, GitHub and YouTrack are supported.

This feature is disabled by default. To enable it, you must set `ticket.enabled` parameter to `true`.
You also need to set these parameters:
Expand Down Expand Up @@ -222,7 +222,7 @@ parameters:
# path to a file containing Jira credentials.
# see below for possible formats.
# if credentials parameter is not empty, it will be used instead of this file.
# this file must not be commited into the repository!
# this file must not be committed into the repository!
credentialsFilePath: .secrets/jira-credentials.txt

github:
Expand All @@ -240,6 +240,19 @@ parameters:
# if credentials parameter is not empty, it will be used instead of this file.
# this file must not be committed into the repository!
credentialsFilePath: null

youtrack:
# e.g. https://your-company.youtrack.cloud
server: https://acme.youtrack.cloud

# YouTrack permanent token
# if this value is empty, credentials file will be used instead.
credentials: %env.YOUTRACK_TOKEN%

# path to a file containing a YouTrack permanent token
# if credentials parameter is not empty, it will be used instead of this file.
# this file must not be committed into the repository!
credentialsFilePath: .secrets/youtrack-credentials.txt
```

#### Jira Credentials
Expand Down
28 changes: 27 additions & 1 deletion extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ parametersSchema:
virtualPackages: arrayOf(string(), string())
ticket: structure([
enabled: bool()
tracker: anyOf('jira', 'github')
tracker: anyOf('jira', 'github', 'youtrack')
keyPrefixes: listOf(string())
resolvedStatuses: listOf(string())
jira: structure([
Expand All @@ -21,6 +21,11 @@ parametersSchema:
credentials: schema(string(), nullable())
credentialsFilePath: schema(string(), nullable())
])
youtrack: structure([
server: string()
credentials: schema(string(), nullable())
credentialsFilePath: schema(string(), nullable())
])
])
])

Expand Down Expand Up @@ -90,6 +95,20 @@ parameters:
# this file must not be committed into the repository!
credentialsFilePath: null

youtrack:
# e.g. https://your-company.youtrack.cloud
server: https://youtrack.jetbrains.com

# see README for possible formats.
# if this value is empty, credentials file will be used instead.
credentials: null

# path to a file containing YouTrack credentials.
# see README for possible formats.
# if credentials parameter is not empty, it will be used instead of this file.
# this file must not be commited into the repository!
credentialsFilePath: null

conditionalTags:
staabm\PHPStanTodoBy\TodoByTicketRule:
phpstan.rules.rule: %todo_by.ticket.enabled%
Expand Down Expand Up @@ -151,5 +170,12 @@ services:
- %todo_by.ticket.jira.credentials%
- %todo_by.ticket.jira.credentialsFilePath%

-
class: staabm\PHPStanTodoBy\utils\ticket\YouTrackTicketStatusFetcher
arguments:
- %todo_by.ticket.youtrack.server%
- %todo_by.ticket.youtrack.credentials%
- %todo_by.ticket.youtrack.credentialsFilePath%

-
class: staabm\PHPStanTodoBy\utils\HttpClient
11 changes: 11 additions & 0 deletions src/utils/ticket/TicketRuleConfigurationFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public function create(): TicketRuleConfiguration
);
}

if ('youtrack' === $tracker) {
$fetcher = $this->container->getByType(YouTrackTicketStatusFetcher::class);

return new TicketRuleConfiguration(
$fetcher::getKeyPattern(),
['resolved'],
$keyPrefixes,
$fetcher,
);
}

throw new RuntimeException("Unsupported tracker type: $tracker");
}
}
87 changes: 87 additions & 0 deletions src/utils/ticket/YouTrackTicketStatusFetcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace staabm\PHPStanTodoBy\utils\ticket;

use RuntimeException;
use staabm\PHPStanTodoBy\utils\CredentialsHelper;
use staabm\PHPStanTodoBy\utils\HttpClient;

use function array_key_exists;
use function is_array;

final class YouTrackTicketStatusFetcher implements TicketStatusFetcher
{
private string $host;
private ?string $authorizationHeader;

private HttpClient $httpClient;

/**
* @var array<string, ?string>
*/
private array $cache;

public function __construct(string $host, ?string $credentials, ?string $credentialsFilePath, HttpClient $httpClient)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for discussion: in jira we use issue-states to tell when/whether a ticket is resolved. can/should we do the same for youtrack? does youtrack also have tickets flow thru different states?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Youtrack uses columns to track the state.

From the API documentation the resolved field of the Issue [1] Entity is explained as

The timestamp of the moment when the issue was assigned a state that is considered to be resolved. null if the issue is still in an unresolved state. Read-only. Can be null.

There is something called IssueCustomField [2] which allows you to track custom fields such as state of a ticket. But this is specific to each setup and isn't being used in our setup. So adding support for this is challenging. I would rather stick to the definition of YouTrack itself and it people really need to use custom field support creating a PR to extend the youtrack integration can be done.

{
$credentials = CredentialsHelper::getCredentials($credentials, $credentialsFilePath);

$this->host = $host;
$this->authorizationHeader = $credentials ? self::createAuthorizationHeader($credentials) : null;

$this->cache = [];
$this->httpClient = $httpClient;
}

public function fetchTicketStatus(string $ticketKey): ?string
{
if (array_key_exists($ticketKey, $this->cache)) {
return $this->cache[$ticketKey];
}

$url = "{$this->host}/api/issues/$ticketKey?fields=resolved";
$headers = [];
if (null !== $this->authorizationHeader) {
$headers = [
"Authorization: $this->authorizationHeader",
];
}

[$responseCode, $response] = $this->httpClient->get($url, $headers);

if (200 !== $responseCode) {
throw new RuntimeException("Could not fetch ticket's status from YouTrack with url $url");
}

$data = self::decodeAndValidateResponse($response);

return $this->cache[$ticketKey] = null === $data['resolved'] ? 'open' : 'resolved';
}

public static function getKeyPattern(): string
{
return '[A-Z0-9]+-\d+';
}

private static function createAuthorizationHeader(string $credentials): string
{
return "Bearer $credentials";
}

/** @return array{resolved: ?int} */
private static function decodeAndValidateResponse(string $body): array
{
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);

if (!is_array($data) || !array_key_exists('resolved', $data)) {
self::throwInvalidResponse();
}

return $data;
}

/** @return never */
private static function throwInvalidResponse(): void
{
throw new RuntimeException('YouTrack returned invalid response body');
}
}
1 change: 1 addition & 0 deletions tests-e2e/youtrack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor/
11 changes: 11 additions & 0 deletions tests-e2e/youtrack/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpstan/extension-installer": "^1.3"
}
}
125 changes: 125 additions & 0 deletions tests-e2e/youtrack/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests-e2e/youtrack/expected-errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/tickets.php:3:Should have been resolved in WI-1: fix me.
DannyvdSluijs marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 12 additions & 0 deletions tests-e2e/youtrack/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
parameters:
level: max
paths:
- src/
todo_by:
ticket:
enabled: true
tracker: youtrack
keyPrefixes:
- WI
jira:
server: https://youtrack.jetbrains.com/
3 changes: 3 additions & 0 deletions tests-e2e/youtrack/src/tickets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

// TODO: WI-1 fix me