Skip to content

WIP: webhook demo #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ CodeRoad has a number of configurations:

- `CODEROAD_CONTENT_SECURITY_POLICY_EXEMPTIONS` - a list of CSP exemption hashes. For multiples, separate the list with a space.

- `CODEROAD_WEBHOOK_TOKEN` - an optional token for authenticating/authorizing webhook endpoints. Passed to the webhook endpoint in a `CodeRoad-User-Token` header.

## How to Use Variables

### Local
Expand Down
5 changes: 4 additions & 1 deletion src/actions/onRunReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ const onRunReset = async (action: ResetAction, context: Context): Promise<void>
// if tutorial.config.reset.command, run it
const resetActions = tutorial?.config?.reset
if (resetActions) {
hooks.onReset({ commands: resetActions?.commands, vscodeCommands: resetActions?.vscodeCommands })
hooks.onReset(
{ commands: resetActions?.commands, vscodeCommands: resetActions?.vscodeCommands },
tutorial?.id as string,
)
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/actions/onTutorialConfigContinue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Context from '../services/context/context'
import tutorialConfig from './utils/tutorialConfig'
import { COMMANDS, send } from '../commands'
import logger from '../services/logger'
import { setupWebhook } from '../services/hooks/webhooks'

const onTutorialConfigContinue = async (action: T.Action, context: Context): Promise<void> => {
logger('onTutorialConfigContinue', action)
Expand All @@ -19,6 +20,11 @@ const onTutorialConfigContinue = async (action: T.Action, context: Context): Pro
data: tutorialToContinue,
alreadyConfigured: true,
})

// configure webhook
if (tutorialToContinue.config?.webhook) {
setupWebhook(tutorialToContinue.config.webhook)
}
} catch (e) {
const error = {
type: 'UnknownError',
Expand Down
6 changes: 6 additions & 0 deletions src/actions/onTutorialConfigNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { version, compareVersions } from '../services/dependencies'
import Context from '../services/context/context'
import tutorialConfig from './utils/tutorialConfig'
import { send } from '../commands'
import { setupWebhook } from '../services/hooks/webhooks'

const onTutorialConfigNew = async (action: T.Action, context: Context): Promise<void> => {
try {
Expand Down Expand Up @@ -108,6 +109,11 @@ const onTutorialConfigNew = async (action: T.Action, context: Context): Promise<
return
}

// configure webhook
if (data.config?.webhook) {
setupWebhook(data.config.webhook)
}

// report back to the webview that setup is complete
send({ type: 'TUTORIAL_CONFIGURED' })
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
if (!alreadyConfigured) {
const setupActions = data.config.setup
if (setupActions) {
hooks.onInit(setupActions)
hooks.onInit(setupActions, data.id)
}
}
testRunner = createTestRunner(data, {
Expand Down
3 changes: 3 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ export const DISABLE_RUN_ON_SAVE = (process.env.CODEROAD_DISABLE_RUN_ON_SAVE ||
// for multiple exemptions, separate each with a space "a1 b1"
export const CONTENT_SECURITY_POLICY_EXEMPTIONS: string | null =
process.env.CODEROAD_CONTENT_SECURITY_POLICY_EXEMPTIONS || null

// optional token for authorization/authentication of webhook calls
export const WEBHOOK_TOKEN = process.env.CODEROAD_WEBHOOK_TOKEN || null
25 changes: 22 additions & 3 deletions src/services/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import runCommands from './utils/runCommands'
import runVSCodeCommands from './utils/runVSCodeCommands'
import * as telemetry from '../telemetry'
import { runTest } from '../../actions/onTest'
import logger from '../logger'
import { VERSION } from '../../environment'
import * as webhooks from './webhooks'

// run at the end of when a tutorial is configured
export const onInit = async (actions: TT.StepActions): Promise<void> => {
export const onInit = async (actions: TT.StepActions, tutorialId: string): Promise<void> => {
await loadCommits(actions?.commits)
await runCommands(actions?.commands)
await runVSCodeCommands(actions?.vscodeCommands)
webhooks.onInit({
tutorialId,
coderoadVersion: VERSION,
})
}

// run when a level starts
Expand Down Expand Up @@ -43,10 +47,13 @@ export const onSolutionEnter = async (actions: TT.StepActions): Promise<void> =>
}

// run when "reset" is triggered
export const onReset = async (actions: TT.StepActions): Promise<void> => {
export const onReset = async (actions: TT.StepActions, tutorialId: string): Promise<void> => {
await resetWatchers()
await runCommands(actions?.commands)
await runVSCodeCommands(actions?.vscodeCommands)
webhooks.onReset({
tutorialId,
})
}

// run when an uncaught exception is thrown
Expand All @@ -66,6 +73,11 @@ export const onStepComplete = async ({
}): Promise<void> => {
git.saveCommit(`Save progress: ${stepId}`)
telemetry.onEvent('step_complete', { tutorialId, stepId, levelId, version: VERSION })
webhooks.onStepComplete({
tutorialId,
levelId,
stepId,
})
}

// run when a level is complete (all tasks pass or no tasks)
Expand All @@ -77,9 +89,16 @@ export const onLevelComplete = async ({
levelId: string
}): Promise<void> => {
telemetry.onEvent('level_complete', { tutorialId, levelId, version: VERSION })
webhooks.onLevelComplete({
tutorialId,
levelId,
})
}

// run when all levels are complete
export const onTutorialComplete = async ({ tutorialId }: { tutorialId: string }): Promise<void> => {
telemetry.onEvent('tutorial_complete', { tutorialId, version: VERSION })
webhooks.onTutorialComplete({
tutorialId,
})
}
101 changes: 101 additions & 0 deletions src/services/hooks/webhooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as TT from 'typings/tutorial'
import fetch from 'node-fetch'
import logger from '../logger'
import { WEBHOOK_TOKEN } from '../../environment'

const WEBHOOK_EVENTS = {
init: false,
reset: false,
step_complete: false,
level_complete: false,
tutorial_complete: false,
}

// varaibles set on init
let WEBHOOK_URI: string | undefined

export const setupWebhook = (webhookConfig: TT.WebhookConfig) => {
if (!webhookConfig.url) {
return
}
// set webhook uri
WEBHOOK_URI = webhookConfig.url

// set webhook event triggers
const events = webhookConfig.events as TT.WebhookConfigEvents
for (const eventName of Object.keys(events || {})) {
WEBHOOK_EVENTS[eventName] = events[eventName]
}
}

const callWebhookEndpoint = async <B>(bodyObject: B): Promise<void> => {
if (!WEBHOOK_URI) {
return
}

const headers = { 'Content-Type': 'application/json' }
// if the webhook token is specified as env var, sends a token with the request
if (WEBHOOK_TOKEN) {
headers['CodeRoad-User-Token'] = WEBHOOK_TOKEN
}

const body = JSON.stringify(bodyObject)

try {
const sendEvent = await fetch(WEBHOOK_URI, {
method: 'POST',
headers,
body,
})
if (!sendEvent.ok) {
throw new Error('Error sending event')
}
} catch (err: unknown) {
logger(`Failed to call webhook endpoint ${WEBHOOK_URI} with body ${body}`)
}
}

type WebhookEventInit = {
tutorialId: string
coderoadVersion: string
}

export const onInit = (event: WebhookEventInit): void => {
if (WEBHOOK_EVENTS.init) {
callWebhookEndpoint<WebhookEventInit>(event)
}
}

type WebhookEventReset = {
tutorialId: string
}

export const onReset = (event: WebhookEventReset): void => {
if (WEBHOOK_EVENTS.reset) {
callWebhookEndpoint<WebhookEventReset>(event)
}
}

type WebhookEventStepComplete = { tutorialId: string; version: string; levelId: string; stepId: string }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type WebhookEventStepComplete = { tutorialId: string; version: string; levelId: string; stepId: string }
type WebhookEventStepComplete = { tutorialId: string; version?: string; levelId: string; stepId: string }


export const onStepComplete = (event: WebhookEventStepComplete): void => {
if (WEBHOOK_EVENTS.step_complete) {
callWebhookEndpoint<WebhookEventStepComplete>(event)
}
}

type WebhookEventLevelComplete = { tutorialId: string; version: string; levelId: string }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type WebhookEventLevelComplete = { tutorialId: string; version: string; levelId: string }
type WebhookEventLevelComplete = { tutorialId: string; version?: string; levelId: string }


export const onLevelComplete = (event: WebhookEventLevelComplete): void => {
if (WEBHOOK_EVENTS.level_complete) {
callWebhookEndpoint<WebhookEventLevelComplete>(event)
}
}

type WebhookEevntTutorialComplete = { tutorialId: string; version: string }

export const onTutorialComplete = (event: WebhookEevntTutorialComplete): void => {
if (WEBHOOK_EVENTS.tutorial_complete) {
callWebhookEndpoint<WebhookEevntTutorialComplete>(event)
}
}
Comment on lines +95 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type WebhookEevntTutorialComplete = { tutorialId: string; version: string }
export const onTutorialComplete = (event: WebhookEevntTutorialComplete): void => {
if (WEBHOOK_EVENTS.tutorial_complete) {
callWebhookEndpoint<WebhookEevntTutorialComplete>(event)
}
}
type WebhookEventTutorialComplete = { tutorialId: string; version?: string }
export const onTutorialComplete = (event: WebhookEventTutorialComplete): void => {
if (WEBHOOK_EVENTS.tutorial_complete) {
callWebhookEndpoint<WebhookEventTutorialComplete>(event)
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll merge for now with the note that it may require more of a guarantee.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure I was just having issues running the extension without the ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The published version has the version?. I added it in a separate PR.

@moT01 can you post an issue if you have any more details?

13 changes: 13 additions & 0 deletions typings/tutorial.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type TutorialConfig = {
dependencies?: TutorialDependency[]
setup?: StepActions
reset?: ConfigReset
webhook?: WebhookConfig
}

/** Logical groupings of tasks */
Expand Down Expand Up @@ -92,3 +93,15 @@ export interface TutorialAppVersions {
}

export type VSCodeCommand = string | [string, any]

export interface WebhookConfigEvents {
init?: boolean
reset?: boolean
step_complete?: boolean
level_complete?: boolean
tutorial_complete?: boolean
}
export interface WebhookConfig {
url: string
events?: WebhookConfigEvents
}