15 Development Process Practice How the Iam Project Manages the Development Process

15 Development Process Practice How the IAM Project Manages the Development Process #

Hello, I’m Kong Lingfei.

In Lesson 08 and Lesson 14, I introduced how to design the R&D process and how to efficiently manage projects based on Makefile. Today, we will focus on the R&D process and see how the IAM project efficiently manages the project through Makefile. By completing this lesson, you will not only have a deeper understanding of the content introduced in Lesson 08 and Lesson 14, but also gain a lot of hands-on experience and skills that can be directly applied in practice.

The R&D process consists of many stages, with the development and testing stages requiring the active involvement of developers. Therefore, in this lesson, I will focus on the project management capabilities of Makefile in these two stages and also share some of my design ideas for Makefile.

To demonstrate the process to you, let’s start with a scenario. We have a requirement: add a helloworld command to the IAM client tool iamctl that prints “hello world” to the terminal.

Next, let’s take a look at how to execute each step in the R&D process. First, we enter the development stage.

Development Phase #

The development phase is the main battlefield for developers, completely led by developers. It can be divided into two sub-phases: code development and code submission. Let’s first look at the code development phase.

Code Development #

After receiving the requirements, the first step is to develop the code. At this point, we need to choose a Git workflow that is suitable for the team and project. Since the Git Flow workflow is more suitable for large non-open source projects, we choose the Git Flow workflow here. The specific steps for code development are as follows:

Step 1: Create a new feature branch feature/helloworld based on the develop branch.

$ git checkout -b feature/helloworld develop

Note: The name of the newly created branch must comply with the branch naming rules in the Git Flow workflow. Otherwise, the commit may fail due to the non-conforming branch name. The branch naming rules for the IAM project are shown in the following figure:

Branch Naming Rules

The IAM project uses the pre-commit githooks to ensure that the branch name is compliant. When executing the git commit command in the IAM project root directory, Git will automatically execute the pre-commit script, which checks whether the name of the current branch is compliant.

There is one more thing to note here: Git will not commit the githooks script in the .git/hooks directory. Therefore, we need to ensure that the specified githooks script can still be installed to the .git/hooks directory after developers clone the repository, using the following method:

# Copy githook scripts when execute makefile    
COPY_GITHOOK:=$(shell cp -f githooks/* .git/hooks/) 

The above code is placed in the scripts/make-rules/common.mk file, which is executed each time the make command is executed, ensuring that the githooks are installed in the .git/hooks directory.

Step 2: Add the helloworld command in the feature/helloworld branch.

First, use the iamctl new helloworld command to create the helloworld command template:

$ iamctl new helloworld -d internal/iamctl/cmd/helloworld
Command file generated: internal/iamctl/cmd/helloworld/helloworld.go

Next, edit the internal/iamctl/cmd/cmd.go file and add helloworld.NewCmdHelloworld(f, ioStreams) to the source code file to load the helloworld command. Here, the helloworld command is set to the Troubleshooting and Debugging Commands command group:

import (
    "github.com/marmotedu/iam/internal/iamctl/cmd/helloworld"
)
...
{
    Message: "Troubleshooting and Debugging Commands:",
    Commands: []*cobra.Command{
        validate.NewCmdValidate(f, ioStreams),
        helloworld.NewCmdHelloworld(f, ioStreams),
    },
},

These operations involve the concept of low code. In Lesson 10, I emphasized the use of code generation technology as much as possible. There are two advantages to doing this: on the one hand, it can improve our code development efficiency; on the other hand, it can also ensure standardization and reduce errors caused by manual operations. Therefore, here, I also template the iamctl commands and generate them automatically using iamctl new.

Step 3: Generate the code.

$ make gen

If the changes do not involve code generation, the make gen operation can be skipped. Actually, make gen executes the gen.run pseudo-target:

gen.run: gen.clean gen.errcode gen.docgo.doc

As you can see, when executing make gen.run, it will first clean up the previously generated files, and then automatically generate error code and doc.go files respectively.

Here, it should be noted that the stock code generated by make gen must be idempotent. Only in this way can it ensure that the generated code is the same each time, avoiding problems caused by inconsistency.

More functions related to code generation can be placed in the gen.mk Makefile. For example:

  • gen.docgo.doc represents the automatic generation of the doc.go file.
  • gen.ca.% represents the automatic generation of the iamctl, iam-apiserver, and iam-authz-server certificate files.

Step 4: Copyright check.

If new files are added, we need to execute make verify-copyright to check if the new files have copyright headers.

$ make verify-copyright

If the copyright check fails, you can execute make add-copyright to automatically add the copyright header. Adding the copyright information is only for open-source software. If your software does not need to add copyright information, you can skip this step.

Here is a Makefile writing technique: if a command is needed for a Makefile command, you can make the target depend on a target like tools.verify.addlicense. The tools.verify.addlicense target will check if the tool is installed, and if not, it will be installed first.

.PHONY: copyright.verify
copyright.verify: tools.verify.addlicense 
  ...
tools.verify.%:          
  @if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
.PHONY: install.addlicense                              
install.addlicense:        
  @$(GO) get -u github.com/marmotedu/addlicense

In this way, make copyright.verify can be made as automated as possible, reducing the probability of manual intervention.

Step 5: Code formatting.

$ make format

Executing make format will perform the following formatting operations in order:

  1. Use gofmt to format your code.
  2. Use the goimports tool to automatically add/remove dependent packages, and sort and categorize the dependent packages in alphabetical order.
  3. Use the golines tool to format code exceeding 120 lines according to the golines rules, making it less than 120 lines.
  4. Use go mod edit -fmt to format the go.mod file.

Step 6: Static code analysis.

$ make lint

Regarding static code analysis, you can first understand that there is this step in the code development phase. As for how to operate it, I will give you a detailed introduction in the next lesson.

Step 7: Unit tests.

$ make test

Note that not all packages need to be tested. You can exclude packages that do not need unit tests using the following command:

go test `go list ./...|egrep -v $(subst $(SPACE),'|',$(sort $(EXCLUDE_TESTS)))`

In the go.test command, we also run the following command:

sed -i '/mock_.*.go/d' $(OUTPUT_DIR)/coverage.out

The purpose of running this command is to remove the unit test information of functions in the mock_.*.go files from coverage.out. Functions in the mock_.*.go files do not need unit tests. If they are not removed, they will affect the calculation of the subsequent unit test coverage.

If you want to check the unit test coverage, please execute:

$ make cover

The default test coverage is at least 60%, and you can specify a coverage threshold value in the command line, for example:

$ make cover COVERAGE=90

If the test coverage does not meet the requirement, the following error message will be returned:

test coverage is 62.1%
test coverage does not meet expectations: 90%, please add test cases!
make[1]: *** [go.test.cover] Error 1
make: *** [cover] Error 2

The exit code of the make command here is 1.

If the unit test coverage does not meet the set threshold value, additional test cases need to be added, otherwise merging into the develop and master branches is prohibited. The IAM project is configured with GitHub Actions CI automation pipeline, which will automatically run and check whether the unit test coverage meets the requirement.

Step 8, build.

Finally, we execute the make build command to build all binary installation files under the cmd/ directory.

$ make build

make build will automatically build all components under the cmd/ directory. If you only want to build one or more specific components, you can pass the BINS option, separating the components with spaces and enclosing them in double quotation marks:

$ make build BINS="iam-apiserver iamctl"

With these operations, we have completed all the operations in the code development stage.

If you feel that there are too many make commands to be executed manually, you can directly execute the make command:

$ make
=============> Generating IAM error code go source files
=============> Generating error code markdown documentation
=============> Generating missing doc.go for go packages
=============> Verifying the boilerplate headers for all files
=============> Formatting codes
=============> Running golangci to lint source codes
=============> Running unit test
...
=============> Building binary iam-pump v0.7.2-24-g5814e7b for linux amd64
=============> Building binary iamctl v0.7.2-24-g5814e7b for linux amd64
...

Executing make directly will execute the pseudo-target all, which depends on the pseudo-target all: tidy gen add-copyright format lint cover build, which means executing the following operations: adding/deleting dependencies, generating code, automatically adding copyright headers, formatting code, static code analysis, coverage testing, and building.

One point to note here is that all depends on cover, and cover actually executes go.test.cover, which in turn depends on go.test, so cover actually runs unit tests first and then checks if the unit test coverage meets the preset threshold value.

Finally, it is important to note that during the development stage, we can execute operations such as make gen, make format, make lint, make cover as needed, in order to detect and correct issues early.

Code submission #

After completing the code development, we need to submit the code to the remote repository, which consists of the following steps.

Step 1, after finishing development, commit the code to the feature/helloworld branch and push it to the remote repository.

$ git add internal/iamctl/cmd/helloworld internal/iamctl/cmd/cmd.go
$ git commit -m "feat: add new iamctl command 'helloworld'"
$ git push origin feature/helloworld

Here, I recommend only adding changes related to feature/helloworld, so that you know what changes are made in a commit for future reference. Therefore, I do not recommend using the git add . approach to submit changes.

When committing a commit, the commit-msg Git hook checks whether the commit message conforms to the Angular Commit Message specification. If it does not conform, an error will be reported. The commit-msg hook invokes go-gitlint to check the commit message. go-gitlint reads the commit message format configured in .gitlint file:

--subject-regex=^((Merge branch.*of.*)|((revert: )?(feat|fix|perf|style|refactor|test|ci|docs|chore)(\(.+\))?: [^A-Z].*[^.]$))
--subject-maxlen=72
--body-regex=^([^\r\n]{0,72}(\r?\n|$))*$

The IAM project is configured with GitHub Actions. When code is pushed, it triggers a CI pipeline that executes the ‘make all’ target. The GitHub Actions CI pipeline execution record is shown in the following image:

If the CI fails, the code needs to be modified until the CI pipeline is successful.

Now, let’s take a look at the GitHub Actions configuration:

name: IamCI

on: 
  push:
    branchs:
    - '*'
  pull_request:
    types: [opened, reopened]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.16

    - name: all
      run: make

It can be seen that GitHub Actions actually performs 3 steps: checkout code, set up the Go build environment, and execute the make command (i.e., execute the ‘make all’ target).

GitHub Actions also executes the ‘make all’ target to match the locally executed ‘make all’ target, ensuring that the CI process online is consistent with the local one. This way, when the make command is executed successfully locally, it will also pass online. It is important to maintain a consistent execution process and results. Otherwise, it would be troublesome if a make command passes locally but fails online.

The second step is to submit a pull request.

After logging in to GitHub, create a pull request based on the ‘feature/helloworld’ branch and specify reviewers for code review. The specific steps are shown in the following image:

When a new pull request is created, it also triggers the CI pipeline.

The third step is to notify reviewers to review the code after creating the pull request. GitHub will also send an internal message.

The fourth step is for reviewers to review the code.

Reviewers review the content based on the reviewed GitHub diff and add comments, and select Comment (only comment), Approve (pass), or Request Changes (fail and require modifications), as shown in the following image:

If the review fails, the feature developer can directly correct the code on the ‘feature/helloworld’ branch, push it to the remote ‘feature/helloworld’ branch, and then notify the reviewers for another round of review. Because a push event occurs, it triggers the GitHub Actions CI pipeline.

The fifth step is after the code review passes, the maintainer can merge the new code into the ‘develop’ branch.

Using the ‘Create a merge commit’ method, merge the pull request into the ‘develop’ branch, as shown in the following image:

The ‘Create a merge commit’ operation is actually ‘git merge –no-ff’, where all the commits on the ‘feature/helloworld’ branch are added to the ‘develop’ branch, and a merge commit is generated. Using this method, it is clear who made which submissions, and it is also more convenient to trace the history.

The sixth step is triggering the CI pipeline after merging into the ‘develop’ branch.

At this point, all the development phase operations are completed, and the overall process is as follows:

After merging into the ‘develop’ branch, we can move on to the next phase of the development stage, which is the testing stage.

Testing Phase #

During the testing phase, developers are primarily responsible for providing test packages and fixing bugs discovered during testing. During this process, they may also discover new requirements or changes, so it is necessary to assess whether these new requirements or changes should be included in the current iteration.

The workflow for the testing phase is as follows:

Step 1: Create a release branch based on the develop branch and test the code.

$ git checkout -b release/1.0.0 develop
$ make

Step 2: Submit for testing.

Submit the code from the release/1.0.0 branch to the testing team for testing. Let’s assume a scenario where the requirement is to print “hello world”, but it is printing “Hello World” instead and needs to be fixed. So how should you proceed?

You can directly modify the code in the release/1.0.0 branch. After making the changes, build and submit the code locally:

$ make
$ git add internal/iamctl/cmd/helloworld/
$ git commit -m "fix: fix helloworld print bug"
$ git push origin release/1.0.0

After pushing to release/1.0.0, GitHub Actions will run the CI pipeline. If the pipeline is successful, provide the code to the testing team. If the testing fails, make further modifications until the pipeline is successful.

The testing team will conduct thorough testing on the code in the release/1.0.0 branch, including functional testing, performance testing, integration testing, and system testing.

Step 3: After passing the testing, merge the feature branch into the master and develop branches.

$ git checkout develop
$ git merge --no-ff release/1.0.0
$ git checkout master
$ git merge --no-ff release/1.0.0
$ git tag -a v1.0.0 -m "add print hello world"   # Tag the master branch

At this point, the operations in the testing phase are basically complete. The artifacts of the testing phase are the code in the master and develop branches.

Step 4: Delete the feature/helloworld branch, and optionally, delete the release/1.0.0 branch.

After merging our code into the master/develop branches, feature developers can choose whether or not to keep the feature branch. However, unless there are specific reasons, it is recommended to delete it because having too many feature branches not only makes things messy, but can also affect performance. The deletion process is as follows:

$ git branch -d feature/helloworld

Makefile Project Management Tips for IAM Project #

In the above content, we have experienced the Makefile project management functionality of the IAM project based on the development process. These are the core functionalities that you should master, but the IAM project’s Makefile also has many other functionalities and design techniques. Next, I will share with you some valuable Makefile project management tips.

Automatic help Parsing #

As the project expands, the Makefile is likely to continuously add new management functionalities, and these functionalities also need to be included in the make help output. However, it is cumbersome and error-prone to modify the make help command every time a new target is added. So here, I use the automatic parsing method to generate the make help output:

## help: Show this help info.
.PHONY: help
help: Makefile
  @echo -e "\nUsage: make <TARGETS> <OPTIONS> ...\n\nTargets:"
  @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
  @echo "$$USAGE_OPTIONS"

In the help target command, the sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' command automatically parses the comment lines starting with ## in the Makefile, thereby generating the make help output.

Specify Variable Values in Options #

Variables can be specified in the Makefile options by the following assignment method:

ifeq ($(origin COVERAGE),undefined)
COVERAGE := 60
endif

For example, if we execute make, the COVERAGE is set to the default value of 60; if we execute make COVERAGE=90, then the COVERAGE value is 90. In this way, we can control the behavior of the Makefile more flexibly.

Automatically Generate CHANGELOG #

It is best for a project to have a CHANGELOG to show the changes between each version as part of the Release Notes. However, it is cumbersome and not easy to adhere to if you have to manually write the CHANGELOG every time. So here, we can use the git-chglog tool to automatically generate it.

The configuration file for the git-chglog tool of the IAM project is placed in the .chglog directory. You can refer to it when learning the git-chglog tool.

Automatically Generate Version Numbers #

A project also needs a version number, and the most commonly used one currently is semantic versioning. However, if developers rely on manually assigning version numbers, it is inefficient and often prone to issues such as missing version numbers or improper version numbering. So the best way is to automatically generate the version number using a tool. In the IAM project, the gsemver tool is used to automatically generate version numbers.

The entire version number of the IAM project is generated using the scripts/ensure_tag.sh script:

version=v`gsemver bump`
if [ -z "`git tag -l $version`" ];then
  git tag -a -m "release version $version" $version
fi

In the scripts/ensure_tag.sh script, the gsemver bump command is used to automatically generate a semantic version number, and the git tag -a command is executed to tag the repository with a version number. The gsemver command generates the version number based on the Commit Message.

After that, all version numbers used in the Makefile and Shell scripts are uniformly managed using the VERSION variable in scripts/make-rules/common.mk file:

VERSION := $(shell git describe --tags --always --match='v*')

The above Shell command uses git describe to get the tag (version number) closest to the current commit.

When executing git describe, if the tag that meets the conditions points to the latest commit, only the tag name will be displayed; otherwise, there will be relevant suffixes to describe how many commits are made after that tag and the latest commit ID. For example:

$ git describe --tags --always --match='v*'
v1.0.0-3-g1909e47

Here is an explanation of the characters in the version number:

  • 3: Indicates that there have been 3 commits since the tag v1.0.0.
  • g1909e47: g is an abbreviation for git, which is very useful in environments where multiple management tools coexist.
  • 1909e47: 7 characters represent the first 7 characters of the latest commit ID.

Finally, explanation of the parameters:

  • –tags, use all tags instead of only annotated tags. git tag <tagname> generates an unannotated tag, and git tag -a <tagname> -m '<message>' generates an annotated tag.
  • –always, if there are no available tags in the repository, use the commit abbreviation instead of the tag.
  • –match, consider only tags that match the given pattern.

Maintain Consistency #

Above, we introduced some management functionalities, such as checking if Commit Messages comply with the Angular Commit Message specification, generating CHANGELOG automatically, and generating version numbers automatically. These can be done using Makefile or manually. For example, to check if all Commits of IAM comply with the Angular Commit Message specification, you can execute the following command:

$ go-gitlint
b62db1f: subject does not match regex [^(revert: )?(feat|fix|perf|style|refactor|test|ci|docs|chore)(\(.+\))?: [^A-Z].*[^.]$]

You can also manually generate the CHANGELOG with the following command:

$ git-chglog v1.0.0 CHANGELOG/CHANGELOG-1.0.0.md

You can also execute gsemver to generate the version number:

$ gsemver bump
1.0.1

It is important to emphasize that we should ensure that whether performed manually or through Makefile operations, the results of checking the git commit message specification, generated CHANGELOG, and generated version numbers are consistent. This requires us to adopt the same operation method.

Summary #

In the entire development process, there are two stages in which developers need to be deeply involved: the development stage and the testing stage. In the development stage, after developers complete the code development, they usually need to perform operations such as generating code, copyright checking, code formatting, static code analysis, unit testing, and building. We can integrate these operations in a Makefile to improve efficiency and unify the workflow.

In addition, the IAM project uses some techniques when writing the Makefile. For example, in the make help command, the help information is completed by parsing the comments in the Makefile file. The CHANGELOG can be automatically generated using git-chglog, and semantic versioning can be automatically generated using gsemver.

Exercises #

  1. Take a look at how the make dependencies command is implemented in the IAM project. What are the benefits of this implementation?
  2. In the IAM project, gofmt, goimports, and golines are used as three formatting tools. Think about whether there are any other formatting tools that are worth integrating into the make format target command.

Feel free to share your thoughts and insights in the comments section. Let’s discuss and see you in the next lesson!