diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index ebf0dc5..298f129 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -10,33 +10,16 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - laravel: [8.*, 9.*, 10.*, 11.*] - php: [8.0, 8.1, 8.2, 8.3] + laravel: [10.*, 11.*] + php: [8.2, 8.3, 8.4] dependency-version: [prefer-stable] include: - - laravel: 8.* - testbench: 6.* - phpunit: 9.* - - laravel: 9.* - testbench: 7.* - phpunit: 9.* - laravel: 10.* testbench: 8.* phpunit: 9.* - laravel: 11.* testbench: 9.* phpunit: 10.* - exclude: - - laravel: 8.* - php: 8.2 - - laravel: 8.* - php: 8.3 - - laravel: 10.* - php: 8.0 - - laravel: 11.* - php: 8.0 - - laravel: 11.* - php: 8.1 name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} steps: @@ -45,6 +28,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + coverage: none - name: Get Composer Cache Directory id: composer-cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 3efc695..e5c4f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,28 @@ # CHANGELOG -## V2.0.2 +## 3.0.0 + +- Add support for GitHub Apps +- Drop support for PHP 8.0 and 8.1 +- Drop support for Laravel 8 and 9 +- Drop support for GitHub Personal Access Tokens + +Run `php artisan vendor:publish --tag=publish-config` to publish the new configuration file. + +See "GitHub credentials" in README.md for information about creating a GitHub Apps. + +## 2.0.2 - Fix cms navigation bug -## V2.0.1 +## 2.0.1 - Add event for when publication was started -## V2.0.0 +## 2.0.0 - Nova 4 added as dependency. The tool is now compatible with Nova 4. -## V1.0.0 +## 1.0.0 - Initial version for Nova 3. diff --git a/README.md b/README.md index 21e51f1..10f25e8 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ [Return To Top](#nova-publish) -- PHP 8.0, 8.1, 8.2 -- Laravel 8, 9, 10 +- PHP 8.2, 8.3, 8.4 +- Laravel 9, 10 - Nova 4 ## Installation @@ -66,13 +66,13 @@ Publish configuration php artisan vendor:publish --provider="Publish\ToolServiceProvider" ``` -Configure GitHub credentials, set the path to the workflow file and configure an application version. +Configure [GitHub credentials](#github-credentials), set the name of workflow file and configure an application version. ======= ## Local development -Run `npm run dev` to watch for changes in the `resources/js` directory. +Run `yarn run dev` to watch for changes in the `resources/js` directory. Use the local checkout in a project that uses this plugin. [The Composer documentation explains how to do this.](https://getcomposer.org/doc/05-repositories.md#path) @@ -80,20 +80,19 @@ Use the local checkout in a project that uses this plugin. [The Composer documen To add a language or change an existing translation, please read the [Laravel documentation about overriding package language files](https://laravel.com/docs/10.x/localization#overriding-package-language-files). -## GitHub API credentials +## GitHub credentials -Personal Access Tokens (PATs) are currently the only way to access the GitHub API. The token is created by a GitHub user. So when this user is removed from the GitHub organization the token must be recreated by another user. Not ideal, so there is room for improvement. +You need a GitHub Apps to use this tool. The application must have access to the repository where the workflow is located. -Create a Personal Access Token: https://github.com/settings/tokens +[About creating GitHub Apps](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps). -- Note: the name of the project -- Expiration: No expiration (or you have to replace the token every time it expires) -- Scopes: "repo" and "workflow" - -Add the created token as environment variable `PUBLISH_GITHUB_PERSONAL_ACCESS_TOKEN`. - -You GitHub username must be stored in `PUBLISH_GITHUB_USERNAME`. +Use the application ID and private key in `config/publish.php`. ## Contribute You need a Nova license to run the tests. + +## Release new version + +- Run `yarn run prod` to build the assets, and commit the changes +- Add the new version to `CHANGELOG.md` \ No newline at end of file diff --git a/composer.json b/composer.json index 2ab281e..7d92e6f 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ ], "license": "MIT", "require": { - "php": "^8.0|^8.1|^8.2|^8.3", + "php": "^8.2|^8.3|^8.4", + "ext-openssl": "*", "guzzlehttp/guzzle": "^7.3", "laravel/nova": "^4.0" }, diff --git a/config/publish.php b/config/publish.php index 028b8ba..51cdb58 100644 --- a/config/publish.php +++ b/config/publish.php @@ -3,7 +3,7 @@ return [ /* |-------------------------------------------------------------------------- - | GitHub credentials + | GitHub App credentials |-------------------------------------------------------------------------- | | Publish uses these credentials to connect to the GitHub API. The token @@ -11,10 +11,18 @@ | */ - "github_username" => env("PUBLISH_GITHUB_USERNAME"), - "github_personal_access_token" => env( - "PUBLISH_GITHUB_PERSONAL_ACCESS_TOKEN" - ), + "application_id" => env("NOVA_PUBLISH_APPLICATION_ID"), + "private_key" => env("NOVA_PUBLISH_PRIVATE_KEY"), + + /* + |-------------------------------------------------------------------------- + | GitHub repository information + |-------------------------------------------------------------------------- + | + */ + + "owner" => env("NOVA_PUBLISH_OWNER", "norday-agency"), + "repository" => env("NOVA_PUBLISH_REPOSITORY"), /* |-------------------------------------------------------------------------- @@ -28,7 +36,7 @@ | https://api.github.com/repos/grrr-amsterdam/nova-publish/actions/workflows/my-workflow_dispatch-workflow.yml | */ - "workflow_path" => "https://api.github.com/path/to/workflow.yml", + "workflow" => env("NOVA_PUBLISH_WORKFLOW"), /* |-------------------------------------------------------------------------- diff --git a/dist/js/tool.js b/dist/js/tool.js index 26e2acc..542fefe 100644 --- a/dist/js/tool.js +++ b/dist/js/tool.js @@ -1 +1 @@ -(()=>{"use strict";var e={9:(e,t,n)=>{n.d(t,{Z:()=>a});var r=n(645),o=n.n(r)()((function(e){return e[1]}));o.push([e.id,"",""]);const a=o},645:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,r){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(r)for(var a=0;a{var r,o=function(){return void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r},a=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),i=[];function s(e){for(var t=-1,n=0;n{t.Z=(e,t)=>{const n=e.__vccOpts||e;for(const[e,r]of t)n[e]=r;return n}}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var a=t[r]={id:r,exports:{}};return e[r](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.nc=void 0,(()=>{const e=Vue;var t={class:"mb-6"},r={key:0,class:"error text-error-message mb-6"},o={key:1,class:"mb-6"},a={key:0},i={key:2};const s={mounted:function(){this.updateStatus(),this.startStatusRefresh()},props:{publishing:{type:Boolean,default:!0},lastRun:Object,error:String},data:function(){return{error:"",publishing:!1,lastRun:void 0,currentLocale:Nova.config("currentLocale")}},methods:{publish:function(){var e=this;this.publishing=!0,Nova.request().post("/nova-vendor/publish/publish").then((function(t){e.error=""})).catch((function(t){e.error=t.message,e.publishing=!1}))},updateStatus:function(){var e=this;Nova.request().get("/nova-vendor/publish/last-publish-run").then((function(t){console.log(t.data),e.lastRun=t.data,e.publishing="completed"!==t.data.status,e.error=""})).catch((function(t){e.error=t.message}))},startStatusRefresh:function(){var e=this;window.setInterval((function(){e.updateStatus()}),1e4)},formatDate:function(e){return new Intl.DateTimeFormat(Nova.config("currentLocale"),{dateStyle:"full",timeStyle:"long"}).format(new Date(e))}}};var c=n(379),l=n.n(c),u=n(9),d={insert:"head",singleton:!1};l()(u.Z,d);u.Z.locals;const f=(0,n(744).Z)(s,[["render",function(n,s,c,l,u,d){var f=(0,e.resolveComponent)("Head"),p=(0,e.resolveComponent)("heading"),h=(0,e.resolveComponent)("default-button");return(0,e.openBlock)(),(0,e.createElementBlock)("div",null,[(0,e.createVNode)(f,{title:n.__("Publish a new site")},null,8,["title"]),(0,e.createVNode)(p,{class:"mb-6"},{default:(0,e.withCtx)((function(){return[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Publish")),1)]})),_:1}),(0,e.createElementVNode)("p",t,(0,e.toDisplayString)(n.__("Publish the site to make your changes visible to the public")),1),(0,e.createVNode)(h,{onClick:d.publish,disabled:!!u.publishing,class:"mb-6"},{default:(0,e.withCtx)((function(){return[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Publish a new site")),1)]})),_:1},8,["onClick","disabled"]),u.error?((0,e.openBlock)(),(0,e.createElementBlock)("p",r,(0,e.toDisplayString)(n.__("Something went wrong. Please contact Norday. This is the error:"))+' "'+(0,e.toDisplayString)(u.error)+'" ',1)):(0,e.createCommentVNode)("",!0),u.lastRun&&"completed"===u.lastRun.status?((0,e.openBlock)(),(0,e.createElementBlock)("p",o,[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Website published last at :date",{date:d.formatDate(u.lastRun.updated_at)}))+" ",1),"failure"===u.lastRun.conclusion?((0,e.openBlock)(),(0,e.createElementBlock)("span",a,(0,e.toDisplayString)(n.__("Unfortunately this went wrong. Please contact Norday.")),1)):(0,e.createCommentVNode)("",!0)])):(0,e.createCommentVNode)("",!0),u.lastRun&&"completed"!==u.lastRun.status?((0,e.openBlock)(),(0,e.createElementBlock)("p",i,(0,e.toDisplayString)(n.__("Started your publication at :date, please wait a few minutes.",{date:d.formatDate(u.lastRun.created_at)})),1)):(0,e.createCommentVNode)("",!0)])}]]);Nova.booting((function(e){Nova.inertia("PublishTool",f)}))})()})(); \ No newline at end of file +(()=>{"use strict";var e={236:(e,t,n)=>{n.d(t,{Z:()=>a});var r=n(645),o=n.n(r)()((function(e){return e[1]}));o.push([e.id,"",""]);const a=o},645:e=>{e.exports=function(e){var t=[];return t.toString=function(){return this.map((function(t){var n=e(t);return t[2]?"@media ".concat(t[2]," {").concat(n,"}"):n})).join("")},t.i=function(e,n,r){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(r)for(var a=0;a{var r,o=function(){return void 0===r&&(r=Boolean(window&&document&&document.all&&!window.atob)),r},a=function(){var e={};return function(t){if(void 0===e[t]){var n=document.querySelector(t);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[t]=n}return e[t]}}(),i=[];function s(e){for(var t=-1,n=0;n{t.Z=(e,t)=>{const n=e.__vccOpts||e;for(const[e,r]of t)n[e]=r;return n}}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var a=t[r]={id:r,exports:{}};return e[r](a,a.exports,n),a.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.nc=void 0,(()=>{const e=Vue;var t={class:"mb-6"},r={key:0,class:"error text-error-message mb-6"},o={key:1,class:"mb-6"},a={key:0},i={key:2};const s={mounted:function(){this.updateStatus(),this.startStatusRefresh()},props:{publishing:{type:Boolean,default:!0},lastRun:Object,error:String},data:function(){return{error:"",publishing:!1,lastRun:void 0,currentLocale:Nova.config("currentLocale")}},methods:{publish:function(){var e=this;this.publishing=!0,Nova.request().post("/nova-vendor/publish/publish").then((function(){e.error="",e.updateStatus()})).catch((function(t){e.error=t.response.data.message||t.message,e.publishing=!1}))},updateStatus:function(){var e=this;Nova.request().get("/nova-vendor/publish/last-publish-run").then((function(t){e.lastRun=t.data,e.publishing="completed"!==t.data.status,e.error=""})).catch((function(t){e.error=t.response.data.message||t.message}))},startStatusRefresh:function(){var e=this;window.setInterval((function(){e.updateStatus()}),1e4)},formatDate:function(e){return new Intl.DateTimeFormat(Nova.config("currentLocale"),{dateStyle:"full",timeStyle:"long"}).format(new Date(e))}}};var c=n(379),l=n.n(c),u=n(236),d={insert:"head",singleton:!1};l()(u.Z,d);u.Z.locals;const f=(0,n(744).Z)(s,[["render",function(n,s,c,l,u,d){var f=(0,e.resolveComponent)("Head"),p=(0,e.resolveComponent)("heading"),h=(0,e.resolveComponent)("default-button");return(0,e.openBlock)(),(0,e.createElementBlock)("div",null,[(0,e.createVNode)(f,{title:n.__("Publish a new site")},null,8,["title"]),(0,e.createVNode)(p,{class:"mb-6"},{default:(0,e.withCtx)((function(){return[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Publish")),1)]})),_:1}),(0,e.createElementVNode)("p",t,(0,e.toDisplayString)(n.__("Publish the site to make your changes visible to the public")),1),(0,e.createVNode)(h,{onClick:d.publish,disabled:!!u.publishing,class:"mb-6"},{default:(0,e.withCtx)((function(){return[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Publish a new site")),1)]})),_:1},8,["onClick","disabled"]),u.error?((0,e.openBlock)(),(0,e.createElementBlock)("p",r,(0,e.toDisplayString)(n.__("Something went wrong. Please contact Norday. This is the error:"))+' "'+(0,e.toDisplayString)(u.error)+'" ',1)):(0,e.createCommentVNode)("",!0),u.lastRun&&"completed"===u.lastRun.status?((0,e.openBlock)(),(0,e.createElementBlock)("p",o,[(0,e.createTextVNode)((0,e.toDisplayString)(n.__("Website published last at :date",{date:d.formatDate(u.lastRun.updated_at)}))+" ",1),"failure"===u.lastRun.conclusion?((0,e.openBlock)(),(0,e.createElementBlock)("span",a,(0,e.toDisplayString)(n.__("Unfortunately this went wrong. Please contact Norday.")),1)):(0,e.createCommentVNode)("",!0)])):(0,e.createCommentVNode)("",!0),u.lastRun&&"completed"!==u.lastRun.status?((0,e.openBlock)(),(0,e.createElementBlock)("p",i,(0,e.toDisplayString)(n.__("Started your publication at :date, please wait a few minutes.",{date:d.formatDate(u.lastRun.created_at)})),1)):(0,e.createCommentVNode)("",!0)])}]]);Nova.booting((function(e){Nova.inertia("PublishTool",f)}))})()})(); \ No newline at end of file diff --git a/resources/js/components/Tool.vue b/resources/js/components/Tool.vue index 6672e1a..c45fd2f 100644 --- a/resources/js/components/Tool.vue +++ b/resources/js/components/Tool.vue @@ -66,11 +66,12 @@ export default { this.publishing = true; Nova.request() .post("/nova-vendor/publish/publish") - .then((response) => { + .then(() => { this.error = ""; + this.updateStatus(); }) .catch((error) => { - this.error = error.message; + this.error = error.response.data.message || error.message; this.publishing = false; }); }, @@ -78,13 +79,12 @@ export default { Nova.request() .get("/nova-vendor/publish/last-publish-run") .then((lastRun) => { - console.log(lastRun.data); this.lastRun = lastRun.data; this.publishing = lastRun.data.status !== "completed"; this.error = ""; }) .catch((error) => { - this.error = error.message; + this.error = error.response.data.message || error.message; }); }, startStatusRefresh() { diff --git a/src/Exception.php b/src/Exception.php index 6b0593a..303a800 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -4,10 +4,35 @@ class Exception extends \Exception { + public static function failedToSignData(): self + { + return new self("Failed to sign the JWT data."); + } + + public static function gitHubAppNotInstalled(string $owner): self + { + return new self( + "Organisation $owner not found in installations. Install the GitHub app in your organisation.", + 404 + ); + } + + public static function gitHubError(int $code, string $message): self + { + return new self("GitHub API error ($code): $message"); + } + + public static function invalidPrivateKey(): self + { + return new self( + "'publish.private_key' contains an invalid private key." + ); + } + public static function noFirstRun(string $workflow_path) { return new self( - "Workflow $workflow_path hasn't run yet. Run it one time manually via GitHub to kickstart nova-publish." + "Workflow $workflow_path hasn't run yet. Run it once manually via GitHub to kickstart nova-publish." ); } } diff --git a/src/GitHubApi.php b/src/GitHubApi.php new file mode 100644 index 0000000..aa613ed --- /dev/null +++ b/src/GitHubApi.php @@ -0,0 +1,94 @@ +jwt->createToken())->get( + "https://api.github.com/app/installations" + ); + + if ($response->status() >= 400) { + throw Exception::gitHubError( + $response->json("status"), + $response->json("message") + ); + } + + foreach ($response->json() as $installation) { + if ($installation["account"]["login"] === $this->owner) { + return $installation; + } + } + + throw Exception::gitHubAppNotInstalled($this->owner); + } + + public function getAccessToken(string $accessTokenUrl): array + { + $response = Http::withToken($this->jwt->createToken())->post( + $accessTokenUrl + ); + + if ($response->status() >= 400) { + throw Exception::gitHubError( + $response->json("status"), + $response->json("message") + ); + } + + return $response->json(); + } + + public function getRuns(string $accessToken, string $workflow): array + { + $response = Http::withBasicAuth("x-access-token", $accessToken)->get( + "https://api.github.com/repos/{$this->owner}/{$this->repository}/actions/workflows/{$workflow}/runs" + ); + + if ($response->status() >= 400) { + throw Exception::gitHubError( + $response->json("status"), + $response->json("message") + ); + } + + return $response->json("workflow_runs"); + } + + public function dispatchWorkflow( + string $accessToken, + string $workflow, + string $ref + ): bool { + $body = json_encode(["ref" => $ref]); + if ($body === false) { + throw new Exception( + 500, + "Failed to encode body for Github workflow dispatch endpoint." + ); + } + $response = Http::withBasicAuth("x-access-token", $accessToken)->post( + "https://api.github.com/repos/{$this->owner}/{$this->repository}/actions/workflows/{$workflow}/dispatches", + ["ref" => $ref] + ); + + if ($response->status() >= 400) { + throw Exception::gitHubError( + $response->json("status"), + $response->json("message") + ); + } + + return true; + } +} diff --git a/src/GitHubConclusion.php b/src/GitHubConclusion.php new file mode 100644 index 0000000..725442c --- /dev/null +++ b/src/GitHubConclusion.php @@ -0,0 +1,11 @@ +expiration = 10 * 60; + } + + public function createToken(): string + { + return $this->cache->remember( + "nova-publish-jwt-token", + $this->expiration, + function (): string { + $header = [ + "alg" => "RS256", + "typ" => "JWT", + ]; + + $payload = [ + "iat" => time() - 60, + "exp" => time() + $this->expiration, + "iss" => $this->applicationId, + ]; + + $header = json_encode($header); + $payload = json_encode($payload); + if ($header === false || $payload === false) { + throw new \Exception( + "Failed to encode JWT header or payload." + ); + } + $header = $this->base64url_encode($header); + $payload = $this->base64url_encode($payload); + + $privateKey = openssl_pkey_get_private($this->privateKey); + if ($privateKey === false) { + throw Exception::invalidPrivateKey(); + } + + $data = "$header.$payload"; + $success = openssl_sign( + $data, + $signature, + $privateKey, + "sha256WithRSAEncryption" + ); + if ($success === false) { + throw Exception::failedToSignData(); + } + + $signature = $this->base64url_encode($signature); + + return "$header.$payload.$signature"; + } + ); + } + + private function base64url_encode(string $data): string + { + return rtrim(strtr(base64_encode($data), "+/", "-_"), "="); + } +} diff --git a/src/PublishManager.php b/src/PublishManager.php index ad88a86..54df877 100644 --- a/src/PublishManager.php +++ b/src/PublishManager.php @@ -2,35 +2,31 @@ namespace Publish; +use DateTime; +use Illuminate\Cache\Repository; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Http; use Publish\Events\PublicationWasStarted; class PublishManager { - private $github; - - public function __construct() - { - $this->github = Http::withBasicAuth( - config("publish.github_username"), - config("publish.github_personal_access_token") - )->withHeaders(["Accept" => "application/vnd.github.v3+json"]); + public function __construct( + private GitHubApi $gitHubApi, + private Repository $cache, + private string $workflow + ) { } public function getRuns(): Collection { - $workflowPath = config("publish.workflow_path"); - - $runs = $this->github - ->get("$workflowPath/runs") - ->throw() - ->json("workflow_runs"); + $runs = $this->gitHubApi->getRuns( + $this->getGitHubAccessToken(), + $this->workflow + ); return collect($runs) ->sortByDesc("created_at") - ->whenEmpty(function () use ($workflowPath) { - throw Exception::noFirstRun($workflowPath); + ->whenEmpty(function () { + throw Exception::noFirstRun($this->workflow); }) ->map(fn($response) => Run::createFromGithubResponse($response)); } @@ -38,7 +34,7 @@ public function getRuns(): Collection public function getLastRun(): Run { return $this->getRuns() - ->where("conclusion", "!=", Run::CONCLUSION_CANCELLED) + ->where("conclusion", "!=", GitHubConclusion::cancelled->value) ->first(); } @@ -47,19 +43,44 @@ public function publish(string $ref): void /** @var Run $run */ $run = $this->getRuns()->first(); - if ($run->status !== Run::STATUS_COMPLETED) { + if ($run->status !== GitHubStatus::completed) { return; } event(new PublicationWasStarted($ref)); - $this->github - ->post(config("publish.workflow_path") . "/dispatches", [ - "ref" => $ref, - "inputs" => - config("publish.workflow_inputs") ?: new \stdClass(), - ]) - ->throw() - ->json(); + $this->gitHubApi->dispatchWorkflow( + $this->getGitHubAccessToken(), + $this->workflow, + $ref + ); + } + + protected function getGitHubAccessToken(): string + { + $cacheKey = "nova-publish-github-access-token"; + + /** @var string|null $accessToken */ + $accessToken = $this->cache->get($cacheKey); + + if ($accessToken) { + return $accessToken; + } + + $installation = $this->gitHubApi->getInstallation(); + + /** @var array{token: string, expires_at: int} $accessToken */ + $accessToken = $this->gitHubApi->getAccessToken( + $installation["access_tokens_url"] + ); + + $this->cache->set( + $cacheKey, + $accessToken["token"], + (new DateTime($accessToken["expires_at"]))->getTimestamp() - + (new DateTime())->getTimestamp() + ); + + return $accessToken["token"]; } } diff --git a/src/Run.php b/src/Run.php index 5dc84bd..0919823 100644 --- a/src/Run.php +++ b/src/Run.php @@ -6,29 +6,22 @@ class Run implements Arrayable { - const CONCLUSION_SUCCESS = "success"; - const CONCLUSION_FAILURE = "failure"; - const CONCLUSION_CANCELLED = "cancelled"; - const CONCLUSION_SKIPPED = "skipped"; - - const STATUS_COMPLETED = "completed"; - const STATUS_QUEUED = "queued"; - const STATUS_IN_PROGRESS = "in_progress"; - - public ?string $conclusion; + public ?GitHubConclusion $conclusion; public string $created_at; - public string $status; + public GitHubStatus $status; public string $updated_at; public static function createFromGithubResponse(array $data): Run { $run = new self(); - $run->conclusion = $data["conclusion"]; + $run->conclusion = $data["conclusion"] + ? GitHubConclusion::from($data["conclusion"]) + : null; $run->created_at = $data["created_at"]; - $run->status = $data["status"]; + $run->status = GitHubStatus::from($data["status"]); $run->updated_at = $data["updated_at"]; return $run; } @@ -36,9 +29,9 @@ public static function createFromGithubResponse(array $data): Run public function toArray() { return [ - "conclusion" => $this->conclusion, + "conclusion" => $this->conclusion?->value, "created_at" => $this->created_at, - "status" => $this->status, + "status" => $this->status->value, "updated_at" => $this->updated_at, ]; } diff --git a/src/ToolServiceProvider.php b/src/ToolServiceProvider.php index 0e1069f..4b7288b 100644 --- a/src/ToolServiceProvider.php +++ b/src/ToolServiceProvider.php @@ -7,16 +7,15 @@ use Laravel\Nova\Events\ServingNova; use Laravel\Nova\Http\Middleware\Authenticate; use Laravel\Nova\Nova; +use Publish\Http\Controllers\PublishController; use Publish\Http\Middleware\Authorize; class ToolServiceProvider extends ServiceProvider { /** * Bootstrap any application services. - * - * @return void */ - public function boot() + public function boot(): void { $this->loadJsonTranslationsFrom(__DIR__ . "/../resources/lang"); @@ -43,10 +42,8 @@ public function boot() /** * Register the tool's routes. - * - * @return void */ - protected function routes() + protected function routes(): void { if ($this->app->routesAreCached()) { return; @@ -64,11 +61,32 @@ protected function routes() /** * Register any application services. - * - * @return void */ - public function register() + public function register(): void { - $this->app->bind(PublishManager::class, PublishManager::class); + $this->app + ->when(GitHubApi::class) + ->needs('$owner') + ->giveConfig("publish.owner"); + + $this->app + ->when(GitHubApi::class) + ->needs('$repository') + ->giveConfig("publish.repository"); + + $this->app + ->when(JWT::class) + ->needs('$applicationId') + ->giveConfig("publish.application_id"); + + $this->app + ->when(JWT::class) + ->needs('$privateKey') + ->giveConfig("publish.private_key"); + + $this->app + ->when(PublishManager::class) + ->needs('$workflow') + ->giveConfig("publish.workflow"); } } diff --git a/tests/PublishManagerTest.php b/tests/PublishManagerTest.php deleted file mode 100644 index c0afd3c..0000000 --- a/tests/PublishManagerTest.php +++ /dev/null @@ -1,58 +0,0 @@ -expectException(Exception::class); - Http::fake([ - "github.com/*" => Http::response(["workflow_runs" => []]), - ]); - - $manager = new PublishManager(); - $manager->getLastRun(); - } - - public function testGetLastRun(): void - { - Carbon::setTestNow(now()); - - Http::fake([ - "github.com/*" => Http::response( - [ - "workflow_runs" => [ - [ - "conclusion" => null, - "created_at" => now()->format(DATE_ATOM), - "updated_at" => "123", - "status" => "asdf", - ], - [ - "conclusion" => null, - "created_at" => now() - ->subMinute() - ->format(DATE_ATOM), - "updated_at" => "123", - "status" => "asdf", - ], - ], - ], - 200, - [] - ), - ]); - - $manager = new PublishManager(); - $actual = $manager->getLastRun(); - - $this->assertSame(now()->format(DATE_ATOM), $actual->created_at); - Carbon::setTestNow(); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index bdc2785..bf5f1c1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,15 +10,15 @@ class TestCase extends \Orchestra\Testbench\TestCase protected function defineEnvironment($app) { - $app["config"]->set("publish.github_username", "username"); - $app["config"]->set( - "publish.github_personal_access_token", - "access_token" - ); - $app["config"]->set( - "publish.workflow_path", - "https://api.github.com/repos/grrr-amsterdam/nova-publish/actions/workflows/some-workflow.yml" + openssl_pkey_export( + openssl_pkey_get_private(openssl_pkey_new()), + $privateKey ); + $app["config"]->set("publish.application_id", 123456); + $app["config"]->set("publish.private_key", $privateKey); + $app["config"]->set("publish.owner", "norday-agency"); + $app["config"]->set("publish.repository", "norday.nl"); + $app["config"]->set("publish.workflow", "some-workflow.yml"); } protected function getPackageProviders($app)