detached 4d75a1f

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +490 -15
M docs/cli/attachments.md +6 -3
@@ -2,6 +2,9 @@
22
33 Manage attachments on local or remote Gordon instances.
44
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+
58 ## Requirements
69
710 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
4245 gordon attachments list app.example.com
4346 gordon attachments list backend
4447
45-# Remote mode
48+# Remote (override)
4649 gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN
4750 ```
4851
@@ -91,7 +94,7 @@ gordon attachments add app.example.com postgres:18
9194 # Add cache to a network group (shared by all domains in the group)
9295 gordon attachments add backend redis:7-alpine
9396
94-# Remote
97+# Remote (override)
9598 gordon attachments add app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN
9699 ```
97100
@@ -129,7 +132,7 @@ gordon attachments remove app.example.com postgres:18
129132 # Remove from network group
130133 gordon attachments remove backend redis:7-alpine
131134
132-# Remote
135+# Remote (override)
133136 gordon attachments remove app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN
134137 ```
135138
@@ -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 ## Requirements8 ## 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.com45 gordon attachments list app.example.com
43 gordon attachments list backend46 gordon attachments list backend
44 47
45-# Remote mode48+# Remote (override)
46 gordon attachments list --remote https://gordon.mydomain.com --token $TOKEN49 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-alpine95 gordon attachments add backend redis:7-alpine
93 96
94-# Remote97+# Remote (override)
95 gordon attachments add app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN98 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 group132 # Remove from network group
130 gordon attachments remove backend redis:7-alpine133 gordon attachments remove backend redis:7-alpine
131 134
132-# Remote135+# Remote (override)
133 gordon attachments remove app.example.com postgres:18 --remote https://gordon.mydomain.com --token $TOKEN136 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
55 Commands are organized by where they run:
66
77 - **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
99 - **Client Commands** - CLI utilities that don't require a running Gordon server
1010
1111 ## Server Commands (local only)
@@ -15,7 +15,7 @@ Commands are organized by where they run:
1515 | `gordon serve` | Start the Gordon server | [serve](./serve.md) |
1616 | `gordon auth` | Manage Gordon server authentication | [auth](./auth.md) |
1717
18-## Management Commands (local or --remote)
18+## Management Commands (local or remote)
1919
2020 | Command | Description | Documentation |
2121 |---------|-------------|---------------|
@@ -23,6 +23,9 @@ Commands are organized by where they run:
2323 | `gordon attachments` | Manage container attachments | [attachments](./attachments.md) |
2424 | `gordon secrets` | Manage secrets | [secrets](./secrets.md) |
2525 | `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) |
2629 | `gordon reload` | Reload configuration and sync containers | [serve](./serve.md#gordon-reload) |
2730 | `gordon logs` | Display Gordon process or container logs | [serve](./serve.md#gordon-logs) |
2831 | `gordon status` | Show Gordon server status | [status](./status.md) |
@@ -48,6 +51,18 @@ gordon reload
4851 # Deploy a specific route
4952 gordon deploy myapp.example.com
5053
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+
5166 # View logs
5267 gordon logs # Gordon process logs
5368 gordon logs -f # Follow process logs
@@ -97,7 +112,9 @@ gordon remotes use prod
97112
98113 ### Remote Targeting
99114
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.
101118
102119 **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.
103120
@@ -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 Gordon7 - **Server Commands** - Must run on the machine hosting Gordon
8-- **Management Commands** - Work locally or remotely via `--remote` flag8+- **Management Commands** - Work locally or remotely
9 - **Client Commands** - CLI utilities that don't require a running Gordon server9 - **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 route51 # Deploy a specific route
49 gordon deploy myapp.example.com52 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 logs66 # View logs
52 gordon logs # Gordon process logs67 gordon logs # Gordon process logs
53 gordon logs -f # Follow process logs68 gordon logs -f # Follow process logs
@@ -97,7 +112,9 @@ gordon remotes use prod
97 112
98 ### Remote Targeting113 ### 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 @@
22
33 Manage routes on local or remote Gordon instances.
44
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+
58 ## gordon routes
69
710 ### Subcommands
@@ -66,7 +69,7 @@ gordon routes add myapp.example.com myapp:latest
6669 gordon routes add myapp.example.com myapp:latest
6770 gordon routes add api.example.com myapi:v2.1.0
6871
69-# Remote
72+# Remote (override)
7073 gordon routes add myapp.example.com myapp:latest --remote https://gordon.mydomain.com --token $TOKEN
7174 ```
7275
@@ -100,7 +103,7 @@ gordon routes remove myapp.example.com
100103 # Local
101104 gordon routes remove myapp.example.com
102105
103-# Remote
106+# Remote (override)
104107 gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN
105108 ```
106109
@@ -138,7 +141,7 @@ Triggers a fresh image pull and container redeployment for the specified route.
138141 # Local
139142 gordon routes deploy myapp.example.com
140143
141-# Remote
144+# Remote (override)
142145 gordon routes deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN
143146 ```
144147
@@ -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 routes8 ## gordon routes
6 9
7 ### Subcommands10 ### Subcommands
@@ -66,7 +69,7 @@ gordon routes add myapp.example.com myapp:latest
66 gordon routes add myapp.example.com myapp:latest69 gordon routes add myapp.example.com myapp:latest
67 gordon routes add api.example.com myapi:v2.1.070 gordon routes add api.example.com myapi:v2.1.0
68 71
69-# Remote72+# Remote (override)
70 gordon routes add myapp.example.com myapp:latest --remote https://gordon.mydomain.com --token $TOKEN73 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 # Local103 # Local
101 gordon routes remove myapp.example.com104 gordon routes remove myapp.example.com
102 105
103-# Remote106+# Remote (override)
104 gordon routes remove myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN107 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 # Local141 # Local
139 gordon routes deploy myapp.example.com142 gordon routes deploy myapp.example.com
140 143
141-# Remote144+# Remote (override)
142 gordon routes deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN145 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 @@
22
33 Manage secrets on local or remote Gordon instances.
44
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+
58 Storage depends on the secrets backend:
69 - `pass`: secrets are stored in pass under `gordon/env/<domain>/<KEY>`
710 - `sops` or `unsafe`: secrets are stored in domain `.env` files
@@ -45,7 +48,7 @@ gordon secrets list <domain>
4548 # Local
4649 gordon secrets list myapp.local
4750
48-# Remote
51+# Remote (override)
4952 gordon secrets list myapp.local --remote https://gordon.mydomain.com --token $TOKEN
5053 ```
5154
@@ -92,7 +95,7 @@ gordon secrets set myapp.local DATABASE_URL "postgres://..."
9295 gordon secrets set myapp.local DATABASE_URL "postgres://user:pass@postgres:5432/db"
9396 gordon secrets set myapp.local API_KEY "your-api-key"
9497
95-# Remote
98+# Remote (override)
9699 gordon secrets set myapp.local DATABASE_URL "postgres://..." --remote https://gordon.mydomain.com --token $TOKEN
97100 ```
98101
@@ -126,7 +129,7 @@ gordon secrets remove <domain> <key>
126129 # Local
127130 gordon secrets remove myapp.local DATABASE_URL
128131
129-# Remote
132+# Remote (override)
130133 gordon secrets remove myapp.local DATABASE_URL --remote https://gordon.mydomain.com --token $TOKEN
131134 ```
132135
@@ -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` files10 - `sops` or `unsafe`: secrets are stored in domain `.env` files
@@ -45,7 +48,7 @@ gordon secrets list <domain>
45 # Local48 # Local
46 gordon secrets list myapp.local49 gordon secrets list myapp.local
47 50
48-# Remote51+# Remote (override)
49 gordon secrets list myapp.local --remote https://gordon.mydomain.com --token $TOKEN52 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-# Remote98+# Remote (override)
96 gordon secrets set myapp.local DATABASE_URL "postgres://..." --remote https://gordon.mydomain.com --token $TOKEN99 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 # Local129 # Local
127 gordon secrets remove myapp.local DATABASE_URL130 gordon secrets remove myapp.local DATABASE_URL
128 131
129-# Remote132+# Remote (override)
130 gordon secrets remove myapp.local DATABASE_URL --remote https://gordon.mydomain.com --token $TOKEN133 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]
172172 | `--remote` | Remote Gordon URL |
173173 | `--token` | Authentication token for remote |
174174
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+
175178 ### Description
176179
177180 **Local mode:** Sends `SIGUSR2` to the Gordon process with the specified domain.
@@ -190,7 +193,7 @@ Both modes trigger:
190193 gordon deploy myapp.example.com
191194 gordon deploy api.example.com
192195
193-# Remote deployment
196+# Remote deployment (override)
194197 gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN
195198 ```
196199
@@ -229,6 +232,9 @@ gordon logs [domain] [options]
229232 | `--remote` | | | Remote Gordon URL |
230233 | `--token` | | | Authentication token for remote |
231234
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+
232238 ### Examples
233239
234240 ```bash
@@ -243,7 +249,7 @@ gordon logs myapp.local # Last 50 lines from container
243249 gordon logs myapp.local -f # Follow container logs
244250 gordon logs myapp.local -n 100 # Last 100 lines from container
245251
246-# Remote mode
252+# Remote mode (override)
247253 gordon logs --remote https://gordon.mydomain.com --token $TOKEN
248254 gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN
249255 ```
@@ -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 ### Description178 ### 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.com193 gordon deploy myapp.example.com
191 gordon deploy api.example.com194 gordon deploy api.example.com
192 195
193-# Remote deployment196+# Remote deployment (override)
194 gordon deploy myapp.example.com --remote https://gordon.mydomain.com --token $TOKEN197 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 ### Examples238 ### Examples
233 239
234 ```bash240 ```bash
@@ -243,7 +249,7 @@ gordon logs myapp.local # Last 50 lines from container
243 gordon logs myapp.local -f # Follow container logs249 gordon logs myapp.local -f # Follow container logs
244 gordon logs myapp.local -n 100 # Last 100 lines from container250 gordon logs myapp.local -n 100 # Last 100 lines from container
245 251
246-# Remote mode252+# Remote mode (override)
247 gordon logs --remote https://gordon.mydomain.com --token $TOKEN253 gordon logs --remote https://gordon.mydomain.com --token $TOKEN
248 gordon logs myapp.local --remote https://gordon.mydomain.com --token $TOKEN254 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.
1010 gordon status --remote https://gordon.mydomain.com --token $TOKEN
1111 ```
1212
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.
1414
1515 ### Output
1616
@@ -10,7 +10,7 @@ Display server configuration and container status for all routes.
10 gordon status --remote https://gordon.mydomain.com --token $TOKEN10 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 ### Output15 ### Output
16 16
M docs/config/logging.md +3 -0
@@ -43,6 +43,9 @@ max_age = 28
4343 | `file.max_backups` | int | `3` | Number of old files to keep |
4444 | `file.max_age` | int | `28` | Days to keep old files |
4545
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+
4649 ### Container Logs
4750
4851 | 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 Logs49 ### 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+}

Keyboard shortcuts

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

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

Branches 7

Tags 106

↑↓ moveenter openesc close