Home | Send Feedback | Share on Bluesky |

Exposing build information of Spring Boot and Angular applications

Published: 12. June 2019  •  java, spring, javascript

In most desktop applications, you can find the application's version number in an "About" dialog.

About dialog from Notepad++

notepad++ about

The same information is useful in web applications. It helps users report problems, helps operators verify which build is deployed, and makes it much easier to confirm that a Service Worker or deployment pipeline picked up the newest version.

In this tutorial, we will expose build and Git information from a Spring Boot backend and an Angular frontend.

Spring Boot

Spring Boot already knows how to expose build information. You only have to make that metadata available during the build.

If you use Maven, configure the Spring Boot Maven plugin to generate META-INF/build-info.properties and add the Git commit ID plugin so the build also creates git.properties.

      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <executions>
          <execution>
            <goals>
              <goal>build-info</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
          <plugin>
              <groupId>io.github.git-commit-id</groupId>
              <artifactId>git-commit-id-maven-plugin</artifactId>
          </plugin>

pom.xml

When the application starts, Spring Boot reads these files and creates BuildProperties and GitProperties beans automatically.

In the sample application, both beans are injected into the main controller, which exposes three small endpoints: one for build information, one for Git information, and one for the active and default profiles.

  private final BuildProperties buildProperties;

  private final GitProperties gitProperties;

  private final Environment environment;

  public Application(BuildProperties buildProperties, GitProperties gitProperties,
      Environment environment) {
    this.buildProperties = buildProperties;
    this.gitProperties = gitProperties;
    this.environment = environment;
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @GetMapping("/build-info")
  public BuildProperties buildInfo() {
    return this.buildProperties;
  }

  @GetMapping("/git-info")
  public GitProperties gitInfo() {
    return this.gitProperties;
  }

  @GetMapping("/profile-info")
  public Map<String, Object> profileInfo() {
    return Map.of("activeProfiles",
        String.join(",", this.environment.getActiveProfiles()), "defaultProfiles",
        String.join(",", this.environment.getDefaultProfiles()));
  }

Application.java

If you only want to expose the version number, return just that value instead of the full object.

@GetMapping("/version")
public String version() {
  return this.buildProperties.getVersion();
}

Only expose the information that is actually useful. A public version endpoint is often fine, but detailed Git metadata is usually more appropriate for developers and operators.

With Actuator

If you already use Spring Boot Actuator, you can also expose build and Git metadata through the info endpoint.

Only the health endpoint is exposed over HTTP by default, so you have to opt in to the endpoints you need explicitly.

management.endpoints.web.exposure.include=health,info,env
management.info.git.mode=full

With this configuration in place, BuildProperties and GitProperties appear under /actuator/info.

Actuator also has an env endpoint, but it exposes much more than the active profile. If all you need is profile information, a dedicated endpoint like the one in the sample application is the better choice.

Angular

Angular does not generate build metadata for you, but the CLI's environment files make it straightforward to add.

The sample application keeps its environment values in src/environments. The application code imports the base environment file, and the production build replaces that file with environment.prod.ts.

import {environment} from '../environments/environment';

app.component.ts

          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "outputHashing": "all"
            },

angular.json

The production environment file stores the values we want to surface in the UI.

import {Env} from './env';

export const environment: Env = {
  production: true,
  serverURL: 'http://localhost:8080',
  version: '0.0.1',
  buildTimestamp: 1773987576,
  shortCommitId: '5145160',
  commitId: '5145160dec08e87765640b59a1fba7d7a83d3500',
  commitTime: 1773985782
};

environment.prod.ts

The development environment file contains matching fields so the application can render the same UI in development mode.

export const environment: Env = {
  production: false,
  serverURL: 'http://localhost:8080',
  version: 'DEVELOPMENT',
  buildTimestamp: null,
  shortCommitId: null,
  commitId: null,
  commitTime: null
};

environment.ts

Generating build metadata

The sample project uses a small Node.js script that reads the package version, fetches the latest Git commit, and updates environment.prod.ts before the production build runs.

Install the Git helper library first:

npm install --save-dev simple-git

Then add a script like this:

const fs = require('node:fs/promises');
const path = require('node:path');
const packageJson = require('./package.json');
const {simpleGit} = require('simple-git');

const environmentProdPath = path.join(__dirname, 'src', 'environments',
    'environment.prod.ts');

function replaceEnvironmentValue(content, key, value) {
  return content.replace(new RegExp(`(${key}: )([^\n]+?)(,?)$`, 'm'),
      (_, prefix, __, trailingComma) => `${prefix}${value}${trailingComma}`);
}

async function main() {
  const git = simpleGit(__dirname);
  const log = await git.log({maxCount: 1});
  const latestCommit = log.latest;

  if (!latestCommit) {
    throw new Error('Unable to read the latest Git commit');
  }

  let environmentFile = await fs.readFile(environmentProdPath, 'utf8');
  environmentFile = replaceEnvironmentValue(environmentFile, 'version',
      `'${packageJson.version}'`);
  environmentFile = replaceEnvironmentValue(environmentFile, 'buildTimestamp',
      Math.floor(Date.now() / 1000));
  environmentFile = replaceEnvironmentValue(environmentFile, 'shortCommitId',
      `'${latestCommit.hash.slice(0, 7)}'`);
  environmentFile = replaceEnvironmentValue(environmentFile, 'commitId',
      `'${latestCommit.hash}'`);
  environmentFile = replaceEnvironmentValue(environmentFile, 'commitTime',
      Math.floor(new Date(latestCommit.date).getTime() / 1000));

  await fs.writeFile(environmentProdPath, environmentFile);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

build-info-env.js

The script writes these values into the production environment file:

After that, the Angular application can read the values directly from the environment object.

  readonly clientInfo: ClientInfo = {
    version: environment.version,
    buildTimestamp: environment.buildTimestamp ? environment.buildTimestamp * 1000 : null,
    shortCommitId: environment.shortCommitId,
    commitId: environment.commitId,
    commitTime: environment.commitTime ? environment.commitTime * 1000 : null
  };

  readonly info$: Observable<{ build: BuildInfo, git: GitInfo, profile: ProfileInfo }> =
      forkJoin({
        build: this.httpClient.get<BuildInfo>(`${environment.serverURL}/build-info`),
        git: this.httpClient.get<GitInfo>(`${environment.serverURL}/git-info`),
        profile: this.httpClient.get<ProfileInfo>(`${environment.serverURL}/profile-info`)
      });

app.component.ts

And the template can display them to the user.

<h3>Build Info</h3>
<p>Version: <strong>{{ clientInfo.version }}</strong></p>
<p>Build Time: <strong>{{ clientInfo.buildTimestamp | date:'medium' }}</strong></p>

<h3>Git Info</h3>
<p>Commit Id: <strong>{{ clientInfo.commitId }}</strong></p>
<p>Short Commit Id: <strong>{{ clientInfo.shortCommitId }}</strong></p>
<p>Commit Time: <strong>{{ clientInfo.commitTime | date:'medium' }}</strong></p>

app.component.html

As with the backend, only publish the metadata that helps your users or your operations workflow.

Automated Release

The sample Angular project uses the npm version lifecycle to automate releases.

build-prod updates environment.prod.ts and runs the production build. The release-* scripts delegate to npm version, and the version lifecycle script rebuilds the application and stages the generated files before npm creates the release commit and tag.

  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "build-prod": "node ./build-info-env.js && ng build --configuration production",
    "serve-dist": "ws --hostname localhost -d dist/app/browser -p 1234 -o --log.format stats",
    "release-patch": "npm version patch",
    "release-minor": "npm version minor",
    "release-major": "npm version major",
    "version": "npm run build-prod && git add package.json src/environments/environment.prod.ts",
    "format": "npx prettier --write ."
  },

package.json

This gives you a clean release flow:

npm run release-patch

For minor and major releases, run one of these commands:

npm run release-minor
npm run release-major

If you ever want to bump the version number without creating the Git commit and tag that npm version normally generates, use npm version --no-git-tag-version ... instead. The npm version documentation covers that workflow in more detail.

This concludes the tutorial about exposing build and Git information in Spring Boot and Angular applications.

You can find the source code for all examples presented in this blog post on GitHub.