We're happy you're reading this and willing to contribute! We also appreciate if you read through this document to help us accepting your contributions.
This project and everyone participating in it is governed by the Contributor Covenant Code of Conduct. By participating, you are expected to uphold this code.
Feel free to open new issues for bugs and support requests. Before doing so we ask you to:
- search for similar requests that might have been opened in the past to avoid repetitiveness
- read through the docs and see if a similar was already addressed. Also read through the examples
- make sure you're running the latest release
If you can't find any answer, when submitting a new issue please provide as much information as you can to help us understand. Don't forget to:
- include a clear title and description
- give as much relevant information as possible, which may include a code snippet or an executable test case
- give instructions on how to reproduce the problem
- describe the expected behavior and the current behavior
- tell us about the kind of deliverable you're using and their version and describe the environment so we can reproduce it
We will do our best to take relevant requests and update the documentation or publish a new example post about the specific use case in order to make it reusable by others.
This project is versioned according to Semantic Versioning.
If you're going to contribute with code or documentation please read through the following sections. When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the repository owners.
This project uses the GitHub flow branching strategy. Before contributing make sure you have a clear understanding of it.
We use Conventional Commits so please give commit messages the same format, like:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
One-line messages are fine for small changes, for example:
$ git commit -m "feat: brand new feature added"
while for bigger changes additional description in the body is welcome, like:
$ git commit -m "feat: brand new feature added
>
> Here is the detailed description of what has changed."
Allowed types are:
feat
: new featuresfix
: a bug fixbuild
: changes that affect the build system or external dependencieschore
: various changes not falling into other categoriesci
: changes to CI configuration files and scriptsdocs
: documentation only changesstyle
: changes that do not affect the meaning of the code but just its coding styerefactor
: code changes that neither fix bugs nor add featuresperf
: changes affecting performancestest
: changes on the test code and suites
Comments using the feat
type will bump the minor number while those using the fix
type bump the patch number. Other types do not bump any version number.
Breaking changes must have an exclamation mark at the end of the type (i.e. feat!: brand new feature added
) or a body line starting with BREAKING CHANGE:
(i.e. BREAKING CHANGE: this feature breaks backward compatibility
). Breaking changes will bump the major number.
Allowed scopes are:
config
: the change affects the configurationcore
: the change affects the core logic
Other scopes will be added in the future.
You can work on the project on any platform (Linux, Windows, Mac). You need to have installed:
- Git
- Go 1.17 or above
- Java JDK or JRE version 17 or above, either from Oracle or OpenJDK
- Gradle 7.x or above
- Docker CE
- Jekyll 4.2.0 or above (also requires Ruby 2.4.0 or above)
You also need a local copy of the repository that you can get by running:
$ git clone https://github.com/mooltiverse/nyx.git
You can use any IDE, just make sure you don't clutter the repo with IDE files.
The repository uses Gradle as the primary build tool and GitHub Actions for CI/CD. GitHub Actions, in turn, run Gradle tasks. This gives contributor a perfectly consistent environment to work locally before they commit or submit pull requests. Some tasks, like release
and publish
can only be executed on the CI/CD platform as they require access to some secrets variables not available locally.
The repository is divided into several sub projects, all under the modules
directory plus the documentation, in the docs
directory.
The resulting layout is:
nyx
+-- .github
| \--- workflows # GitHub Actions worflows
+-- docs # Documentation micro site
| +--- _data # Data files used by the documentation site
| +--- _pages # Documentation content pages
| +--- _posts # Documentation articles (i.e. examples)
| \--- _config.yml # Jekyll configuration for the documentation site
+-- gradle # Gradle wrapper files
+-- modules # Project sub-modules
| +--- docker # Docker image sub module
| \--- go # Go sub modules
| +--- errors # Go errors sub module
| +--- nyx # Go Main library sub module
| +--- utils # Go utilities sub module
| \--- version # Go Version library sub module
| \--- java # Java sub modules
| +--- gradle # Gradle plugin sub module
| +--- main # Java Main library sub module
| \--- version # Java Version library sub module
+-- build.gradle # Gradle build script (main)
+-- gradle.properties # Project and system properties
+-- gradlew # Gradle wrapper
+-- gradlew.bat # Gradle wrapper (for Windows)
\-- settings.gradle # Gradle project and sub-modules definition
Git ignored and non relevant items are omitted from the list. All sub-modules have a build.gradle
script file and a src
directory containing the source sets, not shown above.
Use the Gradle wrapper to run Gradle, like:
$ ./gradlew [TASK] # on Linux and Mac
> gradlew [TASK] # on Windows
where the main tasks are:
clean
to restore the entire project directory to its initial stateassemble
to build the project artifactstest
to run unit tests,integrationTest
for itegration tests tests,functionalTest
for functional tests testscheck
to run all tests (unit, integration and functional) andverify
to run all tests and other verification tasks like lintingbuild
to build the entire project and run all testspublish
to publish project outcomes to distribution servicesrelease
to tag the current version and publish it as a release
The publish
and release
tasks can only be executed on the CI/CD platform.
The recommended JDK version is 20
or newer. JDK version older than 17
is not supported.
The JDK version affects the number of functional tests excuted for the Gradle plugin, according to the Gradle compatibility matrix. This means that newer JDKs will run functional tests against a reduced set of Gradle versions because TestKit uses the original Java binaries for each tested Gradle release and Java classes compiled for JVM versions published after the Gradle release would raise an exception like unsupported class file major version XY
. You don't need to worry about this (as it's already taken care of in the functional test suites) unless you need to run functional tests against a specific gradle version that is not covered by the tests due to the JDK version you're using.
CI/CD is configured using GitHub Actions pipelines.
Sometimes the build brakes for reasons that are not related to code. Most frequent reasons for failure are:
- tests hit GitHub rate limits because they are very request intensive and when this happens all requests performed during tests fail until the rate limit is reset by GitHub
- the Nexus Server used to publish Java artifacts sometimes breaks connections
When tests fail because of the above reasons they can be easily relaunched. To do so, just click on the failed job in the GitHub Actions page and re-run.
When the Publish job fails there are a few cleaning steps to take before relaunching because the tasks performed in that job are not repeatable. Before re-running the job, using administrative credentials, you need to:
- connect to the Nexus repository and delete all stale Staging Repositories, if any
- delete the GitHub Release that was already published by the job, if any
- delete the GitHub tag that was already published by the job, if any
- delete the GitHub packages that were already published by the job, if any; for each package click on Package Settings, in the bottom right, then Manage Versions, and delete the specific version
- delete the Gradle plugin that was already published by the job, if any
After the above cleaning has been done the Release job can be re-launched from the GitHub Actions page.
The documentation is under the docs
directory and uses GitHub Pages for rendering. The documentation is a micro site written in Markdown files and rendered using Jekyll, the static web site generator supported by GitHub pages out of the box, along with the Minimal Mistakes theme. The site is available at https://mooltiverse.github.io/nyx/.
The regular content files are in the docs/_pages
directory while the examples, F.A.Qq. and other post sources are in docs/_posts
. The navigation is modelled in the docs/_data/navigation.yml
file and it defines the navigation items you see on top of the site and the sidebar.
Images are in docs/assets
and vector graphics (.svg
) are preferred over other formats. Diagrams and charts have been edited with Lucid tools (Lucidspark and Lucidchart).
When authoring content make sure that:
- the front matter of your pages is properly defined, with special regards to the title, layout, permalink (for pages), tags and categories (for posts and examples)
- content is properly formatted using the right helpers and utility classes
- you have a basic knowledge of Kramdown, the Markdown superset used by Jekyll
To test the site, get into the docs
directory and run:
$ bundle update
$ bundle exec jekyll serve --watch
then open a browser window to http://localhost:4000/ to see the changes (other options are available). Also make sure that running ./gradlew build test
doesn't break.
To troubleshoot the documentation site or to set up your local environment it's worth knowing that the documentation site has been bootstrapped by installing Ruby and Jekyll by following the Jekyll installation instructions and initialized using Bundler then following the Minimal Mistakes theme Quick Start Guide.
When contributing code make sure that you also provide extensive tests for the changes you make and that those tests achieve a sufficient coverage.
Some tests, specifically among functional tests, are very extensive and may take hours to complete. While running the entire suite of tests is critical and must always be performed for any release, during daily work we can run a reduced, yet significant, set of tests. For example we can run tests against the latest version of Gradle only rather than any supported version.
To limit the number of tests to a smaller set you can run setting the quickTests
property to true
when launching the Gradle script, like:
./gradlew -PquickTests=true functionalTest
Since Nyx supports remote repositories with different authentication options and and their extra features, tests must cover those features as well. To do so we need a test user on each platform along with its credentials. These are the currently used test accounts:
These accounts are used to to test multiple authentication options, dynamically create repositories, fiddle with them and clean up at the end of a test run. Complete cleanup is performed at the end of tests so when tests complete successfully there should be no contents left on the above accounts. In case some tests fail some stale repository might remain and can be deleted manually from the UI once logged in as the test user or just run ./gradlew testClean
.
These users are not connected to any team or organization so they do not expose any sensitive information. In case they get compromised we can replace them with some new accounts at any time.
Credentials for these users are managed by Nyx project owners but they are also safely passed to GitHub Actions jobs so that they can be used by CI/CD.
The credentials use for test accounts are:
- the user name
- an authentication token (OAuth2, Personal Access Token)
- an SSH key pair, with a public key and two versions of the private key: one is passphrase protected and the other is not protected
- a passphrase for password protected private keys
When testing locally you can still run tests by passing credentials for your own users (never use your personal accounts, create additional test users instead) and pass their credentials to Gradle scripts as environment variables or system properties. See the Gradle Build Environment for more.
Example using a local gradle.properties
file in the GRADLE_USER_HOME
(which has to be defined) directory with content:
# GitHub test user
gitHubTestUserName=nyxtest20200701
gitHubTestUserToken=<token goes here>
gitHubTestUserPublicKey=<public key goes here>
gitHubTestUserPrivateKeyPassphrase=<passphrase goes here>
gitHubTestUserPrivateKeyWithPassphrase=<private key goes here>
gitHubTestUserPrivateKeyWithoutPassphrase=<private key goes here>
# GitLab test user
gitLabTestUserName=nyxtest20200701
gitLabTestUserToken=<token goes here>
gitLabTestUserPublicKey=<public key goes here>
gitLabTestUserPrivateKeyPassphrase=<passphrase goes here>
gitLabTestUserPrivateKeyWithPassphrase=<private key goes here>
gitLabTestUserPrivateKeyWithoutPassphrase=<private key goes here>
Examples using the command line or environment variables:
# Example 1: pass the credentials as system properties on the Gradle command line
$ ./gradlew -PgitHubTestUserToken=<GITHUB_TOKEN> -PgitHubTestUserPublicKey=<PUBLIC_KEY> -PgitHubTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE> -PgitHubTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY> -PgitHubTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY> -PgitLabTestUserToken=<GITLAB_TOKEN> -PgitLabTestUserPublicKey=<PUBLIC_KEY> -PgitLabTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE> -PgitLabTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY> -PgitLabTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY> test
# Example 2: pass the credentials as environment variables
export ORG_GRADLE_PROJECT_gitHubTestUserToken=<GITHUB_TOKEN>
export ORG_GRADLE_PROJECT_gitHubTestUserPublicKey=<PUBLIC_KEY>
export ORG_GRADLE_PROJECT_gitHubTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE>
export ORG_GRADLE_PROJECT_gitHubTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY>
export ORG_GRADLE_PROJECT_gitHubTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY>
export ORG_GRADLE_PROJECT_gitLabTestUserToken=<GITLAB_TOKEN>
export ORG_GRADLE_PROJECT_gitLabTestUserPublicKey=<PUBLIC_KEY>
export ORG_GRADLE_PROJECT_gitLabTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE>
export ORG_GRADLE_PROJECT_gitLabTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY>
export ORG_GRADLE_PROJECT_gitLabTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY>
$ ./gradlew test
# Example 3: use an additional gradle.properties in your $GRADLE_USER_HOME where you define the above properties
# This is not requested if you already have the GRADLE_USER_HOME variable already defined. It can point wherever you want
export GRADLE_USER_HOME=~
# Create or edit the $GRADLE_USER_HOME/gradle.properties with the properties like these
gitHubTestUserToken=<GITHUB_TOKEN>
gitHubTestUserPublicKey=<PUBLIC_KEY>
gitHubTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE>
gitHubTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY>
gitHubTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY>
gitLabTestUserToken=<GITLAB_TOKEN>
gitLabTestUserPublicKey=<PUBLIC_KEY>
gitLabTestUserPrivateKeyPassphrase=<PRIVATE_KEY_PASSPHRASE>
gitLabTestUserPrivateKeyWithPassphrase=<PASSPHRASE_PROTECTED_PRIVATE_KEY>
gitLabTestUserPrivateKeyWithoutPassphrase=<UNPROTECTED_PRIVATE_KEY>
# Run Gradle as normal
$ ./gradlew test
Examples on how to use credentials passed as secrets on GitHub Actions see the .github\workflows\continuous-integration.yml
file. Please note that multi-line secrets (like private keys) can be set as they are in the GitHub projects setting (in the secrets section) even if the build script will then pass them as environment variables.
For more on the above options see the Gradle Build Environment. In any case, never store your credentials along with the project files.
Tests are executed also using SSH keypairs to authenticate to Git services. Private keys are tested in two ways:
- password protected, using a passphrase to protect the local private key
- not password protected, with the private key being unencrypted
You can use the same keypair for both kinds of tests. This is actually suggested so you have the same public key to load on remote services like GitHub and GitLab. You just need to have the private key in two versions: password protected and not protected.
You can generate a keypair with an unprotected private key by running ssh-keygen -t ed25519
(just press ENTER when prompted for the passphrase, to use the Ed25519 algorithm but you can use any other as long as it's supported locally and remotely). This will create the ~/.ssh/id_ed25519
file containing the private key and ~/.ssh/id_ed25519.pub
with the public key. Then you can make a copy of the ~/.ssh/id_ed25519
(let's say to ~/.ssh/id_ed25519.clear
) file and run ssh-keygen -p -f ~/.ssh/id_ed25519
to create a new version of the ~/.ssh/id_ed25519
, protected by the passphrase you like.
Now you have the two versions of the private keys in ~/.ssh/id_ed25519
and ~/.ssh/id_ed25519.clear
, password protected and unprotected, respectively, and you can copy their contents to the properties described above to make them available for testing.
One caveat here: the contents of key files in ~/.ssh
is on multiple lines so when you have to pass their contents as properties or environment variables you need to transform them into single lines. However, line breaks matter so you just need to replace every line break with \n
. For example, you can transform:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABBIR/mwmF
cmpIQMAGXJaOTQAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAEQ6MY269p/A+qF
...
to:
-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABBIR/mwmF\ncmpIQMAGXJaOTQAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAEQ6MY269p/A+qF
...
You can get the list of algorithms supported by the remote server running nmap --script ssh2-enum-algos -sV -p <REMOTE_SERVER_PORT> <REMOTE_SERVER_ADDRESS>
.
Gradle scans are published by any Gradle execution on the CI servers, while if running locally they must be explicitly enabled by adding --scan
to the Gradle arguments, like ./gradlew testClean --scan
.
To retrieve the URL for a Gradle scan just look at the end of the console output from Gradle for a row like:
Publishing build scan...
https://gradle.com/s/ac7cj7nadxrmm
Gradle scans are configured in the settings.gradle
file.
This is open source software. Consider the people who will read your code, and make it look nice for them.
We also:
- use spaces instead of tabs
- document code pervasively so that it can serve as an example for future readers
We manage technical debt carefully either by using correct issue labels and commenting code with proper TODO
comments, linking issues and other sources of informations. Every time we introduce or detect some technical debt we make sure it's properly managed.
Eclipse and VSCode use the .project
and .project
files to define how the project is built. In order to keep these files in sync with Gradle (which is still the primary source of information for build scripts and classpaths) you can run the eclipse
task in advance. You can also run the cleanEclipse
to clean previous files.
Example:
# Clean previous files
$ ./gradlew cleanEclipse
# Generate the files from Gradle definitions
$ ./gradlew eclipse
Because these files define local specific paths they are ignored by Git and they need to be generated on the local environment.
Also note that since the project is split into different modules, different copies of these files are available in the different sub-project directories.