Skip to content

Commit

Permalink
Merge pull request #29 from bakerkretzmar/sqlite-etc
Browse files Browse the repository at this point in the history
Support SQLite
  • Loading branch information
bakerkretzmar authored Nov 7, 2023
2 parents b9588a9 + 07dd2e1 commit ef353d5
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 50 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ Example:
clone-certificate: 90051
```

### Databases

This action creates a new database for each preview site and deletes the database when the preview site is deleted. If your Forge server has one of the default supported database engines installed (MySQL, MariaDB, or PostgreSQL), that database engine will be used and no additional configuration is necessary.

To use SQLite, set your preview sites’ `DB_CONNECTION` environment variable to `sqlite` using [the `environment` input](#environment):

```yaml
- uses: bakerkretzmar/laravel-deploy-preview@v2
with:
forge-token: ${{ secrets.FORGE_TOKEN }}
servers: |
qa-1.acme.dev 60041
environment: |
DB_CONNECTION=sqlite
```

## 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build": "ncc build src/index.ts --source-map --license licenses.txt",
"debug": "ncc run src/debug.ts",
"test": "vitest unit",
"test:integration": "vitest run integration --test-timeout=30000"
"test:integration": "vitest run integration --test-timeout=60000"
},
"dependencies": {
"@actions/core": "^1.10.1",
Expand Down
28 changes: 24 additions & 4 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export async function createPreview({
repository: string;
servers: { id: number; domain: string }[];
afterDeploy?: string;
environment?: Record<string, string>;
environment: Record<string, string>;
certificate?: { type: 'clone'; certificate: number } | { type: 'existing'; certificate: string; key: string };
}) {
core.info(`Creating preview site for branch: ${branch}.`);
Expand All @@ -32,7 +32,11 @@ export async function createPreview({
}

core.info(`Creating site: ${siteName}.`);
site = await Site.create(servers[0].id, siteName, normalizeDatabaseName(branch));
site = await Site.create(
servers[0].id,
siteName,
environment.DB_CONNECTION === 'sqlite' ? '' : normalizeDatabaseName(branch),
);

if (certificate?.type === 'existing') {
core.info('Installing existing SSL certificate.');
Expand All @@ -48,9 +52,21 @@ export async function createPreview({
core.info(`Installing repository: ${repository}.`);
await site.installRepository(repository, branch);

const sqliteEnvironment =
environment.DB_CONNECTION === 'sqlite'
? {
DB_HOST: undefined,
DB_PORT: undefined,
DB_DATABASE: undefined,
DB_USERNAME: undefined,
DB_PASSWORD: undefined,
}
: {};

core.info('Updating `.env` file.');
await site.setEnvironmentVariables({
DB_DATABASE: normalizeDatabaseName(branch),
...sqliteEnvironment,
...environment,
});

Expand All @@ -77,9 +93,11 @@ export async function createPreview({
export async function destroyPreview({
branch,
servers,
environment = {},
}: {
branch: string;
servers: { id: number; domain: string }[];
environment: Record<string, string>;
}) {
core.info(`Removing preview site: ${branch}.`);

Expand All @@ -103,6 +121,8 @@ export async function destroyPreview({
core.info('Deleting site.');
await site.delete();

core.info('Deleting database.');
await site.deleteDatabase(normalizeDatabaseName(branch));
if (environment.DB_CONNECTION !== 'sqlite') {
core.info('Deleting database.');
await site.deleteDatabase(normalizeDatabaseName(branch));
}
}
33 changes: 22 additions & 11 deletions src/forge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { sleep, until } from './lib.js';
import { sleep, until, updateDotEnvString } from './lib.js';

type ServerPayload = {
id: number;
Expand Down Expand Up @@ -34,6 +34,13 @@ type DatabasePayload = {
name: string;
};

type CommandPayload = {
id: number;
command: string;
status: string;
duration: string;
};

export class ForgeError extends Error {
axiosError: AxiosError;
data?: unknown;
Expand Down Expand Up @@ -194,6 +201,17 @@ export class Forge {
await this.post(`servers/${server}/sites/${site}/certificates/${certificate}/activate`);
}

static async runCommand(server: number, site: number, command: string) {
return (await this.post<{ command: CommandPayload }>(`servers/${server}/sites/${site}/commands`, { command })).data
.command;
}

static async getCommand(server: number, site: number, command: number) {
return (
await this.get<{ command: CommandPayload; output: string }>(`servers/${server}/sites/${site}/commands/${command}`)
).data;
}

static token(token: string) {
this.#token = token;
}
Expand Down Expand Up @@ -308,16 +326,9 @@ export class Site {
);
}

async setEnvironmentVariables(variables: Record<string, string>) {
let env = await Forge.getEnvironmentFile(this.server_id, this.id);
Object.entries(variables).map(([key, value]) => {
if (new RegExp(`${key}=`).test(env)) {
env = env.replace(new RegExp(`${key}=.*`), `${key}=${value}`);
} else {
env += `\n${key}=${value}\n`;
}
});
await Forge.updateEnvironmentFile(this.server_id, this.id, env);
async setEnvironmentVariables(variables: Record<string, string | undefined>) {
const env = await Forge.getEnvironmentFile(this.server_id, this.id);
await Forge.updateEnvironmentFile(this.server_id, this.id, updateDotEnvString(env, variables));
}

async installScheduler() {
Expand Down
11 changes: 11 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export function normalizeDomainName(input: string) {
return input.replace(/\W+/g, '-').substring(0, 63).replace(/^-|-$/g, '');
}

export function updateDotEnvString(env: string, variables: Record<string, string | undefined>) {
Object.entries(variables).map(([key, value]) => {
if (new RegExp(`${key}=`).test(env)) {
env = env.replace(new RegExp(`${key}=.*\n?`), value === undefined ? '' : `${key}=${value}\n`);
} else {
env += `\n${key}=${value}\n`;
}
});
return env;
}

// function serverWithFewestSites(servers: Server[], sites: Site[]): Server {
// const serverSites = sites.reduce((carry: { [_: string]: number }, site: Site) => {
// carry[site.server_id] ??= 0;
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function run() {
await destroyPreview({
branch: pr.pull_request.head.ref,
servers,
environment,
});
}
} catch (error) {
Expand Down
155 changes: 122 additions & 33 deletions tests/integration/forge.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as crypto from 'node:crypto';
import { afterAll, describe, expect, test } from 'vitest';
import { Forge, ForgeError } from '../../src/forge';
import { normalizeDatabaseName, until } from '../../src/lib';
import { normalizeDatabaseName, until, updateDotEnvString } from '../../src/lib';

// @ts-expect-error import.meta not set up
Forge.token(import.meta.env.VITE_FORGE_TOKEN);
Expand Down Expand Up @@ -129,38 +129,6 @@ describe('sites', () => {
expect.assertions(3);
});

// Needs a repo or smth installed first or Forge 500s
test.todo('enable quick deploy', async () => {
const name = `test-${id()}.laravel-deploy-preview.com`;

let site = await Forge.createSite(server, name, '');

expect(site).toMatchObject({
server_id: server,
name: name,
status: 'installing',
});

await until(
() => site.status === 'installed',
async () => (site = await Forge.getSite(server, site.id)),
);

expect(site).toMatchObject({
server_id: server,
name: name,
status: 'installed',
});

site = await Forge.enableQuickDeploy(server, site.id);

expect(site).toMatchObject({
server_id: server,
name: name,
quick_deploy: 'true',
});
});

test('create SSL certificate', async () => {
const name = `test-${id()}.laravel-deploy-preview.com`;

Expand Down Expand Up @@ -374,4 +342,125 @@ describe('sites', () => {
active: true,
});
});

test.todo('install repository');

test.todo('enable quick deploy');
// site = await Forge.enableQuickDeploy(server, site.id);
// expect(site).toMatchObject({
// server_id: server,
// name: name,
// quick_deploy: 'true',
// });

test('handle failing to enable quick deploy', async () => {
const name = `test-${id()}.laravel-deploy-preview.com`;

let site = await Forge.createSite(server, name, '');

expect(site).toMatchObject({
server_id: server,
name: name,
status: 'installing',
});

await until(
() => site.status === 'installed',
async () => (site = await Forge.getSite(server, site.id)),
);

expect(site).toMatchObject({
server_id: server,
name: name,
status: 'installed',
});

try {
await Forge.enableQuickDeploy(server, site.id);
} catch (e) {
expect(e).toBeInstanceOf(ForgeError);
expect(e.message).toBe('Forge API request failed with status code 400.');
expect(e.data).toMatchObject({
message: 'The site does not yet have an application installed. Please install an application and try again.',
});
}

expect.assertions(5);
});

test.todo('update environment file');

test.todo('deploy site');

test.todo('run command');

test.todo('handle failed command');
// status will be 'failed'

test('use sqlite', async () => {
const name = `test-${id()}.laravel-deploy-preview.com`;

let site = await Forge.createSite(server, name, '');

await until(
() => site.status === 'installed',
async () => (site = await Forge.getSite(server, site.id)),
);

await Forge.createGitProject(server, site.id, 'bakerkretzmar/laravel-deploy-preview-app', 'main');

await until(
() => site.repository_status === 'installed',
async () => (site = await Forge.getSite(server, site.id)),
3,
);

const env = await Forge.getEnvironmentFile(server, site.id);

await Forge.updateEnvironmentFile(
server,
site.id,
updateDotEnvString(env, {
DB_CONNECTION: 'sqlite',
DB_DATABASE: undefined,
}),
);

let command1 = await Forge.runCommand(server, site.id, 'ls database');
let output1 = '';

await until(
() => command1.status === 'finished',
async () => ({ command: command1, output: output1 } = await Forge.getCommand(server, site.id, command1.id)),
);

expect(output1).not.toContain('database.sqlite');

await Forge.deploy(server, site.id);

await until(
() => site.deployment_status === null,
async () => (site = await Forge.getSite(server, site.id)),
);

let command2 = await Forge.runCommand(server, site.id, 'ls database');
let output2 = '';

await until(
() => command2.status === 'finished',
async () => ({ command: command2, output: output2 } = await Forge.getCommand(server, site.id, command2.id)),
);

expect(output2).toContain('database.sqlite');

let command3 = await Forge.runCommand(server, site.id, 'php artisan db:monitor --databases=sqlite');
let output3 = '';

await until(
() => command3.status === 'finished',
async () => ({ command: command3, output: output3 } = await Forge.getCommand(server, site.id, command3.id)),
);

expect(output3).toMatch(/sqlite \.+ \[\] OK/);
});
});
Loading

0 comments on commit ef353d5

Please sign in to comment.