This commit is contained in:
Julien Tant 2026-05-25 07:00:06 -07:00 committed by GitHub
commit 34b43b09eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1246 changed files with 282391 additions and 0 deletions

557
.github/workflows/playbooks-ci.yml vendored Normal file
View file

@ -0,0 +1,557 @@
name: Playbooks CI
on:
pull_request:
paths:
- "core-plugins/mattermost-plugin-playbooks/**"
push:
branches:
- master
paths:
- "core-plugins/mattermost-plugin-playbooks/**"
schedule:
- cron: "0 03 * * 1-6" # Daily at 03:00 UTC from Monday through Saturday.
defaults:
run:
shell: bash
working-directory: core-plugins/mattermost-plugin-playbooks
env:
TERM: xterm
IS_NOT_FORK: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
jobs:
go:
name: Playbooks / Compute Go Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.calculate.outputs.GO_VERSION }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Calculate version
id: calculate
run: echo GO_VERSION=$(grep -E '^go [0-9]+\.[0-9]+' go.mod | cut -d' ' -f2) >> "${GITHUB_OUTPUT}"
lint:
name: Playbooks / Lint
runs-on: ubuntu-22.04
needs:
- go
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/cache-node-modules
id: cache-node-modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
core-plugins/mattermost-plugin-playbooks/webapp/node_modules
core-plugins/mattermost-plugin-playbooks/e2e-tests/node_modules
key: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
restore-keys: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
- name: ci/setup-webapp-npm-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
env:
NODE_ENV: development
run: |
cd webapp
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/setup-e2e-npm-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
env:
NODE_ENV: development
run: |
cd e2e-tests
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/checking-code-style
run: make check-style
- name: ci/go-tidy
run: go mod tidy -v
- name: ci/check-diff-on-gomod
run: git --no-pager diff --exit-code go.mod go.sum || (echo "Please run \"go mod tidy\" and commit the changes in go.mod and go.sum." && exit 1)
- name: ci/run-make-apply
run: make apply
- name: ci/check-diff-on-generated-manifest-files
run: git --no-pager diff --exit-code *manifest.* || (echo "Please run \"make apply\" and commit the changes in the generated manifests." && exit 1)
- name: ci/run-make-i18n-extract-webapp
run: make i18n-extract-webapp
- name: ci/check-diff-on-webapp-i18n-files
run: git --no-pager diff --exit-code webapp/i18n/en.json || (echo "Please run \"make i18n-extract\" and commit the changes in webapp/i18n/en.json." && exit 1)
- name: ci/run-make-graphql
run: make graphql
- name: ci/check-diff-on-webapp-graphql-generated-files
run: git --no-pager diff --exit-code webapp/src/graphql/generated/ || (echo "Please run \"make graphql\" and commit the changes." && exit 1)
dist:
name: Playbooks / Dist
runs-on: ubuntu-22.04
needs:
- go
- lint
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/build-for-upload
run: make dist
- name: ci/upload-build-artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: |
core-plugins/mattermost-plugin-playbooks/dist/playbooks-*.tar.gz
retention-days: 7 ## No need to keep build artifacts for more than 7 days
- name: ci/ensure-build-on-all-platforms ## Verify builds on all platforms *after* ci/build-for-upload to keep artifact small
run: make dist
dist-fips:
name: Playbooks / Dist FIPS
# Skip FIPS testing for forks, which won't have docker login credentials.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- go
- lint
container:
image: mattermostdevelopment/mattermost-build-server-fips:${{ needs.go.outputs.version }}
credentials:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
env:
FIPS_ENABLED: true
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/build-for-upload
run: make dist
- name: ci/upload-build-artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: |
core-plugins/mattermost-plugin-playbooks/dist/playbooks-*.tar.gz
retention-days: 7 ## No need to keep build artifacts for more than 7 days
- name: ci/ensure-build-on-all-platforms ## Verify builds on all platforms *after* ci/build-for-upload to keep artifact small
run: make dist
upload:
name: Playbooks / Upload
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- dist
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: core-plugins/mattermost-plugin-playbooks/dist
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload dist to S3
run: |
aws s3 cp dist/playbooks-*.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/mattermost-plugin-playbooks/mattermost-plugin-playbooks-${{ steps.short-sha.outputs.sha }}.tar.gz
upload-fips:
name: Playbooks / Upload FIPS
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- dist-fips
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact-fips
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: core-plugins/mattermost-plugin-playbooks/dist-fips
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload dist-fips to S3
run: |
aws s3 cp dist-fips/playbooks-*.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/mattermost-plugin-playbooks/mattermost-plugin-playbooks-fips-${{ steps.short-sha.outputs.sha }}.tar.gz
test:
name: Playbooks / Test
runs-on: ubuntu-22.04
needs:
- lint
- go
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: mmuser
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/test-with-db
uses: ./core-plugins/mattermost-plugin-playbooks/.github/actions/test-with-db
test-fips:
name: Playbooks / Test FIPS
# Skip FIPS testing for forks, which won't have docker login credentials.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- lint
- go
container:
image: mattermostdevelopment/mattermost-build-server-fips:${{ needs.go.outputs.version }}
credentials:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: mmuser
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/test-with-db
uses: ./core-plugins/mattermost-plugin-playbooks/.github/actions/test-with-db
generate-specs:
name: Playbooks / Generate E2E Specs
runs-on: ubuntu-22.04
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/generate-specs
id: generate-specs
uses: ./core-plugins/mattermost-plugin-playbooks/.github/actions/generate-specs
with:
parallelism: 3
directory: core-plugins/mattermost-plugin-playbooks/e2e-tests
search_path: tests/integration
e2e-cypress-tests:
name: Playbooks / E2E Cypress (${{ matrix.runId }})
runs-on: ubuntu-22.04
needs:
- go
- lint
- generate-specs
- dist
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
services:
postgres:
image: postgres:15.3
env:
POSTGRES_USER: mmuser
POSTGRES_PASSWORD: mostest
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
minio:
image: minio/minio:RELEASE.2019-10-11T00-38-09Z
env:
MINIO_ACCESS_KEY: minioaccesskey
MINIO_SECRET_KEY: miniosecretkey
MINIO_SSE_MASTER_KEY: "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574"
inbucket:
image: mattermost/inbucket:release-1.2.0
ports:
- 10080:10080
- 10110:10110
- 10025:10025
elasticsearch:
image: mattermost/mattermost-elasticsearch-docker:7.0.0
env:
http.host: "0.0.0.0"
http.port: 9200
http.cors.enabled: "true"
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
http.cors.allow-credentials: "true"
transport.host: "127.0.0.1"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- 9200:9200
mattermost-server:
image: mattermostdevelopment/mattermost-enterprise-edition:master
env:
DB_HOST: postgres
DB_PORT_NUMBER: 5432
MM_DBNAME: mattermost_test
MM_USERNAME: mmuser
MM_PASSWORD: mostest
CI_INBUCKET_HOST: inbucket
CI_INBUCKET_PORT: 10080
CI_MINIO_HOST: minio
IS_CI: true
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
MM_CLUSTERSETTINGS_READONLYCONFIG: false
MM_EMAILSETTINGS_SMTPSERVER: inbucket
MM_EMAILSETTINGS_SMTPPORT: 10025
MM_ELASTICSEARCHSETTINGS_CONNECTIONURL: http://elasticsearch:9200
MM_EXPERIMENTALSETTINGS_USENEWSAMLLIBRARY: true
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10"
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_PLUGINSETTINGS_ENABLEUPLOADS: true
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: true
MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: false
MM_SERVICESETTINGS_ENABLELEGACYSIDEBAR: true
MM_TEAMSETTINGS_MAXUSERSPERTEAM: 10000
MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW: false
MM_SERVICEENVIRONMENT: test
MM_SERVICESETTINGS_EXPERIMENTALSTRICTCSRFENFORCEMENT: false
MM_SERVICESETTINGS_STRICTCSRFENFORCEMENT: false
ports:
- 8065:8065
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: core-plugins/mattermost-plugin-playbooks/dist
- name: ci/e2e-test
uses: ./core-plugins/mattermost-plugin-playbooks/.github/actions/e2e-test
with:
CYPRESS_serverEdition: E20
SPECS: ${{ matrix.specs }}
e2e-cypress-tests-fips:
name: Playbooks / E2E Cypress FIPS (${{ matrix.runId }})
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- go
- lint
- generate-specs
- dist-fips
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
services:
postgres:
image: postgres:15.3
env:
POSTGRES_USER: mmuser
POSTGRES_PASSWORD: mostest
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
minio:
image: minio/minio:RELEASE.2019-10-11T00-38-09Z
env:
MINIO_ACCESS_KEY: minioaccesskey
MINIO_SECRET_KEY: miniosecretkey
MINIO_SSE_MASTER_KEY: "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574"
inbucket:
image: mattermost/inbucket:release-1.2.0
ports:
- 10080:10080
- 10110:10110
- 10025:10025
elasticsearch:
image: mattermost/mattermost-elasticsearch-docker:7.0.0
env:
http.host: "0.0.0.0"
http.port: 9200
http.cors.enabled: "true"
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
http.cors.allow-credentials: "true"
transport.host: "127.0.0.1"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- 9200:9200
mattermost-server:
image: mattermostdevelopment/mattermost-enterprise-fips-edition:master
env:
DB_HOST: postgres
DB_PORT_NUMBER: 5432
MM_DBNAME: mattermost_test
MM_USERNAME: mmuser
MM_PASSWORD: mostest
CI_INBUCKET_HOST: inbucket
CI_INBUCKET_PORT: 10080
CI_MINIO_HOST: minio
IS_CI: true
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
MM_CLUSTERSETTINGS_READONLYCONFIG: false
MM_EMAILSETTINGS_SMTPSERVER: inbucket
MM_EMAILSETTINGS_SMTPPORT: 10025
MM_ELASTICSEARCHSETTINGS_CONNECTIONURL: http://elasticsearch:9200
MM_EXPERIMENTALSETTINGS_USENEWSAMLLIBRARY: true
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10"
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_PLUGINSETTINGS_ENABLEUPLOADS: true
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: true
MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: false
MM_SERVICESETTINGS_ENABLELEGACYSIDEBAR: true
MM_TEAMSETTINGS_MAXUSERSPERTEAM: 10000
MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW: false
MM_SERVICEENVIRONMENT: test
MM_SERVICESETTINGS_EXPERIMENTALSTRICTCSRFENFORCEMENT: false
MM_SERVICESETTINGS_STRICTCSRFENFORCEMENT: false
ports:
- 8065:8065
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: core-plugins/mattermost-plugin-playbooks/dist
- name: ci/e2e-test
uses: ./core-plugins/mattermost-plugin-playbooks/.github/actions/e2e-test
with:
CYPRESS_serverEdition: E20
SPECS: ${{ matrix.specs }}
ARTIFACT_SUFFIX: -fips
report-test-results:
name: Playbooks / Report Test Results
if: always() && github.event.pull_request.head.repo.full_name == github.repository
needs:
- test
- test-fips
- e2e-cypress-tests
- e2e-cypress-tests-fips
runs-on: ubuntu-22.04
permissions:
checks: write
steps:
- name: ci/checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/download-test-results
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- name: ci/publish-results
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
run: python3 .github/scripts/junit_report.py

50
.github/workflows/playbooks-codeql.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Playbooks CodeQL
on:
push:
branches: [master]
paths:
- "core-plugins/mattermost-plugin-playbooks/**"
pull_request:
branches: [master]
paths:
- "core-plugins/mattermost-plugin-playbooks/**"
schedule:
- cron: "43 14 * * 2" # Weekly on Tuesdays
permissions:
contents: read
defaults:
run:
shell: bash
working-directory: core-plugins/mattermost-plugin-playbooks
jobs:
analyze:
permissions:
security-events: write # for github/codeql-action/autobuild to send a status report
name: Playbooks / Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ["go", "javascript"]
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
debug: false
config-file: ./core-plugins/mattermost-plugin-playbooks/.github/codeql/codeql-config.yml
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View file

@ -0,0 +1,27 @@
# http://editorconfig.org/
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.{js, jsx, ts, tsx, json, html}]
indent_style = space
indent_size = 4
[webapp/package.json]
indent_size = 2
[{Makefile, *.mk}]
indent_style = tab
[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false

View file

@ -0,0 +1,30 @@
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
3. Take a look at other repository-specific documentation at https://developers.mattermost.com/contribute/getting-started/
REMEMBER TO:
- Run `make i18n-extract` and commit changes to synchronize any new or removed messages
- Run `make check-style` to check for style errors (required for all pull requests)
- Run `make test` to ensure unit tests passed
-->
## Summary
<!--
A description of what this pull request does
-->
## Ticket Link
<!--
If this pull request addresses a Help Wanted ticket, please link the relevant GitHub issue, e.g.:
Fixes: https://github.com/mattermost/mattermost-server/issues/XXXXX
Otherwise, link the Jira ticket.
-->
## Checklist
<!-- Check off items as they are completed. ~~Strike through~~ items if they don't apply -->
- [ ] Gated by experimental feature flag
- [ ] Unit tests updated

View file

@ -0,0 +1,115 @@
# Copyright 2022 Mattermost, Inc.
name: "e2e-test"
description: This action used to runs cypress e2e integration tests
inputs:
CYPRESS_serverEdition:
description: The cypress server edition
required: true
SPECS:
description: The cypress specs to run
required: true
ARTIFACT_SUFFIX:
description: Optional suffix for artifact names to avoid conflicts
required: false
default: ""
runs:
using: "composite"
steps:
- name: ci/setup-go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "${{ env.GO_VERSION }}"
cache: false
- name: ci/setup-node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: "core-plugins/mattermost-plugin-playbooks/.nvmrc"
cache: "npm"
cache-dependency-path: |
core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json
core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json
- name: ci/cache-node-modules
id: cache-node-modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
core-plugins/mattermost-plugin-playbooks/webapp/node_modules
core-plugins/mattermost-plugin-playbooks/e2e-tests/node_modules
key: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
restore-keys: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
- name: ci/disable-ALSA
run: printf "pcm.!default {\n type plug\n slave.pcm \"null\"\n}\n" > ~/.asoundrc;
shell: bash
- name: ci/restore-postresql
uses: docker://postgres:14
env:
TEST_DATABASE_URL: postgres://mmuser:mostest@postgres:5432/mattermost_test
with:
entrypoint: ./core-plugins/mattermost-plugin-playbooks/.github/actions/e2e-test/generate-test-data.sh
- name: ci/installing-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
NODE_ENV: development
run: |
cd webapp
npm install --ignore-scripts --no-save --legacy-peer-deps
cd ../e2e-tests
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/installing-cypress-deps
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
NODE_ENV: development
run: |
sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb -y
cd e2e-tests/
npx cypress install
- name: ci/install-playbooks
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_ADMIN_USERNAME: sysadmin
MM_ADMIN_PASSWORD: Sys@dmin-sample1
run: make upload-to-server
- name: ci/run-cypress-tests
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
TYPE: NONE
PULL_REQUEST: ""
BROWSER: chrome
HEADLESS: "true"
DASHBOARD_ENABLE: "false"
FULL_REPORT: "false"
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_ADMIN_USERNAME: sysadmin
MM_ADMIN_PASSWORD: Sys@dmin-sample1
CYPRESS_serverEdition: ${{ inputs.CYPRESS_serverEdition }}
MM_SERVICESETTINGS_EXPERIMENTALSTRICTCSRFENFORCEMENT: "false"
MM_SERVICESETTINGS_STRICTCSRFENFORCEMENT: "false"
TERM: xterm
run: |
cd e2e-tests
npm run test -- --spec "${{ inputs.SPECS }}"
- name: ci/upload-cypress-test-results
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: cypress-test-results-${{ matrix.runId }}${{ inputs.ARTIFACT_SUFFIX }}
path: |
core-plugins/mattermost-plugin-playbooks/e2e-tests/results/junit
core-plugins/mattermost-plugin-playbooks/e2e-tests/tests/screenshots
retention-days: 14 ## No need to keep test results more than 14 days

View file

@ -0,0 +1,5 @@
#!/bin/bash
psql -d $TEST_DATABASE_URL -v "ON_ERROR_STOP=1" -c "CREATE DATABASE migrated;";
psql -d $TEST_DATABASE_URL -v "ON_ERROR_STOP=1" -c "CREATE DATABASE latest;";
psql -d $TEST_DATABASE_URL -v "ON_ERROR_STOP=1" mattermost_test < core-plugins/mattermost-plugin-playbooks/e2e-tests/db-setup/mattermost.sql;

View file

@ -0,0 +1,31 @@
# Copyright 2022 Mattermost, Inc.
name: "generate-specs"
description: This action used to split cypress integration tests based on the parallelism provided
inputs:
directory:
description: The directory of the test suite
required: true
search_path:
description: The path to look for from within the directory
required: true
parallelism:
description: The parallelism for the tests
required: true
outputs:
specs:
description: The specs generated for the strategy
value: ${{ steps.generate-specs.outputs.specs }}
runs:
using: "composite"
steps:
- name: ci/generate-specs
id: generate-specs
env:
PARALLELISM: ${{ inputs.parallelism }}
SEARCH_PATH: ${{ inputs.search_path }}
DIRECTORY: ${{ inputs.directory }}
run: |
go run ${{ github.action_path }}/split-tests.go > output.json
echo "specs=$(cat output.json)" >> $GITHUB_OUTPUT
shell: bash

View file

@ -0,0 +1,116 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"encoding/json"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
type Specs struct {
searchPath string
directory string
parallelism int
rawFiles []string
groupedFiles []SpecGroup
}
type SpecGroup struct {
RunID string `json:"runId"`
Specs string `json:"specs"`
}
type Output struct {
Include []SpecGroup `json:"include"`
}
func newSpecGroup(runId string, specs string) *SpecGroup {
return &SpecGroup{
RunID: runId,
Specs: specs,
}
}
func newSpecs(directory string, searchPath string, parallelism int) *Specs {
return &Specs{
directory: directory,
searchPath: searchPath,
parallelism: parallelism,
}
}
func (s *Specs) findFiles() {
fileSystem := os.DirFS(filepath.Join(s.directory, s.searchPath))
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Find all files matching _spec.(js|ts)
r := regexp.MustCompile(".*_spec.(js|ts)$")
if r.MatchString(path) {
s.rawFiles = append(s.rawFiles, filepath.Join(s.searchPath, path))
}
return nil
})
if err != nil {
panic(err)
}
}
func (s *Specs) generateSplits() {
// Split to chunks based on the parallelism provided
chunkSize := int(math.Ceil(float64(len(s.rawFiles)) / float64(s.parallelism)))
runNo := 1
// We can figure out a more sophisticated way to split the tests
// We can use metadata in order to group them manually
for i := 0; i <= len(s.rawFiles); i += chunkSize {
end := i + chunkSize
if end > len(s.rawFiles) {
end = len(s.rawFiles)
}
fileGroup := strings.Join(s.rawFiles[i:end], ",")
specGroup := newSpecGroup(fmt.Sprintf("%d", runNo), fileGroup)
s.groupedFiles = append(s.groupedFiles, *specGroup)
// Break when we reach the end to avoid duplicate groups
if end == len(s.rawFiles) {
break
}
runNo++
}
}
func (s *Specs) dumpSplits() {
// Dump json format for GitHub actions
o := &Output{
Include: s.groupedFiles,
}
b, err := json.Marshal(o)
if err != nil {
panic(err)
}
os.Stdout.Write(b)
}
func main() {
searchPath := os.Getenv("SEARCH_PATH")
directory := os.Getenv("DIRECTORY")
parallelism, _ := strconv.Atoi(os.Getenv("PARALLELISM"))
specs := newSpecs(directory, searchPath, parallelism)
specs.findFiles()
specs.generateSplits()
specs.dumpSplits()
}

View file

@ -0,0 +1,54 @@
# Copyright 2022 Mattermost, Inc.
name: "test-with-db"
description: This action used to runs tests with db integration
runs:
using: "composite"
steps:
- name: ci/cache-node-modules
id: cache-node-modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
core-plugins/mattermost-plugin-playbooks/webapp/node_modules
core-plugins/mattermost-plugin-playbooks/e2e-tests/node_modules
key: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
restore-keys: ${{ runner.os }}-playbooks-node-modules-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/webapp/package-lock.json') }}-${{ hashFiles('core-plugins/mattermost-plugin-playbooks/e2e-tests/package-lock.json') }}
- name: ci/setup-webapp-npm-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
NODE_ENV: development
run: |
cd webapp
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/test-db-integration
shell: bash
working-directory: ${{ github.workspace }}/core-plugins/mattermost-plugin-playbooks
env:
IS_CI: true
POSTGRES_USER: mmuser
POSTGRES_DB: mattermost_test
MYSQL_ROOT_PASSWORD: mostest
MYSQL_DATABASE: mattermost_test
MYSQL_USER: mmuser
MYSQL_PASSWORD: mostest
MARIADB_ROOT_PASSWORD: mostest
MARIADB_DATABASE: mattermost_test
MARIADB_USER: mmuser
MARIADB_PASSWORD: mostest
run: make test
- name: ci/upload-test-results
if: always()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ${{ github.job }}-results
path: |
core-plugins/mattermost-plugin-playbooks/report.xml
core-plugins/mattermost-plugin-playbooks/client/report.xml
core-plugins/mattermost-plugin-playbooks/build/report.xml
retention-days: 5 ## No need to keep test results for more than 5 days

View file

@ -0,0 +1 @@
comment: false

View file

@ -0,0 +1,14 @@
name: "CodeQL config"
query-filters:
- exclude:
problem.severity:
- warning
- recommendation
- exclude:
id: go/log-injection
paths-ignore:
- e2e-tests
- '**/*_test.go'
- '**/*.test.*'

View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Parse JUnit XML artifacts and publish a GitHub Check Run with the results."""
import glob
import json
import os
import subprocess
import sys
import xml.etree.ElementTree as ET
def parse_results(pattern="**/*.xml"):
total = failed = skipped = 0
annotations = []
for f in glob.glob(pattern, recursive=True):
try:
root = ET.parse(f).getroot()
for tc in root.iter("testcase"):
total += 1
if tc.find("skipped") is not None:
skipped += 1
elif tc.find("failure") is not None or tc.find("error") is not None:
el = tc.find("failure") if tc.find("failure") is not None else tc.find("error")
failed += 1
name = f"{tc.get('classname', '')}.{tc.get('name', '')}".strip(".")
message = (el.get("message") or el.text or "").strip()[:500]
annotations.append({"name": name, "message": message})
except Exception as e:
print(f"Warning: could not parse {f}: {e}", file=sys.stderr)
passed = total - failed - skipped
return total, passed, failed, skipped, annotations
def build_summary(total, passed, failed, skipped, annotations):
icon = "" if failed == 0 else ""
lines = [
"| Tests | Passed | Failed | Skipped |",
"|-------|--------|--------|---------|",
f"| {total} | {passed} | {failed} | {skipped} |",
]
if annotations:
lines += [
"",
"<details><summary>Failed tests</summary>",
"",
]
for a in annotations[:50]:
first_line = next(iter(a.get("message", "").splitlines()), "")[:120]
lines.append(f"- **{a['name']}**: {first_line}")
lines.append("</details>")
if len(annotations) > 50:
lines.append(f"_...and {len(annotations) - 50} more_")
return "\n".join(lines)
def create_check_run(owner, repo, sha, title, summary, conclusion):
payload = {
"name": "JUnit Test Report",
"head_sha": sha,
"status": "completed",
"conclusion": conclusion,
"output": {
"title": title,
"summary": summary,
},
}
try:
result = subprocess.run(
["gh", "api", f"repos/{owner}/{repo}/check-runs",
"--method", "POST",
"--input", "-"],
input=json.dumps(payload).encode(),
capture_output=True,
timeout=30,
)
except subprocess.TimeoutExpired:
print("Error: timed out waiting for GitHub API", file=sys.stderr)
sys.exit(1)
if result.returncode != 0:
print(f"Error creating check run: {result.stderr.decode()}", file=sys.stderr)
sys.exit(1)
def main():
owner = os.environ["GITHUB_REPOSITORY_OWNER"]
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
sha = os.environ["GITHUB_SHA"]
total, passed, failed, skipped, annotations = parse_results()
title = f"{total} tests run, {passed} passed, {skipped} skipped, {failed} failed."
summary = build_summary(total, passed, failed, skipped, annotations)
conclusion = "success" if failed == 0 else "failure"
print(title)
create_check_run(owner, repo, sha, title, summary, conclusion)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,542 @@
name: ci
on:
pull_request:
push:
branches:
- master
schedule:
- cron: "0 03 * * 1-6" # Daily at 03:00 UTC from Monday through Saturday.
defaults:
run:
shell: bash
env:
TERM: xterm
IS_NOT_FORK: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
jobs:
go:
name: Compute Go Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.calculate.outputs.GO_VERSION }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: Calculate version
id: calculate
run: echo GO_VERSION=$(grep -E '^go [0-9]+\.[0-9]+' go.mod | cut -d' ' -f2) >> "${GITHUB_OUTPUT}"
lint:
runs-on: ubuntu-22.04
needs:
- go
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/cache-node-modules
id: cache-node-modules
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
webapp/node_modules
e2e-tests/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('webapp/package-lock.json') }}-${{ hashFiles('e2e-tests/package-lock.json') }}
restore-keys: ${{ runner.os }}-node-modules-${{ hashFiles('webapp/package-lock.json') }}-${{ hashFiles('e2e-tests/package-lock.json') }}
- name: ci/setup-webapp-npm-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
env:
NODE_ENV: development
run: |
cd webapp
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/setup-e2e-npm-deps
if: steps.cache-node-modules.outputs.cache-hit != 'true'
env:
NODE_ENV: development
run: |
cd e2e-tests
npm install --ignore-scripts --no-save --legacy-peer-deps
- name: ci/checking-code-style
run: make check-style
- name: ci/go-tidy
run: go mod tidy -v
- name: ci/check-diff-on-gomod
run: git --no-pager diff --exit-code go.mod go.sum || (echo "Please run \"go mod tidy\" and commit the changes in go.mod and go.sum." && exit 1)
- name: ci/run-make-apply
run: make apply
- name: ci/check-diff-on-generated-manifest-files
run: git --no-pager diff --exit-code *manifest.* || (echo "Please run \"make apply\" and commit the changes in the generated manifests." && exit 1)
- name: ci/run-make-i18n-extract-webapp
run: make i18n-extract-webapp
- name: ci/check-diff-on-webapp-i18n-files
run: git --no-pager diff --exit-code webapp/i18n/en.json || (echo "Please run \"make i18n-extract\" and commit the changes in webapp/i18n/en.json." && exit 1)
- name: ci/run-make-graphql
run: make graphql
- name: ci/check-diff-on-webapp-graphql-generated-files
run: git --no-pager diff --exit-code webapp/src/graphql/generated/ || (echo "Please run \"make graphql\" and commit the changes." && exit 1)
dist:
runs-on: ubuntu-22.04
needs:
- go
- lint
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/build-for-upload
run: make dist
- name: ci/upload-build-artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: |
dist/playbooks-*.tar.gz
retention-days: 7 ## No need to keep build artifacts for more than 7 days
- name: ci/ensure-build-on-all-platforms ## Verify builds on all platforms *after* ci/build-for-upload to keep artifact small
run: make dist
dist-fips:
# Skip FIPS testing for forks, which won't have docker login credentials.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- go
- lint
container:
image: mattermostdevelopment/mattermost-build-server-fips:${{ needs.go.outputs.version }}
credentials:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
env:
FIPS_ENABLED: true
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/build-for-upload
run: make dist
- name: ci/upload-build-artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: |
dist/playbooks-*.tar.gz
retention-days: 7 ## No need to keep build artifacts for more than 7 days
- name: ci/ensure-build-on-all-platforms ## Verify builds on all platforms *after* ci/build-for-upload to keep artifact small
run: make dist
upload:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- dist
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: dist
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload dist to S3
run: |
aws s3 cp dist/playbooks-*.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/mattermost-plugin-playbooks/mattermost-plugin-playbooks-${{ steps.short-sha.outputs.sha }}.tar.gz
upload-fips:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- dist-fips
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact-fips
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: dist-fips
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Upload dist-fips to S3
run: |
aws s3 cp dist-fips/playbooks-*.tar.gz s3://${{ secrets.AWS_S3_BUCKET }}/mattermost-plugin-playbooks/mattermost-plugin-playbooks-fips-${{ steps.short-sha.outputs.sha }}.tar.gz
test:
runs-on: ubuntu-22.04
needs:
- lint
- go
container:
image: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: mmuser
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/test-with-db
uses: ./.github/actions/test-with-db
test-fips:
# Skip FIPS testing for forks, which won't have docker login credentials.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- lint
- go
container:
image: mattermostdevelopment/mattermost-build-server-fips:${{ needs.go.outputs.version }}
credentials:
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: mmuser
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/configure-git-safe-directory
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: ci/test-with-db
uses: ./.github/actions/test-with-db
generate-specs:
runs-on: ubuntu-22.04
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/generate-specs
id: generate-specs
uses: ./.github/actions/generate-specs
with:
parallelism: 3
directory: e2e-tests
search_path: tests/integration
e2e-cypress-tests:
runs-on: ubuntu-22.04
needs:
- go
- lint
- generate-specs
- dist
name: e2e-cypress-tests-run-${{ matrix.runId }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
services:
postgres:
image: postgres:15.3
env:
POSTGRES_USER: mmuser
POSTGRES_PASSWORD: mostest
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
minio:
image: minio/minio:RELEASE.2019-10-11T00-38-09Z
env:
MINIO_ACCESS_KEY: minioaccesskey
MINIO_SECRET_KEY: miniosecretkey
MINIO_SSE_MASTER_KEY: "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574"
inbucket:
image: mattermost/inbucket:release-1.2.0
ports:
- 10080:10080
- 10110:10110
- 10025:10025
elasticsearch:
image: mattermost/mattermost-elasticsearch-docker:7.0.0
env:
http.host: "0.0.0.0"
http.port: 9200
http.cors.enabled: "true"
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
http.cors.allow-credentials: "true"
transport.host: "127.0.0.1"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- 9200:9200
mattermost-server:
image: mattermostdevelopment/mattermost-enterprise-edition:master
env:
DB_HOST: postgres
DB_PORT_NUMBER: 5432
MM_DBNAME: mattermost_test
MM_USERNAME: mmuser
MM_PASSWORD: mostest
CI_INBUCKET_HOST: inbucket
CI_INBUCKET_PORT: 10080
CI_MINIO_HOST: minio
IS_CI: true
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
MM_CLUSTERSETTINGS_READONLYCONFIG: false
MM_EMAILSETTINGS_SMTPSERVER: inbucket
MM_EMAILSETTINGS_SMTPPORT: 10025
MM_ELASTICSEARCHSETTINGS_CONNECTIONURL: http://elasticsearch:9200
MM_EXPERIMENTALSETTINGS_USENEWSAMLLIBRARY: true
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10"
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_PLUGINSETTINGS_ENABLEUPLOADS: true
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: true
MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: false
MM_SERVICESETTINGS_ENABLELEGACYSIDEBAR: true
MM_TEAMSETTINGS_MAXUSERSPERTEAM: 10000
MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW: false
MM_SERVICEENVIRONMENT: test
MM_SERVICESETTINGS_EXPERIMENTALSTRICTCSRFENFORCEMENT: false
MM_SERVICESETTINGS_STRICTCSRFENFORCEMENT: false
ports:
- 8065:8065
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-${{ steps.short-sha.outputs.sha }}
path: dist
- name: ci/e2e-test
uses: ./.github/actions/e2e-test
with:
CYPRESS_serverEdition: E20
SPECS: ${{ matrix.specs }}
e2e-cypress-tests-fips:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-22.04
needs:
- go
- lint
- generate-specs
- dist-fips
name: e2e-cypress-tests-fips-run-${{ matrix.runId }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
services:
postgres:
image: postgres:15.3
env:
POSTGRES_USER: mmuser
POSTGRES_PASSWORD: mostest
POSTGRES_DB: mattermost_test
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
minio:
image: minio/minio:RELEASE.2019-10-11T00-38-09Z
env:
MINIO_ACCESS_KEY: minioaccesskey
MINIO_SECRET_KEY: miniosecretkey
MINIO_SSE_MASTER_KEY: "my-minio-key:6368616e676520746869732070617373776f726420746f206120736563726574"
inbucket:
image: mattermost/inbucket:release-1.2.0
ports:
- 10080:10080
- 10110:10110
- 10025:10025
elasticsearch:
image: mattermost/mattermost-elasticsearch-docker:7.0.0
env:
http.host: "0.0.0.0"
http.port: 9200
http.cors.enabled: "true"
http.cors.allow-origin: "http://localhost:1358,http://127.0.0.1:1358"
http.cors.allow-headers: "X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization"
http.cors.allow-credentials: "true"
transport.host: "127.0.0.1"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- 9200:9200
mattermost-server:
image: mattermostdevelopment/mattermost-enterprise-fips-edition:master
env:
DB_HOST: postgres
DB_PORT_NUMBER: 5432
MM_DBNAME: mattermost_test
MM_USERNAME: mmuser
MM_PASSWORD: mostest
CI_INBUCKET_HOST: inbucket
CI_INBUCKET_PORT: 10080
CI_MINIO_HOST: minio
IS_CI: true
MM_LICENSE: "${{ secrets.MM_E2E_TEST_LICENSE_ONPREM_ENT }}"
MM_CLUSTERSETTINGS_READONLYCONFIG: false
MM_EMAILSETTINGS_SMTPSERVER: inbucket
MM_EMAILSETTINGS_SMTPPORT: 10025
MM_ELASTICSEARCHSETTINGS_CONNECTIONURL: http://elasticsearch:9200
MM_EXPERIMENTALSETTINGS_USENEWSAMLLIBRARY: true
MM_SQLSETTINGS_DATASOURCE: "postgres://mmuser:mostest@postgres:5432/mattermost_test?sslmode=disable&connect_timeout=10"
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_PLUGINSETTINGS_ENABLEUPLOADS: true
MM_SERVICESETTINGS_SITEURL: http://localhost:8065
MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS: true
MM_ANNOUNCEMENTSETTINGS_ADMINNOTICESENABLED: false
MM_SERVICESETTINGS_ENABLELEGACYSIDEBAR: true
MM_TEAMSETTINGS_MAXUSERSPERTEAM: 10000
MM_SERVICESETTINGS_ENABLEONBOARDINGFLOW: false
MM_SERVICEENVIRONMENT: test
MM_SERVICESETTINGS_EXPERIMENTALSTRICTCSRFENFORCEMENT: false
MM_SERVICESETTINGS_STRICTCSRFENFORCEMENT: false
ports:
- 8065:8065
steps:
- name: ci/checkout-repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: "0"
- name: ci/get-short-sha
id: short-sha
run: echo "sha=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: ci/download-build-artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: playbooks-bundle-fips-${{ steps.short-sha.outputs.sha }}
path: dist
- name: ci/e2e-test
uses: ./.github/actions/e2e-test
with:
CYPRESS_serverEdition: E20
SPECS: ${{ matrix.specs }}
ARTIFACT_SUFFIX: -fips
report-test-results:
if: always() && github.event.pull_request.head.repo.full_name == github.repository
needs:
- e2e-cypress-tests
- e2e-cypress-tests-fips
- test
- test-fips
runs-on: ubuntu-22.04
permissions:
checks: write
steps:
- name: ci/checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ci/download-test-results
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
- name: ci/publish-results
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
run: python3 .github/scripts/junit_report.py

View file

@ -0,0 +1,45 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '43 14 * * 2'
permissions:
contents: read
jobs:
analyze:
permissions:
security-events: write # for github/codeql-action/autobuild to send a status report
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
debug: false
config-file: ./.github/codeql/codeql-config.yml
# Autobuild attempts to build any compiled languages
- name: Autobuild
uses: github/codeql-action/autobuild@v4
# Perform Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

View file

@ -0,0 +1,34 @@
.eslintcache
.stylelintcache
report.xml
bin/
dist/
webapp/src/manifest.ts
server/manifest.go
server/data/
server/e2etest.config.json
server/logs/
mprocs.yaml
# Mac
.DS_Store
# Jetbrains
.idea/
# VS Code
.vscode
# Zed
.zed
# Notice
.notice-work
.aider*
.env
# Claude
**/CLAUDE.local.md
.claude
CLAUDE.md
.cursorrules

View file

@ -0,0 +1,4 @@
include:
- project: mattermost/ci/$CI_PROJECT_NAME
ref: main
file: private.yml

View file

@ -0,0 +1 @@
mainConfiguration: https://github.com/mattermost/mattermost-gitpod-config

View file

@ -0,0 +1,77 @@
version: "2"
linters:
default: none
enable:
- copyloopvar
- errcheck
- gocheckcompilerdirectives
- gosec
- govet
- ineffassign
- misspell
- revive
- staticcheck
- unconvert
- unused
settings:
govet:
disable:
- fieldalignment
- unusedwrite
enable-all: true
revive:
rules:
- name: redefines-builtin-id
disabled: true
staticcheck:
checks: ["all", "-QF1008"]
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- goconst
- gosec
- unparam
path: _test\.go
- linters:
- gocritic
- revive
path: server/bot/logger.go
- linters:
- revive
text: unused-parameter
- linters:
- gosec
text: G115
- path: (.+)\.go$
text: G404
- path: (.+)\.go$
text: 'shadow: declaration of "err" shadows declaration at'
paths:
- example.*.go
- server/manifest.go
- build/
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
settings:
gofmt:
simplify: true
exclusions:
generated: lax
paths:
- example.*.go
- server/manifest.go
- build/
- third_party$
- builtin$
- examples$

View file

@ -0,0 +1 @@
24.11

View file

@ -0,0 +1,3 @@
{
"ignore": "used in module|graphql/generated"
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,39 @@
The Mattermost Source Available License license (the “Source Available License”)
Copyright (c) 2015-present Mattermost
With regard to the Mattermost Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Mattermost Terms of Service, available at
https://mattermost.com/enterprise-edition-terms/ (the “EE Terms”), or other
agreement governing the use of the Software, as agreed by you and Mattermost,
and otherwise have a valid Mattermost Enterprise E20 subscription for the
correct number of user seats. Subject to the foregoing sentence, you are free
to modify this Software and publish patches to the Software. You agree that
Mattermost and/or its licensors (as applicable) retain all right, title and
interest in and to all such modifications and/or patches, and all such
modifications and/or patches may only be used, copied, modified, displayed,
distributed, or otherwise exploited with a valid Mattermost Enterprise E20
Edition subscription for the correct number of user seats. Notwithstanding
the foregoing, you may copy and modify the Software for development and testing
purposes, without requiring a subscription. You agree that Mattermost and/or
its licensors (as applicable) retain all right, title and interest in and to
all such modifications. You are not granted any other rights beyond what is
expressly stated herein. Subject to the foregoing, it is forbidden to copy,
merge, publish, distribute, sublicense, and/or sell the Software.
The full text of this EE License 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.
For all third party components incorporated into the Mattermost Software, those
components are licensed under the original license provided by the owner of the
applicable component.

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,404 @@
GO ?= $(shell command -v go 2> /dev/null)
GOFLAGS ?= $(GOFLAGS:)
NPM ?= $(shell command -v npm 2> /dev/null)
CURL ?= $(shell command -v curl 2> /dev/null)
MM_DEBUG ?=
GOPATH ?= $(shell go env GOPATH)
GO_TEST_FLAGS ?= -race
GO_BUILD_FLAGS ?=
MM_UTILITIES_DIR ?= ../mattermost-utilities
DLV_DEBUG_PORT := 2346
DEFAULT_GOOS ?= $(shell go env GOOS)
DEFAULT_GOARCH ?= $(shell go env GOARCH)
export GO111MODULE=on
# We need to export GOBIN to allow it to be set
# for processes spawned from the Makefile
export GOBIN ?= $(PWD)/bin
# You can include assets this directory into the bundle. This can be e.g. used to include profile pictures.
ASSETS_DIR ?= assets
## Define the default target (make all)
.PHONY: default
default: all
# Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed.
include build/setup.mk
BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz
# Include custom makefile, if present
ifneq ($(wildcard build/custom.mk),)
include build/custom.mk
endif
ifneq ($(MM_DEBUG),)
GO_BUILD_GCFLAGS = -gcflags "all=-N -l"
else
GO_BUILD_GCFLAGS =
endif
# ====================================================================================
# Semver release tagging
# Usage: make tag-release [bump-type] [DRY_RUN=1] [FORCE=1] [VERSION=X.Y.Z] [RELEASE_ARGS="..."]
# Examples:
# make tag-release # Interactive mode
# make tag-release patch # Bump patch version
# make tag-release minor-rc # Start minor RC cycle
# make tag-release rc-finalize # Finalize RC to stable
# DRY_RUN=1 make tag-release patch # Dry run
# FORCE=1 make tag-release patch # Force (skip validation errors)
# VERSION=2.6.2 make tag-release # Explicit version
# make tag-release RELEASE_ARGS="--version=2.6.2" # Explicit version (alternative)
TAG_RELEASE_BUMP := $(word 2,$(MAKECMDGOALS))
ifneq ($(filter tag-release,$(MAKECMDGOALS)),)
ifneq ($(TAG_RELEASE_BUMP),)
$(eval $(TAG_RELEASE_BUMP):;@:)
endif
endif
RELEASE_ARGS ?=
RELEASE_FLAGS := $(RELEASE_ARGS)
ifneq ($(DRY_RUN),)
RELEASE_FLAGS += --dry-run
endif
ifneq ($(FORCE),)
RELEASE_FLAGS += --force
endif
ifneq ($(VERSION),)
RELEASE_FLAGS += --version=$(VERSION)
endif
# ====================================================================================
.PHONY: tag-release
## Tag a semver release interactively or with bump type (DRY_RUN=1, FORCE=1)
tag-release:
./build/bin/release $(TAG_RELEASE_BUMP) $(RELEASE_FLAGS)
## Checks the code style, tests, builds and bundles the plugin.
.PHONY: all
all: check-style test dist
## Ensures the plugin manifest is valid
.PHONY: manifest-check
manifest-check:
./build/bin/manifest check
## Propagates plugin manifest information into the server/ and webapp/ folders.
.PHONY: apply
apply:
./build/bin/manifest apply
## Install go tools
install-go-tools:
@echo Installing go tools
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6
$(GO) install github.com/golang/mock/mockgen@v1.6.0
$(GO) install gotest.tools/gotestsum@v1.7.0
$(GO) install github.com/cortesi/modd/cmd/modd@latest
$(GO) install github.com/mattermost/mattermost-govet/v2@3f08281c344327ac09364f196b15f9a81c7eff08
## Runs eslint and golangci-lint
.PHONY: check-style
check-style: manifest-check apply webapp/node_modules e2e-tests/node_modules install-go-tools
@echo Checking for style guide compliance
ifneq ($(HAS_WEBAPP),)
cd webapp && npm run lint
cd webapp && npm run check-types
endif
cd e2e-tests && npm run check
# It's highly recommended to run go-vet first
# to find potential compile errors that could introduce
# weird reports at golangci-lint step
ifneq ($(HAS_SERVER),)
@echo Running golangci-lint
$(GO) vet ./...
$(GOBIN)/golangci-lint run ./...
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -license -license.year=2020 -license.ignore=server/graphql/models.go ./...
endif
## Fix JS file ESLint issues
.PHONY: fix-style
fix-style: apply webapp/node_modules e2e-tests/node_modules
@echo Fixing lint issues to follow style guide
ifneq ($(HAS_WEBAPP),)
cd webapp && npm run fix
endif
cd e2e-tests && npm run fix
## Builds the server, if it exists, for all supported architectures, unless MM_SERVICESETTINGS_ENABLEDEVELOPER is set
.PHONY: server
server:
ifneq ($(HAS_SERVER),)
ifneq ($(MM_DEBUG),)
$(info DEBUG mode is on; to disable, unset MM_DEBUG)
endif
mkdir -p server/dist;
ifneq ($(MM_SERVICESETTINGS_ENABLEDEVELOPER),)
@echo Building plugin only for $(DEFAULT_GOOS)-$(DEFAULT_GOARCH) because MM_SERVICESETTINGS_ENABLEDEVELOPER is enabled
cd server && env GOOS=$(DEFAULT_GOOS) GOARCH=$(DEFAULT_GOARCH) $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-$(DEFAULT_GOOS)-$(DEFAULT_GOARCH);
ifneq ($(MM_DEBUG),)
cd server && ./dist/plugin-$(DEFAULT_GOOS)-$(DEFAULT_GOARCH) graphqlcheck
endif
else
cd server && env GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-amd64;
ifeq ($(FIPS_ENABLED),true)
@echo Only building linux-amd64 for FIPS
else
cd server && env GOOS=linux GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-arm64;
endif
endif
endif
## Ensures NPM dependencies are installed without having to run this all the time.
webapp/node_modules: $(wildcard webapp/package.json)
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) install --ignore-scripts --legacy-peer-deps
touch $@
endif
## Ensures NPM dependencies are installed without having to run this all the time.
e2e-tests/node_modules: $(wildcard e2e-tests/package.json)
ifneq ($(HAS_WEBAPP),)
cd e2e-tests && $(NPM) install
touch $@
endif
## Builds the webapp, if it exists.
.PHONY: webapp
webapp: webapp/node_modules
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run graphql;
ifeq ($(MM_DEBUG),)
cd webapp && $(NPM) run build;
else
cd webapp && $(NPM) run debug;
endif
endif
## Generates a tar bundle of the plugin for install.
.PHONY: bundle
bundle:
rm -rf dist/
mkdir -p dist/$(PLUGIN_ID)
./build/bin/manifest dist
ifneq ($(wildcard LICENSE.txt),)
cp -r LICENSE.txt dist/$(PLUGIN_ID)/
endif
ifneq ($(wildcard NOTICE.txt),)
cp -r NOTICE.txt dist/$(PLUGIN_ID)/
endif
ifneq ($(wildcard $(ASSETS_DIR)/.),)
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
endif
ifneq ($(HAS_PUBLIC),)
cp -r public dist/$(PLUGIN_ID)/public/
endif
ifneq ($(HAS_SERVER),)
mkdir -p dist/$(PLUGIN_ID)/server
cp -r server/dist dist/$(PLUGIN_ID)/server/
endif
ifneq ($(HAS_WEBAPP),)
mkdir -p dist/$(PLUGIN_ID)/webapp
cp -r webapp/dist dist/$(PLUGIN_ID)/webapp/
endif
ifeq ($(shell uname),Darwin)
cd dist && tar --disable-copyfile -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
else
cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)
endif
@echo plugin built at: dist/$(BUNDLE_NAME)
## Builds and bundles the plugin.
.PHONY: dist
dist: apply server webapp bundle
## Builds and installs the plugin to a server.
.PHONY: deploy
deploy: dist upload-to-server
## Builds and installs the plugin to a server, updating the plugin automatically when changed.
.PHONY: watch
watch: apply install-go-tools bundle upload-to-server
$(GOBIN)/modd
## Watch mode for webapp side
.PHONY: watch-webapp
watch-webapp:
ifeq ($(MM_DEBUG),)
cd webapp && $(NPM) run build:watch
else
cd webapp && $(NPM) run debug:watch
endif
## Builds and installs the plugin to a server, then starts the webpack dev server on 9005
.PHONY: dev
dev: apply server bundle webapp/node_modules
cd webapp && $(NPM) run dev-server
## Installs a previous built plugin with updated webpack assets to a server.
.PHONY: deploy-from-watch
deploy-from-watch: bundle upload-to-server
.PHONY: upload-to-server
upload-to-server:
./build/bin/pluginctl deploy $(PLUGIN_ID) dist/$(BUNDLE_NAME)
## Setup dlv for attaching, identifying the plugin PID for other targets.
.PHONY: setup-attach
setup-attach:
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
$(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w))
@if [ ${NUM_PID} -gt 2 ]; then \
echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \
exit 1; \
fi
## Check if setup-attach succeeded.
.PHONY: check-attach
check-attach:
@if [ -z ${PLUGIN_PID} ]; then \
echo "Could not find plugin PID; the plugin is not running. Exiting."; \
exit 1; \
else \
echo "Located Plugin running with PID: ${PLUGIN_PID}"; \
fi
## Attach dlv to an existing plugin instance.
.PHONY: attach
attach: setup-attach check-attach
dlv attach ${PLUGIN_PID}
## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT.
.PHONY: attach-headless
attach-headless: setup-attach check-attach
dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient
## Detach dlv from an existing plugin instance, if previously attached.
.PHONY: detach
detach: setup-attach
@DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \
if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \
echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \
kill -9 $$DELVE_PID ; \
fi
## Runs any lints and unit tests defined for the server and webapp, if they exist.
.PHONY: test
test: apply webapp/node_modules install-go-tools
ifneq ($(HAS_SERVER),)
$(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./...
endif
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run test;
endif
@echo "Running submodule tests..."
cd client && $(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./...
cd build && $(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./...
## Creates a coverage report for the server code.
.PHONY: coverage
coverage: apply webapp/node_modules
ifneq ($(HAS_SERVER),)
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
$(GO) tool cover -html=server/coverage.txt
endif
## Extract strings for translation from the source code.
.PHONY: i18n-extract
i18n-extract: i18n-extract-webapp i18n-extract-server
i18n-extract-webapp:
ifneq ($(HAS_WEBAPP),)
cd webapp && $(NPM) run extract
endif
i18n-extract-server:
ifneq ($(HAS_SERVER),)
$(GO) install -modfile=go.tools.mod github.com/mattermost/mattermost-utilities/mmgotool
mkdir -p server/i18n
cp assets/i18n/en.json server/i18n/en.json
cd server && $(GOBIN)/mmgotool i18n extract --portal-dir="" --skip-dynamic
mv server/i18n/en.json assets/i18n/en.json
rmdir server/i18n
endif
## Exit on empty translation strings and translation source strings
i18n-check:
ifneq ($(HAS_SERVER),)
$(GO) install -modfile=go.tools.mod github.com/mattermost/mattermost-utilities/mmgotool
mkdir -p server/i18n
cp assets/i18n/en.json server/i18n/en.json
cd server && $(GOBIN)/mmgotool i18n clean-empty --portal-dir="" --check
cd server && $(GOBIN)/mmgotool i18n check-empty-src --portal-dir=""
rmdir server/i18n
endif
## Disable the plugin.
.PHONY: disable
disable: detach
./build/bin/pluginctl disable $(PLUGIN_ID)
## Enable the plugin.
.PHONY: enable
enable:
./build/bin/pluginctl enable $(PLUGIN_ID)
## Generate derived types from schema files
.PHONY: graphql
graphql:
cd webapp && npm run graphql
$(GO) install github.com/jkrajniak/graphql-codegen-go@v1.2.1
cd server && $(GOBIN)/graphql-codegen-go -schemas api/schema.graphqls -packageName graphql -out graphql/models.go
## Reset the plugin, effectively disabling and re-enabling it on the server.
.PHONY: reset
reset: detach
./build/bin/pluginctl reset $(PLUGIN_ID)
## Kill all instances of the plugin, detaching any existing dlv instance.
.PHONY: kill
kill: detach
$(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}'))
@for PID in ${PLUGIN_PID}; do \
echo "Killing plugin pid $$PID"; \
kill -9 $$PID; \
done; \
## Clean removes all build artifacts.
.PHONY: clean
clean:
rm -fr dist/
ifneq ($(HAS_SERVER),)
rm -fr server/coverage.txt
rm -fr server/dist
endif
ifneq ($(HAS_WEBAPP),)
rm -fr webapp/junit.xml
rm -fr webapp/dist
rm -fr webapp/node_modules
endif
rm -fr build/bin/
.PHONY: logs
logs:
./build/bin/pluginctl logs $(PLUGIN_ID)
.PHONY: logs-watch
logs-watch:
./build/bin/pluginctl logs-watch $(PLUGIN_ID)
# Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
# Mattermost Playbooks
[![Release](https://img.shields.io/github/v/release/mattermost/mattermost-plugin-playbooks)](https://github.com/mattermost/mattermost-plugin-playbooks/releases/latest)
Mattermost Playbooks allows your team to create and run playbooks from within Mattermost. For configuration and administration information visit our [documentation](https://docs.mattermost.com/guides/playbooks.html).
![Mattermost Playbooks](assets/incident_response.png)
## Development Builds
In your `mattermost-server` configuration (`config/config.json`), set the following values:
`ServiceSettings.EnableLocalMode: true`
`PluginSettings.EnableUploads: true`
and restart the server. Once done, the relevant `make` commands should be able to install builds. Those commands are:
`make deploy` - builds and installs the plugin a single time
`make watch` - continuously builds and installs when files change
which are run from the repo root.
## License
This repository is licensed under the Apache 2.0 License, except for the [server/enterprise](server/enterprise) directory which is licensed under the [Mattermost Source Available License](LICENSE.enterprise). See [Mattermost Source Available License](https://docs.mattermost.com/overview/faq.html#mattermost-source-available-license) to learn more.
Although a valid Mattermost Enterprise license is required to access all features if using this plugin in production, the [Mattermost Source Available License](LICENSE.txt) allows you to compile and test this plugin in development and testing environments without a Mattermost Enterprise license. As such, we welcome community contributions to this plugin.
If you're running Mattermost Starter and don't already have a valid license, you can obtain a trial license from **System Console > Edition and License**. If you're running the Team Edition of Mattermost, including when you run the server directly from source, you may instead configure your server to enable both testing (`ServiceSettings.EnableTesting`) and developer mode (`ServiceSettings.EnableDeveloper`). These settings are not recommended in production environments. See [Contributing](#contributing) to learn more about how to set up your development environment.
## Generating test data
To quickly test Mattermost Playbooks, use the following test commands to create playbook runs populated with random data:
- `/playbook test create-playbooks [total playbooks]` - Provide a number of total playbooks that will be created. The command creates one or more playbooks based on the given parameter.
* An example command looks like: `/playbook test create-playbooks 5`
- `/playbook test create-playbook-run [playbook ID] [timestamp] [playbook run name]` - Provide the ID of an existing playbook to which the current user has access, a timestamp, and a playbook run name. The command creates an ongoing playbook run with the creation date set to the specified timestamp.
* An example command looks like: `/playbook test create-playbook-run 6utgh6qg7p8ndeef9edc583cpc 2020-11-23 PR-Testing`
- `/playbook test bulk-data [ongoing] [ended] [days] [seed]` - Provide a number of ongoing and ended playbook runs, a number of days, and an optional random seed. The command creates the given number of ongoing and ended playbook runs, with creation dates randomly between `n` days ago and the day when the command was issued. The seed may be used to reproduce the same outcome on multiple invocations. Names are generated randomly.
* An example command looks like: `/playbook test bulk-data 10 3 342 2`
## Running E2E tests
When running E2E tests, the local `mattermost-server` configuration may be unexpectedly modified if either `on_prem_default_config.json` or `cloud_default_default_config.json` (depending on the server edition) has conflicting values for the same keys. This can be avoided by setting `CYPRESS_developerMode=true` when calling Cypress scripts. For example: `CYPRESS_developerMode=true npm run cypress:open`.
## How to Release
Run `make tag-release` to launch an interactive TUI for creating releases. The tool validates branch requirements, checks for conflicts, and creates signed tags.
### Standard Release Flow (with RC cycle)
The typical release process uses release candidates for testing before final release:
```bash
# 1. Start RC cycle from master
make tag-release minor-rc # Creates v2.7.0-rc1
# 2. Test, fix bugs, increment RC as needed
make tag-release rc # Creates v2.7.0-rc2
make tag-release rc # Creates v2.7.0-rc3
# 3. Finalize when ready
make tag-release rc-finalize # Creates v2.7.0
# 4. Create release branch for future patches
git branch release-2.7
git push origin release-2.7
```
### Patch Releases (hotfixes)
For hotfixes on existing releases, work from the release branch:
```bash
git checkout release-2.6
# ... fix bug ...
make tag-release patch # Creates v2.6.2
```
### Quick Reference
| Scenario | Branch | Command | Example |
|----------|--------|---------|---------|
| New minor | master | `minor` | v2.6.0 → v2.7.0 |
| Start RC cycle | master | `minor-rc` | v2.6.0 → v2.7.0-rc1 |
| Bump RC | master | `rc` | v2.7.0-rc1 → v2.7.0-rc2 |
| Finalize RC | master | `rc-finalize` | v2.7.0-rc2 → v2.7.0 |
| Hotfix | release-X.Y | `patch` | v2.6.1 → v2.6.2 |
| Major release | master | `major` | v2.9.0 → v3.0.0 |
### Options
- **Interactive mode**: `make tag-release` (no arguments) launches a TUI menu
- **Explicit version**: `VERSION=2.7.0 make tag-release`
- **Dry run**: `DRY_RUN=1 make tag-release` to preview without executing
- **Force mode**: `FORCE=1 make tag-release` to bypass validation errors (shows warnings instead)
## Contributing
This plugin contains both a server and web app portion. Read our documentation about the [Developer Workflow](https://developers.mattermost.com/extend/plugins/developer-workflow/) and [Developer Setup](https://developers.mattermost.com/extend/plugins/developer-setup/) for more information about developing and extending plugins.
For more information about contributing to Mattermost, and the different ways you can contribute, see [https://www.mattermost.org/contribute-to-mattermost](https://mattermost.com/contribute/?redirect_source=mm-org).
### Logging
Logging should use the logrus package (not `pluginAPI.Log`, `mlog`, or `log`). The standard logger is automatically wired into the pluginAPI and proxied through the server:
```go
logger := logrus.WithField("playbook_run_id", playbookRunID)
err := findUserForPlaybookRunAndTeam(playbookRunID, userID, teamID)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"user_id": userID,
"team_id": teamID,
}).Warn("Failed to find user for playbook run and team")
}
```
A few guidelines when logging:
* Use the appropriate level:
* Error: an error log should require some human action to fix something upon receipt
* Warn: a warning log might require investigation if it occurs in bulk, but does not require human action
* Info: a information log provides context that will typically be logged by default
* Debug: a debug log provides context that will typically be logged only on demand
* Write static log messages (`Failed to find user for playbook run and team`) instead of interpolating parameters into the log message itself (`Failed to find user %s for playbook run %s and team %s`)
* Use snake case when naming fields. Try to name these fields consistently with other usage.
* Pass errors using `WithError`.
* Use `WithFields` when passing more than one field that is not an `err`.
* Common fields can be set once instead of being passed for every log
### DB Migrations
DB migrations should be placed in `sqlstore/migrations.go` as they are the ones being run at the moment.
After transitioning to a new migration schema, the `sqlstore/migrations/future` folder will be utilised.
It would ease the transition if migrations are also added there for both drivers (mysql, postgres).
All migrations in the `future` folder should have both migration directions - `up` and `down`.
## Popular searches for Help Wanted issues:
* [Help wanted tickets currently up for grab]([https://github.com/mattermost/mattermost-server/issues?q=is%3Aopen+is%3Aissue+label%3AArea%2FPlaybooks+label%3A%22Up+For+Grabs%22](https://github.com/mattermost/mattermost-plugin-playbooks/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22+label%3A%22Up+For+Grabs%22))
* [Good first issue tickets]([https://github.com/mattermost/mattermost-server/issues?q=is%3Aopen+is%3Aissue+label%3AArea%2FPlaybooks+label%3A%22Good+First+Issue%22+label%3A%22Up+For+Grabs%22](https://github.com/mattermost/mattermost-plugin-playbooks/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22))
For more information, join the discussion in the [`Developers: Playbooks` channel](https://community.mattermost.com/core/channels/developers-playbooks).

View file

@ -0,0 +1,222 @@
[
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} zapnul aktualizace statusu pro [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} vypnul aktualizace stavu pro běh [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} vyžádal aktualizaci stavu pro [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} je účastník spuštění a chce se přidat k tomuto kanálu. Kterýkoliv člen kanálu ho může přizvat.\n"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Máte 0 přiřazených úkolů."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Máte {{.Count}} přiřazený úkol, který má být odevzdán:",
"few": "Máte {{.Count}} přiřazené úkoly, které mají být odevzdány:",
"other": "Máte {{.Count}} přiřazených úkolů, které mají být odevzdány:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Máte {{.Count}} přiřazený úkol:",
"few": "Máte {{.Count}} přiřazených úkolů:",
"other": "Máte {{.Count}} přiřazených úkolů:"
}
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "K odevzdání před {{.Count}} dny"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Vaše přiřazené úkoly"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "K odevzdání včera"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "K odevzdání dnes"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "K odevzdání za {{.Count}} den",
"few": "K odevzdání za {{.Count}} dnů",
"other": "K odevzdání za {{.Count}} dnů"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Máte **{{.Count}} přiřazený úkol k odevzdání po dnešku**.",
"few": "Máte **{{.Count}} přiřazených úkolů k odevzdání po dnešku**.",
"other": "Máte **{{.Count}} přiřazených úkolů k odevzdání po dnešku**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Prosím použijte `/playbook todo` k zobrazení všech Vašich úkolů."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Máte 0 aktuálních běhů."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Máte {{.Count}} aktuální běh:",
"few": "Máte {{.Count}} aktuální běhy:",
"other": "Máte {{.Count}} aktuálních běhů:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Aktuální běhy"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Máte 0 běhů po termínu."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Máte {{.Count}} běh po termínu pro aktualizaci statusu:",
"few": "Máte {{.Count}} běhů po termínu pro status update:",
"other": "Máte {{.Count}} běhů po termínu pro status update:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Aktualizace statusu po termínu"
},
{
"id": "app.command.execute.error",
"translation": "Není možné provést příkaz."
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Dokončit"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Krátký souhrn zobrazený na časové ose"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Přidat do časové osy běhu"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Existuje {{.Count}} nevyřízený úkol. Opravdu chcete dokončit *{{.RunName}} pro všechny účastníky?",
"few": "Existují **{{.Count}} nevyřízené úkoly**. Opravdu chcete dokončit *{{.RunName}}* pro všechny účastníky?",
"other": "Existuje **{{.Count}} nevyřízených úkoly**. Opravdu chcete dokončit *{{.RunName}}* pro všechny účastníky?"
}
},
{
"id": "app.user.new_run.intro",
"translation": "**Vlastník** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "Název běhu"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Spustit běh"
},
{
"id": "app.user.new_run.title",
"translation": "Spustit playbook"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Popis"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Název"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Přidat úkol"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Přidat nový úkol"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook běh"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Přidat do časové osy běhu"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Souhrn"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max. 64 znaků"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Dokončit běh"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Také označit jako dokončené"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Potvrdit dokončení"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Změn od poslední aktualizace stavu"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Poskytnout aktualizaci pro zúčastněné. Tato zpráva bude odeslána do {{.Count}} kanálu.",
"few": "Poskytnout aktualizaci pro zúčastněné. Tato zpráva bude odeslána do {{.Count}} kanálů.",
"other": "Poskytnout aktualizaci pro zúčastněné. Tato zpráva bude odeslána do {{.Count}} kanálů."
}
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Připomínka pro další aktualizaci"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Příspěvek"
},
{
"id": "app.user.run.update_status.title",
"translation": "Aktualizace stavu"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "zobrazeno, protože úkol byl upraven"
}
]

View file

@ -0,0 +1,234 @@
[
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Du hast eine zugewiesene Aufgabe:",
"other": "Du hast insgesamt {{.Count}} zugewiesene Aufgaben:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Du hast keine zugewiesene Aufgabe."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Du hast eine zugewiesene Aufgabe, die jetzt fällig ist:",
"other": "Du hast {{.Count}} zugewiesene Aufgaben, die jetzt fällig sind:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Deine zugewiesenen Aufgaben"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Gestern fällig"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Fällig vor {{.Count}} Tagen"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Fällig heute"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Fällig morgen",
"other": "Fällig in {{.Count}} Tagen"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Du hast **eine zugewiesene Aufgabe, die nach dem heutige Tag fällig ist**.",
"other": "Du hast **{{.Count}} zugewiesene Aufgaben, die nach dem heutige Tag fällig sind**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Bitte benutze `/playbook todo` um alle deine Aufgaben anzuzeigen."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Du hast keinen aktiven Durchlauf."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Du hast einen aktiven Durchlauf:",
"other": "Du hast {{.Count}} aktive Durchläufe:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Aktive Durchläufe"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Du hast keine überfälligen Durchläufe."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Du hast einen überfälligen Durchlauf für einen Statusaktualisierung:",
"other": "Du hast {{.Count}} überfällige Durchläufe für einen Statusaktualisierung:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Überfällige Statusaktualisierungen"
},
{
"id": "app.command.execute.error",
"translation": "Kann Befehl nicht ausführen."
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} hat eine Statusaktualisierung für [{{.RunName}}]({{.RunURL}}) angefordert. \n"
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here - @{{.Name}} möchten an diesem Durchlauf teilnehmen. Um sie als Teilnehmer hinzuzufügen, füge sie bitte diesem Kanal hinzu.\n"
},
{
"id": "app.user.run.retro_publish",
"translation": "@{{.Name}} hat eine Retrospektive veröffentlicht\n[Ganze Retrospektive ansehen]({{.URL}})\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} nimmt am Durchlauf teil. @{{.Name}} wurde nicht automatisch zu diesem privaten Kanal hinzugefügt, aber jedes Mitglied kann @{{.Name}} hinzufügen.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.Name}} wurde zum Durchlauf von @{{.RequesterName}} hinzugefügt. @{{.Name}} wurde nicht zum Kanal hinzugefügt, da @{{.RequesterName}} kein Mitglied des privaten Kanals ist.\n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} ist ein Teilnehmer und möchte diesem Kanal beitreten. Jedes Mitglied des Kanals kann ihn einladen.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) aktiviert"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} hat die Statusaktualisierungen für [{{.RunName}}]({{.RunURL}}) deaktiviert"
},
{
"id": "app.user.run.update_status.title",
"translation": "Statusaktualisierung"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Nachricht"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Erinnerung an das nächste Update"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in einem Kanal veröffentlicht.",
"other": "Bringe die Beteiligten auf den neuesten Stand. Dieser Beitrag wird in {{.Count}} Kanälen veröffentlicht."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Markiere den Durchlauf auch als beendet"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Durchlauf beenden"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Änderung seit der letzten Aktualisierung"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Beenden des Durchlaufs bestätigen"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Durchlauf beenden"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Es gibt eine **{{.Count}} offene Aufgabe**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?",
"other": "Es gibt **{{.Count}} offene Aufgaben**. Bist du sicher, dass du den Durchlauf *{{.RunName}}* für alle Teilnehmer beenden willst?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Zur Zeitleiste hinzufügen"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Kurze Zusammenfassung auf der Zeitachse"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max. 64 Zeichen"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Zusammenfassung"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Zur Zeitleiste hinzufügen"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook-Durchlauf"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Neue Aufgabe hinzufügen"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Aufgabe hinzufügen"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Beschreibung"
},
{
"id": "app.user.new_run.title",
"translation": "Playbook starten"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Starte Durchlauf"
},
{
"id": "app.user.new_run.run_name",
"translation": "Durchlaufname"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[Klicke hier]({{.RunURL}}), um dein eigenes Playbook zu erstellen."
},
{
"id": "app.user.new_run.intro",
"translation": "**Eigentümer** {{.Username}}"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "angezeigt, weil die Aufgabe geändert wurde"
}
]

View file

@ -0,0 +1,214 @@
[
{
"id": "app.command.execute.error",
"translation": "Unable to execute command."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Overdue Status Updates"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "You have {{.Count}} run overdue for a status update:",
"other": "You have {{.Count}} runs overdue for a status update:"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "You have 0 runs overdue."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Runs in Progress"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "You have {{.Count}} run currently in progress:",
"other": "You have {{.Count}} runs currently in progress:"
}
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "You have 0 runs currently in progress."
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Please use `/playbook todo` to see all your tasks."
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "You have **{{.Count}} assigned task due after today**.",
"other": "You have **{{.Count}} assigned tasks due after today**."
}
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Due in {{.Count}} day",
"other": "Due in {{.Count}} days"
}
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Due today"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Due {{.Count}} days ago"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Due yesterday"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Your assigned tasks"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "You have {{.Count}} assigned task:",
"other": "You have {{.Count}} total assigned tasks:"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "You have {{.Count}} assigned task that is now due:",
"other": "You have {{.Count}} assigned tasks that are now due:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "You have 0 assigned tasks."
},
{
"id": "app.user.new_run.intro",
"translation": "**Owner** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "Run name"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Start run"
},
{
"id": "app.user.new_run.title",
"translation": "Run playbook"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Description"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Add task"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Add new task"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Summary"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 chars"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Short summary shown in the timeline"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish *{{.RunName}}* for all participants?",
"other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Finish"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Confirm finish"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Change since last update"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Finish run"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Also mark the run as finished"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.",
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels."
}
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Reminder for next update"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Post"
},
{
"id": "app.user.run.update_status.title",
"translation": "Status update"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "shown because the task was modified"
}
]

View file

@ -0,0 +1,210 @@
[
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Due in {{.Count}} day",
"other": "Due in {{.Count}} days"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "You have 0 runs overdue."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Runs in Progress"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "You have 0 runs currently in progress."
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Please use `/playbook todo` to see all your tasks."
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Due today"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Due {{.Count}} days ago"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Due yesterday"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Your assigned tasks"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "You have 0 assigned tasks."
},
{
"id": "app.user.new_run.intro",
"translation": "**Owner** {{.Username}}"
},
{
"id": "app.command.execute.error",
"translation": "Unable to execute command."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Overdue Status Updates"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Start run"
},
{
"id": "app.user.new_run.title",
"translation": "Run playbook"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Summary"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 chars"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Short summary shown in the timeline"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?",
"other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Finish run"
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "You have {{.Count}} assigned task that is now due:",
"other": "You have {{.Count}} assigned tasks that are now due:"
}
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Change since last update"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Finish run"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Also mark the run as finished"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "You have {{.Count}} run overdue for a status update:",
"other": "You have {{.Count}} runs overdue for a status update:"
}
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "You have {{.Count}} run currently in progress:",
"other": "You have {{.Count}} runs currently in progress:"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "You have **{{.Count}} assigned task due after today**.",
"other": "You have **{{.Count}} assigned tasks due after today**."
}
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Description"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Add task"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Add new task"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Reminder for next update"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Update status"
},
{
"id": "app.user.run.update_status.title",
"translation": "Status update"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "You have {{.Count}} assigned task:",
"other": "You have {{.Count}} total assigned tasks:"
}
},
{
"id": "app.user.new_run.run_name",
"translation": "Run name"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Confirm finish run"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.",
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels."
}
}
]

View file

@ -0,0 +1,250 @@
[
{
"id": "app.user.digest.tasks.zero_outstanding",
"translation": "Imaš 0 neobavljenih zadataka."
},
{
"id": "app.user.digest.tasks.num_outstanding",
"translation": {
"one": "Imaš {{.Count}} neobavljeni zadatak:",
"few": "Imaš {{.Count}} neobavljena zadatka:",
"other": "Imaš {{.Count}} neobavljenih zadataka:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Tebi dodijeljeni zadaci"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Imaš 0 izvođenja trenutačno u tijeku."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Imaš {{.Count}} izvođenje trenutačno u tijeku:",
"few": "Imaš {{.Count}} izvođenja trenutačno u tijeku:",
"other": "Imaš {{.Count}} izvođenja trenutačno u tijeku:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Izvođenja u tijeku"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Imaš 0 izvođenja s prekoračenjem roka."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Imaš {{.Count}} izvođenje s prekoračenjem roka za aktualiziranje stanja:",
"few": "Imaš {{.Count}} izvođenja s prekoračenjem roka za aktualiziranje stanja:",
"other": "Imaš {{.Count}} izvođenja s prekoračenjem roka za aktualiziranje stanja:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Aktualiziranja stanja prekoračenja roka"
},
{
"id": "app.command.execute.error",
"translation": "Nije moguće izvršiti naredbu."
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Nemaš nijedan dodijeljeni zadatak."
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Imaš {{.Count}} dodijeljen zadatak:",
"few": "Imaš {{.Count}} dodijeljena zadatka:",
"other": "Imaš {{.Count}} dodijeljenih zadataka:"
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Koristi `/playbook todo` za prikaz svih tvojih zadataka."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Imaš {{.Count}} dodijeljen zadatak čiji je rok sada:",
"few": "Imaš {{.Count}} dodijeljena zadatka čiji su rokovi sada:",
"other": "Imaš {{.Count}} dodijeljenih zadataka čiji su rokovi sada:"
}
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Rok: jučer"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Rok: prije {{.Count}} dana"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Rok je danas"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Rok: {{.Count}} dan",
"few": "Rok: {{.Count}} dana",
"other": "Rok: {{.Count}} dana"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Imaš **{{.Count}} dodijeljen zadatak s rokom nakon danas**.",
"few": "Imaš **{{.Count}} dodijeljena zadatka s rokom nakon danas**.",
"other": "Imaš **{{.Count}} dodijeljenih zadataka s rokom nakon danas**."
}
},
{
"id": "app.user.run.request_update",
"translation": "@here @{{.Name}} je zatražio/la aktualiziranje stanja za [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here @{{.Name}} želi sudjelovati u ovom izvođenju. Da bi postali članovi izvođenja, dodaj ih ovom kanalu.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.RequesterName}} je dodao/la @{{.Name}} u izvođenje . Nisu automatski dodani u kanal jer @{{.RequesterName}} nije član kanala, a kanal je privatan.\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} se pridružio/la izvođenju. Nisu automatski dodani na ovaj privatni kanal, ali bilo koji član kanala ih može pozvati da se pridruže.\n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} je sudionik izvođenja i želi se pridružiti ovom kanalu. Svaki član kanala ih može pozvati.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} je aktivirao/la aktualiziranja stanja za [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} je deaktivirao/la aktualiziranja stanja za [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.title",
"translation": "Aktualiziranje stanja"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Objava"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Podsjetnik za sljedeće aktualiziranje"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Obavijesti sudionike o aktualnom stanju. Ova će se objava poslati na {{.Count}} kanal.",
"few": "Obavijesti sudionike o aktualnom stanju. Ova će se objava poslati na {{.Count}} kanala.",
"other": "Obavijesti sudionike o aktualnom stanju. Ova će se objava poslati na {{.Count}} kanala."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Također, označi izvođenje kao završeno"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Završi izvođenje"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Promjena od zadnjeg aktualiziranja"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Potvrdi završavanje"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Završi"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Postoji **{{.Count}} neobavljen zadatak**. Stvarno želiš završiti *{{.RunName}}* za sve sudionike?",
"few": "Postoje **{{.Count}} neobavljena zadatka**. Stvarno želiš završiti *{{.RunName}}* za sve sudionike?",
"other": "Postoji **{{.Count}} neobavljenih zadataka**. Stvarno želiš završiti *{{.RunName}}* za sve sudionike?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Dodaj u vremensku crtu izvođenja"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Kratak sažetak prikazan na vremenskoj crti"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Maks. 64 znakova"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Sažetak"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Dodaj u vremensku crtu izvođenja"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Izvođenje priručnika"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Dodaj novi zadatak"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Dodaj zadatak"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Ime"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Opis"
},
{
"id": "app.user.new_run.title",
"translation": "Priručnik izvođenja"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Pokreni izvođenje"
},
{
"id": "app.user.new_run.run_name",
"translation": "Ime izvođenja"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[Pritisni ovdje]({{.RunURL}}) za izradu vlastitog priručnika."
},
{
"id": "app.user.new_run.intro",
"translation": "**Vlasnik** {{.Username}}"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "prikazano, jer je zadatak promijenjen"
}
]

View file

@ -0,0 +1,230 @@
[
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Önnek van **{{.Count}} Önhöz rendelt feladat aminek a határideje ma után van**.",
"other": "Önnek van **{{.Count}} Önhöz rendelt feladat aminek a határideje ma után van**."
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Önhöz {{.Count}} feladat van hozzárendelve:",
"other": "Önhöz {{.Count}} feladat van hozzárendelve:"
}
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Határidő lejár {{.Count}} nap múlva",
"other": "Határidő lejár {{.Count}} nap múlva"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Önnek {{.Count}} Önhöz rendelt feladata van késedelemben:",
"other": "Önnek {{.Count}} Önhöz rendelt feladata van késedelemben:"
}
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here — @{{.Name}} részt szeretne venni ebben a futásban. Ahhoz, hogy résztvevők lehessenek, adja hozzá őket ehhez a csatornához.\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} kért egy állapot frissítést a [{{.RunName}}]({{.RunURL}}) futásról. \n"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Önhöz 0 feladat van hozzárendelve."
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Önhöz rendelt feladatok"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Tegnap lejárt"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Határidő lejárt {{.Count}} nappal ezelőtt"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Határidő ma"
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Kérem használja a `/playbook todo` parancsot az összes feladat megtekintéséhez."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Önnek 0 futása van éppen folyamatban."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Önnek {{.Count}} futása van lejárt állapot frissítéssel:",
"other": "Önnek {{.Count}} futása van lejárt állapot frissítéssel:"
}
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Önnek {{.Count}} futása van folyamatban:",
"other": "Önnek {{.Count}} futása van folyamatban:"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Önnek 0 futása van késedelemben."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Késedelmes állapot frissítések"
},
{
"id": "app.command.execute.error",
"translation": "Nem sikerült végrehajtani a parancsot."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Futások folyamatban"
},
{
"id": "app.user.run.retro_publish",
"translation": "@{{.Name}} közzétett egy visszatekintőt\n[A teljes visszatekintő megtekintése]({{.URL}})\n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} egy futás résztvevője és csatlakozni szeretne ehhez a csatornához. A csatorna bármelyik tagja meghívhatja.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.Name}} a @{{.RequesterName}} által hozzá lett adva a futáshoz. Nem kerültek automatikusan hozzá a csatornához, mivel @{{.RequesterName}} nem tagja a csatornának, és a csatorna privát.\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} csatlakozott a futáshoz. Nem kerültek automatikusan hozzádásra a privát csatornához, de a csatorna bármelyik tagja meghívhatja őket, hogy csatlakozzanak.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} engedélyezte az állapot frissítést a [{{.RunName}}]({{.RunURL}}) futáshoz"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} letiltotta az állapot frissítést a [{{.RunName}}]({{.RunURL}}) futáshoz"
},
{
"id": "app.user.new_run.intro",
"translation": "**Tulajdonos** {{.Username}}"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Feladat hozzáadása"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Rövid összefoglaló az idővonalon"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Van **{{.Count}} elmaradt feladat**. Biztos, hogy be kívánja fejezni a *{{.RunName}}* futást minden résztvevő számára?",
"other": "Van **{{.Count}} elmaradt feladat**. Biztos, hogy be kívánja fejezni a *{{.RunName}}* futást minden résztvevő számára?"
}
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Tájékoztassa az érdekelt feleket. Ezt a bejegyzést {{.Count}} csatornában fogjuk közzétenni.",
"other": "Tájékoztassa az érdekelt feleket. Ezt a bejegyzést {{.Count}} csatornában fogjuk közzétenni."
}
},
{
"id": "app.user.new_run.run_name",
"translation": "Futás neve"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Futás indítása"
},
{
"id": "app.user.new_run.title",
"translation": "Forgatókönyv indítása"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Leírás"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Név"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Új feladat hozzáadása"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Forgatókönyv futás"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Hozzáadás a futás idővonalához"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Összefoglaló"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 karakter"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Hozzáadás a futás idővonalához"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Befejezés"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Befejezés jóváhagyása"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Módosítások az utolsó frissítés óta"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Futás befejezése"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Továbbá a futás megjelölése befejezettként"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Emlékeztető a következő frissítéshez"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Küldés"
},
{
"id": "app.user.run.update_status.title",
"translation": "Állapot frissítés"
},
{
"id": "app.user.new_run.playbook",
"translation": "Forgatókönyv"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "megjelenítve mert a feladat módosult"
}
]

View file

@ -0,0 +1,202 @@
[
{
"id": "app.command.execute.error",
"translation": "Tidak dapat menjalankan perintah."
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Selesai menjalankan"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Konfirmasi selesai menjalankan"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Menambahkan tugas baru"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Anda memiliki 0 run yang sedang berjalan saat ini."
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Gunakan `/playbook todo` untuk melihat semua tugas Anda."
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Jatuh tempo hari ini"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Karena kemarin"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Jatuh tempo {{.Count}} hari yang lalu"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Tugas yang Anda berikan"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Anda memiliki 0 tugas yang diberikan."
},
{
"id": "app.user.new_run.intro",
"translation": "**Pemilik** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "Jalankan nama"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Mulai jalankan"
},
{
"id": "app.user.new_run.title",
"translation": "Menjalankan buku pedoman"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Deskripsi"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Nama"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Tambahkan tugas"
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Pembaruan Status yang Terlambat"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Anda memiliki 0 tunggakan."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Berjalan dalam Proses"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Menambahkan ke timeline yang sedang berjalan"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Ringkasan"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Maksimal 64 karakter"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Rangkuman singkat yang ditampilkan di timeline"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Menambahkan ke timeline yang sedang berjalan"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} adalah peserta lari dan ingin bergabung dengan saluran ini. Setiap anggota saluran dapat mengundang mereka.\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} meminta pembaruan status untuk [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} menonaktifkan pembaruan status untuk [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} mengaktifkan pembaruan status untuk [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Perubahan sejak pembaruan terakhir"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Selesai menjalankan"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Juga tandai lari sebagai selesai"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Pengingat untuk pembaruan berikutnya"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Perbarui status"
},
{
"id": "app.user.run.update_status.title",
"translation": "Pembaruan status"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "You have {{.Count}} run currently in progress:"
}
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "Due in {{.Count}} day"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "You have **{{.Count}} assigned task due after today**."
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "You have {{.Count}} assigned task that is now due:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "You have {{.Count}} assigned task:"
}
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "You have {{.Count}} run overdue for a status update:"
}
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel."
}
}
]

View file

@ -0,0 +1,206 @@
[
{
"id": "app.user.run.update_status.title",
"translation": "ステータスの更新"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "投稿"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "次回更新のお知らせ"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "関係者に最新情報を提供します。この投稿は {{.Count}} チャンネルにブロードキャストされます。"
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "また、実行を終了としてマークする"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "実行を終了する"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を有効化しました"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "前回更新時からの変更点"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} は [{{.RunName}}]({{.RunURL}}) のステータス更新を無効化しました"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} は [{{.RunName}}]({{.RunURL}}) のステータス変更を要求しました。 \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} は実行の参加者で、このチャンネルに参加したいようです。チャンネルのメンバーなら誰でも招待できます。\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "終了の確認"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "実行を終了する"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "**{{.Count}} 個の未処理のタスク**があります。 すべての参加者の実行 *{{.RunName}}* を終了してもよろしいですか?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "実行タイムラインに追加する"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "タイムラインに表示される短い要約"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "最大 64 文字"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "説明"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "概要"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "実行タイムラインに追加する"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbookを実行"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "新しいタスクを追加"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "タスクを追加"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "名前"
},
{
"id": "app.user.new_run.title",
"translation": "Playbookを実行"
},
{
"id": "app.user.new_run.submit_label",
"translation": "実行開始"
},
{
"id": "app.user.new_run.run_name",
"translation": "実行名"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[ここ]({{.RunURL}}) をクリックして、独自のPlaybookを作成することができます。"
},
{
"id": "app.user.new_run.intro",
"translation": "**オーナー** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "割り当てられたタスクは0件です。"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "割り当てられたタスクは合計で {{.Count}} 件です:"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "現在、期限切れのタスクが {{.Count}} 件割り当てられています:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "あなたに割り当てられたタスク"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "期限は昨日まででした"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "期限 {{.Count}} 日前"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "期限は本日までです"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "{{.Count}} 日後に期限切れになります"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "今日以降に期限切れとなるタスクが **{{.Count}} 件割り当てられています**。"
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "`/playbook todo`を使用して、すべてのタスクを見ることができます。"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "進行中の実行は 0 です。"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "現在、 {{.Count}} の実行が進行中です:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "進行中の実行"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "ステータス更新の期日が過ぎた実行が {{.Count}} あります:"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "期限切れの実行は 0 です。"
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "期限切れステータスの更新"
},
{
"id": "app.command.execute.error",
"translation": "コマンドを実行できませんでした。"
}
]

View file

@ -0,0 +1,202 @@
[
{
"id": "app.user.digest.tasks.due_today",
"translation": "오늘 마감"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "실행 완료"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "실행 완료 확인"
},
{
"id": "app.user.run.request_update",
"translation": "여기 - @{{.Name}}이 [{{.RunName}}]({{.RunURL}})에 대한 상태 업데이트를 요청했습니다. \n"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "만료 {{.Count}}일 전"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "어제 마감"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "할당된 작업"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "할당된 작업이 0개입니다."
},
{
"id": "app.user.new_run.playbook",
"translation": "플레이북"
},
{
"id": "app.user.new_run.run_name",
"translation": "실행 이름"
},
{
"id": "app.user.new_run.submit_label",
"translation": "실행 시작"
},
{
"id": "app.user.new_run.title",
"translation": "플레이북 실행하기"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "설명"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "이름"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "작업 추가"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "새 작업 추가"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "플레이북 실행"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "실행 타임라인에 추가"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "요약"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "마지막 업데이트 이후 변경 사항"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "실행 완료"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "최대 64자"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "타임라인에 표시되는 짧은 요약"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "실행 타임라인에 추가"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "또한 실행을 완료로 표시합니다."
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
},
{
"id": "app.user.new_run.intro",
"translation": "**소유자** {{.Username}}"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "현재 진행 중인 실행이 0건입니다."
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "모든 작업을 보려면 `/플레이북 할 일`을 사용하세요."
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "다음 업데이트 알림"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "업데이트 상태"
},
{
"id": "app.user.run.update_status.title",
"translation": "상태 업데이트"
},
{
"id": "app.command.execute.error",
"translation": "명령을 실행할 수 없습니다."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "연체 상태 업데이트"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "연체된 런이 0개입니다."
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "진행 중 실행"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "Due in {{.Count}} day"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "You have {{.Count}} assigned task:"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "You have {{.Count}} assigned task that is now due:"
}
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel."
}
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "You have {{.Count}} run currently in progress:"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "You have **{{.Count}} assigned task due after today**."
}
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "You have {{.Count}} run overdue for a status update:"
}
}
]

View file

@ -0,0 +1,210 @@
[
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Танд {{.Count}}-н хариуцсан даалгавар байна:",
"other": "Танд нийт {{.Count}}-н хариуцсан даалгавар байна:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Таны хариусан даалгавар"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Өчигдөр эцсийн хугацаа"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Эцсийн хугацаа {{.Count}} өдрийн өмнө"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Өнөөдөр эцсийн хугацаа"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Ганц\nЭцсийн хугацаа {{.Count}} хоног",
"other": "Олон\nЭцсийн хугацаа {{.Count}} хоногт"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Ганц\nТанд **өнөөдрөөс хойш хугацаа хэтрэх томилсон даалгавар {{.Count}} байна**.",
"other": "Олон\nТанд **өнөөдрөөс хойш хугацаа хэтрэх томилсон даалгавар {{.Count}} байна**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "'/playbook todo' -г ашиглаж өөрийн бүх таскаа харна уу."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Танд одоогоор боловсруулагдаж байгаа үйлдэл 0 байна."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Ганц үйлдэл\nТаны {{.Count}} үйлдэл процеслогдож байна:",
"other": "Олон үйлдэл\nТаны {{.Count}} үйлдэл процеслогдож байна:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Үйлдэл хийгдэж байна"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Танд хугацаа хэтэрсэн шинэчлэл үйлдэл 0 байна."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Ганц тоо\nТанд {{.Count}} төлөвийн шинэчлэлтийн хугацаа хэтэрсэн байна:",
"other": "Олон тоо\nТанд {{.Count}} төлөвийн шинэчлэлтийн хугацаа хэтэрсэн байна:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Хугацаа хэтэрсэн төлөвийн мэдээлэл"
},
{
"id": "app.command.execute.error",
"translation": "Командыг биелүүлэх боломжгүй."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "You have {{.Count}} assigned task that is now due:",
"other": "You have {{.Count}} assigned tasks that are now due:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "You have 0 assigned tasks."
},
{
"id": "app.user.new_run.intro",
"translation": "**Owner** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "Run name"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Start run"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Summary"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 chars"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Short summary shown in the timeline"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Add to run timeline"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?",
"other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?"
}
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Finish run"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Confirm finish run"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Change since last update"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Finish run"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Also mark the run as finished"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.",
"other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels."
}
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Reminder for next update"
},
{
"id": "app.user.new_run.title",
"translation": "Run playbook"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Description"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Name"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Add task"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Add new task"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Update status"
},
{
"id": "app.user.run.update_status.title",
"translation": "Status update"
}
]

View file

@ -0,0 +1,210 @@
[
{
"id": "app.command.execute.error",
"translation": "Kunne ikke utføre kommandoen."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Forfalte statusoppdateringer"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Du har {{.Count}} overskredet fristen for en statusoppdatering:",
"other": "Du har {{.Count}} kjøringer som er forsinket for en statusoppdatering:"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Du har 0 kjøringer som er forsinket."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Du har 0 kjøringer som pågår for øyeblikket."
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Bruk `/playbook todo` for å se alle oppgavene dine."
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Forfall i dag"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Forfall {{.Count}} dager siden"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Forfall i går"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Dine tildelte oppgaver"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Du har {{.Count}} tildelt en oppgave:",
"other": "Du har {{.Count}} totalt tildelte oppgaver:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Du har 0 tildelte oppgaver."
},
{
"id": "app.user.new_run.intro",
"translation": "**Eier** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "Kjør navn"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Beskrivelse"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Navn"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Legg til oppgave"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Legg til ny oppgave"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Legg til i tidslinjen for kjøring"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Sammendrag"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Maks 64 tegn"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Legg til i tidslinjen for kjøring"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Fullfør løpet"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Bekreft målkjøring"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} er en løpsdeltaker og ønsker å bli med i denne kanalen. Alle medlemmer av kanalen kan invitere dem.\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} ba om en statusoppdatering for [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} deaktiverte statusoppdateringene for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Endring siden forrige oppdatering"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Fullfør løpet"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Merk også kjøringen som fullført"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Påminnelse om neste oppdatering"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Oppdater status"
},
{
"id": "app.user.run.update_status.title",
"translation": "Statusoppdatering"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Du har {{.Count}} kjøring som pågår for øyeblikket:",
"other": "Du har {{.Count}} kjøringer som pågår for øyeblikket:"
}
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Forfall om {{.Count}} dag",
"other": "Forfall om {{.Count}} dager"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Du har {{.Count}} tildelt en oppgave som nå forfaller:",
"other": "Du har {{.Count}} tildelte oppgaver som nå forfaller:"
}
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Det er **{{.Count}} utestående oppgave**. Er du sikker på at du vil fullføre løpet *{{.RunName}}* for alle deltakerne?",
"other": "Det er **{{.Count}} utestående oppgaver**. Er du sikker på at du vil fullføre løpet *{{.RunName}}* for alle deltakerne?"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Pågående løp"
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Du har **{{.Count}} tildelte oppgaver som forfaller etter i dag**.",
"other": "Du har **{{.Count}} tildelte oppgaver som forfaller etter i dag**."
}
},
{
"id": "app.user.new_run.submit_label",
"translation": "Start kjøring"
},
{
"id": "app.user.new_run.title",
"translation": "Kjør Playbook"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Run"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Kort sammendrag vist i tidslinjen"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} aktiverte statusoppdateringene for [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Gi en oppdatering til interessentene. Dette innlegget vil bli sendt til kanalen {{.Count}}.",
"other": "Gi en oppdatering til interessentene. Dette innlegget vil bli sendt til {{.Count}} kanaler."
}
}
]

View file

@ -0,0 +1,218 @@
[
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Je hebt **{{.Count}} taak toegewezen gekregen na vandaag**.",
"other": "Je hebt **{{.Count}} taken toegewezen gekregen na vandaag**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Gebruik `/playbook todo` om al jouw taken te zien."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Je hebt 0 lopende uitvoeringen."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Je hebt {{.Count}} uitvoering lopen:",
"other": "Je hebt {{.Count}} uitvoeringen lopen:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Lopende uitvoeringen"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Je hebt 0 achterstallige uitvoeringen."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Je hebt {{.Count}} uitvoering die een status update nodig hebben:",
"other": "Je hebt {{.Count}} uitvoeringen die een status update nodig hebben:"
}
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Samenvatting"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Voeg nieuwe taak toe"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Voeg taak toe"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Naam"
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Achterstallige statusupdates"
},
{
"id": "app.command.execute.error",
"translation": "Kan commando niet uitvoeren."
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Verloopt binnen {{.Count}} dag",
"other": "Verloopt binnen {{.Count}} dagen"
}
},
{
"id": "app.user.run.update_status.title",
"translation": "Status bijwerken"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Verzenden"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Herinnering voor de volgende update"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Geef een update aan de belanghebbenden. Dit bericht zal worden uitgezonden naar {{.Count}} kanaal.",
"other": "Geef een update aan de belanghebbenden. Dit bericht zal worden uitgezonden naar {{.Count}} kanalen."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "De uitvoering ook markeren als beëindigd"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Uitvoering beëindigen"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Wijziging sinds laatste update"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} heeft de statusupdates voor [{{.RunName}}]({{.RunURL}}) uitgeschakeld"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} heeft de statusupdates voor [{{.RunName}}]({{.RunURL}}) uitgeschakeld"
},
{
"id": "app.user.run.request_update",
"translation": "@hier - @{{.Name}} vroeg een statusupdate aan voor [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} is een deelnemer aan de uitvoering en wil lid worden van dit kanaal. Elk lid van het kanaal kan hen uitnodigen.\n"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Beëindigen bevestigen"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Er is **{{.Count}} openstaande taak**. Weet je zeker dat je *{{.RunName}}* voor alle deelnemer wilt beëindigen?",
"other": "Er is **{{.Count}} openstaande taken**. Weet je zeker dat je *{{.RunName}}* voor alle deelnemer wilt beëindigen?"
}
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Voltooien"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Toevoegen aan uitvoeringstijdlijn"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Korte samenvatting in de tijdlijn"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Maximaal 64 tekens"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Toevoegen aan uitvoeringstijdlijn"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook Uitvoering"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Omschrijving"
},
{
"id": "app.user.new_run.title",
"translation": "Playbook uitvoeren"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Uitvoering starten"
},
{
"id": "app.user.new_run.run_name",
"translation": "Naam van de uitvoering"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[Klik hier]({{.RunURL}}) om je eigen playbook te maken."
},
{
"id": "app.user.new_run.intro",
"translation": "**Eigenaar** {{.Username}}"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Je hebt 0 toegewezen taken."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Je hebt {{.Count}} een taak toegewezen gekregen die nu verlopen:",
"other": "Je hebt {{.Count}} taken toegewezen gekregen die nu verlopen:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Je hebt {{.Count}} toegewezen taak:",
"other": "Je hebt {{.Count}} toegewezen taken:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Jouw toegewezen taken"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Verliep gisteren"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Verliep {{.Count}} dagen geleden"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Verloopt vandaag"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "weergegeven omdat de taak gewijzigd werd"
}
]

View file

@ -0,0 +1,242 @@
[
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Masz 0 przydzielonych zadań."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Masz {{.Count}} przydzielone zadanie, którego termin wykonania właśnie upływa:",
"few": "Masz {{.Count}} przydzielone zadania, których termin wykonania właśnie upływa:",
"many": "Masz {{.Count}} przydzielonych zadań, których termin wykonania właśnie upływa:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Masz {{.Count}} przydzielone zadanie:",
"few": "Masz {{.Count}} przydzielone zadania:",
"many": "Masz {{.Count}} przydzielonych zadań:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Twoje przydzielone zadania"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Termin na wczoraj"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Termin {{.Count}} dni temu"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Do zapłaty dzisiaj"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Termin płatności za {{.Count}} dzień",
"few": "Termin płatności za {{.Count}} dni",
"many": "Termin płatności za {{.Count}} dni"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Masz **{{.Count}} przydzielone zadanie, którego termin wykonania wypada po dzisiejszym dniu**.",
"few": "Masz **{{.Count}} przydzielone zadania, których termin wykonania wypada po dzisiejszym dniu**.",
"many": "Masz **{{.Count}} przydzielonych zadań, których termin wykonania wypada po dzisiejszym dniu**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Użyj `/playbook todo`, aby zobaczyć wszystkie swoje zadania."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Obecnie masz 0 uruchomień w trakcie."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Masz {{.Count}} uruchomienie w toku:",
"few": "Masz {{.Count}} uruchomienia w toku:",
"many": "Masz {{.Count}} uruchomień w toku:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Uruchomienia w Trakcie"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Masz 0 zaległych uruchomień."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Masz {{.Count}} zaległość w aktualizacji statusu:",
"few": "Masz {{.Count}} zaległości w aktualizacji statusu:",
"many": "Masz {{.Count}} zaległości w aktualizacji statusu:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Zaległe aktualizacje statusu"
},
{
"id": "app.command.execute.error",
"translation": "Nie można wykonać polecenia."
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} zażądał aktualizacji statusu dla [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here - @{.Name}} chce wziąć udział w tym uruchomieniu. Aby uwzględnić ich jako uczestników, dodaj ich do tego kanału.\n"
},
{
"id": "app.user.run.retro_publish",
"translation": "@{.Name}} opublikował retrospektywę\n[Zobacz pełną retrospektywę]({{.URL}}).\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} dołączył do przebiegu. Nie zostali oni automatycznie dodani do tego prywatnego kanału, ale każdy członek kanału może ich zaprosić do przyłączenia się.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.Name}} został dodany do przebiegu przez @{{.RequesterName}}. Nie zostali oni automatycznie dodani do kanału, ponieważ @{{.RequesterName}} nie jest członkiem kanału, a kanał jest prywatny.\n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} jest uczestnikiem uruchomienia i chce dołączyć do tego kanału. Każdy członek kanału może go zaprosić.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} włączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} wyłączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbok"
},
{
"id": "app.user.run.update_status.title",
"translation": "Aktualizacje statusu"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Post"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Przypomnienie o następnej aktualizacji"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanał.",
"few": "Przedstawić aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanały.",
"many": "Przedstawić aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanałów."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Oznacz również uruchomienie jako zakończone"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Zakończ uruchomienie"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Zmiana od ostatniej aktualizacji"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Potwierdź zakończenie"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Zakończ"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Istnieje **{{.Count}} zaległe zadanie**. Czy na pewno chcesz ukończyć *{{.RunName}}* dla wszystkich uczestników?",
"few": "Istnieją **{{.Count}} zaległe zadania**. Czy na pewno chcesz ukończyć *{{.RunName}}* dla wszystkich uczestników?",
"many": "Istnieje **{{.Count}} zaległych zadań**. Czy na pewno chcesz ukończyć *{{.RunName}}* dla wszystkich uczestników?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Dodaj do osi czasu uruchomienia"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Krótkie podsumowanie widoczne na osi czasu"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Maksymalnie 64 znaki"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Podsumowanie"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Dodaj do osi czasu uruchomienia"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Uruchomienie Playbooka"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Dodaj nowe zadanie"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Dodaj zadanie"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Nazwa"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Opis"
},
{
"id": "app.user.new_run.title",
"translation": "Uruchom playbook"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Rozpocznij uruchomienie"
},
{
"id": "app.user.new_run.run_name",
"translation": "Nazwa uruchomienia"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[Kliknij tutaj]({{.RunURL}}), aby stworzyć własny playbook."
},
{
"id": "app.user.new_run.intro",
"translation": "**Właściciel** {{.Username}}"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "ponieważ zadanie zostało zmodyfikowane"
}
]

View file

@ -0,0 +1,230 @@
[
{
"id": "app.user.digest.tasks.zero_outstanding",
"translation": "У вас 0 нерешенных задач."
},
{
"id": "app.user.digest.tasks.num_outstanding",
"translation": {
"one": "У вас {{.Count}} нерешенная задача:",
"few": "У вас {{.Count}} нерешенных задачи:",
"many": "У вас {{.Count}} нерешенных задач:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Ваши назначенные задачи"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "В настоящее время у вас нет запусков."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "У вас запущен {{.Count}}:",
"few": "У вас запущено {{.Count}}:",
"many": "У вас запущены {{.Count}}:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Выполняется"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "У вас просрочено 0 запусков."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "У вас есть {{.Count}} просроченный запуск для обновления статуса:",
"few": "У вас есть {{.Count}} просроченных запуска для обновления статуса:",
"many": "У вас есть {{.Count}} просроченных запусков для обновления статуса:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Просроченные обновления статуса"
},
{
"id": "app.command.execute.error",
"translation": "Невозможно выполнить команду."
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Вчера"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Срок выполнения {{.Count}} дн. назад"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Сегодня"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Через {{.Count}} день",
"few": "Через {{.Count}} дня",
"many": "Через {{.Count}} дней"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "У вас есть **{{.Count}} назначенная задача, которую необходимо выполнить после сегодняшнего дня**.",
"few": "У вас есть **{{.Count}} назначенные задачи, которые необходимо выполнить после сегодняшнего дня**.",
"many": "У вас есть **{{.Count}} назначенных задач, которые необходимо выполнить после сегодняшнего дня**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Пожалуйста, используйте `/playbook todo`, чтобы увидеть все свои задачи."
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "У вас есть {{.Count}} назначенная задача:",
"few": "У вас есть {{.Count}} назначенные задачи:",
"many": "У вас есть {{.Count}} назначенных задач:"
}
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "У вас есть {{.Count}} назначенная задача, которая должна быть выполнена сейчас:",
"few": "У вас есть {{.Count}} назначенные задачи, которые должны быть выполнены сейчас:",
"many": "У вас есть {{.Count}} назначенных задач, которые должны быть выполнены сейчас:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "У вас нет назначенных задач."
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} включил обновление статуса для этого запуска"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} отключил обновление статуса для этого запуска"
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} запросил обновление статуса. \n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} является участником запуска и хочет присоединиться к этому каналу. Любой участник канала может пригласить его.\n"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Изменения с момента последнего обновления"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "",
"few": "",
"many": "Предоставь обновленную информацию заинтересованным сторонам. Этот пост будет транслироваться на каналах {{.Count}}."
}
},
{
"id": "app.user.run.update_status.title",
"translation": "Обновление статуса"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Подтверди финишную пробежку"
},
{
"id": "app.user.new_run.intro",
"translation": "**Владелец** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Пособие"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Финишный рывок"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Также пометьте выполнение как завершенное"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Напоминание о следующем обновлении"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Обновление статуса"
},
{
"id": "app.user.new_run.run_name",
"translation": "Название бега"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Стартовый забег"
},
{
"id": "app.user.new_run.title",
"translation": "Запусти сценарий"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Описание"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Имя"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Добавь задание"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Добавь новое задание"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Пособие \"Бег"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Добавить в график выполнения"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Резюме"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Максимум 64 символа"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Краткое содержание, отображаемое на временной шкале"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Добавить в график выполнения"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "",
"few": "",
"many": "Есть **{{.Count}} невыполненных заданий**. Ты уверен, что хочешь завершить забег *{{.RunName}}* для всех участников?"
}
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Финишный рывок"
}
]

View file

@ -0,0 +1,230 @@
[
{
"id": "app.command.execute.error",
"translation": "Kunde inte utföra kommandot."
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Status om förseningar"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Du har 0 tilldelade uppgifter."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Du har {{.Count}} tilldelad uppgift som nu är försenad:",
"other": "Du har {{.Count}} tilldelade uppgifter som nu är försenade:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "Du har tilldelats {{.Count}} uppgift:",
"other": "Du har tilldelats totalt {{.Count}} uppgifter:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Dina tilldelade uppgifter"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Skulle utförts igår"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Försenad {{.Count}} dagar"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Ska utföras idag"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "Ska utföras inom {{.Count}} dag",
"other": "Ska utföras inom {{.Count}} dagar"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Du har **{{.Count}} tilldelad uppgift som ska utföras idag**.",
"other": "Du har **{{.Count}} tilldelade uppgifter som ska utföras idag**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Använd `/playbook todo` för att se alla dina uppgifter."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Du har 0 pågående körningar."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Du har {{.Count}} körning som för närvarande pågår:",
"other": "Du har {{.Count}} körningar som för närvarande pågår:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Körningar som pågår"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "Du har 0 försenade körningar."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Du har {{.Count}} körning som borde ha fått en statusuppdatering:",
"other": "Du har {{.Count}} körningar som borde ha fått en statusuppdatering:"
}
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} begärde en statusuppdatering för [{{.RunName}}]({{.RunURL}}). \n"
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here - @{{.Name}} vill delta i denna körning. Om du vill inkludera dem som deltagare lägger du till dem i den här kanalen.\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} har anslutit till körningen. De har inte lagts till i den privata kanalen men vilken medlem om helst kan bjuda in dem att ansluta.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.Name}} har lagts till i körningen av @{{.RequesterName}}. De har inte blivit automatiskt tillagda i kanalen eftersom @{{.RequesterName}} inte är en kanalmedlem och detta är en privat kanal.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} aktiverade statusuppdateringar för [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} inaktiverade statusuppdateringarna för [{{.RunName}}]({{.RunURL}})"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} är en deltagare i en körning och vill gå med i den här kanalen. Alla medlemmar i kanalen kan bjuda in dem.\n"
},
{
"id": "app.user.run.update_status.title",
"translation": "Statusuppdateringar"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Skicka"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Påminnelse om nästa uppdatering"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Ge en uppdatering till intressenterna. Detta inlägg kommer att sändas till {{.Count}} kanal.",
"other": "Ge en uppdatering till intressenterna. Detta inlägg kommer att sändas till {{.Count}} kanaler."
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Markera också körningen som avslutad"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Slutför körningen"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Förändring sedan den senaste uppdateringen"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Bekräfta att avsluta körningen"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Slutför körningen"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Det finns **{{.Count}} utestående uppgift**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?",
"other": "Det finns **{{.Count}} utestående uppgifter**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?"
}
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Lägg till i tidslinjen för körning"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Kort sammanfattning som visas i tidslinjen"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "Max 64 tecken"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Sammanfattning"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Lägg till i tidslinjen för körning"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Kör Playbook"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Lägg till en ny uppgift"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Lägg till uppgift"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Namn"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Beskrivning"
},
{
"id": "app.user.new_run.title",
"translation": "Kör playbook"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Starta körning"
},
{
"id": "app.user.new_run.run_name",
"translation": "Namn på körning"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "[Klicka här]({{.RunURL}}) för att skapa din egen playbook."
},
{
"id": "app.user.new_run.intro",
"translation": "**Ägare** {{.Username}}"
},
{
"id": "playbooks.checklist.condition.reason.modified",
"translation": "visas eftersom uppgiften ändrades"
}
]

View file

@ -0,0 +1,230 @@
[
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "Atanmış bir göreviniz yok."
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"one": "Süresi dolmuş {{.Count}} atanmış göreviniz var:",
"other": "Süresi dolmuş {{.Count}} atanmış göreviniz var:"
}
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"one": "{{.Count}} atanmış göreviniz var:",
"other": "{{.Count}} atanmış göreviniz var:"
}
},
{
"id": "app.user.digest.tasks.heading",
"translation": "Atanmış görevleriniz"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "Süresi dün doldu"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "Süresi {{.Count}} gün önce doldu"
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "Süresi bugün dolacak"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"one": "{{.Count}} gün içinde süresi dolacak",
"other": "{{.Count}} gün içinde süresi dolacak"
}
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"one": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**.",
"other": "Bugünden sonra süresi dolacak **{{.Count}} göreviniz var**."
}
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "Tüm görevlerinizi görüntülemek için `/playbook todo` kullanın."
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "Süren 0 çalışmanız var."
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"one": "Süren {{.Count}} çalışmanız var:",
"other": "Süren {{.Count}} çalışmanız var:"
}
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "Süren çalışmalar"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "0 çalışma gecikmeniz var."
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"one": "Bir durum güncellemesi için {{.Count}} çalışma gecikmeniz var:",
"other": "Bir durum güncellemesi için {{.Count}} çalışma gecikmeniz var:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "Gecikmiş durum güncellemeleri"
},
{
"id": "app.command.execute.error",
"translation": "Komut yürütülemedi."
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}}, [{{.RunName}}]({{.RunURL}}) için bir durum güncellemesi istedi. \n"
},
{
"id": "app.user.run.request_get_involved",
"translation": "@here — @{{.Name}} bu oyuna katılmak istiyor. Oyuna katılabilmesi için lütfen onu bu kanala ekleyin.\n"
},
{
"id": "app.user.run.retro_publish",
"translation": "@{{.Name}} bir geçmiş değerlendirmesi yayınladı\n[Geçmiş değerlendirmesine bakın]({{.URL}})\n"
},
{
"id": "app.user.run.joined_run_channel_private_participate",
"translation": "@{{.Name}} oyuna katıldı. Bu özel kanala otomatik olarak eklenmedi, ancak kanalın herhangi bir üyesi onu katılmaya davet edebilir.\n"
},
{
"id": "app.user.run.joined_run_channel_private_add_participant",
"translation": "@{{.Name}}, @{{.RequesterName}} tarafından oyuna eklendi. @{{.RequesterName}} bir kanal üyesi olmadığından ve kanal özel olduğundan otomatik olarak kanala eklenmedi.\n"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} bir oyun katılımcısı ve bu kanala katılmak istiyor. Kanalın herhangi bir üyesi onu çağırabilir.\n"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini etkinleştirdi"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}}, [{{.RunName}}]({{.RunURL}}) için durum güncellemelerini devre dışı bıraktı"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "Oyun zaman akışına ekle"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "Zaman akışında görüntülenecek kısa açıklama"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "En fazla 64 karakter"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "Özet"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "Oyun zaman akışı ekle"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Senaryo oyunu"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "Yeni görev ekle"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "Görev ekle"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "Ad"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "Açıklama"
},
{
"id": "app.user.new_run.title",
"translation": "Senaryoyu oyna"
},
{
"id": "app.user.new_run.submit_label",
"translation": "Oyunu başlat"
},
{
"id": "app.user.new_run.run_name",
"translation": "Oyun adı"
},
{
"id": "app.user.new_run.playbook",
"translation": "Senaryo"
},
{
"id": "app.user.new_run.new_playbook",
"translation": "Kendi senaryonuzu oluşturmak için [buraya tıklayın]({{.RunURL}})."
},
{
"id": "app.user.new_run.intro",
"translation": "**Sahibi** {{.Username}}"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"one": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?",
"other": "Bekleyen **{{.Count}} görev** var. *{{.RunName}}* oyununu tüm katılımcılar için tamamlamak istediğinize emin misiniz?"
}
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "Ayrıca oyunu da tamamlanmış olarak işaretle"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "Oyunu tamamla"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "Oyunu tamamlamayı onayla"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "Oyunu tamamla"
},
{
"id": "app.user.run.update_status.title",
"translation": "Durum güncellemesi"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "Gönder"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "Sonraki güncelleme anımsatıcısı"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"one": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak.",
"other": "Paydaşlara bir güncelleme duyurun. Bu gönderi {{.Count}} kanalında yayınlanacak."
}
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "Son güncellemeden sonraki değişiklik"
}
]

View file

@ -0,0 +1,202 @@
[
{
"id": "app.command.execute.error",
"translation": "无法执行命令。"
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "逾期状态更新"
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "您的 {{.Count}} 流程已逾期,无法更新状态:"
}
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "您有 0 次流程逾期。"
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "您目前有 0 个流程正在进行中。"
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "请使用 `/playbook todo` 查看所有任务。"
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "您有**{{.Count}}项分配的任务在今天之后到期**。"
}
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "{{.Count}}天后到期"
}
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "今天到期"
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "您有 {{.Count}} 分配的任务,现在到期:"
}
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "您有 0 项指定任务。"
},
{
"id": "app.user.new_run.intro",
"translation": "**所有者** {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "流程名称"
},
{
"id": "app.user.new_run.submit_label",
"translation": "开始流程"
},
{
"id": "app.user.new_run.title",
"translation": "流程 Playbook"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "说明"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "名称"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "摘要"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "最多 64 个字符"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "时间轴中显示的简短摘要"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "添加到流程时间轴"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "完成流程"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "有**{{.Count}}项任务尚未完成**。您确定要完成所有参与者的流程 *{{.RunName}}* 吗?"
}
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "确认完成流程"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} 启用 [{{.RunName}}]({{.RunURL}}) 的状态更新"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "自上次更新以来的变化"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "完成流程"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "同时将流程标记为已完成"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "更新状态"
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "流程进行中"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "您当前正在进行 {{.Count}} 流程:"
}
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "{{.Count}}天前到期"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "昨天到期"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "您的指定任务"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "您共有 {{.Count}} 项已分配任务:"
}
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "添加任务"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "添加新任务"
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook 流程"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "添加到流程时间轴"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} 是一名流程参与者,希望加入此频道。频道的任何成员都可以邀请他们。\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here - @{{.Name}} 请求更新 [{{.RunName}}]({{.RunURL}}) 的状态。 \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} 禁用了 [{{.RunName}}]({{.RunURL}}) 的状态更新"
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "向利益相关者提供最新信息。本职位将在 {{.Count}} 频道上发布。"
}
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "下次更新提醒"
},
{
"id": "app.user.run.update_status.title",
"translation": "状态更新"
}
]

View file

@ -0,0 +1,202 @@
[
{
"id": "app.command.execute.error",
"translation": "無法執行指令。"
},
{
"id": "app.user.digest.overdue_status_updates.zero_overdue",
"translation": "您有 0 次逾期。"
},
{
"id": "app.user.digest.runs_in_progress.heading",
"translation": "執行中"
},
{
"id": "app.user.digest.runs_in_progress.num_in_progress",
"translation": {
"other": "你目前有 {{.Count}} 項正在進行中:"
}
},
{
"id": "app.user.digest.runs_in_progress.zero_in_progress",
"translation": "你目前有0項正在執行中。"
},
{
"id": "app.user.digest.tasks.all_tasks_command",
"translation": "請使用 `/playbook todo`查看你所有的任務。"
},
{
"id": "app.user.digest.tasks.due_in_x_days",
"translation": {
"other": "{{.Count}} 天後到期"
}
},
{
"id": "app.user.digest.tasks.due_today",
"translation": "今天到期"
},
{
"id": "app.user.digest.tasks.due_x_days_ago",
"translation": "{{.Count}} 天前到期"
},
{
"id": "app.user.digest.tasks.due_yesterday",
"translation": "昨天到期"
},
{
"id": "app.user.digest.tasks.heading",
"translation": "您分配的任務"
},
{
"id": "app.user.run.add_checklist_item.description",
"translation": "描述"
},
{
"id": "app.user.digest.tasks.zero_assigned",
"translation": "你目前沒有任何指派的任務。"
},
{
"id": "app.user.new_run.intro",
"translation": "擁有者 {{.Username}}"
},
{
"id": "app.user.new_run.playbook",
"translation": "Playbook"
},
{
"id": "app.user.new_run.run_name",
"translation": "執行名稱"
},
{
"id": "app.user.new_run.title",
"translation": "執行playbook"
},
{
"id": "app.user.run.add_checklist_item.name",
"translation": "名稱"
},
{
"id": "app.user.run.add_checklist_item.title",
"translation": "增加新任務"
},
{
"id": "app.user.digest.tasks.num_assigned",
"translation": {
"other": "你總共分配了 {{.Count}} 個任務:"
}
},
{
"id": "app.user.digest.overdue_status_updates.heading",
"translation": "過期狀態更新"
},
{
"id": "app.user.digest.tasks.num_assigned_due_until_today",
"translation": {
"other": "你有 {{.Count}} 個指派的任務現在已經到期:"
}
},
{
"id": "app.user.digest.overdue_status_updates.num_overdue",
"translation": {
"other": "您的狀態更新已逾期 {{.Count}} 次:"
}
},
{
"id": "app.user.new_run.submit_label",
"translation": "開始執行"
},
{
"id": "app.user.run.add_checklist_item.submit_label",
"translation": "新增任務"
},
{
"id": "app.user.digest.tasks.due_after_today",
"translation": {
"other": "您有 **{{.Count}} 個分配的任務在今天之後到期**。"
}
},
{
"id": "app.user.run.add_to_timeline.playbook_run",
"translation": "Playbook 執行"
},
{
"id": "app.user.run.add_to_timeline.submit_label",
"translation": "新增到執行時間軸"
},
{
"id": "app.user.run.add_to_timeline.summary",
"translation": "摘要"
},
{
"id": "app.user.run.add_to_timeline.summary.help",
"translation": "最多 64 個字元"
},
{
"id": "app.user.run.add_to_timeline.summary.placeholder",
"translation": "在時間軸中顯示簡短摘要"
},
{
"id": "app.user.run.add_to_timeline.title",
"translation": "新增至執行時間軸"
},
{
"id": "app.user.run.confirm_finish.submit_label",
"translation": "執行完成"
},
{
"id": "app.user.run.confirm_finish.title",
"translation": "確認完程執行"
},
{
"id": "app.user.run.update_status.finish_run",
"translation": "執行完成"
},
{
"id": "app.user.run.update_status.finish_run.placeholder",
"translation": "同時將執行標記為已完成"
},
{
"id": "app.user.run.request_join_channel",
"translation": "@{{.Name}} 是執行參與者,想要加入此頻道。該頻道的任何成員都可以邀請他們。\n"
},
{
"id": "app.user.run.request_update",
"translation": "@here — @{{.Name}} 請求 [{{.RunName}}]({{.RunURL}}) 的狀態更新。 \n"
},
{
"id": "app.user.run.status_disable",
"translation": "@{{.Username}} 禁用了 [{{.RunName}}]({{.RunURL}}) 的狀態更新"
},
{
"id": "app.user.run.status_enable",
"translation": "@{{.Username}} 啟用了 [{{.RunName}}]({{.RunURL}}) 的狀態更新"
},
{
"id": "app.user.run.update_status.title",
"translation": "更新狀態"
},
{
"id": "app.user.run.update_status.change_since_last_update",
"translation": "自上次更新以來的異動"
},
{
"id": "app.user.run.update_status.reminder_for_next_update",
"translation": "下次更新提醒"
},
{
"id": "app.user.run.update_status.submit_label",
"translation": "更新狀態"
},
{
"id": "app.user.run.confirm_finish.num_outstanding",
"translation": {
"other": "有 **{{.Count}} 個未完成的任務**。您確定要為所有參與者完成跑步 *{{.RunName}}* 嗎?"
}
},
{
"id": "app.user.run.update_status.num_channel",
"translation": {
"other": "向利益相關者提供更新。此文章將被廣播到 {{.Count}} 個頻道。"
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View file

@ -0,0 +1,27 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_15_126" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="5" width="87" height="81">
<path fill-rule="evenodd" clip-rule="evenodd" d="M89.786 5H3V85.512H57.23C55.9387 82.364 55.2762 78.9936 55.28 75.591C55.28 61.154 66.984 49.451 81.421 49.451C84.346 49.451 87.16 49.931 89.786 50.818V5Z" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_15_126)">
<path d="M61.5361 37.4767H80.9221C81.8017 37.4767 82.6453 37.8261 83.2672 38.448C83.8892 39.07 84.2386 39.9135 84.2386 40.7931V58.4513C84.2386 65.1828 78.7818 70.6388 72.0503 70.6388H71.9932C65.2617 70.6405 59.804 65.1845 59.8031 58.453V39.2097C59.8031 38.2524 60.5788 37.4767 61.5361 37.4767ZM74.6386 33.9864C76.7217 33.9864 78.7195 33.1588 80.1925 31.6858C81.6655 30.2128 82.493 28.215 82.493 26.1319C82.493 24.0488 81.6655 22.051 80.1925 20.578C78.7195 19.105 76.7217 18.2774 74.6386 18.2774C72.5554 18.2774 70.5576 19.105 69.0846 20.578C67.6116 22.051 66.7841 24.0488 66.7841 26.1319C66.7841 28.215 67.6116 30.2128 69.0846 31.6858C70.5576 33.1588 72.5554 33.9864 74.6386 33.9864Z" fill="#5059C9"/>
<path d="M50.2039 33.9864C56.4693 33.9864 61.5482 28.9066 61.5482 22.6403C61.5482 16.3758 56.4693 11.296 50.2039 11.296C43.9385 11.296 38.8596 16.3749 38.8596 22.6412C38.8596 28.9066 43.9385 33.9864 50.2039 33.9864ZM65.3308 37.4767H33.331C32.4618 37.498 31.6366 37.8634 31.0367 38.4927C30.4367 39.122 30.1111 39.9637 30.1313 40.8329V60.9739C29.8788 71.833 38.4705 80.8459 49.3305 81.1114C60.1905 80.8459 68.7831 71.833 68.5297 60.973V40.8329C68.5497 39.9638 68.2239 39.1224 67.624 38.4932C67.0241 37.8641 66.199 37.4988 65.33 37.4775L65.3308 37.4767Z" fill="#7B83EB"/>
<path opacity="0.1" d="M51.0765 37.4767V65.6999C51.0722 66.3328 50.881 66.9503 50.527 67.475C50.173 67.9996 49.6719 68.408 49.0866 68.6489C48.7061 68.8106 48.2962 68.8936 47.882 68.8936H31.6672C31.4377 68.3213 31.234 67.739 31.0566 67.1485C30.4455 65.1456 30.1337 63.0635 30.1313 60.9695V40.8286C30.1109 39.9606 30.4358 39.12 31.0348 38.4915C31.6338 37.863 32.4579 37.498 33.3258 37.4767H51.0765Z" fill="black"/>
<path opacity="0.2" d="M49.3305 37.4767V67.4451C49.3305 67.8585 49.2475 68.2684 49.0866 68.6489C48.8456 69.2342 48.4371 69.7353 47.9123 70.0893C47.3875 70.4433 46.7698 70.6345 46.1368 70.6388H32.4878C32.1896 70.069 31.9158 69.4867 31.6672 68.8936C31.4327 68.3232 31.2289 67.7406 31.0566 67.1485C30.4455 65.1456 30.1337 63.0635 30.1313 60.9695V40.8286C30.1109 39.9606 30.4358 39.12 31.0348 38.4915C31.6338 37.863 32.4579 37.498 33.3258 37.4767H49.3305Z" fill="black"/>
<path opacity="0.2" d="M49.3305 37.4767V63.9539C49.3242 64.7991 48.9857 65.6078 48.3881 66.2055C47.7906 66.8032 46.982 67.1419 46.1368 67.1485H31.0566C30.4455 65.1456 30.1337 63.0635 30.1313 60.9695V40.8286C30.1109 39.9606 30.4358 39.12 31.0348 38.4915C31.6338 37.863 32.4579 37.498 33.3258 37.4767H49.3305Z" fill="black"/>
<path opacity="0.2" d="M47.5854 37.4767V63.9539C47.579 64.7991 47.2405 65.6078 46.643 66.2055C46.0455 66.8032 45.2368 67.1419 44.3917 67.1485H31.0566C30.4455 65.1456 30.1337 63.0635 30.1313 60.9695V40.8286C30.1109 39.9606 30.4358 39.12 31.0348 38.4915C31.6338 37.863 32.4579 37.498 33.3258 37.4767H47.5854Z" fill="black"/>
<path opacity="0.1" d="M51.0765 28.4534V33.9509C50.779 33.9682 50.5006 33.9855 50.2031 33.9855C49.9065 33.9855 49.6271 33.9682 49.3305 33.9509C48.7414 33.9118 48.1572 33.8183 47.5854 33.6716C45.8429 33.2589 44.2229 32.4396 42.8579 31.2807C41.4928 30.1219 40.4214 28.6564 39.7313 27.004C39.4905 26.4413 39.3034 25.8569 39.1727 25.2589H47.882C48.7282 25.2621 49.5389 25.5997 50.1373 26.1981C50.7357 26.7965 51.0733 27.6072 51.0765 28.4534Z" fill="black"/>
<path opacity="0.2" d="M49.3305 30.1986V33.9518C48.7414 33.9124 48.1572 33.8186 47.5854 33.6716C45.8429 33.2589 44.2229 32.4396 42.8579 31.2807C41.4928 30.1219 40.4214 28.6564 39.7313 27.004H46.1368C46.983 27.0072 47.7936 27.3449 48.3919 27.9433C48.9901 28.5417 49.3275 29.3524 49.3305 30.1986Z" fill="black"/>
<path opacity="0.2" d="M49.3305 30.1986V33.9518C48.7414 33.9124 48.1572 33.8186 47.5854 33.6716C45.8429 33.2589 44.2229 32.4396 42.8579 31.2807C41.4928 30.1219 40.4214 28.6564 39.7313 27.004H46.1368C46.983 27.0072 47.7936 27.3449 48.3919 27.9433C48.9901 28.5417 49.3275 29.3524 49.3305 30.1986Z" fill="black"/>
<path opacity="0.2" d="M47.5854 30.1986V33.6716C45.843 33.2588 44.223 32.4395 42.8579 31.2807C41.4929 30.1218 40.4215 28.6563 39.7313 27.004H44.3917C45.2378 27.0075 46.0483 27.3452 46.6465 27.9435C47.2447 28.5419 47.5822 29.3525 47.5854 30.1986Z" fill="black"/>
<path d="M12.3867 27.0049H44.3856C45.2341 27.0049 46.0478 27.3419 46.6479 27.9418C47.2479 28.5417 47.5851 29.3553 47.5854 30.2038V62.2036C47.5854 63.0522 47.2482 63.8661 46.6482 64.4661C46.0481 65.0662 45.2343 65.4033 44.3856 65.4033H12.3867C11.5381 65.4033 10.7242 65.0662 10.1241 64.4661C9.52406 63.8661 9.18695 63.0522 9.18695 62.2036V30.2038C9.18695 29.3551 9.52406 28.5413 10.1241 27.9412C10.7242 27.3412 11.5381 27.004 12.3867 27.004V27.0049Z" fill="url(#paint0_linear_15_126)"/>
<path d="M36.8057 39.1837H30.4089V56.6023H26.334V39.1837H19.9666V35.805H36.8057V39.1837Z" fill="white"/>
</g>
<path d="M74.984 86.31V91.295L75.368 90.92L81.724 84.713L81.889 84.551L81.724 84.39L75.369 78.145L74.984 77.767V82.752C73.4216 82.7276 71.8898 82.3145 70.527 81.55C69.1388 80.7452 67.9762 79.6029 67.147 78.229C66.3343 76.834 65.9113 75.2464 65.922 73.632C65.922 72.073 66.282 70.653 66.997 69.368L67.081 69.217L66.958 69.097L64.637 66.815L64.436 66.618L64.286 66.857C62.971 68.961 62.311 71.221 62.311 73.632C62.311 75.916 62.893 78.05 64.055 80.029L64.057 80.032C65.201 81.9165 66.7977 83.4854 68.702 84.596L68.705 84.598C70.6189 85.6928 72.7803 86.2813 74.985 86.308L74.984 86.31ZM75.436 68.96V64.514C76.9907 64.5373 78.5127 64.9639 79.853 65.752L79.857 65.754C81.2658 66.53 82.4343 67.6781 83.235 69.073L83.237 69.077C84.0746 70.4481 84.5117 72.0263 84.499 73.633C84.499 75.193 84.139 76.613 83.423 77.898L83.339 78.05L83.464 78.17L85.785 80.414L85.985 80.608L86.134 80.372C86.7559 79.3777 87.2381 78.3027 87.567 77.177C87.929 76.033 88.109 74.852 88.109 73.633C88.1182 71.3825 87.5158 69.1717 86.366 67.237L86.364 67.234C85.22 65.3495 83.6233 63.7807 81.719 62.67L81.715 62.668C79.8014 61.5734 77.6404 60.9849 75.436 60.958V55.97L75.052 56.345L68.697 62.552L68.532 62.713L68.696 62.875L75.052 69.12L75.436 69.497V68.96Z" fill="#4850B9" stroke="#4850B9" stroke-width="0.523"/>
<defs>
<linearGradient id="paint0_linear_15_126" x1="15.8571" y1="24.5048" x2="40.9144" y2="67.9026" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A62C3"/>
<stop offset="0.5" stop-color="#4D55BD"/>
<stop offset="1" stop-color="#3940AB"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="64" height="64" fill="white"/>
<path d="M23.984 15.208H20C18.912 15.208 17.968 15.592 17.168 16.36C16.4 17.128 16.016 18.072 16.016 19.192V46.216C16.016 47.304 16.4 48.232 17.168 49C17.968 49.8 18.912 50.2 20 50.2H44C45.12 50.2 46.064 49.8 46.832 49C47.6 48.232 47.984 47.304 47.984 46.216V19.192C47.984 18.072 47.6 17.128 46.832 16.36C46.064 15.592 45.12 15.208 44 15.208H40.016V18.184H42.992C43.536 18.184 44 18.392 44.384 18.808C44.8 19.192 45.008 19.656 45.008 20.2V45.208C45.008 45.752 44.8 46.216 44.384 46.6C44 46.984 43.536 47.176 42.992 47.176H21.008C20.464 47.176 19.984 46.984 19.568 46.6C19.184 46.216 18.992 45.752 18.992 45.208V20.2C18.992 19.656 19.184 19.192 19.568 18.808C19.984 18.392 20.464 18.184 21.008 18.184H23.984V15.208ZM29.984 12.184C29.728 12.184 29.504 12.296 29.312 12.52C29.12 12.712 29.024 12.936 29.024 13.192V14.2H26V19.192H38V14.2H35.024V13.192C35.024 12.936 34.912 12.712 34.688 12.52C34.496 12.296 34.272 12.184 34.016 12.184H29.984ZM29.024 40.216H41.984V42.184H29.024V40.216ZM23.024 39.688H26V42.712H23.024V39.688ZM29.024 32.2H41.984V34.216H29.024V32.2ZM23.024 31.72H26V34.696H23.024V31.72ZM29.024 24.184H41.984V26.2H29.024V24.184ZM22.832 26.392L21.824 25.384L22.832 24.376L23.84 25.384L26 23.176L27.008 24.184L23.84 27.4L22.832 26.392Z" fill="#3F4350"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
bin

View file

@ -0,0 +1,38 @@
# Include custom targets and environment variables here
## Generate mocks.
mocks:
ifneq ($(HAS_SERVER),)
mockgen -destination server/bot/mocks/mock_poster.go github.com/mattermost/mattermost-plugin-playbooks/server/bot Poster
mockgen -destination server/app/mocks/mock_job_once_scheduler.go github.com/mattermost/mattermost-plugin-playbooks/server/app JobOnceScheduler
mockgen -destination server/app/mocks/mock_condition_store.go github.com/mattermost/mattermost-plugin-playbooks/server/app ConditionStore
mockgen -destination server/app/mocks/mock_playbook_store.go github.com/mattermost/mattermost-plugin-playbooks/server/app PlaybookStore
mockgen -destination server/app/mocks/mock_property_service.go github.com/mattermost/mattermost-plugin-playbooks/server/app PropertyService
mockgen -destination server/app/mocks/mock_auditor.go github.com/mattermost/mattermost-plugin-playbooks/server/app Auditor
mockgen -destination server/sqlstore/mocks/mock_kvapi.go github.com/mattermost/mattermost-plugin-playbooks/server/sqlstore KVAPI
mockgen -destination server/sqlstore/mocks/mock_storeapi.go github.com/mattermost/mattermost-plugin-playbooks/server/sqlstore StoreAPI
mockgen -destination server/sqlstore/mocks/mock_configurationapi.go github.com/mattermost/mattermost-plugin-playbooks/server/sqlstore ConfigurationAPI
endif
## Runs the redocly server.
.PHONY: docs-server
docs-server:
npx @redocly/openapi-cli@1.0.0-beta.3 preview-docs server/api/api.yaml
## Re-generate tests-e2e/db-setup/mattermost.sql from the Postgres image expected to be running
## in the developer's Docker environment.
.PHONY: tests-e2e/db-setup/mattermost.sql
tests-e2e/db-setup/mattermost.sql:
docker exec mattermost-postgres pg_dump \
--username=mmuser \
--clean \
--if-exists \
--exclude-table ir_incident \
--exclude-table ir_playbook \
--exclude-table ir_playbookmember \
--exclude-table ir_statusposts \
--exclude-table ir_system \
--exclude-table ir_timelineevent \
--exclude-table ir_userinfo \
--exclude-table ir_viewedchannel \
mattermost_test > tests-e2e/db-setup/mattermost.sql

View file

@ -0,0 +1,72 @@
module github.com/mattermost/mattermost-plugin-starter-template/build
go 1.24.0
toolchain go1.24.11
require (
github.com/mattermost/mattermost/server/public v0.1.12
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.10.2
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.3 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.22 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -0,0 +1,341 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64=
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4=
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
github.com/mattermost/mattermost/server/public v0.1.12 h1:qlIU/llY0FWdHWQPtvncddQ99KJATPUX6wRHBlt8mfQ=
github.com/mattermost/mattermost/server/public v0.1.12/go.mod h1:3RJZfl7sMedX6ihX+JMFOIAzCHhd0WQnuez+UFQS80k=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME=
github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View file

@ -0,0 +1,220 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
)
const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually.
package main
import (
"encoding/json"
"strings"
"github.com/mattermost/mattermost/server/public/model"
)
var manifest *model.Manifest
const manifestStr = ` + "`" + `
%s
` + "`" + `
func init() {
_ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest)
}
`
const pluginIDJSFileTemplate = `// This file is automatically generated. Do not modify it manually.
const manifest = JSON.parse(` + "`" + `
%s
` + "`" + `);
export default manifest;
`
// These build-time vars are read from shell commands and populated in ../setup.mk
var (
BuildHashShort string
BuildTagLatest string
BuildTagCurrent string
)
func main() {
if len(os.Args) <= 1 {
panic("no cmd specified")
}
manifest, err := findManifest()
if err != nil {
panic("failed to find manifest: " + err.Error())
}
cmd := os.Args[1]
switch cmd {
case "id":
dumpPluginID(manifest)
case "version":
dumpPluginVersion(manifest)
case "has_server":
if manifest.HasServer() {
fmt.Printf("true")
}
case "has_webapp":
if manifest.HasWebapp() {
fmt.Printf("true")
}
case "apply":
if err := applyManifest(manifest); err != nil {
panic("failed to apply manifest: " + err.Error())
}
case "dist":
if err := distManifest(manifest); err != nil {
panic("failed to write manifest to dist directory: " + err.Error())
}
case "check":
if err := manifest.IsValid(); err != nil {
panic("failed to check manifest: " + err.Error())
}
default:
panic("unrecognized command: " + cmd)
}
}
func findManifest() (*model.Manifest, error) {
_, manifestFilePath, err := model.FindManifest(".")
if err != nil {
return nil, errors.Wrap(err, "failed to find manifest in current working directory")
}
manifestFile, err := os.Open(manifestFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath)
}
defer manifestFile.Close()
// Re-decode the manifest, disallowing unknown fields. When we write the manifest back out,
// we don't want to accidentally clobber anything we won't preserve.
var manifest model.Manifest
decoder := json.NewDecoder(manifestFile)
decoder.DisallowUnknownFields()
if err = decoder.Decode(&manifest); err != nil {
return nil, errors.Wrap(err, "failed to parse manifest")
}
// If no version is listed in the manifest, generate one based on the state of the current
// commit, and use the first version we find (to prevent causing errors)
if manifest.Version == "" {
var version string
tags := strings.Fields(BuildTagCurrent)
for _, t := range tags {
if strings.HasPrefix(t, "v") {
version = t
break
}
}
if version == "" {
if BuildTagLatest != "" {
version = BuildTagLatest + "+" + BuildHashShort
} else {
version = "v0.0.0+" + BuildHashShort
}
}
manifest.Version = strings.TrimPrefix(version, "v")
}
// If no release notes specified, generate one from the latest tag, if present.
if manifest.ReleaseNotesURL == "" && BuildTagLatest != "" {
manifest.ReleaseNotesURL = manifest.HomepageURL + "releases/tag/" + BuildTagLatest
}
return &manifest, nil
}
// dumpPluginId writes the plugin id from the given manifest to standard out
func dumpPluginID(manifest *model.Manifest) {
fmt.Printf("%s", manifest.Id)
}
// dumpPluginVersion writes the plugin version from the given manifest to standard out
func dumpPluginVersion(manifest *model.Manifest) {
fmt.Printf("%s", manifest.Version)
}
// applyManifest propagates the plugin_id into the server and webapp folders, as necessary
func applyManifest(manifest *model.Manifest) error {
if manifest.HasServer() {
// generate JSON representation of Manifest.
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
manifestStr := string(manifestBytes)
// write generated code to file by using Go file template.
if err := os.WriteFile(
"server/manifest.go",
[]byte(fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)),
0600,
); err != nil {
return errors.Wrap(err, "failed to write server/manifest.go")
}
}
if manifest.HasWebapp() {
// generate JSON representation of Manifest.
// JSON is very similar and compatible with JS's object literals. so, what we do here
// is actually JS code generation.
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
manifestStr := string(manifestBytes)
// Escape newlines
manifestStr = strings.ReplaceAll(manifestStr, `\n`, `\\n`)
// write generated code to file by using JS file template.
if err := os.WriteFile(
"webapp/src/manifest.ts",
[]byte(fmt.Sprintf(pluginIDJSFileTemplate, manifestStr)),
0600,
); err != nil {
return errors.Wrap(err, "failed to open webapp/src/manifest.ts")
}
}
return nil
}
// distManifest writes the manifest file to the dist directory
func distManifest(manifest *model.Manifest) error {
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(fmt.Sprintf("dist/%s/plugin.json", manifest.Id), manifestBytes, 0600); err != nil {
return errors.Wrap(err, "failed to write plugin.json")
}
return nil
}

View file

@ -0,0 +1,188 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
const (
logsPerPage = 100 // logsPerPage is the number of log entries to fetch per API call
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
)
// logs fetches the latest 500 log entries from Mattermost,
// and prints only the ones related to the plugin to stdout.
func logs(ctx context.Context, client *model.Client4, pluginID string) error {
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}
logs, err := fetchLogs(ctx, client, 0, 500, pluginID, time.Unix(0, 0))
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}
err = printLogEntries(logs)
if err != nil {
return fmt.Errorf("failed to print logs entries: %w", err)
}
return nil
}
// watchLogs fetches log entries from Mattermost and print them to stdout.
// It will return without an error when ctx is canceled.
func watchLogs(ctx context.Context, client *model.Client4, pluginID string) error {
err := checkJSONLogsSetting(ctx, client)
if err != nil {
return err
}
now := time.Now()
var oldestEntry string
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
var page int
for {
logs, err := fetchLogs(ctx, client, page, logsPerPage, pluginID, now)
if err != nil {
return fmt.Errorf("failed to fetch log entries: %w", err)
}
var allNew bool
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)
err = printLogEntries(logs)
if err != nil {
return fmt.Errorf("failed to print logs entries: %w", err)
}
if !allNew {
// No more logs to fetch
break
}
page++
}
}
}
}
// checkOldestEntry check a if logs contains new log entries.
// It returns the filtered slice of log entries, the new oldest entry and whether or not all entries were new.
func checkOldestEntry(logs []string, oldest string) ([]string, string, bool) {
if len(logs) == 0 {
return nil, oldest, false
}
newOldestEntry := logs[(len(logs) - 1)]
i := slices.Index(logs, oldest)
switch i {
case -1:
// Every log entry is new
return logs, newOldestEntry, true
case len(logs) - 1:
// No new log entries
return nil, oldest, false
default:
// Filter out oldest log entry
return logs[i+1:], newOldestEntry, false
}
}
// fetchLogs fetches log entries from Mattermost
// and filters them based on pluginID and timestamp.
func fetchLogs(ctx context.Context, client *model.Client4, page, perPage int, pluginID string, since time.Time) ([]string, error) {
logs, _, err := client.GetLogs(ctx, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
}
logs, err = filterLogEntries(logs, pluginID, since)
if err != nil {
return nil, fmt.Errorf("failed to filter log entries: %w", err)
}
return logs, nil
}
// filterLogEntries filters a given slice of log entries by pluginID.
// It also filters out any entries which timestamps are older then since.
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
type logEntry struct {
PluginID string `json:"plugin_id"`
Timestamp string `json:"timestamp"`
}
var ret []string
for _, e := range logs {
var le logEntry
err := json.Unmarshal([]byte(e), &le)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal log entry into JSON: %w", err)
}
if le.PluginID != pluginID {
continue
}
let, err := time.Parse(timeStampFormat, le.Timestamp)
if err != nil {
return nil, fmt.Errorf("unknown timestamp format: %w", err)
}
if let.Before(since) {
continue
}
// Log entries returned by the API have a newline a prefix.
// Remove that to make printing consistent.
e = strings.TrimPrefix(e, "\n")
ret = append(ret, e)
}
return ret, nil
}
// printLogEntries prints a slice of log entries to stdout.
func printLogEntries(entries []string) error {
for _, e := range entries {
_, err := io.WriteString(os.Stdout, e+"\n")
if err != nil {
return fmt.Errorf("failed to write log entry to stdout: %w", err)
}
}
return nil
}
func checkJSONLogsSetting(ctx context.Context, client *model.Client4) error {
cfg, _, err := client.GetConfig(ctx)
if err != nil {
return fmt.Errorf("failed to fetch config: %w", err)
}
if cfg.LogSettings.FileJson == nil || !*cfg.LogSettings.FileJson {
return errors.New("JSON output for file logs are disabled. Please enable LogSettings.FileJson via the configration in Mattermost.") //nolint:revive,stylecheck
}
return nil
}

View file

@ -0,0 +1,205 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"fmt"
"testing"
"time"
)
func TestCheckOldestEntry(t *testing.T) {
for name, tc := range map[string]struct {
logs []string
oldest string
expectedLogs []string
expectedOldest string
expectedAllNew bool
}{
"nil logs": {
logs: nil,
oldest: "oldest",
expectedLogs: nil,
expectedOldest: "oldest",
expectedAllNew: false,
},
"empty logs": {
logs: []string{},
oldest: "oldest",
expectedLogs: nil,
expectedOldest: "oldest",
expectedAllNew: false,
},
"no new entries, one old entry": {
logs: []string{"old"},
oldest: "old",
expectedLogs: []string{},
expectedOldest: "old",
expectedAllNew: false,
},
"no new entries, multipile old entries": {
logs: []string{"old1", "old2", "old3"},
oldest: "old3",
expectedLogs: []string{},
expectedOldest: "old3",
expectedAllNew: false,
},
"one new entry, no old entry": {
logs: []string{"new"},
oldest: "old",
expectedLogs: []string{"new"},
expectedOldest: "new",
expectedAllNew: true,
},
"multipile new entries, no old entry": {
logs: []string{"new1", "new2", "new3"},
oldest: "old",
expectedLogs: []string{"new1", "new2", "new3"},
expectedOldest: "new3",
expectedAllNew: true,
},
"one new entry, one old entry": {
logs: []string{"old", "new"},
oldest: "old",
expectedLogs: []string{"new"},
expectedOldest: "new",
expectedAllNew: false,
},
"one new entry, multipile old entries": {
logs: []string{"old1", "old2", "old3", "new"},
oldest: "old3",
expectedLogs: []string{"new"},
expectedOldest: "new",
expectedAllNew: false,
},
"multipile new entries, ultipile old entries": {
logs: []string{"old1", "old2", "old3", "new1", "new2", "new3"},
oldest: "old3",
expectedLogs: []string{"new1", "new2", "new3"},
expectedOldest: "new3",
expectedAllNew: false,
},
} {
t.Run(name, func(t *testing.T) {
logs, oldest, allNew := checkOldestEntry(tc.logs, tc.oldest)
if allNew != tc.expectedAllNew {
t.Logf("expected allNew: %v, got %v", tc.expectedAllNew, allNew)
t.Fail()
}
if oldest != tc.expectedOldest {
t.Logf("expected oldest: %v, got %v", tc.expectedOldest, oldest)
t.Fail()
}
compareSlice(t, tc.expectedLogs, logs)
})
}
}
func TestFilterLogEntries(t *testing.T) {
now := time.Now()
for name, tc := range map[string]struct {
logs []string
pluginID string
since time.Time
expectedLogs []string
expectedErr bool
}{
"nil slice": {
logs: nil,
expectedLogs: nil,
expectedErr: false,
},
"empty slice": {
logs: []string{},
expectedLogs: nil,
expectedErr: false,
},
"no JSON": {
logs: []string{
`{"foo"`,
},
expectedLogs: nil,
expectedErr: true,
},
"unknown time format": {
logs: []string{
`{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53"}`,
},
pluginID: "some.plugin.id",
expectedLogs: nil,
expectedErr: true,
},
"one matching entry": {
logs: []string{
`{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`,
},
pluginID: "some.plugin.id",
expectedLogs: []string{
`{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`,
},
expectedErr: false,
},
"filter out non plugin entries": {
logs: []string{
`{"message":"bar1", "timestamp": "2023-12-18 10:58:52.091 +01:00"}`,
`{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`,
`{"message":"bar2", "timestamp": "2023-12-18 10:58:54.091 +01:00"}`,
},
pluginID: "some.plugin.id",
expectedLogs: []string{
`{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`,
},
expectedErr: false,
},
"filter out old entries": {
logs: []string{
fmt.Sprintf(`{"message":"old2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(-2*time.Second).Format(timeStampFormat)),
fmt.Sprintf(`{"message":"old1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(-1*time.Second).Format(timeStampFormat)),
fmt.Sprintf(`{"message":"now", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Format(timeStampFormat)),
fmt.Sprintf(`{"message":"new1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(1*time.Second).Format(timeStampFormat)),
fmt.Sprintf(`{"message":"new2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(2*time.Second).Format(timeStampFormat)),
},
pluginID: "some.plugin.id",
since: now,
expectedLogs: []string{
fmt.Sprintf(`{"message":"new1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(1*time.Second).Format(timeStampFormat)),
fmt.Sprintf(`{"message":"new2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(2*time.Second).Format(timeStampFormat)),
},
expectedErr: false,
},
} {
t.Run(name, func(t *testing.T) {
logs, err := filterLogEntries(tc.logs, tc.pluginID, tc.since)
if tc.expectedErr {
if err == nil {
t.Logf("expected error, got nil")
t.Fail()
}
} else {
if err != nil {
t.Logf("expected no error, got %v", err)
t.Fail()
}
}
compareSlice(t, tc.expectedLogs, logs)
})
}
}
func compareSlice[S ~[]E, E comparable](t *testing.T, expected, got S) {
if len(expected) != len(got) {
t.Logf("expected len: %v, got %v", len(expected), len(got))
t.FailNow()
}
for i := 0; i < len(expected); i++ {
if expected[i] != got[i] {
t.Logf("expected [%d]: %v, got %v", i, expected[i], got[i])
t.Fail()
}
}
}

View file

@ -0,0 +1,187 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// main handles deployment of the plugin to a development server using the Client4 API.
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
const commandTimeout = 120 * time.Second
const helpText = `
Usage:
pluginctl deploy <plugin id> <bundle path>
pluginctl disable <plugin id>
pluginctl enable <plugin id>
pluginctl reset <plugin id>
`
func main() {
err := pluginctl()
if err != nil {
fmt.Printf("Failed: %s\n", err.Error())
fmt.Print(helpText)
os.Exit(1)
}
}
func pluginctl() error {
if len(os.Args) < 3 {
return errors.New("invalid number of arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), commandTimeout)
defer cancel()
client, err := getClient(ctx)
if err != nil {
return err
}
switch os.Args[1] {
case "deploy":
if len(os.Args) < 4 {
return errors.New("invalid number of arguments")
}
return deploy(ctx, client, os.Args[2], os.Args[3])
case "disable":
return disablePlugin(ctx, client, os.Args[2])
case "enable":
return enablePlugin(ctx, client, os.Args[2])
case "reset":
return resetPlugin(ctx, client, os.Args[2])
case "logs":
return logs(ctx, client, os.Args[2])
case "logs-watch":
return watchLogs(context.WithoutCancel(ctx), client, os.Args[2]) // Keep watching forever
default:
return errors.New("invalid second argument")
}
}
func getClient(ctx context.Context) (*model.Client4, error) {
socketPath := os.Getenv("MM_LOCALSOCKETPATH")
if socketPath == "" {
socketPath = model.LocalModeSocketPath
}
client, connected := getUnixClient(socketPath)
if connected {
log.Printf("Connecting using local mode over %s", socketPath)
return client, nil
}
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
log.Printf("No socket found at %s for local mode deployment. Attempting to authenticate with credentials.", socketPath)
}
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
adminToken := os.Getenv("MM_ADMIN_TOKEN")
adminUsername := os.Getenv("MM_ADMIN_USERNAME")
adminPassword := os.Getenv("MM_ADMIN_PASSWORD")
if siteURL == "" {
return nil, errors.New("MM_SERVICESETTINGS_SITEURL is not set")
}
client = model.NewAPIv4Client(siteURL)
if adminToken != "" {
log.Printf("Authenticating using token against %s.", siteURL)
client.SetToken(adminToken)
return client, nil
}
if adminUsername != "" && adminPassword != "" {
client := model.NewAPIv4Client(siteURL)
log.Printf("Authenticating as %s against %s.", adminUsername, siteURL)
_, _, err := client.Login(ctx, adminUsername, adminPassword)
if err != nil {
return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, err)
}
return client, nil
}
return nil, errors.New("one of MM_ADMIN_TOKEN or MM_ADMIN_USERNAME/MM_ADMIN_PASSWORD must be defined")
}
func getUnixClient(socketPath string) (*model.Client4, bool) {
_, err := net.Dial("unix", socketPath)
if err != nil {
return nil, false
}
return model.NewAPIv4SocketClient(socketPath), true
}
// deploy attempts to upload and enable a plugin via the Client4 API.
// It will fail if plugin uploads are disabled.
func deploy(ctx context.Context, client *model.Client4, pluginID, bundlePath string) error {
pluginBundle, err := os.Open(bundlePath)
if err != nil {
return fmt.Errorf("failed to open %s: %w", bundlePath, err)
}
defer pluginBundle.Close()
log.Print("Uploading plugin via API.")
_, _, err = client.UploadPluginForced(ctx, pluginBundle)
if err != nil {
return fmt.Errorf("failed to upload plugin bundle: %s", err.Error())
}
log.Print("Enabling plugin.")
_, err = client.EnablePlugin(ctx, pluginID)
if err != nil {
return fmt.Errorf("failed to enable plugin: %s", err.Error())
}
return nil
}
// disablePlugin attempts to disable the plugin via the Client4 API.
func disablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
log.Print("Disabling plugin.")
_, err := client.DisablePlugin(ctx, pluginID)
if err != nil {
return fmt.Errorf("failed to disable plugin: %w", err)
}
return nil
}
// enablePlugin attempts to enable the plugin via the Client4 API.
func enablePlugin(ctx context.Context, client *model.Client4, pluginID string) error {
log.Print("Enabling plugin.")
_, err := client.EnablePlugin(ctx, pluginID)
if err != nil {
return fmt.Errorf("failed to enable plugin: %w", err)
}
return nil
}
// resetPlugin attempts to reset the plugin via the Client4 API.
func resetPlugin(ctx context.Context, client *model.Client4, pluginID string) error {
err := disablePlugin(ctx, client, pluginID)
if err != nil {
return err
}
err = enablePlugin(ctx, client, pluginID)
if err != nil {
return err
}
return nil
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
# Ensure that go is installed. Note that this is independent of whether or not a server is being
# built, since the build script itself uses go.
ifeq ($(GO),)
$(error "go is not available: see https://golang.org/doc/install")
endif
# Gather build variables to inject into the manifest tool
BUILD_HASH_SHORT = $(shell git rev-parse --short HEAD)
BUILD_TAG_LATEST = $(shell git describe --tags --match 'v*' --abbrev=0 2>/dev/null)
BUILD_TAG_CURRENT = $(shell git tag --points-at HEAD)
# Ensure that the build tools are compiled. Go's caching makes this quick.
$(shell cd build/manifest && $(GO) build -ldflags '-X "main.BuildHashShort=$(BUILD_HASH_SHORT)" -X "main.BuildTagLatest=$(BUILD_TAG_LATEST)" -X "main.BuildTagCurrent=$(BUILD_TAG_CURRENT)"' -o ../bin/manifest)
# Ensure that the deployment tools are compiled. Go's caching makes this quick.
$(shell cd build/pluginctl && $(GO) build -o ../bin/pluginctl)
# Ensure that the release tool is compiled. Go's caching makes this quick.
$(shell cd build/release && $(GO) build -o ../bin/release)
# Extract the plugin id from the manifest.
PLUGIN_ID ?= $(shell build/bin/manifest id)
ifeq ($(PLUGIN_ID),)
$(error "Cannot parse id from $(MANIFEST_FILE)")
endif
# Extract the plugin version from the manifest.
PLUGIN_VERSION ?= $(shell build/bin/manifest version)
ifeq ($(PLUGIN_VERSION),)
$(error "Cannot parse version from $(MANIFEST_FILE)")
endif
# Determine if a server is defined in the manifest.
HAS_SERVER ?= $(shell build/bin/manifest has_server)
# Determine if a webapp is defined in the manifest.
HAS_WEBAPP ?= $(shell build/bin/manifest has_webapp)
# Determine if a /public folder is in use
HAS_PUBLIC ?= $(wildcard public/.)
# Determine if the mattermost-utilities repo is present
HAS_MM_UTILITIES ?= $(wildcard $(MM_UTILITIES_DIR)/.)
# Store the current path for later use
PWD ?= $(shell pwd)
# Ensure that npm (and thus node) is installed.
ifneq ($(HAS_WEBAPP),)
ifeq ($(NPM),)
$(error "npm is not available: see https://www.npmjs.com/get-npm")
endif
endif

View file

@ -0,0 +1,80 @@
# Changelogs for mattermost-plugin-playbooks
This directory contains generated changelogs for each release, complete with QA review guidance.
## Structure
Each changelog file is named `<from-version>-<to-version>.md` (e.g., `v2.7.0-v2.8.0.md`).
## Changelog Sections
### Standard Sections
- **Summary** — Overview of the release (features, fixes, improvements, breaking changes)
- **Bug Fixes** — Issues that were resolved
- **Features** — New user-facing functionality
- **UI Improvements** — Visual or UX changes
- **Performance** — Speed and optimization improvements
- **Infrastructure** — CI, build system, load testing changes
- **Chores / Maintenance** — Dependencies, refactors, test improvements, translations
- **Commit List** — All commits in the range
- **Recommended Next Version** — Semver suggestion and reasoning
### 🧪 QA Review Checklist (NEW)
**Purpose:** Help QA prioritize and efficiently test RC releases by calling out UI-facing changes.
**What gets included:**
- ✅ New features affecting the UI
- ✅ Bug fixes that change user workflows
- ✅ Navigation and menu changes
- ✅ Visual/branding updates
- ❌ Internal refactors
- ❌ Dependency updates
- ❌ Test improvements
- ❌ Translations
**Information provided for each change:**
1. **PR number and author** — For questions or deeper context
2. **What to test** — Specific steps QA should follow
3. **Impact area(s)** — Where in the UI this change appears (RHS, Modal, Sidebar, etc.)
4. **Regression risk** — High/Medium/Low to help QA prioritize effort
### Organization
The QA section is organized by impact type:
- **Critical UI Changes** — New features, significant fixes, must-test items
- **Visual / UX Changes** — Styling, layout, branding updates
- **Navigation / Flow Changes** — Menu items, routing, link behavior
## For Release Engineers
When generating a new changelog:
1. **Use the standard changelog skill** (see `.pi/skills/changelog/SKILL.md`)
2. **Extract UI-facing PRs** — Identify which PRs changed the UI
3. **Populate QA section** — For each UI PR, add:
- PR number and author
- Clear description
- Test guidance (specific steps or areas)
- Impact location
- Regression risk level
4. **Save the file**`changelogs/<from>-<to>.md`
## For QA Leads
When preparing to test an RC:
1. **Read the QA Review Checklist first** — This tells you what changed and where
2. **Prioritize by Regression Risk** — High-risk items get more thorough testing
3. **Use "What to test" guidance** — Follow the specific steps listed for each change
4. **Know the PR authors** — Reach out if you find issues or need clarification
5. **Check "Impact area(s)"** — Focus your regression testing on these zones
## Tips
- **Each PR entry includes the author** so QA can ask follow-up questions
- **"What to test" is specific** — Not vague; includes actual user workflows
- **Regression risk helps triage** — High-risk fixes get more test coverage
- **Impact areas are consistent** — Helps QA mentally map the app

View file

@ -0,0 +1,92 @@
## Changelog: v2.7.0 → origin/master
### 📋 Summary
15 commits across 12 PRs since v2.7.0. This release contains 3 security/permissions bug fixes, a branding revert (restoring "Playbooks" naming in the RHS after the checklists rebrand was rolled back), a UX improvement (overflow tooltips), and significant load-testing infrastructure work. No breaking changes or new user-facing API endpoints.
---
### 🐛 Bug Fixes
- **[PR #2204](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2204)** — Fix checklist creation requiring `run_create` team permission instead of channel post permission, which blocked users in production where `run_create` is not granted by default — *@jgheithcock*
- **[PR #2192](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2192)** — Fix privilege escalation when changing a playbook's team without manage-members permission (MM-66474); adds destination-team access check — *@jgheithcock*
- **[PR #2191](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2191)** — Enforce playbook view permissions across API endpoints, GraphQL loaders, categories, and slash commands (MM-67325); adds `FilterPlaybooksByViewPermission` — *@jgheithcock*
---
### 💄 UI Improvements
- **[PR #2205](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2205)** — Restore Playbooks branding in RHS: title, channel header tooltip, and AppBar icon reverted from "Checklists" back to "Playbooks"; removed "Powered by Playbooks" footer — *@calebroseland*
- **[PR #2207](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2207)** — Remove the "Playbook Runs are now Checklists" rebrand tour point that is no longer needed — *@calebroseland*
- **[PR #2179](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2179)** — Add tooltips for overflowing/truncated text in checklists RHS (run list card titles and context menu dropdown title) — *@calebroseland*
---
### 🏗️ Infrastructure / Load Testing
- **[PR #2182](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2182)** — Implement browser-based simulation scripts for Playbooks client-side load testing, including build scripts, configuration, and sample scenario — *@M-ZubairAhmed*
- **[PR #2171](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2171)** — Implement the load-test `SimulController` and `GenController` Plugin interfaces (OpenRHS, HookLogin, HookSwitchTeam, HookSwitchChannel, CreatePlaybook, CreateRun) — *@agarciamontoro*
---
### 🔧 Chores / Maintenance
- **[PR #2208](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2208)** — Upgrade `qs` from 6.10.5 to 6.14.1 (security fix) — *@JulienTant*
- **[PR #2201](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2201)** — Clear all ownership rules from CODEOWNERS — *@calebroseland*
- **[PR #2195](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2195)** — Update NOTICE.txt with updated dependencies — *@unified-ci-app*
- **[PR #2189](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2189)** — Isolate release TUI unit tests from real git repo to prevent conflicts with existing tags — *@calebroseland*
- **[PR #2200](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2200)**, **[PR #2196](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2196)**, **[PR #2183](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2183)** — Translations updates from Mattermost Weblate
---
### 🧪 QA Review Checklist
This section highlights **UI-facing changes** that QA should prioritize when validating the RC.
Each item references the PR that introduced the change and the author for follow-up questions.
#### Critical UI Changes
- **[PR #2205](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2205)** — Restore Playbooks branding in RHS (revert checklists rebrand) — *@calebroseland*
- **What to test:** Verify RHS title says "Playbooks" (not "Checklists"), channel header tooltip says "Playbooks", AppBar icon is the original Playbooks icon, and "Powered by Playbooks" footer is gone from the run list
- **Impact area(s):** RHS title bar, Channel Header button tooltip, AppBar icon, Run List
- **Regression risk:** Medium — Touches multiple UI entry points; verify no layout breakage or missing icons
#### Visual / UX Changes
- **[PR #2179](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2179)** — Add tooltips for overflowing text in checklists UI — *@calebroseland*
- **What to test:** Create runs with long names. In the RHS run list, hover over a truncated run name — tooltip should appear with full text. Open the context menu dropdown — hover over a truncated title — tooltip should appear. Short names that aren't truncated should NOT show a tooltip.
- **Impact area(s):** RHS run list cards, Context menu dropdown title
- **[PR #2207](https://github.com/mattermost/mattermost-plugin-playbooks/pull/2207)** — Remove checklists rebrand tour point — *@calebroseland*
- **What to test:** Verify the "Playbook Runs are now Checklists" tour step no longer appears for new or existing users. Ensure the rest of the onboarding tour still completes correctly.
- **Impact area(s):** Onboarding tour, Checklists panel
---
### 📝 Commit List
| Commit | Description |
|--------|-------------|
| `887d9cac` | Fix MM-67325 (#2191) |
| `3044bbc3` | fix(deps): upgrade qs from 6.10.5 to 6.14.1 (#2208) |
| `3b680f95` | MM-67785: Remove checklists rebrand tour point (#2207) |
| `6ddc336b` | [MM-65832] Implement the simulation scripts for Playbooks client-side load testing (#2182) |
| `2a1804a4` | MM-67701: Restore Playbooks branding in RHS (#2205) |
| `78e721b4` | Fix MM-67648 (and preserve MM-66249 fix) (#2204) |
| `c799390d` | chore(repo): clear CODEOWNERS entries (#2201) |
| `b53bc39b` | Translations update from Mattermost Weblate (#2200) |
| `e1d6ec2c` | Fix MM-66474 (#2192) |
| `7be951a2` | chore: Update NOTICE.txt file with updated dependencies (#2195) |
| `3ba38fec` | MM-66895: Add tooltip for overflowing text in checklists UI (#2179) |
| `483f1495` | Translations update from Mattermost Weblate (#2196) |
| `8f646f6e` | MM-66991: Implement the Plugin interface (#2171) |
| `7e4af2ee` | Translations update from Mattermost Weblate (#2183) |
| `dcb70291` | test(release): isolate unit tests from real git repo (#2189) |
---
### 🔖 Recommended Next Version: **v2.8.0**
**Reasoning:** While there are no new API endpoints, the user-visible surface changes significantly. The Playbooks branding revert (#2205, #2207) changes the RHS title, AppBar icon, channel header tooltip, and removes a tour point — users upgrading from v2.7.0 (which shipped with "Checklists" branding) will see a noticeably different UI. The overflow tooltips (#2179) add new UX behavior. The permissions filtering fix (#2191) changes what playbooks are visible across list views, categories, and commands. Taken together, these are meaningful user-facing changes that warrant a MINOR bump.
**→ Recommended version: `v2.8.0`**

View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,63 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
type GenericChannelActionWithoutPayload struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
Enabled bool `json:"enabled"`
DeleteAt int64 `json:"delete_at"`
ActionType string `json:"action_type"`
TriggerType string `json:"trigger_type"`
}
type GenericChannelAction struct {
GenericChannelActionWithoutPayload
Payload interface{} `json:"payload"`
}
type WelcomeMessagePayload struct {
Message string `json:"message" mapstructure:"message"`
}
type PromptRunPlaybookFromKeywordsPayload struct {
Keywords []string `json:"keywords" mapstructure:"keywords"`
PlaybookID string `json:"playbook_id" mapstructure:"playbook_id"`
}
type CategorizeChannelPayload struct {
CategoryName string `json:"category_name" mapstructure:"category_name"`
}
type WelcomeMessageAction struct {
GenericChannelActionWithoutPayload
Payload WelcomeMessagePayload `json:"payload"`
}
const (
// Action types
ActionTypeWelcomeMessage = "send_welcome_message"
ActionTypePromptRunPlaybook = "prompt_run_playbook"
ActionTypeCategorizeChannel = "categorize_channel"
// Trigger types
TriggerTypeNewMemberJoins = "new_member_joins"
TriggerTypeKeywordsPosted = "keywords"
)
// ChannelActionListOptions specifies the optional parameters to the
// ActionsService.List method.
type ChannelActionListOptions struct {
TriggerType string `url:"trigger_type,omitempty"`
ActionType string `url:"action_type,omitempty"`
}
// ChannelActionCreateOptions specifies the parameters for ActionsService.Create method.
type ChannelActionCreateOptions struct {
ChannelID string `json:"channel_id"`
Enabled bool `json:"enabled"`
ActionType string `json:"action_type"`
TriggerType string `json:"trigger_type"`
Payload interface{} `json:"payload"`
}

View file

@ -0,0 +1,78 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
// ActionsService handles communication with the actions related
// methods of the Playbook API.
type ActionsService struct {
client *Client
}
// Create an action. Returns the id of the newly created action.
func (s *ActionsService) Create(ctx context.Context, channelID string, opts ChannelActionCreateOptions) (string, error) {
actionURL := fmt.Sprintf("actions/channels/%s", channelID)
req, err := s.client.newAPIRequest(http.MethodPost, actionURL, opts)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
// List the actions in a channel.
func (s *ActionsService) List(ctx context.Context, channelID string, opts ChannelActionListOptions) ([]GenericChannelAction, error) {
actionURL, err := addOptions(fmt.Sprintf("actions/channels/%s", channelID), opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newAPIRequest(http.MethodGet, actionURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := []GenericChannelAction{}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Update an existing action.
func (s *ActionsService) Update(ctx context.Context, action GenericChannelAction) error {
updateURL := fmt.Sprintf("actions/channels/%s/%s", action.ChannelID, action.ID)
req, err := s.client.newAPIRequest(http.MethodPut, updateURL, action)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
type BotService struct {
client *Client
}
// List the conditions for a run (read-only).
func (s *BotService) Connect(ctx context.Context) error {
connectURL := "bot/connect"
req, err := s.client.newAPIRequest(http.MethodGet, connectURL, nil)
if err != nil {
return fmt.Errorf("failed to build request: %w", err)
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
return fmt.Errorf("unable to connect the bot: %d", resp.StatusCode)
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
type CategoriesService struct {
client *Client
}
// CategoriesIsFavoriteOptions specifies the optional parameters to the
// CategoriesService.IsFavorite method
type CategoriesIsFavoriteOptions struct {
TeamId string `url:"team_id,omitempty"`
ItemId string `url:"item_id,omitempty"`
ItemType string `url:"type,omitempty"`
}
// List the conditions for a run (read-only).
func (s *CategoriesService) IsFavorite(ctx context.Context, opts CategoriesIsFavoriteOptions) (bool, error) {
isFavoriteURL, err := addOptions("my_categories/favorites", opts)
if err != nil {
return false, err
}
req, err := s.client.newAPIRequest(http.MethodGet, isFavoriteURL, nil)
if err != nil {
return false, fmt.Errorf("failed to build request: %w", err)
}
var isFavorite bool
resp, err := s.client.do(ctx, req, &isFavorite)
if err != nil {
return false, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return isFavorite, nil
}
return false, fmt.Errorf("unable to get favorite status: %d", resp.StatusCode)
}

View file

@ -0,0 +1,294 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"github.com/google/go-querystring/query"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/mattermost/mattermost/server/public/model"
)
const (
apiVersion = "v0"
manifestID = "playbooks"
userAgent = "go-client/" + apiVersion
)
// Client manages communication with the Playbooks API.
type Client struct {
// client is the underlying HTTP client used to make API requests.
client *http.Client
// BaseURL is the base HTTP endpoint for the Playbooks plugin.
BaseURL *url.URL
// User agent used when communicating with the Playbooks API.
UserAgent string
// PlaybookRuns is a collection of methods used to interact with playbook runs.
PlaybookRuns *PlaybookRunService
// Playbooks is a collection of methods used to interact with playbooks.
Playbooks *PlaybooksService
// Settings is a collection of methods used to interact with settings.
Settings *SettingsService
// Actions is a collection of methods used to interact with actions.
Actions *ActionsService
// Stats is a collection of methods used to interact with stats.
Stats *StatsService
// Reminders is a collection of methods used to interact with reminders.
Reminders *RemindersService
// TabApp is a collection of methods used to interact with playbooks from the tabapp.
TabApp *TabAppService
// PlaybookConditions is a collection of methods used to interact with playbook conditions.
PlaybookConditions *PlaybookConditionsService
// RunConditions is a collection of methods used to interact with run conditions.
RunConditions *RunConditionsService
// Bot is a collection of methods used to interact with the Playbooks bot.
Bot *BotService
// Bot is a collection of methods used to interact with categories.
Categories *CategoriesService
}
// New creates a new instance of Client using the configuration from the given Mattermost Client.
func New(client4 *model.Client4) (*Client, error) {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: client4.AuthToken},
)
return newClient(client4.URL, oauth2.NewClient(ctx, ts))
}
// newClient creates a new instance of Client from the given URL and http.Client.
func newClient(mattermostSiteURL string, httpClient *http.Client) (*Client, error) {
siteURL, err := url.Parse(mattermostSiteURL)
if err != nil {
return nil, err
}
c := &Client{client: httpClient, BaseURL: siteURL, UserAgent: userAgent}
c.PlaybookRuns = &PlaybookRunService{c}
c.Playbooks = &PlaybooksService{c}
c.Settings = &SettingsService{c}
c.Actions = &ActionsService{c}
c.Stats = &StatsService{c}
c.Reminders = &RemindersService{c}
c.TabApp = &TabAppService{c}
c.PlaybookConditions = &PlaybookConditionsService{c}
c.RunConditions = &RunConditionsService{c}
c.Bot = &BotService{c}
c.Categories = &CategoriesService{c}
return c, nil
}
// newRequest creates an API request, JSON-encoding any given body parameter.
func (c *Client) newRequest(method, endpoint string, body interface{}) (*http.Request, error) {
u, err := c.BaseURL.Parse(endpoint)
if err != nil {
return nil, errors.Wrapf(err, "invalid endpoint %s", endpoint)
}
var buf io.ReadWriter
if body != nil {
buf = &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
err = enc.Encode(body)
if err != nil {
return nil, errors.Wrapf(err, "failed to encode body %s", body)
}
}
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, errors.Wrapf(err, "failed to create http request for url %s", u)
}
if buf != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return req, nil
}
// newAPIRequest creates an API request, JSON-encoding any given body parameter.
func (c *Client) newAPIRequest(method, endpoint string, body interface{}) (*http.Request, error) {
return c.newRequest(method, buildAPIURL(endpoint), body)
}
// buildAPIURL constructs the path to the given endpoint.
func buildAPIURL(endpoint string) string {
return fmt.Sprintf("plugins/%s/api/%s/%s", manifestID, apiVersion, endpoint)
}
// do sends an API request and returns the API response.
//
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred. If v implements the io.Writer
// interface, the raw response body will be written to v, without attempting to
// first decode it.
func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
if ctx == nil {
return nil, errors.New("context must be non-nil")
}
req = req.WithContext(ctx)
resp, err := c.client.Do(req)
if err != nil {
select {
case <-ctx.Done():
return nil, errors.Wrapf(ctx.Err(), "client err=%s", err.Error())
default:
}
return nil, err
}
defer resp.Body.Close()
err = checkResponse(resp)
if err != nil {
return resp, err
}
if v != nil {
if w, ok := v.(io.Writer); ok {
if _, err = io.Copy(w, resp.Body); err != nil {
return nil, err
}
} else {
body, _ := ioutil.ReadAll(resp.Body)
decErr := json.NewDecoder(bytes.NewReader(body)).Decode(v)
if decErr == io.EOF {
// TODO: Confirm if this happens only on empty bodies. If so, check that first before decoding.
decErr = nil // ignore EOF errors caused by empty response body
}
if decErr != nil {
err = decErr
}
}
}
return resp, err
}
type GraphQLInput struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
func (c *Client) DoGraphql(ctx context.Context, input *GraphQLInput, v interface{}) error {
url := "query"
req, err := c.newAPIRequest(http.MethodPost, url, input)
if err != nil {
return err
}
_, err = c.do(ctx, req, v)
if err != nil {
return err
}
return nil
}
// checkResponse checks the API response for an error.
//
// Any response with a status code outside 2xx is considered an error, and its body inspected for
// an optional `Error` property in a JSON struct.
func checkResponse(r *http.Response) error {
if c := r.StatusCode; http.StatusOK <= c && c <= 299 {
return nil
}
errorResponse := &ErrorResponse{
StatusCode: r.StatusCode,
Method: r.Request.Method,
URL: r.Request.URL.String(),
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
errorResponse.Err = fmt.Errorf("failed to read response body: %w", err)
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
if data != nil {
_ = json.Unmarshal(data, errorResponse)
}
return errorResponse
}
// addOption adds the given parameter as an URL query parameters to s.
func addOption(s string, name, value string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qa := u.Query()
qa.Add(name, value)
u.RawQuery = qa.Encode()
return u.String(), nil
}
// addOptions adds the parameters in opts as URL query parameters to s. opts
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opts interface{}) (string, error) {
v := reflect.ValueOf(opts)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qs, err := query.Values(opts)
if err != nil {
return s, errors.Wrapf(err, "failed to opts %+v", opts)
}
// Append to the existing query parameters.
qa := u.Query()
for key, values := range qs {
for _, value := range values {
qa.Add(key, value)
}
}
u.RawQuery = qa.Encode()
return u.String(), nil
}
// addPaginationOptions adds the given pagination parameters as URL query parameters to s.
func addPaginationOptions(s string, page, perPage int) (string, error) {
u, err := url.Parse(s)
if err != nil {
return s, errors.Wrapf(err, "failed to parse %s", s)
}
qa := u.Query()
qa.Add("page", strconv.Itoa(page))
qa.Add("per_page", strconv.Itoa(perPage))
u.RawQuery = qa.Encode()
return u.String(), nil
}

View file

@ -0,0 +1,60 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client_test
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/mattermost/mattermost-plugin-playbooks/client"
"github.com/stretchr/testify/require"
)
// setup sets up a test HTTP server and matching Client.
//
// Tests should register handlers on mux providing mock responses for the API method being tested.
func setup(t *testing.T) (c *client.Client, mux *http.ServeMux, serverURL string) {
baseURLPath := ""
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()
apiHandler := http.NewServeMux()
apiHandler.Handle(baseURLPath+"/", http.StripPrefix(baseURLPath, mux))
// server is a test HTTP server used to provide mock API responses.
server := httptest.NewServer(apiHandler)
t.Cleanup(server.Close)
serverURL = server.URL
// client is the workflows client being tested and is
// configured to use test server.
c, _ = client.NewClient("", &http.Client{})
parsedURL, _ := url.Parse(server.URL + baseURLPath + "/")
c.BaseURL = parsedURL
return c, mux, serverURL
}
func testMethod(t *testing.T, r *http.Request, want string) {
t.Helper()
got := r.Method
require.Equal(t, want, got, "request method: %v, want %v", got, want)
}
type values map[string]string
func testFormValues(t *testing.T, r *http.Request, values values) {
t.Helper()
want := url.Values{}
for k, v := range values {
want.Set(k, v)
}
require.NoError(t, r.ParseForm())
got := r.Form
require.Equal(t, want, got, "request parameters: %v, want %v", got, want)
}

View file

@ -0,0 +1,5 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Package client provides an HTTP client for using the Playbooks API.
package client

View file

@ -0,0 +1,37 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client_test
import (
"context"
"fmt"
"log"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost-plugin-playbooks/client"
)
func Example() {
ctx := context.Background()
client4 := model.NewAPIv4Client("http://localhost:8065")
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
if err != nil {
log.Fatal(err)
}
c, err := client.New(client4)
if err != nil {
log.Fatal(err)
}
playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
}

View file

@ -0,0 +1,52 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"encoding/json"
"errors"
"fmt"
)
// ErrorResponse is an error from an API request.
type ErrorResponse struct {
// Method is the HTTP verb used in the API request.
Method string
// URL is the HTTP endpoint used in the API request.
URL string
// StatusCode is the HTTP status code returned by the API.
StatusCode int
// Err is the error parsed from the API response.
Err error `json:"error"`
}
func (e *ErrorResponse) UnmarshalJSON(data []byte) error {
type Alias ErrorResponse
temp := &struct {
Err string `json:"error"`
*Alias
}{
Alias: (*Alias)(e),
}
// Try to extract a structured error from the body, otherwise fall back to using
// the whole body as the error message.
if err := json.Unmarshal(data, &temp); err != nil || temp.Err == "" {
e.Err = errors.New(string(data))
} else {
e.Err = errors.New(temp.Err)
}
return nil
}
// Unwrap exposes the underlying error of an ErrorResponse.
func (e *ErrorResponse) Unwrap() error {
return e.Err
}
// Error describes the error from the API request.
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("%s %s [%d]: %v", e.Method, e.URL, e.StatusCode, e.Err)
}

View file

@ -0,0 +1,57 @@
module github.com/mattermost/mattermost-plugin-playbooks/client
go 1.23.0
toolchain go1.23.9
require (
github.com/google/go-querystring v1.1.0
github.com/mattermost/mattermost/server/public v0.1.12
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0
golang.org/x/oauth2 v0.25.0
gopkg.in/guregu/null.v4 v4.0.0
)
require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.3 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.22 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -0,0 +1,300 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64=
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8=
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI=
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956/go.mod h1:SRl30Lb7/QoYyohYeVBuqYvvmXSZJxZgiV3Zf6VbxjI=
github.com/mattermost/logr/v2 v2.0.22 h1:npFkXlkAWR9J8payh8ftPcCZvLbHSI125mAM5/r/lP4=
github.com/mattermost/logr/v2 v2.0.22/go.mod h1:0sUKpO+XNMZApeumaid7PYaUZPBIydfuWZ0dqixXo+s=
github.com/mattermost/mattermost/server/public v0.1.12 h1:qlIU/llY0FWdHWQPtvncddQ99KJATPUX6wRHBlt8mfQ=
github.com/mattermost/mattermost/server/public v0.1.12/go.mod h1:3RJZfl7sMedX6ihX+JMFOIAzCHhd0WQnuez+UFQS80k=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME=
github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0=
github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8=
github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=

View file

@ -0,0 +1,197 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"fmt"
"gopkg.in/guregu/null.v4"
)
// Playbook represents the planning before a playbook run is initiated.
type Playbook struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Public bool `json:"public"`
TeamID string `json:"team_id"`
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
NumStages int64 `json:"num_stages"`
NumSteps int64 `json:"num_steps"`
Checklists []Checklist `json:"checklists"`
Members []PlaybookMember `json:"members"`
ReminderMessageTemplate string `json:"reminder_message_template"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
InvitedUserIDs []string `json:"invited_user_ids"`
InvitedGroupIDs []string `json:"invited_group_ids"`
InviteUsersEnabled bool `json:"invite_users_enabled"`
DefaultOwnerID string `json:"default_owner_id"`
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
BroadcastEnabled bool `json:"broadcast_enabled"`
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled"`
Metrics []PlaybookMetricConfig `json:"metrics"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
ChannelID string `json:"channel_id" export:"channel_id"`
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
}
type PlaybookMember struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
SchemeRoles []string `json:"scheme_roles"`
}
const (
MetricTypeDuration = "metric_duration"
MetricTypeCurrency = "metric_currency"
MetricTypeInteger = "metric_integer"
)
// Checklist represents a checklist in a playbook
type Checklist struct {
ID string `json:"id"`
Title string `json:"title"`
Items []ChecklistItem `json:"items"`
UpdateAt int64 `json:"update_at"`
}
// ChecklistItem represents an item in a checklist
type ChecklistItem struct {
ID string `json:"id"`
Title string `json:"title"`
State string `json:"state"`
StateModified int64 `json:"state_modified"`
AssigneeID string `json:"assignee_id"`
AssigneeModified int64 `json:"assignee_modified"`
Command string `json:"command"`
CommandLastRun int64 `json:"command_last_run"`
Description string `json:"description"`
LastSkipped int64 `json:"delete_at"`
DueDate int64 `json:"due_date"`
TaskActions []TaskAction `json:"task_actions"`
ConditionID string `json:"condition_id"`
ConditionAction string `json:"condition_action"`
ConditionReason string `json:"condition_reason"`
UpdateAt int64 `json:"update_at"`
}
// TaskAction represents a task action in an item
type TaskAction struct {
Trigger TriggerAction `json:"trigger"`
Actions []TriggerAction `json:"actions"`
}
// TriggerAction represents a trigger or action in a Task Action
type TriggerAction struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
// PlaybookCreateOptions specifies the parameters for PlaybooksService.Create method.
type PlaybookCreateOptions struct {
Title string `json:"title"`
Description string `json:"description"`
TeamID string `json:"team_id"`
Public bool `json:"public"`
CreatePublicPlaybookRun bool `json:"create_public_playbook_run"`
Checklists []Checklist `json:"checklists"`
Members []PlaybookMember `json:"members"`
BroadcastChannelID string `json:"broadcast_channel_id"`
ReminderMessageTemplate string `json:"reminder_message_template"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
InvitedUserIDs []string `json:"invited_user_ids"`
InvitedGroupIDs []string `json:"invited_group_ids"`
InviteUsersEnabled bool `json:"invite_users_enabled"`
DefaultOwnerID string `json:"default_owner_id"`
DefaultOwnerEnabled bool `json:"default_owner_enabled"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
BroadcastEnabled bool `json:"broadcast_enabled"`
Metrics []PlaybookMetricConfig `json:"metrics"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
ChannelID string `json:"channel_id" export:"channel_id"`
ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"`
}
type PlaybookMetricConfig struct {
ID string `json:"id"`
PlaybookID string `json:"playbook_id"`
Title string `json:"title"`
Description string `json:"description"`
Type string `json:"type"`
Target null.Int `json:"target"`
}
// PlaybookListOptions specifies the optional parameters to the
// PlaybooksService.List method.
type PlaybookListOptions struct {
Sort Sort `url:"sort,omitempty"`
Direction SortDirection `url:"direction,omitempty"`
SearchTeam string `url:"search_term,omitempty"`
WithArchived bool `url:"with_archived,omitempty"`
}
type GetPlaybooksResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []Playbook `json:"items"`
}
type PlaybookStats struct {
RunsInProgress int `json:"runs_in_progress"`
ParticipantsActive int `json:"participants_active"`
RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"`
RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"`
RunsStartedPerWeek []int `json:"runs_started_per_week"`
RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"`
ActiveRunsPerDay []int `json:"active_runs_per_day"`
ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"`
ActiveParticipantsPerDay []int `json:"active_participants_per_day"`
ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"`
MetricOverallAverage []null.Int `json:"metric_overall_average"`
MetricRollingAverage []null.Int `json:"metric_rolling_average"`
MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"`
MetricValueRange [][]int64 `json:"metric_value_range"`
MetricRollingValues [][]int64 `json:"metric_rolling_values"`
LastXRunNames []string `json:"last_x_run_names"`
}
type ChannelPlaybookMode int
const (
PlaybookRunCreateNewChannel ChannelPlaybookMode = iota
PlaybookRunLinkExistingChannel
)
var channelPlaybookTypes = [...]string{
PlaybookRunCreateNewChannel: "create_new_channel",
PlaybookRunLinkExistingChannel: "link_existing_channel",
}
// String creates the string version of the ChannelPlaybookMode
func (cpm ChannelPlaybookMode) String() string {
return channelPlaybookTypes[cpm]
}
// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON)
func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) {
return []byte(channelPlaybookTypes[cpm]), nil
}
// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON)
func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error {
for i, st := range channelPlaybookTypes {
if st == string(text) {
*cpm = ChannelPlaybookMode(i)
return nil
}
}
return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text))
}

View file

@ -0,0 +1,150 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
)
// PlaybookConditionsService handles communication with the playbook condition related
// methods of the Playbooks API.
type PlaybookConditionsService struct {
client *Client
}
// Condition represents a condition that can be applied to playbooks and runs.
type Condition struct {
ID string `json:"id"`
ConditionExpr ConditionExprV1 `json:"condition_expr"`
Version int `json:"version"`
PlaybookID string `json:"playbook_id"`
RunID string `json:"run_id,omitempty"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
}
// ConditionExprV1 represents a logical condition expression.
type ConditionExprV1 struct {
And []ConditionExprV1 `json:"and,omitempty"`
Or []ConditionExprV1 `json:"or,omitempty"`
Is *ComparisonCondition `json:"is,omitempty"`
IsNot *ComparisonCondition `json:"isNot,omitempty"`
}
// ComparisonCondition represents a field comparison condition.
type ComparisonCondition struct {
FieldID string `json:"field_id"`
Value json.RawMessage `json:"value"`
}
// PlaybookConditionListOptions specifies the optional parameters to various
// List methods that support pagination and filtering.
type PlaybookConditionListOptions struct {
Page int `url:"page,omitempty"`
PerPage int `url:"per_page,omitempty"`
}
// GetPlaybookConditionsResults contains the results of the List call.
type GetPlaybookConditionsResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []Condition `json:"items"`
}
// List the conditions for a playbook.
func (s *PlaybookConditionsService) List(ctx context.Context, playbookID string, page, perPage int, opts PlaybookConditionListOptions) (*GetPlaybookConditionsResults, error) {
conditionURL := fmt.Sprintf("playbooks/%s/conditions", playbookID)
conditionURL, err := addPaginationOptions(conditionURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
conditionURL, err = addOptions(conditionURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newAPIRequest(http.MethodGet, conditionURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetPlaybookConditionsResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Create a playbook condition.
func (s *PlaybookConditionsService) Create(ctx context.Context, playbookID string, condition Condition) (*Condition, error) {
conditionURL := fmt.Sprintf("playbooks/%s/conditions", playbookID)
req, err := s.client.newAPIRequest(http.MethodPost, conditionURL, condition)
if err != nil {
return nil, err
}
createdCondition := new(Condition)
resp, err := s.client.do(ctx, req, createdCondition)
if err != nil {
return nil, err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("expected status code %d", http.StatusCreated)
}
return createdCondition, nil
}
// Update a playbook condition.
func (s *PlaybookConditionsService) Update(ctx context.Context, playbookID, conditionID string, condition Condition) (*Condition, error) {
conditionURL := fmt.Sprintf("playbooks/%s/conditions/%s", playbookID, conditionID)
req, err := s.client.newAPIRequest(http.MethodPut, conditionURL, condition)
if err != nil {
return nil, err
}
updatedCondition := new(Condition)
resp, err := s.client.do(ctx, req, updatedCondition)
if err != nil {
return nil, err
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected status code %d", http.StatusOK)
}
return updatedCondition, nil
}
// Delete a playbook condition.
func (s *PlaybookConditionsService) Delete(ctx context.Context, playbookID, conditionID string) error {
conditionURL := fmt.Sprintf("playbooks/%s/conditions/%s", playbookID, conditionID)
req, err := s.client.newAPIRequest(http.MethodDelete, conditionURL, nil)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("expected status code %d", http.StatusNoContent)
}
return nil
}

View file

@ -0,0 +1,310 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"time"
"gopkg.in/guregu/null.v4"
)
// Me is a constant that refers to the current user, and can be used in various APIs in place of
// explicitly specifying the current user's id.
const Me = "me"
// PlaybookRun represents a playbook run.
type PlaybookRun struct {
ID string `json:"id"`
Name string `json:"name"`
Summary string `json:"summary"`
SummaryModifiedAt int64 `json:"summary_modified_at"`
OwnerUserID string `json:"owner_user_id"`
ReporterUserID string `json:"reporter_user_id"`
TeamID string `json:"team_id"`
ChannelID string `json:"channel_id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
EndAt int64 `json:"end_at"`
DeleteAt int64 `json:"delete_at"`
ActiveStage int `json:"active_stage"`
ActiveStageTitle string `json:"active_stage_title"`
PostID string `json:"post_id"`
PlaybookID string `json:"playbook_id"`
Type string `json:"type"`
Checklists []Checklist `json:"checklists"`
StatusPosts []StatusPost `json:"status_posts"`
CurrentStatus string `json:"current_status"`
LastStatusUpdateAt int64 `json:"last_status_update_at"`
ReminderPostID string `json:"reminder_post_id"`
PreviousReminder time.Duration `json:"previous_reminder"`
ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"`
StatusUpdateEnabled bool `json:"status_update_enabled"`
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
ReminderMessageTemplate string `json:"reminder_message_template"`
InvitedUserIDs []string `json:"invited_user_ids"`
InvitedGroupIDs []string `json:"invited_group_ids"`
TimelineEvents []TimelineEvent `json:"timeline_events"`
DefaultOwnerID string `json:"default_owner_id"`
WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"`
Retrospective string `json:"retrospective"`
RetrospectivePublishedAt int64 `json:"retrospective_published_at"`
RetrospectiveWasCanceled bool `json:"retrospective_was_canceled"`
RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds"`
RetrospectiveEnabled bool `json:"retrospective_enabled"`
MessageOnJoin string `json:"message_on_join"`
ParticipantIDs []string `json:"participant_ids"`
CategoryName string `json:"category_name"`
MetricsData []RunMetricData `json:"metrics_data"`
CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"`
RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"`
}
// StatusPost is information added to the playbook run when selecting from the db and sent to the
// client; it is not saved to the db.
type StatusPost struct {
ID string `json:"id"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
}
// StatusPostComplete is the complete status update (post)
// it's similar to StatusPost but with extended info.
type StatusPostComplete struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Message string `json:"message"`
AuthorUserName string `json:"author_user_name"`
}
// Metadata tracks ancillary metadata about a playbook run.
type Metadata struct {
ChannelName string `json:"channel_name"`
ChannelDisplayName string `json:"channel_display_name"`
TeamName string `json:"team_name"`
NumParticipants int64 `json:"num_participants"`
TotalPosts int64 `json:"total_posts"`
Followers []string `json:"followers"`
}
// TimelineEventType describes a type of timeline event.
type TimelineEventType string
const (
PlaybookRunCreated TimelineEventType = "incident_created"
TaskStateModified TimelineEventType = "task_state_modified"
StatusUpdated TimelineEventType = "status_updated"
StatusUpdateRequested TimelineEventType = "status_update_requested"
OwnerChanged TimelineEventType = "owner_changed"
AssigneeChanged TimelineEventType = "assignee_changed"
RanSlashCommand TimelineEventType = "ran_slash_command"
EventFromPost TimelineEventType = "event_from_post"
UserJoinedLeft TimelineEventType = "user_joined_left"
PublishedRetrospective TimelineEventType = "published_retrospective"
CanceledRetrospective TimelineEventType = "canceled_retrospective"
RunFinished TimelineEventType = "run_finished"
RunRestored TimelineEventType = "run_restored"
StatusUpdatesEnabled TimelineEventType = "status_updates_enabled"
StatusUpdatesDisabled TimelineEventType = "status_updates_disabled"
)
// TimelineEvent represents an event recorded to a playbook run's timeline.
type TimelineEvent struct {
ID string `json:"id"`
PlaybookRunID string `json:"playbook_run"`
CreateAt int64 `json:"create_at"`
DeleteAt int64 `json:"delete_at"`
EventAt int64 `json:"event_at"`
EventType TimelineEventType `json:"event_type"`
Summary string `json:"summary"`
Details string `json:"details"`
PostID string `json:"post_id"`
SubjectUserID string `json:"subject_user_id"`
CreatorUserID string `json:"creator_user_id"`
}
// PlaybookRunCreateOptions specifies the parameters for PlaybookRunService.Create method.
type PlaybookRunCreateOptions struct {
Name string `json:"name"`
OwnerUserID string `json:"owner_user_id"`
TeamID string `json:"team_id"`
ChannelID string `json:"channel_id"`
Summary string `json:"summary"`
PostID string `json:"post_id"`
PlaybookID string `json:"playbook_id"`
CreatePublicRun *bool `json:"create_public_run"`
Type string `json:"type"`
}
// RunAction represents the run action settings. Frontend passes this struct to update settings.
type RunAction struct {
BroadcastChannelIDs []string `json:"broadcast_channel_ids"`
WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"`
StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"`
StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"`
}
// RetrospectiveUpdate represents the run retrospective info
type RetrospectiveUpdate struct {
Text string `json:"retrospective"`
Metrics []RunMetricData `json:"metrics"`
}
// Sort enumerates the available fields we can sort on.
type Sort string
const (
// SortByCreateAt sorts by the "create_at" field. It is the default.
SortByCreateAt Sort = "create_at"
// SortByID sorts by the "id" field.
SortByID Sort = "id"
// SortByName sorts by the "name" field.
SortByName Sort = "name"
// SortByOwnerUserID sorts by the "owner_user_id" field.
SortByOwnerUserID Sort = "owner_user_id"
// SortByTeamID sorts by the "team_id" field.
SortByTeamID Sort = "team_id"
// SortByEndAt sorts by the "end_at" field.
SortByEndAt Sort = "end_at"
// SortBySteps sorts playbooks by the number of steps in the playbook.
SortBySteps Sort = "steps"
// SortByStages sorts playbooks by the number of stages in the playbook.
SortByStages Sort = "stages"
// SortByTitle sorts by the "title" field.
SortByTitle Sort = "title"
// SortByRuns sorts by the number of times a playbook has been run.
SortByRuns Sort = "runs"
)
// SortDirection determines whether results are sorted ascending or descending.
type SortDirection string
const (
// Desc sorts the results in descending order.
SortDesc SortDirection = "DESC"
// Asc sorts the results in ascending order.
SortAsc SortDirection = "ASC"
)
// PlaybookRunListOptions specifies the optional parameters to the
// PlaybookRunService.List method.
type PlaybookRunListOptions struct {
// TeamID filters playbook runs to those in the given team.
TeamID string `url:"team_id,omitempty"`
Sort Sort `url:"sort,omitempty"`
Direction SortDirection `url:"direction,omitempty"`
// Statuses filters by InProgress or Ended; defaults to All when no status specified.
Statuses []Status `url:"statuses,omitempty"`
// OwnerID filters by owner's Mattermost user ID. Defaults to blank (no filter). Specify "me" for current user.
OwnerID string `url:"owner_user_id,omitempty"`
// ParticipantID filters playbook runs that have this user as a participant. Defaults to blank (no filter). Specify "me" for current user.
ParticipantID string `url:"participant_id,omitempty"`
// ParticipantOrFollowerID filters playbook runs that have this user as member or as follower. Defaults to blank (no filter). Specify "me" for current user.
ParticipantOrFollowerID string `url:"participant_or_follower,omitempty"`
// SearchTerm returns results of the search term and respecting the other header filter options.
// The search term acts as a filter and respects the Sort and Direction fields (i.e., results are
// not returned in relevance order).
SearchTerm string `url:"search_term,omitempty"`
// PlaybookID filters playbook runs that are derived from this playbook id.
// Defaults to blank (no filter).
PlaybookID string `url:"playbook_id,omitempty"`
// ChannelID filters playbook runs associated with the given channel ID.
// Defaults to blank (no filter).
ChannelID string `url:"channel_id,omitempty"`
// ActiveGTE filters playbook runs that were active after (or equal) to the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
ActiveGTE int64 `url:"active_gte,omitempty"`
// ActiveLT filters playbook runs that were active before the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
ActiveLT int64 `url:"active_lt,omitempty"`
// StartedGTE filters playbook runs that were started after (or equal) to the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
StartedGTE int64 `url:"started_gte,omitempty"`
// StartedLT filters playbook runs that were started before the unix time given (in millis).
// A value of 0 means the filter is ignored (which is the default).
StartedLT int64 `url:"started_lt,omitempty"`
// ActivitySince, if not zero, returns playbook runs that have had any activity since this timestamp.
// Activity includes creation, updates, or completion that occurred after this timestamp (in milliseconds).
// A value of 0 (or negative, normalized to 0) means this filter is not applied.
// This is sent as the "since" URL parameter.
ActivitySince int64 `url:"since,omitempty"`
}
// PlaybookRunList contains the paginated result.
type PlaybookRunList struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []*PlaybookRun
}
// Status is the type used to specify the activity status of the playbook run.
type Status string
const (
StatusInProgress Status = "InProgress"
StatusFinished Status = "Finished"
)
type GetPlaybookRunsResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []PlaybookRun `json:"items"`
}
// StatusUpdateOptions are the fields required to update a playbook run's status
type StatusUpdateOptions struct {
Message string `json:"message"`
Reminder time.Duration `json:"reminder"`
FinishRun bool `json:"finish_run"`
}
// PlaybookRunUpdateOptions are the fields that can be updated for a playbook run
type PlaybookRunUpdateOptions struct {
Name *string `json:"name,omitempty"`
Summary *string `json:"summary,omitempty"`
}
type RunMetricData struct {
MetricConfigID string `json:"metric_config_id"`
Value null.Int `json:"value"`
}
// OwnerInfo holds the summary information of a owner.
type OwnerInfo struct {
UserID string `json:"user_id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Nickname string `json:"nickname"`
}

View file

@ -0,0 +1,468 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
"time"
)
// PlaybookRunService handles communication with the playbook run related
// methods of the Playbooks API.
type PlaybookRunService struct {
client *Client
}
// Get a playbook run.
func (s *PlaybookRunService) Get(ctx context.Context, playbookRunID string) (*PlaybookRun, error) {
playbookRunURL := fmt.Sprintf("runs/%s", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// GetByChannelID gets a playbook run by ChannelID.
func (s *PlaybookRunService) GetByChannelID(ctx context.Context, channelID string) (*PlaybookRun, error) {
channelURL := fmt.Sprintf("runs/channel/%s", channelID)
req, err := s.client.newAPIRequest(http.MethodGet, channelURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// Get a playbook run's metadata.
func (s *PlaybookRunService) GetMetadata(ctx context.Context, playbookRunID string) (*Metadata, error) {
playbookRunURL := fmt.Sprintf("runs/%s/metadata", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
playbookRun := new(Metadata)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
// Get all playbook status updates.
func (s *PlaybookRunService) GetStatusUpdates(ctx context.Context, playbookRunID string) ([]StatusPostComplete, error) {
playbookRunURL := fmt.Sprintf("runs/%s/status-updates", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, err
}
var statusUpdates []StatusPostComplete
resp, err := s.client.do(ctx, req, &statusUpdates)
if err != nil {
return nil, err
}
resp.Body.Close()
return statusUpdates, nil
}
// List the playbook runs.
func (s *PlaybookRunService) List(ctx context.Context, page, perPage int, opts PlaybookRunListOptions) (*GetPlaybookRunsResults, error) {
playbookRunURL := "runs"
playbookRunURL, err := addOptions(playbookRunURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
playbookRunURL, err = addPaginationOptions(playbookRunURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
req, err := s.client.newAPIRequest(http.MethodGet, playbookRunURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetPlaybookRunsResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Create a playbook run.
func (s *PlaybookRunService) Create(ctx context.Context, opts PlaybookRunCreateOptions) (*PlaybookRun, error) {
playbookRunURL := "runs"
req, err := s.client.newAPIRequest(http.MethodPost, playbookRunURL, opts)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("expected status code %d", http.StatusCreated)
}
return playbookRun, nil
}
func (s *PlaybookRunService) UpdateStatus(ctx context.Context, playbookRunID string, message string, reminderInSeconds int64) error {
updateURL := fmt.Sprintf("runs/%s/status", playbookRunID)
opts := StatusUpdateOptions{
Message: message,
Reminder: time.Duration(reminderInSeconds),
}
req, err := s.client.newAPIRequest(http.MethodPost, updateURL, opts)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return nil
}
// Update updates a playbook run.
func (s *PlaybookRunService) Update(ctx context.Context, playbookRunID string, updates PlaybookRunUpdateOptions) (*PlaybookRun, error) {
updateURL := fmt.Sprintf("runs/%s", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPatch, updateURL, updates)
if err != nil {
return nil, err
}
playbookRun := new(PlaybookRun)
resp, err := s.client.do(ctx, req, playbookRun)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbookRun, nil
}
func (s *PlaybookRunService) RequestUpdate(ctx context.Context, playbookRunID, userID string) error {
requestURL := fmt.Sprintf("runs/%s/request-update", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPost, requestURL, nil)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
func (s *PlaybookRunService) Finish(ctx context.Context, playbookRunID string) error {
finishURL := fmt.Sprintf("runs/%s/finish", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPut, finishURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybookRunService) CreateChecklist(ctx context.Context, playbookRunID string, checklist Checklist) error {
createURL := fmt.Sprintf("runs/%s/checklists", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPost, createURL, checklist)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RemoveChecklist(ctx context.Context, playbookRunID string, checklistNumber int) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d", playbookRunID, checklistNumber)
req, err := s.client.newAPIRequest(http.MethodDelete, createURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RenameChecklist(ctx context.Context, playbookRunID string, checklistNumber int, newTitle string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/rename", playbookRunID, checklistNumber)
req, err := s.client.newAPIRequest(http.MethodPut, createURL, struct{ Title string }{newTitle})
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) AddChecklistItem(ctx context.Context, playbookRunID string, checklistNumber int, checklistItem ChecklistItem) error {
addURL := fmt.Sprintf("runs/%s/checklists/%d/add", playbookRunID, checklistNumber)
req, err := s.client.newAPIRequest(http.MethodPost, addURL, checklistItem)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) MoveChecklist(ctx context.Context, playbookRunID string, sourceChecklistIdx, destChecklistIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/move", playbookRunID)
body := struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
}{sourceChecklistIdx, destChecklistIdx}
req, err := s.client.newAPIRequest(http.MethodPost, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) MoveChecklistItem(ctx context.Context, playbookRunID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/move-item", playbookRunID)
body := struct {
SourceChecklistIdx int `json:"source_checklist_idx"`
SourceItemIdx int `json:"source_item_idx"`
DestChecklistIdx int `json:"dest_checklist_idx"`
DestItemIdx int `json:"dest_item_idx"`
}{sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx}
req, err := s.client.newAPIRequest(http.MethodPost, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
// UpdateRetrospective updates the run's retrospective info
func (s *PlaybookRunService) UpdateRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
createURL := fmt.Sprintf("runs/%s/retrospective", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPost, createURL, retroUpdate)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
// PublishRetrospective publishes the run's retrospective
func (s *PlaybookRunService) PublishRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error {
createURL := fmt.Sprintf("runs/%s/retrospective/publish", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPost, createURL, retroUpdate)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected status code %d", http.StatusOK)
}
return err
}
func (s *PlaybookRunService) SetItemAssignee(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, assigneeID string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/assignee", playbookRunID, checklistIdx, itemIdx)
body := struct {
AssigneeID string `json:"assignee_id"`
}{assigneeID}
req, err := s.client.newAPIRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
return err
}
func (s *PlaybookRunService) SetItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, newCommand string) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/command", playbookRunID, checklistIdx, itemIdx)
body := struct {
Command string `json:"command"`
}{newCommand}
req, err := s.client.newAPIRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) RunItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/run", playbookRunID, checklistIdx, itemIdx)
req, err := s.client.newAPIRequest(http.MethodPost, createURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
func (s *PlaybookRunService) SetItemDueDate(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, duedate int64) error {
createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/duedate", playbookRunID, checklistIdx, itemIdx)
body := struct {
DueDate int64 `json:"due_date"`
}{duedate}
req, err := s.client.newAPIRequest(http.MethodPut, createURL, body)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
return err
}
// Get a playbook run.
func (s *PlaybookRunService) GetOwners(ctx context.Context) ([]OwnerInfo, error) {
req, err := s.client.newAPIRequest(http.MethodGet, "runs/owners", nil)
if err != nil {
return nil, err
}
owners := make([]OwnerInfo, 0)
resp, err := s.client.do(ctx, req, &owners)
if err != nil {
return nil, err
}
resp.Body.Close()
return owners, nil
}
// GetPropertyFields gets all property fields for a run. It is a wrapper around GetPropertyFieldsSince with updatedSince set to 0.
func (s *PlaybookRunService) GetPropertyFields(ctx context.Context, playbookRunID string) ([]PropertyField, error) {
return s.GetPropertyFieldsSince(ctx, playbookRunID, 0)
}
// GetPropertyFieldsSince gets all property fields for a run since a given timestamp.
// updatedSince: optional timestamp in milliseconds - only return fields updated after this time (0 = all)
func (s *PlaybookRunService) GetPropertyFieldsSince(ctx context.Context, playbookRunID string, updatedSince int64) ([]PropertyField, error) {
propertyFieldsURL := fmt.Sprintf("runs/%s/property_fields", playbookRunID)
if updatedSince > 0 {
propertyFieldsURL = fmt.Sprintf("%s?updated_since=%d", propertyFieldsURL, updatedSince)
}
req, err := s.client.newAPIRequest(http.MethodGet, propertyFieldsURL, nil)
if err != nil {
return nil, err
}
var fields []PropertyField
resp, err := s.client.do(ctx, req, &fields)
if err != nil {
return nil, err
}
resp.Body.Close()
return fields, nil
}
// GetPropertyValues gets all property values for a run. It is a wrapper around GetPropertyValuesSince with updatedSince set to 0.
func (s *PlaybookRunService) GetPropertyValues(ctx context.Context, playbookRunID string) ([]PropertyValue, error) {
return s.GetPropertyValuesSince(ctx, playbookRunID, 0)
}
// GetPropertyValuesSince gets all property values for a run since a given timestamp.
// updatedSince: optional timestamp in milliseconds - only return values updated after this time (0 = all)
func (s *PlaybookRunService) GetPropertyValuesSince(ctx context.Context, playbookRunID string, updatedSince int64) ([]PropertyValue, error) {
propertyValuesURL := fmt.Sprintf("runs/%s/property_values", playbookRunID)
if updatedSince > 0 {
propertyValuesURL = fmt.Sprintf("%s?updated_since=%d", propertyValuesURL, updatedSince)
}
req, err := s.client.newAPIRequest(http.MethodGet, propertyValuesURL, nil)
if err != nil {
return nil, err
}
var values []PropertyValue
resp, err := s.client.do(ctx, req, &values)
if err != nil {
return nil, err
}
resp.Body.Close()
return values, nil
}
// SetPropertyValue sets a property value for a run.
func (s *PlaybookRunService) SetPropertyValue(ctx context.Context, playbookRunID, fieldID string, value PropertyValueRequest) (*PropertyValue, error) {
propertyValueURL := fmt.Sprintf("runs/%s/property_fields/%s/value", playbookRunID, fieldID)
req, err := s.client.newAPIRequest(http.MethodPut, propertyValueURL, value)
if err != nil {
return nil, err
}
propertyValue := new(PropertyValue)
resp, err := s.client.do(ctx, req, propertyValue)
if err != nil {
return nil, err
}
resp.Body.Close()
return propertyValue, nil
}

View file

@ -0,0 +1,78 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client_test
import (
"context"
"fmt"
"log"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost-plugin-playbooks/client"
)
func ExamplePlaybookRunService_Get() {
ctx := context.Background()
client4 := model.NewAPIv4Client("http://localhost:8065")
client4.Login(context.Background(), "test@example.com", "testtest")
c, err := client.New(client4)
if err != nil {
log.Fatal(err)
}
playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
}
func ExamplePlaybookRunService_List() {
ctx := context.Background()
client4 := model.NewAPIv4Client("http://localhost:8065")
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
if err != nil {
log.Fatal(err.Error())
}
teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1)
if err != nil {
log.Fatal(err.Error())
}
if len(teams) == 0 {
log.Fatal("no teams for this user")
}
c, err := client.New(client4)
if err != nil {
log.Fatal(err)
}
var playbookRuns []client.PlaybookRun
for page := 0; ; page++ {
result, err := c.PlaybookRuns.List(ctx, page, 100, client.PlaybookRunListOptions{
TeamID: teams[0].Id,
Sort: client.SortByCreateAt,
Direction: client.SortDesc,
})
if err != nil {
log.Fatal(err)
}
playbookRuns = append(playbookRuns, result.Items...)
if !result.HasMore {
break
}
}
for _, playbookRun := range playbookRuns {
fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name)
}
}

View file

@ -0,0 +1,349 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"github.com/pkg/errors"
)
// PlaybooksService handles communication with the playbook related
// methods of the Playbook API.
type PlaybooksService struct {
client *Client
}
// Get a playbook.
func (s *PlaybooksService) Get(ctx context.Context, playbookID string) (*Playbook, error) {
playbookURL := fmt.Sprintf("playbooks/%s", playbookID)
req, err := s.client.newAPIRequest(http.MethodGet, playbookURL, nil)
if err != nil {
return nil, err
}
playbook := new(Playbook)
resp, err := s.client.do(ctx, req, playbook)
if err != nil {
return nil, err
}
resp.Body.Close()
return playbook, nil
}
// List the playbooks.
func (s *PlaybooksService) List(ctx context.Context, teamId string, page, perPage int, opts PlaybookListOptions) (*GetPlaybooksResults, error) {
playbookURL := "playbooks"
playbookURL, err := addOption(playbookURL, "team_id", teamId)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
playbookURL, err = addPaginationOptions(playbookURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
playbookURL, err = addOptions(playbookURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newAPIRequest(http.MethodGet, playbookURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetPlaybooksResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}
// Create a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Create(ctx context.Context, opts PlaybookCreateOptions) (string, error) {
// For ease of use set the default if not specificed so it doesn't just error
if opts.ReminderTimerDefaultSeconds == 0 {
opts.ReminderTimerDefaultSeconds = 86400
}
playbookURL := "playbooks"
req, err := s.client.newAPIRequest(http.MethodPost, playbookURL, opts)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
func (s *PlaybooksService) Update(ctx context.Context, playbook Playbook) error {
updateURL := fmt.Sprintf("playbooks/%s", playbook.ID)
req, err := s.client.newAPIRequest(http.MethodPut, updateURL, playbook)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) Archive(ctx context.Context, playbookID string) error {
updateURL := fmt.Sprintf("playbooks/%s", playbookID)
req, err := s.client.newAPIRequest(http.MethodDelete, updateURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) Export(ctx context.Context, playbookID string) ([]byte, error) {
url := fmt.Sprintf("playbooks/%s/export", playbookID)
req, err := s.client.newAPIRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := s.client.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("expected status code %d", http.StatusOK)
}
return result, nil
}
// Duplicate a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Duplicate(ctx context.Context, playbookID string) (string, error) {
url := fmt.Sprintf("playbooks/%s/duplicate", playbookID)
req, err := s.client.newAPIRequest(http.MethodPost, url, nil)
if err != nil {
return "", err
}
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
// Imports a playbook. Returns the id of the newly created playbook
func (s *PlaybooksService) Import(ctx context.Context, toImport []byte, team string) (string, error) {
url := "playbooks/import?team_id=" + team
u, err := s.client.BaseURL.Parse(buildAPIURL(url))
if err != nil {
return "", errors.Wrapf(err, "invalid endpoint %s", url)
}
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(toImport))
if err != nil {
return "", errors.Wrapf(err, "failed to create http request for import")
}
req.Header.Set("Content-Type", "application/json")
var result struct {
ID string `json:"id"`
}
resp, err := s.client.do(ctx, req, &result)
if err != nil {
return "", err
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("expected status code %d", http.StatusCreated)
}
return result.ID, nil
}
func (s *PlaybooksService) Stats(ctx context.Context, playbookID string) (*PlaybookStats, error) {
playbookStatsURL := fmt.Sprintf("stats/playbook?playbook_id=%s", playbookID)
req, err := s.client.newAPIRequest(http.MethodGet, playbookStatsURL, nil)
if err != nil {
return nil, err
}
stats := new(PlaybookStats)
resp, err := s.client.do(ctx, req, stats)
if err != nil {
return nil, err
}
resp.Body.Close()
return stats, nil
}
func (s *PlaybooksService) AutoFollow(ctx context.Context, playbookID string, userID string) error {
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
req, err := s.client.newAPIRequest(http.MethodPut, followsURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) AutoUnfollow(ctx context.Context, playbookID string, userID string) error {
followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID)
req, err := s.client.newAPIRequest(http.MethodDelete, followsURL, nil)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}
func (s *PlaybooksService) GetAutoFollows(ctx context.Context, playbookID string) ([]string, error) {
autofollowsURL := fmt.Sprintf("playbooks/%s/autofollows", playbookID)
req, err := s.client.newAPIRequest(http.MethodGet, autofollowsURL, nil)
if err != nil {
return nil, err
}
var followers []string
resp, err := s.client.do(ctx, req, &followers)
if err != nil {
return nil, err
}
resp.Body.Close()
return followers, nil
}
// GetPropertyFields gets all property fields for a playbook. It is a wrapper around GetPropertyFieldsSince with updatedSince set to 0.
func (s *PlaybooksService) GetPropertyFields(ctx context.Context, playbookID string) ([]PropertyField, error) {
return s.GetPropertyFieldsSince(ctx, playbookID, 0)
}
// GetPropertyFieldsSince gets all property fields for a playbook.
// updatedSince: optional timestamp in milliseconds - only return fields updated after this time (0 = all)
func (s *PlaybooksService) GetPropertyFieldsSince(ctx context.Context, playbookID string, updatedSince int64) ([]PropertyField, error) {
propertyFieldsURL := fmt.Sprintf("playbooks/%s/property_fields", playbookID)
if updatedSince > 0 {
propertyFieldsURL = fmt.Sprintf("%s?updated_since=%d", propertyFieldsURL, updatedSince)
}
req, err := s.client.newAPIRequest(http.MethodGet, propertyFieldsURL, nil)
if err != nil {
return nil, err
}
var fields []PropertyField
resp, err := s.client.do(ctx, req, &fields)
if err != nil {
return nil, err
}
resp.Body.Close()
return fields, nil
}
// CreatePropertyField creates a new property field for a playbook.
func (s *PlaybooksService) CreatePropertyField(ctx context.Context, playbookID string, field PropertyFieldRequest) (*PropertyField, error) {
propertyFieldsURL := fmt.Sprintf("playbooks/%s/property_fields", playbookID)
req, err := s.client.newAPIRequest(http.MethodPost, propertyFieldsURL, field)
if err != nil {
return nil, err
}
propertyField := new(PropertyField)
resp, err := s.client.do(ctx, req, propertyField)
if err != nil {
return nil, err
}
resp.Body.Close()
return propertyField, nil
}
// UpdatePropertyField updates an existing property field for a playbook.
func (s *PlaybooksService) UpdatePropertyField(ctx context.Context, playbookID, fieldID string, field PropertyFieldRequest) (*PropertyField, error) {
propertyFieldURL := fmt.Sprintf("playbooks/%s/property_fields/%s", playbookID, fieldID)
req, err := s.client.newAPIRequest(http.MethodPut, propertyFieldURL, field)
if err != nil {
return nil, err
}
propertyField := new(PropertyField)
resp, err := s.client.do(ctx, req, propertyField)
if err != nil {
return nil, err
}
resp.Body.Close()
return propertyField, nil
}
// DeletePropertyField deletes a property field from a playbook.
func (s *PlaybooksService) DeletePropertyField(ctx context.Context, playbookID, fieldID string) error {
propertyFieldURL := fmt.Sprintf("playbooks/%s/property_fields/%s", playbookID, fieldID)
req, err := s.client.newAPIRequest(http.MethodDelete, propertyFieldURL, nil)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, nil)
if err != nil {
return err
}
resp.Body.Close()
return nil
}

View file

@ -0,0 +1,77 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client_test
import (
"context"
"fmt"
"log"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost-plugin-playbooks/client"
)
func ExamplePlaybooksService_Get() {
ctx := context.Background()
client4 := model.NewAPIv4Client("http://localhost:8065")
client4.Login(context.Background(), "test@example.com", "testtest")
c, err := client.New(client4)
if err != nil {
log.Fatal(err)
}
playbookID := "h4n3h7s1qjf5pkis4dn6cuxgwa"
playbook, err := c.Playbooks.Get(ctx, playbookID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Playbook Name: %s\n", playbook.Title)
}
func ExamplePlaybooksService_List() {
ctx := context.Background()
client4 := model.NewAPIv4Client("http://localhost:8065")
_, _, err := client4.Login(context.Background(), "test@example.com", "testtest")
if err != nil {
log.Fatal(err.Error())
}
teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1)
if err != nil {
log.Fatal(err.Error())
}
if len(teams) == 0 {
log.Fatal("no teams for this user")
}
c, err := client.New(client4)
if err != nil {
log.Fatal(err)
}
var playbooks []client.Playbook
for page := 0; ; page++ {
result, err := c.Playbooks.List(ctx, teams[0].Id, page, 100, client.PlaybookListOptions{
Sort: client.SortByCreateAt,
Direction: client.SortDesc,
})
if err != nil {
log.Fatal(err)
}
playbooks = append(playbooks, result.Items...)
if !result.HasMore {
break
}
}
for _, playbook := range playbooks {
fmt.Printf("Playbook Name: %s\n", playbook.Title)
}
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"encoding/json"
)
// PropertyField represents a property field definition
type PropertyField struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
Attrs map[string]interface{} `json:"attrs"`
}
// PropertyValue represents a property value
type PropertyValue struct {
ID string `json:"id"`
FieldID string `json:"field_id"`
Value json.RawMessage `json:"value"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
DeleteAt int64 `json:"delete_at"`
}
// PropertyFieldRequest represents a request to create or update a property field
type PropertyFieldRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Attrs *PropertyFieldAttrsInput `json:"attrs,omitempty"`
}
// PropertyFieldAttrsInput represents property field attributes for input
type PropertyFieldAttrsInput struct {
Visibility *string `json:"visibility,omitempty"`
SortOrder *float64 `json:"sortOrder,omitempty"`
Options *[]PropertyOptionInput `json:"options,omitempty"`
ParentID *string `json:"parentID,omitempty"`
}
// PropertyOptionInput represents a property option for input
type PropertyOptionInput struct {
ID *string `json:"id,omitempty"`
Name string `json:"name"`
Color *string `json:"color,omitempty"`
}
// PropertyValueRequest represents a request to set a property value
type PropertyValueRequest struct {
Value json.RawMessage `json:"value"`
}

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
type ReminderResetPayload struct {
NewReminderSeconds int `json:"new_reminder_seconds"`
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"io/ioutil"
"net/http"
)
type RemindersService struct {
client *Client
}
func (s *RemindersService) Reset(ctx context.Context, playbookRunID string, payload ReminderResetPayload) error {
resetURL := fmt.Sprintf("runs/%s/reminder", playbookRunID)
req, err := s.client.newAPIRequest(http.MethodPost, resetURL, payload)
if err != nil {
return err
}
resp, err := s.client.do(ctx, req, ioutil.Discard)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("expected status code %d", http.StatusNoContent)
}
return nil
}

View file

@ -0,0 +1,59 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
// RunConditionsService handles communication with the run condition related
// methods of the Playbooks API. Run conditions are read-only.
type RunConditionsService struct {
client *Client
}
// RunConditionListOptions specifies the optional parameters to various
// List methods that support pagination and filtering for run conditions.
type RunConditionListOptions struct {
Page int `url:"page,omitempty"`
PerPage int `url:"per_page,omitempty"`
}
// GetRunConditionsResults contains the results of the List call for run conditions.
type GetRunConditionsResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
HasMore bool `json:"has_more"`
Items []Condition `json:"items"`
}
// List the conditions for a run (read-only).
func (s *RunConditionsService) List(ctx context.Context, runID string, page, perPage int, opts RunConditionListOptions) (*GetRunConditionsResults, error) {
conditionURL := fmt.Sprintf("runs/%s/conditions", runID)
conditionURL, err := addPaginationOptions(conditionURL, page, perPage)
if err != nil {
return nil, fmt.Errorf("failed to build pagination options: %w", err)
}
conditionURL, err = addOptions(conditionURL, opts)
if err != nil {
return nil, fmt.Errorf("failed to build options: %w", err)
}
req, err := s.client.newAPIRequest(http.MethodGet, conditionURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
result := &GetRunConditionsResults{}
resp, err := s.client.do(ctx, req, result)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
resp.Body.Close()
return result, nil
}

View file

@ -0,0 +1,52 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"net/http"
)
type GlobalSettings struct {
EnableExperimentalFeatures bool `json:"enable_experimental_features"`
}
// SettingsService handles communication with the settings related methods.
type SettingsService struct {
client *Client
}
// Get the configured settings.
func (s *SettingsService) Get(ctx context.Context) (*GlobalSettings, error) {
settingsURL := "settings"
req, err := s.client.newAPIRequest(http.MethodGet, settingsURL, nil)
if err != nil {
return nil, err
}
settings := new(GlobalSettings)
resp, err := s.client.do(ctx, req, settings)
if err != nil {
return nil, err
}
resp.Body.Close()
return settings, nil
}
// Update the configured settings.
func (s *SettingsService) Update(ctx context.Context, settings GlobalSettings) error {
settingsURL := "settings"
req, err := s.client.newAPIRequest(http.MethodPut, settingsURL, settings)
if err != nil {
return err
}
_, err = s.client.do(ctx, req, nil)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,199 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Test file for the ActivitySince parameter in PlaybookRunListOptions.
// This test validates that the client properly encodes and sends the 'since'
// parameter, and correctly handles different response scenarios.
package client
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestSinceParameter tests the ActivitySince parameter in PlaybookRunListOptions.
// It covers four test cases:
// 1. With a past timestamp - should return runs active after that time
// 2. With a future timestamp - should return no runs
// 3. Without a since parameter - should return default results
// 4. URL encoding - verifies the parameter is properly encoded in the URL
func TestSinceParameter(t *testing.T) {
// Common response values
const defaultTotalCount = 2
const defaultPageCount = 1
// Setup server to simulate API responses
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Parse query parameters
query := r.URL.Query()
sinceStr := query.Get("since")
// Create response based on since parameter
resp := GetPlaybookRunsResults{
TotalCount: defaultTotalCount,
PageCount: defaultPageCount,
HasMore: false,
Items: []PlaybookRun{},
}
// Parse since parameter if present
if sinceStr != "" {
since, err := strconv.ParseInt(sinceStr, 10, 64)
if err != nil {
http.Error(w, "Invalid since parameter", http.StatusBadRequest)
return
}
// Create timestamps for testing
now := time.Now().UnixMilli()
// Add playbook runs only if the since parameter is in the past
if since < now {
// First run - created before since, updated after since
run1 := PlaybookRun{
ID: "run1",
Name: "Run 1",
CreateAt: since - 10000, // Created before since
UpdateAt: since + 5000, // Updated after since
}
// Second run - both created and updated after since
run2 := PlaybookRun{
ID: "run2",
Name: "Run 2",
CreateAt: since + 1000, // Created after since
UpdateAt: since + 2000, // Updated after since
}
resp.Items = append(resp.Items, run1, run2)
}
}
// Return JSON response
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
// Create test client
parsed, _ := url.Parse(ts.URL)
mockClient4 := model.NewAPIv4Client(parsed.String())
mockClient4.HTTPClient = &http.Client{}
c, err := New(mockClient4)
require.NoError(t, err)
// Test 1: Get runs with since parameter in the past
t.Run("WithSinceParameter", func(t *testing.T) {
// Set since time to 1 hour ago
since := time.Now().Add(-1 * time.Hour).UnixMilli()
// Call API with since parameter
ctx := context.Background()
result, err := c.PlaybookRuns.List(ctx, 0, 100, PlaybookRunListOptions{
ActivitySince: since,
})
// Verify results
require.NoError(t, err)
assert.Equal(t, defaultTotalCount, result.TotalCount, "Should return expected total count")
assert.Len(t, result.Items, 2, "Should return 2 runs")
// Verify first run fields - created before since but updated after
assert.Equal(t, "run1", result.Items[0].ID)
assert.Less(t, result.Items[0].CreateAt, since, "First run should be created before since")
assert.Greater(t, result.Items[0].UpdateAt, since, "First run should be updated after since")
// Verify second run fields - both created and updated after since
assert.Equal(t, "run2", result.Items[1].ID)
assert.Greater(t, result.Items[1].CreateAt, since, "Second run should be created after since")
assert.Greater(t, result.Items[1].UpdateAt, since, "Second run should be updated after since")
})
// Test 2: Get runs with future since parameter (should return empty results)
t.Run("WithFutureSinceParameter", func(t *testing.T) {
// Set since time to 1 hour in the future
since := time.Now().Add(1 * time.Hour).UnixMilli()
// Call API with since parameter
ctx := context.Background()
result, err := c.PlaybookRuns.List(ctx, 0, 100, PlaybookRunListOptions{
ActivitySince: since,
})
// Verify results
require.NoError(t, err)
assert.Equal(t, defaultTotalCount, result.TotalCount)
assert.Len(t, result.Items, 0, "Should have no runs when using future timestamp")
})
// Test 3: Get runs without since parameter
t.Run("WithoutSinceParameter", func(t *testing.T) {
// Call API without since parameter
ctx := context.Background()
result, err := c.PlaybookRuns.List(ctx, 0, 100, PlaybookRunListOptions{})
// Verify results
require.NoError(t, err)
assert.Equal(t, defaultTotalCount, result.TotalCount)
assert.Len(t, result.Items, 0, "Should have no runs when since parameter is omitted")
})
// Test 4: Verify URL encoding of since parameter
t.Run("URLEncoding", func(t *testing.T) {
// Create a specific server just for URL validation
urlCheckServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Check that the since parameter is properly included in the URL
query := r.URL.Query()
sinceStr := query.Get("since")
// Validate parameter exists with expected value
require.NotEmpty(t, sinceStr, "Since parameter should be present in URL")
since, err := strconv.ParseInt(sinceStr, 10, 64)
require.NoError(t, err, "Since parameter should be a valid int64")
assert.Equal(t, int64(12345), since, "Since parameter should match the value we sent")
// Return empty success response
w.WriteHeader(http.StatusOK)
empty := GetPlaybookRunsResults{
TotalCount: 0,
PageCount: 0,
HasMore: false,
Items: []PlaybookRun{},
}
json.NewEncoder(w).Encode(empty)
}))
defer urlCheckServer.Close()
// Create a client pointing to our URL check server
parsedURL, _ := url.Parse(urlCheckServer.URL)
urlCheckClient4 := model.NewAPIv4Client(parsedURL.String())
urlCheckClient4.HTTPClient = &http.Client{}
urlCheckC, err := New(urlCheckClient4)
require.NoError(t, err)
// Make request with specific test value
ctx := context.Background()
_, err = urlCheckC.PlaybookRuns.List(ctx, 0, 100, PlaybookRunListOptions{
ActivitySince: 12345,
})
require.NoError(t, err, "Request with since parameter should succeed")
})
}

View file

@ -0,0 +1,38 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"net/http"
)
// StatsService handles communication with the stats related methods.
type StatsService struct {
client *Client
}
// PlaybookSiteStats holds the data that we want to expose in system console
type PlaybookSiteStats struct {
TotalPlaybooks int `json:"total_playbooks"`
TotalPlaybookRuns int `json:"total_playbook_runs"`
}
// Get the stats that should be displayed in system console.
func (s *StatsService) GetSiteStats(ctx context.Context) (*PlaybookSiteStats, error) {
statsURL := "stats/site"
req, err := s.client.newAPIRequest(http.MethodGet, statsURL, nil)
if err != nil {
return nil, err
}
stats := new(PlaybookSiteStats)
resp, err := s.client.do(ctx, req, stats)
if err != nil {
return nil, err
}
resp.Body.Close()
return stats, nil
}

View file

@ -0,0 +1,68 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"context"
"fmt"
"net/http"
)
// LimitedUser returns the minimum amount of user data needed for the app.
type LimitedUser struct {
UserID string `json:"user_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// LimitedUser returns the minimum amount of post data needed for the app.
type LimitedPost struct {
Message string `json:"message"`
CreateAt int64 `json:"create_at"`
UserID string `json:"user_id"`
}
type TabAppResults struct {
TotalCount int `json:"total_count"`
PageCount int `json:"page_count"`
PerPage int `json:"per_page"`
HasMore bool `json:"has_more"`
Items []PlaybookRun `json:"items"`
Users map[string]LimitedUser `json:"users"`
Posts map[string]LimitedPost `json:"posts"`
}
type TabAppService struct {
client *Client
}
type TabAppGetRunsOptions struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
}
func (s *TabAppService) GetRuns(ctx context.Context, token string, options TabAppGetRunsOptions) (*TabAppResults, error) {
url := fmt.Sprintf("plugins/%s/tabapp/runs", manifestID)
url, err := addOptions(url, options)
if err != nil {
return nil, err
}
req, err := s.client.newRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", token)
tabAppResults := new(TabAppResults)
resp, err := s.client.do(ctx, req, tabAppResults)
if err != nil {
return nil, err
}
resp.Body.Close()
return tabAppResults, nil
}

View file

@ -0,0 +1,7 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
var BuildAPIURL = buildAPIURL
var NewClient = newClient

View file

@ -0,0 +1,142 @@
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package client
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlaybookRunUpdateAtSerialization(t *testing.T) {
// Create a PlaybookRun with UpdateAt field set
now := time.Now().UnixMilli()
run := PlaybookRun{
ID: "test-id",
Name: "Test Run",
CreateAt: now - 1000, // 1 second earlier
UpdateAt: now,
}
// Serialize to JSON
jsonData, err := json.Marshal(run)
require.NoError(t, err, "Failed to marshal PlaybookRun to JSON")
// Validate that UpdateAt field is included in the JSON
jsonStr := string(jsonData)
assert.Contains(t, jsonStr, `"update_at":`, "JSON should contain update_at field")
assert.Contains(t, jsonStr, `"create_at":`, "JSON should contain create_at field")
// Deserialize from JSON
var decodedRun PlaybookRun
err = json.Unmarshal(jsonData, &decodedRun)
require.NoError(t, err, "Failed to unmarshal PlaybookRun from JSON")
// Validate the UpdateAt field was preserved
assert.Equal(t, run.UpdateAt, decodedRun.UpdateAt, "UpdateAt field should be preserved after serialization/deserialization")
assert.Equal(t, now, decodedRun.UpdateAt, "UpdateAt value should be preserved")
}
func TestChecklistUpdateAtSerialization(t *testing.T) {
// Create a Checklist with UpdateAt field set
now := time.Now().UnixMilli()
checklist := Checklist{
ID: "test-checklist-id",
Title: "Test Checklist",
UpdateAt: now,
Items: []ChecklistItem{},
}
// Serialize to JSON
jsonData, err := json.Marshal(checklist)
require.NoError(t, err, "Failed to marshal Checklist to JSON")
// Validate that UpdateAt field is included in the JSON
jsonStr := string(jsonData)
assert.Contains(t, jsonStr, `"update_at":`, "JSON should contain update_at field")
// Deserialize from JSON
var decodedChecklist Checklist
err = json.Unmarshal(jsonData, &decodedChecklist)
require.NoError(t, err, "Failed to unmarshal Checklist from JSON")
// Validate the UpdateAt field was preserved
assert.Equal(t, checklist.UpdateAt, decodedChecklist.UpdateAt, "UpdateAt field should be preserved after serialization/deserialization")
assert.Equal(t, now, decodedChecklist.UpdateAt, "UpdateAt value should be preserved")
}
func TestChecklistItemUpdateAtSerialization(t *testing.T) {
// Create a ChecklistItem with UpdateAt field set
now := time.Now().UnixMilli()
item := ChecklistItem{
ID: "test-item-id",
Title: "Test Item",
UpdateAt: now,
}
// Serialize to JSON
jsonData, err := json.Marshal(item)
require.NoError(t, err, "Failed to marshal ChecklistItem to JSON")
// Validate that UpdateAt field is included in the JSON
jsonStr := string(jsonData)
assert.Contains(t, jsonStr, `"update_at":`, "JSON should contain update_at field")
// Deserialize from JSON
var decodedItem ChecklistItem
err = json.Unmarshal(jsonData, &decodedItem)
require.NoError(t, err, "Failed to unmarshal ChecklistItem from JSON")
// Validate the UpdateAt field was preserved
assert.Equal(t, item.UpdateAt, decodedItem.UpdateAt, "UpdateAt field should be preserved after serialization/deserialization")
assert.Equal(t, now, decodedItem.UpdateAt, "UpdateAt value should be preserved")
}
func TestNestedUpdateAtSerialization(t *testing.T) {
// Create a nested structure to test the complete serialization path
now := time.Now().UnixMilli()
// Create a checklist item with UpdateAt
item := ChecklistItem{
ID: "test-item-id",
Title: "Test Item",
UpdateAt: now,
}
// Create a checklist with the item
checklist := Checklist{
ID: "test-checklist-id",
Title: "Test Checklist",
UpdateAt: now + 1000, // 1 second later
Items: []ChecklistItem{item},
}
// Create a PlaybookRun with the checklist
run := PlaybookRun{
ID: "test-run-id",
Name: "Test Run",
CreateAt: now - 1000, // 1 second earlier
UpdateAt: now + 2000, // 2 seconds later
Checklists: []Checklist{checklist},
}
// Serialize to JSON
jsonData, err := json.Marshal(run)
require.NoError(t, err, "Failed to marshal nested structure to JSON")
// Deserialize from JSON
var decodedRun PlaybookRun
err = json.Unmarshal(jsonData, &decodedRun)
require.NoError(t, err, "Failed to unmarshal nested structure from JSON")
// Validate the UpdateAt fields were preserved at all levels
assert.Equal(t, run.UpdateAt, decodedRun.UpdateAt, "Run UpdateAt should be preserved")
require.Len(t, decodedRun.Checklists, 1, "Should have one checklist")
assert.Equal(t, checklist.UpdateAt, decodedRun.Checklists[0].UpdateAt, "Checklist UpdateAt should be preserved")
require.Len(t, decodedRun.Checklists[0].Items, 1, "Should have one checklist item")
assert.Equal(t, item.UpdateAt, decodedRun.Checklists[0].Items[0].UpdateAt, "ChecklistItem UpdateAt should be preserved")
}

View file

@ -0,0 +1,49 @@
# Use /e2e-test command to execute cypress integration tests
Date: **2023-01-17**
Status: **Under Discussion**
Contents:
- [Context](#context)
- [Decision](#decision)
- [Implementation](#implementation)
- [Notes](#notes)
## Context
Following our initiative to deprecate CirleCI in favor of Github Actions we are also trying to reduce costs in the process.
Analyzing our pipelines we figured out that integration tests take too much time and are resource hungry. We want to introduce a new process to mitigate the cost and provide a more user friendly developer experience.
## Decision
We want to introduce a custom PR command `/e2e-test` in order to manually trigger integration tests. The idea is similar to mattermod command introduced [here](https://github.com/mattermost/mattermost-mattermod/pull/327). The proposal includes limiting the e2e testing only on approved PRs to avoid community increased load .
![e2e](assets/e2e-test-ci-workflow.jpeg)
## Implementation
In order to avoid unexessary coding we can use Github Actions built in functionality to take action based on PR comments . No need to increase complexity by adding functionality on our bots . A simple example will be as below :
```yaml
name: Trigger on PR Comment
on:
issue_comment:
types:
- created
jobs:
e2e-test:
# Check if the comments originate from pull request (because Issues and PRs have the same API)
# Check that comment starts with /e2e-test
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/e2e-test') }}
runs-on: ubuntu-latest
steps:
- name: ci/integration-tests
run: echo "Let's execute some integration tests"
```
## Notes
- Further parameterization can be introduced with flags on the comment body
- Developers can change the functionality easily on the same repo without extra code

View file

@ -0,0 +1,3 @@
# Architecture Decision Records
- [Use e2e-test command to trigger integration tests](0001-use-command-for-e2e-tests.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,123 @@
{
"extends": [
"plugin:mattermost/react",
"plugin:cypress/recommended"
],
"plugins": [
"@babel/eslint-plugin",
"mattermost",
"import",
"no-only-tests",
"@typescript-eslint",
"cypress"
],
"parser": "@typescript-eslint/parser",
"env": {
"cypress/globals": true
},
"rules": {
"header/header": [
2,
"line",
" Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\n See LICENSE.txt for license information.",
2
],
"cypress/assertion-before-screenshot": "warn",
"cypress/no-assigning-return-values": "error",
"cypress/no-force": "warn",
"cypress/no-async-tests": "error",
"cypress/no-pause": "error",
"cypress/no-unnecessary-waiting": 0,
"cypress/unsafe-to-chain-command": 0,
"func-names": 0,
"import/no-unresolved": 0,
"max-nested-callbacks": 0,
"no-unused-expressions": 0,
"no-process-env": 0,
"no-duplicate-imports": 0,
"no-undefined": 0,
"no-use-before-define": 0,
"import/no-duplicates": 2,
"mattermost/use-external-link": 2,
"eol-last": ["error", "always"],
"import/order": [
0,
{
"newlines-between": "always-and-inside-groups",
"groups": [
"builtin",
"external",
[
"internal",
"parent"
],
"sibling",
"index"
]
}
],
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}],
"max-lines": ["warn", {"max": 800, "skipBlankLines": true, "skipComments": true}]
},
"overrides": [
{
"files": ["**/*.ts"],
"extends": [
"plugin:@typescript-eslint/recommended"
],
"rules": {
"camelcase": 0,
"no-shadow": 0,
"import/no-unresolved": 0, // ts handles this better
"@typescript-eslint/naming-convention": [
2,
{
"selector": "function",
"format": ["camelCase", "PascalCase"]
},
{
"selector": "variable",
"format": ["camelCase", "PascalCase", "UPPER_CASE"]
},
{
"selector": "parameter",
"format": ["camelCase", "PascalCase"],
"leadingUnderscore": "allow"
},
{
"selector": "typeLike",
"format": ["PascalCase"]
}
],
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-unused-vars": [
2,
{
"vars": "all",
"args": "after-used"
}
],
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/prefer-interface": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/indent": [
2,
4,
{
"SwitchCase": 0
}
],
"@typescript-eslint/no-use-before-define": [
2,
{
"classes": false,
"functions": false,
"variables": false
}
]
}
}
]
}

View file

@ -0,0 +1,16 @@
# env, cert, key, license
.env*
*.crt
*.key
*.license
# Plugin
*.tar.gz
# node
*.lock
node_modules
results
tests/screenshots
tests/videos

View file

@ -0,0 +1 @@
legacy-peer-deps=true

View file

@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {defineConfig} from 'cypress';
export default defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 20000,
downloadsFolder: 'tests/downloads',
fixturesFolder: 'tests/fixtures',
numTestsKeptInMemory: 0,
screenshotsFolder: 'tests/screenshots',
taskTimeout: 20000,
video: false,
viewportWidth: 1300,
reporter: 'cypress-multi-reporters',
reporterOptions: {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
mochaFile: 'results/junit/test_results[hash].xml',
},
},
env: {
adminEmail: 'sysadmin@sample.mattermost.com',
adminUsername: 'sysadmin',
adminPassword: 'Sys@dmin-sample1',
allowedUntrustedInternalConnections: 'localhost',
cwsURL: 'http://localhost:8076',
cwsAPIURL: 'http://localhost:8076',
dbClient: 'postgres',
dbConnection: 'postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable&connect_timeout=10',
elasticsearchConnectionURL: 'http://localhost:9200',
firstTest: false,
keycloakAppName: 'mattermost',
keycloakBaseUrl: 'http://localhost:8484',
keycloakUsername: 'mmuser',
keycloakPassword: 'mostest',
ldapServer: 'localhost',
ldapPort: 389,
minioAccessKey: 'minioaccesskey',
minioSecretKey: 'miniosecretkey',
minioS3Bucket: 'mattermost-test',
minioS3Endpoint: 'localhost:9000',
minioS3SSL: false,
numberOfTrialUsers: 100,
resetBeforeTest: false,
runLDAPSync: true,
secondServerURL: 'http://localhost/s/p',
serverEdition: 'Team',
serverClusterEnabled: false,
serverClusterName: 'mm_dev_cluster',
serverClusterHostCount: 3,
smtpUrl: 'http://localhost:9001',
webhookBaseUrl: 'http://localhost:3000',
},
e2e: {
setupNodeEvents(on, config) {
return require('./tests/plugins/index.js')(on, config); // eslint-disable-line global-require
},
baseUrl: process.env.MM_SERVICESETTINGS_SITEURL || 'http://localhost:8065',
excludeSpecPattern: '**/node_modules/**/*',
specPattern: 'tests/integration/**/*_spec.{js,ts}',
supportFile: 'tests/support/index.js',
testIsolation: false,
},
retries: {
openMode: 0,
runMode: 2,
},
});

View file

@ -0,0 +1,7 @@
--
-- Just add the system admin.
--
COPY public.users (id, createat, updateat, deleteat, username, password, authdata, authservice, email, emailverified, nickname, firstname, lastname, "position", roles, allowmarketing, props, notifyprops, lastpasswordupdate, lastpictureupdate, failedattempts, locale, timezone, mfaactive, mfasecret, remoteid) FROM stdin;
qanmxu8aafgdipxiibkuos1uaw 1634316565952 1634316566065 0 sysadmin $2a$10$FF7zZzLYW80liKKJaKGVFOc6xCsKU2OZfwCvGBmF4xACuwstyPFN. \N sysadmin@sample.mattermost.com t Kenneth Moreno Software Test Engineer III system_admin system_user f {} {"push": "mention", "email": "true", "channel": "true", "desktop": "mention", "comments": "never", "first_name": "false", "push_status": "away", "mention_keys": "", "push_threads": "all", "desktop_sound": "true", "email_threads": "all", "desktop_threads": "all"} 1634316565952 0 0 en {"manualTimezone": "", "automaticTimezone": "", "useAutomaticTimezone": "true"} f \N
\.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,98 @@
{
"engines": {
"node": "20.x || 22.x || >=24.0.0",
"npm": ">=10.1.0"
},
"devDependencies": {
"@babel/eslint-parser": "7.21.8",
"@babel/eslint-plugin": "7.19.1",
"@cypress/request": "2.88.11",
"@mattermost/client": "10.6.0",
"@mattermost/types": "10.6.0",
"@testing-library/cypress": "10.1.0",
"@types/async": "3.2.20",
"@types/authenticator": "1.1.1",
"@types/express": "4.17.17",
"@types/fs-extra": "11.0.1",
"@types/lodash": "4.14.194",
"@types/lodash.intersection": "4.4.7",
"@types/lodash.mapkeys": "4.6.7",
"@types/lodash.without": "4.4.7",
"@types/mime-types": "2.1.1",
"@types/mochawesome": "6.2.1",
"@types/pdf-parse": "1.1.1",
"@types/recursive-readdir": "2.2.1",
"@types/shelljs": "0.8.12",
"@types/uuid": "9.0.1",
"@typescript-eslint/eslint-plugin": "5.59.2",
"@typescript-eslint/parser": "5.59.2",
"async": "3.2.4",
"authenticator": "1.1.5",
"aws-sdk": "2.1371.0",
"axios": "1.4.0",
"axios-retry": "3.4.0",
"chai": "4.3.7",
"chalk": "4.1.2",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"cross-env": "7.0.3",
"cypress": "15.12.0",
"cypress-file-upload": "5.0.8",
"cypress-multi-reporters": "1.6.3",
"cypress-plugin-tab": "1.0.5",
"cypress-real-events": "^1.15.0",
"cypress-wait-until": "1.7.2",
"dayjs": "1.11.7",
"deepmerge": "4.3.1",
"dotenv": "16.0.3",
"eslint": "8.39.0",
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-cypress": "2.13.3",
"eslint-plugin-header": "3.1.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#5b0c972eacf19286e4c66221b39113bf8728a99e",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-react": "7.32.2",
"express": "4.18.2",
"extract-zip": "2.0.1",
"knex": "2.4.2",
"localforage": "1.10.0",
"lodash.intersection": "4.4.0",
"lodash.mapkeys": "4.6.0",
"lodash.without": "4.4.0",
"lodash.xor": "4.5.0",
"mime": "3.0.0",
"mime-types": "2.1.35",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"mocha-multi-reporters": "1.5.1",
"mochawesome": "7.1.3",
"mochawesome-merge": "4.3.0",
"mochawesome-report-generator": "6.2.0",
"moment-timezone": "0.5.43",
"path": "0.12.7",
"pdf-parse": "1.1.1",
"pg": "8.10.0",
"recursive-readdir": "2.2.3",
"shelljs": "0.8.5",
"timezones.json": "1.7.0",
"typescript": "5.0.4",
"uuid": "9.0.0",
"yargs": "17.7.2"
},
"scripts": {
"check-types": "tsc -b",
"cypress:open": "cross-env TZ=Etc/UTC cypress open",
"cypress:run": "cross-env TZ=Etc/UTC cypress run",
"cypress:run:chrome": "cross-env TZ=Etc/UTC cypress run --browser chrome",
"cypress:run:firefox": "cross-env TZ=Etc/UTC cypress run --browser firefox",
"cypress:run:edge": "cross-env TZ=Etc/UTC cypress run --browser edge",
"cypress:run:electron": "cross-env TZ=Etc/UTC cypress run --browser electron",
"benchmarks:run-server": "cd mattermost && bin/mattermost",
"start:webhook": "node webhook_serve.js",
"test": "cross-env TZ=Etc/UTC cypress run",
"test:ci": "node run_tests.js",
"uniq-meta": "grep -r \"^// $META:\" cypress | grep -ow '@\\w*' | sort | uniq",
"check": "eslint --ext .js,.ts . --quiet --cache",
"fix": "eslint --ext .js,.ts . --quiet --fix --cache"
}
}

Some files were not shown because too many files have changed in this diff Show more