diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f8621eb4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +server/manifest.go linguist-generated=true diff --git a/.gitignore b/.gitignore index 1521c8b7..c30ecd19 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ dist +server/manifest.go diff --git a/Makefile b/Makefile index fc2c58c4..5342f195 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ 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 ?= -MANIFEST_FILE ?= plugin.json GOPATH ?= $(shell go env GOPATH) GO_TEST_FLAGS ?= -race GO_BUILD_FLAGS ?= @@ -13,6 +12,10 @@ 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 @@ -22,7 +25,6 @@ default: all # Verify environment, and define PLUGIN_ID, PLUGIN_VERSION, HAS_SERVER and HAS_WEBAPP as needed. include build/setup.mk -include build/legacy.mk BUNDLE_NAME ?= $(PLUGIN_ID)-$(PLUGIN_VERSION).tar.gz @@ -41,9 +43,20 @@ endif .PHONY: all all: check-style test dist +## 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/cmd/golangci-lint@v1.51.1 + $(GO) install gotest.tools/gotestsum@v1.7.0 + ## Runs eslint and golangci-lint .PHONY: check-style -check-style: webapp/node_modules +check-style: apply webapp/node_modules install-go-tools @echo Checking for style guide compliance ifneq ($(HAS_WEBAPP),) @@ -51,14 +64,13 @@ ifneq ($(HAS_WEBAPP),) 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),) - @if ! [ -x "$$(command -v golangci-lint)" ]; then \ - echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \ - exit 1; \ - fi; \ - @echo Running golangci-lint - golangci-lint run ./... + $(GO) vet ./... + $(GOBIN)/golangci-lint run ./... endif ## Builds the server, if it exists, for all supported architectures, unless MM_SERVICESETTINGS_ENABLEDEVELOPER is set. @@ -104,7 +116,7 @@ endif bundle: rm -rf dist/ mkdir -p dist/$(PLUGIN_ID) - cp $(MANIFEST_FILE) dist/$(PLUGIN_ID)/ + ./build/bin/manifest dist ifneq ($(wildcard $(ASSETS_DIR)/.),) cp -r $(ASSETS_DIR) dist/$(PLUGIN_ID)/ endif @@ -125,7 +137,7 @@ endif ## Builds and bundles the plugin. .PHONY: dist -dist: server webapp bundle +dist: apply server webapp bundle ## Builds and installs the plugin to a server. .PHONY: deploy @@ -134,7 +146,7 @@ deploy: dist ## Builds and installs the plugin to a server, updating the webapp automatically when changed. .PHONY: watch -watch: server bundle +watch: apply server bundle ifeq ($(MM_DEBUG),) cd webapp && $(NPM) run build:watch else @@ -188,9 +200,20 @@ detach: setup-attach ## Runs any lints and unit tests defined for the server and webapp, if they exist. .PHONY: test -test: webapp/node_modules +test: apply 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: apply webapp/node_modules install-go-tools ifneq ($(HAS_SERVER),) - $(GO) test -v $(GO_TEST_FLAGS) ./server/... + $(GOBIN)/gotestsum --format standard-verbose --junitfile report.xml -- ./... endif ifneq ($(HAS_WEBAPP),) cd webapp && $(NPM) run test; @@ -198,7 +221,7 @@ endif ## Creates a coverage report for the server code. .PHONY: coverage -coverage: webapp/node_modules +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 diff --git a/README.md b/README.md index 016e33c7..cf202986 100644 --- a/README.md +++ b/README.md @@ -164,3 +164,12 @@ The `/autolink` commands allow the users to easily edit the configurations. ## Development This plugin contains a server portion. Read our documentation about the [Developer Workflow](https://developers.mattermost.com/integrate/plugins/developer-workflow/) and [Developer Setup](https://developers.mattermost.com/integrate/plugins/developer-setup/) for more information about developing and extending plugins. + +### Releasing new versions + +The version of a plugin is determined at compile time, automatically populating a `version` field in the [plugin manifest](plugin.json): +* If the current commit matches a tag, the version will match after stripping any leading `v`, e.g. `1.3.1`. +* Otherwise, the version will combine the nearest tag with `git rev-parse --short HEAD`, e.g. `1.3.1+d06e53e1`. +* If there is no version tag, an empty version will be combined with the short hash, e.g. `0.0.0+76081421`. + +To disable this behaviour, manually populate and maintain the `version` field. diff --git a/build/legacy.mk b/build/legacy.mk deleted file mode 100644 index 0c327339..00000000 --- a/build/legacy.mk +++ /dev/null @@ -1,3 +0,0 @@ -.PHONY: apply -apply: - @echo make apply is deprecated and has no effect. diff --git a/build/manifest/main.go b/build/manifest/main.go index 957a5002..c57fe107 100644 --- a/build/manifest/main.go +++ b/build/manifest/main.go @@ -4,11 +4,50 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/mattermost/mattermost/server/public/model" "github.com/pkg/errors" ) +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") @@ -37,6 +76,16 @@ func main() { 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()) + } + default: panic("unrecognized command: " + cmd) } @@ -62,6 +111,32 @@ func findManifest() (*model.Manifest, error) { 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 } @@ -74,3 +149,63 @@ func dumpPluginID(manifest *model.Manifest) { 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 +} diff --git a/build/setup.mk b/build/setup.mk index 493b06fc..b90963af 100644 --- a/build/setup.mk +++ b/build/setup.mk @@ -4,8 +4,13 @@ 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 -o ../bin/manifest) +$(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) diff --git a/plugin.json b/plugin.json index 83abf77e..22217750 100644 --- a/plugin.json +++ b/plugin.json @@ -4,9 +4,7 @@ "description": "Automatically rewrite text matching a regular expression into a Markdown link.", "homepage_url": "https://github.com/mattermost/mattermost-plugin-autolink", "support_url": "https://github.com/mattermost/mattermost-plugin-autolink/issues", - "release_notes_url": "https://github.com/mattermost/mattermost-plugin-autolink/releases/tag/v1.4.0", "icon_path": "assets/icon.svg", - "version": "1.4.0", "min_server_version": "5.16.0", "server": { "executables": { @@ -19,7 +17,7 @@ "executable": "" }, "settings_schema": { - "header": "Configure this plugin directly in the `config.json` file, or using the `/autolink` command. Learn more [in our documentation](https://github.com/mattermost/mattermost-plugin-autolink/blob/master/README.md).\n\n To report an issue, make a suggestion, or contribute, [check the plugin repository](https://github.com/mattermost/mattermost-plugin-autolink).", + "header": "Configure this plugin directly in the config.json file, or using the /autolink command. Learn more [in our documentation](https://github.com/mattermost/mattermost-plugin-autolink/blob/master/README.md).\n\n To report an issue, make a suggestion, or contribute, [check the plugin repository](https://github.com/mattermost/mattermost-plugin-autolink).", "footer": "", "settings": [ { diff --git a/server/manifest.go b/server/manifest.go deleted file mode 100644 index bb9e6611..00000000 --- a/server/manifest.go +++ /dev/null @@ -1,11 +0,0 @@ -// This file is automatically generated. Do not modify it manually. - -package main - -var manifest = struct { - ID string - Version string -}{ - ID: "mattermost-autolink", - Version: "1.4.0", -}