Merge pull request #75 from bnema/feat/push-version-from-tag-refs
feat(push): derive VERSION from CI tag refs and git describe
4 files · +138 -23
M docs/cli/push.md +5 -4
| @@ -24,7 +24,7 @@ gordon push <domain> [options] | |||
| 24 | 24 | | `-f, --file` | Path to Dockerfile (default: `./Dockerfile`, used with `--build`) | | |
| 25 | 25 | | `--platform` | Target platform for buildx (default: `linux/amd64`) | | |
| 26 | 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 | 28 | | `--no-confirm` | Skip deploy confirmation prompt | | |
| 29 | 29 | | `--no-deploy` | Push only; skip deployment prompt | | |
| 30 | 30 | | `--remote` | Remote Gordon URL | | |
| @@ -36,10 +36,11 @@ gordon push <domain> [options] | |||
| 36 | 36 | Gordon registry, pushes it, and optionally deploys it. | |
| 37 | 37 | ||
| 38 | 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` | |
| 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. | |
| 41 | 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 | 44 | - Use `-f/--file` to build from a Dockerfile outside the current directory root. | |
| 44 | 45 | - The version tag and `latest` are both pushed (unless the version is `latest`). | |
| 45 | 46 | ||
| @@ -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 | |||
| 124 | 124 | ||
| 125 | 125 | ## 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 | 129 | ```bash | |
| 130 | 130 | 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 | 137 | ## 8. Deploy Your First App | |
| 136 | 138 | ||
| 137 | 139 | On your local machine: | |
| 138 | 140 | ||
| 139 | 141 | ```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 | |
| 142 | 145 | ||
| 143 | - | # Build your app | |
| 144 | - | docker build -t myapp . | |
| 146 | + | # Build, push, and deploy through Gordon | |
| 147 | + | gordon push myapp --build --no-confirm | |
| 148 | + | ``` | |
| 145 | 149 | ||
| 146 | - | # Tag for your registry | |
| 147 | - | docker tag myapp gordon.mydomain.com/myapp:latest | |
| 150 | + | What this command does: | |
| 148 | 151 | ||
| 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. | |
| 152 | 159 | ||
| 153 | 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 | 164 | Push a new image to deploy with zero downtime: | |
| 158 | 165 | ||
| 159 | 166 | ```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 | |
| 164 | 169 | ``` | |
| 165 | 170 | ||
| 166 | 171 | Gordon automatically: | |
| @@ -124,31 +124,38 @@ sudo loginctl enable-linger $USER | |||||
| 124 | 124 | ||||
| 125 | ## 7. Generate a Deploy Token | 125 | ## 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 | ```bash | 129 | ```bash | ||
| 130 | gordon auth token generate --subject deploy --scopes push,pull --expiry 0 | 130 | 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 App | 137 | ## 8. Deploy Your First App | ||
| 136 | 138 | ||||
| 137 | On your local machine: | 139 | On your local machine: | ||
| 138 | 140 | ||||
| 139 | ```bash | 141 | ```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.com | 143 | + | gordon remotes add prod https://gordon.mydomain.com --token <your-token> |
| 144 | + | gordon remotes use prod | |||
| 142 | 145 | ||||
| 143 | - | # Build your app | 146 | + | # 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 registry | 150 | + | What this command does: |
| 147 | - | docker tag myapp gordon.mydomain.com/myapp:latest | |||
| 148 | 151 | ||||
| 149 | - | # Push to Gordon | 152 | + | - `gordon push myapp` resolves the route for `myapp`, chooses a version tag, |
| 150 | - | docker push gordon.mydomain.com/myapp:latest | 153 | + | 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 | ```bash | 166 | ```bash | ||
| 160 | - | # Make changes, rebuild | 167 | + | # 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: | |||
| 66 | 66 | cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx") | |
| 67 | 67 | cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)") | |
| 68 | 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 | 70 | cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)") | |
| 71 | 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 | 404 | func determineVersion(ctx context.Context, tag string) string { | |
| 405 | 405 | version := tag | |
| 406 | + | if version == "" { | |
| 407 | + | version = versionFromTagRefs(os.Getenv) | |
| 408 | + | } | |
| 406 | 409 | if version == "" { | |
| 407 | 410 | version = getGitVersion(ctx) | |
| 408 | 411 | } | |
| @@ -412,6 +415,44 @@ func determineVersion(ctx context.Context, tag string) string { | |||
| 412 | 415 | 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 | 456 | func buildAndPush(ctx context.Context, version, platform, dockerfile string, buildArgs []string, versionRef, latestRef string) error { | |
| 416 | 457 | if _, err := os.Stat(dockerfile); os.IsNotExist(err) { | |
| 417 | 458 | return fmt.Errorf("dockerfile not found: %s", dockerfile) | |
| @@ -423,6 +464,7 @@ func buildAndPush(ctx context.Context, version, platform, dockerfile string, bui | |||
| 423 | 464 | // using docker push gives us chunked uploads (~5MB per request). | |
| 424 | 465 | fmt.Println("\nBuilding image...") | |
| 425 | 466 | buildCmd := exec.CommandContext(ctx, "docker", buildImageArgs(version, platform, dockerfile, buildArgs, versionRef, latestRef)...) // #nosec G204 | |
| 467 | + | buildCmd.Env = append(os.Environ(), "VERSION="+version) | |
| 426 | 468 | buildCmd.Stdout = os.Stdout | |
| 427 | 469 | buildCmd.Stderr = os.Stderr | |
| 428 | 470 | if err := buildCmd.Run(); err != nil { | |
| @@ -451,7 +493,7 @@ func buildImageArgs(version, platform, dockerfile string, buildArgs []string, ve | |||
| 451 | 493 | "--platform", platform, | |
| 452 | 494 | "-f", dockerfile, | |
| 453 | 495 | "-t", latestRef, | |
| 454 | - | "--build-arg", "VERSION=" + version, | |
| 496 | + | "--build-arg", "VERSION", | |
| 455 | 497 | } | |
| 456 | 498 | if version != "latest" { | |
| 457 | 499 | args = append(args, "-t", versionRef) | |
| @@ -639,9 +681,9 @@ func parseImageRef(image string) (registry, name, tag string) { | |||
| 639 | 681 | 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 | 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 | 687 | if err != nil { | |
| 646 | 688 | return "" | |
| 647 | 689 | } | |
| @@ -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 := tag | 405 | 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 version | 415 | 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 G204 | 466 | 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.Stdout | 468 | buildCmd.Stdout = os.Stdout | ||
| 427 | buildCmd.Stderr = os.Stderr | 469 | 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 | return | 681 | 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) { | |||
| 72 | 72 | assert.Contains(t, args, "--platform") | |
| 73 | 73 | assert.Contains(t, args, "-f") | |
| 74 | 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 | 144 | 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) { | ||