From 20668603c869687e1bfa14d32187e7274dfec2e9 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:42:32 -0700 Subject: [PATCH 1/7] Add gravity update --- README.md | 1 + src/client.test.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++- src/client.ts | 41 +++++++++++++++---- src/config.test.ts | 4 ++ src/config.ts | 4 ++ 5 files changed, 141 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 93927cb0..80ae4e5d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ It is recommended you run this service with Docker. | `SECONDARY_HOST_(#)_BASE_URL` | Yes | N/A | `http://192.168.1.3` or `https://pihole2.example.com` | The base URL of your secondary Pi-hole, including the scheme (HTTP or HTTPS) and port but not including a following slash. Replace `(#)` with a number, starting at `1`, to add multiple secondary Pi-holes. | | `SECONDARY_HOST_(#)_PASSWORD` | Yes | N/A | `mypassword2` | The password used to log in to the admin interface. | | `INTERVAL_MINUTES` | No | 30 | Any non-zero positive integer, for example `5`, `30`, or `1440` | How long to wait between synchronizations. Defaults to five minutes. Remember that the DNS server on your secondary servers restarts everytime a sync is performed. | +| `UPDATE_GRAVITY` | No | `true` | `true`/`false` | Triggers a gravity update after a backup has been uploaded to a secondary Pi-hole. This updates adlists and restarts gravity. | | `SYNC_WHITELIST` | No | `true` | `true`/`false` | Copies the whitelist | | `SYNC_REGEX_WHITELIST` | No | `true` | `true`/`false` | Copies the regex whitelist | | `SYNC_BLACKLIST` | No | `true` | `true`/`false` | Copies the blacklist | diff --git a/src/client.test.ts b/src/client.test.ts index 9a088968..711e6d83 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -231,8 +231,104 @@ describe('Client', () => { }); }); - test('should upload backup successfully', async () => { + test('should throw error if gravity update fails', async () => { + teleporter + .post('/admin/scripts/pi-hole/php/teleporter.php') + .reply( + 200, + 'Processed adlist (14 entries)
\n' + + 'Processed adlist group assignments (13 entries)
\n' + + 'Processed blacklist (exact) (0 entries)
\n' + + 'Processed blacklist (regex) (3 entries)
\n' + + 'Processed client (8 entries)
\n' + + 'Processed client group assignments (16 entries)
\n' + + 'Processed local DNS records (41 entries)
\n' + + 'Processed domain_audit (0 entries)
\n' + + 'Processed black-/whitelist group assignments (10 entries)
\n' + + 'Processed group (3 entries)
\n' + + 'Processed whitelist (exact) (4 entries)
\n' + + 'Processed whitelist (regex) (0 entries)
\n' + + 'OK' + ); + teleporter + .get('/admin/scripts/pi-hole/php/gravity.sh.php', undefined) + .reply(200, '\ndata: \n\ndata: [✓] TCP (IPv6)\ndata: \ndata: \n\ndata:'); + + const expectError = expect(client.uploadBackup(backup)).rejects; + + await expectError.toBeInstanceOf(ErrorNotification); + await expectError.toMatchObject({ + message: 'Failed updating gravity on "http://10.0.0.2".', + verbose: { + host: 'http://10.0.0.2', + status: 200, + eventStream: '[✓] TCP (IPv6)' + } + }); + }); + + test('should upload backup and update gravity successfully', async () => { + const syncOptions = jest.spyOn(Config, 'syncOptions', 'get'); + + let requestBody = ''; + teleporter + .post('/admin/scripts/pi-hole/php/teleporter.php', (body) => (requestBody = body)) + .reply( + 200, + 'Processed adlist (14 entries)
\n' + + 'Processed adlist group assignments (13 entries)
\n' + + 'Processed blacklist (exact) (0 entries)
\n' + + 'Processed blacklist (regex) (3 entries)
\n' + + 'Processed client (8 entries)
\n' + + 'Processed client group assignments (16 entries)
\n' + + 'Processed local DNS records (41 entries)
\n' + + 'Processed domain_audit (0 entries)
\n' + + 'Processed black-/whitelist group assignments (10 entries)
\n' + + 'Processed group (3 entries)
\n' + + 'Processed whitelist (exact) (4 entries)
\n' + + 'Processed whitelist (regex) (0 entries)
\n' + + 'OK' + ); + teleporter + .get('/admin/scripts/pi-hole/php/gravity.sh.php', undefined) + .reply( + 200, + '\ndata: \n\ndata: [✓] TCP (IPv6)\ndata: \ndata: \n\ndata: [✓] Pi-hole blocking is enabled\ndata: \n\ndata:' + ); + + const result = await client.uploadBackup(backup); + + expect(result).toStrictEqual(true); + expect(syncOptions).toHaveBeenCalled(); + expect(requestBody).toContain( + 'name="token"\r\n\r\nabcdefgijklmnopqrstuvwxyzabcdefgijklmnopqrst' + ); + expect(requestBody).toContain('name="whitelist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="regex_whitelist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="blacklist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="regexlist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="adlist"\r\n\r\ntrue'); + expect(requestBody).toContain('name="client"\r\n\r\ntrue'); + expect(requestBody).toContain('name="group"\r\n\r\ntrue'); + expect(requestBody).toContain('name="auditlog"\r\n\r\nfalse'); + expect(requestBody).toContain('name="staticdhcpleases"\r\n\r\nfalse'); + expect(requestBody).toContain('name="localdnsrecords"\r\n\r\ntrue'); + expect(requestBody).toContain('name="localcnamerecords"\r\n\r\ntrue'); + expect(requestBody).toContain('name="flushtables"\r\n\r\ntrue'); + expect(requestBody).toContain('name="action"\r\n\r\nin'); + expect(requestBody).toContain( + 'name="zip_file"; filename="backup.tar.gz"\r\nContent-Type: application/octet-stream' + ); + expect(requestBody.match(/Content-Disposition: form-data; name=/g)).toHaveLength( + 15 + ); + }); + + test('should not update gravity if `updateGravity` is disabled', async () => { const syncOptions = jest.spyOn(Config, 'syncOptions', 'get'); + const updateGravity = jest + .spyOn(Config, 'updateGravity', 'get') + .mockReturnValue(false); let requestBody = ''; teleporter @@ -280,6 +376,7 @@ describe('Client', () => { expect(requestBody.match(/Content-Disposition: form-data; name=/g)).toHaveLength( 15 ); + updateGravity.mockRestore(); }); }); }); diff --git a/src/client.ts b/src/client.ts index c2623859..88290b19 100644 --- a/src/client.ts +++ b/src/client.ts @@ -110,26 +110,53 @@ export class Client { form.append('action', 'in'); form.append('zip_file', backup, 'backup.tar.gz'); - const response = await this.fetch( + const uploadResponse = await this.fetch( `${this.host.baseUrl}/admin/scripts/pi-hole/php/teleporter.php`, { body: form, method: 'POST' } ); - const text = await response.text(); - if (response.status !== 200 || !text.endsWith('OK')) + const uploadText = await uploadResponse.text(); + if (uploadResponse.status !== 200 || !uploadText.endsWith('OK')) throw new ErrorNotification({ - message: `Error: failed to upload backup to "${this.host.baseUrl}".`, + message: `Failed to upload backup to "${this.host.baseUrl}".`, verbose: { host: this.host.baseUrl, - status: response.status, - responseBody: text + status: uploadResponse.status, + responseBody: uploadText } }); Log.info(chalk.green(`✔️ Backup uploaded to ${this.host.baseUrl}!`)); - Log.verbose(`Result:\n${chalk.blue(text)}`); + Log.verbose(`Result:\n${chalk.blue(uploadText)}`); + + if (Config.updateGravity) { + Log.info(chalk.yellow(`➡️ Updating gravity on ${this.host.baseUrl}...`)); + const gravityUpdateResponse = await this.fetch( + `${this.host.baseUrl}/admin/scripts/pi-hole/php/gravity.sh.php`, + { method: 'GET' } + ); + + const updateText = (await gravityUpdateResponse.text()) + .replaceAll('\ndata:', '') + .trim(); + if ( + gravityUpdateResponse.status !== 200 || + !updateText.endsWith('Pi-hole blocking is enabled') + ) + throw new ErrorNotification({ + message: `Failed updating gravity on "${this.host.baseUrl}".`, + verbose: { + host: this.host.baseUrl, + status: gravityUpdateResponse.status, + eventStream: updateText + } + }); + + Log.info(chalk.green(`✔️ Gravity updated on ${this.host.baseUrl}!`)); + Log.verbose(`Result:\n${chalk.blue(updateText)}`); + } return true; } diff --git a/src/config.test.ts b/src/config.test.ts index 7941fa6c..9295877f 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -238,6 +238,10 @@ describe('Config', () => { }); }); + describe('updateGravity', () => { + testToHaveDefaultAndOverride('updateGravity', true, 'UPDATE_GRAVITY'); + }); + describe('verboseMode', () => { testToHaveDefaultAndOverride('verboseMode', false, 'VERBOSE'); }); diff --git a/src/config.ts b/src/config.ts index 2d2cdcb0..de45ad16 100644 --- a/src/config.ts +++ b/src/config.ts @@ -64,6 +64,10 @@ export class Config { return this._syncOptions; } + static get updateGravity(): boolean { + return process.env['UPDATE_GRAVITY'] !== 'false'; + } + static get verboseMode(): boolean { return process.env['VERBOSE'] === 'true'; } From 6cd5a32fa5f02996b3689431c6e7aa860a1ac9cd Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:43:00 -0700 Subject: [PATCH 2/7] Make client error wording consistent --- src/client.test.ts | 20 ++++++++++---------- src/client.ts | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client.test.ts b/src/client.test.ts index 711e6d83..34b373c2 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -1,10 +1,10 @@ import { jest } from '@jest/globals'; import nock from 'nock'; import { Blob } from 'node-fetch'; -import { ErrorNotification } from './notify'; import { Client } from './client'; import type { Host } from './config'; import { Config } from './config'; +import { ErrorNotification } from './notify'; describe('Client', () => { const host: Host = { @@ -38,7 +38,7 @@ describe('Client', () => { await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ message: - 'Error: there was an error logging in to "http://10.0.0.2" - are you able to log in with the configured password?', + 'There was an error logging in to "http://10.0.0.2" - are you able to log in with the configured password?', verbose: { host: 'http://10.0.0.2', status: 500, @@ -58,7 +58,7 @@ describe('Client', () => { await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ message: - 'Error: no token could be found while logging in to "http://10.0.0.2" - are you able to log in with the configured password?', + 'No token could be found while logging in to "http://10.0.0.2" - are you able to log in with the configured password?', verbose: { host: 'http://10.0.0.2', innerHtml: '' @@ -79,7 +79,7 @@ describe('Client', () => { await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ message: - 'Error: a token was found but could not be validated while logging in to "http://10.0.0.2" - are you able to log in with the configured password?', + 'A token was found but could not be validated while logging in to "http://10.0.0.2" - are you able to log in with the configured password?', verbose: { host: 'http://10.0.0.2', token: 'abcdef' @@ -124,7 +124,7 @@ describe('Client', () => { await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ - message: 'Error: failed to download backup from "http://10.0.0.2".', + message: 'Failed to download backup from "http://10.0.0.2".', verbose: { host: 'http://10.0.0.2', status: 500, @@ -142,7 +142,7 @@ describe('Client', () => { await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ - message: 'Error: failed to download backup from "http://10.0.0.2".', + message: 'Failed to download backup from "http://10.0.0.2".', verbose: { host: 'http://10.0.0.2', status: 200, @@ -199,14 +199,14 @@ describe('Client', () => { teleporter.done(); }); - test('should throw BackupUploadError if response is non-200', async () => { + test('should throw error if response is non-200', async () => { teleporter.post('/admin/scripts/pi-hole/php/teleporter.php').reply(500); const expectError = expect(client.uploadBackup(backup)).rejects; await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ - message: 'Error: failed to upload backup to "http://10.0.0.2".', + message: 'Failed to upload backup to "http://10.0.0.2".', verbose: { host: 'http://10.0.0.2', status: 500, @@ -215,14 +215,14 @@ describe('Client', () => { }); }); - test('should throw BackupUploadError if response does not end with "OK"', async () => { + test('should throw error if response does not end with "OK"', async () => { teleporter.post('/admin/scripts/pi-hole/php/teleporter.php').reply(200); const expectError = expect(client.uploadBackup(backup)).rejects; await expectError.toBeInstanceOf(ErrorNotification); await expectError.toMatchObject({ - message: 'Error: failed to upload backup to "http://10.0.0.2".', + message: 'Failed to upload backup to "http://10.0.0.2".', verbose: { host: 'http://10.0.0.2', status: 200, diff --git a/src/client.ts b/src/client.ts index 88290b19..5d049094 100644 --- a/src/client.ts +++ b/src/client.ts @@ -34,7 +34,7 @@ export class Client { }); if (response.status !== 200) throw new ErrorNotification({ - message: `Error: there was an error logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, + message: `There was an error logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, verbose: { host: host.baseUrl, status: response.status, @@ -53,7 +53,7 @@ export class Client { const tokenDiv = root.querySelector('#token'); if (!tokenDiv) throw new ErrorNotification({ - message: `Error: no token could be found while logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, + message: `No token could be found while logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, verbose: { host: host.baseUrl, innerHtml: root.innerHTML @@ -63,7 +63,7 @@ export class Client { const token = tokenDiv.innerText; if (token.length != 44) throw new ErrorNotification({ - message: `Error: a token was found but could not be validated while logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, + message: `A token was found but could not be validated while logging in to "${host.baseUrl}" - are you able to log in with the configured password?`, verbose: { host: host.baseUrl, token: token @@ -89,7 +89,7 @@ export class Client { response.headers.get('content-type') !== 'application/gzip' ) throw new ErrorNotification({ - message: `Error: failed to download backup from "${this.host.baseUrl}".`, + message: `Failed to download backup from "${this.host.baseUrl}".`, verbose: { host: this.host.baseUrl, status: response.status, From 21737a96fee2de8c0db50771f3bfe12cee637144 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:43:23 -0700 Subject: [PATCH 3/7] Stringify non-string logging --- src/log.test.ts | 13 +++++++++++++ src/log.ts | 11 ++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/log.test.ts b/src/log.test.ts index 7fb7f7e7..7653c728 100644 --- a/src/log.test.ts +++ b/src/log.test.ts @@ -10,6 +10,7 @@ describe('Log', () => { afterEach(() => { jest.resetModules(); + jest.restoreAllMocks(); }); afterAll(() => { @@ -27,6 +28,18 @@ describe('Log', () => { `${chalk.dim('8/27/2022, 8:17:31 AM')}: Hello world` ); }); + + test('should log stringified', () => { + const consoleLog = jest.spyOn(console, 'log'); + + Log.info({ foo: 'bar' }); + + expect(consoleLog).toHaveBeenCalledTimes(1); + expect(consoleLog).toHaveBeenCalledWith( + // eslint-disable-next-line no-useless-escape + `${chalk.dim('8/27/2022, 8:17:31 AM')}: {\"foo\":\"bar\"}` + ); + }); }); describe('verbose', () => { diff --git a/src/log.ts b/src/log.ts index e73a2911..fbab9958 100644 --- a/src/log.ts +++ b/src/log.ts @@ -3,15 +3,20 @@ import { Config } from './config.js'; export class Log { static info(message: unknown) { - console.log(`${this.timestamp}: ${message}`); + console.log(`${this.timestamp}: ${this.stringify(message)}`); } static verbose(message: unknown) { - if (Config.verboseMode && message) this.info(message); + if (Config.verboseMode && message) this.info(this.stringify(message)); } static error(message: unknown) { - console.error(`${this.timestamp}: ${chalk.red(message)}`); + console.error(`${this.timestamp}: ${chalk.red(this.stringify(message))}`); + } + + private static stringify(message: unknown): string { + if (typeof message === 'string') return message; + else return JSON.stringify(message); } private static get timestamp(): string { From e7299861188d0273535111ac6bda0a8d89a75976 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:50:19 -0700 Subject: [PATCH 4/7] Return non-zero exit code if `runOnce` --- src/notify.ts | 2 +- src/sync.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/notify.ts b/src/notify.ts index 054f46e1..061376f3 100644 --- a/src/notify.ts +++ b/src/notify.ts @@ -80,7 +80,7 @@ export class Notify { await this.dispatch(`⚠ Failed`, formatted); } - if (exit) process.exit(1); + if (exit || Config.runOnce) process.exit(1); } static queueError(error: NotificationInterface): void { diff --git a/src/sync.test.ts b/src/sync.test.ts index f9c196fc..4a361157 100644 --- a/src/sync.test.ts +++ b/src/sync.test.ts @@ -15,6 +15,7 @@ describe('entrypoint', () => { let notifyOfFailure: ReturnType; let notifyQueueError: ReturnType; let notifyOfSuccess: ReturnType; + let processExit: ReturnType; let primaryHostClient: Client; let secondaryHostClient1: Client; let secondaryHostClient2: Client; @@ -59,6 +60,7 @@ describe('entrypoint', () => { secondaryTwoResult?: Promise; } = {}) => { jest.spyOn(Config, 'runOnce', 'get').mockReturnValue(true); + processExit = jest.spyOn(process, 'exit').mockReturnValue(undefined as never); primaryHost = jest .spyOn(Config, 'primaryHost', 'get') .mockReturnValue(primaryHostValue); @@ -110,6 +112,7 @@ describe('entrypoint', () => { expect(notifyOfSuccess).toHaveBeenCalledWith({ message: '2/2 hosts synced.' }); + expect(processExit).not.toHaveBeenCalled(); }); test('should perform sync and partially succeed', async () => { @@ -132,6 +135,7 @@ describe('entrypoint', () => { sendNotification: true, message: '1/2 hosts synced.' }); + expect(processExit).toHaveBeenCalledTimes(1); }); test('should perform sync and fail', async () => { @@ -161,6 +165,7 @@ describe('entrypoint', () => { expect(notifyOfFailure).toHaveBeenCalledWith({ message: '0/2 hosts synced.' }); + expect(processExit).toHaveBeenCalledTimes(1); }); test('should perform sync and fail', async () => { @@ -182,6 +187,7 @@ describe('entrypoint', () => { ); expect(secondaryHostClient1.uploadBackup).not.toHaveBeenCalled(); expect(secondaryHostClient2.uploadBackup).not.toHaveBeenCalled(); + expect(processExit).toHaveBeenCalledTimes(1); }); test('should wait if `runOnce` is false', async () => { @@ -204,5 +210,6 @@ describe('entrypoint', () => { expect(notifyOfSuccess).toHaveBeenCalledWith({ message: '2/2 hosts synced.' }); + expect(processExit).not.toHaveBeenCalled(); }); }); From c8e100440f259cdb2a6a62e3e1ba3915b3eed48b Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 18:50:38 -0700 Subject: [PATCH 5/7] Add tertiary Pi-hole to e2e test --- docker-compose.test.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index e48e61d9..5a1da534 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -18,6 +18,15 @@ services: interval: 10s timeout: 10s retries: 5 + tertiary: + image: pihole/pihole:latest + environment: + WEBPASSWORD: tertiary_password1 + healthcheck: + test: ['CMD', 'curl', '-f', 'http://tertiary/admin/'] + interval: 10s + timeout: 10s + retries: 5 orbital-sync: build: context: . @@ -26,9 +35,14 @@ services: condition: service_healthy secondary: condition: service_healthy + tertiary: + condition: service_healthy environment: PRIMARY_HOST_BASE_URL: 'http://primary' PRIMARY_HOST_PASSWORD: 'primary_password1' SECONDARY_HOST_1_BASE_URL: 'http://secondary' SECONDARY_HOST_1_PASSWORD: 'secondary_password1' + SECONDARY_HOST_2_BASE_URL: 'http://tertiary' + SECONDARY_HOST_2_PASSWORD: 'tertiary_password1' RUN_ONCE: 'true' + VERBOSE: 'true' From 9c6c4f492735236f712961e51070952307f716e3 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 19:15:57 -0700 Subject: [PATCH 6/7] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e25842..49c3288a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,16 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v1.2.2-beta.0](https://github.com/mattwebbio/orbital-sync/compare/v1.2.1...v1.2.2-beta.0) + +- Add gravity update [`3b4e4d9`](https://github.com/mattwebbio/orbital-sync/commit/3b4e4d9b540fc416ad1b067b44767538d1f9a485) +- Make client error wording consistent [`1c3be22`](https://github.com/mattwebbio/orbital-sync/commit/1c3be22dca01314c28014bf1bffcc5e6546d52ff) +- Stringify non-string logging [`e84e036`](https://github.com/mattwebbio/orbital-sync/commit/e84e03641c1508ec2764e828198a38deb3043484) + #### [v1.2.1](https://github.com/mattwebbio/orbital-sync/compare/v1.2.0...v1.2.1) +> 30 September 2022 + - Bump @typescript-eslint/parser from 5.36.2 to 5.38.1 [`#47`](https://github.com/mattwebbio/orbital-sync/pull/47) - Bump nodemailer from 6.7.8 to 6.8.0 [`#48`](https://github.com/mattwebbio/orbital-sync/pull/48) - Bump jest and @types/jest [`#49`](https://github.com/mattwebbio/orbital-sync/pull/49) @@ -16,6 +24,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Bump node-html-parser from 6.0.0 to 6.1.1 [`#45`](https://github.com/mattwebbio/orbital-sync/pull/45) - Bump @typescript-eslint/eslint-plugin from 5.36.2 to 5.38.1 [`#43`](https://github.com/mattwebbio/orbital-sync/pull/43) - Bump @honeybadger-io/js from 4.1.3 to 4.3.1 [`#44`](https://github.com/mattwebbio/orbital-sync/pull/44) +- Update CHANGELOG.md [`5c2542a`](https://github.com/mattwebbio/orbital-sync/commit/5c2542a6b19e7cca8c20cf6240dc0903c6685390) #### [v1.2.0](https://github.com/mattwebbio/orbital-sync/compare/v1.1.3-beta.2...v1.2.0) From 241c52d08baec3273222aae5ee32ea2870011cb7 Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Fri, 30 Sep 2022 19:15:58 -0700 Subject: [PATCH 7/7] 1.2.2-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d325df6..debcb184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orbital-sync", - "version": "1.2.1", + "version": "1.2.2-beta.0", "type": "module", "main": "dist/index.js", "license": "MIT",