fix(preview): docs corrections, test error handling, and config warning
- Replace invalid Go duration "7d" with "168h" in docs/config/preview.md - Fix pr-* naming examples: stripped prefix means pr-42 → 42--app.example.com - Add language specifier to fenced code block in docs/deployment/gitlab-ci.md - Check errors from svc.List in integration tests (was silently discarding) - Log warning when preview TTL parsing fails in config/service.go
- commit
- b84b07f20aeba1bc20ae406f128f7bb57192f969
- author
- brice · · 2026-03-19 05:27 UTC
- parent
- e2d78b5
- signature
- unsigned
4 files · +17 -11
M docs/config/preview.md +7 -7
| @@ -33,7 +33,7 @@ The `ttl` field accepts Go duration strings: | |||
| 33 | 33 | |-------|---------| | |
| 34 | 34 | | `"24h"` | 24 hours | | |
| 35 | 35 | | `"48h"` | 48 hours (default) | | |
| 36 | - | | `"7d"` | 7 days | | |
| 36 | + | | `"168h"` | 7 days | | |
| 37 | 37 | | `"0"` | Never expire (manual deletion only) | | |
| 38 | 38 | ||
| 39 | 39 | ### `tag_patterns` Matching | |
| @@ -59,7 +59,7 @@ For example, with `separator = "--"` and base route `app.example.com`: | |||
| 59 | 59 | | Image Tag | Preview Domain | | |
| 60 | 60 | |-----------|----------------| | |
| 61 | 61 | | `preview-my-feature` | `my-feature--app.example.com` | | |
| 62 | - | | `pr-42` | `pr-42--app.example.com` | | |
| 62 | + | | `pr-42` | `42--app.example.com` | | |
| 63 | 63 | | `preview-fix-login` | `fix-login--app.example.com` | | |
| 64 | 64 | ||
| 65 | 65 | The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens. | |
| @@ -138,7 +138,7 @@ jobs: | |||
| 138 | 138 | issue_number: context.issue.number, | |
| 139 | 139 | owner: context.repo.owner, | |
| 140 | 140 | repo: context.repo.repo, | |
| 141 | - | body: `Preview deployed: https://pr-${{ github.event.pull_request.number }}--app.example.com` | |
| 141 | + | body: `Preview deployed: https://${{ github.event.pull_request.number }}--app.example.com` | |
| 142 | 142 | }) | |
| 143 | 143 | ``` | |
| 144 | 144 | ||
| @@ -166,9 +166,9 @@ jobs: | |||
| 166 | 166 | steps: | |
| 167 | 167 | - name: Delete preview environment | |
| 168 | 168 | env: | |
| 169 | - | TAG: pr-${{ github.event.pull_request.number }} | |
| 169 | + | PR_NUMBER: ${{ github.event.pull_request.number }} | |
| 170 | 170 | run: | | |
| 171 | - | gordon --server ${{ vars.GORDON_SERVER }} preview delete ${TAG}--app.example.com | |
| 171 | + | gordon --server ${{ vars.GORDON_SERVER }} preview delete ${PR_NUMBER}--app.example.com | |
| 172 | 172 | ``` | |
| 173 | 173 | ||
| 174 | 174 | ## Lifecycle | |
| @@ -227,8 +227,8 @@ preserve = false | |||
| 227 | 227 | ``` | |
| 228 | 228 | ||
| 229 | 229 | With this config: | |
| 230 | - | - Pushing `myapp:pr-99` creates `pr-99--app.example.com` with cloned volumes | |
| 231 | - | - Pushing `myapi:preview-auth-refactor` creates `preview-auth-refactor--api.example.com` | |
| 230 | + | - Pushing `myapp:pr-99` creates `99--app.example.com` with cloned volumes | |
| 231 | + | - Pushing `myapi:preview-auth-refactor` creates `auth-refactor--api.example.com` | |
| 232 | 232 | - Both previews expire after 48 hours and volumes are removed on teardown | |
| 233 | 233 | ||
| 234 | 234 | ## Related | |
| @@ -33,7 +33,7 @@ The `ttl` field accepts Go duration strings: | |||||
| 33 | |-------|---------| | 33 | |-------|---------| | ||
| 34 | | `"24h"` | 24 hours | | 34 | | `"24h"` | 24 hours | | ||
| 35 | | `"48h"` | 48 hours (default) | | 35 | | `"48h"` | 48 hours (default) | | ||
| 36 | - | | `"7d"` | 7 days | | 36 | + | | `"168h"` | 7 days | |
| 37 | | `"0"` | Never expire (manual deletion only) | | 37 | | `"0"` | Never expire (manual deletion only) | | ||
| 38 | 38 | ||||
| 39 | ### `tag_patterns` Matching | 39 | ### `tag_patterns` Matching | ||
| @@ -59,7 +59,7 @@ For example, with `separator = "--"` and base route `app.example.com`: | |||||
| 59 | | Image Tag | Preview Domain | | 59 | | Image Tag | Preview Domain | | ||
| 60 | |-----------|----------------| | 60 | |-----------|----------------| | ||
| 61 | | `preview-my-feature` | `my-feature--app.example.com` | | 61 | | `preview-my-feature` | `my-feature--app.example.com` | | ||
| 62 | - | | `pr-42` | `pr-42--app.example.com` | | 62 | + | | `pr-42` | `42--app.example.com` | |
| 63 | | `preview-fix-login` | `fix-login--app.example.com` | | 63 | | `preview-fix-login` | `fix-login--app.example.com` | | ||
| 64 | 64 | ||||
| 65 | The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens. | 65 | The tag prefix matched by `tag_patterns` is stripped to keep domains short. Slashes in tags are replaced with hyphens. | ||
| @@ -138,7 +138,7 @@ jobs: | |||||
| 138 | issue_number: context.issue.number, | 138 | issue_number: context.issue.number, | ||
| 139 | owner: context.repo.owner, | 139 | owner: context.repo.owner, | ||
| 140 | repo: context.repo.repo, | 140 | repo: context.repo.repo, | ||
| 141 | - | body: `Preview deployed: https://pr-${{ github.event.pull_request.number }}--app.example.com` | 141 | + | body: `Preview deployed: https://${{ github.event.pull_request.number }}--app.example.com` |
| 142 | }) | 142 | }) | ||
| 143 | ``` | 143 | ``` | ||
| 144 | 144 | ||||
| @@ -166,9 +166,9 @@ jobs: | |||||
| 166 | steps: | 166 | steps: | ||
| 167 | - name: Delete preview environment | 167 | - name: Delete preview environment | ||
| 168 | env: | 168 | env: | ||
| 169 | - | TAG: pr-${{ github.event.pull_request.number }} | 169 | + | PR_NUMBER: ${{ github.event.pull_request.number }} |
| 170 | run: | | 170 | run: | | ||
| 171 | - | gordon --server ${{ vars.GORDON_SERVER }} preview delete ${TAG}--app.example.com | 171 | + | gordon --server ${{ vars.GORDON_SERVER }} preview delete ${PR_NUMBER}--app.example.com |
| 172 | ``` | 172 | ``` | ||
| 173 | 173 | ||||
| 174 | ## Lifecycle | 174 | ## Lifecycle | ||
| @@ -227,8 +227,8 @@ preserve = false | |||||
| 227 | ``` | 227 | ``` | ||
| 228 | 228 | ||||
| 229 | With this config: | 229 | With this config: | ||
| 230 | - | - Pushing `myapp:pr-99` creates `pr-99--app.example.com` with cloned volumes | 230 | + | - Pushing `myapp:pr-99` creates `99--app.example.com` with cloned volumes |
| 231 | - | - Pushing `myapi:preview-auth-refactor` creates `preview-auth-refactor--api.example.com` | 231 | + | - Pushing `myapi:preview-auth-refactor` creates `auth-refactor--api.example.com` |
| 232 | - Both previews expire after 48 hours and volumes are removed on teardown | 232 | - Both previews expire after 48 hours and volumes are removed on teardown | ||
| 233 | 233 | ||||
| 234 | ## Related | 234 | ## Related | ||
M docs/deployment/gitlab-ci.md +1 -1
| @@ -181,7 +181,7 @@ Gordon automatically detects the version tag from GitLab CI environment variable | |||
| 181 | 181 | ||
| 182 | 182 | ### Authentication Failed | |
| 183 | 183 | ||
| 184 | - | ``` | |
| 184 | + | ```text | |
| 185 | 185 | Error: failed to verify token | |
| 186 | 186 | ``` | |
| 187 | 187 | ||
| @@ -181,7 +181,7 @@ Gordon automatically detects the version tag from GitLab CI environment variable | |||||
| 181 | 181 | ||||
| 182 | ### Authentication Failed | 182 | ### Authentication Failed | ||
| 183 | 183 | ||||
| 184 | - | ``` | 184 | + | ```text |
| 185 | Error: failed to verify token | 185 | Error: failed to verify token | ||
| 186 | ``` | 186 | ``` | ||
| 187 | 187 | ||||
M internal/usecase/auto/preview/integration_test.go +4 -2
| @@ -50,7 +50,8 @@ func TestPreviewLifecycle_Integration(t *testing.T) { | |||
| 50 | 50 | err = svc.Delete(ctx, "test-feat") | |
| 51 | 51 | require.NoError(t, err) | |
| 52 | 52 | ||
| 53 | - | all, _ = svc.List(ctx) | |
| 53 | + | all, err = svc.List(ctx) | |
| 54 | + | require.NoError(t, err) | |
| 54 | 55 | assert.Empty(t, all) | |
| 55 | 56 | } | |
| 56 | 57 | ||
| @@ -83,6 +84,7 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) { | |||
| 83 | 84 | assert.Equal(t, "short-lived", expired[0].Name) | |
| 84 | 85 | ||
| 85 | 86 | // Should be gone | |
| 86 | - | all, _ := svc.List(ctx) | |
| 87 | + | all, err := svc.List(ctx) | |
| 88 | + | require.NoError(t, err) | |
| 87 | 89 | assert.Empty(t, all) | |
| 88 | 90 | } | |
| @@ -50,7 +50,8 @@ func TestPreviewLifecycle_Integration(t *testing.T) { | |||||
| 50 | err = svc.Delete(ctx, "test-feat") | 50 | err = svc.Delete(ctx, "test-feat") | ||
| 51 | require.NoError(t, err) | 51 | require.NoError(t, err) | ||
| 52 | 52 | ||||
| 53 | - | all, _ = svc.List(ctx) | 53 | + | all, err = svc.List(ctx) |
| 54 | + | require.NoError(t, err) | |||
| 54 | assert.Empty(t, all) | 55 | assert.Empty(t, all) | ||
| 55 | } | 56 | } | ||
| 56 | 57 | ||||
| @@ -83,6 +84,7 @@ func TestPreviewLifecycle_CleanupExpired(t *testing.T) { | |||||
| 83 | assert.Equal(t, "short-lived", expired[0].Name) | 84 | assert.Equal(t, "short-lived", expired[0].Name) | ||
| 84 | 85 | ||||
| 85 | // Should be gone | 86 | // Should be gone | ||
| 86 | - | all, _ := svc.List(ctx) | 87 | + | all, err := svc.List(ctx) |
| 88 | + | require.NoError(t, err) | |||
| 87 | assert.Empty(t, all) | 89 | assert.Empty(t, all) | ||
| 88 | } | 90 | } | ||
M internal/usecase/config/service.go +5 -1
| @@ -139,7 +139,11 @@ func (s *Service) loadConfigValues() Config { | |||
| 139 | 139 | ||
| 140 | 140 | // Preview config | |
| 141 | 141 | previewTTLStr := s.viper.GetString("auto.preview.ttl") | |
| 142 | - | previewTTL, _ := time.ParseDuration(previewTTLStr) | |
| 142 | + | previewTTL, err := time.ParseDuration(previewTTLStr) | |
| 143 | + | if err != nil && previewTTLStr != "" { | |
| 144 | + | log := zerowrap.FromCtx(context.Background()) | |
| 145 | + | log.Warn().Err(err).Str("ttl", previewTTLStr).Msg("invalid preview TTL format, using default 48h (use Go duration strings, e.g. \"168h\" for 7 days)") | |
| 146 | + | } | |
| 143 | 147 | if previewTTL == 0 { | |
| 144 | 148 | previewTTL = 48 * time.Hour | |
| 145 | 149 | } | |
| @@ -139,7 +139,11 @@ func (s *Service) loadConfigValues() Config { | |||||
| 139 | 139 | ||||
| 140 | // Preview config | 140 | // Preview config | ||
| 141 | previewTTLStr := s.viper.GetString("auto.preview.ttl") | 141 | previewTTLStr := s.viper.GetString("auto.preview.ttl") | ||
| 142 | - | previewTTL, _ := time.ParseDuration(previewTTLStr) | 142 | + | previewTTL, err := time.ParseDuration(previewTTLStr) |
| 143 | + | if err != nil && previewTTLStr != "" { | |||
| 144 | + | log := zerowrap.FromCtx(context.Background()) | |||
| 145 | + | log.Warn().Err(err).Str("ttl", previewTTLStr).Msg("invalid preview TTL format, using default 48h (use Go duration strings, e.g. \"168h\" for 7 days)") | |||
| 146 | + | } | |||
| 143 | if previewTTL == 0 { | 147 | if previewTTL == 0 { | ||
| 144 | previewTTL = 48 * time.Hour | 148 | previewTTL = 48 * time.Hour | ||
| 145 | } | 149 | } | ||