Skip to content

Commit

Permalink
Add ability to use or clone existing certificate
Browse files Browse the repository at this point in the history
  • Loading branch information
bakerkretzmar committed Oct 23, 2023
1 parent 7bbb6ac commit d12e80f
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 8 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
Expand Down
18 changes: 15 additions & 3 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type CreateConfig = {
servers: Array<{ id: number; domain: string }>;
afterDeploy?: string;
environment?: Record<string, string>;
certificate?: { type: 'clone' | 'existing'; certificate: string; key?: string };
info?: Function;
debug?: Function;
local?: boolean;
Expand All @@ -29,6 +30,7 @@ export async function createPreview({
servers,
afterDeploy = '',
environment = {},
certificate,
info = console.log,
debug = console.log,
local = false,
Expand All @@ -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);
Expand Down
55 changes: 53 additions & 2 deletions src/forge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type JobPayload = {
type CertificatePayload = {
id: number;
status: string;
activation_status?: string | null;
active: boolean;
};

Expand Down Expand Up @@ -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<CertificatePayload> {
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<CertificatePayload> {
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<CertificatePayload> {
return (await this.post(`servers/${server}/sites/${site}/certificates/letsencrypt`, { domains: [domain] })).data
.certificate;
}

static async listCertificates(server: number, site: number): Promise<CertificatePayload[]> {
return (await this.get(`servers/${server}/sites/${site}/certificates`)).data.certificates;
}

static async getCertificate(server: number, site: number, certificate: number): Promise<CertificatePayload> {
return (await this.get(`servers/${server}/sites/${site}/certificates/${certificate}`)).data.certificate;
}

static async activateCertificate(server: number, site: number, certificate: number): Promise<void> {
await this.post(`servers/${server}/sites/${site}/certificates/${certificate}/activate`);
}

static setToken(token: string): void {
this.#token = token;
}
Expand Down Expand Up @@ -278,15 +315,29 @@ export class Site {
await Forge.updateDeployScript(this.server_id, this.id, `${script}\n${append}`);
}

async installCertificate(): Promise<void> {
async obtainCertificate(): Promise<void> {
this.certificate_id = (await Forge.obtainCertificate(this.server_id, this.id, this.name)).id;
}

async installExistingCertificate(certificate: string, key: string): Promise<void> {
this.certificate_id = (await Forge.installExistingCertificate(this.server_id, this.id, certificate, key)).id;
}

async cloneExistingCertificate(certificate: number): Promise<void> {
this.certificate_id = (await Forge.cloneExistingCertificate(this.server_id, this.id, certificate)).id;
}

async ensureCertificateActivated(): Promise<void> {
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);
}
},
);
}

Expand Down
36 changes: 36 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -42,6 +77,7 @@ if (pr.action === 'opened') {
servers,
afterDeploy,
environment,
certificate,
info: core.info,
debug: core.debug,
});
Expand Down

0 comments on commit d12e80f

Please sign in to comment.