detached ebd2ab3

Merge pull request #75 from bnema/feat/push-version-from-tag-refs

feat(push): derive VERSION from CI tag refs and git describe
commit
ebd2ab31128901c0731143862bc0509060b4c70e
author
bnema · · 2026-02-11 20:55 UTC
committer
GitHub · · 2026-02-11 20:55 UTC
merge of
95fe066 + cad4385 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v2.16.0
4 files · +138 -23
M docs/cli/push.md +5 -4
@@ -24,7 +24,7 @@ gordon push <domain> [options]
2424 | `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) |
2525 | `--platform` | Target platform for buildx (default: `linux/amd64`) |
2626 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |
27-| `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) |
27+| `--tag` | Override version tag (default: tag ref from CI, then `git describe --tags --dirty`) |
2828 | `--no-confirm` | Skip deploy confirmation prompt |
2929 | `--no-deploy` | Push only; skip deployment prompt |
3030 | `--remote` | Remote Gordon URL |
@@ -36,10 +36,11 @@ gordon push <domain> [options]
3636 Gordon registry, pushes it, and optionally deploys it.
3737
3838 - The registry and repository are derived from the route image on the server.
39-- The version tag defaults to the latest git tag. If no tag is found, `latest`
40- is used.
39+- The version tag defaults to a CI tag ref (like `refs/tags/v1.2.3`) when available,
40+ then falls back to `git describe --tags --dirty` (for example
41+ `v1.2.3-4-gabc1234` or `v1.2.3-dirty`). If no tag is found, `latest` is used.
4142 - When `--build` is set, the command builds with `docker buildx build --load`
42- and injects `VERSION=<tag>` plus any `--build-arg` values.
43+ and injects `VERSION` into the build environment plus any `--build-arg` values.
4344 - Use `-f/--file` to build from a Dockerfile outside the current directory root.
4445 - The version tag and `latest` are both pushed (unless the version is `latest`).
4546
@@ -24,7 +24,7 @@ gordon push <domain> [options]
24 | `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) |24 | `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) |
25 | `--platform` | Target platform for buildx (default: `linux/amd64`) |25 | `--platform` | Target platform for buildx (default: `linux/amd64`) |
26 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |26 | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) |
27-| `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) |27+| `--tag` | Override version tag (default: tag ref from CI, then `git describe --tags --dirty`) |
28 | `--no-confirm` | Skip deploy confirmation prompt |28 | `--no-confirm` | Skip deploy confirmation prompt |
29 | `--no-deploy` | Push only; skip deployment prompt |29 | `--no-deploy` | Push only; skip deployment prompt |
30 | `--remote` | Remote Gordon URL |30 | `--remote` | Remote Gordon URL |
@@ -36,10 +36,11 @@ gordon push <domain> [options]
36 Gordon registry, pushes it, and optionally deploys it.36 Gordon registry, pushes it, and optionally deploys it.
37 37
38 - The registry and repository are derived from the route image on the server.38 - The registry and repository are derived from the route image on the server.
39-- The version tag defaults to the latest git tag. If no tag is found, `latest`39+- The version tag defaults to a CI tag ref (like `refs/tags/v1.2.3`) when available,
40- is used.40+ then falls back to `git describe --tags --dirty` (for example
41+ `v1.2.3-4-gabc1234` or `v1.2.3-dirty`). If no tag is found, `latest` is used.
41 - When `--build` is set, the command builds with `docker buildx build --load`42 - When `--build` is set, the command builds with `docker buildx build --load`
42- and injects `VERSION=<tag>` plus any `--build-arg` values.43+ and injects `VERSION` into the build environment plus any `--build-arg` values.
43 - Use `-f/--file` to build from a Dockerfile outside the current directory root.44 - Use `-f/--file` to build from a Dockerfile outside the current directory root.
44 - The version tag and `latest` are both pushed (unless the version is `latest`).45 - The version tag and `latest` are both pushed (unless the version is `latest`).
45 46
M docs/getting-started.md +20 -15
@@ -124,31 +124,38 @@ sudo loginctl enable-linger $USER
124124
125125 ## 7. Generate a Deploy Token
126126
127-Create a token for pushing images to your registry (skip if auth is disabled):
127+Create a token for remote CLI deploys (skip if auth is disabled):
128128
129129 ```bash
130130 gordon auth token generate --subject deploy --scopes push,pull --expiry 0
131131 ```
132132
133-Save this token securely - you'll need it for Docker login.
133+`--expiry 0` creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.
134+
135+Save this token securely - you'll use it with `gordon --remote` (or a saved remote), and it should be limited to remote deploy use.
134136
135137 ## 8. Deploy Your First App
136138
137139 On your local machine:
138140
139141 ```bash
140-# Log in to your Gordon registry (skip if auth is disabled)
141-docker login -u deploy -p <your-token> gordon.mydomain.com
142+# Save and select your Gordon remote (one-time)
143+gordon remotes add prod https://gordon.mydomain.com --token <your-token>
144+gordon remotes use prod
142145
143-# Build your app
144-docker build -t myapp .
146+# Build, push, and deploy through Gordon
147+gordon push myapp --build --no-confirm
148+```
145149
146-# Tag for your registry
147-docker tag myapp gordon.mydomain.com/myapp:latest
150+What this command does:
148151
149-# Push to Gordon
150-docker push gordon.mydomain.com/myapp:latest
151-```
152+- `gordon push myapp` resolves the route for `myapp`, chooses a version tag,
153+ pushes image tags to Gordon's registry, and triggers deploy.
154+- `--build` runs `docker buildx build` first, injecting `VERSION` from the tag
155+ into the build and then pushing both version and `latest` tags. This requires
156+ Docker with Buildx; a Podman-only setup is not supported for `--build`.
157+- `--no-confirm` skips the interactive "Deploy now?" prompt so the deploy runs
158+ immediately.
152159
153160 Your app is now live at `https://app.mydomain.com`!
154161
@@ -157,10 +164,8 @@ Your app is now live at `https://app.mydomain.com`!
157164 Push a new image to deploy with zero downtime:
158165
159166 ```bash
160-# Make changes, rebuild
161-docker build -t myapp .
162-docker tag myapp gordon.mydomain.com/myapp:latest
163-docker push gordon.mydomain.com/myapp:latest
167+# Make changes, then build + push + deploy
168+gordon push myapp --build --no-confirm
164169 ```
165170
166171 Gordon automatically:
@@ -124,31 +124,38 @@ sudo loginctl enable-linger $USER
124 124
125 ## 7. Generate a Deploy Token125 ## 7. Generate a Deploy Token
126 126
127-Create a token for pushing images to your registry (skip if auth is disabled):127+Create a token for remote CLI deploys (skip if auth is disabled):
128 128
129 ```bash129 ```bash
130 gordon auth token generate --subject deploy --scopes push,pull --expiry 0130 gordon auth token generate --subject deploy --scopes push,pull --expiry 0
131 ```131 ```
132 132
133-Save this token securely - you'll need it for Docker login.133+`--expiry 0` creates a non-expiring token. Prefer a finite expiry and a rotation policy unless you explicitly need a long-lived deploy token.
134+
135+Save this token securely - you'll use it with `gordon --remote` (or a saved remote), and it should be limited to remote deploy use.
134 136
135 ## 8. Deploy Your First App137 ## 8. Deploy Your First App
136 138
137 On your local machine:139 On your local machine:
138 140
139 ```bash141 ```bash
140-# Log in to your Gordon registry (skip if auth is disabled)142+# Save and select your Gordon remote (one-time)
141-docker login -u deploy -p <your-token> gordon.mydomain.com143+gordon remotes add prod https://gordon.mydomain.com --token <your-token>
144+gordon remotes use prod
142 145
143-# Build your app146+# Build, push, and deploy through Gordon
144-docker build -t myapp .147+gordon push myapp --build --no-confirm
148+```
145 149
146-# Tag for your registry150+What this command does:
147-docker tag myapp gordon.mydomain.com/myapp:latest
148 151
149-# Push to Gordon152+- `gordon push myapp` resolves the route for `myapp`, chooses a version tag,
150-docker push gordon.mydomain.com/myapp:latest153+ pushes image tags to Gordon's registry, and triggers deploy.
151-```154+- `--build` runs `docker buildx build` first, injecting `VERSION` from the tag
155+ into the build and then pushing both version and `latest` tags. This requires
156+ Docker with Buildx; a Podman-only setup is not supported for `--build`.
157+- `--no-confirm` skips the interactive "Deploy now?" prompt so the deploy runs
158+ immediately.
152 159
153 Your app is now live at `https://app.mydomain.com`!160 Your app is now live at `https://app.mydomain.com`!
154 161
@@ -157,10 +164,8 @@ Your app is now live at `https://app.mydomain.com`!
157 Push a new image to deploy with zero downtime:164 Push a new image to deploy with zero downtime:
158 165
159 ```bash166 ```bash
160-# Make changes, rebuild167+# Make changes, then build + push + deploy
161-docker build -t myapp .168+gordon push myapp --build --no-confirm
162-docker tag myapp gordon.mydomain.com/myapp:latest
163-docker push gordon.mydomain.com/myapp:latest
164 ```169 ```
165 170
166 Gordon automatically:171 Gordon automatically:
M internal/adapters/in/cli/push.go +46 -4
@@ -66,7 +66,7 @@ Examples:
6666 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")
6767 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")
6868 cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")
69- cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)")
69+ cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: CI tag ref or git describe)")
7070 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")
7171 cmd.Flags().StringVar(&domainFlag, "domain", "", "Explicit domain override (legacy mode)")
7272
@@ -403,6 +403,9 @@ func parseLabelPair(pair string) (key, value string, ok bool) {
403403
404404 func determineVersion(ctx context.Context, tag string) string {
405405 version := tag
406+ if version == "" {
407+ version = versionFromTagRefs(os.Getenv)
408+ }
406409 if version == "" {
407410 version = getGitVersion(ctx)
408411 }
@@ -412,6 +415,44 @@ func determineVersion(ctx context.Context, tag string) string {
412415 return version
413416 }
414417
418+func versionFromTagRefs(getenv func(string) string) string {
419+ if ref := strings.TrimSpace(getenv("GITHUB_REF")); ref != "" {
420+ if tag := parseTagRef(ref); tag != "" {
421+ return tag
422+ }
423+ }
424+
425+ if strings.TrimSpace(getenv("GITHUB_REF_TYPE")) == "tag" {
426+ if tag := strings.TrimSpace(getenv("GITHUB_REF_NAME")); tag != "" {
427+ return tag
428+ }
429+ }
430+
431+ if tag := strings.TrimSpace(getenv("CI_COMMIT_TAG")); tag != "" {
432+ return tag
433+ }
434+
435+ if ref := strings.TrimSpace(getenv("BUILD_SOURCEBRANCH")); ref != "" {
436+ if tag := parseTagRef(ref); tag != "" {
437+ return tag
438+ }
439+ }
440+
441+ return ""
442+}
443+
444+func parseTagRef(ref string) string {
445+ ref = strings.TrimSpace(strings.TrimSuffix(ref, "^{}"))
446+ if !strings.HasPrefix(ref, "refs/tags/") {
447+ return ""
448+ }
449+ tag := strings.TrimPrefix(ref, "refs/tags/")
450+ if tag == "" {
451+ return ""
452+ }
453+ return tag
454+}
455+
415456 func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
416457 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
417458 return fmt.Errorf("dockerfile not found: %s", dockerfile)
@@ -423,6 +464,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
423464 // using docker push gives us chunked uploads (~5MB per request).
424465 fmt.Println("\nBuilding image...")
425466 buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204
467+ buildCmd.Env = append(os.Environ(), "VERSION="+version)
426468 buildCmd.Stdout = os.Stdout
427469 buildCmd.Stderr = os.Stderr
428470 if err := buildCmd.Run(); err != nil {
@@ -451,7 +493,7 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve
451493 "--platform", platform,
452494 "-f", dockerfile,
453495 "-t", latestRef,
454- "--build-arg", "VERSION=" + version,
496+ "--build-arg", "VERSION",
455497 }
456498 if version != "latest" {
457499 args = append(args, "-t", versionRef)
@@ -639,9 +681,9 @@ func parseImageRef(image string) (registry, name, tag string) {
639681 return
640682 }
641683
642-// getGitVersion returns the latest git tag, or empty string if unavailable.
684+// getGitVersion returns git describe output, or empty string if unavailable.
643685 func getGitVersion(ctx context.Context) string {
644- out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--abbrev=0").Output()
686+ out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output()
645687 if err != nil {
646688 return ""
647689 }
@@ -66,7 +66,7 @@ Examples:
66 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")66 cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx")
67 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")67 cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)")
68 cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")68 cmd.Flags().StringVarP(&dockerfile, "file", "f", "", "Path to Dockerfile (default: ./Dockerfile, used with --build)")
69- cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)")69+ cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: CI tag ref or git describe)")
70 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")70 cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)")
71 cmd.Flags().StringVar(&domainFlag, "domain", "", "Explicit domain override (legacy mode)")71 cmd.Flags().StringVar(&domainFlag, "domain", "", "Explicit domain override (legacy mode)")
72 72
@@ -403,6 +403,9 @@ func parseLabelPair(pair string) (key, value string, ok bool) {
403 403
404 func determineVersion(ctx context.Context, tag string) string {404 func determineVersion(ctx context.Context, tag string) string {
405 version := tag405 version := tag
406+ if version == "" {
407+ version = versionFromTagRefs(os.Getenv)
408+ }
406 if version == "" {409 if version == "" {
407 version = getGitVersion(ctx)410 version = getGitVersion(ctx)
408 }411 }
@@ -412,6 +415,44 @@ func determineVersion(ctx context.Context, tag string) string {
412 return version415 return version
413 }416 }
414 417
418+func versionFromTagRefs(getenv func(string) string) string {
419+ if ref := strings.TrimSpace(getenv("GITHUB_REF")); ref != "" {
420+ if tag := parseTagRef(ref); tag != "" {
421+ return tag
422+ }
423+ }
424+
425+ if strings.TrimSpace(getenv("GITHUB_REF_TYPE")) == "tag" {
426+ if tag := strings.TrimSpace(getenv("GITHUB_REF_NAME")); tag != "" {
427+ return tag
428+ }
429+ }
430+
431+ if tag := strings.TrimSpace(getenv("CI_COMMIT_TAG")); tag != "" {
432+ return tag
433+ }
434+
435+ if ref := strings.TrimSpace(getenv("BUILD_SOURCEBRANCH")); ref != "" {
436+ if tag := parseTagRef(ref); tag != "" {
437+ return tag
438+ }
439+ }
440+
441+ return ""
442+}
443+
444+func parseTagRef(ref string) string {
445+ ref = strings.TrimSpace(strings.TrimSuffix(ref, "^{}"))
446+ if !strings.HasPrefix(ref, "refs/tags/") {
447+ return ""
448+ }
449+ tag := strings.TrimPrefix(ref, "refs/tags/")
450+ if tag == "" {
451+ return ""
452+ }
453+ return tag
454+}
455+
415 func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {456 func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error {
416 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {457 if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
417 return fmt.Errorf("dockerfile not found: %s", dockerfile)458 return fmt.Errorf("dockerfile not found: %s", dockerfile)
@@ -423,6 +464,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui
423 // using docker push gives us chunked uploads (~5MB per request).464 // using docker push gives us chunked uploads (~5MB per request).
424 fmt.Println("\nBuilding image...")465 fmt.Println("\nBuilding image...")
425 buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204466 buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204
467+ buildCmd.Env = append(os.Environ(), "VERSION="+version)
426 buildCmd.Stdout = os.Stdout468 buildCmd.Stdout = os.Stdout
427 buildCmd.Stderr = os.Stderr469 buildCmd.Stderr = os.Stderr
428 if err := buildCmd.Run(); err != nil {470 if err := buildCmd.Run(); err != nil {
@@ -451,7 +493,7 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve
451 "--platform", platform,493 "--platform", platform,
452 "-f", dockerfile,494 "-f", dockerfile,
453 "-t", latestRef,495 "-t", latestRef,
454- "--build-arg", "VERSION=" + version,496+ "--build-arg", "VERSION",
455 }497 }
456 if version != "latest" {498 if version != "latest" {
457 args = append(args, "-t", versionRef)499 args = append(args, "-t", versionRef)
@@ -639,9 +681,9 @@ func parseImageRef(image string) (registry, name, tag string) {
639 return681 return
640 }682 }
641 683
642-// getGitVersion returns the latest git tag, or empty string if unavailable.684+// getGitVersion returns git describe output, or empty string if unavailable.
643 func getGitVersion(ctx context.Context) string {685 func getGitVersion(ctx context.Context) string {
644- out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--abbrev=0").Output()686+ out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--dirty").Output()
645 if err != nil {687 if err != nil {
646 return ""688 return ""
647 }689 }
M internal/adapters/in/cli/push_test.go +67 -0
@@ -72,6 +72,73 @@ func TestBuildAndPush_BuildArgs(t *testing.T) {
7272 assert.Contains(t, args, "--platform")
7373 assert.Contains(t, args, "-f")
7474 assert.Contains(t, args, "Dockerfile")
75+ assert.Contains(t, args, "VERSION")
76+ assert.NotContains(t, args, "VERSION=v1.0.0")
77+}
78+
79+func TestParseTagRef(t *testing.T) {
80+ tests := []struct {
81+ name string
82+ ref string
83+ want string
84+ }{
85+ {name: "github tag ref", ref: "refs/tags/v1.2.3", want: "v1.2.3"},
86+ {name: "peeled tag ref", ref: "refs/tags/v1.2.3^{}", want: "v1.2.3"},
87+ {name: "branch ref", ref: "refs/heads/main", want: ""},
88+ {name: "empty", ref: "", want: ""},
89+ }
90+
91+ for _, tt := range tests {
92+ t.Run(tt.name, func(t *testing.T) {
93+ assert.Equal(t, tt.want, parseTagRef(tt.ref))
94+ })
95+ }
96+}
97+
98+func TestVersionFromTagRefs(t *testing.T) {
99+ tests := []struct {
100+ name string
101+ env map[string]string
102+ want string
103+ }{
104+ {
105+ name: "github ref tag",
106+ env: map[string]string{"GITHUB_REF": "refs/tags/v2.0.0"},
107+ want: "v2.0.0",
108+ },
109+ {
110+ name: "github ref type and name",
111+ env: map[string]string{
112+ "GITHUB_REF_TYPE": "tag",
113+ "GITHUB_REF_NAME": "v2.1.0",
114+ },
115+ want: "v2.1.0",
116+ },
117+ {
118+ name: "gitlab commit tag",
119+ env: map[string]string{"CI_COMMIT_TAG": "v3.0.0"},
120+ want: "v3.0.0",
121+ },
122+ {
123+ name: "azure source branch tag ref",
124+ env: map[string]string{"BUILD_SOURCEBRANCH": "refs/tags/v4.0.0"},
125+ want: "v4.0.0",
126+ },
127+ {
128+ name: "no tag refs",
129+ env: map[string]string{"GITHUB_REF": "refs/heads/main"},
130+ want: "",
131+ },
132+ }
133+
134+ for _, tt := range tests {
135+ t.Run(tt.name, func(t *testing.T) {
136+ got := versionFromTagRefs(func(key string) string {
137+ return tt.env[key]
138+ })
139+ assert.Equal(t, tt.want, got)
140+ })
141+ }
75142 }
76143
77144 func TestBuildImageArgs_CustomDockerfile(t *testing.T) {
@@ -72,6 +72,73 @@ func TestBuildAndPush_BuildArgs(t *testing.T) {
72 assert.Contains(t, args, "--platform")72 assert.Contains(t, args, "--platform")
73 assert.Contains(t, args, "-f")73 assert.Contains(t, args, "-f")
74 assert.Contains(t, args, "Dockerfile")74 assert.Contains(t, args, "Dockerfile")
75+ assert.Contains(t, args, "VERSION")
76+ assert.NotContains(t, args, "VERSION=v1.0.0")
77+}
78+
79+func TestParseTagRef(t *testing.T) {
80+ tests := []struct {
81+ name string
82+ ref string
83+ want string
84+ }{
85+ {name: "github tag ref", ref: "refs/tags/v1.2.3", want: "v1.2.3"},
86+ {name: "peeled tag ref", ref: "refs/tags/v1.2.3^{}", want: "v1.2.3"},
87+ {name: "branch ref", ref: "refs/heads/main", want: ""},
88+ {name: "empty", ref: "", want: ""},
89+ }
90+
91+ for _, tt := range tests {
92+ t.Run(tt.name, func(t *testing.T) {
93+ assert.Equal(t, tt.want, parseTagRef(tt.ref))
94+ })
95+ }
96+}
97+
98+func TestVersionFromTagRefs(t *testing.T) {
99+ tests := []struct {
100+ name string
101+ env map[string]string
102+ want string
103+ }{
104+ {
105+ name: "github ref tag",
106+ env: map[string]string{"GITHUB_REF": "refs/tags/v2.0.0"},
107+ want: "v2.0.0",
108+ },
109+ {
110+ name: "github ref type and name",
111+ env: map[string]string{
112+ "GITHUB_REF_TYPE": "tag",
113+ "GITHUB_REF_NAME": "v2.1.0",
114+ },
115+ want: "v2.1.0",
116+ },
117+ {
118+ name: "gitlab commit tag",
119+ env: map[string]string{"CI_COMMIT_TAG": "v3.0.0"},
120+ want: "v3.0.0",
121+ },
122+ {
123+ name: "azure source branch tag ref",
124+ env: map[string]string{"BUILD_SOURCEBRANCH": "refs/tags/v4.0.0"},
125+ want: "v4.0.0",
126+ },
127+ {
128+ name: "no tag refs",
129+ env: map[string]string{"GITHUB_REF": "refs/heads/main"},
130+ want: "",
131+ },
132+ }
133+
134+ for _, tt := range tests {
135+ t.Run(tt.name, func(t *testing.T) {
136+ got := versionFromTagRefs(func(key string) string {
137+ return tt.env[key]
138+ })
139+ assert.Equal(t, tt.want, got)
140+ })
141+ }
75 }142 }
76 143
77 func TestBuildImageArgs_CustomDockerfile(t *testing.T) {144 func TestBuildImageArgs_CustomDockerfile(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close