feat(cli): add no-confirm push flag and update docs
- commit
- 4d75a1ffeea97235802009e1e143c94a4044dccf
- author
- brice · · 2026-02-02 18:00 UTC
- committer
- brice · · 2026-02-02 18:05 UTC
- parent
- 72d9712
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +490 -15
M docs/cli/attachments.md +6 -3
| @@ -2,6 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Manage attachments on local or remote Gordon instances. | |
| 4 | 4 | ||
| 5 | + | Remote targeting uses client config or an active remote by default. | |
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |
| 7 | + | ||
| 5 | 8 | ## Requirements | |
| 6 | 9 | ||
| 7 | 10 | Attachments require `network_isolation.enabled = true` in your configuration. Without network isolation, containers use Docker's default bridge network which does not provide DNS resolution - your app won't be able to reach attachments by hostname (e.g., `postgres:5432`). | |
| @@ -42,7 +45,7 @@ gordon attachments list | |||
| 42 | 45 | gordon attachments list app.example.com | |
| 43 | 46 | gordon attachments list backend | |
| 44 | 47 | ||
| 45 | - | # Remote mode | |
| 48 | + | # Remote (override) | |
| 46 | 49 | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | |
| 47 | 50 | ``` | |
| 48 | 51 | ||
| @@ -91,7 +94,7 @@ gordon attachments add app.example.com postgres:18 | |||
| 91 | 94 | # Add cache to a network group (shared by all domains in the group) | |
| 92 | 95 | gordon attachments add backend redis:7-alpine | |
| 93 | 96 | ||
| 94 | - | # Remote | |
| 97 | + | # Remote (override) | |
| 95 | 98 | gordon attachments add app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | |
| 96 | 99 | ``` | |
| 97 | 100 | ||
| @@ -129,7 +132,7 @@ gordon attachments remove app.example.com postgres:18 | |||
| 129 | 132 | # Remove from network group | |
| 130 | 133 | gordon attachments remove backend redis:7-alpine | |
| 131 | 134 | ||
| 132 | - | # Remote | |
| 135 | + | # Remote (override) | |
| 133 | 136 | gordon attachments remove app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | |
| 134 | 137 | ``` | |
| 135 | 138 | ||
| @@ -2,6 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | Manage attachments on local or remote Gordon instances. | 3 | Manage attachments on local or remote Gordon instances. | ||
| 4 | 4 | ||||
| 5 | + | Remote targeting uses client config or an active remote by default. | |||
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |||
| 7 | + | ||||
| 5 | ## Requirements | 8 | ## Requirements | ||
| 6 | 9 | ||||
| 7 | Attachments require `network_isolation.enabled = true` in your configuration. Without network isolation, containers use Docker's default bridge network which does not provide DNS resolution - your app won't be able to reach attachments by hostname (e.g., `postgres:5432`). | 10 | Attachments require `network_isolation.enabled = true` in your configuration. Without network isolation, containers use Docker's default bridge network which does not provide DNS resolution - your app won't be able to reach attachments by hostname (e.g., `postgres:5432`). | ||
| @@ -42,7 +45,7 @@ gordon attachments list | |||||
| 42 | gordon attachments list app.example.com | 45 | gordon attachments list app.example.com | ||
| 43 | gordon attachments list backend | 46 | gordon attachments list backend | ||
| 44 | 47 | ||||
| 45 | - | # Remote mode | 48 | + | # Remote (override) |
| 46 | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | 49 | gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN | ||
| 47 | ``` | 50 | ``` | ||
| 48 | 51 | ||||
| @@ -91,7 +94,7 @@ gordon attachments add app.example.com postgres:18 | |||||
| 91 | # Add cache to a network group (shared by all domains in the group) | 94 | # Add cache to a network group (shared by all domains in the group) | ||
| 92 | gordon attachments add backend redis:7-alpine | 95 | gordon attachments add backend redis:7-alpine | ||
| 93 | 96 | ||||
| 94 | - | # Remote | 97 | + | # Remote (override) |
| 95 | gordon attachments add app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | 98 | gordon attachments add app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | ||
| 96 | ``` | 99 | ``` | ||
| 97 | 100 | ||||
| @@ -129,7 +132,7 @@ gordon attachments remove app.example.com postgres:18 | |||||
| 129 | # Remove from network group | 132 | # Remove from network group | ||
| 130 | gordon attachments remove backend redis:7-alpine | 133 | gordon attachments remove backend redis:7-alpine | ||
| 131 | 134 | ||||
| 132 | - | # Remote | 135 | + | # Remote (override) |
| 133 | gordon attachments remove app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | 136 | gordon attachments remove app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN | ||
| 134 | ``` | 137 | ``` | ||
| 135 | 138 | ||||
M docs/cli/index.md +20 -3
| @@ -5,7 +5,7 @@ Gordon provides a command-line interface for server management, deployment, and | |||
| 5 | 5 | Commands are organized by where they run: | |
| 6 | 6 | ||
| 7 | 7 | - **Server Commands** - Must run on the machine hosting Gordon | |
| 8 | - | - **Management Commands** - Work locally or remotely via `--remote` flag | |
| 8 | + | - **Management Commands** - Work locally or remotely | |
| 9 | 9 | - **Client Commands** - CLI utilities that don't require a running Gordon server | |
| 10 | 10 | ||
| 11 | 11 | ## Server Commands (local only) | |
| @@ -15,7 +15,7 @@ Commands are organized by where they run: | |||
| 15 | 15 | | `gordon serve` | Start the Gordon server | [serve](./serve.md) | | |
| 16 | 16 | | `gordon auth` | Manage Gordon server authentication | [auth](./auth.md) | | |
| 17 | 17 | ||
| 18 | - | ## Management Commands (local or --remote) | |
| 18 | + | ## Management Commands (local or remote) | |
| 19 | 19 | ||
| 20 | 20 | | Command | Description | Documentation | | |
| 21 | 21 | |---------|-------------|---------------| | |
| @@ -23,6 +23,9 @@ Commands are organized by where they run: | |||
| 23 | 23 | | `gordon attachments` | Manage container attachments | [attachments](./attachments.md) | | |
| 24 | 24 | | `gordon secrets` | Manage secrets | [secrets](./secrets.md) | | |
| 25 | 25 | | `gordon deploy` | Manually deploy or redeploy a route | [serve](./serve.md#gordon-deploy) | | |
| 26 | + | | `gordon restart` | Restart a running container | [restart](./restart.md) | | |
| 27 | + | | `gordon push` | Tag, push, and optionally deploy an image | [push](./push.md) | | |
| 28 | + | | `gordon rollback` | Roll back to a previous image tag | [rollback](./rollback.md) | | |
| 26 | 29 | | `gordon reload` | Reload configuration and sync containers | [serve](./serve.md#gordon-reload) | | |
| 27 | 30 | | `gordon logs` | Display Gordon process or container logs | [serve](./serve.md#gordon-logs) | | |
| 28 | 31 | | `gordon status` | Show Gordon server status | [status](./status.md) | | |
| @@ -48,6 +51,18 @@ gordon reload | |||
| 48 | 51 | # Deploy a specific route | |
| 49 | 52 | gordon deploy myapp.example.com | |
| 50 | 53 | ||
| 54 | + | # Restart a running container | |
| 55 | + | gordon restart myapp.example.com | |
| 56 | + | ||
| 57 | + | # Push an image and deploy | |
| 58 | + | gordon push myapp.example.com --build | |
| 59 | + | ||
| 60 | + | # Push and deploy without confirmation | |
| 61 | + | gordon push myapp.example.com --no-confirm | |
| 62 | + | ||
| 63 | + | # Roll back to a previous tag | |
| 64 | + | gordon rollback myapp.example.com | |
| 65 | + | ||
| 51 | 66 | # View logs | |
| 52 | 67 | gordon logs # Gordon process logs | |
| 53 | 68 | gordon logs -f # Follow process logs | |
| @@ -97,7 +112,9 @@ gordon remotes use prod | |||
| 97 | 112 | ||
| 98 | 113 | ### Remote Targeting | |
| 99 | 114 | ||
| 100 | - | The CLI can target remote Gordon instances using the `--remote` flag or `GORDON_REMOTE` environment variable. | |
| 115 | + | The CLI can target remote Gordon instances using client config, an active remote, `--remote`, | |
| 116 | + | or `GORDON_REMOTE` environment variable. Use `--remote` and `--token` as global overrides | |
| 117 | + | when you want to bypass your saved configuration. | |
| 101 | 118 | ||
| 102 | 119 | **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API. | |
| 103 | 120 | ||
| @@ -5,7 +5,7 @@ Gordon provides a command-line interface for server management, deployment, and | |||||
| 5 | Commands are organized by where they run: | 5 | Commands are organized by where they run: | ||
| 6 | 6 | ||||
| 7 | - **Server Commands** - Must run on the machine hosting Gordon | 7 | - **Server Commands** - Must run on the machine hosting Gordon | ||
| 8 | - | - **Management Commands** - Work locally or remotely via `--remote` flag | 8 | + | - **Management Commands** - Work locally or remotely |
| 9 | - **Client Commands** - CLI utilities that don't require a running Gordon server | 9 | - **Client Commands** - CLI utilities that don't require a running Gordon server | ||
| 10 | 10 | ||||
| 11 | ## Server Commands (local only) | 11 | ## Server Commands (local only) | ||
| @@ -15,7 +15,7 @@ Commands are organized by where they run: | |||||
| 15 | | `gordon serve` | Start the Gordon server | [serve](./serve.md) | | 15 | | `gordon serve` | Start the Gordon server | [serve](./serve.md) | | ||
| 16 | | `gordon auth` | Manage Gordon server authentication | [auth](./auth.md) | | 16 | | `gordon auth` | Manage Gordon server authentication | [auth](./auth.md) | | ||
| 17 | 17 | ||||
| 18 | - | ## Management Commands (local or --remote) | 18 | + | ## Management Commands (local or remote) |
| 19 | 19 | ||||
| 20 | | Command | Description | Documentation | | 20 | | Command | Description | Documentation | | ||
| 21 | |---------|-------------|---------------| | 21 | |---------|-------------|---------------| | ||
| @@ -23,6 +23,9 @@ Commands are organized by where they run: | |||||
| 23 | | `gordon attachments` | Manage container attachments | [attachments](./attachments.md) | | 23 | | `gordon attachments` | Manage container attachments | [attachments](./attachments.md) | | ||
| 24 | | `gordon secrets` | Manage secrets | [secrets](./secrets.md) | | 24 | | `gordon secrets` | Manage secrets | [secrets](./secrets.md) | | ||
| 25 | | `gordon deploy` | Manually deploy or redeploy a route | [serve](./serve.md#gordon-deploy) | | 25 | | `gordon deploy` | Manually deploy or redeploy a route | [serve](./serve.md#gordon-deploy) | | ||
| 26 | + | | `gordon restart` | Restart a running container | [restart](./restart.md) | | |||
| 27 | + | | `gordon push` | Tag, push, and optionally deploy an image | [push](./push.md) | | |||
| 28 | + | | `gordon rollback` | Roll back to a previous image tag | [rollback](./rollback.md) | | |||
| 26 | | `gordon reload` | Reload configuration and sync containers | [serve](./serve.md#gordon-reload) | | 29 | | `gordon reload` | Reload configuration and sync containers | [serve](./serve.md#gordon-reload) | | ||
| 27 | | `gordon logs` | Display Gordon process or container logs | [serve](./serve.md#gordon-logs) | | 30 | | `gordon logs` | Display Gordon process or container logs | [serve](./serve.md#gordon-logs) | | ||
| 28 | | `gordon status` | Show Gordon server status | [status](./status.md) | | 31 | | `gordon status` | Show Gordon server status | [status](./status.md) | | ||
| @@ -48,6 +51,18 @@ gordon reload | |||||
| 48 | # Deploy a specific route | 51 | # Deploy a specific route | ||
| 49 | gordon deploy myapp.example.com | 52 | gordon deploy myapp.example.com | ||
| 50 | 53 | ||||
| 54 | + | # Restart a running container | |||
| 55 | + | gordon restart myapp.example.com | |||
| 56 | + | ||||
| 57 | + | # Push an image and deploy | |||
| 58 | + | gordon push myapp.example.com --build | |||
| 59 | + | ||||
| 60 | + | # Push and deploy without confirmation | |||
| 61 | + | gordon push myapp.example.com --no-confirm | |||
| 62 | + | ||||
| 63 | + | # Roll back to a previous tag | |||
| 64 | + | gordon rollback myapp.example.com | |||
| 65 | + | ||||
| 51 | # View logs | 66 | # View logs | ||
| 52 | gordon logs # Gordon process logs | 67 | gordon logs # Gordon process logs | ||
| 53 | gordon logs -f # Follow process logs | 68 | gordon logs -f # Follow process logs | ||
| @@ -97,7 +112,9 @@ gordon remotes use prod | |||||
| 97 | 112 | ||||
| 98 | ### Remote Targeting | 113 | ### Remote Targeting | ||
| 99 | 114 | ||||
| 100 | - | The CLI can target remote Gordon instances using the `--remote` flag or `GORDON_REMOTE` environment variable. | 115 | + | The CLI can target remote Gordon instances using client config, an active remote, `--remote`, |
| 116 | + | or `GORDON_REMOTE` environment variable. Use `--remote` and `--token` as global overrides | |||
| 117 | + | when you want to bypass your saved configuration. | |||
| 101 | 118 | ||||
| 102 | **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API. | 119 | **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API. | ||
| 103 | 120 | ||||
A docs/cli/push.md +71 -0
| @@ -0,0 +1,71 @@ | |||
| 1 | + | # Push Command | |
| 2 | + | ||
| 3 | + | Tag, push, and optionally deploy an image to the Gordon registry. | |
| 4 | + | ||
| 5 | + | ## gordon push | |
| 6 | + | ||
| 7 | + | ### Synopsis | |
| 8 | + | ||
| 9 | + | ```bash | |
| 10 | + | gordon push <domain> [options] | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | ### Arguments | |
| 14 | + | ||
| 15 | + | | Argument | Description | | |
| 16 | + | |----------|-------------| | |
| 17 | + | | `<domain>` | The route domain to resolve the registry and repository from | | |
| 18 | + | ||
| 19 | + | ### Options | |
| 20 | + | ||
| 21 | + | | Option | Description | | |
| 22 | + | |--------|-------------| | |
| 23 | + | | `--build` | Build the image first using `docker buildx` | | |
| 24 | + | | `--platform` | Target platform for buildx (default: `linux/amd64`) | | |
| 25 | + | | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) | | |
| 26 | + | | `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) | | |
| 27 | + | | `--no-confirm` | Skip deploy confirmation prompt | | |
| 28 | + | | `--no-deploy` | Push only; skip deployment prompt | | |
| 29 | + | | `--remote` | Remote Gordon URL | | |
| 30 | + | | `--token` | Authentication token for remote | | |
| 31 | + | ||
| 32 | + | ### Description | |
| 33 | + | ||
| 34 | + | `gordon push` resolves the route image for `<domain>`, tags the image for the | |
| 35 | + | Gordon registry, pushes it, and optionally deploys it. | |
| 36 | + | ||
| 37 | + | - The registry and repository are derived from the route image on the server. | |
| 38 | + | - The version tag defaults to the latest git tag. If no tag is found, `latest` | |
| 39 | + | is used. | |
| 40 | + | - When `--build` is set, the command uses `docker buildx build --push` and | |
| 41 | + | injects `VERSION=<tag>` plus any `--build-arg` values. | |
| 42 | + | - The version tag and `latest` are both pushed (unless the version is `latest`). | |
| 43 | + | ||
| 44 | + | ### Examples | |
| 45 | + | ||
| 46 | + | ```bash | |
| 47 | + | # Build, push, and deploy | |
| 48 | + | gordon push myapp.example.com --build | |
| 49 | + | ||
| 50 | + | # Push existing local image, skip deploy | |
| 51 | + | gordon push myapp.example.com --tag v1.2.0 --no-deploy | |
| 52 | + | ||
| 53 | + | # Push and deploy without confirmation | |
| 54 | + | gordon push myapp.example.com --no-confirm | |
| 55 | + | ||
| 56 | + | # Build for ARM and pass build args | |
| 57 | + | gordon push myapp.example.com --build --platform linux/arm64 --build-arg CGO_ENABLED=0 | |
| 58 | + | ``` | |
| 59 | + | ||
| 60 | + | ### Notes | |
| 61 | + | ||
| 62 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |
| 63 | + | - `--build` requires Docker with Buildx available. Docker Desktop includes Buildx; | |
| 64 | + | on Linux, install the `docker-buildx-plugin` package. | |
| 65 | + | - `docker buildx` must be available when using `--build`. | |
| 66 | + | ||
| 67 | + | ## Related | |
| 68 | + | ||
| 69 | + | - [CLI Overview](./index.md) | |
| 70 | + | - [Routes Command](./routes.md) | |
| 71 | + | - [Serve Command](./serve.md) | |
| @@ -0,0 +1,71 @@ | |||||
| 1 | + | # Push Command | |||
| 2 | + | ||||
| 3 | + | Tag, push, and optionally deploy an image to the Gordon registry. | |||
| 4 | + | ||||
| 5 | + | ## gordon push | |||
| 6 | + | ||||
| 7 | + | ### Synopsis | |||
| 8 | + | ||||
| 9 | + | ```bash | |||
| 10 | + | gordon push <domain> [options] | |||
| 11 | + | ``` | |||
| 12 | + | ||||
| 13 | + | ### Arguments | |||
| 14 | + | ||||
| 15 | + | | Argument | Description | | |||
| 16 | + | |----------|-------------| | |||
| 17 | + | | `<domain>` | The route domain to resolve the registry and repository from | | |||
| 18 | + | ||||
| 19 | + | ### Options | |||
| 20 | + | ||||
| 21 | + | | Option | Description | | |||
| 22 | + | |--------|-------------| | |||
| 23 | + | | `--build` | Build the image first using `docker buildx` | | |||
| 24 | + | | `--platform` | Target platform for buildx (default: `linux/amd64`) | | |||
| 25 | + | | `--build-arg` | Additional build args (repeatable, `KEY=VALUE`) | | |||
| 26 | + | | `--tag` | Override version tag (default: `git describe --tags --abbrev=0`) | | |||
| 27 | + | | `--no-confirm` | Skip deploy confirmation prompt | | |||
| 28 | + | | `--no-deploy` | Push only; skip deployment prompt | | |||
| 29 | + | | `--remote` | Remote Gordon URL | | |||
| 30 | + | | `--token` | Authentication token for remote | | |||
| 31 | + | ||||
| 32 | + | ### Description | |||
| 33 | + | ||||
| 34 | + | `gordon push` resolves the route image for `<domain>`, tags the image for the | |||
| 35 | + | Gordon registry, pushes it, and optionally deploys it. | |||
| 36 | + | ||||
| 37 | + | - The registry and repository are derived from the route image on the server. | |||
| 38 | + | - The version tag defaults to the latest git tag. If no tag is found, `latest` | |||
| 39 | + | is used. | |||
| 40 | + | - When `--build` is set, the command uses `docker buildx build --push` and | |||
| 41 | + | injects `VERSION=<tag>` plus any `--build-arg` values. | |||
| 42 | + | - The version tag and `latest` are both pushed (unless the version is `latest`). | |||
| 43 | + | ||||
| 44 | + | ### Examples | |||
| 45 | + | ||||
| 46 | + | ```bash | |||
| 47 | + | # Build, push, and deploy | |||
| 48 | + | gordon push myapp.example.com --build | |||
| 49 | + | ||||
| 50 | + | # Push existing local image, skip deploy | |||
| 51 | + | gordon push myapp.example.com --tag v1.2.0 --no-deploy | |||
| 52 | + | ||||
| 53 | + | # Push and deploy without confirmation | |||
| 54 | + | gordon push myapp.example.com --no-confirm | |||
| 55 | + | ||||
| 56 | + | # Build for ARM and pass build args | |||
| 57 | + | gordon push myapp.example.com --build --platform linux/arm64 --build-arg CGO_ENABLED=0 | |||
| 58 | + | ``` | |||
| 59 | + | ||||
| 60 | + | ### Notes | |||
| 61 | + | ||||
| 62 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |||
| 63 | + | - `--build` requires Docker with Buildx available. Docker Desktop includes Buildx; | |||
| 64 | + | on Linux, install the `docker-buildx-plugin` package. | |||
| 65 | + | - `docker buildx` must be available when using `--build`. | |||
| 66 | + | ||||
| 67 | + | ## Related | |||
| 68 | + | ||||
| 69 | + | - [CLI Overview](./index.md) | |||
| 70 | + | - [Routes Command](./routes.md) | |||
| 71 | + | - [Serve Command](./serve.md) | |||
A docs/cli/restart.md +52 -0
| @@ -0,0 +1,52 @@ | |||
| 1 | + | # Restart Command | |
| 2 | + | ||
| 3 | + | Restart a running container for a route. | |
| 4 | + | ||
| 5 | + | ## gordon restart | |
| 6 | + | ||
| 7 | + | ### Synopsis | |
| 8 | + | ||
| 9 | + | ```bash | |
| 10 | + | gordon restart <domain> [options] | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | ### Arguments | |
| 14 | + | ||
| 15 | + | | Argument | Description | | |
| 16 | + | |----------|-------------| | |
| 17 | + | | `<domain>` | The route domain to restart | | |
| 18 | + | ||
| 19 | + | ### Options | |
| 20 | + | ||
| 21 | + | | Option | Description | | |
| 22 | + | |--------|-------------| | |
| 23 | + | | `--with-attachments` | Also restart attached services (databases, caches) | | |
| 24 | + | | `--remote` | Remote Gordon URL | | |
| 25 | + | | `--token` | Authentication token for remote | | |
| 26 | + | ||
| 27 | + | ### Description | |
| 28 | + | ||
| 29 | + | Restarts the container for the specified route. This is useful after updating | |
| 30 | + | environment variables or secrets without performing a full redeploy. | |
| 31 | + | ||
| 32 | + | When `--with-attachments` is set, Gordon also restarts any attachment containers | |
| 33 | + | for the route. | |
| 34 | + | ||
| 35 | + | ### Examples | |
| 36 | + | ||
| 37 | + | ```bash | |
| 38 | + | # Restart main container only | |
| 39 | + | gordon restart myapp.example.com | |
| 40 | + | ||
| 41 | + | # Restart with attachments | |
| 42 | + | gordon restart myapp.example.com --with-attachments | |
| 43 | + | ``` | |
| 44 | + | ||
| 45 | + | ### Notes | |
| 46 | + | ||
| 47 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |
| 48 | + | ||
| 49 | + | ## Related | |
| 50 | + | ||
| 51 | + | - [CLI Overview](./index.md) | |
| 52 | + | - [Secrets Command](./secrets.md) | |
| @@ -0,0 +1,52 @@ | |||||
| 1 | + | # Restart Command | |||
| 2 | + | ||||
| 3 | + | Restart a running container for a route. | |||
| 4 | + | ||||
| 5 | + | ## gordon restart | |||
| 6 | + | ||||
| 7 | + | ### Synopsis | |||
| 8 | + | ||||
| 9 | + | ```bash | |||
| 10 | + | gordon restart <domain> [options] | |||
| 11 | + | ``` | |||
| 12 | + | ||||
| 13 | + | ### Arguments | |||
| 14 | + | ||||
| 15 | + | | Argument | Description | | |||
| 16 | + | |----------|-------------| | |||
| 17 | + | | `<domain>` | The route domain to restart | | |||
| 18 | + | ||||
| 19 | + | ### Options | |||
| 20 | + | ||||
| 21 | + | | Option | Description | | |||
| 22 | + | |--------|-------------| | |||
| 23 | + | | `--with-attachments` | Also restart attached services (databases, caches) | | |||
| 24 | + | | `--remote` | Remote Gordon URL | | |||
| 25 | + | | `--token` | Authentication token for remote | | |||
| 26 | + | ||||
| 27 | + | ### Description | |||
| 28 | + | ||||
| 29 | + | Restarts the container for the specified route. This is useful after updating | |||
| 30 | + | environment variables or secrets without performing a full redeploy. | |||
| 31 | + | ||||
| 32 | + | When `--with-attachments` is set, Gordon also restarts any attachment containers | |||
| 33 | + | for the route. | |||
| 34 | + | ||||
| 35 | + | ### Examples | |||
| 36 | + | ||||
| 37 | + | ```bash | |||
| 38 | + | # Restart main container only | |||
| 39 | + | gordon restart myapp.example.com | |||
| 40 | + | ||||
| 41 | + | # Restart with attachments | |||
| 42 | + | gordon restart myapp.example.com --with-attachments | |||
| 43 | + | ``` | |||
| 44 | + | ||||
| 45 | + | ### Notes | |||
| 46 | + | ||||
| 47 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |||
| 48 | + | ||||
| 49 | + | ## Related | |||
| 50 | + | ||||
| 51 | + | - [CLI Overview](./index.md) | |||
| 52 | + | - [Secrets Command](./secrets.md) | |||
A docs/cli/rollback.md +54 -0
| @@ -0,0 +1,54 @@ | |||
| 1 | + | # Rollback Command | |
| 2 | + | ||
| 3 | + | Roll back a route to a previous image tag. | |
| 4 | + | ||
| 5 | + | ## gordon rollback | |
| 6 | + | ||
| 7 | + | ### Synopsis | |
| 8 | + | ||
| 9 | + | ```bash | |
| 10 | + | gordon rollback <domain> [options] | |
| 11 | + | ``` | |
| 12 | + | ||
| 13 | + | ### Arguments | |
| 14 | + | ||
| 15 | + | | Argument | Description | | |
| 16 | + | |----------|-------------| | |
| 17 | + | | `<domain>` | The route domain to roll back | | |
| 18 | + | ||
| 19 | + | ### Options | |
| 20 | + | ||
| 21 | + | | Option | Description | | |
| 22 | + | |--------|-------------| | |
| 23 | + | | `--tag` | Target tag (skips interactive selection) | | |
| 24 | + | | `--remote` | Remote Gordon URL | | |
| 25 | + | | `--token` | Authentication token for remote | | |
| 26 | + | ||
| 27 | + | ### Description | |
| 28 | + | ||
| 29 | + | `gordon rollback` lists available tags in the Gordon registry for the route's | |
| 30 | + | repository, then deploys the selected version. Semver tags are sorted in | |
| 31 | + | descending order first, followed by non-semver tags. | |
| 32 | + | ||
| 33 | + | If the selected tag matches the current running tag, no action is taken. | |
| 34 | + | ||
| 35 | + | ### Examples | |
| 36 | + | ||
| 37 | + | ```bash | |
| 38 | + | # Interactive selection | |
| 39 | + | gordon rollback myapp.example.com | |
| 40 | + | ||
| 41 | + | # Roll back to a specific tag | |
| 42 | + | gordon rollback myapp.example.com --tag v1.2.0 | |
| 43 | + | ``` | |
| 44 | + | ||
| 45 | + | ### Notes | |
| 46 | + | ||
| 47 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |
| 48 | + | - Tags are read from the Gordon registry configured on the server. | |
| 49 | + | ||
| 50 | + | ## Related | |
| 51 | + | ||
| 52 | + | - [CLI Overview](./index.md) | |
| 53 | + | - [Push Command](./push.md) | |
| 54 | + | - [Routes Command](./routes.md) | |
| @@ -0,0 +1,54 @@ | |||||
| 1 | + | # Rollback Command | |||
| 2 | + | ||||
| 3 | + | Roll back a route to a previous image tag. | |||
| 4 | + | ||||
| 5 | + | ## gordon rollback | |||
| 6 | + | ||||
| 7 | + | ### Synopsis | |||
| 8 | + | ||||
| 9 | + | ```bash | |||
| 10 | + | gordon rollback <domain> [options] | |||
| 11 | + | ``` | |||
| 12 | + | ||||
| 13 | + | ### Arguments | |||
| 14 | + | ||||
| 15 | + | | Argument | Description | | |||
| 16 | + | |----------|-------------| | |||
| 17 | + | | `<domain>` | The route domain to roll back | | |||
| 18 | + | ||||
| 19 | + | ### Options | |||
| 20 | + | ||||
| 21 | + | | Option | Description | | |||
| 22 | + | |--------|-------------| | |||
| 23 | + | | `--tag` | Target tag (skips interactive selection) | | |||
| 24 | + | | `--remote` | Remote Gordon URL | | |||
| 25 | + | | `--token` | Authentication token for remote | | |||
| 26 | + | ||||
| 27 | + | ### Description | |||
| 28 | + | ||||
| 29 | + | `gordon rollback` lists available tags in the Gordon registry for the route's | |||
| 30 | + | repository, then deploys the selected version. Semver tags are sorted in | |||
| 31 | + | descending order first, followed by non-semver tags. | |||
| 32 | + | ||||
| 33 | + | If the selected tag matches the current running tag, no action is taken. | |||
| 34 | + | ||||
| 35 | + | ### Examples | |||
| 36 | + | ||||
| 37 | + | ```bash | |||
| 38 | + | # Interactive selection | |||
| 39 | + | gordon rollback myapp.example.com | |||
| 40 | + | ||||
| 41 | + | # Roll back to a specific tag | |||
| 42 | + | gordon rollback myapp.example.com --tag v1.2.0 | |||
| 43 | + | ``` | |||
| 44 | + | ||||
| 45 | + | ### Notes | |||
| 46 | + | ||||
| 47 | + | - Remote mode required. See [CLI Overview](./index.md) for targeting options. | |||
| 48 | + | - Tags are read from the Gordon registry configured on the server. | |||
| 49 | + | ||||
| 50 | + | ## Related | |||
| 51 | + | ||||
| 52 | + | - [CLI Overview](./index.md) | |||
| 53 | + | - [Push Command](./push.md) | |||
| 54 | + | - [Routes Command](./routes.md) | |||
M docs/cli/routes.md +6 -3
| @@ -2,6 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Manage routes on local or remote Gordon instances. | |
| 4 | 4 | ||
| 5 | + | Remote targeting uses client config or an active remote by default. | |
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |
| 7 | + | ||
| 5 | 8 | ## gordon routes | |
| 6 | 9 | ||
| 7 | 10 | ### Subcommands | |
| @@ -66,7 +69,7 @@ gordon routes add myapp.example.com myapp:latest | |||
| 66 | 69 | gordon routes add myapp.example.com myapp:latest | |
| 67 | 70 | gordon routes add api.example.com myapi:v2.1.0 | |
| 68 | 71 | ||
| 69 | - | # Remote | |
| 72 | + | # Remote (override) | |
| 70 | 73 | gordon routes add myapp.example.com myapp:latest --remote https://gordon.mydomain.com --token $TOKEN | |
| 71 | 74 | ``` | |
| 72 | 75 | ||
| @@ -100,7 +103,7 @@ gordon routes remove myapp.example.com | |||
| 100 | 103 | # Local | |
| 101 | 104 | gordon routes remove myapp.example.com | |
| 102 | 105 | ||
| 103 | - | # Remote | |
| 106 | + | # Remote (override) | |
| 104 | 107 | gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | |
| 105 | 108 | ``` | |
| 106 | 109 | ||
| @@ -138,7 +141,7 @@ Triggers a fresh image pull and container redeployment for the specified route. | |||
| 138 | 141 | # Local | |
| 139 | 142 | gordon routes deploy myapp.example.com | |
| 140 | 143 | ||
| 141 | - | # Remote | |
| 144 | + | # Remote (override) | |
| 142 | 145 | gordon routes deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | |
| 143 | 146 | ``` | |
| 144 | 147 | ||
| @@ -2,6 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | Manage routes on local or remote Gordon instances. | 3 | Manage routes on local or remote Gordon instances. | ||
| 4 | 4 | ||||
| 5 | + | Remote targeting uses client config or an active remote by default. | |||
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |||
| 7 | + | ||||
| 5 | ## gordon routes | 8 | ## gordon routes | ||
| 6 | 9 | ||||
| 7 | ### Subcommands | 10 | ### Subcommands | ||
| @@ -66,7 +69,7 @@ gordon routes add myapp.example.com myapp:latest | |||||
| 66 | gordon routes add myapp.example.com myapp:latest | 69 | gordon routes add myapp.example.com myapp:latest | ||
| 67 | gordon routes add api.example.com myapi:v2.1.0 | 70 | gordon routes add api.example.com myapi:v2.1.0 | ||
| 68 | 71 | ||||
| 69 | - | # Remote | 72 | + | # Remote (override) |
| 70 | gordon routes add myapp.example.com myapp:latest --remote https://gordon.mydomain.com --token $TOKEN | 73 | gordon routes add myapp.example.com myapp:latest --remote https://gordon.mydomain.com --token $TOKEN | ||
| 71 | ``` | 74 | ``` | ||
| 72 | 75 | ||||
| @@ -100,7 +103,7 @@ gordon routes remove myapp.example.com | |||||
| 100 | # Local | 103 | # Local | ||
| 101 | gordon routes remove myapp.example.com | 104 | gordon routes remove myapp.example.com | ||
| 102 | 105 | ||||
| 103 | - | # Remote | 106 | + | # Remote (override) |
| 104 | gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | 107 | gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | ||
| 105 | ``` | 108 | ``` | ||
| 106 | 109 | ||||
| @@ -138,7 +141,7 @@ Triggers a fresh image pull and container redeployment for the specified route. | |||||
| 138 | # Local | 141 | # Local | ||
| 139 | gordon routes deploy myapp.example.com | 142 | gordon routes deploy myapp.example.com | ||
| 140 | 143 | ||||
| 141 | - | # Remote | 144 | + | # Remote (override) |
| 142 | gordon routes deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | 145 | gordon routes deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | ||
| 143 | ``` | 146 | ``` | ||
| 144 | 147 | ||||
M docs/cli/secrets.md +6 -3
| @@ -2,6 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Manage secrets on local or remote Gordon instances. | |
| 4 | 4 | ||
| 5 | + | Remote targeting uses client config or an active remote by default. | |
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |
| 7 | + | ||
| 5 | 8 | Storage depends on the secrets backend: | |
| 6 | 9 | - `pass`: secrets are stored in pass under `gordon/env/<domain>/<KEY>` | |
| 7 | 10 | - `sops` or `unsafe`: secrets are stored in domain `.env` files | |
| @@ -45,7 +48,7 @@ gordon secrets list <domain> | |||
| 45 | 48 | # Local | |
| 46 | 49 | gordon secrets list myapp.local | |
| 47 | 50 | ||
| 48 | - | # Remote | |
| 51 | + | # Remote (override) | |
| 49 | 52 | gordon secrets list myapp.local --remote https://gordon.mydomain.com --token $TOKEN | |
| 50 | 53 | ``` | |
| 51 | 54 | ||
| @@ -92,7 +95,7 @@ gordon secrets set myapp.local DATABASE_URL "postgres://..." | |||
| 92 | 95 | gordon secrets set myapp.local DATABASE_URL "postgres://user:pass@postgres:5432/db" | |
| 93 | 96 | gordon secrets set myapp.local API_KEY "your-api-key" | |
| 94 | 97 | ||
| 95 | - | # Remote | |
| 98 | + | # Remote (override) | |
| 96 | 99 | gordon secrets set myapp.local DATABASE_URL "postgres://..." --remote https://gordon.mydomain.com --token $TOKEN | |
| 97 | 100 | ``` | |
| 98 | 101 | ||
| @@ -126,7 +129,7 @@ gordon secrets remove <domain> <key> | |||
| 126 | 129 | # Local | |
| 127 | 130 | gordon secrets remove myapp.local DATABASE_URL | |
| 128 | 131 | ||
| 129 | - | # Remote | |
| 132 | + | # Remote (override) | |
| 130 | 133 | gordon secrets remove myapp.local DATABASE_URL --remote https://gordon.mydomain.com --token $TOKEN | |
| 131 | 134 | ``` | |
| 132 | 135 | ||
| @@ -2,6 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | Manage secrets on local or remote Gordon instances. | 3 | Manage secrets on local or remote Gordon instances. | ||
| 4 | 4 | ||||
| 5 | + | Remote targeting uses client config or an active remote by default. | |||
| 6 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |||
| 7 | + | ||||
| 5 | Storage depends on the secrets backend: | 8 | Storage depends on the secrets backend: | ||
| 6 | - `pass`: secrets are stored in pass under `gordon/env/<domain>/<KEY>` | 9 | - `pass`: secrets are stored in pass under `gordon/env/<domain>/<KEY>` | ||
| 7 | - `sops` or `unsafe`: secrets are stored in domain `.env` files | 10 | - `sops` or `unsafe`: secrets are stored in domain `.env` files | ||
| @@ -45,7 +48,7 @@ gordon secrets list <domain> | |||||
| 45 | # Local | 48 | # Local | ||
| 46 | gordon secrets list myapp.local | 49 | gordon secrets list myapp.local | ||
| 47 | 50 | ||||
| 48 | - | # Remote | 51 | + | # Remote (override) |
| 49 | gordon secrets list myapp.local --remote https://gordon.mydomain.com --token $TOKEN | 52 | gordon secrets list myapp.local --remote https://gordon.mydomain.com --token $TOKEN | ||
| 50 | ``` | 53 | ``` | ||
| 51 | 54 | ||||
| @@ -92,7 +95,7 @@ gordon secrets set myapp.local DATABASE_URL "postgres://..." | |||||
| 92 | gordon secrets set myapp.local DATABASE_URL "postgres://user:pass@postgres:5432/db" | 95 | gordon secrets set myapp.local DATABASE_URL "postgres://user:pass@postgres:5432/db" | ||
| 93 | gordon secrets set myapp.local API_KEY "your-api-key" | 96 | gordon secrets set myapp.local API_KEY "your-api-key" | ||
| 94 | 97 | ||||
| 95 | - | # Remote | 98 | + | # Remote (override) |
| 96 | gordon secrets set myapp.local DATABASE_URL "postgres://..." --remote https://gordon.mydomain.com --token $TOKEN | 99 | gordon secrets set myapp.local DATABASE_URL "postgres://..." --remote https://gordon.mydomain.com --token $TOKEN | ||
| 97 | ``` | 100 | ``` | ||
| 98 | 101 | ||||
| @@ -126,7 +129,7 @@ gordon secrets remove <domain> <key> | |||||
| 126 | # Local | 129 | # Local | ||
| 127 | gordon secrets remove myapp.local DATABASE_URL | 130 | gordon secrets remove myapp.local DATABASE_URL | ||
| 128 | 131 | ||||
| 129 | - | # Remote | 132 | + | # Remote (override) |
| 130 | gordon secrets remove myapp.local DATABASE_URL --remote https://gordon.mydomain.com --token $TOKEN | 133 | gordon secrets remove myapp.local DATABASE_URL --remote https://gordon.mydomain.com --token $TOKEN | ||
| 131 | ``` | 134 | ``` | ||
| 132 | 135 | ||||
M docs/cli/serve.md +8 -2
| @@ -172,6 +172,9 @@ gordon deploy <domain> [options] | |||
| 172 | 172 | | `--remote` | Remote Gordon URL | | |
| 173 | 173 | | `--token` | Authentication token for remote | | |
| 174 | 174 | ||
| 175 | + | Remote targeting uses client config or an active remote by default. | |
| 176 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |
| 177 | + | ||
| 175 | 178 | ### Description | |
| 176 | 179 | ||
| 177 | 180 | **Local mode:** Sends `SIGUSR2` to the Gordon process with the specified domain. | |
| @@ -190,7 +193,7 @@ Both modes trigger: | |||
| 190 | 193 | gordon deploy myapp.example.com | |
| 191 | 194 | gordon deploy api.example.com | |
| 192 | 195 | ||
| 193 | - | # Remote deployment | |
| 196 | + | # Remote deployment (override) | |
| 194 | 197 | gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | |
| 195 | 198 | ``` | |
| 196 | 199 | ||
| @@ -229,6 +232,9 @@ gordon logs [domain] [options] | |||
| 229 | 232 | | `--remote` | | | Remote Gordon URL | | |
| 230 | 233 | | `--token` | | | Authentication token for remote | | |
| 231 | 234 | ||
| 235 | + | Remote targeting uses client config or an active remote by default. | |
| 236 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |
| 237 | + | ||
| 232 | 238 | ### Examples | |
| 233 | 239 | ||
| 234 | 240 | ```bash | |
| @@ -243,7 +249,7 @@ gordon logs myapp.local # Last 50 lines from container | |||
| 243 | 249 | gordon logs myapp.local -f # Follow container logs | |
| 244 | 250 | gordon logs myapp.local -n 100 # Last 100 lines from container | |
| 245 | 251 | ||
| 246 | - | # Remote mode | |
| 252 | + | # Remote mode (override) | |
| 247 | 253 | gordon logs --remote https://gordon.mydomain.com --token $TOKEN | |
| 248 | 254 | gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN | |
| 249 | 255 | ``` | |
| @@ -172,6 +172,9 @@ gordon deploy <domain> [options] | |||||
| 172 | | `--remote` | Remote Gordon URL | | 172 | | `--remote` | Remote Gordon URL | | ||
| 173 | | `--token` | Authentication token for remote | | 173 | | `--token` | Authentication token for remote | | ||
| 174 | 174 | ||||
| 175 | + | Remote targeting uses client config or an active remote by default. | |||
| 176 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |||
| 177 | + | ||||
| 175 | ### Description | 178 | ### Description | ||
| 176 | 179 | ||||
| 177 | **Local mode:** Sends `SIGUSR2` to the Gordon process with the specified domain. | 180 | **Local mode:** Sends `SIGUSR2` to the Gordon process with the specified domain. | ||
| @@ -190,7 +193,7 @@ Both modes trigger: | |||||
| 190 | gordon deploy myapp.example.com | 193 | gordon deploy myapp.example.com | ||
| 191 | gordon deploy api.example.com | 194 | gordon deploy api.example.com | ||
| 192 | 195 | ||||
| 193 | - | # Remote deployment | 196 | + | # Remote deployment (override) |
| 194 | gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | 197 | gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN | ||
| 195 | ``` | 198 | ``` | ||
| 196 | 199 | ||||
| @@ -229,6 +232,9 @@ gordon logs [domain] [options] | |||||
| 229 | | `--remote` | | | Remote Gordon URL | | 232 | | `--remote` | | | Remote Gordon URL | | ||
| 230 | | `--token` | | | Authentication token for remote | | 233 | | `--token` | | | Authentication token for remote | | ||
| 231 | 234 | ||||
| 235 | + | Remote targeting uses client config or an active remote by default. | |||
| 236 | + | Use `--remote` and `--token` to override. See [CLI Overview](./index.md). | |||
| 237 | + | ||||
| 232 | ### Examples | 238 | ### Examples | ||
| 233 | 239 | ||||
| 234 | ```bash | 240 | ```bash | ||
| @@ -243,7 +249,7 @@ gordon logs myapp.local # Last 50 lines from container | |||||
| 243 | gordon logs myapp.local -f # Follow container logs | 249 | gordon logs myapp.local -f # Follow container logs | ||
| 244 | gordon logs myapp.local -n 100 # Last 100 lines from container | 250 | gordon logs myapp.local -n 100 # Last 100 lines from container | ||
| 245 | 251 | ||||
| 246 | - | # Remote mode | 252 | + | # Remote mode (override) |
| 247 | gordon logs --remote https://gordon.mydomain.com --token $TOKEN | 253 | gordon logs --remote https://gordon.mydomain.com --token $TOKEN | ||
| 248 | gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN | 254 | gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN | ||
| 249 | ``` | 255 | ``` | ||
M docs/cli/status.md +1 -1
| @@ -10,7 +10,7 @@ Display server configuration and container status for all routes. | |||
| 10 | 10 | gordon status --remote https://gordon.mydomain.com --token $TOKEN | |
| 11 | 11 | ``` | |
| 12 | 12 | ||
| 13 | - | > **Note:** The status command requires remote mode. Use `--remote` flag or set `GORDON_REMOTE` environment variable. | |
| 13 | + | > **Note:** The status command requires remote mode. See [CLI Overview](./index.md) for targeting options. | |
| 14 | 14 | ||
| 15 | 15 | ### Output | |
| 16 | 16 | ||
| @@ -10,7 +10,7 @@ Display server configuration and container status for all routes. | |||||
| 10 | gordon status --remote https://gordon.mydomain.com --token $TOKEN | 10 | gordon status --remote https://gordon.mydomain.com --token $TOKEN | ||
| 11 | ``` | 11 | ``` | ||
| 12 | 12 | ||||
| 13 | - | > **Note:** The status command requires remote mode. Use `--remote` flag or set `GORDON_REMOTE` environment variable. | 13 | + | > **Note:** The status command requires remote mode. See [CLI Overview](./index.md) for targeting options. |
| 14 | 14 | ||||
| 15 | ### Output | 15 | ### Output | ||
| 16 | 16 | ||||
M docs/config/logging.md +3 -0
| @@ -43,6 +43,9 @@ max_age = 28 | |||
| 43 | 43 | | `file.max_backups` | int | `3` | Number of old files to keep | | |
| 44 | 44 | | `file.max_age` | int | `28` | Days to keep old files | | |
| 45 | 45 | ||
| 46 | + | The Admin API and `gordon logs` read from the process log file. Keep | |
| 47 | + | `logging.file.enabled` set to `true` if you need process log streaming. | |
| 48 | + | ||
| 46 | 49 | ### Container Logs | |
| 47 | 50 | ||
| 48 | 51 | | Option | Type | Default | Description | | |
| @@ -43,6 +43,9 @@ max_age = 28 | |||||
| 43 | | `file.max_backups` | int | `3` | Number of old files to keep | | 43 | | `file.max_backups` | int | `3` | Number of old files to keep | | ||
| 44 | | `file.max_age` | int | `28` | Days to keep old files | | 44 | | `file.max_age` | int | `28` | Days to keep old files | | ||
| 45 | 45 | ||||
| 46 | + | The Admin API and `gordon logs` read from the process log file. Keep | |||
| 47 | + | `logging.file.enabled` set to `true` if you need process log streaming. | |||
| 48 | + | ||||
| 46 | ### Container Logs | 49 | ### Container Logs | ||
| 47 | 50 | ||||
| 48 | | Option | Type | Default | Description | | 51 | | Option | Type | Default | Description | | ||
A internal/adapters/in/cli/push.go +263 -0
| @@ -0,0 +1,263 @@ | |||
| 1 | + | package cli | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "os/exec" | |
| 8 | + | "regexp" | |
| 9 | + | "strings" | |
| 10 | + | ||
| 11 | + | "github.com/spf13/cobra" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 14 | + | "github.com/bnema/gordon/internal/adapters/in/cli/ui/components" | |
| 15 | + | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | |
| 16 | + | "github.com/bnema/gordon/pkg/validation" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func newPushCmd() *cobra.Command { | |
| 20 | + | var ( | |
| 21 | + | noDeploy bool | |
| 22 | + | noConfirm bool | |
| 23 | + | build bool | |
| 24 | + | platform string | |
| 25 | + | tag string | |
| 26 | + | buildArgs []string | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | cmd := &cobra.Command{ | |
| 30 | + | Use: "push <domain>", | |
| 31 | + | Short: "Tag, push, and optionally deploy an image", | |
| 32 | + | Long: `Tags a local image for the Gordon registry and pushes it. | |
| 33 | + | Uses git tags for versioning. Optionally triggers deployment after push. | |
| 34 | + | ||
| 35 | + | With --build, builds the image first using docker buildx. | |
| 36 | + | ||
| 37 | + | Examples: | |
| 38 | + | gordon push myapp.example.com --remote ... | |
| 39 | + | gordon push myapp.example.com --build --remote ... | |
| 40 | + | gordon push myapp.example.com --tag v1.2.0 --no-deploy --remote ... | |
| 41 | + | gordon push myapp.example.com --build --build-arg CGO_ENABLED=0 --remote ...`, | |
| 42 | + | Args: cobra.ExactArgs(1), | |
| 43 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 44 | + | return runPush(cmd.Context(), args[0], tag, build, platform, buildArgs, noDeploy, noConfirm) | |
| 45 | + | }, | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | cmd.Flags().BoolVar(&noDeploy, "no-deploy", false, "Push only, don't trigger deploy") | |
| 49 | + | cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Skip deploy confirmation prompt") | |
| 50 | + | cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx") | |
| 51 | + | cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)") | |
| 52 | + | cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)") | |
| 53 | + | cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)") | |
| 54 | + | ||
| 55 | + | return cmd | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, buildArgs []string, noDeploy bool, noConfirm bool) error { | |
| 59 | + | client, isRemote := GetRemoteClient() | |
| 60 | + | if !isRemote { | |
| 61 | + | fmt.Println(styles.RenderError("push command requires --remote flag or GORDON_REMOTE env var")) | |
| 62 | + | return fmt.Errorf("push requires remote mode") | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | route, err := client.GetRoute(ctx, pushDomain) | |
| 66 | + | if err != nil { | |
| 67 | + | return fmt.Errorf("failed to get route: %w", err) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | registry, imageName, _ := parseImageRef(route.Image) | |
| 71 | + | if registry == "" || imageName == "" { | |
| 72 | + | return fmt.Errorf("cannot parse registry/image from route image: %s", route.Image) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | version := determineVersion(ctx, tag) | |
| 76 | + | if version != "latest" { | |
| 77 | + | if err := validation.ValidateReference(version); err != nil { | |
| 78 | + | return fmt.Errorf("invalid version tag %q: %w", version, err) | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | for _, ba := range buildArgs { | |
| 83 | + | if err := validateBuildArg(ba); err != nil { | |
| 84 | + | return err | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | versionRef := fmt.Sprintf("%s/%s:%s", registry, imageName, version) | |
| 89 | + | latestRef := fmt.Sprintf("%s/%s:latest", registry, imageName) | |
| 90 | + | ||
| 91 | + | fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef)) | |
| 92 | + | if version != "latest" { | |
| 93 | + | fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef)) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | if build { | |
| 97 | + | if err := buildAndPush(ctx, version, platform, buildArgs, versionRef, latestRef); err != nil { | |
| 98 | + | return err | |
| 99 | + | } | |
| 100 | + | } else { | |
| 101 | + | if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil { | |
| 102 | + | return err | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | fmt.Println(styles.RenderSuccess("Push complete")) | |
| 107 | + | ||
| 108 | + | if !noDeploy { | |
| 109 | + | return deployAfterPush(ctx, client, pushDomain, noConfirm) | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | return nil | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func determineVersion(ctx context.Context, tag string) string { | |
| 116 | + | version := tag | |
| 117 | + | if version == "" { | |
| 118 | + | version = getGitVersion(ctx) | |
| 119 | + | } | |
| 120 | + | if version == "" { | |
| 121 | + | version = "latest" | |
| 122 | + | } | |
| 123 | + | return version | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func buildAndPush(ctx context.Context, version, platform string, buildArgs []string, versionRef, latestRef string) error { | |
| 127 | + | if _, err := os.Stat("Dockerfile"); os.IsNotExist(err) { | |
| 128 | + | return fmt.Errorf("no Dockerfile found in current directory") | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | fmt.Println("\nBuilding and pushing...") | |
| 132 | + | args := []string{ | |
| 133 | + | "buildx", "build", | |
| 134 | + | "--platform", platform, | |
| 135 | + | "-t", versionRef, | |
| 136 | + | "-t", latestRef, | |
| 137 | + | "--build-arg", "VERSION=" + version, | |
| 138 | + | } | |
| 139 | + | for _, ba := range buildArgs { | |
| 140 | + | args = append(args, "--build-arg", ba) | |
| 141 | + | } | |
| 142 | + | args = append(args, "--push", ".") | |
| 143 | + | ||
| 144 | + | dockerCmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204 | |
| 145 | + | dockerCmd.Stdout = os.Stdout | |
| 146 | + | dockerCmd.Stderr = os.Stderr | |
| 147 | + | if err := dockerCmd.Run(); err != nil { | |
| 148 | + | return fmt.Errorf("docker buildx failed: %w", err) | |
| 149 | + | } | |
| 150 | + | return nil | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error { | |
| 154 | + | localImage := fmt.Sprintf("%s/%s", registry, imageName) | |
| 155 | + | ||
| 156 | + | fmt.Println("\nChecking local image...") | |
| 157 | + | if !dockerImageExists(ctx, localImage) { | |
| 158 | + | return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | fmt.Println("Tagging...") | |
| 162 | + | if err := dockerTag(ctx, localImage, versionRef); err != nil { | |
| 163 | + | return fmt.Errorf("failed to tag %s: %w", versionRef, err) | |
| 164 | + | } | |
| 165 | + | if version != "latest" { | |
| 166 | + | if err := dockerTag(ctx, localImage, latestRef); err != nil { | |
| 167 | + | return fmt.Errorf("failed to tag %s: %w", latestRef, err) | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | fmt.Println("Pushing...") | |
| 172 | + | if err := dockerPush(ctx, versionRef); err != nil { | |
| 173 | + | return fmt.Errorf("failed to push %s: %w", versionRef, err) | |
| 174 | + | } | |
| 175 | + | if version != "latest" { | |
| 176 | + | if err := dockerPush(ctx, latestRef); err != nil { | |
| 177 | + | return fmt.Errorf("failed to push %s: %w", latestRef, err) | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | return nil | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func deployAfterPush(ctx context.Context, client *remote.Client, pushDomain string, noConfirm bool) error { | |
| 184 | + | if !noConfirm { | |
| 185 | + | confirmed, err := components.RunConfirm("Deploy now?", components.WithDefaultYes()) | |
| 186 | + | if err != nil { | |
| 187 | + | return err | |
| 188 | + | } | |
| 189 | + | if !confirmed { | |
| 190 | + | return nil | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | result, err := client.Deploy(ctx, pushDomain) | |
| 195 | + | if err != nil { | |
| 196 | + | return fmt.Errorf("failed to deploy: %w", err) | |
| 197 | + | } | |
| 198 | + | containerID := result.ContainerID | |
| 199 | + | if len(containerID) > 12 { | |
| 200 | + | containerID = containerID[:12] | |
| 201 | + | } | |
| 202 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Deployed %s (container: %s)", pushDomain, containerID))) | |
| 203 | + | return nil | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | // parseImageRef splits "registry/name:tag" into components. | |
| 207 | + | func parseImageRef(image string) (registry, name, tag string) { | |
| 208 | + | // Handle "reg.bnema.dev/jp-bot:latest" or "reg.bnema.dev/jp-bot" | |
| 209 | + | parts := strings.SplitN(image, "/", 2) | |
| 210 | + | if len(parts) != 2 { | |
| 211 | + | return "", "", "" | |
| 212 | + | } | |
| 213 | + | registry = parts[0] | |
| 214 | + | nameTag := parts[1] | |
| 215 | + | if idx := strings.LastIndex(nameTag, ":"); idx != -1 { | |
| 216 | + | name = nameTag[:idx] | |
| 217 | + | tag = nameTag[idx+1:] | |
| 218 | + | } else { | |
| 219 | + | name = nameTag | |
| 220 | + | tag = "latest" | |
| 221 | + | } | |
| 222 | + | return | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | // getGitVersion returns the latest git tag, or empty string if unavailable. | |
| 226 | + | func getGitVersion(ctx context.Context) string { | |
| 227 | + | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--abbrev=0").Output() | |
| 228 | + | if err != nil { | |
| 229 | + | return "" | |
| 230 | + | } | |
| 231 | + | return strings.TrimSpace(string(out)) | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | func dockerTag(ctx context.Context, src, dst string) error { | |
| 235 | + | cmd := exec.CommandContext(ctx, "docker", "tag", src, dst) | |
| 236 | + | cmd.Stdout = os.Stdout | |
| 237 | + | cmd.Stderr = os.Stderr | |
| 238 | + | return cmd.Run() | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | func dockerPush(ctx context.Context, ref string) error { | |
| 242 | + | cmd := exec.CommandContext(ctx, "docker", "push", ref) | |
| 243 | + | cmd.Stdout = os.Stdout | |
| 244 | + | cmd.Stderr = os.Stderr | |
| 245 | + | return cmd.Run() | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | func dockerImageExists(ctx context.Context, image string) bool { | |
| 249 | + | cmd := exec.CommandContext(ctx, "docker", "image", "inspect", image) // #nosec G204 | |
| 250 | + | cmd.Stdout = nil | |
| 251 | + | cmd.Stderr = nil | |
| 252 | + | return cmd.Run() == nil | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore. | |
| 256 | + | var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`) | |
| 257 | + | ||
| 258 | + | func validateBuildArg(arg string) error { | |
| 259 | + | if !buildArgPattern.MatchString(arg) { | |
| 260 | + | return fmt.Errorf("invalid build arg %q: must match KEY=VALUE where KEY is [a-zA-Z_][a-zA-Z0-9_]*", arg) | |
| 261 | + | } | |
| 262 | + | return nil | |
| 263 | + | } | |
| @@ -0,0 +1,263 @@ | |||||
| 1 | + | package cli | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 6 | + | "os" | |||
| 7 | + | "os/exec" | |||
| 8 | + | "regexp" | |||
| 9 | + | "strings" | |||
| 10 | + | ||||
| 11 | + | "github.com/spf13/cobra" | |||
| 12 | + | ||||
| 13 | + | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |||
| 14 | + | "github.com/bnema/gordon/internal/adapters/in/cli/ui/components" | |||
| 15 | + | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | |||
| 16 | + | "github.com/bnema/gordon/pkg/validation" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | func newPushCmd() *cobra.Command { | |||
| 20 | + | var ( | |||
| 21 | + | noDeploy bool | |||
| 22 | + | noConfirm bool | |||
| 23 | + | build bool | |||
| 24 | + | platform string | |||
| 25 | + | tag string | |||
| 26 | + | buildArgs []string | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | cmd := &cobra.Command{ | |||
| 30 | + | Use: "push <domain>", | |||
| 31 | + | Short: "Tag, push, and optionally deploy an image", | |||
| 32 | + | Long: `Tags a local image for the Gordon registry and pushes it. | |||
| 33 | + | Uses git tags for versioning. Optionally triggers deployment after push. | |||
| 34 | + | ||||
| 35 | + | With --build, builds the image first using docker buildx. | |||
| 36 | + | ||||
| 37 | + | Examples: | |||
| 38 | + | gordon push myapp.example.com --remote ... | |||
| 39 | + | gordon push myapp.example.com --build --remote ... | |||
| 40 | + | gordon push myapp.example.com --tag v1.2.0 --no-deploy --remote ... | |||
| 41 | + | gordon push myapp.example.com --build --build-arg CGO_ENABLED=0 --remote ...`, | |||
| 42 | + | Args: cobra.ExactArgs(1), | |||
| 43 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 44 | + | return runPush(cmd.Context(), args[0], tag, build, platform, buildArgs, noDeploy, noConfirm) | |||
| 45 | + | }, | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | cmd.Flags().BoolVar(&noDeploy, "no-deploy", false, "Push only, don't trigger deploy") | |||
| 49 | + | cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Skip deploy confirmation prompt") | |||
| 50 | + | cmd.Flags().BoolVar(&build, "build", false, "Build the image first using docker buildx") | |||
| 51 | + | cmd.Flags().StringVar(&platform, "platform", "linux/amd64", "Target platform (used with --build)") | |||
| 52 | + | cmd.Flags().StringVar(&tag, "tag", "", "Override version tag (default: git describe)") | |||
| 53 | + | cmd.Flags().StringArrayVar(&buildArgs, "build-arg", nil, "Additional build args (used with --build)") | |||
| 54 | + | ||||
| 55 | + | return cmd | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func runPush(ctx context.Context, pushDomain, tag string, build bool, platform string, buildArgs []string, noDeploy bool, noConfirm bool) error { | |||
| 59 | + | client, isRemote := GetRemoteClient() | |||
| 60 | + | if !isRemote { | |||
| 61 | + | fmt.Println(styles.RenderError("push command requires --remote flag or GORDON_REMOTE env var")) | |||
| 62 | + | return fmt.Errorf("push requires remote mode") | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | route, err := client.GetRoute(ctx, pushDomain) | |||
| 66 | + | if err != nil { | |||
| 67 | + | return fmt.Errorf("failed to get route: %w", err) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | registry, imageName, _ := parseImageRef(route.Image) | |||
| 71 | + | if registry == "" || imageName == "" { | |||
| 72 | + | return fmt.Errorf("cannot parse registry/image from route image: %s", route.Image) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | version := determineVersion(ctx, tag) | |||
| 76 | + | if version != "latest" { | |||
| 77 | + | if err := validation.ValidateReference(version); err != nil { | |||
| 78 | + | return fmt.Errorf("invalid version tag %q: %w", version, err) | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | for _, ba := range buildArgs { | |||
| 83 | + | if err := validateBuildArg(ba); err != nil { | |||
| 84 | + | return err | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | versionRef := fmt.Sprintf("%s/%s:%s", registry, imageName, version) | |||
| 89 | + | latestRef := fmt.Sprintf("%s/%s:latest", registry, imageName) | |||
| 90 | + | ||||
| 91 | + | fmt.Printf("Image: %s\n", styles.Theme.Bold.Render(versionRef)) | |||
| 92 | + | if version != "latest" { | |||
| 93 | + | fmt.Printf("Also: %s\n", styles.Theme.Bold.Render(latestRef)) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | if build { | |||
| 97 | + | if err := buildAndPush(ctx, version, platform, buildArgs, versionRef, latestRef); err != nil { | |||
| 98 | + | return err | |||
| 99 | + | } | |||
| 100 | + | } else { | |||
| 101 | + | if err := tagAndPush(ctx, registry, imageName, version, versionRef, latestRef); err != nil { | |||
| 102 | + | return err | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | fmt.Println(styles.RenderSuccess("Push complete")) | |||
| 107 | + | ||||
| 108 | + | if !noDeploy { | |||
| 109 | + | return deployAfterPush(ctx, client, pushDomain, noConfirm) | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | return nil | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func determineVersion(ctx context.Context, tag string) string { | |||
| 116 | + | version := tag | |||
| 117 | + | if version == "" { | |||
| 118 | + | version = getGitVersion(ctx) | |||
| 119 | + | } | |||
| 120 | + | if version == "" { | |||
| 121 | + | version = "latest" | |||
| 122 | + | } | |||
| 123 | + | return version | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func buildAndPush(ctx context.Context, version, platform string, buildArgs []string, versionRef, latestRef string) error { | |||
| 127 | + | if _, err := os.Stat("Dockerfile"); os.IsNotExist(err) { | |||
| 128 | + | return fmt.Errorf("no Dockerfile found in current directory") | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | fmt.Println("\nBuilding and pushing...") | |||
| 132 | + | args := []string{ | |||
| 133 | + | "buildx", "build", | |||
| 134 | + | "--platform", platform, | |||
| 135 | + | "-t", versionRef, | |||
| 136 | + | "-t", latestRef, | |||
| 137 | + | "--build-arg", "VERSION=" + version, | |||
| 138 | + | } | |||
| 139 | + | for _, ba := range buildArgs { | |||
| 140 | + | args = append(args, "--build-arg", ba) | |||
| 141 | + | } | |||
| 142 | + | args = append(args, "--push", ".") | |||
| 143 | + | ||||
| 144 | + | dockerCmd := exec.CommandContext(ctx, "docker", args...) // #nosec G204 | |||
| 145 | + | dockerCmd.Stdout = os.Stdout | |||
| 146 | + | dockerCmd.Stderr = os.Stderr | |||
| 147 | + | if err := dockerCmd.Run(); err != nil { | |||
| 148 | + | return fmt.Errorf("docker buildx failed: %w", err) | |||
| 149 | + | } | |||
| 150 | + | return nil | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | func tagAndPush(ctx context.Context, registry, imageName, version, versionRef, latestRef string) error { | |||
| 154 | + | localImage := fmt.Sprintf("%s/%s", registry, imageName) | |||
| 155 | + | ||||
| 156 | + | fmt.Println("\nChecking local image...") | |||
| 157 | + | if !dockerImageExists(ctx, localImage) { | |||
| 158 | + | return fmt.Errorf("local image %s not found; build and tag it before pushing", localImage) | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | fmt.Println("Tagging...") | |||
| 162 | + | if err := dockerTag(ctx, localImage, versionRef); err != nil { | |||
| 163 | + | return fmt.Errorf("failed to tag %s: %w", versionRef, err) | |||
| 164 | + | } | |||
| 165 | + | if version != "latest" { | |||
| 166 | + | if err := dockerTag(ctx, localImage, latestRef); err != nil { | |||
| 167 | + | return fmt.Errorf("failed to tag %s: %w", latestRef, err) | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | fmt.Println("Pushing...") | |||
| 172 | + | if err := dockerPush(ctx, versionRef); err != nil { | |||
| 173 | + | return fmt.Errorf("failed to push %s: %w", versionRef, err) | |||
| 174 | + | } | |||
| 175 | + | if version != "latest" { | |||
| 176 | + | if err := dockerPush(ctx, latestRef); err != nil { | |||
| 177 | + | return fmt.Errorf("failed to push %s: %w", latestRef, err) | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | return nil | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func deployAfterPush(ctx context.Context, client *remote.Client, pushDomain string, noConfirm bool) error { | |||
| 184 | + | if !noConfirm { | |||
| 185 | + | confirmed, err := components.RunConfirm("Deploy now?", components.WithDefaultYes()) | |||
| 186 | + | if err != nil { | |||
| 187 | + | return err | |||
| 188 | + | } | |||
| 189 | + | if !confirmed { | |||
| 190 | + | return nil | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | result, err := client.Deploy(ctx, pushDomain) | |||
| 195 | + | if err != nil { | |||
| 196 | + | return fmt.Errorf("failed to deploy: %w", err) | |||
| 197 | + | } | |||
| 198 | + | containerID := result.ContainerID | |||
| 199 | + | if len(containerID) > 12 { | |||
| 200 | + | containerID = containerID[:12] | |||
| 201 | + | } | |||
| 202 | + | fmt.Println(styles.RenderSuccess(fmt.Sprintf("Deployed %s (container: %s)", pushDomain, containerID))) | |||
| 203 | + | return nil | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | // parseImageRef splits "registry/name:tag" into components. | |||
| 207 | + | func parseImageRef(image string) (registry, name, tag string) { | |||
| 208 | + | // Handle "reg.bnema.dev/jp-bot:latest" or "reg.bnema.dev/jp-bot" | |||
| 209 | + | parts := strings.SplitN(image, "/", 2) | |||
| 210 | + | if len(parts) != 2 { | |||
| 211 | + | return "", "", "" | |||
| 212 | + | } | |||
| 213 | + | registry = parts[0] | |||
| 214 | + | nameTag := parts[1] | |||
| 215 | + | if idx := strings.LastIndex(nameTag, ":"); idx != -1 { | |||
| 216 | + | name = nameTag[:idx] | |||
| 217 | + | tag = nameTag[idx+1:] | |||
| 218 | + | } else { | |||
| 219 | + | name = nameTag | |||
| 220 | + | tag = "latest" | |||
| 221 | + | } | |||
| 222 | + | return | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | // getGitVersion returns the latest git tag, or empty string if unavailable. | |||
| 226 | + | func getGitVersion(ctx context.Context) string { | |||
| 227 | + | out, err := exec.CommandContext(ctx, "git", "describe", "--tags", "--abbrev=0").Output() | |||
| 228 | + | if err != nil { | |||
| 229 | + | return "" | |||
| 230 | + | } | |||
| 231 | + | return strings.TrimSpace(string(out)) | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | func dockerTag(ctx context.Context, src, dst string) error { | |||
| 235 | + | cmd := exec.CommandContext(ctx, "docker", "tag", src, dst) | |||
| 236 | + | cmd.Stdout = os.Stdout | |||
| 237 | + | cmd.Stderr = os.Stderr | |||
| 238 | + | return cmd.Run() | |||
| 239 | + | } | |||
| 240 | + | ||||
| 241 | + | func dockerPush(ctx context.Context, ref string) error { | |||
| 242 | + | cmd := exec.CommandContext(ctx, "docker", "push", ref) | |||
| 243 | + | cmd.Stdout = os.Stdout | |||
| 244 | + | cmd.Stderr = os.Stderr | |||
| 245 | + | return cmd.Run() | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | func dockerImageExists(ctx context.Context, image string) bool { | |||
| 249 | + | cmd := exec.CommandContext(ctx, "docker", "image", "inspect", image) // #nosec G204 | |||
| 250 | + | cmd.Stdout = nil | |||
| 251 | + | cmd.Stderr = nil | |||
| 252 | + | return cmd.Run() == nil | |||
| 253 | + | } | |||
| 254 | + | ||||
| 255 | + | // buildArgPattern matches KEY=VALUE where KEY starts with a letter or underscore. | |||
| 256 | + | var buildArgPattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*$`) | |||
| 257 | + | ||||
| 258 | + | func validateBuildArg(arg string) error { | |||
| 259 | + | if !buildArgPattern.MatchString(arg) { | |||
| 260 | + | return fmt.Errorf("invalid build arg %q: must match KEY=VALUE where KEY is [a-zA-Z_][a-zA-Z0-9_]*", arg) | |||
| 261 | + | } | |||
| 262 | + | return nil | |||
| 263 | + | } | |||