In previous posts I introduced how I sliced up my architecture using a composite user interface at the top.
In this post I'll explain in more detail how I use the Yarn package manager with Github Actions and Github Packages to achieve a practical workflow for packaging and composing apps.
Set up a repository
For each of the level 3 business capabilities identified I will set up a github repository to contain the source code of the web components exposed by a particular service supporting the capability, e.g. organizationalplanning.service.components
. This particular repository holds the web component code required to plan the organizational structure for next season in our sports club.
Setting up a yarn workspace
Each capability will expose multiple web component packages. At the very least there will be a separation between the read side and the write side, but there can be more.
Yarn is one of the only javascript package managers that supports having multiple packages per repository by means of it's workspace feature. To set up such a workspace, you need to add a package.json
file at the root of the repository. This file specifies to the package manager where the folders for each of the packages are. My folders are in a parent folder called _apps
, for reasons I will explain in a future post, but you can name it whatever makes sense for you.
{
"private": true,
"name": "clubmanagement.organizationalplanning",
"workspaces": [
"_apps/*"
]
}
As I'm using private github repositories for my component code, I will also need to tell yarn about the existance of my private package feeds. There is a file called .yarnrc
to configure yarn. For every package in the @messagehandler
scope, yarn will go search for it on https://npm.pkg.github.com
the others will be resolved from https://registry.yarnpkg.com
.
registry "https://registry.yarnpkg.com"
"@messagehandler:registry" "https://npm.pkg.github.com"
--*.modules-folder "_yarn"
version-sign-git-tag false
Besides setting the package feeds, I've also configured it to put any downloaded dependency in the _yarn
folder (node_modules
is the default). During the build git tag based versioning will be turned off so that packages inside the workspace can be versioned independently, versioning packages individually will be taken care of in the upcoming build automation section.
Package definitions
In my _apps
folder, there is a subfolder per exposed package. E.g:
organizationalplanning
for the read side componentsorganizationalplanning.admin
for the write side components.
Inside each of these folders there is another package.json
file describing the content of each respective component package. E.g. the package.json
for organizationalplanning
looks like this.
{
"name": "@messagehandler/clubmanagement.organizationalplanning",
"version": "1.0.22",
"main": "/js/clubmanagement.organizationalplanning.app.js",
"repository": {
"type": "git",
"url": "https://github.com/MessageHandler/organizationalplanning.service.components.git",
"directory": "_apps/organizationalplanning"
},
"author": "Yves Goeleven <yves@goeleven.com>",
"license": "UNLICENSED",
"publishConfig": {
"registry": "https://npm.pkg.github.com/"
},
"dependencies": {
"clubmanagement.shell": "npm:@messagehandler/clubmanagement.shell",
"clubmanagement.authentication": "npm:@messagehandler/clubmanagement.authentication",
"clubmanagement.authorization": "npm:@messagehandler/clubmanagement.authorization",
"messagehandler.eventsourcing": "npm:@messagehandler/messagehandler.eventsourcing"
}
}
The package definition contains the following attributes:
name
: this represents the package nameclubmanagement.organizationalplanning
in the@messagehandler
scope that will identify the package after uploading it to the package feed.version
: each package is versioned independently by yarn during the build.main
: this is the package entry point javascript file. Irrelevant for this use case.repository
: this attribute points to the code repository and directory in that repository where the code for the package resides.author
: yours truly.license
: as this is proprietary code, it requires anUNLICENSED
configuration.publishConfig
: this is the github package feed where yarn will push the package to during the build.dependencies
: these are other packages that the current package depends upon. All are in the@messagehandler
scope and therefore resolved from private repositories in github because of the.yarnrc
configuration setting for the scope.
Build automation
Once the package definitions are set, yarn will need to actually create the packages and publish them to the private package feed. The Github Actions infrastructure makes this fairly easy. In the .github\workflows\
folder of your repository, you can add .yml
files for each action that you want to run.
Here is an example action where it first checks out the repository on an ubuntu
machine, then installs node.js
and yarn
, which in turn get used to update the version in package.json
followed by publishing a new package to the github registry https://npm.pkg.github.com/
in the @messagehandler
scope.
name: publish organizationalplanning package
on:
push:
branches:
- master
paths:
- '_apps/organizationalplanning/**'
jobs:
publish:
runs-on: ubuntu-latest
env:
working-directory: ./_apps/organizationalplanning
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://npm.pkg.github.com/
scope: '@messagehandler'
- run: |
yarn install
git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" && git config --global user.name "$GITHUB_ACTOR"
yarn version --patch
git pull --rebase
git push
yarn publish
working-directory: $
env:
NODE_AUTH_TOKEN: $
Note: the git pull --rebase
command is there because multiple actions will run in parallel and each will update the version of it's component resulting in conflicting commits. Recently however Github Actions introduced a way to chain actions together, which is probably a better way to isolate the builds.
After committing, pushing and running the action a new package version should appear on the repository's package feed.
Composing the app from packages
Once the packages have been published on the Github Packages feed, you can start to use them from within your apps.
The process is similar to setting up an individual package. Make sure you have the .yarnrc
file copied in your app and add a package.json
file which describes the dependencies of your app.
{
"name": "@messagehandler/clubmanagement.coordination",
"version": "1.0.0",
"main": "index.js",
"repository": "https://github.com/MessageHandler/coordination.clubmanagement.io.git",
"author": "Yves Goeleven <yves@goeleven.com>",
"license": "UNLICENSED",
"dependencies": {
"clubmanagement.shell": "npm:@messagehandler/clubmanagement.shell",
"clubmanagement.authentication": "npm:@messagehandler/clubmanagement.authentication",
"clubmanagement.authorization": "npm:@messagehandler/clubmanagement.authorization",
"clubmanagement.coordination": "npm:@messagehandler/clubmanagement.coordination",
"clubmanagement.segmentation": "npm:@messagehandler/clubmanagement.segmentation",
"clubmanagement.organizationalplanning.admin": "npm:@messagehandler/clubmanagement.organizationalplanning.admin",
"clubmanagement.profile.admin": "npm:@messagehandler/clubmanagement.profile.admin",
"clubmanagement.roster.admin": "npm:@messagehandler/clubmanagement.roster.admin",
"messagehandler.eventsourcing": "npm:@messagehandler/messagehandler.eventsourcing"
}
}
After configuring the dependencies you can call yarn install
to download them into your local folder. Once new versions become available you can upgrade them using the yarn upgrade --latest
command.
And now your web components are available for use in your app.
In future posts I will come back to other pieces of infrastructure code that sit between downloading and actually using specific web components from the downloaded files, more specifically the static site generator I use and the feature toggle infrastructure.
How did you implement UI composition?
I am very interested to hear how you are handling UI composition in your projects, so don't hesitate to reach out!