Skip to content

Commit

Permalink
Merge branch 'gristlabs:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
atropos112 authored Jul 7, 2024
2 parents 344fcec + 786ba6b commit 5ad4dd3
Show file tree
Hide file tree
Showing 78 changed files with 690 additions and 270 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/docker_latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ jobs:

- name: Build Node.js code
run: |
pushd ext && \
{ if [ -e package.json ] ; then yarn install --frozen-lockfile --modules-folder=../../node_modules; fi } && \
popd
rm -rf ext
yarn run build:prod
- name: Run tests
run: TEST_IMAGE=${{ github.repository_owner }}/${{ matrix.image.name }}:experimental VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:docker

- name: Restore the ext/ directory
if: matrix.image.name != 'grist-oss'
run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }}

- name: Log in to Docker Hub
uses: docker/login-action@v1
with:
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/fly-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# fly-deploy will be triggered on completion of this workflow to actually deploy the code to fly.io.

name: fly.io Build
on:
pull_request:
branches: [ main ]
types: [labeled, opened, synchronize, reopened]

# Allows running this workflow manually from the Actions tab
workflow_dispatch:

jobs:
build:
name: Build Docker image
runs-on: ubuntu-latest
# Build when the 'preview' label is added, or when PR is updated with this label present.
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'preview'))
steps:
- uses: actions/checkout@v4
- name: Build and export Docker image
id: docker-build
run: >
docker build -t grist-core:preview . &&
docker image save grist-core:preview -o grist-core.tar
- name: Save PR information
run: |
echo PR_NUMBER=${{ github.event.number }} >> ./pr-info.txt
echo PR_SOURCE=${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }} >> ./pr-info.txt
echo PR_SHASUM=${{ github.event.pull_request.head.sha }} >> ./pr-info.txt
# PR_SOURCE looks like <owner>/<repo>-<branch>.
# For example, if the GitHub user "foo" forked grist-core as "grist-bar", and makes a PR from their branch named "baz",
# it will be "foo/grist-bar-baz". deploy.js later replaces "/" with "-", making it "foo-grist-bar-baz".
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: |
./grist-core.tar
./pr-info.txt
if-no-files-found: "error"
20 changes: 10 additions & 10 deletions .github/workflows/fly-cleanup.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Fly Cleanup
name: fly.io Cleanup
on:
schedule:
# Once a day, clean up jobs marked as expired
Expand All @@ -12,12 +12,12 @@ env:

jobs:
clean:
name: Clean stale deployed apps
runs-on: ubuntu-latest
if: github.repository_owner == 'gristlabs'
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: 0.1.66
- run: node buildtools/fly-deploy.js clean
name: Clean stale deployed apps
runs-on: ubuntu-latest
if: github.repository_owner == 'gristlabs'
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: 0.2.72
- run: node buildtools/fly-deploy.js clean
70 changes: 70 additions & 0 deletions .github/workflows/fly-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Follow-up of fly-build, with access to secrets for making deployments.
# This workflow runs in the target repo context. It does not, and should never execute user-supplied code.
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

name: fly.io Deploy
on:
workflow_run:
workflows: ["fly.io Build"]
types:
- completed

jobs:
deploy:
name: Deploy app to fly.io
runs-on: ubuntu-latest
if: |
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v4
- name: Set up flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: 0.2.72
- name: Download artifacts
uses: actions/github-script@v7
with:
script: |
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{ github.event.workflow_run.id }},
});
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docker-image"
})[0];
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/docker-image.zip', Buffer.from(download.data));
- name: Extract artifacts
id: extract_artifacts
run: |
unzip docker-image.zip
cat ./pr-info.txt >> $GITHUB_OUTPUT
- name: Load Docker image
run: docker load --input grist-core.tar
- name: Deploy to fly.io
id: fly_deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
BRANCH_NAME: ${{ steps.extract_artifacts.outputs.PR_SOURCE }}
run: |
node buildtools/fly-deploy.js deploy
flyctl config -c ./fly.toml env | awk '/APP_HOME_URL/{print "DEPLOY_URL=" $2}' >> $GITHUB_OUTPUT
flyctl config -c ./fly.toml env | awk '/FLY_DEPLOY_EXPIRATION/{print "EXPIRES=" $2}' >> $GITHUB_OUTPUT
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: ${{ steps.extract_artifacts.outputs.PR_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `Deployed commit \`${{ steps.extract_artifacts.outputs.PR_SHASUM }}\` as ${{ steps.fly_deploy.outputs.DEPLOY_URL }} (until ${{ steps.fly_deploy.outputs.EXPIRES }})`
})
36 changes: 36 additions & 0 deletions .github/workflows/fly-destroy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This workflow runs in the target repo context, as it is triggered via pull_request_target.
# It does not, and should not have access to code in the PR.
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

name: fly.io Destroy
on:
pull_request_target:
branches: [ main ]
types: [unlabeled, closed]

# Allows running this workflow manually from the Actions tab
workflow_dispatch:

jobs:
destroy:
name: Remove app from fly.io
runs-on: ubuntu-latest
# Remove the deployment when 'preview' label is removed, or the PR is closed.
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request_target' &&
(github.event.action == 'closed' ||
(github.event.action == 'unlabeled' && github.event.label.name == 'preview')))
steps:
- uses: actions/checkout@v4
- name: Set up flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: 0.2.72
- name: Destroy fly.io app
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
BRANCH_NAME: ${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }}
# See fly-build for what BRANCH_NAME looks like.
id: fly_destroy
run: node buildtools/fly-deploy.js destroy
64 changes: 0 additions & 64 deletions .github/workflows/fly.yml

This file was deleted.

17 changes: 12 additions & 5 deletions app/client/ui/WebhookPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,24 @@ const WEBHOOK_COLUMNS = [
type: 'Text',
label: t('Status'),
},
{
id: VirtualId(),
colId: 'authorization',
type: 'Text',
label: t('Header Authorization'),
},
] as const;

/**
* Layout of fields in a view, with a specific ordering.
*/
const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [
'name', 'memo',
'eventTypes', 'url',
'tableId', 'isReadyColumn',
'watchedColIdsText', 'webhookId',
'enabled', 'status'
'eventTypes', 'tableId',
'watchedColIdsText', 'isReadyColumn',
'url', 'authorization',
'webhookId', 'enabled',
'status'
];

/**
Expand All @@ -136,7 +143,7 @@ class WebhookExternalTable implements IExternalTable {
public name = 'GristHidden_WebhookTable';
public initialActions = _prepareWebhookInitialActions(this.name);
public saveableFields = [
'tableId', 'watchedColIdsText', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn',
'tableId', 'watchedColIdsText', 'url', 'authorization', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn',
];
public webhooks: ObservableArray<UIWebhookSummary> = observableArray<UIWebhookSummary>([]);

Expand Down
4 changes: 4 additions & 0 deletions app/common/Triggers-ti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Webhook = t.iface([], {

export const WebhookFields = t.iface([], {
"url": "string",
"authorization": t.opt("string"),
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
"tableId": "string",
"watchedColIds": t.opt(t.array("string")),
Expand All @@ -29,6 +30,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret

export const WebhookSubscribe = t.iface([], {
"url": "string",
"authorization": t.opt("string"),
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
"watchedColIds": t.opt(t.array("string")),
"enabled": t.opt("boolean"),
Expand All @@ -45,6 +47,7 @@ export const WebhookSummary = t.iface([], {
"id": "string",
"fields": t.iface([], {
"url": "string",
"authorization": t.opt("string"),
"unsubscribeKey": "string",
"eventTypes": t.array("string"),
"isReadyColumn": t.union("string", "null"),
Expand All @@ -64,6 +67,7 @@ export const WebhookUpdate = t.iface([], {

export const WebhookPatch = t.iface([], {
"url": t.opt("string"),
"authorization": t.opt("string"),
"eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))),
"tableId": t.opt("string"),
"watchedColIds": t.opt(t.array("string")),
Expand Down
4 changes: 4 additions & 0 deletions app/common/Triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Webhook {

export interface WebhookFields {
url: string;
authorization?: string;
eventTypes: Array<"add"|"update">;
tableId: string;
watchedColIds?: string[];
Expand All @@ -26,6 +27,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv
// tableId from the url) but generics are not yet supported by ts-interface-builder
export interface WebhookSubscribe {
url: string;
authorization?: string;
eventTypes: Array<"add"|"update">;
watchedColIds?: string[];
enabled?: boolean;
Expand All @@ -42,6 +44,7 @@ export interface WebhookSummary {
id: string;
fields: {
url: string;
authorization?: string;
unsubscribeKey: string;
eventTypes: string[];
isReadyColumn: string|null;
Expand All @@ -64,6 +67,7 @@ export interface WebhookUpdate {
// ts-interface-builder
export interface WebhookPatch {
url?: string;
authorization?: string;
eventTypes?: Array<"add"|"update">;
tableId?: string;
watchedColIds?: string[];
Expand Down
27 changes: 27 additions & 0 deletions app/common/normalizedDateTimeString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import moment from 'moment-timezone';

/**
* Output an ISO8601 format datetime string, with timezone.
* Any string fed in without timezone is expected to be in UTC.
*
* When connected to postgres, dates will be extracted as Date objects,
* with timezone information. The normalization done here is not
* really needed in this case.
*
* Timestamps in SQLite are stored as UTC, and read as strings
* (without timezone information). The normalization here is
* pretty important in this case.
*/
export function normalizedDateTimeString(dateTime: any): string {
if (!dateTime) { return dateTime; }
if (dateTime instanceof Date) {
return moment(dateTime).toISOString();
}
if (typeof dateTime === 'string' || typeof dateTime === 'number') {
// When SQLite returns a string, it will be in UTC.
// Need to make sure it actually have timezone info in it
// (will not by default).
return moment.utc(dateTime).toISOString();
}
throw new Error(`normalizedDateTimeString cannot handle ${dateTime}`);
}
Loading

0 comments on commit 5ad4dd3

Please sign in to comment.