From f61fd8b83ef6baf66128506b1c6c2f17326b0d47 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 6 May 2026 15:57:08 +0100 Subject: [PATCH] Add project access token rotation API Co-authored-by: Dorian Boulc'h <5851082+dorianboulch@users.noreply.github.com> --- CHANGELOG.md | 1 + src/Api/Projects.php | 20 ++++++++++++++ tests/Api/ProjectsTest.php | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5b2e80..664822c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Add support for personal access tokens * Add support for `job_inputs` and `job_variables_attributes` in `Jobs::play` * Add support for filters in `Projects::projectAccessTokens` +* Add support for `Projects::rotateProjectAccessToken` * Add support for listing merge requests associated with a commit * Add support for `without_project_bots` in `Users::all` * Add support for date filters and `finished_at` ordering in `Deployments::all` diff --git a/src/Api/Projects.php b/src/Api/Projects.php index 9effeb46..8c1e3ed0 100644 --- a/src/Api/Projects.php +++ b/src/Api/Projects.php @@ -1270,6 +1270,26 @@ public function createProjectAccessToken(int|string $project_id, array $paramete return $this->post($this->getProjectPath($project_id, 'access_tokens'), $resolver->resolve($parameters)); } + /** + * @param array $parameters { + * + * @var \DateTimeInterface $expires_at expiration date of the access token + * } + */ + public function rotateProjectAccessToken(int|string $project_id, int|string $token_id, array $parameters = []): mixed + { + $resolver = new OptionsResolver(); + $dateNormalizer = function (Options $resolver, \DateTimeInterface $value): string { + return $value->format('Y-m-d'); + }; + $resolver->setDefined('expires_at') + ->setAllowedTypes('expires_at', \DateTimeInterface::class) + ->setNormalizer('expires_at', $dateNormalizer) + ; + + return $this->post($this->getProjectPath($project_id, 'access_tokens/'.self::encodePath($token_id).'/rotate'), $resolver->resolve($parameters)); + } + public function deleteProjectAccessToken(int|string $project_id, int|string $token_id): mixed { return $this->delete($this->getProjectPath($project_id, 'access_tokens/'.$token_id)); diff --git a/tests/Api/ProjectsTest.php b/tests/Api/ProjectsTest.php index 3599b749..d58161ae 100644 --- a/tests/Api/ProjectsTest.php +++ b/tests/Api/ProjectsTest.php @@ -2769,6 +2769,61 @@ public function shouldCreateProjectAccessToken(): void ])); } + #[Test] + public function shouldRotateProjectAccessToken(): void + { + $expectedArray = [ + 'scopes' => [ + 'api', + 'read_repository', + ], + 'active' => true, + 'name' => 'test', + 'revoked' => false, + 'created_at' => '2021-01-21T19:35:37.921Z', + 'user_id' => 166, + 'id' => 58, + 'expires_at' => '2021-01-31', + 'token' => 'D4y...Wzr', + ]; + $expiresAt = new DateTime('2021-01-31'); + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('projects/1/access_tokens/2/rotate', ['expires_at' => '2021-01-31']) + ->willReturn($expectedArray); + + $this->assertEquals($expectedArray, $api->rotateProjectAccessToken(1, 2, ['expires_at' => $expiresAt])); + } + + #[Test] + public function shouldRotateCurrentProjectAccessToken(): void + { + $expectedArray = [ + 'scopes' => [ + 'api', + 'read_repository', + ], + 'active' => true, + 'name' => 'test', + 'revoked' => false, + 'created_at' => '2021-01-21T19:35:37.921Z', + 'user_id' => 166, + 'id' => 58, + 'expires_at' => '2021-01-31', + 'token' => 'D4y...Wzr', + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('projects/1/access_tokens/self/rotate', []) + ->willReturn($expectedArray); + + $this->assertEquals($expectedArray, $api->rotateProjectAccessToken(1, 'self')); + } + #[Test] public function shouldDeleteProjectAccessToken(): void {