chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Include any files or directories that you don't want to be copied to your
|
||||||
|
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||||
|
#
|
||||||
|
# For more help, visit the .dockerignore file reference guide at
|
||||||
|
# https://docs.docker.com/go/build-context-dockerignore/
|
||||||
|
|
||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.next
|
||||||
|
**/.cache
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/compose.y*ml
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
.oxlintrc.json linguist-language=jsonc gitlab-language=jsonc
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Botpress V12
|
||||||
|
url: https://github.com/botpress/v12
|
||||||
|
about: If you're experiencing issues when using the on premise version of Botpress, please report the issue on the v12 repo.
|
||||||
|
- name: Botpress Cloud
|
||||||
|
url: https://discord.gg/botpress
|
||||||
|
about: To get assistance as fast as possible, please reach us on our Discord server.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Botpress Cloud
|
||||||
|
description: File a bug report or feature request for Botpress Cloud
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Thank you for taking the time to fill out this issue report.\n\nKeep in mind that you will get a faster response time by talking to us on [our Discord Server](https://discord.gg/botpress)."
|
||||||
|
- type: checkboxes
|
||||||
|
id: dx-related-confirmation
|
||||||
|
attributes:
|
||||||
|
label: 'Make sure the issue is related to Botpress Cloud.'
|
||||||
|
description: 'When in doubt, please reach out on [our Discord Server](https://discord.gg/botpress).'
|
||||||
|
options:
|
||||||
|
- label: 'I confirm that the reported bug or feature request is not related to Botpress v12 and below (on premise version)'
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: report-id
|
||||||
|
attributes:
|
||||||
|
label: 'Report ID'
|
||||||
|
description: 'Directly in the Studio, click on the "Report a Problem" button in the bottom left corner to create a report. You can then copy the report ID. Providing a report ID will allow us to help you faster. This information is safe to share publicly.'
|
||||||
|
placeholder: 'e.g. report_0123456789ABCDEFGHIJKLMNOPQRS'
|
||||||
|
- type: textarea
|
||||||
|
id: issue-description
|
||||||
|
attributes:
|
||||||
|
label: 'Description of the bug or feature request'
|
||||||
|
description: 'What is your bug or feature request ? Make sure to add logs, context or any information that you think might help understand and solve the issue.'
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: 'Create or Update Secret'
|
||||||
|
description: 'Creates or updates a secret in the repository'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
secret_name:
|
||||||
|
description: 'Secret name'
|
||||||
|
required: true
|
||||||
|
secret_value:
|
||||||
|
description: 'Secret value'
|
||||||
|
required: true
|
||||||
|
sync_secret_app_id:
|
||||||
|
description: 'Sync secret app id'
|
||||||
|
required: true
|
||||||
|
sync_secret_app_private_key:
|
||||||
|
description: 'Sync secret app private key'
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@v2
|
||||||
|
with:
|
||||||
|
app-id: ${{ inputs.sync_secret_app_id }}
|
||||||
|
private-key: ${{ inputs.sync_secret_app_private_key }}
|
||||||
|
- name: Set secret
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
|
SECRET_NAME: ${{ inputs.secret_name }}
|
||||||
|
SECRET_VALUE: ${{ inputs.secret_value }}
|
||||||
|
run: |
|
||||||
|
gh secret set "$SECRET_NAME" --body "$SECRET_VALUE"
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
name: Deploy Integrations
|
||||||
|
description: Deploys integrations
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
extra_filter:
|
||||||
|
type: string
|
||||||
|
description: 'Pnpm additional filters to select integrations to deploy'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
force:
|
||||||
|
type: boolean
|
||||||
|
description: 'Force re-deploying integrations'
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
description: 'Ask the backend to perform validation without actually deploying'
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
token_cloud_ops_account:
|
||||||
|
description: 'Cloud Ops account token'
|
||||||
|
required: true
|
||||||
|
cloud_ops_workspace_id:
|
||||||
|
description: 'Cloud Ops workspace id'
|
||||||
|
required: true
|
||||||
|
shopify_admin_automation_token:
|
||||||
|
description: 'Shopify app automation token for the Shopify Admin app (app-scoped, env-specific)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
shopify_storefront_automation_token:
|
||||||
|
description: 'Shopify app automation token for the Shopify Storefront app (app-scoped, env-specific)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Deploys Integrations
|
||||||
|
env:
|
||||||
|
INPUT_ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
INPUT_FORCE: ${{ inputs.force }}
|
||||||
|
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||||
|
INPUT_EXTRA_FILTER: ${{ inputs.extra_filter }}
|
||||||
|
TOKEN_CLOUD_OPS_ACCOUNT: ${{ inputs.token_cloud_ops_account }}
|
||||||
|
CLOUD_OPS_WORKSPACE_ID: ${{ inputs.cloud_ops_workspace_id }}
|
||||||
|
SHOPIFY_ADMIN_AUTOMATION_TOKEN: ${{ inputs.shopify_admin_automation_token }}
|
||||||
|
SHOPIFY_STOREFRONT_AUTOMATION_TOKEN: ${{ inputs.shopify_storefront_automation_token }}
|
||||||
|
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "$INPUT_ENVIRONMENT" = "staging" ]; then
|
||||||
|
api_url="https://api.botpress.dev"
|
||||||
|
else
|
||||||
|
api_url="https://api.botpress.cloud"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# login
|
||||||
|
|
||||||
|
echo "### Logging in to $api_url ###"
|
||||||
|
pnpm bp login -y --api-url "$api_url" --workspaceId "$CLOUD_OPS_WORKSPACE_ID" --token "$TOKEN_CLOUD_OPS_ACCOUNT"
|
||||||
|
|
||||||
|
# deploy
|
||||||
|
|
||||||
|
if [ "$INPUT_FORCE" = "true" ]; then
|
||||||
|
redeploy=1
|
||||||
|
else
|
||||||
|
redeploy=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$INPUT_DRY_RUN" = "true" ]; then
|
||||||
|
dryrun="--dryRun"
|
||||||
|
is_dry_run=1
|
||||||
|
else
|
||||||
|
dryrun=""
|
||||||
|
is_dry_run=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
all_filters="-F '{integrations/*}' $INPUT_EXTRA_FILTER"
|
||||||
|
list_integrations_cmd="pnpm list $all_filters --json"
|
||||||
|
integration_paths=$(eval "$list_integrations_cmd" | jq -r 'map(.path) | .[]')
|
||||||
|
|
||||||
|
for integration_path in $integration_paths; do
|
||||||
|
integration=$(basename "$integration_path")
|
||||||
|
exists=$(./.github/scripts/integration-exists.sh "$integration")
|
||||||
|
|
||||||
|
base_command="bp deploy -v -y --noBuild --visibility public --allowDeprecated $dryrun"
|
||||||
|
|
||||||
|
integration_deployed=false
|
||||||
|
if [ "$exists" -eq 0 ]; then
|
||||||
|
echo -e "\nDeploying integration: ### $integration ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{integrations/$integration}" -c exec -- "$base_command"
|
||||||
|
integration_deployed=true
|
||||||
|
elif [ "$redeploy" -eq 1 ]; then
|
||||||
|
echo -e "\nRe-deploying integration: ### $integration ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{integrations/$integration}" -c exec -- "$base_command"
|
||||||
|
integration_deployed=true
|
||||||
|
else
|
||||||
|
echo -e "\nSkipping integration: ### $integration ###\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# upload sandbox scripts
|
||||||
|
integration_implements_sandbox=$(./.github/scripts/integration-implements-sandbox.sh "$integration")
|
||||||
|
if [ "$integration_implements_sandbox" = "true" ] && [ "$integration_deployed" = "true" ] && [ "$is_dry_run" -eq 0 ]; then
|
||||||
|
echo -e "\nUploading integration sandbox scripts\n"
|
||||||
|
base_upload_command="uploadSandboxScripts --apiUrl=$api_url --workspaceId=$CLOUD_OPS_WORKSPACE_ID --token=$TOKEN_CLOUD_OPS_ACCOUNT --userEmail=cloud-ops@botpress.com"
|
||||||
|
# shellcheck disable=SC2086 # base_upload_command contains multiple space-separated arguments
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{integrations/$integration}" run -- $base_upload_command
|
||||||
|
fi
|
||||||
|
|
||||||
|
# deploy shopify app manifest (config-as-code) for the current environment
|
||||||
|
manifest_file="$integration_path/shopify.app.$INPUT_ENVIRONMENT.toml"
|
||||||
|
if [ "$integration_deployed" = "true" ] && [ "$is_dry_run" -eq 0 ] && [ -f "$manifest_file" ]; then
|
||||||
|
case "$integration" in
|
||||||
|
shopify-admin) shopify_token="$SHOPIFY_ADMIN_AUTOMATION_TOKEN" ;;
|
||||||
|
shopify-storefront) shopify_token="$SHOPIFY_STOREFRONT_AUTOMATION_TOKEN" ;;
|
||||||
|
*) shopify_token="" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$shopify_token" ]; then
|
||||||
|
echo "::warning::Found $manifest_file but no Shopify automation token for $integration - skipping manifest deploy"
|
||||||
|
else
|
||||||
|
echo -e "\nDeploying Shopify app manifest ($INPUT_ENVIRONMENT): ### $integration ###\n"
|
||||||
|
shopify_deploy_command="pnpm shopify app deploy --config $INPUT_ENVIRONMENT --allow-updates --source-control-url $COMMIT_URL"
|
||||||
|
SHOPIFY_APP_AUTOMATION_TOKEN="$shopify_token" \
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{integrations/$integration}" -c exec -- "$shopify_deploy_command"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Deploy Interfaces
|
||||||
|
description: Deploys interfaces
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
extra_filter:
|
||||||
|
type: string
|
||||||
|
description: 'Pnpm additional filters to select interfaces to deploy'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
force:
|
||||||
|
type: boolean
|
||||||
|
description: 'Force re-deploying interfaces'
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
token_cloud_ops_account:
|
||||||
|
description: 'Cloud Ops account token'
|
||||||
|
required: true
|
||||||
|
cloud_ops_workspace_id:
|
||||||
|
description: 'Cloud Ops workspace id'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Deploys Interfaces
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
INPUT_FORCE: ${{ inputs.force }}
|
||||||
|
INPUT_EXTRA_FILTER: ${{ inputs.extra_filter }}
|
||||||
|
TOKEN_CLOUD_OPS_ACCOUNT: ${{ inputs.token_cloud_ops_account }}
|
||||||
|
CLOUD_OPS_WORKSPACE_ID: ${{ inputs.cloud_ops_workspace_id }}
|
||||||
|
run: |
|
||||||
|
if [ "$INPUT_ENVIRONMENT" = "staging" ]; then
|
||||||
|
api_url="https://api.botpress.dev"
|
||||||
|
else
|
||||||
|
api_url="https://api.botpress.cloud"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# login
|
||||||
|
|
||||||
|
echo "### Logging in to $api_url ###"
|
||||||
|
pnpm bp login -y --api-url "$api_url" --workspaceId "$CLOUD_OPS_WORKSPACE_ID" --token "$TOKEN_CLOUD_OPS_ACCOUNT"
|
||||||
|
|
||||||
|
# deploy
|
||||||
|
|
||||||
|
if [ "$INPUT_FORCE" = "true" ]; then
|
||||||
|
redeploy=1
|
||||||
|
else
|
||||||
|
redeploy=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
all_filters="-F '{interfaces/*}' $INPUT_EXTRA_FILTER"
|
||||||
|
list_interfaces_cmd="pnpm list $all_filters --json"
|
||||||
|
interface_paths=$(eval "$list_interfaces_cmd" | jq -r 'map(.path) | .[]')
|
||||||
|
|
||||||
|
for interface_path in $interface_paths; do
|
||||||
|
interface=$(basename "$interface_path")
|
||||||
|
exists=$(./.github/scripts/interface-exists.sh "$interface")
|
||||||
|
|
||||||
|
base_command="bp deploy -v -y --visibility public"
|
||||||
|
if [ "$exists" -eq 0 ]; then
|
||||||
|
echo -e "\nDeploying interface: ### $interface ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{interfaces/$interface}" -c exec -- "$base_command"
|
||||||
|
elif [ "$redeploy" -eq 1 ]; then
|
||||||
|
echo -e "\nRe-deploying interface: ### $interface ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{interfaces/$interface}" -c exec -- "$base_command"
|
||||||
|
else
|
||||||
|
echo -e "\nSkipping interface: ### $interface ###\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Deploy Plugins
|
||||||
|
description: Deploys plugins
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
environment:
|
||||||
|
type: choice
|
||||||
|
description: 'Environment to deploy to'
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
extra_filter:
|
||||||
|
type: string
|
||||||
|
description: 'Pnpm additional filters to select plugins to deploy'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
force:
|
||||||
|
type: boolean
|
||||||
|
description: 'Force re-deploying plugins'
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
token_cloud_ops_account:
|
||||||
|
description: 'Cloud Ops account token'
|
||||||
|
required: true
|
||||||
|
cloud_ops_workspace_id:
|
||||||
|
description: 'Cloud Ops workspace id'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Deploys Plugins
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
INPUT_ENVIRONMENT: ${{ inputs.environment }}
|
||||||
|
INPUT_FORCE: ${{ inputs.force }}
|
||||||
|
INPUT_EXTRA_FILTER: ${{ inputs.extra_filter }}
|
||||||
|
TOKEN_CLOUD_OPS_ACCOUNT: ${{ inputs.token_cloud_ops_account }}
|
||||||
|
CLOUD_OPS_WORKSPACE_ID: ${{ inputs.cloud_ops_workspace_id }}
|
||||||
|
run: |
|
||||||
|
if [ "$INPUT_ENVIRONMENT" = "staging" ]; then
|
||||||
|
api_url="https://api.botpress.dev"
|
||||||
|
else
|
||||||
|
api_url="https://api.botpress.cloud"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# login
|
||||||
|
|
||||||
|
echo "### Logging in to $api_url ###"
|
||||||
|
pnpm bp login -y --api-url "$api_url" --workspaceId "$CLOUD_OPS_WORKSPACE_ID" --token "$TOKEN_CLOUD_OPS_ACCOUNT"
|
||||||
|
|
||||||
|
# deploy
|
||||||
|
|
||||||
|
if [ "$INPUT_FORCE" = "true" ]; then
|
||||||
|
redeploy=1
|
||||||
|
else
|
||||||
|
redeploy=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
all_filters="-F '{plugins/*}' $INPUT_EXTRA_FILTER"
|
||||||
|
list_plugins_cmd="pnpm list $all_filters --json"
|
||||||
|
plugin_paths=$(eval "$list_plugins_cmd" | jq -r 'map(.path) | .[]')
|
||||||
|
|
||||||
|
for plugin_path in $plugin_paths; do
|
||||||
|
plugin=$(basename "$plugin_path")
|
||||||
|
exists=$(./.github/scripts/plugin-exists.sh "$plugin")
|
||||||
|
|
||||||
|
base_command="bp deploy -v -y --visibility public"
|
||||||
|
|
||||||
|
if [ "$exists" -eq 0 ]; then
|
||||||
|
echo -e "\nDeploying plugin: ### $plugin ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{plugins/$plugin}" -c exec -- "$base_command"
|
||||||
|
elif [ "$redeploy" -eq 1 ]; then
|
||||||
|
echo -e "\nRe-deploying plugin: ### $plugin ###\n"
|
||||||
|
pnpm retry -n 2 -- pnpm -F "{plugins/$plugin}" -c exec -- "$base_command"
|
||||||
|
else
|
||||||
|
echo -e "\nSkipping plugin: ### $plugin ###\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: Docker Build
|
||||||
|
description: 'Build and push a Docker image to the registry'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
repository:
|
||||||
|
description: 'The name of the repository to build'
|
||||||
|
required: true
|
||||||
|
dockerfile:
|
||||||
|
description: 'The name of the Dockerfile to build'
|
||||||
|
required: true
|
||||||
|
push:
|
||||||
|
description: 'Whether to push the image to the registry'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
minify:
|
||||||
|
description: 'Whether to minify the build'
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
tag:
|
||||||
|
description: 'Optional Docker tag'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
depot-project:
|
||||||
|
description: 'Depot project ID (required if no depot.json; get it from depot.dev)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- uses: aws-actions/configure-aws-credentials@v3
|
||||||
|
with:
|
||||||
|
role-session-name: container_pusher
|
||||||
|
role-to-assume: arn:aws:iam::986677156374:role/actions/build/container_pusher
|
||||||
|
aws-region: us-east-1
|
||||||
|
|
||||||
|
- uses: aws-actions/amazon-ecr-login@v1
|
||||||
|
id: ecr
|
||||||
|
with:
|
||||||
|
mask-password: true
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v4
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ${{ steps.ecr.outputs.registry }}/${{ inputs.repository }}
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=raw,enable=${{ inputs.tag != '' }},value=${{ inputs.tag }}
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=sha,enable=${{ !startsWith(github.ref, 'refs/tags') }},prefix=,format=long
|
||||||
|
|
||||||
|
- name: Set BUILD_DATE
|
||||||
|
id: meta_date
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export TZ=America/Toronto
|
||||||
|
echo "timestamp=$(date +"%Y-%m-%d %H:%M:%S")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Create ECR Registry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
ECR_REPOSITORY: ${{ inputs.repository }}
|
||||||
|
ECR_REGISTRY: ${{ steps.ecr.outputs.registry }}
|
||||||
|
run: |
|
||||||
|
aws --version
|
||||||
|
aws ecr create-repository --repository-name "$ECR_REPOSITORY" || true
|
||||||
|
aws ssm get-parameter --name '/cloud/container-registry/ecr-policy-document' --query 'Parameter.Value' | jq -r > repository-policy.json
|
||||||
|
aws ecr set-repository-policy --repository-name "$ECR_REPOSITORY" --policy-text file://repository-policy.json &> /dev/null
|
||||||
|
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
|
||||||
|
- uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: ${{ inputs.depot-project }}
|
||||||
|
build-args: |
|
||||||
|
MINIFY=${{ inputs.minify }}
|
||||||
|
DOCKER_TAG=${{ inputs.tag }}
|
||||||
|
BUILD_DATE=${{ steps.meta_date.outputs.timestamp }}
|
||||||
|
file: ${{ inputs.dockerfile }}
|
||||||
|
context: .
|
||||||
|
push: ${{ inputs.push }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: 'Refresh Instagram Access Tokens'
|
||||||
|
description: 'Refreshes Instagram access token, updates the integrations secrets and update the token GitHub secrets'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
token_cloud_ops_account:
|
||||||
|
description: 'Cloud Ops account token'
|
||||||
|
required: true
|
||||||
|
cloud_ops_workspace_id:
|
||||||
|
description: 'Cloud Ops workspace id'
|
||||||
|
required: true
|
||||||
|
api_url:
|
||||||
|
description: 'API URL'
|
||||||
|
required: true
|
||||||
|
current_token:
|
||||||
|
description: 'Current token'
|
||||||
|
required: true
|
||||||
|
secret_name:
|
||||||
|
description: 'Secret name'
|
||||||
|
required: true
|
||||||
|
sync_secret_app_id:
|
||||||
|
description: 'Create or update secret app id'
|
||||||
|
required: true
|
||||||
|
sync_secret_app_private_key:
|
||||||
|
description: 'Create or update secret app private key'
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Refresh Access Tokens
|
||||||
|
id: refresh_tokens
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BP_API_URL: ${{ inputs.api_url }}
|
||||||
|
BP_WORKSPACE_ID: ${{ inputs.cloud_ops_workspace_id }}
|
||||||
|
BP_TOKEN: ${{ inputs.token_cloud_ops_account }}
|
||||||
|
run: |
|
||||||
|
refresh_result=$(pnpm -F 'instagram' exec -- pnpm run --silent refreshTokens --json --refreshToken "${{ inputs.current_token }}")
|
||||||
|
new_token=$(echo "$refresh_result" | jq -r '.refreshedToken')
|
||||||
|
if [ -z "$new_token" ]; then
|
||||||
|
echo "❌ Error: Failed to refresh token" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$refresh_result" | jq -r '.message'
|
||||||
|
echo "✅ Tokens refreshed successfully"
|
||||||
|
echo "::add-mask::$new_token"
|
||||||
|
echo "new_token=$new_token" >> "$GITHUB_OUTPUT"
|
||||||
|
- uses: ./.github/actions/create-or-update-secret
|
||||||
|
with:
|
||||||
|
secret_name: ${{ inputs.secret_name }}
|
||||||
|
secret_value: ${{ steps.refresh_tokens.outputs.new_token }}
|
||||||
|
sync_secret_app_id: ${{ inputs.sync_secret_app_id }}
|
||||||
|
sync_secret_app_private_key: ${{ inputs.sync_secret_app_private_key }}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Setup
|
||||||
|
description: install dependencies and build
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
extra_filters:
|
||||||
|
description: 'Turbo additional filters to select packages to build'
|
||||||
|
required: false
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- uses: pnpm/action-setup@v4 # uses the version from package.json
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: '.tool-versions'
|
||||||
|
package-manager-cache: false
|
||||||
|
|
||||||
|
- name: Install Node Gyp Build
|
||||||
|
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||||
|
run: |
|
||||||
|
npm install -g node-gyp-build
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Cache Turbo
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.turbo
|
||||||
|
node_modules/.cache/turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ github.workflow_ref }}
|
||||||
|
restore-keys: |
|
||||||
|
turbo-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||||
|
env:
|
||||||
|
BP_VERBOSE: 'true'
|
||||||
|
EXTRA_FILTERS: ${{ inputs.extra_filters }}
|
||||||
|
run: |
|
||||||
|
# shellcheck disable=SC2086 # EXTRA_FILTERS contains multiple space-separated filter arguments
|
||||||
|
pnpm turbo run build $EXTRA_FILTERS
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Update Linear Status
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
linearApiKey:
|
||||||
|
required: true
|
||||||
|
teamName:
|
||||||
|
required: true
|
||||||
|
targetLabel:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
shell: bash
|
||||||
|
run: pnpm install tsx -w --save-dev
|
||||||
|
|
||||||
|
- name: Update issues
|
||||||
|
shell: bash
|
||||||
|
run: npx tsx ./.github/scripts/update-linear.ts
|
||||||
|
env:
|
||||||
|
LINEAR_API_KEY: ${{inputs.linearApiKey}}
|
||||||
|
TEAM_NAME: ${{inputs.teamName}}
|
||||||
|
TARGET_LABEL: ${{inputs.targetLabel}}
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: integration name is not provided" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
integration=$1
|
||||||
|
|
||||||
|
integration_path="integrations/$integration"
|
||||||
|
if ! integration_def=$(pnpm bp read --work-dir "$integration_path" --json); then
|
||||||
|
echo "Error: Failed to read integration definition for \"$integration\". Check the integration for TypeScript errors." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
name=$(echo "$integration_def" | jq -r ".name")
|
||||||
|
version=$(echo "$integration_def" | jq -r ".version")
|
||||||
|
|
||||||
|
exists=$(pnpm bp integrations ls --name "$name" --version-number "$version" --json | jq '[ .[] | select(.public) ] | length') # 0 if not exists
|
||||||
|
echo "$exists"
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: integration name is not provided" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
integration=$1
|
||||||
|
|
||||||
|
integration_path="integrations/$integration"
|
||||||
|
if ! integration_def=$(pnpm bp read --work-dir "$integration_path" --json); then
|
||||||
|
echo "Error: Failed to read integration definition for \"$integration\". Check the integration for TypeScript errors." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
category=$(echo "$integration_def" | jq -r '.attributes.category // empty')
|
||||||
|
|
||||||
|
valid_categories=(
|
||||||
|
"AI Models"
|
||||||
|
"Business Operations"
|
||||||
|
"CRM & Sales"
|
||||||
|
"Communication & Channels"
|
||||||
|
"Customer Support"
|
||||||
|
"Developer Tools"
|
||||||
|
"E-commerce & Payments"
|
||||||
|
"File Management"
|
||||||
|
"Marketing & Email"
|
||||||
|
"Other"
|
||||||
|
"Project Management"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$category" ]; then
|
||||||
|
echo "Integration \"$integration\" is missing the \"category\" attribute in its definition file" >&2
|
||||||
|
echo "Valid categories are: $(printf '"%s", ' "${valid_categories[@]}" | sed 's/, $//')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for valid_category in "${valid_categories[@]}"; do
|
||||||
|
if [ "$category" = "$valid_category" ]; then
|
||||||
|
echo "Integration \"$integration\" has a valid category: \"$category\""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Integration \"$integration\" has an invalid category: \"$category\"" >&2
|
||||||
|
echo "Valid categories are: $(printf '"%s", ' "${valid_categories[@]}" | sed 's/, $//')" >&2
|
||||||
|
exit 1
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: integration name is not provided" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
integration=$1
|
||||||
|
integration_path="integrations/$integration"
|
||||||
|
|
||||||
|
has_sandbox=false
|
||||||
|
sandbox_script="$integration_path/sandboxIdentifierExtract.vrl"
|
||||||
|
if [ -f "$sandbox_script" ]; then
|
||||||
|
has_sandbox=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$has_sandbox"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: interface name is not provided" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
interface=$1
|
||||||
|
|
||||||
|
interface_path="interfaces/$interface"
|
||||||
|
interface_def=$(pnpm bp read --work-dir "$interface_path" --json)
|
||||||
|
|
||||||
|
name=$(echo "$interface_def" | jq -r ".name")
|
||||||
|
version=$(echo "$interface_def" | jq -r ".version")
|
||||||
|
|
||||||
|
command="pnpm bp interfaces get \"$name@$version\" --json >/dev/null 2>&1"
|
||||||
|
exists=$(eval "$command" && echo 1 || echo 0)
|
||||||
|
echo "$exists"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: plugin name is not provided" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
plugin=$1
|
||||||
|
|
||||||
|
plugin_path="plugins/$plugin"
|
||||||
|
plugin_def=$(pnpm bp read --work-dir "$plugin_path" --json)
|
||||||
|
|
||||||
|
name=$(echo "$plugin_def" | jq -r ".name")
|
||||||
|
version=$(echo "$plugin_def" | jq -r ".version")
|
||||||
|
|
||||||
|
command="pnpm bp plugins get \"$name@$version\" --json >/dev/null 2>&1"
|
||||||
|
exists=$(eval "$command" && echo 1 || echo 0)
|
||||||
|
echo "$exists"
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { LinearClient, Team, WorkflowState, IssueLabel } from '@linear/sdk'
|
||||||
|
|
||||||
|
const LINEAR_API_KEY = process.env.LINEAR_API_KEY
|
||||||
|
const SOURCE_STATE_NAME = 'Staging'
|
||||||
|
const TARGET_STATE_NAME = 'Production (Done)'
|
||||||
|
const TEAM_NAME = process.env.TEAM_NAME
|
||||||
|
const TARGET_LABEL = process.env.TARGET_LABEL
|
||||||
|
|
||||||
|
if (!LINEAR_API_KEY) {
|
||||||
|
throw new Error('No LINEAR_API_KEY environment variable')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TEAM_NAME) {
|
||||||
|
throw new Error('No TEAM_NAME environment variable')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TARGET_LABEL) {
|
||||||
|
throw new Error('No TARGET_LABEL environment variable')
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLinearIssues()
|
||||||
|
|
||||||
|
async function getTeam(): Promise<Team> {
|
||||||
|
const client = new LinearClient({ apiKey: LINEAR_API_KEY })
|
||||||
|
const teams = await client.teams()
|
||||||
|
const targetTeam = teams.nodes.find((t) => t.name === TEAM_NAME)
|
||||||
|
|
||||||
|
if (!targetTeam) throw new Error(`Could not find team with name "${TEAM_NAME}"`)
|
||||||
|
return targetTeam
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStates(targetTeam: Team): Promise<{ sourceState: WorkflowState; targetState: WorkflowState }> {
|
||||||
|
const states = await targetTeam.states()
|
||||||
|
const sourceState = states.nodes.find((s) => s.name === SOURCE_STATE_NAME)
|
||||||
|
const targetState = states.nodes.find((s) => s.name === TARGET_STATE_NAME)
|
||||||
|
|
||||||
|
if (!sourceState) throw new Error(`Could not find workflow state ${SOURCE_STATE_NAME}`)
|
||||||
|
if (!targetState) throw new Error(`Could not find workflow state ${TARGET_STATE_NAME}`)
|
||||||
|
|
||||||
|
console.info(`Found source state: ${sourceState.name} (${sourceState.id})`)
|
||||||
|
console.info(`Found target state: ${targetState.name} (${targetState.id})`)
|
||||||
|
|
||||||
|
return { sourceState, targetState }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTargetLabels(targetTeam: Team): Promise<IssueLabel[]> {
|
||||||
|
let labels = await targetTeam.labels()
|
||||||
|
const labelsArray = [...labels.nodes]
|
||||||
|
|
||||||
|
while (labels.pageInfo.hasNextPage) {
|
||||||
|
labels = await labels.fetchNext()
|
||||||
|
labelsArray.push(...labels.nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetLabels = labelsArray.filter(
|
||||||
|
(label) => label.name === TARGET_LABEL || label.name.startsWith(TARGET_LABEL + '/')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (targetLabels.length === 0) {
|
||||||
|
throw new Error(`Could not find any labels matching ${TARGET_LABEL}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Found ${targetLabels.length} matching label(s):`)
|
||||||
|
targetLabels.forEach((label) => {
|
||||||
|
console.info(` - ${label.name} (${label.id})`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return targetLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateLinearIssues() {
|
||||||
|
const targetTeam = await getTeam()
|
||||||
|
const { sourceState, targetState } = await getStates(targetTeam)
|
||||||
|
const targetLabels = await getTargetLabels(targetTeam)
|
||||||
|
|
||||||
|
const targetLabelIds = targetLabels.map((label) => label.id)
|
||||||
|
|
||||||
|
const issues = await targetTeam.issues({
|
||||||
|
filter: {
|
||||||
|
labels: { some: { id: { in: targetLabelIds } } },
|
||||||
|
state: { id: { eq: sourceState.id } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.info(`Found ${issues.nodes.length} issue(s) in state "${SOURCE_STATE_NAME}"`)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
issues.nodes.map(async (issue) => {
|
||||||
|
console.info(`Updating issue ${issue.identifier} to ${TARGET_STATE_NAME}`)
|
||||||
|
await issue.update({ stateId: targetState.id })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: Check All
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-all:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
checks: read
|
||||||
|
statuses: read
|
||||||
|
steps:
|
||||||
|
- name: Check All
|
||||||
|
uses: upsidr/merge-gatekeeper@v1
|
||||||
|
with:
|
||||||
|
timeout: 900 # 15 minutes
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
self: check-all
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Check Integration Categories
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-integration-categories:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v46
|
||||||
|
with:
|
||||||
|
files: 'integrations/**/*.{ts,md,svg,vrl}'
|
||||||
|
separator: '\n'
|
||||||
|
- name: Check integration categories
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||||
|
run: |
|
||||||
|
modified_integrations=$(echo -e "$CHANGED_FILES" | awk -F'/' '{print $2}' | sort -u)
|
||||||
|
|
||||||
|
should_fail=0
|
||||||
|
|
||||||
|
for integration in $modified_integrations; do
|
||||||
|
if [ ! -d "integrations/$integration" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking $integration"
|
||||||
|
|
||||||
|
if ! ./.github/scripts/integration-has-valid-category.sh "$integration"; then
|
||||||
|
should_fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_fail" -ne 0 ]; then
|
||||||
|
echo "Please add a valid category attribute to your integration definition before pushing your changes."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Check Integration Versions
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-integration-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Login to Botpress
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
WORKSPACE_ID: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
run: pnpm bp login -y --token "$TOKEN" --workspace-id "$WORKSPACE_ID"
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v46
|
||||||
|
with:
|
||||||
|
files: 'integrations/**/*.{ts,md,svg,vrl}'
|
||||||
|
separator: '\n'
|
||||||
|
- name: Check integration versions
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||||
|
run: |
|
||||||
|
modified_integrations=$(echo -e "$CHANGED_FILES" | awk -F'/' '{print $2}' | sort -u)
|
||||||
|
|
||||||
|
should_fail=0
|
||||||
|
|
||||||
|
SKIP_INTEGRATIONS=("chat")
|
||||||
|
|
||||||
|
for integration in $modified_integrations; do
|
||||||
|
skip=0
|
||||||
|
for skip_integration in "${SKIP_INTEGRATIONS[@]}"; do
|
||||||
|
if [ "$integration" = "$skip_integration" ]; then
|
||||||
|
echo "Skipping $integration"
|
||||||
|
skip=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $skip -eq 1 ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking $integration"
|
||||||
|
exists=$(./.github/scripts/integration-exists.sh "$integration")
|
||||||
|
|
||||||
|
if [ "$exists" -ne 0 ]; then
|
||||||
|
echo "Integration $integration is already deployed publicly with the same version. Please update the version of your integration."
|
||||||
|
should_fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_fail" -ne 0 ]; then
|
||||||
|
echo "Please update the version of your integration before pushing your changes."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Check Plugins Versions
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-plugins-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Login to Botpress
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
WORKSPACE_ID: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
run: pnpm bp login -y --token "$TOKEN" --workspace-id "$WORKSPACE_ID"
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v46
|
||||||
|
with:
|
||||||
|
files: 'plugins/**/*.{ts,md,svg}'
|
||||||
|
separator: '\n'
|
||||||
|
- name: Check plugin versions
|
||||||
|
env:
|
||||||
|
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
|
||||||
|
run: |
|
||||||
|
modified_plugins=$(echo -e "$CHANGED_FILES" | awk -F'/' '{print $2}' | sort -u)
|
||||||
|
|
||||||
|
should_fail=0
|
||||||
|
|
||||||
|
for plugin in $modified_plugins; do
|
||||||
|
echo "Checking $plugin"
|
||||||
|
exists=$(./.github/scripts/plugin-exists.sh "$plugin")
|
||||||
|
|
||||||
|
if [ "$exists" -ne 0 ]; then
|
||||||
|
echo "Plugin $plugin is already deployed publicly with the same version. Please update the version of your plugin."
|
||||||
|
should_fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$should_fail" -ne 0 ]; then
|
||||||
|
echo "Please update the version of your plugin before pushing your changes."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Check Shell Scripts
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/*.sh'
|
||||||
|
- .github/workflows
|
||||||
|
- .github/actions
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-shells:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Check shell scripts
|
||||||
|
uses: botpress/gh-actions/check-shells@v3
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Deploy Bots
|
||||||
|
|
||||||
|
on: workflow_dispatch
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
env:
|
||||||
|
BUGBUSTER_GITHUB_TOKEN: ${{ secrets.BUGBUSTER_GITHUB_TOKEN }}
|
||||||
|
BUGBUSTER_GITHUB_WEBHOOK_SECRET: ${{ secrets.BUGBUSTER_GITHUB_WEBHOOK_SECRET }}
|
||||||
|
BUGBUSTER_LINEAR_API_KEY: ${{ secrets.BUGBUSTER_LINEAR_API_KEY }}
|
||||||
|
BUGBUSTER_LINEAR_WEBHOOK_SIGNING_SECRET: ${{ secrets.BUGBUSTER_LINEAR_WEBHOOK_SIGNING_SECRET }}
|
||||||
|
BUGBUSTER_TELEGRAM_BOT_TOKEN: ${{ secrets.BUGBUSTER_TELEGRAM_BOT_TOKEN }}
|
||||||
|
BUGBUSTER_SLACK_REFRESH_TOKEN: ${{ secrets.BUGBUSTER_SLACK_REFRESH_TOKEN }}
|
||||||
|
BUGBUSTER_SLACK_CLIENT_ID: ${{ secrets.BUGBUSTER_SLACK_CLIENT_ID }}
|
||||||
|
BUGBUSTER_SLACK_CLIENT_SECRET: ${{ secrets.BUGBUSTER_SLACK_CLIENT_SECRET }}
|
||||||
|
BUGBUSTER_SLACK_SIGNING_SECRET: ${{ secrets.BUGBUSTER_SLACK_SIGNING_SECRET }}
|
||||||
|
CLOG_SLACK_CLIENT_ID: ${{ secrets.CLOG_SLACK_CLIENT_ID }}
|
||||||
|
CLOG_SLACK_CLIENT_SECRET: ${{ secrets.CLOG_SLACK_CLIENT_SECRET }}
|
||||||
|
CLOG_SLACK_SIGNING_SECRET: ${{ secrets.CLOG_SLACK_SIGNING_SECRET }}
|
||||||
|
CLOG_SLACK_REFRESH_TOKEN: ${{ secrets.CLOG_SLACK_REFRESH_TOKEN }}
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Deploy Bots
|
||||||
|
env:
|
||||||
|
WORKSPACE_ID: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
TOKEN: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
api_url="https://api.botpress.cloud"
|
||||||
|
|
||||||
|
# login
|
||||||
|
|
||||||
|
echo "### Logging in to $api_url ###"
|
||||||
|
pnpm bp login -y --api-url "$api_url" --workspaceId "$WORKSPACE_ID" --token "$TOKEN"
|
||||||
|
|
||||||
|
# deploy
|
||||||
|
|
||||||
|
bots="pnpm list -F bugbuster -F clog --json"
|
||||||
|
bot_paths=$(eval "$bots" | jq -r 'map(.path) | .[]')
|
||||||
|
|
||||||
|
for bot_path in $bot_paths; do
|
||||||
|
bot_name=$(basename "$bot_path")
|
||||||
|
echo -e "\nDeploying bot: ### $bot_name ###\n"
|
||||||
|
bot_id=$(pnpm bp bots new --name "$bot_name" --json --if-not-exists | jq -r ".id")
|
||||||
|
pnpm retry -n 2 -- pnpm -F "$bot_name" -c exec -- "pnpm bp deploy -v -y --botId $bot_id"
|
||||||
|
done
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
name: Deploy Chat Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branchOrCommit:
|
||||||
|
description: 'Branch/Commit/Tag to deploy'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'master'
|
||||||
|
skipDeploy:
|
||||||
|
description: 'Skip deployment to ECS'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
builder:
|
||||||
|
description: 'Image Builder'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- docker
|
||||||
|
- depot
|
||||||
|
default: 'depot'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-chat-production:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: botpress/gh-actions/full-service-deploy@v3.3
|
||||||
|
with:
|
||||||
|
service: chat
|
||||||
|
repository: chat-integration
|
||||||
|
dockerfile: integrations/chat/Dockerfile
|
||||||
|
context: .
|
||||||
|
builder: ${{ inputs.builder || 'depot' }}
|
||||||
|
depot-project: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
|
role-ecs-update: botpress_infra_update
|
||||||
|
skip-ecs-update: ${{ inputs.skipDeploy }}
|
||||||
|
create-tag: true
|
||||||
|
ref: ${{ inputs.branchOrCommit || github.sha }}
|
||||||
|
environment: production
|
||||||
|
|
||||||
|
deploy-chat-lambda:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.branchOrCommit || github.sha }}
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Deploy to Lambda
|
||||||
|
run: |
|
||||||
|
pnpm bp login -y --api-url "https://api.botpress.cloud" --workspaceId "${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}" --token "${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}"
|
||||||
|
pnpm -F "{integrations/chat}" -c exec -- bp deploy -v -y --noBuild --visibility public --allowDeprecated
|
||||||
|
|
||||||
|
ping-success:
|
||||||
|
needs: [deploy-chat-production, deploy-chat-lambda]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}"
|
||||||
|
|
||||||
|
ping-failure:
|
||||||
|
needs: [deploy-chat-production, deploy-chat-lambda]
|
||||||
|
if: ${{ failure() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_PRODUCTION_PING_URL }}/fail"
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Deploy Chat Staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'integrations/chat/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branchOrCommit:
|
||||||
|
description: 'Branch/Commit/Tag to deploy'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: 'master'
|
||||||
|
skipDeploy:
|
||||||
|
description: 'Skip deployment to ECS'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
builder:
|
||||||
|
description: 'Image Builder'
|
||||||
|
required: false
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- docker
|
||||||
|
- depot
|
||||||
|
default: 'depot'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-chat-staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: botpress/gh-actions/full-service-deploy@v3.3
|
||||||
|
with:
|
||||||
|
service: chat
|
||||||
|
repository: chat-integration
|
||||||
|
dockerfile: integrations/chat/Dockerfile
|
||||||
|
context: .
|
||||||
|
builder: ${{ inputs.builder || 'depot' }}
|
||||||
|
depot-project: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
|
role-ecs-update: botpress_infra_update
|
||||||
|
skip-ecs-update: ${{ inputs.skipDeploy }}
|
||||||
|
ref: ${{ inputs.branchOrCommit || github.sha }}
|
||||||
|
environment: staging
|
||||||
|
|
||||||
|
deploy-chat-lambda:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.branchOrCommit || github.sha }}
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Deploy to Lambda
|
||||||
|
run: |
|
||||||
|
pnpm bp login -y --api-url "https://api.botpress.dev" --workspaceId "${{ secrets.STAGING_CLOUD_OPS_WORKSPACE_ID }}" --token "${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}"
|
||||||
|
pnpm -F "{integrations/chat}" -c exec -- bp deploy -v -y --noBuild --visibility public --allowDeprecated
|
||||||
|
|
||||||
|
ping-success:
|
||||||
|
needs: [deploy-chat-staging, deploy-chat-lambda]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}"
|
||||||
|
|
||||||
|
ping-failure:
|
||||||
|
needs: [deploy-chat-staging, deploy-chat-lambda]
|
||||||
|
if: ${{ failure() }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: curl -m 10 --retry 5 "${{ secrets.CHAT_DEPLOY_STAGING_PING_URL }}/fail"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Deploy Integrations Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force:
|
||||||
|
description: 'Force re-deploying integrations'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-production:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Deploy Interfaces
|
||||||
|
uses: ./.github/actions/deploy-interfaces
|
||||||
|
with:
|
||||||
|
environment: 'production'
|
||||||
|
force: ${{ github.event.inputs.force == 'true' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
- name: Deploy Integrations
|
||||||
|
uses: ./.github/actions/deploy-integrations
|
||||||
|
with:
|
||||||
|
environment: 'production'
|
||||||
|
extra_filter: "-F '!docusign' -F '!chat' -F '!grafana'"
|
||||||
|
force: ${{ github.event.inputs.force == 'true' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
shopify_admin_automation_token: ${{ secrets.PRODUCTION_SHOPIFY_ADMIN_AUTOMATION_TOKEN }}
|
||||||
|
shopify_storefront_automation_token: ${{ secrets.PRODUCTION_SHOPIFY_STOREFRONT_AUTOMATION_TOKEN }}
|
||||||
|
- name: Deploy Plugins
|
||||||
|
uses: ./.github/actions/deploy-plugins
|
||||||
|
with:
|
||||||
|
extra_filter: "-F '!analytics' -F '!logger' -F '!personality' -F '!synchronizer' -F '!knowledge'"
|
||||||
|
environment: 'production'
|
||||||
|
force: ${{ github.event.inputs.force == 'true' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
|
||||||
|
- name: Update Linear Status
|
||||||
|
uses: ./.github/actions/update-linear-status
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
linearApiKey: ${{secrets.LINEAR_API_KEY}}
|
||||||
|
teamName: 'SHELL (Integration)'
|
||||||
|
targetLabel: 'area/integrations'
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Deploy Integrations Staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths: 'integrations/**'
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force:
|
||||||
|
description: 'Force re-deploying integrations'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-staging:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Deploy Interfaces
|
||||||
|
uses: ./.github/actions/deploy-interfaces
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
with:
|
||||||
|
environment: 'staging'
|
||||||
|
force: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.STAGING_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
- name: Deploy Integrations
|
||||||
|
uses: ./.github/actions/deploy-integrations
|
||||||
|
with:
|
||||||
|
environment: 'staging'
|
||||||
|
extra_filter: "-F '!chat'"
|
||||||
|
force: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' }}
|
||||||
|
dry_run: ${{ github.event_name == 'pull_request' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.STAGING_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
shopify_admin_automation_token: ${{ secrets.STAGING_SHOPIFY_ADMIN_AUTOMATION_TOKEN }}
|
||||||
|
shopify_storefront_automation_token: ${{ secrets.STAGING_SHOPIFY_STOREFRONT_AUTOMATION_TOKEN }}
|
||||||
|
- name: Deploy Plugins
|
||||||
|
uses: ./.github/actions/deploy-plugins
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
|
with:
|
||||||
|
environment: 'staging'
|
||||||
|
force: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force == 'true' }}
|
||||||
|
token_cloud_ops_account: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.STAGING_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: Check Chat Integration Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'integrations/chat/**'
|
||||||
|
- 'packages/sdk/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: depot-ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: ${{ secrets.DEPOT_PROJECT_ID }}
|
||||||
|
build-args: |
|
||||||
|
MINIFY=true
|
||||||
|
file: ./integrations/chat/Dockerfile
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: chat-integration:test
|
||||||
|
|
||||||
|
- name: Start Docker container
|
||||||
|
run: |
|
||||||
|
docker run -d \
|
||||||
|
--name chat-test \
|
||||||
|
-p 8081:8081 \
|
||||||
|
-e SECRET_SIGNAL_URL=https://chat.botpress.dev \
|
||||||
|
chat-integration:test
|
||||||
|
|
||||||
|
- name: Wait for container to be ready
|
||||||
|
run: |
|
||||||
|
echo "Waiting for container to start..."
|
||||||
|
for i in {1..30}; do
|
||||||
|
if docker ps --filter "name=chat-test" --filter "status=running" | grep -q chat-test; then
|
||||||
|
echo "Container is running"
|
||||||
|
sleep 5
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if docker ps -a --filter "name=chat-test" --filter "status=exited" | grep -q chat-test; then
|
||||||
|
echo "Container exited prematurely!"
|
||||||
|
docker logs chat-test
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Waiting for container... ($i/30)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Container did not start within expected time"
|
||||||
|
docker ps -a
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Check health endpoint
|
||||||
|
run: |
|
||||||
|
echo "Checking /health endpoint..."
|
||||||
|
for i in {1..15}; do
|
||||||
|
if curl -f http://localhost:8081/health; then
|
||||||
|
echo "Health check passed!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $i/15 failed, retrying..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Health check failed after 15 attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Show container logs on failure
|
||||||
|
if: failure()
|
||||||
|
run: docker logs chat-test
|
||||||
|
|
||||||
|
- name: Cleanup container
|
||||||
|
if: always()
|
||||||
|
run: docker rm -f chat-test || true
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Production version matches master
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 8 * * 1-5'
|
||||||
|
workflow_dispatch: # optional manual trigger
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Login to Botpress
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
WORKSPACE_ID: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
run: pnpm bp login -y --token "$TOKEN" --workspace-id "$WORKSPACE_ID"
|
||||||
|
- name: Check integration versions
|
||||||
|
env:
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WORKFLOW_SEAGULL_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
SKIP_INTEGRATIONS=("chat" "docusign" "zendesk-messaging-hitl")
|
||||||
|
|
||||||
|
integrations=$(find integrations -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -n1 basename | sort -u)
|
||||||
|
should_fail=0
|
||||||
|
outdated_integrations=""
|
||||||
|
skipped_integrations=""
|
||||||
|
|
||||||
|
for integration in $integrations; do
|
||||||
|
# Check if integration should be skipped
|
||||||
|
skip=0
|
||||||
|
for skip_integration in "${SKIP_INTEGRATIONS[@]}"; do
|
||||||
|
if [ "$integration" = "$skip_integration" ]; then
|
||||||
|
echo "Skipping $integration"
|
||||||
|
skip=1
|
||||||
|
skipped_integrations="$skipped_integrations> $integration\n"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $skip -eq 1 ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking $integration"
|
||||||
|
exists=$(.github/scripts/integration-exists.sh "$integration")
|
||||||
|
|
||||||
|
if [ "$exists" -eq 0 ]; then
|
||||||
|
echo "Integration $integration is not up to date. Please deploy the latest version of your integration."
|
||||||
|
should_fail=1
|
||||||
|
outdated_integrations="$outdated_integrations> $integration\n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $should_fail -eq 1 ]; then
|
||||||
|
message="\n\nThe following integrations are out of date:\n$outdated_integrations"
|
||||||
|
message="$message\nThe following integrations were skipped:\n$skipped_integrations"
|
||||||
|
message="$message\nPlease run the deployment for the out-of-date integrations in ${SERVER_URL}/${REPO}"
|
||||||
|
|
||||||
|
echo -e "\n\nSending curl request to Slack webhook"
|
||||||
|
echo "Response:"
|
||||||
|
curl -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"key": "prod-master-version-verification",
|
||||||
|
"text": "'"$message"'"
|
||||||
|
}'
|
||||||
|
echo -e "\nCurl request sent"
|
||||||
|
|
||||||
|
echo -e "$message"
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Publish Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-packages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/* -F llmz -F @bpinternal/zui'
|
||||||
|
|
||||||
|
- name: update npm
|
||||||
|
shell: bash
|
||||||
|
run: npm install -g npm@^11.5.1 # Required for OIDC login
|
||||||
|
|
||||||
|
- name: Publish Zui
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/zui'
|
||||||
|
- name: Publish Client
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/client'
|
||||||
|
- name: Publish SDK
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/sdk'
|
||||||
|
- name: Publish CLI
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/cli'
|
||||||
|
- name: Publish Chat Client
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/chat-client'
|
||||||
|
- name: Publish Cognitive Client
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/cognitive'
|
||||||
|
- name: Publish Vai
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/vai'
|
||||||
|
- name: Publish Zai
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/zai'
|
||||||
|
- name: Publish LLMz
|
||||||
|
uses: botpress/gh-actions/publish-if-not-exists@master
|
||||||
|
with:
|
||||||
|
path: './packages/llmz'
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Purge Cache
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
jobs:
|
||||||
|
purge-cache:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Delete Cache Entries
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh extension install actions/gh-actions-cache
|
||||||
|
|
||||||
|
echo "Fetching list of cache keys for branch '$BRANCH'"
|
||||||
|
cacheKeysForBranch=$(gh actions-cache list --limit 100 -R "$REPO" -B "$BRANCH" | cut -f 1 )
|
||||||
|
|
||||||
|
## Setting this to not fail the workflow while deleting cache keys.
|
||||||
|
set +e
|
||||||
|
echo "Deleting cache entries..."
|
||||||
|
|
||||||
|
for cacheKey in $cacheKeysForBranch
|
||||||
|
do
|
||||||
|
echo "Deleting entry: $cacheKey"
|
||||||
|
gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Refresh Instagram Access Tokens Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 10:00 AM EST (15:00 UTC) on the first Tuesday of every month
|
||||||
|
- cron: '0 15 1-7 * 2'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
refresh-tokens:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpresshub/instagram'
|
||||||
|
- name: Refresh Instagram Access Tokens
|
||||||
|
uses: ./.github/actions/refresh-instagram-tokens
|
||||||
|
with:
|
||||||
|
token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
api_url: https://api.botpress.cloud
|
||||||
|
current_token: ${{ secrets.PRODUCTION_INSTAGRAM_ACCESS_TOKEN }}
|
||||||
|
secret_name: PRODUCTION_INSTAGRAM_ACCESS_TOKEN
|
||||||
|
sync_secret_app_id: ${{ vars.SYNC_SECRET_APP_ID }}
|
||||||
|
sync_secret_app_private_key: ${{ secrets.SYNC_SECRET_APP_PRIVATE_KEY }}
|
||||||
|
ping-success:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
needs: [refresh-tokens]
|
||||||
|
steps:
|
||||||
|
- env:
|
||||||
|
PING_URL: ${{ secrets.INSTAGRAM_REFRESH_TOKEN_PING_URL }}
|
||||||
|
run: curl -m 10 --retry 5 "$PING_URL"
|
||||||
|
ping-failure:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
if: ${{ failure() }}
|
||||||
|
needs: [refresh-tokens]
|
||||||
|
steps:
|
||||||
|
- env:
|
||||||
|
PING_URL: ${{ secrets.INSTAGRAM_REFRESH_TOKEN_PING_URL }}
|
||||||
|
run: curl -m 10 --retry 5 "$PING_URL/fail"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Refresh Instagram Access Tokens Staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run at 10:00 AM EST (15:00 UTC) on the first Tuesday of every month
|
||||||
|
- cron: '0 15 1-7 * 2'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
refresh-tokens:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpresshub/instagram'
|
||||||
|
- name: Refresh Instagram Access Tokens
|
||||||
|
uses: ./.github/actions/refresh-instagram-tokens
|
||||||
|
with:
|
||||||
|
token_cloud_ops_account: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
cloud_ops_workspace_id: ${{ secrets.STAGING_CLOUD_OPS_WORKSPACE_ID }}
|
||||||
|
api_url: https://api.botpress.dev
|
||||||
|
current_token: ${{ secrets.STAGING_INSTAGRAM_ACCESS_TOKEN }}
|
||||||
|
secret_name: STAGING_INSTAGRAM_ACCESS_TOKEN
|
||||||
|
sync_secret_app_id: ${{ vars.SYNC_SECRET_APP_ID }}
|
||||||
|
sync_secret_app_private_key: ${{ secrets.SYNC_SECRET_APP_PRIVATE_KEY }}
|
||||||
|
ping-success:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
needs: [refresh-tokens]
|
||||||
|
steps:
|
||||||
|
- env:
|
||||||
|
PING_URL: ${{ secrets.STAGING_INSTAGRAM_REFRESH_TOKEN_PING_URL }}
|
||||||
|
run: curl -m 10 --retry 5 "$PING_URL"
|
||||||
|
ping-failure:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
if: ${{ failure() }}
|
||||||
|
needs: [refresh-tokens]
|
||||||
|
steps:
|
||||||
|
- env:
|
||||||
|
PING_URL: ${{ secrets.STAGING_INSTAGRAM_REFRESH_TOKEN_PING_URL }}
|
||||||
|
run: curl -m 10 --retry 5 "$PING_URL/fail"
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Run Chat Tests
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-chat-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpresshub/chat -F @bp-bots/echo -F @botpress/chat'
|
||||||
|
- name: Install tilt
|
||||||
|
run: curl -k -fsSL https://raw.githubusercontent.com/tilt-dev/tilt/master/scripts/install.sh | bash
|
||||||
|
- name: Run Tilt
|
||||||
|
env:
|
||||||
|
BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
tilt ci -f ./echo.Tiltfile -- \
|
||||||
|
--skip-install \
|
||||||
|
--skip-build \
|
||||||
|
--bp-domain 'botpress.dev' \
|
||||||
|
--bp-workspace-id '95de33eb-1551-4af9-9088-e5dcb02efd09' \
|
||||||
|
--bp-token "$BP_TOKEN"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Run Checks
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-checks:
|
||||||
|
runs-on: depot-ubuntu-22.04-8
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- run: pnpm run check:dep
|
||||||
|
- run: pnpm run check:sherif
|
||||||
|
- run: pnpm run check:oxlint
|
||||||
|
- run: pnpm run check:format
|
||||||
|
- run: pnpm run check:eslint
|
||||||
|
- run: pnpm run check:bplint
|
||||||
|
- run: pnpm run check:type
|
||||||
|
- run: pnpm run test
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Run CLI Tests Windows
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-cli-tests-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/cli'
|
||||||
|
- env:
|
||||||
|
WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
# shellcheck disable=all
|
||||||
|
pnpm run -F cli test:e2e -v --api-url https://api.botpress.dev --workspace-id $env:WORKSPACE_ID --workspace-handle clitests --token $env:TOKEN --sdk-path "$(pwd)/packages/sdk" --client-path "$(pwd)/packages/client"
|
||||||
|
shell: pwsh
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Run CLI Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request: {}
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-cli-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/cli'
|
||||||
|
- env:
|
||||||
|
WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
pnpm run -F cli test:e2e -v \
|
||||||
|
--api-url https://api.botpress.dev \
|
||||||
|
--workspace-id "$WORKSPACE_ID" \
|
||||||
|
--workspace-handle clitests \
|
||||||
|
--token "$TOKEN" \
|
||||||
|
--sdk-path "$(pwd)/packages/sdk" \
|
||||||
|
--client-path "$(pwd)/packages/client"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Run Client Tests
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-client-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/client'
|
||||||
|
- run: |
|
||||||
|
# pnpm -F client exec puppeteer browsers install chrome
|
||||||
|
pnpm -F client run test:e2e
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Run Cognitive Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'packages/cognitive/**'
|
||||||
|
- '.github/workflows/run-cognitive-tests.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-cognitive-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/cognitive'
|
||||||
|
- name: Login
|
||||||
|
env:
|
||||||
|
BP_API_URL: 'https://api.botpress.dev'
|
||||||
|
BP_WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
pnpm bp login
|
||||||
|
- name: Run Cognitive Tests
|
||||||
|
id: 'run-tests'
|
||||||
|
env:
|
||||||
|
CLOUD_PAT: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
bot_id=$(pnpm bp bots new --json | jq -r ".id")
|
||||||
|
echo "bot_id=$bot_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
export CLOUD_API_ENDPOINT='https://api.botpress.dev'
|
||||||
|
export CLOUD_BOT_ID="$bot_id"
|
||||||
|
pnpm -F cognitive run test:e2e
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
BOT_ID: ${{ steps.run-tests.outputs.bot_id }}
|
||||||
|
run: |
|
||||||
|
pnpm bp bots rm "$BOT_ID"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Run LLMz Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'packages/llmz/**'
|
||||||
|
- '.github/workflows/run-llmz-tests.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-llmz-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F llmz'
|
||||||
|
- name: Login
|
||||||
|
env:
|
||||||
|
BP_API_URL: 'https://api.botpress.dev'
|
||||||
|
BP_WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
pnpm bp login
|
||||||
|
- name: Run LLMz Tests
|
||||||
|
id: 'run-tests'
|
||||||
|
env:
|
||||||
|
CLOUD_PAT: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
bot_id=$(pnpm bp bots new --json | jq -r ".id")
|
||||||
|
echo "bot_id=$bot_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
export CLOUD_API_ENDPOINT='https://api.botpress.dev'
|
||||||
|
export CLOUD_BOT_ID="$bot_id"
|
||||||
|
pnpm -F llmz run test:e2e
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
BOT_ID: ${{ steps.run-tests.outputs.bot_id }}
|
||||||
|
run: |
|
||||||
|
pnpm bp bots rm "$BOT_ID"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Run Vai Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'packages/vai/**'
|
||||||
|
- '.github/workflows/run-vai-tests.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-vai-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/vai'
|
||||||
|
- name: Login
|
||||||
|
env:
|
||||||
|
BP_API_URL: 'https://api.botpress.dev'
|
||||||
|
BP_WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
pnpm bp login
|
||||||
|
- name: Run Vai Tests
|
||||||
|
id: 'run-tests'
|
||||||
|
env:
|
||||||
|
CLOUD_PAT: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
bot_id=$(pnpm bp bots new --json | jq -r ".id")
|
||||||
|
echo "bot_id=$bot_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
export CLOUD_API_ENDPOINT='https://api.botpress.dev'
|
||||||
|
export CLOUD_BOT_ID="$bot_id"
|
||||||
|
pnpm -F vai run test:e2e
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
BOT_ID: ${{ steps.run-tests.outputs.bot_id }}
|
||||||
|
run: |
|
||||||
|
pnpm bp bots rm "$BOT_ID"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Run Zai Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'packages/zai/**'
|
||||||
|
- '.github/workflows/run-zai-tests.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-zai-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
extra_filters: '-F @botpress/zai'
|
||||||
|
- name: Login
|
||||||
|
env:
|
||||||
|
BP_API_URL: 'https://api.botpress.dev'
|
||||||
|
BP_WORKSPACE_ID: ${{ secrets.STAGING_E2E_TESTS_WORKSPACE_ID }}
|
||||||
|
BP_TOKEN: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
pnpm bp login
|
||||||
|
- name: Run Zai Tests
|
||||||
|
id: 'run-tests'
|
||||||
|
env:
|
||||||
|
CLOUD_PAT: ${{ secrets.STAGING_TOKEN_CLOUD_OPS_ACCOUNT }}
|
||||||
|
run: |
|
||||||
|
bot_id=$(pnpm bp bots new --json | jq -r ".id")
|
||||||
|
echo "bot_id=$bot_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
export CLOUD_API_ENDPOINT='https://api.botpress.dev'
|
||||||
|
export CLOUD_BOT_ID="$bot_id"
|
||||||
|
pnpm -F zai run test:e2e
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: ${{ always() }}
|
||||||
|
env:
|
||||||
|
BOT_ID: ${{ steps.run-tests.outputs.bot_id }}
|
||||||
|
run: |
|
||||||
|
pnpm bp bots rm "$BOT_ID"
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
node_modules
|
||||||
|
bp_modules
|
||||||
|
dist
|
||||||
|
gen
|
||||||
|
local/data
|
||||||
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
|
__snapshots__
|
||||||
|
.ignore.me.*
|
||||||
|
.env*
|
||||||
|
.botpress
|
||||||
|
.botpresshome
|
||||||
|
.botpresshome.*
|
||||||
|
.shopify
|
||||||
|
.turbo
|
||||||
|
.genenv/
|
||||||
|
.genenv.*
|
||||||
|
tilt_config.json
|
||||||
|
/.idea
|
||||||
|
hubspot.config.yml
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
|
lefthook.yml
|
||||||
|
.claude
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"strictness": 2,
|
||||||
|
"commentTypes": ["info", "logic", "syntax"],
|
||||||
|
"triggerOnUpdates": false,
|
||||||
|
"shouldUpdateDescription": false,
|
||||||
|
"updateExistingSummaryComment": true,
|
||||||
|
"summarySection": {
|
||||||
|
"included": true,
|
||||||
|
"collapsible": true,
|
||||||
|
"defaultOpen": false
|
||||||
|
},
|
||||||
|
"issuesTableSection": {
|
||||||
|
"included": true,
|
||||||
|
"collapsible": true,
|
||||||
|
"defaultOpen": false
|
||||||
|
},
|
||||||
|
"confidenceScoreSection": {
|
||||||
|
"included": true,
|
||||||
|
"collapsible": true,
|
||||||
|
"defaultOpen": false
|
||||||
|
},
|
||||||
|
"sequenceDiagramSection": {
|
||||||
|
"included": true,
|
||||||
|
"collapsible": true,
|
||||||
|
"defaultOpen": false
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "error-handling",
|
||||||
|
"rule": "All async functions must use try-catch blocks."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "promise-handling",
|
||||||
|
"rule": "All promises must be awaited."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "catch-handling",
|
||||||
|
"rule": "All catch clauses must not use `any` as the error type (implicit or explicit). Instead, type the caught value as `unknown` and narrow it before use.\nThe following snippet is an example solution for this problem:\n\n```Typescript\ntry {\n ...\n} catch (thrown: unknown) {\n const error = thrown instanceof Error ? thrown : new Error(String(thrown))\n}\n```"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "configure-integration-identifier-type",
|
||||||
|
"rule": "When choosing the identifier of an integration, it should be something unique and if there is an extract script, it should match the value a global webhook extracts. For instance, if the global webhook extracts a `hubId`, then the identifier should be the `hubId` and not the `webhookId`."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pnpx lint-staged
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pnpm check
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "import", "promise", "vitest"],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2024": true,
|
||||||
|
"node": true,
|
||||||
|
"builtin": true
|
||||||
|
},
|
||||||
|
"globals": {},
|
||||||
|
"settings": {},
|
||||||
|
"ignorePatterns": [
|
||||||
|
".git/",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.cjs",
|
||||||
|
"**/*.mjs",
|
||||||
|
"**/*.jsx",
|
||||||
|
"**/*.md.ts",
|
||||||
|
"**/cdk.out/",
|
||||||
|
"**/dist/",
|
||||||
|
"**/node_modules/",
|
||||||
|
"**/bp_modules/",
|
||||||
|
"**/.botpress/",
|
||||||
|
"**/gen/",
|
||||||
|
"**/bp_modules/",
|
||||||
|
"**/.turbo/",
|
||||||
|
"**/.genenv/",
|
||||||
|
"packages/llmz/examples/",
|
||||||
|
"node_modules/",
|
||||||
|
"**/.adk/",
|
||||||
|
"**/.ignore.me.*"
|
||||||
|
],
|
||||||
|
"rules": {},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/*.{ts,tsx}"],
|
||||||
|
"jsPlugins": ["eslint-plugin-prettier", "@stylistic/eslint-plugin"],
|
||||||
|
"rules": {
|
||||||
|
"eslint/no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
"warn",
|
||||||
|
"dir",
|
||||||
|
"time",
|
||||||
|
"timeEnd",
|
||||||
|
"timeLog",
|
||||||
|
"trace",
|
||||||
|
"assert",
|
||||||
|
"clear",
|
||||||
|
"count",
|
||||||
|
"countReset",
|
||||||
|
"group",
|
||||||
|
"groupEnd",
|
||||||
|
"table",
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"dirxml",
|
||||||
|
"error",
|
||||||
|
"groupCollapsed",
|
||||||
|
"Console",
|
||||||
|
"profile",
|
||||||
|
"profileEnd",
|
||||||
|
"timeStamp",
|
||||||
|
"context"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eslint/no-cond-assign": "error",
|
||||||
|
"eslint/no-const-assign": "error",
|
||||||
|
"eslint/no-debugger": "error",
|
||||||
|
"eslint/no-sparse-arrays": "error",
|
||||||
|
"eslint/no-unreachable": "error",
|
||||||
|
"eslint/max-lines-per-function": "off",
|
||||||
|
"eslint/default-case": "off",
|
||||||
|
"eslint/default-case-last": "error",
|
||||||
|
"eslint/max-depth": "error",
|
||||||
|
"eslint/no-eval": "error",
|
||||||
|
"eslint/no-return-assign": "error",
|
||||||
|
"eslint/no-var": "error",
|
||||||
|
"eslint/no-duplicate-imports": "error",
|
||||||
|
"typescript/consistent-type-definitions": ["error", "type"],
|
||||||
|
"typescript/prefer-namespace-keyword": "error",
|
||||||
|
"eslint/curly": ["error", "multi-line"],
|
||||||
|
"eslint/eqeqeq": ["error", "smart"],
|
||||||
|
"typescript/no-unused-vars": "off",
|
||||||
|
"eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"args": "after-used",
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unicorn/no-empty-file": "error",
|
||||||
|
"unicorn/no-lonely-if": "off",
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"arrow-body-style": "off",
|
||||||
|
"complexity": ["off"],
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"@stylistic/member-delimiter-style": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"multiline": {
|
||||||
|
"delimiter": "none",
|
||||||
|
"requireLast": true
|
||||||
|
},
|
||||||
|
"singleline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/quotes": [
|
||||||
|
"error",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"avoidEscape": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/semi": ["error", "never"],
|
||||||
|
"@stylistic/type-annotation-spacing": "error",
|
||||||
|
"@stylistic/brace-style": "off",
|
||||||
|
"@stylistic/eol-last": "error",
|
||||||
|
"@stylistic/linebreak-style": ["error", "unix"],
|
||||||
|
"@stylistic/no-trailing-spaces": "error",
|
||||||
|
"no-shadow": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "off",
|
||||||
|
"pedantic": "off",
|
||||||
|
"perf": "off",
|
||||||
|
"suspicious": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# based on https://gist.github.com/thinkricardo/74f37d82b686de371b0853a5d66d559c
|
||||||
|
|
||||||
|
# ignore all files
|
||||||
|
*
|
||||||
|
|
||||||
|
# include all folders
|
||||||
|
!**/
|
||||||
|
|
||||||
|
# include files to format
|
||||||
|
!*.ts
|
||||||
|
!*.tsx
|
||||||
|
!*.json
|
||||||
|
!*.yaml
|
||||||
|
!*.yml
|
||||||
|
!*.md
|
||||||
|
!*.mjs
|
||||||
|
|
||||||
|
# exclusions
|
||||||
|
# **/*.d.ts
|
||||||
|
pnpm-lock.yaml
|
||||||
|
gen
|
||||||
|
dist
|
||||||
|
.botpress
|
||||||
|
.botpresshome
|
||||||
|
.botpresshome.*
|
||||||
|
bp_modules
|
||||||
|
.genenv/
|
||||||
|
.genenv.*
|
||||||
|
.turbo
|
||||||
|
*.md.ts
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pnpm 10.29.3
|
||||||
|
nodejs 22.17.0
|
||||||
|
tilt 0.35.0
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "oxc.oxc-vscode", "vitest.explorer"]
|
||||||
|
}
|
||||||
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug TS File",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
|
"args": ["${file}"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"skipFiles": ["<node_internals>/**", "node_modules/**"],
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Test File",
|
||||||
|
"autoAttachChildProcesses": true,
|
||||||
|
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||||
|
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||||
|
"args": ["run", "${fileBasename}"],
|
||||||
|
"cwd": "${fileDirname}",
|
||||||
|
"smartStep": true,
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug CLI",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"args": ["${workspaceRoot}/packages/cli", "${input:cli-command}"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"skipFiles": ["<node_internals>/**", "node_modules/**"],
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "cli-command",
|
||||||
|
"type": "pickString",
|
||||||
|
"description": "Select a command to run",
|
||||||
|
"options": ["login", "logout", "init", "generate", "bundle", "build", "serve", "deploy"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"git.branchProtection": ["master"],
|
||||||
|
"vitest.disableWorkspaceWarning": true,
|
||||||
|
"js/ts.tsdk.path": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
# Global
|
||||||
|
* @botpress/swordfish
|
||||||
|
|
||||||
|
# Packages
|
||||||
|
|
||||||
|
## API / Client / CLI / SDK
|
||||||
|
/packages/chat-api @botpress/kraken
|
||||||
|
/packages/chat-client @botpress/kraken
|
||||||
|
/packages/client @botpress/kraken
|
||||||
|
/packages/cli @botpress/kraken
|
||||||
|
/packages/sdk @botpress/kraken
|
||||||
|
/packages/zui @botpress/swordfish
|
||||||
|
|
||||||
|
## Common / Utils
|
||||||
|
/packages/common @botpress/shell
|
||||||
|
/packages/sdk-addons @botpress/shell
|
||||||
|
|
||||||
|
## AI
|
||||||
|
/packages/cognitive @botpress/dragon
|
||||||
|
/packages/llmz @slvnperron @botpress/orca @botpress/swordfish
|
||||||
|
/packages/vai @slvnperron @botpress/orca @botpress/swordfish
|
||||||
|
/packages/zai @slvnperron @botpress/orca @botpress/swordfish
|
||||||
|
|
||||||
|
# Bots
|
||||||
|
/bots @botpress/shell
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
/plugins @botpress/shell
|
||||||
|
|
||||||
|
# Integrations
|
||||||
|
/integrations @botpress/shell
|
||||||
|
|
||||||
|
## LLM
|
||||||
|
/integrations/anthropic @botpress/dragon
|
||||||
|
/integrations/cerebras @botpress/dragon
|
||||||
|
/integrations/fireworks-ai @botpress/dragon
|
||||||
|
/integrations/google-ai @botpress/dragon
|
||||||
|
/integrations/groq @botpress/dragon
|
||||||
|
/integrations/openai @botpress/dragon
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
/integrations/chat @botpress/kraken
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2023 Botpress Technologies, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# WeHub 来源说明
|
||||||
|
|
||||||
|
- 原始项目:`botpress/botpress`
|
||||||
|
- 原始仓库:https://github.com/botpress/botpress
|
||||||
|
- 导入方式:上游默认分支的最新快照
|
||||||
|
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||||
|
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
|
||||||
|
|
||||||
|
Report them privately through [GitHub private vulnerability reporting](https://github.com/botpress/botpress/security/advisories/new). If you can't use GitHub, email [security@botpress.com](mailto:security@botpress.com).
|
||||||
|
|
||||||
|
Please include as much of the following as you can:
|
||||||
|
|
||||||
|
- The type of issue (e.g. sandbox escape, injection, authentication bypass, credential exposure)
|
||||||
|
- The affected package(s) and version(s), or the affected integration
|
||||||
|
- Step-by-step instructions to reproduce, with a proof of concept if possible
|
||||||
|
- The impact as you understand it, including how an attacker might exploit it
|
||||||
|
|
||||||
|
### What to expect
|
||||||
|
|
||||||
|
- We will acknowledge your report within a reasonable time.
|
||||||
|
- We will keep you informed as we triage, confirm, and fix the issue.
|
||||||
|
- We coordinate disclosure with you. We aim to fix confirmed vulnerabilities within 90 days of the initial report. Once a fix is released, we publish a GitHub security advisory and credit you for the discovery unless you prefer to stay anonymous. If we need more time, we will communicate this and work out a mutually agreed extension before any disclosure.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This repository contains the Botpress SDK, CLI, client libraries, and the source of official integrations and plugins. Reports are in scope if the vulnerability is in code that lives here. Only the latest published version of each package receives security fixes.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- The Botpress Cloud platform and its infrastructure (app.botpress.cloud, APIs, webchat hosting). Report those to [security@botpress.com](mailto:security@botpress.com) instead.
|
||||||
|
- Bots and agents built by Botpress users
|
||||||
|
- Vulnerabilities in third-party services that integrations connect to (report those to the respective vendor)
|
||||||
|
- Vulnerabilities in dependencies with no demonstrated exploit path through this codebase (report those upstream)
|
||||||
|
- Denial of service through volume, brute force, or resource exhaustion
|
||||||
|
- Social engineering, phishing, and physical attacks
|
||||||
|
- Prompt injection that only affects the output quality of a user's own bot. Prompt injection that crosses a security boundary (unauthorized tool execution, data exfiltration, sandbox escape) is in scope.
|
||||||
|
|
||||||
|
Botpress v12 lives in a separate repository ([botpress/v12](https://github.com/botpress/v12)) and is not covered by this policy.
|
||||||
|
|
||||||
|
## Safe harbor
|
||||||
|
|
||||||
|
We consider security research conducted in line with this policy to be authorized. We will not pursue legal action for good-faith research that stays in scope, avoids privacy violations and service disruption, and gives us reasonable time to fix issues before public disclosure.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
local_resource(
|
||||||
|
name='install',
|
||||||
|
cmd="pnpm install"
|
||||||
|
)
|
||||||
|
|
||||||
|
local_resource(
|
||||||
|
name='build',
|
||||||
|
cmd="pnpm build",
|
||||||
|
deps=['install'],
|
||||||
|
)
|
||||||
|
|
||||||
|
local_resource(
|
||||||
|
name='check',
|
||||||
|
cmd="pnpm check",
|
||||||
|
deps=['install', 'build'],
|
||||||
|
)
|
||||||
|
|
||||||
|
local_resource(
|
||||||
|
name='test',
|
||||||
|
cmd="pnpm test",
|
||||||
|
deps=['install', 'build'],
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import * as genenv from './.genenv'
|
||||||
|
import github from './bp_modules/github'
|
||||||
|
import linear from './bp_modules/linear'
|
||||||
|
import slack from './bp_modules/slack'
|
||||||
|
import telegram from './bp_modules/telegram'
|
||||||
|
|
||||||
|
// TODO: use default options
|
||||||
|
const toJSONSchemaOptions: Partial<sdk.z.transforms.JSONSchemaGenerationOptions> = {
|
||||||
|
discriminatedUnionStrategy: 'anyOf',
|
||||||
|
discriminator: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new sdk.BotDefinition({
|
||||||
|
states: {
|
||||||
|
watchedTeams: {
|
||||||
|
type: 'bot',
|
||||||
|
schema: sdk.z.object({
|
||||||
|
teamKeys: sdk.z
|
||||||
|
.array(sdk.z.string())
|
||||||
|
.title('Team Keys')
|
||||||
|
.describe('The keys of the teams for which BugBuster should lint issues'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
lastLintedId: {
|
||||||
|
type: 'workflow',
|
||||||
|
schema: sdk.z.object({
|
||||||
|
id: sdk.z.string().optional().title('ID').describe('The ID of the last successfully linted issue'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
lintResults: {
|
||||||
|
type: 'workflow',
|
||||||
|
schema: sdk.z.object({
|
||||||
|
issues: sdk.z.array(
|
||||||
|
sdk.z.discriminatedUnion('result', [
|
||||||
|
sdk.z.object({
|
||||||
|
identifier: sdk.z.string().title('Identifier').describe('The issue identifier'),
|
||||||
|
result: sdk.z.literal('failed').title('Result').describe('The lint result'),
|
||||||
|
messages: sdk.z.array(sdk.z.string()).title('Messages').describe('The lint error messages'),
|
||||||
|
}),
|
||||||
|
sdk.z.object({
|
||||||
|
identifier: sdk.z.string().title('Identifier').describe('The issue identifier'),
|
||||||
|
result: sdk.z.enum(['succeeded', 'ignored']).title('Result').describe('The lint result'),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
notificationChannels: {
|
||||||
|
type: 'bot',
|
||||||
|
schema: sdk.z.object({
|
||||||
|
channels: sdk.z
|
||||||
|
.array(
|
||||||
|
sdk.z.object({
|
||||||
|
conversationId: sdk.z.string().title('Conversation ID').describe('The conversation ID'),
|
||||||
|
name: sdk.z.string().title('Name').describe('The channel name'),
|
||||||
|
teams: sdk.z
|
||||||
|
.array(sdk.z.string())
|
||||||
|
.title('Teams')
|
||||||
|
.describe('The teams for which notifications will be sent to the channel'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.title('Channel')
|
||||||
|
.describe('The Slack channel where notifications will be sent'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workflows: {
|
||||||
|
lintAll: {
|
||||||
|
input: {
|
||||||
|
schema: sdk.z.object({
|
||||||
|
verbose: sdk.z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.title('Verbose')
|
||||||
|
.describe('Post the detailed lint results (per issue) to the Slack channel'),
|
||||||
|
comment: sdk.z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.title('Comment')
|
||||||
|
.describe('Whether to comment the lint results on the Linear issues themselves (defaults to true)'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
output: { schema: sdk.z.object({}) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
timeToLintAll: {
|
||||||
|
schema: sdk.z.object({}),
|
||||||
|
},
|
||||||
|
timeToCheckIssuesState: {
|
||||||
|
schema: sdk.z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recurringEvents: {
|
||||||
|
timeToLintAll: {
|
||||||
|
payload: sdk.z.object({}),
|
||||||
|
type: 'timeToLintAll',
|
||||||
|
schedule: {
|
||||||
|
cron: '0 13 * * 1', // runs every week on Monday at 8AM EST
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeToCheckIssuesState: {
|
||||||
|
payload: sdk.z.object({}),
|
||||||
|
type: 'timeToCheckIssuesState',
|
||||||
|
schedule: {
|
||||||
|
cron: '0 * * * *', // runs every hour on the hour
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__advanced: {
|
||||||
|
toJSONSchemaOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addIntegration(github, {
|
||||||
|
enabled: true,
|
||||||
|
configurationType: 'manualPAT',
|
||||||
|
configuration: {
|
||||||
|
personalAccessToken: genenv.BUGBUSTER_GITHUB_TOKEN,
|
||||||
|
githubWebhookSecret: genenv.BUGBUSTER_GITHUB_WEBHOOK_SECRET,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addIntegration(telegram, {
|
||||||
|
enabled: true,
|
||||||
|
configurationType: null,
|
||||||
|
configuration: {
|
||||||
|
botToken: genenv.BUGBUSTER_TELEGRAM_BOT_TOKEN,
|
||||||
|
typingIndicatorEmoji: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addIntegration(linear, {
|
||||||
|
enabled: true,
|
||||||
|
configurationType: 'apiKey',
|
||||||
|
configuration: {
|
||||||
|
apiKey: genenv.BUGBUSTER_LINEAR_API_KEY,
|
||||||
|
webhookSigningSecret: genenv.BUGBUSTER_LINEAR_WEBHOOK_SIGNING_SECRET,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addIntegration(slack, {
|
||||||
|
enabled: true,
|
||||||
|
configurationType: null,
|
||||||
|
configuration: {
|
||||||
|
typingIndicatorEmoji: false,
|
||||||
|
replyBehaviour: {
|
||||||
|
location: 'channel',
|
||||||
|
onlyOnBotMention: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 792 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
import rootConfig from '../../eslint.config.mjs'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...rootConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@bp-bots/bugbuster",
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "genenv -o ./.genenv/index.ts -e BUGBUSTER_GITHUB_TOKEN -e BUGBUSTER_GITHUB_WEBHOOK_SECRET -e BUGBUSTER_LINEAR_API_KEY -e BUGBUSTER_LINEAR_WEBHOOK_SIGNING_SECRET -e BUGBUSTER_TELEGRAM_BOT_TOKEN",
|
||||||
|
"check:type": "tsc --noEmit",
|
||||||
|
"check:bplint": "bp lint",
|
||||||
|
"build": "bp add -y && bp build"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@botpress/client": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@botpress/cli": "workspace:*",
|
||||||
|
"@botpress/common": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*",
|
||||||
|
"@botpresshub/github": "workspace:*",
|
||||||
|
"@botpresshub/linear": "workspace:*",
|
||||||
|
"@botpresshub/slack": "workspace:*",
|
||||||
|
"@botpresshub/telegram": "workspace:*",
|
||||||
|
"@bpinternal/genenv": "0.0.1"
|
||||||
|
},
|
||||||
|
"bpDependencies": {
|
||||||
|
"github": "../../integrations/github",
|
||||||
|
"linear": "../../integrations/linear",
|
||||||
|
"slack": "../../integrations/slack",
|
||||||
|
"telegram": "../../integrations/telegram"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Bugbuster
|
||||||
|
|
||||||
|
A simple bot built on top of the Botpress [SDK](https://botpress.com/docs/integrations/sdk/overview) that lints Linear issues and synchronizes them with GitHub issues.
|
||||||
|
|
||||||
|
<img src="./bugbuster.png" />
|
||||||
|
|
||||||
|
## Linear
|
||||||
|
|
||||||
|
Once connected to your Linear workspace, the bot comments on newly created or updated issues when linting errors are detected, and automatically resolves those comments once the linting errors are fixed.
|
||||||
|
|
||||||
|
The following linting rules are applied (among others):
|
||||||
|
|
||||||
|
- An issue must have a title, description, priority, and estimate
|
||||||
|
- An issue must be linked to a project or have a goal tag
|
||||||
|
- If an issue is in the _Blocked_ state, it must have either a blocking reason tag or a blocking issue
|
||||||
|
- If an issue is in the _In Progress_ state, it must have an assignee
|
||||||
|
|
||||||
|
## GitHub
|
||||||
|
|
||||||
|
Once connected to your GitHub repository, the bot detects new issues and automatically creates a corresponding Linear issue in the Triage state, including all relevant information.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CommandProcessor } from './services/command-processor'
|
||||||
|
import { IssueProcessor } from './services/issue-processor'
|
||||||
|
import { IssueStateChecker } from './services/issue-state-checker'
|
||||||
|
import { RecentlyLintedManager } from './services/recently-linted-manager'
|
||||||
|
import { StateService } from './services/state-service'
|
||||||
|
import { TeamsManager } from './services/teams-manager'
|
||||||
|
import * as types from './types'
|
||||||
|
import * as utils from './utils'
|
||||||
|
|
||||||
|
export const bootstrap = (props: types.CommonHandlerProps) => {
|
||||||
|
const { client, logger, ctx } = props
|
||||||
|
const botpress = utils.botpress.BotpressApi.create(props)
|
||||||
|
|
||||||
|
const linear = utils.linear.LinearApi.create(client)
|
||||||
|
const stateService = new StateService(linear)
|
||||||
|
const teamsManager = new TeamsManager(linear, client, ctx.botId)
|
||||||
|
const recentlyLintedManager = new RecentlyLintedManager(linear)
|
||||||
|
const issueProcessor = new IssueProcessor(logger, linear, stateService, teamsManager, ctx.botId)
|
||||||
|
const issueStateChecker = new IssueStateChecker(linear, stateService, logger, ctx.botId)
|
||||||
|
const commandProcessor = new CommandProcessor(client, teamsManager, ctx.botId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
botpress,
|
||||||
|
linear,
|
||||||
|
stateService,
|
||||||
|
teamsManager,
|
||||||
|
recentlyLintedManager,
|
||||||
|
issueProcessor,
|
||||||
|
issueStateChecker,
|
||||||
|
commandProcessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
const TEAM_NAME_FOR_NEW_ISSUES = 'Engineering'
|
||||||
|
|
||||||
|
export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = async (props): Promise<void> => {
|
||||||
|
const githubIssue = props.event.payload
|
||||||
|
|
||||||
|
props.logger.info('Received GitHub issue', githubIssue)
|
||||||
|
|
||||||
|
const { botpress, linear } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
const _handleError =
|
||||||
|
(context: string) =>
|
||||||
|
(thrown: unknown): Promise<never> =>
|
||||||
|
botpress.handleError({ context, conversationId: undefined }, thrown)
|
||||||
|
|
||||||
|
const linearResponse = await props.client
|
||||||
|
.callAction({
|
||||||
|
type: 'linear:createIssue',
|
||||||
|
input: {
|
||||||
|
teamName: TEAM_NAME_FOR_NEW_ISSUES,
|
||||||
|
description: githubIssue.issue.body,
|
||||||
|
title: githubIssue.issue.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to create a Linear issue from the GitHub issue'))
|
||||||
|
|
||||||
|
const comment = [
|
||||||
|
'This issue was created from GitHub by BugBuster Bot.',
|
||||||
|
'',
|
||||||
|
`GitHub Issue: [${githubIssue.issue.name}](${githubIssue.issue.url})`,
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
await linear
|
||||||
|
.createComment({
|
||||||
|
body: comment,
|
||||||
|
issueId: linearResponse.output.issue.id,
|
||||||
|
botId: props.ctx.botId,
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to create a comment on the Linear issue created from GitHub'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './github-issue-opened'
|
||||||
|
export * from './linear-issue-updated'
|
||||||
|
export * from './linear-issue-created'
|
||||||
|
export * from './message-created'
|
||||||
|
export * from './lint-all'
|
||||||
|
export * from './time-to-lint-all'
|
||||||
|
export * from './time-to-check-issues-state'
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export const handleLinearIssueCreated: bp.EventHandlers['linear:issueCreated'] = async (props) => {
|
||||||
|
const { event } = props
|
||||||
|
const { number: issueNumber, teamKey } = event.payload
|
||||||
|
|
||||||
|
const { botpress, issueProcessor } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)
|
||||||
|
|
||||||
|
props.logger.info('Linear issue created event received', `${teamKey}-${issueNumber}`)
|
||||||
|
const issue = await issueProcessor
|
||||||
|
.findIssue(issueNumber, teamKey)
|
||||||
|
.catch(_handleError('trying to find the created Linear issue'))
|
||||||
|
|
||||||
|
if (!issue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await issueProcessor.lintIssue(issue).catch(_handleError('trying to lint the created Linear issue'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] = async (props) => {
|
||||||
|
const { event, logger } = props
|
||||||
|
const { number: issueNumber, teamKey } = event.payload
|
||||||
|
|
||||||
|
const { botpress, issueProcessor, recentlyLintedManager } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)
|
||||||
|
logger.info('Linear issue updated event received', `${teamKey}-${issueNumber}`)
|
||||||
|
const issue = await issueProcessor
|
||||||
|
.findIssue(issueNumber, teamKey)
|
||||||
|
.catch(_handleError('trying to find the updated Linear issue'))
|
||||||
|
|
||||||
|
if (!issue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecentlyLinted = await recentlyLintedManager
|
||||||
|
.isRecentlyLinted(issue)
|
||||||
|
.catch(_handleError('trying to get recently linted issues'))
|
||||||
|
|
||||||
|
await issueProcessor.lintIssue(issue, isRecentlyLinted).catch(_handleError('trying to lint the updated Linear issue'))
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import * as types from 'src/types'
|
||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
const LINEAR_ISSUE_BASE_URL = 'https://linear.app/botpress/issue/'
|
||||||
|
|
||||||
|
export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => {
|
||||||
|
const { client, workflow, ctx, conversation } = props
|
||||||
|
|
||||||
|
const verbose = workflow.input.verbose ?? false
|
||||||
|
const comment = workflow.input.comment ?? true
|
||||||
|
|
||||||
|
const { botpress, issueProcessor } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { id: lastLintedId },
|
||||||
|
},
|
||||||
|
} = await client
|
||||||
|
.getOrSetState({
|
||||||
|
id: workflow.id,
|
||||||
|
name: 'lastLintedId',
|
||||||
|
type: 'workflow',
|
||||||
|
payload: {},
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to get last linted issue ID'))
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { issues: lintResults },
|
||||||
|
},
|
||||||
|
} = await client
|
||||||
|
.getOrSetState({
|
||||||
|
id: workflow.id,
|
||||||
|
name: 'lintResults',
|
||||||
|
type: 'workflow',
|
||||||
|
payload: { issues: [] },
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to get previous lint results'))
|
||||||
|
|
||||||
|
let hasNextPage = false
|
||||||
|
let endCursor: string | undefined = lastLintedId
|
||||||
|
do {
|
||||||
|
const pagedIssues = await issueProcessor
|
||||||
|
.listRelevantIssues(endCursor)
|
||||||
|
.catch(_handleError('trying to list all issues'))
|
||||||
|
|
||||||
|
const pageResults: types.LintResult[] = []
|
||||||
|
for (const issue of pagedIssues.issues) {
|
||||||
|
const lintResult = await issueProcessor
|
||||||
|
.lintIssue(issue, undefined, { comment })
|
||||||
|
.catch(_handleError(`trying to lint issue ${issue.identifier}`))
|
||||||
|
lintResults.push(lintResult)
|
||||||
|
pageResults.push(lintResult)
|
||||||
|
|
||||||
|
await workflow.acknowledgeStartOfProcessing().catch(_handleError('trying to acknowledge start of processing'))
|
||||||
|
|
||||||
|
endCursor = issue.id
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client
|
||||||
|
.setState({
|
||||||
|
id: workflow.id,
|
||||||
|
name: 'lastLintedId',
|
||||||
|
type: 'workflow',
|
||||||
|
payload: { id: endCursor },
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to update last linted issue ID')),
|
||||||
|
client
|
||||||
|
.setState({
|
||||||
|
id: workflow.id,
|
||||||
|
name: 'lintResults',
|
||||||
|
type: 'workflow',
|
||||||
|
payload: { issues: lintResults },
|
||||||
|
})
|
||||||
|
.catch(_handleError('trying to update lint results')),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPage = pagedIssues.pagination?.hasNextPage ?? false
|
||||||
|
|
||||||
|
if (verbose && conversation?.id) {
|
||||||
|
const failedCount = lintResults.filter((result) => result.result === 'failed').length
|
||||||
|
const newlyFailed = pageResults.filter((result) => result.result === 'failed')
|
||||||
|
|
||||||
|
const progressLine = `Linting... linted ${lintResults.length} issue(s) so far (${failedCount} with errors).`
|
||||||
|
const failedList = newlyFailed.map((result) => `- ${_issueLink(result.identifier)}`).join('\n')
|
||||||
|
const message = newlyFailed.length > 0 ? `${progressLine}\n${failedList}` : progressLine
|
||||||
|
|
||||||
|
await botpress.respondText(conversation.id, message).catch(() => {})
|
||||||
|
}
|
||||||
|
} while (hasNextPage)
|
||||||
|
|
||||||
|
if (conversation?.id) {
|
||||||
|
const message = _buildResultMessage(lintResults)
|
||||||
|
await botpress.respondText(conversation.id, message).catch(() => {})
|
||||||
|
await workflow.setCompleted()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { channels },
|
||||||
|
},
|
||||||
|
} = await client.getOrSetState({
|
||||||
|
id: ctx.botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const channel of channels) {
|
||||||
|
const relevantIssues = lintResults.filter((result) =>
|
||||||
|
channel.teams.some((team) => result.identifier.includes(team))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relevantIssues.length > 0) {
|
||||||
|
await botpress.respondText(channel.conversationId, _buildResultMessage(relevantIssues)).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await workflow.setCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleLintAllTimeout: bp.WorkflowHandlers['lintAll'] = async (props) => {
|
||||||
|
const { conversation } = props
|
||||||
|
const { botpress } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
if (conversation?.id) {
|
||||||
|
await botpress.respondText(conversation.id, "Error: the 'lintAll' operation timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _issueLink = (identifier: string) => `[${identifier}](${LINEAR_ISSUE_BASE_URL + identifier})`
|
||||||
|
|
||||||
|
const _buildResultMessage = (results: types.LintResult[]) => {
|
||||||
|
const failedIssuesLinks = results
|
||||||
|
.filter((result) => result.result === 'failed')
|
||||||
|
.slice(0, 10) // Limit to 10 issues to avoid spamming the message
|
||||||
|
.map((result) => _issueLink(result.identifier))
|
||||||
|
|
||||||
|
let messageDetail = 'No issue contained lint errors.'
|
||||||
|
if (failedIssuesLinks.length === 1) {
|
||||||
|
messageDetail = `This issue contained lint errors: ${failedIssuesLinks[0]}.`
|
||||||
|
} else if (failedIssuesLinks.length > 1) {
|
||||||
|
messageDetail = `These issues contained lint errors: ${failedIssuesLinks.join(', ')}.`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Linting complete. ${messageDetail}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { CommandDefinition } from 'src/types'
|
||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
const MESSAGING_INTEGRATIONS = ['telegram', 'slack']
|
||||||
|
|
||||||
|
export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => {
|
||||||
|
const { conversation, message } = props
|
||||||
|
if (!MESSAGING_INTEGRATIONS.includes(conversation.integration)) {
|
||||||
|
props.logger.info(`Ignoring message from ${conversation.integration}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
conversation.integration === 'slack' &&
|
||||||
|
(conversation.channel === 'channel' || conversation.channel === 'thread')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { botpress, commandProcessor } = boot.bootstrap(props)
|
||||||
|
const commandListMessage = _buildListCommandsMessage(commandProcessor.commandDefinitions)
|
||||||
|
|
||||||
|
if (message.type !== 'text') {
|
||||||
|
await botpress.respondText(conversation.id, commandListMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.payload.text) {
|
||||||
|
await botpress.respondText(conversation.id, commandListMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [command, ...args] = message.payload.text.trim().split(' ')
|
||||||
|
if (!command) {
|
||||||
|
await botpress.respondText(conversation.id, commandListMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandDefinition = commandProcessor.commandDefinitions.find((commandImpl) => commandImpl.name === command)
|
||||||
|
if (!commandDefinition) {
|
||||||
|
await botpress.respondText(conversation.id, commandListMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandDefinition.requiredArgs && args.length < commandDefinition.requiredArgs?.length) {
|
||||||
|
await botpress.respondText(
|
||||||
|
conversation.id,
|
||||||
|
`Error: a minimum of ${commandDefinition.requiredArgs.length} argument(s) is required.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const _handleError = (context: string, thrown: unknown) =>
|
||||||
|
botpress.handleError({ context, conversationId: conversation.id }, thrown)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await commandDefinition.implementation(args, conversation.id)
|
||||||
|
await botpress.respondText(conversation.id, `${result.success ? '' : 'Error: '}${result.message}`)
|
||||||
|
} catch (thrown) {
|
||||||
|
await _handleError(`trying to run ${commandDefinition.name}`, thrown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _buildListCommandsMessage = (definitions: CommandDefinition[]) => {
|
||||||
|
const commands = definitions.map(_buildCommandMessage).join('\n')
|
||||||
|
return `Unknown command. Here's a list of possible commands:\n${commands}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const _buildCommandMessage = (definition: CommandDefinition) => {
|
||||||
|
const requiredArgs = definition.requiredArgs?.map((arg) => `<${arg}>`).join(' ')
|
||||||
|
const optionalArgs = definition.optionalArgs?.map((arg) => `[${arg}]`).join(' ')
|
||||||
|
|
||||||
|
return `${definition.name} ${requiredArgs ?? ''} ${optionalArgs ?? ''}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import * as types from 'src/types'
|
||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
const statesToProcess: types.StateAttributes[] = [
|
||||||
|
{
|
||||||
|
commonStateName: 'STAGING',
|
||||||
|
maxTimeSinceLastUpdate: '-P1W',
|
||||||
|
warningComment: 'BugBuster bot detected that this issue has been in staging for over a week',
|
||||||
|
buildWarningReason: (issueIdentifier) => `Issue ${issueIdentifier} has been in staging for over a week`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commonStateName: 'BLOCKED',
|
||||||
|
maxTimeSinceLastUpdate: '-P1M',
|
||||||
|
warningComment: 'BugBuster bot detected that this issue has been blocked for over a month',
|
||||||
|
buildWarningReason: (issueIdentifier) => `Issue ${issueIdentifier} has been blocked for over a month`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const handleTimeToCheckIssuesState: bp.EventHandlers['timeToCheckIssuesState'] = async (props) => {
|
||||||
|
const { logger } = props
|
||||||
|
const { botpress, teamsManager, issueStateChecker } = boot.bootstrap(props)
|
||||||
|
const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)
|
||||||
|
|
||||||
|
logger.info("Validating issues' states...")
|
||||||
|
|
||||||
|
const teams = await teamsManager.listWatchedTeams().catch(_handleError('trying to list teams'))
|
||||||
|
|
||||||
|
for (const state of statesToProcess) {
|
||||||
|
await issueStateChecker
|
||||||
|
.processIssues({
|
||||||
|
stateAttributes: state,
|
||||||
|
teams,
|
||||||
|
})
|
||||||
|
.catch(_handleError("trying to check issues' states"))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Finished validating issues' states...")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as boot from '../bootstrap'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export const handleTimeToLintAll: bp.EventHandlers['timeToLintAll'] = async (props) => {
|
||||||
|
const { client, logger } = props
|
||||||
|
logger.info("'timeToLintAll' event received.")
|
||||||
|
|
||||||
|
const { botpress } = boot.bootstrap(props)
|
||||||
|
|
||||||
|
const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown)
|
||||||
|
|
||||||
|
await client
|
||||||
|
.getOrCreateWorkflow({
|
||||||
|
name: 'lintAll',
|
||||||
|
input: {},
|
||||||
|
discriminateByStatusGroup: 'active',
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
|
.catch(_handleError("trying to start the 'lintAll' workflow"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import * as handlers from './handlers'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export const bot = new bp.Bot({ actions: {} })
|
||||||
|
|
||||||
|
bot.on.event('github:issueOpened', handlers.handleGithubIssueOpened)
|
||||||
|
bot.on.event('linear:issueUpdated', handlers.handleLinearIssueUpdated)
|
||||||
|
bot.on.event('linear:issueCreated', handlers.handleLinearIssueCreated)
|
||||||
|
bot.on.event('timeToLintAll', handlers.handleTimeToLintAll)
|
||||||
|
bot.on.event('timeToCheckIssuesState', handlers.handleTimeToCheckIssuesState)
|
||||||
|
bot.on.message('*', handlers.handleMessageCreated)
|
||||||
|
|
||||||
|
bot.on.workflowStart('lintAll', handlers.handleLintAll)
|
||||||
|
bot.on.workflowContinue('lintAll', handlers.handleLintAll)
|
||||||
|
bot.on.workflowTimeout('lintAll', handlers.handleLintAllTimeout)
|
||||||
|
|
||||||
|
export default bot
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
import * as types from '../types'
|
||||||
|
import { TeamsManager } from './teams-manager'
|
||||||
|
import { Client } from '.botpress'
|
||||||
|
|
||||||
|
const MISSING_ARGS_ERROR = 'More arguments are required with this command.'
|
||||||
|
|
||||||
|
export class CommandProcessor {
|
||||||
|
public constructor(
|
||||||
|
private _client: Client,
|
||||||
|
private _teamsManager: TeamsManager,
|
||||||
|
private _botId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private _listTeams: types.CommandImplementation = async () => {
|
||||||
|
const teams = await this._teamsManager.listWatchedTeams()
|
||||||
|
const message = teams.length > 0 ? teams.join(', ') : 'You have no watched teams.'
|
||||||
|
return { success: true, message }
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addTeam: types.CommandImplementation = async ([team]: string[]) => {
|
||||||
|
if (!team) {
|
||||||
|
return { success: false, message: MISSING_ARGS_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._teamsManager.addWatchedTeam(team)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Success: the team with the key '${team}' has been added to the watched team list.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeTeam: types.CommandImplementation = async ([team]: string[]) => {
|
||||||
|
if (!team) {
|
||||||
|
return { success: false, message: MISSING_ARGS_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._teamsManager.removeWatchedTeam(team)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Success: the team with the key '${team}' has been removed from the watched team list.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lintAll: types.CommandImplementation = async (args: string[], conversationId: string) => {
|
||||||
|
let verbose = false
|
||||||
|
let comment = true
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg === '--verbose') {
|
||||||
|
verbose = true
|
||||||
|
} else if (arg === '--no-comments') {
|
||||||
|
comment = false
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Unknown argument '${arg}'. Supported arguments are '--verbose' and '--no-comments'.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._client.getOrCreateWorkflow({
|
||||||
|
name: 'lintAll',
|
||||||
|
input: { verbose, comment },
|
||||||
|
discriminateByStatusGroup: 'active',
|
||||||
|
conversationId,
|
||||||
|
status: 'pending',
|
||||||
|
})
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
verbose ? 'detailed lint results will be posted here' : null,
|
||||||
|
comment ? null : 'issues will not be commented on Linear',
|
||||||
|
].filter((detail) => detail !== null)
|
||||||
|
|
||||||
|
const message =
|
||||||
|
details.length > 0 ? `Launched 'lintAll' workflow: ${details.join(', ')}.` : "Launched 'lintAll' workflow."
|
||||||
|
|
||||||
|
return { success: true, message }
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addNotifChannel: types.CommandImplementation = async ([channelToAdd, ...teams]: string[]) => {
|
||||||
|
if (!channelToAdd || !teams[0]) {
|
||||||
|
return { success: false, message: MISSING_ARGS_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { channels },
|
||||||
|
},
|
||||||
|
} = await this._client.getOrSetState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchedTeams = await this._teamsManager.listWatchedTeams()
|
||||||
|
if (!teams.every((team) => watchedTeams.includes(team))) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'make sure every team you want to add is being watched.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingChannel = channels.find((channel) => channel.name === channelToAdd)
|
||||||
|
if (!existingChannel) {
|
||||||
|
const channelId = await this._resolveChannelId(channelToAdd)
|
||||||
|
if (!channelId) {
|
||||||
|
return { success: false, message: `Could not find a Slack channel matching '${channelToAdd}'.` }
|
||||||
|
}
|
||||||
|
const conversation = await this._client.callAction({
|
||||||
|
type: 'slack:getOrCreateChannelConversation',
|
||||||
|
input: { conversation: { channelId } },
|
||||||
|
})
|
||||||
|
channels.push({ conversationId: conversation.output.conversationId, name: channelToAdd, teams })
|
||||||
|
} else {
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (!existingChannel.teams.includes(team)) {
|
||||||
|
existingChannel.teams.push(team)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._client.setState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Notifications for team(s) ${teams.join(', ')} will be posted in channel ${channelToAdd}.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _resolveChannelId(input: string): Promise<string | undefined> {
|
||||||
|
const { output } = await this._client.callAction({
|
||||||
|
type: 'slack:findTarget',
|
||||||
|
input: { query: input, channel: 'channel' },
|
||||||
|
})
|
||||||
|
const exact = output.targets.find((t) => t.displayName === input)
|
||||||
|
const match = exact ?? output.targets[0]
|
||||||
|
return match?.tags.id
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeNotifChannel: types.CommandImplementation = async ([channelToRemove]: string[]) => {
|
||||||
|
if (!channelToRemove) {
|
||||||
|
return { success: false, message: MISSING_ARGS_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { channels },
|
||||||
|
},
|
||||||
|
} = await this._client.getOrSetState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!channels.find((channel) => channel.name === channelToRemove)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `channel '${channelToRemove}' is not part of the notification channels.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._client.setState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: channels.filter((channel) => channel.name !== channelToRemove) },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Notification channel ${channelToRemove} has been removed.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeNotifChannelTeam: types.CommandImplementation = async ([channelToRemove, teamToRemove]: string[]) => {
|
||||||
|
if (!channelToRemove || !teamToRemove) {
|
||||||
|
return { success: false, message: MISSING_ARGS_ERROR }
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { channels },
|
||||||
|
},
|
||||||
|
} = await this._client.getOrSetState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const channel = channels.find((channel) => channel.name === channelToRemove)
|
||||||
|
if (!channel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `channel '${channelToRemove}' is not part of the notification channels.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.teams.find((team) => team === teamToRemove)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `channel ${channel.name} does not receive notifications from team '${teamToRemove}'.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.teams = channel.teams.filter((team) => team !== teamToRemove)
|
||||||
|
|
||||||
|
await this._client.setState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Team ${teamToRemove} has been removed from channel ${channelToRemove}.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listNotifChannels: types.CommandImplementation = async () => {
|
||||||
|
const {
|
||||||
|
state: {
|
||||||
|
payload: { channels },
|
||||||
|
},
|
||||||
|
} = await this._client.getOrSetState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'notificationChannels',
|
||||||
|
type: 'bot',
|
||||||
|
payload: { channels: [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
let message = 'There is no set Slack notification channel.'
|
||||||
|
if (channels.length > 0) {
|
||||||
|
message = this._buildNotifChannelsMessage(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildNotifChannelsMessage(channels: { name: string; teams: string[] }[]) {
|
||||||
|
return `The Slack notification channels are:\n${channels.map(this._getMessageForChannel).join('\n')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getMessageForChannel(channel: { name: string; teams: string[] }) {
|
||||||
|
const { name, teams } = channel
|
||||||
|
return `- channel ${name} for team(s) ${teams.join(', ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public commandDefinitions: types.CommandDefinition[] = [
|
||||||
|
{
|
||||||
|
name: '#listTeams',
|
||||||
|
implementation: this._listTeams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#addTeam',
|
||||||
|
implementation: this._addTeam,
|
||||||
|
requiredArgs: ['teamName'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#removeTeam',
|
||||||
|
implementation: this._removeTeam,
|
||||||
|
requiredArgs: ['teamName'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#lintAll',
|
||||||
|
implementation: this._lintAll,
|
||||||
|
optionalArgs: ['--verbose', '--no-comments'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#addNotifChannel',
|
||||||
|
implementation: this._addNotifChannel,
|
||||||
|
requiredArgs: ['channelName', 'teamName1'],
|
||||||
|
optionalArgs: ['teamName2 ...'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#removeNotifChannel',
|
||||||
|
implementation: this._removeNotifChannel,
|
||||||
|
requiredArgs: ['channelName'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#removeNotifChannelTeam',
|
||||||
|
implementation: this._removeNotifChannelTeam,
|
||||||
|
requiredArgs: ['channelName', 'teamName'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#listNotifChannels',
|
||||||
|
implementation: this._listNotifChannels,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import * as types from '../../types'
|
||||||
|
import * as lin from '../../utils/linear-utils'
|
||||||
|
import { StateService } from '../state-service'
|
||||||
|
import * as tm from '../teams-manager'
|
||||||
|
import { lintIssue } from './lint-issue'
|
||||||
|
|
||||||
|
const IGNORED_STATES: types.CommonStateName[] = ['TRIAGE', 'BACKLOG', 'DONE', 'CANCELED', 'STALE', 'DUPLICATE']
|
||||||
|
const LINTIGNORE_LABEL_NAME = 'lintignore'
|
||||||
|
|
||||||
|
export class IssueProcessor {
|
||||||
|
public constructor(
|
||||||
|
private _logger: sdk.BotLogger,
|
||||||
|
private _linear: lin.LinearApi,
|
||||||
|
private _stateService: StateService,
|
||||||
|
private _teamsManager: tm.TeamsManager,
|
||||||
|
private _botId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The corresponding issue, or `undefined` if the issue is not found or not valid.
|
||||||
|
*/
|
||||||
|
public async findIssue(issueNumber: number, teamKey: string | undefined): Promise<lin.Issue | undefined> {
|
||||||
|
if (!issueNumber || !teamKey) {
|
||||||
|
this._logger.error('Missing issueNumber or teamKey in event payload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const watchedTeams = await this._teamsManager.listWatchedTeams()
|
||||||
|
if (!(await this._linear.isTeam(teamKey)) || !watchedTeams.includes(teamKey)) {
|
||||||
|
this._logger.info(`Ignoring issue of team "${teamKey}"`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const issue = await this._linear.findIssue({ teamKey, issueNumber })
|
||||||
|
if (!issue) {
|
||||||
|
this._logger.warn(`Issue with number ${issueNumber} not found in team ${teamKey}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listRelevantIssues(endCursor?: string): Promise<{ issues: lin.Issue[]; pagination?: lin.Pagination }> {
|
||||||
|
const watchedTeams = await this._teamsManager.listWatchedTeams()
|
||||||
|
if (watchedTeams.length === 0) {
|
||||||
|
throw new Error('You have no watched teams.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateIdsToOmit = await this._stateService.mapToStateIds(IGNORED_STATES)
|
||||||
|
|
||||||
|
return await this._linear.listIssues(
|
||||||
|
{
|
||||||
|
teamKeys: watchedTeams,
|
||||||
|
stateIdsToOmit,
|
||||||
|
},
|
||||||
|
endCursor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async lintIssue(
|
||||||
|
issue: lin.Issue,
|
||||||
|
isRecentlyLinted?: boolean,
|
||||||
|
options?: { comment?: boolean }
|
||||||
|
): Promise<types.LintResult> {
|
||||||
|
const shouldComment = options?.comment ?? true
|
||||||
|
const state = await this._stateService.getIssueCommonStateName(issue)
|
||||||
|
if (!state) {
|
||||||
|
this._logger.warn(
|
||||||
|
`Issue ${issue.identifier} has an unknown state ${issue.state.name}. Ignoring linting for this issue.`
|
||||||
|
)
|
||||||
|
return { identifier: issue.identifier, result: 'ignored' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IGNORED_STATES.includes(state) || issue.labels.nodes.some((label) => label.name === LINTIGNORE_LABEL_NAME)) {
|
||||||
|
return { identifier: issue.identifier, result: 'ignored' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = lintIssue(issue, state)
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
this._logger.info(`Issue ${issue.identifier} passed all lint checks.`)
|
||||||
|
await this._linear.resolveComments(issue)
|
||||||
|
return { identifier: issue.identifier, result: 'succeeded' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const warningMessage = `Issue ${issue.identifier} has ${errors.length} lint errors.`
|
||||||
|
if (isRecentlyLinted) {
|
||||||
|
this._logger.warn(`${warningMessage} Not commenting the issue because it has been linted recently.`)
|
||||||
|
return { identifier: issue.identifier, result: 'succeeded' }
|
||||||
|
}
|
||||||
|
|
||||||
|
this._logger.warn(warningMessage)
|
||||||
|
|
||||||
|
if (shouldComment) {
|
||||||
|
await this._linear.createComment({
|
||||||
|
issueId: issue.id,
|
||||||
|
botId: this._botId,
|
||||||
|
body: [
|
||||||
|
`BugBuster Bot found the following problems with ${issue.identifier}:`,
|
||||||
|
'',
|
||||||
|
...errors.map((error) => `- ${error.message}`),
|
||||||
|
].join('\n'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { identifier: issue.identifier, messages: errors.map((error) => error.message), result: 'failed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { test, assert } from 'vitest'
|
||||||
|
import { isIssueTitleFormatValid } from './issue-title-format-validator'
|
||||||
|
|
||||||
|
test('assert that a title containing only words is valid', async () => {
|
||||||
|
const title = 'any string'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets not on the first or second word is valid', async () => {
|
||||||
|
const title = 'any string [abc]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one opening square bracket on the first word is valid', async () => {
|
||||||
|
const title = '[abc'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one closing square bracket on the first word is valid', async () => {
|
||||||
|
const title = 'abc]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with and one opening square bracket on the second word is valid', async () => {
|
||||||
|
const title = 'abc[def'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one closing square bracket on the second word is valid', async () => {
|
||||||
|
const title = 'abc def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesiss not on the first or second word is valid', async () => {
|
||||||
|
const title = 'any string (abc)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one opening parenthesis on the first word is valid', async () => {
|
||||||
|
const title = '(abc'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one closing parenthesis on the first word is valid', async () => {
|
||||||
|
const title = 'abc)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one opening parenthesis on the second word is valid', async () => {
|
||||||
|
const title = 'abc(def'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one closing parenthesis on the second word is valid', async () => {
|
||||||
|
const title = 'abc def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with one closing parenthesis on the second word is valid', async () => {
|
||||||
|
const title = 'abc def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isTrue(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the first word is invalid', async () => {
|
||||||
|
const title = '[abc] def'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the second word is invalid', async () => {
|
||||||
|
const title = 'abc [def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the second word with no space between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc[def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the second word with multiple spaces between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc [def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the second word with multiple spaces between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc [def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with square brackets on the first and second words is invalid', async () => {
|
||||||
|
const title = '[abc def]'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the first word is invalid', async () => {
|
||||||
|
const title = '(abc) def'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the second word is invalid', async () => {
|
||||||
|
const title = 'abc (def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the second word with no space between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc(def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the second word with multiple spaces between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc (def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the second word with multiple spaces between the first and second word is invalid', async () => {
|
||||||
|
const title = 'abc (def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assert that a title with parenthesis on the first and second words is invalid', async () => {
|
||||||
|
const title = '(abc def)'
|
||||||
|
|
||||||
|
const actual = isIssueTitleFormatValid(title)
|
||||||
|
|
||||||
|
assert.isFalse(actual)
|
||||||
|
})
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const PATTERN = /^\w{0,} {0,}((\[.{1,}\])|(\(.{1,}\)))/
|
||||||
|
|
||||||
|
export function isIssueTitleFormatValid(title: string) {
|
||||||
|
return !title.match(PATTERN)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import * as types from '../../types'
|
||||||
|
import * as lin from '../../utils/linear-utils'
|
||||||
|
import { isIssueTitleFormatValid } from './issue-title-format-validator'
|
||||||
|
|
||||||
|
export type IssueLint = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lintIssue = (issue: lin.Issue, state: types.CommonStateName): IssueLint[] => {
|
||||||
|
const lints: string[] = []
|
||||||
|
|
||||||
|
if (!_hasLabelOfCategory(issue, 'type')) {
|
||||||
|
lints.push(`Issue ${issue.identifier} is missing a type label.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBlockedLabel = _hasLabelOfCategory(issue, 'blocked')
|
||||||
|
const hasBlockedRelation = issue.inverseRelations.nodes.some((relation) => relation.type === 'blocks')
|
||||||
|
|
||||||
|
if (state === 'BLOCKED' && !issue.assignee && !hasBlockedRelation) {
|
||||||
|
lints.push(`Issue ${issue.identifier} is blocked but has no assignee.`)
|
||||||
|
}
|
||||||
|
if (state === 'BLOCKED' && !hasBlockedLabel && !hasBlockedRelation) {
|
||||||
|
lints.push(`Issue ${issue.identifier} is blocked but missing a "blocked" label or a blocking issue.`)
|
||||||
|
}
|
||||||
|
if (state === 'BACKLOG' && issue.assignee) {
|
||||||
|
lints.push(`Issue ${issue.identifier} has an assignee but is still in the backlog.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasArea = issue.labels.nodes.some((label) => label.name.startsWith('area/'))
|
||||||
|
if (!hasArea) {
|
||||||
|
lints.push(`Issue ${issue.identifier} is missing an "area/" label.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issue.priority) {
|
||||||
|
lints.push(`Issue ${issue.identifier} is missing a priority.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issue.estimate === null && state !== 'BLOCKED') {
|
||||||
|
// blocked issues can be unestimated
|
||||||
|
lints.push(`Issue ${issue.identifier} is missing an estimate.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueIsEpic = _hasLabel({ issue, label: 'epic', category: 'type' })
|
||||||
|
if (issue.estimate && issue.estimate > 8 && !issueIsEpic) {
|
||||||
|
lints.push(
|
||||||
|
`Issue ${issue.identifier} has an estimate greater than 8 (${issue.estimate}). Consider breaking it down or making it an epic.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIssueTitleFormatValid(issue.title)) {
|
||||||
|
lints.push(
|
||||||
|
`Issue ${issue.identifier} has unconventional commit syntax in the title. Issue title should not attempt to follow a formal syntax.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueProject = issue.project
|
||||||
|
if (issueProject && issueProject.completedAt) {
|
||||||
|
lints.push(
|
||||||
|
`Issue ${issue.identifier} is associated with a completed project (${issueProject.name}). Consider removing the project association.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lints.map((message) => ({ message }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const _hasLabelOfCategory = (issue: lin.Issue, category: string) => {
|
||||||
|
return issue.labels.nodes.some((label) => label.parent?.name === category)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _hasLabel = (props: { issue: lin.Issue; label: string; category?: string }) => {
|
||||||
|
const { issue, label, category } = props
|
||||||
|
return issue.labels.nodes.some((l) => l.name === label && (category ? l.parent?.name === category : true))
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import * as types from '../types'
|
||||||
|
import * as lin from '../utils/linear-utils'
|
||||||
|
import * as sts from './state-service'
|
||||||
|
|
||||||
|
export class IssueStateChecker {
|
||||||
|
public constructor(
|
||||||
|
private _linear: lin.LinearApi,
|
||||||
|
private _stateService: sts.StateService,
|
||||||
|
private _logger: sdk.BotLogger,
|
||||||
|
private _botId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async processIssues(props: { stateAttributes: types.StateAttributes; teams: string[] }) {
|
||||||
|
const { stateAttributes, teams } = props
|
||||||
|
|
||||||
|
const stateIdsToInclude = await this._stateService.mapToStateIds([stateAttributes.commonStateName])
|
||||||
|
|
||||||
|
let hasNextPage = false
|
||||||
|
let endCursor: string | undefined = undefined
|
||||||
|
do {
|
||||||
|
const { issues, pagination } = await this._linear.listIssues(
|
||||||
|
{
|
||||||
|
teamKeys: teams,
|
||||||
|
stateIdsToInclude,
|
||||||
|
updatedBefore: stateAttributes.maxTimeSinceLastUpdate,
|
||||||
|
},
|
||||||
|
endCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const issue of issues) {
|
||||||
|
await this._linear.createComment({
|
||||||
|
issueId: issue.id,
|
||||||
|
botId: this._botId,
|
||||||
|
body: stateAttributes.warningComment,
|
||||||
|
})
|
||||||
|
this._logger.warn(stateAttributes.buildWarningReason(issue.identifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPage = pagination?.hasNextPage ?? false
|
||||||
|
endCursor = pagination?.endCursor
|
||||||
|
} while (hasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as lin from '../utils/linear-utils'
|
||||||
|
|
||||||
|
const RECENT_THRESHOLD: number = 1000 * 60 * 10 // 10 minutes
|
||||||
|
|
||||||
|
export class RecentlyLintedManager {
|
||||||
|
public constructor(private _linear: lin.LinearApi) {}
|
||||||
|
|
||||||
|
public async isRecentlyLinted(issue: lin.Issue): Promise<boolean> {
|
||||||
|
const me = await this._linear.getViewerId()
|
||||||
|
const timestamps = issue.comments.nodes
|
||||||
|
.filter((comment) => comment.user?.id === me)
|
||||||
|
.map((comment) => new Date(comment.createdAt).getTime())
|
||||||
|
const now = new Date().getTime()
|
||||||
|
for (const timestamp of timestamps) {
|
||||||
|
if (now - timestamp < RECENT_THRESHOLD) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import * as types from '../types'
|
||||||
|
import * as lin from '../utils/linear-utils'
|
||||||
|
|
||||||
|
type StateResolver = {
|
||||||
|
byName?: Record<string, types.CommonStateName>
|
||||||
|
default: types.CommonStateName
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMON_STATE_BY_TYPE: Record<types.LinearStateType, StateResolver> = {
|
||||||
|
triage: { default: 'TRIAGE' },
|
||||||
|
backlog: { default: 'BACKLOG' },
|
||||||
|
unstarted: { default: 'TODO' },
|
||||||
|
started: {
|
||||||
|
byName: { staging: 'STAGING', blocked: 'BLOCKED' },
|
||||||
|
default: 'IN_PROGRESS',
|
||||||
|
},
|
||||||
|
completed: { default: 'DONE' },
|
||||||
|
canceled: {
|
||||||
|
byName: { stale: 'STALE' },
|
||||||
|
default: 'CANCELED',
|
||||||
|
},
|
||||||
|
duplicate: {
|
||||||
|
default: 'DUPLICATE',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateEntry = { state: types.LinearState; key?: types.CommonStateName }
|
||||||
|
|
||||||
|
export class StateService {
|
||||||
|
private _stateEntries?: StateEntry[] = undefined
|
||||||
|
|
||||||
|
public constructor(private _linear: lin.LinearApi) {}
|
||||||
|
|
||||||
|
private async _getClassifiedStates(): Promise<StateEntry[]> {
|
||||||
|
if (!this._stateEntries) {
|
||||||
|
const states = await this._linear.getStates()
|
||||||
|
this._stateEntries = states.map((state) => ({ state, key: this._findCommonState(state) }))
|
||||||
|
}
|
||||||
|
return this._stateEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mapToStateIds(keys: types.CommonStateName[]): Promise<string[]> {
|
||||||
|
const states = await this._getClassifiedStates()
|
||||||
|
return keys.flatMap((key) => {
|
||||||
|
const relevantStates = states.filter((state) => state.key === key)
|
||||||
|
const ids = relevantStates.map((state) => state.state.id)
|
||||||
|
return ids
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIssueCommonStateName(issue: lin.Issue): Promise<types.CommonStateName | undefined> {
|
||||||
|
const states = await this._getClassifiedStates()
|
||||||
|
const state = states.find((s) => s.state.id === issue.state.id)
|
||||||
|
if (!state) {
|
||||||
|
throw new Error(`State with ID "${issue.state.id}" not found.`)
|
||||||
|
}
|
||||||
|
return state.key
|
||||||
|
}
|
||||||
|
|
||||||
|
private _findCommonState = (state: types.LinearState): types.CommonStateName | undefined => {
|
||||||
|
const resolver = COMMON_STATE_BY_TYPE[state.type]
|
||||||
|
if (!resolver) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const name = state.name.toLowerCase()
|
||||||
|
return resolver.byName?.[name] ?? resolver.default
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as lin from '../utils/linear-utils'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export class TeamsManager {
|
||||||
|
public constructor(
|
||||||
|
private _linear: lin.LinearApi,
|
||||||
|
private _client: bp.Client,
|
||||||
|
private _botId: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async addWatchedTeam(key: string): Promise<void> {
|
||||||
|
const teamKeys = await this._getWatchedTeams()
|
||||||
|
if (teamKeys.includes(key)) {
|
||||||
|
throw new Error(`The team with the key '${key}' is already being watched.`)
|
||||||
|
}
|
||||||
|
if (!(await this._linear.isTeam(key))) {
|
||||||
|
throw new Error(`The team with the key '${key}' does not exist.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._setWatchedTeams([...teamKeys, key])
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeWatchedTeam(key: string): Promise<void> {
|
||||||
|
const teamKeys = await this._getWatchedTeams()
|
||||||
|
if (!teamKeys.includes(key)) {
|
||||||
|
throw new Error(`The team with the key '${key}' is not currently being watched.`)
|
||||||
|
}
|
||||||
|
await this._setWatchedTeams(teamKeys.filter((team) => team !== key))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listWatchedTeams(): Promise<string[]> {
|
||||||
|
const teamKeys = await this._getWatchedTeams()
|
||||||
|
return teamKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getWatchedTeams = async () => {
|
||||||
|
return (
|
||||||
|
await this._client.getOrSetState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'watchedTeams',
|
||||||
|
type: 'bot',
|
||||||
|
payload: {
|
||||||
|
teamKeys: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).state.payload.teamKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setWatchedTeams = async (teamKeys: string[]) => {
|
||||||
|
await this._client.setState({
|
||||||
|
id: this._botId,
|
||||||
|
name: 'watchedTeams',
|
||||||
|
type: 'bot',
|
||||||
|
payload: {
|
||||||
|
teamKeys,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
export type CommonHandlerProps = bp.WorkflowHandlerProps['lintAll'] | bp.EventHandlerProps | bp.MessageHandlerProps
|
||||||
|
|
||||||
|
export type LintResult =
|
||||||
|
| {
|
||||||
|
identifier: string
|
||||||
|
result: 'failed'
|
||||||
|
messages: string[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
identifier: string
|
||||||
|
result: 'succeeded' | 'ignored'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinearStateType = 'triage' | 'backlog' | 'unstarted' | 'started' | 'completed' | 'canceled' | 'duplicate'
|
||||||
|
export type CommonStateName =
|
||||||
|
| 'IN_PROGRESS'
|
||||||
|
| 'STAGING'
|
||||||
|
| 'DONE'
|
||||||
|
| 'BACKLOG'
|
||||||
|
| 'TODO'
|
||||||
|
| 'TRIAGE'
|
||||||
|
| 'CANCELED'
|
||||||
|
| 'BLOCKED'
|
||||||
|
| 'STALE'
|
||||||
|
| 'DUPLICATE'
|
||||||
|
|
||||||
|
export type StateAttributes = {
|
||||||
|
commonStateName: CommonStateName
|
||||||
|
maxTimeSinceLastUpdate: ISO8601Duration
|
||||||
|
warningComment: string
|
||||||
|
buildWarningReason: (issueIdentifier: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinearTeam = {
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
description?: string | undefined
|
||||||
|
icon?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LinearState = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: LinearStateType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ISO8601Duration = string
|
||||||
|
|
||||||
|
type CommandResult = { success: boolean; message: string }
|
||||||
|
export type CommandImplementation = (args: string[], conversationId: string) => CommandResult | Promise<CommandResult>
|
||||||
|
export type CommandDefinition = {
|
||||||
|
name: string
|
||||||
|
requiredArgs?: string[]
|
||||||
|
optionalArgs?: string[]
|
||||||
|
implementation: CommandImplementation
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import * as types from '../types'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
type BotMessage = Pick<bp.ClientInputs['createMessage'], 'type' | 'payload'>
|
||||||
|
|
||||||
|
type ErrorHandlerProps = {
|
||||||
|
context: string
|
||||||
|
conversationId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BotpressApi {
|
||||||
|
private constructor(
|
||||||
|
private _client: bp.Client,
|
||||||
|
private _botId: string,
|
||||||
|
private _logger: sdk.BotLogger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static create(props: types.CommonHandlerProps): BotpressApi {
|
||||||
|
return new BotpressApi(props.client, props.ctx.botId, props.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async respond(conversationId: string, msg: BotMessage): Promise<void> {
|
||||||
|
await this._client.createMessage({
|
||||||
|
type: msg.type,
|
||||||
|
payload: msg.payload,
|
||||||
|
conversationId,
|
||||||
|
userId: this._botId,
|
||||||
|
tags: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async respondText(conversationId: string, msg: string): Promise<void> {
|
||||||
|
return this.respond(conversationId, {
|
||||||
|
type: 'text',
|
||||||
|
payload: { text: msg },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleError = async (props: ErrorHandlerProps, thrown: unknown): Promise<never> => {
|
||||||
|
const error = thrown instanceof Error ? thrown : new Error(String(thrown))
|
||||||
|
const message = `An error occured while ${props.context}: ${error.message}`
|
||||||
|
this._logger.error(message)
|
||||||
|
if (props.conversationId) {
|
||||||
|
await this.respondText(props.conversationId, message).catch(() => {}) // if this fails, there's nothing we can do
|
||||||
|
}
|
||||||
|
throw new sdk.RuntimeError(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * as linear from './linear-utils'
|
||||||
|
export * as botpress from './botpress-utils'
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import * as types from '../../types'
|
||||||
|
import * as graphql from './graphql-queries'
|
||||||
|
import { Client } from '.botpress'
|
||||||
|
|
||||||
|
const ISSUES_PER_PAGE = 50
|
||||||
|
const STATES_PER_PAGE = 200
|
||||||
|
|
||||||
|
export class LinearApi {
|
||||||
|
private _teams?: types.LinearTeam[] = undefined
|
||||||
|
private _states?: types.LinearState[] = undefined
|
||||||
|
private _viewerId?: string = undefined
|
||||||
|
|
||||||
|
private constructor(private _bpClient: Client) {}
|
||||||
|
|
||||||
|
public static create(bpClient: Client): LinearApi {
|
||||||
|
return new LinearApi(bpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getViewerId(): Promise<string> {
|
||||||
|
if (this._viewerId) {
|
||||||
|
return this._viewerId
|
||||||
|
}
|
||||||
|
const { output: me } = await this._bpClient.callAction({
|
||||||
|
type: 'linear:getUser',
|
||||||
|
input: {},
|
||||||
|
})
|
||||||
|
if (!me) {
|
||||||
|
throw new Error('Viewer not found. Please ensure you are authenticated.')
|
||||||
|
}
|
||||||
|
this._viewerId = me.linearId
|
||||||
|
return this._viewerId
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isTeam(teamKey: string) {
|
||||||
|
return (await this.getTeams()).some((team) => team.key === teamKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findIssue(filter: { teamKey: string; issueNumber: number }): Promise<graphql.Issue | undefined> {
|
||||||
|
const { teamKey, issueNumber } = filter
|
||||||
|
|
||||||
|
const { issues } = await this.listIssues({
|
||||||
|
teamKeys: [teamKey],
|
||||||
|
issueNumber,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [issue] = issues
|
||||||
|
if (!issue) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listIssues(
|
||||||
|
filter: {
|
||||||
|
teamKeys: string[]
|
||||||
|
issueNumber?: number
|
||||||
|
stateIdsToOmit?: string[]
|
||||||
|
stateIdsToInclude?: string[]
|
||||||
|
updatedBefore?: types.ISO8601Duration
|
||||||
|
},
|
||||||
|
nextPage?: string
|
||||||
|
): Promise<{ issues: graphql.Issue[]; pagination?: graphql.Pagination }> {
|
||||||
|
const { teamKeys, issueNumber, stateIdsToOmit, stateIdsToInclude, updatedBefore } = filter
|
||||||
|
|
||||||
|
const teams = await this.getTeams()
|
||||||
|
const teamsExist = teamKeys.every((key) => teams.some((team) => team.key === key))
|
||||||
|
if (!teamsExist) {
|
||||||
|
return { issues: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryInput: graphql.GRAPHQL_QUERIES['listIssues'][graphql.QUERY_INPUT] = {
|
||||||
|
filter: {
|
||||||
|
team: { key: { in: teamKeys } },
|
||||||
|
...(issueNumber && { number: { eq: issueNumber } }),
|
||||||
|
state: {
|
||||||
|
id: {
|
||||||
|
...(stateIdsToOmit && { nin: stateIdsToOmit }),
|
||||||
|
...(stateIdsToInclude && { in: stateIdsToInclude }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(updatedBefore && { updatedAt: { lt: updatedBefore } }),
|
||||||
|
},
|
||||||
|
...(nextPage && { after: nextPage }),
|
||||||
|
first: ISSUES_PER_PAGE,
|
||||||
|
orderBy: 'createdAt',
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this._executeGraphqlQuery('listIssues', queryInput)
|
||||||
|
|
||||||
|
return { issues: data.issues.nodes, pagination: data.issues.pageInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resolveComments(issue: graphql.Issue): Promise<void> {
|
||||||
|
const comments = issue.comments.nodes
|
||||||
|
const me = await this.getViewerId()
|
||||||
|
|
||||||
|
const promises: ReturnType<typeof this._bpClient.callAction<'linear:resolveComment'>>[] = []
|
||||||
|
for (const comment of comments) {
|
||||||
|
if (comment.user?.id === me && !comment.parentId && !comment.resolvedAt) {
|
||||||
|
promises.push(this._bpClient.callAction({ type: 'linear:resolveComment', input: { id: comment.id } }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createComment(props: { body: string; issueId: string; botId: string }): Promise<void> {
|
||||||
|
const { body, issueId, botId } = props
|
||||||
|
const conversation = await this._bpClient.callAction({
|
||||||
|
type: 'linear:getOrCreateIssueConversation',
|
||||||
|
input: {
|
||||||
|
conversation: { id: issueId },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await this._bpClient.createMessage({
|
||||||
|
type: 'text',
|
||||||
|
conversationId: conversation.output.conversationId,
|
||||||
|
payload: { text: body },
|
||||||
|
tags: {},
|
||||||
|
userId: botId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findTeamStates(teamKey: string): Promise<graphql.TeamStates | undefined> {
|
||||||
|
const queryInput: graphql.GRAPHQL_QUERIES['findTeamStates'][graphql.QUERY_INPUT] = {
|
||||||
|
filter: { key: { eq: teamKey } },
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this._executeGraphqlQuery('findTeamStates', queryInput)
|
||||||
|
|
||||||
|
const [team] = data.organization.teams.nodes
|
||||||
|
if (!team) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return team
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTeams(): Promise<types.LinearTeam[]> {
|
||||||
|
if (!this._teams) {
|
||||||
|
this._teams = await this._listAllTeams()
|
||||||
|
}
|
||||||
|
return this._teams
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStates(): Promise<types.LinearState[]> {
|
||||||
|
if (!this._states) {
|
||||||
|
this._states = await this._listAllStates()
|
||||||
|
}
|
||||||
|
return this._states
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listAllTeams = async (): Promise<types.LinearTeam[]> => {
|
||||||
|
const response = await this._bpClient.callAction({ type: 'linear:listTeams', input: {} })
|
||||||
|
return response.output.teams
|
||||||
|
}
|
||||||
|
|
||||||
|
private _listAllStates = async (): Promise<types.LinearState[]> => {
|
||||||
|
// We fetch states via GraphQL rather than the linear:listStates action because the action's
|
||||||
|
// output does not include the state `type`, which we need to normalize states across teams.
|
||||||
|
let states: types.LinearState[] = []
|
||||||
|
let after: string | undefined = undefined
|
||||||
|
|
||||||
|
do {
|
||||||
|
const queryInput: graphql.GRAPHQL_QUERIES['listStates'][graphql.QUERY_INPUT] = {
|
||||||
|
first: STATES_PER_PAGE,
|
||||||
|
...(after && { after }),
|
||||||
|
}
|
||||||
|
const data = await this._executeGraphqlQuery('listStates', queryInput)
|
||||||
|
states = states.concat(data.workflowStates.nodes)
|
||||||
|
after = data.workflowStates.pageInfo.hasNextPage ? data.workflowStates.pageInfo.endCursor : undefined
|
||||||
|
} while (after)
|
||||||
|
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _executeGraphqlQuery<K extends keyof graphql.GRAPHQL_QUERIES>(
|
||||||
|
queryName: K,
|
||||||
|
variables: graphql.GRAPHQL_QUERIES[K][graphql.QUERY_INPUT]
|
||||||
|
): Promise<graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE]> {
|
||||||
|
const params = Object.entries(variables).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}))
|
||||||
|
const result = await this._bpClient.callAction({
|
||||||
|
type: 'linear:sendRawGraphqlQuery',
|
||||||
|
input: {
|
||||||
|
query: graphql.GRAPHQL_QUERIES[queryName].query,
|
||||||
|
parameters: params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result.output.result as graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import * as types from 'src/types'
|
||||||
|
|
||||||
|
const QUERY_INPUT = Symbol('graphqlInputType')
|
||||||
|
const QUERY_RESPONSE = Symbol('graphqlResponseType')
|
||||||
|
|
||||||
|
type GraphQLQuery<TInput, TResponse> = {
|
||||||
|
query: string
|
||||||
|
[QUERY_INPUT]: TInput
|
||||||
|
[QUERY_RESPONSE]: TResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Issue = {
|
||||||
|
id: string
|
||||||
|
identifier: string
|
||||||
|
title: string
|
||||||
|
estimate: number | null
|
||||||
|
priority: number
|
||||||
|
assignee: {
|
||||||
|
id: string
|
||||||
|
} | null
|
||||||
|
state: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
labels: {
|
||||||
|
nodes: {
|
||||||
|
name: string
|
||||||
|
parent: {
|
||||||
|
name: string
|
||||||
|
} | null
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
inverseRelations: {
|
||||||
|
nodes: {
|
||||||
|
type: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
project: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
completedAt: string | null
|
||||||
|
} | null
|
||||||
|
comments: {
|
||||||
|
nodes: {
|
||||||
|
id: string
|
||||||
|
resolvedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
} | null
|
||||||
|
parentId: string | null
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TeamStates = {
|
||||||
|
id: string
|
||||||
|
states: {
|
||||||
|
nodes: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Pagination = {
|
||||||
|
hasNextPage: boolean
|
||||||
|
endCursor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GRAPHQL_QUERIES = {
|
||||||
|
listIssues: {
|
||||||
|
query: `
|
||||||
|
query FindIssue($filter: IssueFilter, $first: Int, $after: String, $orderBy: PaginationOrderBy) {
|
||||||
|
issues(filter: $filter, first: $first, after: $after, orderBy: $orderBy) {
|
||||||
|
nodes {
|
||||||
|
id,
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
estimate,
|
||||||
|
priority,
|
||||||
|
assignee {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
state {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
},
|
||||||
|
labels {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
parent {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inverseRelations {
|
||||||
|
nodes {
|
||||||
|
type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
project {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
completedAt
|
||||||
|
}
|
||||||
|
comments {
|
||||||
|
nodes {
|
||||||
|
id,
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
},
|
||||||
|
parentId,
|
||||||
|
resolvedAt,
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[QUERY_INPUT]: {} as {
|
||||||
|
filter: {
|
||||||
|
team?: { key: { in: string[] } }
|
||||||
|
number?: { eq: number }
|
||||||
|
state?: {
|
||||||
|
id: {
|
||||||
|
nin?: string[]
|
||||||
|
in?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedAt?: {
|
||||||
|
lt: types.ISO8601Duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
after?: string
|
||||||
|
first?: number
|
||||||
|
orderBy?: 'createdAt' | 'updatedAt'
|
||||||
|
},
|
||||||
|
[QUERY_RESPONSE]: {} as {
|
||||||
|
issues: {
|
||||||
|
nodes: Issue[]
|
||||||
|
pageInfo: Pagination
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
listStates: {
|
||||||
|
query: `
|
||||||
|
query ListStates($first: Int, $after: String) {
|
||||||
|
workflowStates(first: $first, after: $after) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
type
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[QUERY_INPUT]: {} as {
|
||||||
|
first?: number
|
||||||
|
after?: string
|
||||||
|
},
|
||||||
|
[QUERY_RESPONSE]: {} as {
|
||||||
|
workflowStates: {
|
||||||
|
nodes: types.LinearState[]
|
||||||
|
pageInfo: Pagination
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findTeamStates: {
|
||||||
|
query: `
|
||||||
|
query GetAllTeams($filter: TeamFilter) {
|
||||||
|
organization {
|
||||||
|
teams(filter: $filter) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
key
|
||||||
|
states {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
[QUERY_INPUT]: {} as {
|
||||||
|
filter: {
|
||||||
|
key?: { eq: string }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[QUERY_RESPONSE]: {} as {
|
||||||
|
organization: {
|
||||||
|
teams: {
|
||||||
|
nodes: {
|
||||||
|
id: string
|
||||||
|
states: {
|
||||||
|
nodes: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, GraphQLQuery<object, object>>
|
||||||
|
|
||||||
|
export type GRAPHQL_QUERIES = typeof GRAPHQL_QUERIES
|
||||||
|
export type QUERY_INPUT = typeof QUERY_INPUT
|
||||||
|
export type QUERY_RESPONSE = typeof QUERY_RESPONSE
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './client'
|
||||||
|
export { Issue, Pagination } from './graphql-queries'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": { "*": ["./*"] },
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": [".botpress/**/*", "src/**/*", "*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import slack from 'bp_modules/slack'
|
||||||
|
|
||||||
|
export default new sdk.BotDefinition({
|
||||||
|
states: {
|
||||||
|
metaApiVersions: {
|
||||||
|
type: 'bot',
|
||||||
|
schema: sdk.z.object({
|
||||||
|
currentGraphApiVersion: sdk.z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("The current Meta's Graph API version")
|
||||||
|
.title('Current Graph API Version'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
timeToCheckApi: {
|
||||||
|
schema: sdk.z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recurringEvents: {
|
||||||
|
timeToCheckApi: {
|
||||||
|
type: 'timeToCheckApi',
|
||||||
|
schedule: { cron: '0 * * * *' },
|
||||||
|
payload: sdk.z.object({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).addIntegration(slack, {
|
||||||
|
enabled: true,
|
||||||
|
configurationType: null,
|
||||||
|
configuration: {
|
||||||
|
typingIndicatorEmoji: false,
|
||||||
|
botName: 'Clog',
|
||||||
|
botAvatarUrl: 'https://files.bpcontent.cloud/2025/06/16/20/20250616204038-BRUW6C2R.svg',
|
||||||
|
replyBehaviour: {
|
||||||
|
location: 'channel',
|
||||||
|
onlyOnBotMention: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import rootConfig from '../../eslint.config.mjs'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...rootConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "@bp-bots/clog",
|
||||||
|
"scripts": {
|
||||||
|
"check:type": "tsc --noEmit",
|
||||||
|
"check:bplint": "bp lint",
|
||||||
|
"build": "bp add -y && bp build"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@botpress/client": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*",
|
||||||
|
"cheerio": "^1.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@botpress/cli": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*",
|
||||||
|
"@botpresshub/slack": "workspace:*"
|
||||||
|
},
|
||||||
|
"bpDependencies": {
|
||||||
|
"slack": "../../integrations/slack"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import * as cheerio from 'cheerio'
|
||||||
|
import * as bp from '.botpress'
|
||||||
|
|
||||||
|
const SLACK_CHANNEL_TO_PING = 'alert-squid'
|
||||||
|
|
||||||
|
const bot = new bp.Bot({
|
||||||
|
actions: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const _resolveSlackChannelId = async (client: bp.Client, channelName: string): Promise<string | undefined> => {
|
||||||
|
const { output } = await client.callAction({
|
||||||
|
type: 'slack:findTarget',
|
||||||
|
input: { query: channelName, channel: 'channel' },
|
||||||
|
})
|
||||||
|
const exact = output.targets.find((t) => t.displayName === channelName)
|
||||||
|
return (exact ?? output.targets[0])?.tags.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const _handleApiChange = async (
|
||||||
|
message: string,
|
||||||
|
newGraphApiVersion: string | undefined,
|
||||||
|
props: bp.EventHandlerProps
|
||||||
|
): Promise<void> => {
|
||||||
|
const { client, logger } = props
|
||||||
|
logger.info(message)
|
||||||
|
const channelId = await _resolveSlackChannelId(client, SLACK_CHANNEL_TO_PING)
|
||||||
|
if (!channelId) {
|
||||||
|
logger.error(`Could not find Slack channel '${SLACK_CHANNEL_TO_PING}'`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const response = await client.callAction({
|
||||||
|
type: 'slack:getOrCreateChannelConversation',
|
||||||
|
input: { conversation: { channelId } },
|
||||||
|
})
|
||||||
|
await client.createMessage({
|
||||||
|
type: 'text',
|
||||||
|
conversationId: response.output.conversationId,
|
||||||
|
tags: {},
|
||||||
|
userId: props.ctx.botId,
|
||||||
|
payload: {
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await client.setState({
|
||||||
|
name: 'metaApiVersions',
|
||||||
|
type: 'bot',
|
||||||
|
id: props.ctx.botId,
|
||||||
|
payload: { currentGraphApiVersion: newGraphApiVersion },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if starts with v, has a number of at least one digit, a dot and a single digit (e.g. v00.0)
|
||||||
|
const versionRegexp: RegExp = /v\d+.\d/i
|
||||||
|
const isVersionString = (s: string): boolean => versionRegexp.test(s)
|
||||||
|
|
||||||
|
bot.on.event('timeToCheckApi', async (props) => {
|
||||||
|
const { client, ctx, logger } = props
|
||||||
|
const { state } = await client.getOrSetState({
|
||||||
|
name: 'metaApiVersions',
|
||||||
|
type: 'bot',
|
||||||
|
id: ctx.botId,
|
||||||
|
payload: { currentGraphApiVersion: undefined },
|
||||||
|
})
|
||||||
|
const currentGraphApiVersion = state.payload.currentGraphApiVersion
|
||||||
|
|
||||||
|
const response = await fetch('https://developers.facebook.com/docs/graph-api/changelog/')
|
||||||
|
const html = await response.text()
|
||||||
|
const selector = cheerio.load(html)
|
||||||
|
const newGraphApiVersion = selector('code').first().text().trim()
|
||||||
|
|
||||||
|
if (!isVersionString(newGraphApiVersion)) {
|
||||||
|
await _handleApiChange(
|
||||||
|
`I failed reading the Meta's API version, I received\n${newGraphApiVersion}`,
|
||||||
|
undefined,
|
||||||
|
props
|
||||||
|
)
|
||||||
|
} else if (currentGraphApiVersion === undefined) {
|
||||||
|
await _handleApiChange(
|
||||||
|
`I'll notify you when Meta's Graph API version will change.\nThe current version is ${newGraphApiVersion}`,
|
||||||
|
newGraphApiVersion,
|
||||||
|
props
|
||||||
|
)
|
||||||
|
} else if (currentGraphApiVersion !== newGraphApiVersion) {
|
||||||
|
await _handleApiChange(`Meta's Graph API version changed to: ${newGraphApiVersion}`, newGraphApiVersion, props)
|
||||||
|
} else {
|
||||||
|
logger.info(`Meta's Graph API version is ${newGraphApiVersion}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default bot
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": { "*": ["./*"] },
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": [".botpress/**/*", "src/**/*", "*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import * as sdk from '@botpress/sdk'
|
||||||
|
import * as genenv from './.genenv'
|
||||||
|
import todoist from './bp_modules/todoist'
|
||||||
|
|
||||||
|
export default new sdk.BotDefinition({
|
||||||
|
configuration: {
|
||||||
|
schema: sdk.z.object({}),
|
||||||
|
},
|
||||||
|
states: {},
|
||||||
|
events: {},
|
||||||
|
recurringEvents: {},
|
||||||
|
user: {},
|
||||||
|
conversation: {},
|
||||||
|
})
|
||||||
|
.addIntegration(todoist, {
|
||||||
|
alias: 'todoist-src',
|
||||||
|
enabled: true,
|
||||||
|
configurationType: 'apiToken',
|
||||||
|
configuration: { apiToken: genenv.TODOIST_SRC_API_TOKEN },
|
||||||
|
})
|
||||||
|
.addIntegration(todoist, {
|
||||||
|
alias: 'todoist-dst',
|
||||||
|
enabled: true,
|
||||||
|
configurationType: 'apiToken',
|
||||||
|
configuration: { apiToken: genenv.TODOIST_DST_API_TOKEN },
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import rootConfig from '../../eslint.config.mjs'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...rootConfig,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@bp-bots/doppel-doer",
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "genenv -o ./.genenv/index.ts -e TODOIST_SRC_API_TOKEN -e TODOIST_DST_API_TOKEN",
|
||||||
|
"check:type": "tsc --noEmit",
|
||||||
|
"check:bplint": "bp lint",
|
||||||
|
"build": "bp add -y && bp build"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@botpress/client": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@botpress/cli": "workspace:*",
|
||||||
|
"@botpress/common": "workspace:*",
|
||||||
|
"@botpress/sdk": "workspace:*",
|
||||||
|
"@botpresshub/todoist": "workspace:*",
|
||||||
|
"@bpinternal/genenv": "0.0.1"
|
||||||
|
},
|
||||||
|
"bpDependencies": {
|
||||||
|
"todoist": "../../integrations/todoist"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user