Linting @ Dyte
Edit on GitHubAt Dyte we are now 44 persons, most of them developers, and each one has his own personal code style. This has lead sometimes to huge code conflicts when doing merges that create some annoyances and delays, so we decided to create an unified linting code style for all of Dyte projects (including a Jira ticket too!), just only we have been procrastinating it due to some other priorities. So, after the last merge conflict in a new project just created some days before, we decided to fix that issue once for all. Come and follow us to see how at Dyte we take code quality serious, and how at Dyte we don’t just simply apply a linter to our source code.
Linting
As linting engine we’ll make use of eslint, that’s one of
the current most popular and configurable linting engines for Javascript and
Typescript. In that way, @dyte-in/eslint-config
package is the central element
for our unified linting scheme, since it host the
shareable config
with all our customized linting rules. Later each project can extend the rules
applying their own project specific ones, although we’ve adjusted the common
rules to be used without needing to do any extra customizations.
Shareable config is defined in the .eslint.js
file, being it the package main
export. It can feels extrange to use a hidden file, but that’s on purposse since
it’s one of the filenames that eslint uses when looking for a project config, so
this way we can reuse it to lint the shareable config project itself. That’s the
reason why we are exporting it as a Javascript file instead of as a static JSON
one, too.
The shareable config extends and enable rules from
eslint:recommended and
import/recommended
configs, and for Typescript files it additionally makes use of the
typescript-eslint/recommended and typescript-eslint/recommended-requiring-type-checking configs.
This last one makes use of the actual tsconfig.json file being used by the
project, so our config also looks for and uses it automatically on the project
root, including when using an eslint custom named one, like
tsconfig.eslint.json
file.
Regarding the rules that we’ve enabled or customized to our usage habits, the most interesting ones are:
- consistent-return: enforces
returned values are of the same type. This has helped us to find A LOT of bugs
where we were returning
undefined
to notify failure instead of throwing an exception (or by the case, returning a failed Promise), probably due to some legacy code from EduMeet project that Dyte used on its origins. - import/order:
ensure all
import
statements are sorted alphabetically, for tidyness and prevent duplicates or misconfigurations. - max-len: strict length of lines, where we’ve customized it to allow unlimited length only on commen lines with an URL, so we doesn’t break it.
- no-restricted-globals
and
no-restricted-properties:
configured to prevent usage of deprecated global
event
variable (due to being in most of the cases a bug) and usage of setInterval statement (due to performance and runtime execution stability).
And for Typescript specific rules, the most interesting ones are:
- @typescript-eslint/ban-ts-comment:
forbid usage of
ts-comment
s to disable Typescript checkings without a detailed reason of why it has been disabled. - Multiple rules to notify of usage of unsafe
any
type. - no-restricted-syntax:
configured to prevent usage of Typescript
private
keyword instead of Javascript#[private]
class members.
To use the eslint-config
package is just like using any other eslint
shared
config, and you only need two steps:
-
install the package as a devDependency:
npm install --save-dev @dyte-in/eslint-config
-
extends the eslint config from our project
.eslintrc
file. Since package follows the@<scope>/eslint-config
name, eslint will detect and find it from thenode_modules
folder automatically, so it’s only needed to provide the scope name:{ "extends": ["@dyte-in"] }
Semantic releases
After finishing implementing the
eslint-config
package, it was time to publish it so all the other projects can use it as a
devDependency
. At Dyte we are using the
semantic-release tool to
automate the publish and release of packages new versions, just only we were
copying its configuration by hand. It showed us to be ironic to unify and
automate the linting rules configuration, while at the same time we needed to
copy again by hand the semantic release configuration, so we decided to fix that
too. Not only that, but also it made usage of semantic-release
easier by not
needing to install all the release process dependencies, since they are already
installed by the semantic-release-config
package itself.
Similar to eslint-config package, we created a
shareable configuration
where to store all our common release configurations. This is stored in a
release.config.js
file that’s exported as package main
entry for the same reasons we did
something similar with eslint-config .eslintrc.js
one: to be able to make use
of the config on the project itself, so we can automate the semantic releases of
the semantic-release-config
package itself.
In contrast to eslint-config
, rules are more generic, just only
analyzing the commit,
generating the
release notes and
the changelog (and upgrading
the package version number), and publishing the package release in both the
Github Packages Registry and as
an asset of the Github release.
Only interesting points are that we detect the environment we are working on and
flag the release as a prerelease
in case we are running in our staging
(preproduction) environment, and that we don’t
commit the release version upgrade changes
in the source code if all the previous steps has been succesful.
To use the semantic-release-config
package is only needed two steps:
-
install the package as a devDependency:
npm install --save-dev @dyte-in/semantic-release-config
-
configure semantic-release in
.releaserc
file:{ "extends": "@dyte-in/semantic-release-config" }
To run the semanic-release
command, it’s recommended to install the
semantic-release
package as devDependency and add a semanic-release
script
in the package.json
file:
{
"scripts": {
"semanic-release": "semantic-release"
}
}
Releases in Github Actions
Now that we have created the packages for the unified eslint
and
semantic-release
configs, it’s time to use them in all the other projects. And
yes, both @dyte-in/eslint-config
and @dyte-in/semantic-release-config
are
devDependencies between them and makes use one of the other, but I’m not talking
about that, but about the other projects.
Since we are using kubernetes to deploy our systems (and by extension, Docker), we needed some way to provide access to private packages in Github Packages Registry from inside Docker containers. The way we were doing it was by providing a custom NPM_TOKEN
environment variable (that in fact host a Github Personal Access Token), and use it as authToken
for the Github Packages Registry entry in the project .npmrc file used to define that project dependencies need to be fetch from the Github Packages Registry. Problem with this aproach is that it requires a token with elevated permissions (including write packages) for all the operations related to npm packages also when they are not needed (more specifically, install packages), not allowing a fine grain access control opening a security threat. But more specially, it prevented developers to install packages easily in local environments by not using standard location for users npmrc authentication. This has given us problems in the past, both to setup local environments, but also to install packages inside Github Actions CI servers, since token needed to be provided read access by hand for all the repos that would need to be used from.
For that reasons, we decided to take an aproach more aligned on how Github Actions works, allowing us to simplify the process by removing the addition of useless environment variables, and make it more secure. The first step was obviously remove the usage of the NPM_TOKEN
inside the project .npmrc
file, and any other environment variable being set in the Github environment variables, setting them instead as env entries of the npm install
and npm ci
Github Actions steps, and replace it in all the config files it was being used in the project for GITHUB_TOKEN
, the Github Actions standard default one with lowered permissions (mostly just only read access to the repos). In local environments, it would be using the standard global ~/.npmrc
auth config, so developers would just need to authenticate agains Github Packages Registry only once and forget about it.
- run: npm install
env:
NODE_AUTH_TOKEN: $
On the other hand, for the Github Actions steps that will publish the packages, we’ll use the npm
Github Action standard NODE_AUTH_TOKEN
environment variable with the same Github Personal Access Token we were using before, since it’s just needed only to publish packages. We’ll use to assign the Github Access Token to the GITHUB_TOKEN
environment variable to create the Github release itself, too. There’s a little push down though: Docker containers are fully isolated from the host environment, so usage of the registry-url field in the Github standard setup-node action will not work. We’ll need to replicate what setup-node
does… that in fact, it overwrites and updates the content of the project .npmrc
file to add explicitly the line with the token, just the same way we were doing before 😅 just only using the standard NODE_AUTH_TOKEN
environment variable, and doing it in the checkout code without commiting and pushing it afterwards, so it’s safe to do the modification. In our case, we are doing it inside the Docker container itself, so we are equaly safe here 🙂
RUN echo //npm.pkg.github.com/:_authToken=$NODE_AUTH_TOKEN >> .npmrc
Bonus: print secrets in Github Actions
Github Action detects when you want to print a secret on the console, so it prevents to get them logged by replacing the secret strings with ***
. Sometimes we need to get them printed for debugging purposses, so we need to trick it. The most simple way is to just concatenate the output with the secret using the unix sed command to split the secret string using spaces:
run: echo $ | sed 's/./& /g'
This way, Github Action could not match the output with any of the stored secrets, and would print the string verbatin.
Disclaimer: please don’t do that with your production secrets, just use some ones deditated for testing purposses, and ideally one-use-only throw away ones.
Corporate version previously published at https://dyte.io/blog/linting-at-dyte
Comment on Twitter
You can leave a comment by replying this tweet.