diff --git a/README.md b/README.md index 293a57c..fd62049 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,37 @@ Example: TELESCOPE_ENABLED=false ``` +#### `existing-certificate` & `existing-certificate-key` + +The `existing-certificate` and `existing-certificate-key` input parameters allow you to supply a custom SSL certificate for the preview site instead of obtaining one from Let’s Encrypt. + +Example: + +```yaml +- uses: bakerkretzmar/laravel-deploy-preview@v2 + with: + forge-token: ${{ secrets.FORGE_TOKEN }} + servers: | + qa-1.acme.dev 60041 + existing-certificate: ${{ secrets.SSL_CERTIFICATE }} + existing-certificate-key: ${{ secrets.SSL_PRIVATE_KEY }} +``` + +#### `clone-certificate` + +The `clone-certificate` input parameter allows you to clone an existing Forge SSL certificate for the preview site instead of obtaining one from Let’s Encrypt. The parameter value should be the ID of an existing SSL certificate in Forge. + +Example: + +```yaml +- uses: bakerkretzmar/laravel-deploy-preview@v2 + with: + forge-token: ${{ secrets.FORGE_TOKEN }} + servers: | + qa-1.acme.dev 60041 + clone-certificate: 90051 +``` + ## Development This action is loosely based on GitHub's [hello-world-javascript-action](https://github.com/actions/hello-world-javascript-action) and [typescript-action](https://github.com/actions/typescript-action) templates. It's written in TypeScript and compiled with [`ncc`](https://github.com/vercel/ncc) into a single JavaScript file. diff --git a/action.yml b/action.yml index 0f22f9e..c897d35 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,15 @@ inputs: environment: description: Environment variables to add or update on the preview site. required: false + existing-certificate: + description: Existing SSL certificate to use for the preview site. + required: false + existing-certificate-key: + description: Existing SSL certificate private key to use for the preview site. + required: false + clone-certificate: + description: Forge SSL certificate ID to clone to the preview site. + required: false runs: using: node20 main: dist/index.js diff --git a/package-lock.json b/package-lock.json index 7a906de..d3104a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "laravel-deploy-preview", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laravel-deploy-preview", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "devDependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 4ef606e..03fca52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "laravel-deploy-preview", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "description": "GitHub action to deploy PR preview sites for Laravel apps.", "author": "Jacob Baker-Kretzmar ", diff --git a/src/action.ts b/src/action.ts index 4162063..aa9eeb3 100644 --- a/src/action.ts +++ b/src/action.ts @@ -6,6 +6,7 @@ type CreateConfig = { servers: Array<{ id: number; domain: string }>; afterDeploy?: string; environment?: Record; + certificate?: { type: 'clone' | 'existing'; certificate: string; key?: string }; info?: Function; debug?: Function; local?: boolean; @@ -29,6 +30,7 @@ export async function createPreview({ servers, afterDeploy = '', environment = {}, + certificate, info = console.log, debug = console.log, local = false, @@ -53,9 +55,19 @@ export async function createPreview({ const site = await server.createSite(name, database); info('Site created!'); - info('Obtaining SSL certificate'); - await site.installCertificate(); - info('SSL certificate obtained!'); + if (certificate?.type === 'existing') { + info('Installing SSL certificate'); + await site.installExistingCertificate(certificate.certificate, certificate.key); + info('SSL certificate installed!'); + } else if (certificate?.type === 'clone') { + info('Cloning SSL certificate'); + await site.cloneExistingCertificate(Number(certificate.certificate)); + info('SSL certificate cloned!'); + } else { + info('Obtaining SSL certificate'); + await site.obtainCertificate(); + info('SSL certificate obtained!'); + } info(`Installing '${repository}' Git repository in site`); await site.installRepository(repository, local ? 'main' : name); diff --git a/src/forge.ts b/src/forge.ts index 903e644..8c9907e 100644 --- a/src/forge.ts +++ b/src/forge.ts @@ -25,6 +25,7 @@ type JobPayload = { type CertificatePayload = { id: number; status: string; + activation_status?: string | null; active: boolean; }; @@ -141,15 +142,51 @@ export class Forge { return (await this.post(`servers/${server}/sites/${site}/deployment/deploy`)).data.site; } + static async installExistingCertificate( + server: number, + site: number, + certificate: string, + key: string, + ): Promise { + return ( + await this.post(`servers/${server}/sites/${site}/certificates`, { + type: 'existing', + certificate, + key, + }) + ).data.certificate; + } + + static async cloneExistingCertificate( + server: number, + site: number, + certificate: number, + ): Promise { + return ( + await this.post(`servers/${server}/sites/${site}/certificates`, { + type: 'clone', + certificate_id: certificate, + }) + ).data.certificate; + } + static async obtainCertificate(server: number, site: number, domain: string): Promise { return (await this.post(`servers/${server}/sites/${site}/certificates/letsencrypt`, { domains: [domain] })).data .certificate; } + static async listCertificates(server: number, site: number): Promise { + return (await this.get(`servers/${server}/sites/${site}/certificates`)).data.certificates; + } + static async getCertificate(server: number, site: number, certificate: number): Promise { return (await this.get(`servers/${server}/sites/${site}/certificates/${certificate}`)).data.certificate; } + static async activateCertificate(server: number, site: number, certificate: number): Promise { + await this.post(`servers/${server}/sites/${site}/certificates/${certificate}/activate`); + } + static setToken(token: string): void { this.#token = token; } @@ -278,15 +315,29 @@ export class Site { await Forge.updateDeployScript(this.server_id, this.id, `${script}\n${append}`); } - async installCertificate(): Promise { + async obtainCertificate(): Promise { this.certificate_id = (await Forge.obtainCertificate(this.server_id, this.id, this.name)).id; } + async installExistingCertificate(certificate: string, key: string): Promise { + this.certificate_id = (await Forge.installExistingCertificate(this.server_id, this.id, certificate, key)).id; + } + + async cloneExistingCertificate(certificate: number): Promise { + this.certificate_id = (await Forge.cloneExistingCertificate(this.server_id, this.id, certificate)).id; + } + async ensureCertificateActivated(): Promise { let certificate = await Forge.getCertificate(this.server_id, this.id, this.certificate_id); await until( () => certificate.active, - async () => (certificate = await Forge.getCertificate(this.server_id, this.id, this.certificate_id)), + async () => { + if (certificate.activation_status !== 'activated') { + certificate = await Forge.getCertificate(this.server_id, this.id, this.certificate_id); + } else { + await Forge.activateCertificate(this.server_id, this.id, this.certificate_id); + } + }, ); } diff --git a/src/index.ts b/src/index.ts index ec9549a..b019361 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,41 @@ const environment = core.getMultilineInput('environment', { required: false }).r return { ...all, [key]: value }; }, {}); +const existingCertificate = core.getInput('existing-certificate', { required: false }); +const existingCertificateKey = core.getInput('existing-certificate-key', { required: false }); +const cloneCertificate = core.getInput('clone-certificate', { required: false }); + +if (!!existingCertificate !== !!existingCertificateKey) { + core.error( + `Invalid certificate inputs: ${ + !existingCertificate + ? '`existing-certificate` missing.' + : !existingCertificateKey + ? '`existing-certificate-key` missing' + : '' + }`, + ); +} + +if (!!cloneCertificate && (!!existingCertificate || !!existingCertificateKey)) { + core.error( + "Invalid certificate inputs: cannot use 'existing' and 'clone' inputs together. Remove `existing-certificate` and `existing-certificate-key`, or `clone-certificate`.", + ); +} + +const certificate = !!cloneCertificate + ? { + type: 'clone' as 'clone' | 'existing', + certificate: cloneCertificate, + } + : !!existingCertificate + ? { + type: 'existing' as 'clone' | 'existing', + certificate: existingCertificate, + key: existingCertificateKey, + } + : undefined; + const pr = github.context.payload as PullRequestEvent; if (pr.action === 'opened') { @@ -42,6 +77,7 @@ if (pr.action === 'opened') { servers, afterDeploy, environment, + certificate, info: core.info, debug: core.debug, });