Repository: vmware/govmomi
Pull request #2340 (2021-03-31 11:21:07)

First step towards release automation
First PR towards fully automating the release process with `goreleaser`. Cutting a release is still done manually, but using the goreleaser configuration provided in this PR. Once we gain confidence in the associated Github Actions workflows we can finally switch towards full release automation. This PR also adds more tests and build verification.

## Changes
### (1) Release Artefacts

⚠️ **BREAKING** Release artefacts created by the provided `goreleaser` configuration use a different naming template. In addition, the `386` `GOARCH` is dropped. This is to allow scripts to easily download and reference the correct binary per platform/arch (e.g. using `uname`). Example for `vcsim`:

```bash
vcsim_FreeBSD_armv6.tar.gz
vcsim_Windows_x86_64.zip
vcsim_Darwin_arm64.tar.gz
vcsim_Darwin_x86_64.tar.gz
vcsim_Linux_mips64le.tar.gz
vcsim_FreeBSD_x86_64.tar.gz
vcsim_Linux_x86_64.tar.gz
vcsim_Linux_armv6.tar.gz
vcsim_Linux_arm64.tar.gz
vcsim_FreeBSD_arm64.tar.gz
```

Homebrew files will be created during a manual release but not yet pushed to the pre-configured `govmomi/homebrew-tap`. This will be fixed in a subsequent PR when switching to full release automation. The Homebrew community provides `govc` in the core repo (not validated by govmomi maintainers though).

Docker images (separate for `govc` and `vcsim`) for `linux/amd64` will be created and pushed during a manual run. Example tags pushed for `govc`:

```bash
vmware/govc:6a3ba216
vmware/govc:latest  
vmware/govc:v0.25.0
```

⚠️ **BREAKING** The checksum file is renamed to `checksums.txt`.

### (2) Build

The go modules version is bumped to use `v1.14` [semantics](https://golang.org/doc/modules/gomod-ref#go). The go tool will use vendoring by default. For explicitness and documentation purposes `-mod=vendor` is still passed in various build/test related workflows. Tests will be run against Go `v1.15` and `v1.16`. Builds (releases) will are configured to use Go `v1.16` (once we use automated releases).

`govc version` output is enhanced to include build related information (injected via `goreleaser`). Example:

```bash
# with pseudo commit example using the "long" flag option
govc  version -l
Build Version: v0.25.0
Build Commit: 6a3ba216
Build Date: 2021-04-04T19:00:11Z
```

The old behavior is retained:

```bash
govc version
govc 0.25.0
```

`vcsim` shows version with a new subcommand:

```bash
# with pseudo commit example
./vcsim version
Build Version: 0.25.0
Build Commit: 6a3ba216
Build Date: 2021-04-01T12:01:20Z
```

### (3) New/Updated Workflows

Add `make govc-test` Makefile target in a new `govc-tests` workflow.

Remove `GORACE: history_size=5` from unit tests as it seems to be the cause for flaky unit tests in CI.

Verify build/releasability by running a new workflow on every PR/push to master and as a daily cron job which mimics a `goreleaser` run (without pushing a release, artefacts, etc.).

Fix wording in the stale issue/PR workflow.

Add 10min timeout to most workflows to prevent dangling workflow runs.

### (4) Other

Use a custom `powercli` image in `bats` tests to avoid Docker rate limit pulls: `projects.registry.vmware.com/pez/powerclicore`. 

Instead of using a `curl` container, use `curl` provided as part of the runner to avoid rate limits/reduce time.

The `vcsim` Dockerfile uses `CMD` instead of `ENTRYPOINT` so it's possible to overwrite the executable, e.g. to extract version information with:

```bash
docker run --rm vmware/vcsim /vcsim version
Build Version: 0.25.0
Build Commit: 6a3ba216
Build Date: 2021-04-01T12:01:20Z
```

⚠️ The `govc-test` target seems to be flaky in CI (randomly hangs until timeout enforced) as reported by other users (#2351) and from my experience running the PR checks. That's why this is a separate workflow so it's easy to re-run a failed workflow.

Closes: #2339 
Related: #2302, #1844, #2295

Signed-off-by: Michael Gasch <mgasch@vmware.com>

embano1: Reverted `govc version` behavior (new flag `-l` gives details) and added a `vcsim version` command instead of printing the version for `vcsim` in the help section.

All updates reflected in the PR description above.
embano1: @dougm ready for another review

Patch:
diff --git a/.github/workflows/govmomi-build.yaml b/.github/workflows/govmomi-build.yaml
new file mode 100644
index 00000000..7cc8134c
--- /dev/null
+++ b/.github/workflows/govmomi-build.yaml
@@ -0,0 +1,69 @@
+#  Copyright (c) 2021 VMware, Inc. All Rights Reserved.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+name: Build
+
+on:
+  push:
+    branches: ["main", "master"]
+
+  pull_request:
+    branches: ["main", "master"]
+
+  # also run every night
+  schedule:
+    - cron: "0 1 * * *"
+
+jobs:
+  artifacts:
+    name: Build Snapshot Release (no upload)
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.16
+      - name: GoReleaser Snapshot
+        uses: goreleaser/goreleaser-action@v2
+        with:
+          version: latest
+          args: release --rm-dist --snapshot # snapshot will disable push/release
+      - name: Verify git clean
+        shell: bash
+        run: |
+          if [[ -z "$(git status --porcelain)" ]]; then
+            echo "${{ github.repository }} up to date."
+          else
+            echo "${{ github.repository }} is dirty."
+            echo "::error:: $(git status)"
+            exit 1
+          fi
+      # make artifacts available for inspection
+      # https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts
+      - name: Archive run artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          name: dist
+          # upload only some artifacts for introspection to keep storage size small (delete after 1d)
+          path: |
+            dist/govc_*x86_64.tar.gz
+            dist/vcsim_*x86_64.tar.gz
+            dist/checksums.txt
+          retention-days: 1
diff --git a/.github/workflows/govmomi-check-wip.yaml b/.github/workflows/govmomi-check-wip.yaml
index 5324bf9f..e25d9a89 100644
--- a/.github/workflows/govmomi-check-wip.yaml
+++ b/.github/workflows/govmomi-check-wip.yaml
@@ -13,7 +13,7 @@
 #  limitations under the License.
 
 # Perform "exit 1" if PR title starts with "WIP" to block accidental merges
-name: Check "WIP" in PR Title
+name: Check WIP
 
 on:
   pull_request:
@@ -21,7 +21,7 @@ on:
 
 jobs:
   WIP:
-    name: "Check WIP in title"
+    name: "Check WIP in PR title"
     runs-on: ubuntu-latest
     if: startsWith(github.event.pull_request.title, 'WIP')
     steps:
diff --git a/.github/workflows/govmomi-go-lint.yaml b/.github/workflows/govmomi-go-lint.yaml
index 9f65b375..2050a2f7 100644
--- a/.github/workflows/govmomi-go-lint.yaml
+++ b/.github/workflows/govmomi-go-lint.yaml
@@ -25,7 +25,6 @@ jobs:
 
   autoformat:
     name: Auto-format and Check
-    runs-on: ubuntu-latest
     strategy:
       fail-fast: false # Keep running if one leg fails.
       matrix:
@@ -35,6 +34,8 @@ jobs:
         include:
           - tool: goimports
             importpath: golang.org/x/tools/cmd/goimports
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
 
     steps:
       - name: Set up Go 1.15.x
@@ -82,6 +83,7 @@ jobs:
   lint:
     name: Lint Files
     runs-on: ubuntu-latest
+    timeout-minutes: 10
 
     steps:
       - name: Set up Go 1.15.x
diff --git a/.github/workflows/govmomi-go-build.yaml b/.github/workflows/govmomi-govc-tests.yaml
similarity index 65%
rename from .github/workflows/govmomi-go-build.yaml
rename to .github/workflows/govmomi-govc-tests.yaml
index b5305834..633dcb52 100644
--- a/.github/workflows/govmomi-go-build.yaml
+++ b/.github/workflows/govmomi-govc-tests.yaml
@@ -12,29 +12,28 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-name: Build
+name: govc Tests
 
 on:
   push:
-    branches: [ 'main', 'master' ]
+    branches: ["main", "master"]
 
   pull_request:
-    branches: [ 'main', 'master' ]
+    branches: ["main", "master"]
 
 jobs:
-
-  build:
-    name: Build
+  govc-tests:
+    name: Run govc Tests
     strategy:
       matrix:
-        go-version: [ '1.15', '1.16']
-        buildOS: [ 'linux', 'darwin', 'freebsd', 'windows' ]
-        platform: [ 'ubuntu-latest']
+        go-version: ["1.15", "1.16"]
+        # tests randomly hang on ubuntu-20.04 go v1.16
+        platform: ["ubuntu-18.04"]
 
     runs-on: ${{ matrix.platform }}
+    timeout-minutes: 10
 
     steps:
-
       - name: Set up Go ${{ matrix.go-version }}
         uses: actions/setup-go@v2
         with:
@@ -44,14 +43,5 @@ jobs:
       - name: Check out code
         uses: actions/checkout@v2
 
-      - name: Build govc
-        env:
-          GOOS: ${{ matrix.buildOS }}
-        run: |
-          make -C govc install
-
-      - name: Build vcsim
-        env:
-          GOOS: ${{ matrix.buildOS }}
-        run: |
-          make -C vcsim install
+      - name: Run govc Tests
+        run: make govc-test
diff --git a/.github/workflows/govmomi-stale.yaml b/.github/workflows/govmomi-stale.yaml
index 01599f44..fea2d671 100644
--- a/.github/workflows/govmomi-stale.yaml
+++ b/.github/workflows/govmomi-stale.yaml
@@ -16,33 +16,30 @@ name: Close Stale
 
 on:
   schedule:
-    - cron: '0 1 * * *' # daily
+    - cron: "0 1 * * *" # daily
 
 jobs:
-
   stale:
-    runs-on: 'ubuntu-latest'
+    runs-on: "ubuntu-latest"
 
     steps:
-      - uses: 'actions/stale@v3'
+      - uses: "actions/stale@v3"
         with:
-          repo-token: '${{ secrets.GITHUB_TOKEN }}' # No need to setup
+          repo-token: "${{ secrets.GITHUB_TOKEN }}" # No need to setup
 
           stale-issue-message: |-
             This issue is stale because it has been open for 90 days with no
             activity. It will automatically close after 30 more days of
-            inactivity. Reopen the issue with `/reopen`. Mark the issue as
-            fresh by adding the comment `/remove-lifecycle stale`.
-          stale-issue-label: 'lifecycle/stale'
-          exempt-issue-labels: 'lifecycle/frozen'
+            inactivity. Mark as fresh by adding the comment `/remove-lifecycle stale`.
+          stale-issue-label: "lifecycle/stale"
+          exempt-issue-labels: "lifecycle/frozen"
 
           stale-pr-message: |-
             This Pull Request is stale because it has been open for 90 days with
             no activity. It will automatically close after 30 more days of
-            inactivity. Reopen with `/reopen`. Mark as fresh by adding the
-            comment `/remove-lifecycle stale`.
-          stale-pr-label: 'lifecycle/stale'
-          exempt-pr-labels: 'lifecycle/frozen'
+            inactivity. Mark as fresh by adding the comment `/remove-lifecycle stale`.
+          stale-pr-label: "lifecycle/stale"
+          exempt-pr-labels: "lifecycle/frozen"
 
           days-before-stale: 90
           days-before-close: 30
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 9829f8a8..1abff075 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,76 +1,82 @@
 ---
 project_name: govmomi
 builds:
-- id: govc
-  goos:
-    - linux
-    - darwin
-    - windows
-    - freebsd
-  goarch:
-    - amd64
-    - 386
-    - arm64
-    - mips64le
-  env:
-    - CGO_ENABLED=0
-  main: ./govc/main.go
-  binary: govc
-  flags: -compiler gc
-  ldflags: -X github.com/vmware/govmomi/govc/flags.GitVersion={{.Version}}
-- id: vcsim
-  goos:
-    - linux
-    - darwin
-    - windows
-    - freebsd
-  goarch:
-    - amd64
-    - 386
-    - arm64
-    - mips64le
-  env:
-    - CGO_ENABLED=0
-  main: ./vcsim/main.go
-  binary: vcsim
-  flags: -compiler gc
-  ldflags: -X github.com/vmware/govmomi/vcsim/flags.GitVersion={{.Version}}
+  - id: govc
+    goos: &goos-defs
+      - linux
+      - darwin
+      - windows
+      - freebsd
+    goarch: &goarch-defs
+      - amd64
+      - arm
+      - arm64
+      - mips64le
+    env:
+      - CGO_ENABLED=0
+      - PKGPATH=github.com/vmware/govmomi/govc/flags
+    flags:
+      - "-mod=vendor"
+    main: ./govc/main.go
+    binary: govc
+    ldflags:
+      - "-X {{.Env.PKGPATH}}.BuildVersion={{.Version}} -X {{.Env.PKGPATH}}.BuildCommit={{.ShortCommit}} -X {{.Env.PKGPATH}}.BuildDate={{.Date}}"
+  - id: vcsim
+    goos: *goos-defs
+    goarch: *goarch-defs
+    env:
+      - CGO_ENABLED=0
+    flags:
+      - "-mod=vendor"
+    main: ./vcsim/main.go
+    binary: vcsim
+    ldflags:
+      - "-X main.buildVersion={{.Version}} -X main.buildCommit={{.ShortCommit}} -X main.buildDate={{.Date}}"
 archives:
-- id: govcbuild
-  builds: ['govc']
-  name_template: 'govc_{{ .Os }}_{{ .Arch }}'
-  format: gz
-  format_overrides:
-    - goos: windows
-      format: zip
-  files:
-  - none*
-- id: vcsimbuild
-  builds: ['vcsim']
-  name_template: 'vcsim_{{ .Os }}_{{ .Arch }}'
-  format: gz
-  format_overrides:
-    - goos: windows
-      format: zip
-  files:
-  - none*
+  - id: govcbuild
+    builds:
+      - govc
+    name_template: "govc_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
+    replacements: &replacements
+      darwin: Darwin
+      linux: Linux
+      windows: Windows
+      freebsd: FreeBSD
+      amd64: x86_64
+    format_overrides:
+      - goos: windows
+        format: zip
+  - id: vcsimbuild
+    builds:
+      - vcsim
+    name_template: "vcsim_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
+    replacements: *replacements
+    format_overrides:
+      - goos: windows
+        format: zip
+snapshot:
+  name_template: "{{ .Tag }}-next"
 checksum:
-  name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
+  name_template: "checksums.txt"
 changelog:
   sort: asc
   filters:
     exclude:
-      - '^docs:'
-      - '^test:'
+      - "^docs:"
+      - "^test:"
       - Merge pull request
       - Merge branch
 brews:
   - name: govc
     ids:
-      - govc
+      - govcbuild
     tap:
       owner: govmomi
       name: homebrew-tap
+      # TODO: create token in specified tap repo, add as secret to govmomi repo and reference in release workflow
+      # token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+    # enable once we do fully automated releases
+    skip_upload: true
     commit_author:
       name: Alfred the Narwhal
       email: cna-alfred@vmware.com
@@ -83,10 +89,14 @@ brews:
       bin.install "govc"
   - name: vcsim
     ids:
-      - vcsim
+      - vcsimbuild
     tap:
       owner: govmomi
       name: homebrew-tap
+      # TODO: create token in specified tap repo, add as secret to govmomi repo and reference in release workflow
+      # token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+    # enable once we do fully automated releases
+    skip_upload: true
     commit_author:
       name: Alfred the Narwhal
       email: cna-alfred@vmware.com
@@ -98,23 +108,33 @@ brews:
     install: |
       bin.install "vcsim"
 dockers:
-- image_templates: 
-  - "vmware/govc:{{ .Tag }}"
-  - "vmware/govc:v{{ .Major }}"
-  - "vmware/govc:v{{ .Major }}.{{ .Minor }}"
-  - "vmware/govc:latest"
-  goos: linux
-  goarch: amd64
-  dockerfile: Dockerfile.govc
-  binaries: 
-  - govc
-- image_templates: 
-  - "vmware/vcsim:{{ .Tag }}"
-  - "vmware/vcsim:v{{ .Major }}"
-  - "vmware/vcsim:v{{ .Major }}.{{ .Minor }}"
-  - "vmware/vcsim:latest"
-  goos: linux
-  goarch: amd64
-  dockerfile: Dockerfile.vcsim
-  binaries: 
-  - vcsim
+  - image_templates:
+      - "vmware/govc:{{ .Tag }}"
+      - "vmware/govc:{{ .ShortCommit }}"
+      - "vmware/govc:latest"
+    dockerfile: Dockerfile.govc
+    ids:
+      - govc
+    build_flag_templates:
+      - "--pull"
+      - "--label=org.opencontainers.image.created={{.Date}}"
+      - "--label=org.opencontainers.image.title={{.ProjectName}}"
+      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+      - "--label=org.opencontainers.image.version={{.Version}}"
+      - "--label=org.opencontainers.image.url=https://github.com/vmware/govmomi"
+      - "--platform=linux/amd64"
+  - image_templates:
+      - "vmware/vcsim:{{ .Tag }}"
+      - "vmware/vcsim:{{ .ShortCommit }}"
+      - "vmware/vcsim:latest"
+    dockerfile: Dockerfile.vcsim
+    ids:
+      - vcsim
+    build_flag_templates:
+      - "--pull"
+      - "--label=org.opencontainers.image.created={{.Date}}"
+      - "--label=org.opencontainers.image.title={{.ProjectName}}"
+      - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+      - "--label=org.opencontainers.image.version={{.Version}}"
+      - "--label=org.opencontainers.image.url=https://github.com/vmware/govmomi"
+      - "--platform=linux/amd64"
diff --git a/Dockerfile.vcsim b/Dockerfile.vcsim
index d9863073..31f48a97 100644
--- a/Dockerfile.vcsim
+++ b/Dockerfile.vcsim
@@ -43,4 +43,4 @@ EXPOSE 8989
 COPY vcsim /vcsim
 
 # Set entrypoint to application with container defaults
-ENTRYPOINT ["/vcsim", "-l", "0.0.0.0:8989"]
+CMD ["/vcsim", "-l", "0.0.0.0:8989"]
diff --git a/Makefile b/Makefile
index fc0af800..38e3b5b8 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ install:
 	$(MAKE) -C vcsim install
 
 go-test:
-	GORACE=history_size=5 $(GO) test -timeout 5m -count 1 -race -v $(TEST_OPTS) ./...
+	GORACE=history_size=5 $(GO) test -mod=vendor -timeout 5m -count 1 -race -v $(TEST_OPTS) ./...
 
 govc-test: install
 	./govc/test/images/update.sh
diff --git a/README.md b/README.md
index 2381dc08..1210c336 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-[![Build](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-build.yaml/badge.svg)](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-build.yaml)
-[![Test](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-test.yaml/badge.svg)](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-test.yaml)
+[![Build](https://github.com/vmware/govmomi/actions/workflows/govmomi-build.yaml/badge.svg)](https://github.com/vmware/govmomi/actions/workflows/govmomi-build.yaml)
+[![Tests](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-tests.yaml/badge.svg)](https://github.com/vmware/govmomi/actions/workflows/govmomi-go-tests.yaml)
 [![Go Report Card](https://goreportcard.com/badge/github.com/vmware/govmomi)](https://goreportcard.com/report/github.com/vmware/govmomi)
 [![Latest Release](https://img.shields.io/github/release/vmware/govmomi.svg?logo=github&style=flat-square)](https://github.com/vmware/govmomi/releases/latest)
 [![Go Reference](https://pkg.go.dev/badge/github.com/vmware/govmomi.svg)](https://pkg.go.dev/github.com/vmware/govmomi)
diff --git a/go.mod b/go.mod
index f49b3c1f..4619b312 100644
--- a/go.mod
+++ b/go.mod
@@ -13,4 +13,4 @@ require (
 	github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728
 )
 
-go 1.13
+go 1.14
diff --git a/govc/USAGE.md b/govc/USAGE.md
index 6bd3cc12..a7353dbd 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -4827,7 +4827,9 @@ Options:
 Usage: govc version [OPTIONS]
 
 Options:
-  -require=  Require govc version >= this value
+  -require=     Require govc version >= this value
+  -l=false      Print detailed govc version information
+information
 ```
 
 ## vm.change
diff --git a/govc/build.sh b/govc/build.sh
index 911fcee3..0c34df3a 100755
--- a/govc/build.sh
+++ b/govc/build.sh
@@ -1,5 +1,7 @@
 #!/bin/bash -e
 
+# TODO: deprecate this script as it will be superseded by goreleaser and misses additional build variables defined in
+# flags
 git_version=$(git describe --dirty)
  if [[ $git_version == *-dirty ]] ; then
   echo 'Working tree is dirty.'
@@ -12,7 +14,7 @@ PROGRAM_NAME=govc
 PROJECT_PKG="github.com/vmware/govmomi"
 PROGRAM_PKG="${PROJECT_PKG}/${PROGRAM_NAME}"
 
-export LDFLAGS="-w -X ${PROGRAM_PKG}/flags.GitVersion=${git_version}"
+export LDFLAGS="-w -X ${PROGRAM_PKG}/flags.BuildVersion=${git_version}"
 export BUILD_OS="${BUILD_OS:-darwin linux windows freebsd}"
 export BUILD_ARCH="${BUILD_ARCH:-amd64}"
 
diff --git a/govc/flags/client.go b/govc/flags/client.go
index c19a70b1..d410e6d1 100644
--- a/govc/flags/client.go
+++ b/govc/flags/client.go
@@ -277,7 +277,7 @@ func (flag *ClientFlag) ConfigureTLS(sc *soap.Client) error {
 	sc.Namespace = "urn:" + flag.vimNamespace
 	sc.Version = flag.vimVersion
 
-	sc.UserAgent = fmt.Sprintf("govc/%s", Version)
+	sc.UserAgent = fmt.Sprintf("govc/%s", strings.TrimPrefix(BuildVersion, "v"))
 
 	if err := flag.SetRootCAs(sc); err != nil {
 		return err
diff --git a/govc/flags/version.go b/govc/flags/version.go
index 807e2671..72d0772b 100644
--- a/govc/flags/version.go
+++ b/govc/flags/version.go
@@ -21,14 +21,19 @@ import (
 	"strings"
 )
 
-const Version = "0.24.0"
-
-var GitVersion string
+var (
+	BuildVersion = "v0.0.0" // govc-test requires an (arbitrary) version set
+	BuildCommit  string
+	BuildDate    string
+)
 
 type version []int
 
 func ParseVersion(s string) (version, error) {
+	// remove any trailing "v" version identifier
+	s = strings.TrimPrefix(s, "v")
 	v := make(version, 0)
+
 	ds := strings.Split(s, "-")
 	ps := strings.Split(ds[0], ".")
 	for _, p := range ps {
diff --git a/govc/version/command.go b/govc/version/command.go
index f75b9b39..d8e1009e 100644
--- a/govc/version/command.go
+++ b/govc/version/command.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"flag"
 	"fmt"
+	"strings"
 
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
@@ -29,6 +30,7 @@ type version struct {
 	*flags.EmptyFlag
 
 	require string
+	long    bool // detailed govc version output
 }
 
 func init() {
@@ -37,14 +39,11 @@ func init() {
 
 func (cmd *version) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.require, "require", "", "Require govc version >= this value")
+	f.BoolVar(&cmd.long, "l", false, "Print detailed govc version information")
 }
 
 func (cmd *version) Run(ctx context.Context, f *flag.FlagSet) error {
-	ver := flags.GitVersion
-	if ver == "" {
-		ver = flags.Version
-	}
-
+	ver := strings.TrimPrefix(flags.BuildVersion, "v")
 	if cmd.require != "" {
 		v, err := flags.ParseVersion(ver)
 		if err != nil {
@@ -61,7 +60,13 @@ func (cmd *version) Run(ctx context.Context, f *flag.FlagSet) error {
 		}
 	}
 
-	fmt.Printf("govc %s\n", ver)
+	if cmd.long {
+		fmt.Printf("Build Version: %s\n", flags.BuildVersion)
+		fmt.Printf("Build Commit: %s\n", flags.BuildCommit)
+		fmt.Printf("Build Date: %s\n", flags.BuildDate)
+	} else {
+		fmt.Printf("govc %s\n", ver)
+	}
 
 	return nil
 }
diff --git a/program.mk b/program.mk
index c5f65319..367c41b7 100644
--- a/program.mk
+++ b/program.mk
@@ -24,7 +24,7 @@ TAGS += netgo
 ifeq (,$(strip $(findstring -w,$(LDFLAGS))))
 LDFLAGS += -w
 endif
-BUILD_ARGS := -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -v
+BUILD_ARGS := -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -mod=vendor -v
 
 $(PROGRAM):
 	CGO_ENABLED=0 go build -a $(BUILD_ARGS) -o $@
diff --git a/simulator/folder.go b/simulator/folder.go
index 20ad65e6..a0a4ecc7 100644
--- a/simulator/folder.go
+++ b/simulator/folder.go
@@ -266,7 +266,7 @@ func (p *StoragePod) MoveIntoFolderTask(ctx *Context, c *types.MoveIntoFolder_Ta
 		f := &Folder{Folder: p.Folder}
 		id := f.MoveIntoFolderTask(ctx, c).(*methods.MoveIntoFolder_TaskBody).Res.Returnval
 		ftask := Map.Get(id).(*Task)
-		ftask.Wait()
+		ftask.wait()
 		if ftask.Info.Error != nil {
 			return nil, ftask.Info.Error.Fault
 		}
@@ -688,7 +688,7 @@ func (f *Folder) DestroyTask(ctx *Context, req *types.Destroy_Task) soap.HasFaul
 				}).(*methods.Destroy_TaskBody).Res.Returnval
 
 				t := Map.Get(id).(*Task)
-				t.Wait()
+				t.wait()
 				if t.Info.Error != nil {
 					fault = t.Info.Error.Fault // For example, can't destroy a powered on VM
 				}
diff --git a/simulator/resource_pool.go b/simulator/resource_pool.go
index 7df6e577..05840619 100644
--- a/simulator/resource_pool.go
+++ b/simulator/resource_pool.go
@@ -225,7 +225,7 @@ func (p *ResourcePool) ImportVApp(ctx *Context, req *types.ImportVApp) soap.HasF
 	})
 
 	ctask := Map.Get(res.(*methods.CreateVM_TaskBody).Res.Returnval).(*Task)
-	ctask.Wait()
+	ctask.wait()
 
 	if ctask.Info.Error != nil {
 		body.Fault_ = Fault("", ctask.Info.Error.Fault)
@@ -406,7 +406,7 @@ func (a *VirtualApp) CloneVAppTask(ctx *Context, req *types.CloneVApp_Task) soap
 			})
 
 			ctask := Map.Get(res.(*methods.CloneVM_TaskBody).Res.Returnval).(*Task)
-			ctask.Wait()
+			ctask.wait()
 			if ctask.Info.Error != nil {
 				return nil, ctask.Info.Error.Fault
 			}
diff --git a/simulator/task.go b/simulator/task.go
index fb206892..fa969f8c 100644
--- a/simulator/task.go
+++ b/simulator/task.go
@@ -138,11 +138,10 @@ func (t *Task) Run(ctx *Context) types.ManagedObjectReference {
 // It polls for task completion to avoid the need to set up a PropertyCollector.
 func (t *Task) RunBlocking(ctx *Context) {
 	_ = t.Run(ctx)
-	t.Wait()
+	t.wait()
 }
 
-// Wait blocks until the task is complete.
-func (t *Task) Wait() {
+func (t *Task) wait() {
 	// we do NOT want to share our lock with the tasks's context, because
 	// the goroutine that executes the task will use ctx to update the
 	// state (among other things).
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 4403426b..4bd639f2 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -1251,7 +1251,7 @@ func (vm *VirtualMachine) removeDevice(ctx *Context, devices object.VirtualDevic
 						Datacenter: &dc.Self,
 					})
 					ctask := Map.Get(res.(*methods.DeleteVirtualDisk_TaskBody).Res.Returnval).(*Task)
-					ctask.Wait()
+					ctask.wait()
 				}
 			}
 			Map.Update(vm, []types.PropertyChange{
@@ -1511,14 +1511,14 @@ func (vm *VirtualMachine) ResetVMTask(ctx *Context, req *types.ResetVM_Task) soa
 	task := CreateTask(vm, "reset", func(task *Task) (types.AnyType, types.BaseMethodFault) {
 		res := vm.PowerOffVMTask(ctx, &types.PowerOffVM_Task{This: vm.Self})
 		ctask := Map.Get(res.(*methods.PowerOffVM_TaskBody).Res.Returnval).(*Task)
-		ctask.Wait()
+		ctask.wait()
 		if ctask.Info.Error != nil {
 			return nil, ctask.Info.Error.Fault
 		}
 
 		res = vm.PowerOnVMTask(ctx, &types.PowerOnVM_Task{This: vm.Self})
 		ctask = Map.Get(res.(*methods.PowerOnVM_TaskBody).Res.Returnval).(*Task)
-		ctask.Wait()
+		ctask.wait()
 
 		return nil, nil
 	})
@@ -1773,7 +1773,7 @@ func (vm *VirtualMachine) CloneVMTask(ctx *Context, req *types.CloneVM_Task) soa
 			return nil, ctask.Info.Error.Fault
 		}
 
-		ctask.Wait()
+		ctask.wait()
 
 		ref := ctask.Info.Result.(types.ManagedObjectReference)
 		clone := Map.Get(ref).(*VirtualMachine)
diff --git a/vcsim/README.md b/vcsim/README.md
index 5e6a3a0c..eb642268 100644
--- a/vcsim/README.md
+++ b/vcsim/README.md
@@ -52,6 +52,10 @@ Usage of vcsim:
 
 [model]:https://godoc.org/github.com/vmware/govmomi/simulator#Model
 
+### Version Information
+
+To print detailed (build) information for vcsim run: `vcsim version`. 
+
 ## Examples
 
 The following examples illustrate how **vcsim** flags can be used to change the
diff --git a/vcsim/main.go b/vcsim/main.go
index 53253ba0..d927e4d8 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -49,6 +49,12 @@ import (
 	_ "github.com/vmware/govmomi/vsan/simulator"
 )
 
+var (
+	buildVersion string
+	buildCommit  string
+	buildDate    string
+)
+
 func main() {
 	model := simulator.VPX()
 
@@ -107,7 +113,12 @@ func main() {
 	switch flag.Arg(0) {
 	case "uuidgen": // util-linux not installed on Travis CI
 		fmt.Println(uuid.New().String())
-		return
+		os.Exit(0)
+	case "version":
+		fmt.Printf("Build Version: %s\n", buildVersion)
+		fmt.Printf("Build Commit: %s\n", buildCommit)
+		fmt.Printf("Build Date: %s\n", buildDate)
+		os.Exit(0)
 	}
 
 	if methodDelay != "" {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index eb86d181..13330543 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,14 +1,21 @@
 # github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d
+## explicit
 github.com/a8m/tree
 # github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 => github.com/rasky/go-xdr v0.0.0-20170217172119-4930550ba2e2
+## explicit
 github.com/davecgh/go-xdr/xdr2
 # github.com/google/uuid v0.0.0-20170306145142-6a5e28554805
+## explicit
 github.com/google/uuid
 # github.com/kr/pretty v0.1.0 => github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02
+## explicit
 github.com/kr/pretty
 # github.com/kr/text v0.1.0
+## explicit
 github.com/kr/text
 # github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728
+## explicit
 github.com/vmware/vmw-guestinfo/bdoor
 github.com/vmware/vmw-guestinfo/message
 github.com/vmware/vmw-guestinfo/vmcheck
+# github.com/davecgh/go-xdr => github.com/rasky/go-xdr v0.0.0-20170217172119-4930550ba2e2


Test patch:
diff --git a/.github/workflows/govmomi-go-test.yaml b/.github/workflows/govmomi-go-tests.yaml
similarity index 80%
rename from .github/workflows/govmomi-go-test.yaml
rename to .github/workflows/govmomi-go-tests.yaml
index 286c58fe..905013cc 100644
--- a/.github/workflows/govmomi-go-test.yaml
+++ b/.github/workflows/govmomi-go-tests.yaml
@@ -12,7 +12,7 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-name: Test
+name: Unit Tests
 
 on:
   push:
@@ -22,15 +22,15 @@ on:
     branches: [ 'main', 'master' ]
 
 jobs:
-
-  test:
-    name: Unit Tests
+  go-tests:
+    name: Run Unit Tests
     strategy:
       matrix:
-        go-version: [ '1.15', '1.16' ]
-        platform: [ 'ubuntu-latest' ]
+        go-version: ["1.15", "1.16"]
+        platform: ["ubuntu-latest"]
 
     runs-on: ${{ matrix.platform }}
+    timeout-minutes: 10
 
     steps:
       - name: Set up Go ${{ matrix.go-version }}
@@ -45,6 +45,5 @@ jobs:
       - name: Run Tests
         env:
           TIMEOUT: 5m
-          GORACE: history_size=5
-          TEST_OPTS: ''
-        run: go test -timeout $TIMEOUT -race -v $TEST_OPTS ./...
+          TEST_OPTS: ""
+        run: go test -mod=vendor -timeout $TIMEOUT -count 1 -race -v $TEST_OPTS ./...
diff --git a/govc/flags/version_test.go b/govc/flags/version_test.go
index d45173d3..fc6957dd 100644
--- a/govc/flags/version_test.go
+++ b/govc/flags/version_test.go
@@ -16,25 +16,38 @@ limitations under the License.
 
 package flags
 
-import "testing"
+import (
+	"reflect"
+	"testing"
+)
 
 func TestParseVersion(t *testing.T) {
-	var v version
-	var err error
-
-	v, err = ParseVersion("5.5.5.5")
-	if err != nil {
-		t.Error(err)
+	type args struct {
+		version string
 	}
-
-	if len(v) != 4 {
-		t.Errorf("Expected %d elements, got %d", 4, len(v))
+	tests := []struct {
+		name    string
+		args    args
+		want    version
+		wantErr bool
+	}{
+		{name: "5.5.5.5", args: args{version: "5.5.5.5"}, want: version{5, 5, 5, 5}, wantErr: false},
+		{name: "0.24.0", args: args{version: "0.24.0"}, want: version{0, 24, 0}, wantErr: false},
+		{name: "v0.24.0", args: args{version: "v0.24.0"}, want: version{0, 24, 0}, wantErr: false},
+		{name: "v0.24.0-next", args: args{version: "v0.24.0-next"}, want: version{0, 24, 0}, wantErr: false},
+		{name: "0.10x", args: args{version: "0.10x"}, want: nil, wantErr: true},
 	}
-
-	for i := 0; i < len(v); i++ {
-		if v[i] != 5 {
-			t.Errorf("Expected %d, got %d", 5, v[i])
-		}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ParseVersion(tt.args.version)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseVersion() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ParseVersion() got = %v, want %v", got, tt.want)
+			}
+		})
 	}
 }
 
diff --git a/govc/test/vcsim.bats b/govc/test/vcsim.bats
index a72c71a2..6ba702ec 100755
--- a/govc/test/vcsim.bats
+++ b/govc/test/vcsim.bats
@@ -20,7 +20,7 @@ load test_helper
   server=$(govc env -x GOVC_URL_HOST)
   port=$(govc env -x GOVC_URL_PORT)
 
-  docker run --rm vmware/powerclicore /usr/bin/pwsh -f - <<EOF
+  docker run --rm projects.registry.vmware.com/pez/powerclicore@sha256:09b29f69c0653f871f6d569f7c4c03c952909f68a27e9792ef2f7c8653235668 /usr/bin/pwsh -f - <<EOF
 Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -confirm:\$false | Out-Null
 Connect-VIServer -Server $server -Port $port -User user -Password pass
 
@@ -301,7 +301,7 @@ docker_name() {
   assert_success
 
   ip=$(govc object.collect -s vm/$vm guest.ipAddress)
-  run docker run --rm curlimages/curl curl -f "http://$ip/vcsim.bats"
+  run curl -f "http://$ip/vcsim.bats"
   assert_success
 
   # test suspend/resume
diff --git a/simulator/entity_test.go b/simulator/entity_test.go
index e708b57c..2fb75332 100644
--- a/simulator/entity_test.go
+++ b/simulator/entity_test.go
@@ -60,7 +60,7 @@ func TestRename(t *testing.T) {
 		}).(*methods.Rename_TaskBody).Res.Returnval
 
 		task := Map.Get(id).(*Task)
-		task.Wait()
+		task.wait()
 
 		if task.Info.State != expect {
 			t.Errorf("state=%s", task.Info.State)
diff --git a/simulator/task_test.go b/simulator/task_test.go
index 66257258..502b2ff4 100644
--- a/simulator/task_test.go
+++ b/simulator/task_test.go
@@ -58,7 +58,7 @@ func TestNewTask(t *testing.T) {
 	add.fault = &types.ManagedObjectNotFound{}
 
 	task.Run(SpoofContext())
-	task.Wait()
+	task.wait()
 
 	if info.State != types.TaskInfoStateError {
 		t.Fail()