diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cd88554 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml new file mode 100644 index 0000000..59dcbb8 --- /dev/null +++ b/.github/workflows/_build.yml @@ -0,0 +1,32 @@ +name: Build + +on: + workflow_call: + +permissions: + contents: read + checks: write + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.22.1 + - name: Install XPortal + run: | + go install go.lumeweb.com/xportal/cmd/xportal@latest + - name: Checkout Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Extract Repo Name + id: repo-name + run: echo "REPO_NAME=$(echo ${{ github.repository }} | cut -d '/' -f 2)" >> $GITHUB_OUTPUT + - name: Build + run: | + PLUGIN=$(readlink -f .) + xportal build --with go.lumeweb.com/${{ steps.repo-name.outputs.REPO_NAME }} --replace go.lumeweb.com/${{ steps.repo-name.outputs.REPO_NAME }}=$PLUGIN \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5adae30 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,11 @@ +name: Build + +on: + push: + branches: + - '**' + +concurrency: ${{ github.workflow }}-${{ github.ref }} +jobs: + build: + uses: ./.github/workflows/_build.yml diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml new file mode 100644 index 0000000..c269de8 --- /dev/null +++ b/.github/workflows/changeset-release.yml @@ -0,0 +1,74 @@ +name: Changeset Release + +on: + workflow_dispatch: + inputs: + type: + description: 'Change Type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Change Description (in markdown)' + required: true + type: string + +jobs: + create-release-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Generate Random Title + id: random-title + run: | + words=( + "playful" "gentle" "swift" "bright" "calm" "wise" "bold" "keen" "warm" "pure" + "fox" "deer" "hawk" "wolf" "bear" "dove" "swan" "lion" "tiger" "eagle" + "stream" "cloud" "storm" "brook" "river" "lake" "ocean" "peak" "cliff" "dale" + ) + size=${#words[@]} + title="" + for i in {1..3}; do + index=$((RANDOM % size)) + title="$title-${words[$index]}" + done + # Remove leading dash and output + title=${title#-} + echo "title=${title}" >> $GITHUB_OUTPUT + + - name: Get Package Version + id: package-version + run: | + PACKAGE_NAME=$(node -p "require('./package.json').name") + echo "name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT + + - name: Create Changelog Entry + shell: bash + run: | + mkdir -p .changeset + printf -- "---\n\"%s\": %s\n---\n\n%s\n" \ + "${{ steps.package-version.outputs.name }}" \ + "${{ inputs.type }}" \ + "${{ inputs.description }}" > ".changeset/${{ steps.random-title.outputs.title }}.md" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + branch: release + delete-branch: true + title: 'chore: release' + commit-message: 'chore: release' + body: | + 🚀 Release PR created automatically + + Type: ${{ inputs.type }} + Description: ${{ inputs.description }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b1a162f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + branches: + - "**" + paths: + - '.changeset/**' + - '.changeset-release/**' + - 'package.json' + tags: + - '**' + pull_request: + types: + - closed + branches: + - '**' + paths: + - '.changeset/**' + - '.changeset-release/**' + - 'package.json' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: LumeWeb/golang-versioner-action@v0.1.8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/update-ui.yml b/.github/workflows/update-ui.yml new file mode 100644 index 0000000..1def31f --- /dev/null +++ b/.github/workflows/update-ui.yml @@ -0,0 +1,49 @@ +name: Handle UI Update + +on: + repository_dispatch: + types: [update-ui] + +jobs: + update-dependency: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.23.0' + + - name: Update Go dependency + run: | + COMMIT_HASH="${{ github.event.client_payload.commit_hash }}" + VERSION="${{ github.event.client_payload.version }}" + APP_NAME="${{ github.event.client_payload.app_name }}" + TIMESTAMP=$(date -u +"%Y%m%d%H%M%S") + + # Generate the new version string + NEW_VERSION="v${VERSION}-${APP_NAME}-go.0.${TIMESTAMP}-${COMMIT_HASH}" + + # Update the dependency + GOPROXY=direct go get "go.lumeweb.com/web/go/${APP_NAME}@${COMMIT_HASH}" + go mod tidy + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.PAT_TOKEN }} + commit-message: "chore: update UI dependency to ${{ github.event.client_payload.version }}" + title: "chore: update UI dependency to ${{ github.event.client_payload.version }}" + body: | + Updates UI dependency to latest changes. + + Source: ${{ github.event.client_payload.repository }}@${{ github.event.client_payload.commit_hash }} + Version: ${{ github.event.client_payload.version }} + branch: deps/update-ui + base: develop + delete-branch: true + labels: | + dependencies + automated pr \ No newline at end of file diff --git a/build/build.go b/build/build.go new file mode 100644 index 0000000..92fae0b --- /dev/null +++ b/build/build.go @@ -0,0 +1,19 @@ +package build + +import ( + "go.lumeweb.com/portal/build" +) + +var ( + Version string + GitCommit string + GitBranch string + BuildTime string + GoVersion string + Platform string + Architecture string +) + +func GetInfo() build.BuildInfo { + return build.New(Version, GitCommit, GitBranch, BuildTime, GoVersion, Platform, Architecture) +} diff --git a/frontend.go b/frontend.go new file mode 100644 index 0000000..1d95b59 --- /dev/null +++ b/frontend.go @@ -0,0 +1,18 @@ +package main + +import ( + "go.lumeweb.com/portal-plugin-frontend/build" + "go.lumeweb.com/portal-plugin-frontend/internal" + "go.lumeweb.com/portal-plugin-frontend/internal/api" + "go.lumeweb.com/portal/core" +) + +func init() { + core.RegisterPlugin(core.PluginInfo{ + ID: internal.PLUGIN_NAME, + Version: build.GetInfo(), + API: func() (core.API, []core.ContextBuilderOption, error) { + return api.NewAPI() + }, + }) +} diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..6e7efa6 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,128 @@ +package api + +import ( + "archive/zip" + "bytes" + "context" + _ "embed" + "fmt" + "github.com/google/go-github/v50/github" + "github.com/gorilla/mux" + "go.lumeweb.com/portal-plugin-frontend/internal" + "go.uber.org/zap" + "io" + "net/http" + "net/url" + "strings" + + pluginCfg "go.lumeweb.com/portal-plugin-frontend/internal/config" + "go.lumeweb.com/portal/config" + "go.lumeweb.com/portal/core" + "go.lumeweb.com/portal/middleware" + portal_frontend "go.lumeweb.com/web/go/portal-frontend" +) + +var _ core.API = (*API)(nil) + +type API struct { + ctx core.Context + config config.Manager + logger *core.Logger +} + +func (a *API) Config() config.APIConfig { + return &pluginCfg.APIConfig{} +} + +func (a *API) Name() string { + return internal.PLUGIN_NAME +} + +func NewAPI() (*API, []core.ContextBuilderOption, error) { + api := &API{} + + opts := core.ContextOptions( + core.ContextWithStartupFunc(func(ctx core.Context) error { + api.ctx = ctx + api.config = ctx.Config() + api.logger = ctx.APILogger(api) + + return nil + }), + ) + + return api, opts, nil +} + +func (a *API) Configure(router *mux.Router, _ core.AccessService) error { + // Middleware setup + corsHandler := middleware.CorsMiddleware(nil) + + router.Use(corsHandler) + + var httpHandler http.Handler + cfg := a.config.GetAPI(internal.PLUGIN_NAME).(*pluginCfg.APIConfig) + if cfg.GitRepo != "" { + u, err := url.Parse(cfg.GitRepo) + if err != nil { + return err + } + path := u.Path + path = strings.TrimPrefix(path, "/") + path = strings.TrimSuffix(path, "/") + components := strings.Split(path, "/") + if len(components) < 2 { + return fmt.Errorf("invalid Git repository URL: %s", cfg.GitRepo) + } + owner := components[0] + repo := components[1] + + client := github.NewClient(nil) + release, _, err := client.Repositories.GetLatestRelease(context.Background(), owner, repo) + if err != nil { + return err + } + zipURL := *release.ZipballURL + resp, err := http.Get(zipURL) + if err != nil { + return err + } + defer func() { + if err := resp.Body.Close(); err != nil { + a.logger.Error("failed to close response body", zap.Error(err)) + } + }() + + buf, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + byteReader := bytes.NewReader(buf) + zipFs, err := zip.NewReader(byteReader, int64(byteReader.Len())) + if err != nil { + return err + } + + httpHandler = http.FileServer(http.FS(zipFs)) + } else { + httpHandler = portal_frontend.Handler() + } + + router.PathPrefix("/assets/").Handler(httpHandler) + router.PathPrefix("/").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool { + return !strings.HasPrefix(r.URL.Path, "/api/") + }).Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.URL.Path = "/" + httpHandler.ServeHTTP(w, r) + })) + + return nil +} + +func (a *API) Subdomain() string { + return "" +} + +func (a *API) AuthTokenName() string { + return core.AUTH_COOKIE_NAME +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4b35626 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,17 @@ +package config + +import ( + "go.lumeweb.com/portal/config" +) + +const PLUGIN_NAME = "frontend" + +var _ config.APIConfig = (*APIConfig)(nil) + +type APIConfig struct { + GitRepo string `config:"git_repo"` +} + +func (A APIConfig) Defaults() map[string]any { + return map[string]any{} +} diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 0000000..97f149a --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,3 @@ +package internal + +const PLUGIN_NAME = "frontend"