Refactor codebase with improved structure and logging
- Restructured info.go with extracted helper functions for better readability - Enhanced updateassets.go with cleaner asset processing logic and better error handling - Improved client.go formatting and logging consistency - Added logs.go for centralized logging functionality - Updated dependencies in go.mod to include tint as direct dependency - Cleaned up README.md with simplified installation instructions and structure - Added comprehensive assets/ directory with build configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c01c9c2843
commit
71a7b0de11
18 changed files with 852 additions and 105 deletions
30
README.md
30
README.md
|
@ -20,14 +20,7 @@ Download the latest binary from the [releases page](https://github.com/mattermos
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using go install
|
# Using go install
|
||||||
go install github.com/mattermost/pluginctl/cmd/pluginctl@latest
|
go install github.com/mattermost/pluginctl/cmd/pluginctl
|
||||||
|
|
||||||
# Using Homebrew (if available)
|
|
||||||
brew install mattermost/tap/pluginctl
|
|
||||||
|
|
||||||
# Using Scoop on Windows (if available)
|
|
||||||
scoop bucket add mattermost https://github.com/mattermost/scoop-bucket.git
|
|
||||||
scoop install pluginctl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
@ -42,7 +35,7 @@ pluginctl info
|
||||||
pluginctl --help
|
pluginctl --help
|
||||||
|
|
||||||
# Show version
|
# Show version
|
||||||
pluginctl --version
|
pluginctl version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plugin Path Configuration
|
### Plugin Path Configuration
|
||||||
|
@ -50,11 +43,13 @@ pluginctl --version
|
||||||
`pluginctl` supports multiple ways to specify the plugin directory:
|
`pluginctl` supports multiple ways to specify the plugin directory:
|
||||||
|
|
||||||
1. **Command-line flag** (highest priority):
|
1. **Command-line flag** (highest priority):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pluginctl --plugin-path /path/to/plugin info
|
pluginctl --plugin-path /path/to/plugin info
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Environment variable**:
|
2. **Environment variable**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
|
||||||
pluginctl info
|
pluginctl info
|
||||||
|
@ -70,11 +65,6 @@ pluginctl --version
|
||||||
|
|
||||||
Run `pluginctl --help` to see all available commands and options.
|
Run `pluginctl --help` to see all available commands and options.
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Go 1.24.3 or later (for building from source)
|
|
||||||
- Valid Mattermost plugin directory with `plugin.json` manifest file
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
@ -93,13 +83,6 @@ make dev
|
||||||
make check-changes
|
make check-changes
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Make Targets
|
|
||||||
|
|
||||||
- `make dev` - Quick development build
|
|
||||||
- `make check-changes` - Validate changes (lint, security, test)
|
|
||||||
- `make test` - Run tests
|
|
||||||
- `make help` - Show all available targets
|
|
||||||
|
|
||||||
See `make help` for the complete list of available targets.
|
See `make help` for the complete list of available targets.
|
||||||
|
|
||||||
### Adding New Commands
|
### Adding New Commands
|
||||||
|
@ -136,6 +119,5 @@ This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENS
|
||||||
|
|
||||||
For questions, issues, or feature requests, please:
|
For questions, issues, or feature requests, please:
|
||||||
|
|
||||||
1. Check the [issues](https://github.com/mattermost/pluginctl/issues) page
|
- Check the [issues](https://github.com/mattermost/pluginctl/issues) or create a new issue if your problem isn't already reported
|
||||||
2. Create a new issue if your problem isn't already reported
|
- Join the [Mattermost Community](https://community.mattermost.com/) for general discussion
|
||||||
3. Join the [Mattermost Community](https://community.mattermost.com/) for general discussion
|
|
||||||
|
|
2
assets/.gitattributes
vendored
Normal file
2
assets/.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
server/manifest.go linguist-generated=true
|
||||||
|
webapp/src/manifest.js linguist-generated=true
|
1
assets/.gitpod.yml
Normal file
1
assets/.gitpod.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mainConfiguration: https://github.com/mattermost/mattermost-gitpod-config
|
1
assets/.nvmrc
Normal file
1
assets/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
20.11
|
43
assets/Makefile
Normal file
43
assets/Makefile
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
GO ?= $(shell command -v go 2> /dev/null)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Include modular makefiles
|
||||||
|
include build/*.mk
|
83
assets/build/build.mk
Normal file
83
assets/build/build.mk
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Build Targets
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
|
||||||
|
## 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 CGO_ENABLED=0 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-$(DEFAULT_GOOS)-$(DEFAULT_GOARCH);
|
||||||
|
else
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-amd64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-linux-arm64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-darwin-amd64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-darwin-arm64;
|
||||||
|
cd server && env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(GO_BUILD_FLAGS) $(GO_BUILD_GCFLAGS) -trimpath -o dist/plugin-windows-amd64.exe;
|
||||||
|
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
|
||||||
|
touch $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Builds the webapp, if it exists.
|
||||||
|
.PHONY: webapp
|
||||||
|
webapp: webapp/node_modules
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
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 $(ASSETS_DIR)/.),)
|
||||||
|
cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_PUBLIC),)
|
||||||
|
cp -r public dist/$(PLUGIN_ID)/
|
||||||
|
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: server webapp bundle
|
47
assets/build/deploy.mk
Normal file
47
assets/build/deploy.mk
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Deployment and Plugin Management
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Builds and installs the plugin to a server.
|
||||||
|
.PHONY: deploy
|
||||||
|
deploy: dist
|
||||||
|
./build/bin/pluginctl deploy --bundle-path dist/$(BUNDLE_NAME)
|
||||||
|
|
||||||
|
## Builds and installs the plugin to a server, updating the webapp automatically when changed.
|
||||||
|
.PHONY: watch
|
||||||
|
watch: server bundle
|
||||||
|
ifeq ($(MM_DEBUG),)
|
||||||
|
cd webapp && $(NPM) run build:watch
|
||||||
|
else
|
||||||
|
cd webapp && $(NPM) run debug:watch
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Installs a previous built plugin with updated webpack assets to a server.
|
||||||
|
.PHONY: deploy-from-watch
|
||||||
|
deploy-from-watch: bundle
|
||||||
|
pluginctl deploy --bundle-path dist/$(BUNDLE_NAME)
|
||||||
|
|
||||||
|
## Disable the plugin.
|
||||||
|
.PHONY: disable
|
||||||
|
disable: detach
|
||||||
|
pluginctl disable
|
||||||
|
|
||||||
|
## Enable the plugin.
|
||||||
|
.PHONY: enable
|
||||||
|
enable:
|
||||||
|
pluginctl enable
|
||||||
|
|
||||||
|
## Reset the plugin, effectively disabling and re-enabling it on the server.
|
||||||
|
.PHONY: reset
|
||||||
|
reset: detach
|
||||||
|
pluginctl reset
|
||||||
|
|
||||||
|
## View plugin logs.
|
||||||
|
.PHONY: logs
|
||||||
|
logs:
|
||||||
|
pluginctl logs
|
||||||
|
|
||||||
|
## Watch plugin logs.
|
||||||
|
.PHONY: logs-watch
|
||||||
|
logs-watch:
|
||||||
|
pluginctl logs --watch
|
53
assets/build/dev.mk
Normal file
53
assets/build/dev.mk
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Development and Debugging
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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; \
|
44
assets/build/setup.mk
Normal file
44
assets/build/setup.mk
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Extract the plugin id from the manifest.
|
||||||
|
PLUGIN_ID ?= $(shell pluginctl manifest id)
|
||||||
|
ifeq ($(PLUGIN_ID),)
|
||||||
|
$(error "Cannot parse id from $(MANIFEST_FILE)")
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Extract the plugin version from the manifest.
|
||||||
|
PLUGIN_VERSION ?= $(shell pluginctl 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 pluginctl manifest has_server)
|
||||||
|
|
||||||
|
# Determine if a webapp is defined in the manifest.
|
||||||
|
HAS_WEBAPP ?= $(shell pluginctl 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
|
57
assets/build/test.mk
Normal file
57
assets/build/test.mk
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Testing and Quality Assurance
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## Install go tools
|
||||||
|
install-go-tools:
|
||||||
|
@echo Installing go tools
|
||||||
|
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0
|
||||||
|
$(GO) install gotest.tools/gotestsum@v1.7.0
|
||||||
|
|
||||||
|
## Runs eslint and golangci-lint
|
||||||
|
.PHONY: check-style
|
||||||
|
check-style: manifest-check webapp/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
|
||||||
|
|
||||||
|
# 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 ./...
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Runs any lints and unit tests defined for the server and webapp, if they exist.
|
||||||
|
.PHONY: test
|
||||||
|
test: webapp/node_modules install-go-tools
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
$(GOBIN)/gotestsum -- -v ./...
|
||||||
|
endif
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
cd webapp && $(NPM) run test;
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Runs any lints and unit tests defined for the server and webapp, if they exist, optimized
|
||||||
|
## for a CI environment.
|
||||||
|
.PHONY: test-ci
|
||||||
|
test-ci: 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
|
||||||
|
|
||||||
|
## Creates a coverage report for the server code.
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage: webapp/node_modules
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
$(GO) test $(GO_TEST_FLAGS) -coverprofile=server/coverage.txt ./server/...
|
||||||
|
$(GO) tool cover -html=server/coverage.txt
|
||||||
|
endif
|
42
assets/build/utils.mk
Normal file
42
assets/build/utils.mk
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Utilities
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
## 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/
|
||||||
|
|
||||||
|
## Extract strings for translation from the source code.
|
||||||
|
.PHONY: i18n-extract
|
||||||
|
i18n-extract:
|
||||||
|
ifneq ($(HAS_WEBAPP),)
|
||||||
|
ifeq ($(HAS_MM_UTILITIES),)
|
||||||
|
@echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||||
|
else
|
||||||
|
cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-webapp --webapp-dir $(PWD)/webapp
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Generate mocks for testing.
|
||||||
|
.PHONY: mock
|
||||||
|
mock:
|
||||||
|
ifneq ($(HAS_SERVER),)
|
||||||
|
go install github.com/golang/mock/mockgen@v1.6.0
|
||||||
|
mockgen -destination=server/command/mocks/mock_commands.go -package=mocks github.com/mattermost/mattermost-plugin-starter-template/server/command Command
|
||||||
|
endif
|
||||||
|
|
||||||
|
## Show help documentation.
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@cat Makefile build/*.mk | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//g" | sed -e "s/^## //g" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort
|
111
assets/build/versioning.mk
Normal file
111
assets/build/versioning.mk
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
# ====================================================================================
|
||||||
|
# Semantic Versioning
|
||||||
|
# ====================================================================================
|
||||||
|
|
||||||
|
# Used for semver bumping
|
||||||
|
PROTECTED_BRANCH := master
|
||||||
|
APP_NAME := $(shell basename -s .git `git config --get remote.origin.url`)
|
||||||
|
CURRENT_VERSION := $(shell git describe --abbrev=0 --tags)
|
||||||
|
VERSION_PARTS := $(subst ., ,$(subst v,,$(subst -rc, ,$(CURRENT_VERSION))))
|
||||||
|
MAJOR := $(word 1,$(VERSION_PARTS))
|
||||||
|
MINOR := $(word 2,$(VERSION_PARTS))
|
||||||
|
PATCH := $(word 3,$(VERSION_PARTS))
|
||||||
|
RC := $(shell echo $(CURRENT_VERSION) | grep -oE 'rc[0-9]+' | sed 's/rc//')
|
||||||
|
|
||||||
|
# Check if current branch is protected
|
||||||
|
define check_protected_branch
|
||||||
|
@current_branch=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
if ! echo "$(PROTECTED_BRANCH)" | grep -wq "$$current_branch" && ! echo "$$current_branch" | grep -q "^release"; then \
|
||||||
|
echo "Error: Tagging is only allowed from $(PROTECTED_BRANCH) or release branches. You are on $$current_branch branch."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
# Check if there are pending pulls
|
||||||
|
define check_pending_pulls
|
||||||
|
@git fetch; \
|
||||||
|
current_branch=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
if [ "$$(git rev-parse HEAD)" != "$$(git rev-parse origin/$$current_branch)" ]; then \
|
||||||
|
echo "Error: Your branch is not up to date with upstream. Please pull the latest changes before performing a release"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
# Prompt for approval
|
||||||
|
define prompt_approval
|
||||||
|
@read -p "About to bump $(APP_NAME) to version $(1), approve? (y/n) " userinput; \
|
||||||
|
if [ "$$userinput" != "y" ]; then \
|
||||||
|
echo "Bump aborted."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
endef
|
||||||
|
|
||||||
|
.PHONY: patch minor major patch-rc minor-rc major-rc
|
||||||
|
|
||||||
|
patch: ## to bump patch version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval PATCH := $(shell echo $$(($(PATCH)+1))))
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Patch version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
minor: ## to bump minor version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MINOR := $(shell echo $$(($(MINOR)+1))))
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Minor version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
major: ## to bump major version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
$(eval MAJOR := $(shell echo $$(($(MAJOR)+1))))
|
||||||
|
$(eval MINOR := 0)
|
||||||
|
$(eval PATCH := 0)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH))
|
||||||
|
@echo Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH) -m "Bumping $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
@echo Bumped $(APP_NAME) to Major version $(MAJOR).$(MINOR).$(PATCH)
|
||||||
|
|
||||||
|
patch-rc: ## to bump patch release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval RC := $(shell echo $$(($(RC)+1))))
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH)-rc$(RC))
|
||||||
|
@echo Bumping $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Patch RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
|
||||||
|
minor-rc: ## to bump minor release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MINOR := $(shell echo $$(($(MINOR)+1))))
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
@$(eval RC := 1)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH)-rc$(RC))
|
||||||
|
@echo Bumping $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Minor RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
|
||||||
|
major-rc: ## to bump major release candidate version (semver)
|
||||||
|
$(call check_protected_branch)
|
||||||
|
$(call check_pending_pulls)
|
||||||
|
@$(eval MAJOR := $(shell echo $$(($(MAJOR)+1))))
|
||||||
|
@$(eval MINOR := 0)
|
||||||
|
@$(eval PATCH := 0)
|
||||||
|
@$(eval RC := 1)
|
||||||
|
$(call prompt_approval,$(MAJOR).$(MINOR).$(PATCH)-rc$(RC))
|
||||||
|
@echo Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
git tag -s -a v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC) -m "Bumping $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)"
|
||||||
|
git push origin v$(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
||||||
|
@echo Bumped $(APP_NAME) to Major RC version $(MAJOR).$(MINOR).$(PATCH)-rc$(RC)
|
|
@ -27,7 +27,8 @@ func getClient(ctx context.Context) (*model.Client4, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
if os.Getenv("MM_LOCALSOCKETPATH") != "" {
|
||||||
Logger.Info("No socket found for local mode deployment. Attempting to authenticate with credentials.", "socket_path", socketPath)
|
Logger.Info("No socket found for local mode deployment. Attempting to authenticate with credentials.",
|
||||||
|
"socket_path", socketPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL")
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -2,7 +2,10 @@ module github.com/mattermost/pluginctl
|
||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require github.com/mattermost/mattermost/server/public v0.1.15
|
require (
|
||||||
|
github.com/lmittmann/tint v1.1.2
|
||||||
|
github.com/mattermost/mattermost/server/public v0.1.15
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||||
|
@ -303,7 +306,6 @@ require (
|
||||||
github.com/ldez/tagliatelle v0.5.0 // indirect
|
github.com/ldez/tagliatelle v0.5.0 // indirect
|
||||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||||
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 // indirect
|
github.com/letsencrypt/boulder v0.0.0-20250411005613-d800055fe666 // indirect
|
||||||
github.com/lmittmann/tint v1.1.2 // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/macabu/inamedparam v0.1.3 // indirect
|
github.com/macabu/inamedparam v0.1.3 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
|
46
info.go
46
info.go
|
@ -19,30 +19,44 @@ func InfoCommand() error {
|
||||||
|
|
||||||
// PrintPluginInfo displays formatted plugin information.
|
// PrintPluginInfo displays formatted plugin information.
|
||||||
func PrintPluginInfo(manifest *model.Manifest) error {
|
func PrintPluginInfo(manifest *model.Manifest) error {
|
||||||
|
printBasicInfo(manifest)
|
||||||
|
printCodeComponents(manifest)
|
||||||
|
printSettingsSchema(manifest)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// printBasicInfo prints basic plugin information.
|
||||||
|
func printBasicInfo(manifest *model.Manifest) {
|
||||||
Logger.Info("Plugin Information:")
|
Logger.Info("Plugin Information:")
|
||||||
Logger.Info("==================")
|
Logger.Info("==================")
|
||||||
|
|
||||||
// Basic plugin info
|
|
||||||
Logger.Info("ID:", "value", manifest.Id)
|
Logger.Info("ID:", "value", manifest.Id)
|
||||||
Logger.Info("Name:", "value", manifest.Name)
|
Logger.Info("Name:", "value", manifest.Name)
|
||||||
Logger.Info("Version:", "value", manifest.Version)
|
Logger.Info("Version:", "value", manifest.Version)
|
||||||
|
|
||||||
// Minimum Mattermost version
|
minVersion := manifest.MinServerVersion
|
||||||
if manifest.MinServerVersion != "" {
|
if minVersion == "" {
|
||||||
Logger.Info("Min MM Version:", "value", manifest.MinServerVersion)
|
minVersion = "Not specified"
|
||||||
} else {
|
|
||||||
Logger.Info("Min MM Version:", "value", "Not specified")
|
|
||||||
}
|
}
|
||||||
|
Logger.Info("Min MM Version:", "value", minVersion)
|
||||||
|
|
||||||
// Description if available
|
|
||||||
if manifest.Description != "" {
|
if manifest.Description != "" {
|
||||||
Logger.Info("Description:", "value", manifest.Description)
|
Logger.Info("Description:", "value", manifest.Description)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printCodeComponents prints information about server and webapp code.
|
||||||
|
func printCodeComponents(manifest *model.Manifest) {
|
||||||
Logger.Info("Code Components:")
|
Logger.Info("Code Components:")
|
||||||
Logger.Info("================")
|
Logger.Info("================")
|
||||||
|
|
||||||
// Server code presence
|
printServerCodeInfo(manifest)
|
||||||
|
printWebappCodeInfo(manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printServerCodeInfo prints server code information.
|
||||||
|
func printServerCodeInfo(manifest *model.Manifest) {
|
||||||
if HasServerCode(manifest) {
|
if HasServerCode(manifest) {
|
||||||
Logger.Info("Server Code:", "value", "Yes")
|
Logger.Info("Server Code:", "value", "Yes")
|
||||||
if manifest.Server != nil && len(manifest.Server.Executables) > 0 {
|
if manifest.Server != nil && len(manifest.Server.Executables) > 0 {
|
||||||
|
@ -55,8 +69,10 @@ func PrintPluginInfo(manifest *model.Manifest) error {
|
||||||
} else {
|
} else {
|
||||||
Logger.Info("Server Code:", "value", "No")
|
Logger.Info("Server Code:", "value", "No")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Webapp code presence
|
// printWebappCodeInfo prints webapp code information.
|
||||||
|
func printWebappCodeInfo(manifest *model.Manifest) {
|
||||||
if HasWebappCode(manifest) {
|
if HasWebappCode(manifest) {
|
||||||
Logger.Info("Webapp Code:", "value", "Yes")
|
Logger.Info("Webapp Code:", "value", "Yes")
|
||||||
if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" {
|
if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" {
|
||||||
|
@ -65,15 +81,15 @@ func PrintPluginInfo(manifest *model.Manifest) error {
|
||||||
} else {
|
} else {
|
||||||
Logger.Info("Webapp Code:", "value", "No")
|
Logger.Info("Webapp Code:", "value", "No")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Settings schema
|
// printSettingsSchema prints settings schema information.
|
||||||
|
func printSettingsSchema(manifest *model.Manifest) {
|
||||||
|
value := "No"
|
||||||
if manifest.SettingsSchema != nil {
|
if manifest.SettingsSchema != nil {
|
||||||
Logger.Info("Settings Schema:", "value", "Yes")
|
value = "Yes"
|
||||||
} else {
|
|
||||||
Logger.Info("Settings Schema:", "value", "No")
|
|
||||||
}
|
}
|
||||||
|
Logger.Info("Settings Schema:", "value", value)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InfoCommandWithPath implements the 'info' command with a custom path.
|
// InfoCommandWithPath implements the 'info' command with a custom path.
|
||||||
|
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"github.com/lmittmann/tint"
|
"github.com/lmittmann/tint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger is the global logger instance
|
// Logger is the global logger instance.
|
||||||
var Logger *slog.Logger
|
var Logger *slog.Logger
|
||||||
|
|
||||||
// InitLogger initializes the global logger
|
// InitLogger initializes the global logger.
|
||||||
func InitLogger() {
|
func InitLogger() {
|
||||||
// Create a tint handler for colorized output
|
// Create a tint handler for colorized output
|
||||||
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
|
@ -24,7 +24,7 @@ func InitLogger() {
|
||||||
Logger = slog.New(handler)
|
Logger = slog.New(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogLevel sets the minimum logging level
|
// SetLogLevel sets the minimum logging level.
|
||||||
func SetLogLevel(level slog.Level) {
|
func SetLogLevel(level slog.Level) {
|
||||||
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
handler := tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
Level: level,
|
Level: level,
|
||||||
|
|
215
logs.go
Normal file
215
logs.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
package pluginctl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logsPerPage = 100
|
||||||
|
defaultLogsPerPage = 500
|
||||||
|
timeStampFormat = "2006-01-02 15:04:05.000 Z07:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunLogsCommand executes the logs command with optional --watch flag.
|
||||||
|
func RunLogsCommand(args []string, pluginPath string) error {
|
||||||
|
// Check for --watch flag
|
||||||
|
watch := false
|
||||||
|
if len(args) > 0 && args[0] == "--watch" {
|
||||||
|
watch = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if watch {
|
||||||
|
return runPluginCommand(args, pluginPath, watchPluginLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runPluginCommand(args, pluginPath, getPluginLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPluginLogs fetches the latest 500 log entries from Mattermost,
|
||||||
|
// and prints only the ones related to the plugin to stdout.
|
||||||
|
func getPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
|
Logger.Info("Getting plugin logs", "plugin_id", pluginID)
|
||||||
|
|
||||||
|
err := checkJSONLogsSetting(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := fetchLogs(ctx, client, LogsRequest{
|
||||||
|
Page: 0,
|
||||||
|
PerPage: defaultLogsPerPage,
|
||||||
|
PluginID: pluginID,
|
||||||
|
Since: time.Unix(0, 0),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch log entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
printLogEntries(logs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchPluginLogs fetches log entries from Mattermost and prints them continuously.
|
||||||
|
// It will return without an error when ctx is canceled.
|
||||||
|
func watchPluginLogs(ctx context.Context, client *model.Client4, pluginID string) error {
|
||||||
|
Logger.Info("Watching plugin logs", "plugin_id", pluginID)
|
||||||
|
|
||||||
|
err := checkJSONLogsSetting(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
var oldestEntry string
|
||||||
|
|
||||||
|
// Use context.WithoutCancel to keep watching even if parent context times out
|
||||||
|
watchCtx := context.WithoutCancel(ctx)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-watchCtx.Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
var page int
|
||||||
|
for {
|
||||||
|
logs, err := fetchLogs(watchCtx, client, LogsRequest{
|
||||||
|
Page: page,
|
||||||
|
PerPage: logsPerPage,
|
||||||
|
PluginID: pluginID,
|
||||||
|
Since: now,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch log entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var allNew bool
|
||||||
|
logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry)
|
||||||
|
|
||||||
|
printLogEntries(logs)
|
||||||
|
|
||||||
|
if !allNew {
|
||||||
|
// No more logs to fetch
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOldestEntry checks 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) (filteredLogs []string, newOldest string, allNew 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogsRequest contains parameters for fetching logs.
|
||||||
|
type LogsRequest struct {
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
PluginID string
|
||||||
|
Since time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchLogs fetches log entries from Mattermost
|
||||||
|
// and filters them based on pluginID and timestamp.
|
||||||
|
func fetchLogs(ctx context.Context, client *model.Client4, req LogsRequest) ([]string, error) {
|
||||||
|
logs, _, err := client.GetLogs(ctx, req.Page, req.PerPage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err = filterLogEntries(logs, req.PluginID, req.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 than since.
|
||||||
|
func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) {
|
||||||
|
type logEntry struct {
|
||||||
|
PluginID string `json:"plugin_id"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
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 as 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) {
|
||||||
|
for _, e := range entries {
|
||||||
|
fmt.Println(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 configuration in Mattermost")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
165
updateassets.go
165
updateassets.go
|
@ -10,9 +10,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/**/*
|
//go:embed assets/*
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
|
const (
|
||||||
|
assetsPrefix = "assets/"
|
||||||
|
assetsPrefixLen = 7
|
||||||
|
directoryPermissions = 0o750
|
||||||
|
filePermissions = 0o600
|
||||||
|
)
|
||||||
|
|
||||||
func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("updateassets command does not accept arguments")
|
return fmt.Errorf("updateassets command does not accept arguments")
|
||||||
|
@ -20,73 +27,22 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
|
|
||||||
Logger.Info("Updating assets in plugin directory", "path", pluginPath)
|
Logger.Info("Updating assets in plugin directory", "path", pluginPath)
|
||||||
|
|
||||||
// Load plugin manifest to check for webapp code
|
|
||||||
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
manifest, err := LoadPluginManifestFromPath(pluginPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
return fmt.Errorf("failed to load plugin manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the plugin has webapp code according to manifest
|
|
||||||
hasWebapp := HasWebappCode(manifest)
|
hasWebapp := HasWebappCode(manifest)
|
||||||
|
updatedCount := 0
|
||||||
|
|
||||||
// Counter for updated files
|
config := AssetProcessorConfig{
|
||||||
var updatedCount int
|
pluginPath: pluginPath,
|
||||||
|
hasWebapp: hasWebapp,
|
||||||
|
updatedCount: &updatedCount,
|
||||||
|
}
|
||||||
|
|
||||||
// Walk through the embedded assets
|
|
||||||
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
|
err = fs.WalkDir(assetsFS, "assets", func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
return processAssetEntry(path, d, err, config)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip the root assets directory
|
|
||||||
if path == "assets" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the "assets/" prefix to get the relative path
|
|
||||||
relativePath := path[7:] // len("assets/") = 7
|
|
||||||
|
|
||||||
// Skip webapp assets if plugin doesn't have webapp code
|
|
||||||
if !hasWebapp && strings.HasPrefix(relativePath, "webapp") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targetPath := filepath.Join(pluginPath, relativePath)
|
|
||||||
|
|
||||||
if d.IsDir() {
|
|
||||||
// Create directory if it doesn't exist
|
|
||||||
if err := os.MkdirAll(targetPath, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Read file content from embedded FS
|
|
||||||
content, err := assetsFS.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read embedded file %s: %w", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if target file exists and compare content
|
|
||||||
existingContent, err := os.ReadFile(targetPath)
|
|
||||||
if err == nil && bytes.Equal(existingContent, content) {
|
|
||||||
// File exists and content is identical, skip update
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create parent directory if it doesn't exist
|
|
||||||
parentDir := filepath.Dir(targetPath)
|
|
||||||
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write file to target location
|
|
||||||
if err := os.WriteFile(targetPath, content, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
|
|
||||||
}
|
|
||||||
Logger.Info("Updated file", "path", relativePath)
|
|
||||||
updatedCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -94,5 +50,96 @@ func RunUpdateAssetsCommand(args []string, pluginPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Info("Assets updated successfully!", "files_updated", updatedCount)
|
Logger.Info("Assets updated successfully!", "files_updated", updatedCount)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssetProcessorConfig struct {
|
||||||
|
pluginPath string
|
||||||
|
hasWebapp bool
|
||||||
|
updatedCount *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAssetEntry(path string, d fs.DirEntry, err error, config AssetProcessorConfig) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "assets" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relativePath := path[assetsPrefixLen:]
|
||||||
|
|
||||||
|
if !config.hasWebapp && strings.HasPrefix(relativePath, "webapp") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(config.pluginPath, relativePath)
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
return createDirectory(targetPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processAssetFile(path, targetPath, relativePath, config.updatedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAssetFile(embeddedPath, targetPath, relativePath string, updatedCount *int) error {
|
||||||
|
shouldUpdate, err := shouldUpdateFile(embeddedPath, targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldUpdate {
|
||||||
|
err = updateFile(embeddedPath, targetPath, relativePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
(*updatedCount)++
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectory(targetPath string) error {
|
||||||
|
if err := os.MkdirAll(targetPath, directoryPermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUpdateFile(embeddedPath, targetPath string) (bool, error) {
|
||||||
|
content, err := assetsFS.ReadFile(embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingContent, err := os.ReadFile(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
// File doesn't exist or other error, should update
|
||||||
|
return true, nil //nolint:nilerr
|
||||||
|
}
|
||||||
|
|
||||||
|
return !bytes.Equal(existingContent, content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateFile(embeddedPath, targetPath, relativePath string) error {
|
||||||
|
content, err := assetsFS.ReadFile(embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read embedded file %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentDir := filepath.Dir(targetPath)
|
||||||
|
if err := os.MkdirAll(parentDir, directoryPermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to create parent directory %s: %w", parentDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(targetPath, content, filePermissions); err != nil {
|
||||||
|
return fmt.Errorf("failed to write file %s: %w", targetPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info("Updated file", "path", relativePath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue