From 535a59e1d3bafaf67e390604e021459c7f15ed15 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Tue, 3 Jun 2025 06:39:02 -0700 Subject: [PATCH 001/840] docs: add link for Coder Desktop docs on workspace page (#18202) --- site/src/modules/resources/SSHButton/SSHButton.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/modules/resources/SSHButton/SSHButton.tsx b/site/src/modules/resources/SSHButton/SSHButton.tsx index 8fb4c3025c5a2..372c6bbf38f7e 100644 --- a/site/src/modules/resources/SSHButton/SSHButton.tsx +++ b/site/src/modules/resources/SSHButton/SSHButton.tsx @@ -73,6 +73,9 @@ export const AgentSSHButton: FC = ({ > Connect via JetBrains IDEs + + Connect via Coder Desktop + SSH configuration From b0d23ca31b466e4e32e97d2e606e4985614528c6 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Tue, 3 Jun 2025 16:23:59 +0100 Subject: [PATCH 002/840] fix: remove duplicated envbuilder vars (#18144) --- examples/templates/kubernetes-devcontainer/main.tf | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/templates/kubernetes-devcontainer/main.tf b/examples/templates/kubernetes-devcontainer/main.tf index 28a49ba2427b1..8fc79fa25c57e 100644 --- a/examples/templates/kubernetes-devcontainer/main.tf +++ b/examples/templates/kubernetes-devcontainer/main.tf @@ -155,19 +155,17 @@ locals { repo_url = data.coder_parameter.repo.value # The envbuilder provider requires a key-value map of environment variables. envbuilder_env = { - # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider - # if the cache repo is enabled. - "ENVBUILDER_GIT_URL" : local.repo_url, - "ENVBUILDER_CACHE_REPO" : var.cache_repo, "CODER_AGENT_TOKEN" : coder_agent.main.token, # Use the docker gateway if the access URL is 127.0.0.1 "CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : var.cache_repo == "" ? local.repo_url : "", # Use the docker gateway if the access URL is 127.0.0.1 "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, "ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")), - "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", - "ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}", + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true" # You may need to adjust this if you get an error regarding deleting files when building the workspace. # For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in # addition to `/var/run`. From da9a313d0cd45c61641d7585771d93ca503a23c9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 3 Jun 2025 16:55:34 +0100 Subject: [PATCH 003/840] fix(cli): allow specifying empty provisioner tag set with --provisioner-tag="-" (#18205) Relates to https://github.com/coder/coder/issues/17818 Note: due to limitations in `cobra/serpent` I ended up having to use `-` to signify absence of provisioner tags. This value is not a valid key-value pair and thus not a valid tag. --- cli/templatepush.go | 58 ++++++++++----- cli/templatepush_test.go | 71 ++++++++++++++++++- .../coder_templates_push_--help.golden | 5 +- docs/reference/cli/templates_push.md | 2 +- 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index 6f8edf61b5085..312c8a466ec50 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strings" "time" @@ -80,6 +81,46 @@ func (r *RootCmd) templatePush() *serpent.Command { createTemplate = true } + var tags map[string]string + // Passing --provisioner-tag="-" allows the user to clear all provisioner tags. + if len(provisionerTags) == 1 && strings.TrimSpace(provisionerTags[0]) == "-" { + cliui.Warn(inv.Stderr, "Not reusing provisioner tags from the previous template version.") + tags = map[string]string{} + } else { + tags, err = ParseProvisionerTags(provisionerTags) + if err != nil { + return err + } + + // If user hasn't provided new provisioner tags, inherit ones from the active template version. + if len(tags) == 0 && template.ActiveVersionID != uuid.Nil { + templateVersion, err := client.TemplateVersion(inv.Context(), template.ActiveVersionID) + if err != nil { + return err + } + tags = templateVersion.Job.Tags + cliui.Info(inv.Stderr, "Re-using provisioner tags from the active template version.") + cliui.Info(inv.Stderr, "Tip: You can override these tags by passing "+cliui.Code(`--provisioner-tag="key=value"`)+".") + cliui.Info(inv.Stderr, " You can also clear all provisioner tags by passing "+cliui.Code(`--provisioner-tag="-"`)+".") + } + } + + { // For clarity, display provisioner tags to the user. + var tmp []string + for k, v := range tags { + if k == provisionersdk.TagScope || k == provisionersdk.TagOwner { + continue + } + tmp = append(tmp, fmt.Sprintf("%s=%q", k, v)) + } + slices.Sort(tmp) + tagStr := strings.Join(tmp, " ") + if len(tmp) == 0 { + tagStr = "" + } + cliui.Info(inv.Stderr, "Provisioner tags: "+cliui.Code(tagStr)) + } + err = uploadFlags.checkForLockfile(inv) if err != nil { return xerrors.Errorf("check for lockfile: %w", err) @@ -104,21 +145,6 @@ func (r *RootCmd) templatePush() *serpent.Command { return err } - tags, err := ParseProvisionerTags(provisionerTags) - if err != nil { - return err - } - - // If user hasn't provided new provisioner tags, inherit ones from the active template version. - if len(tags) == 0 && template.ActiveVersionID != uuid.Nil { - templateVersion, err := client.TemplateVersion(inv.Context(), template.ActiveVersionID) - if err != nil { - return err - } - tags = templateVersion.Job.Tags - inv.Logger.Info(inv.Context(), "reusing existing provisioner tags", "tags", tags) - } - userVariableValues, err := codersdk.ParseUserVariableValues( varsFiles, variablesFile, @@ -214,7 +240,7 @@ func (r *RootCmd) templatePush() *serpent.Command { }, { Flag: "provisioner-tag", - Description: "Specify a set of tags to target provisioner daemons.", + Description: "Specify a set of tags to target provisioner daemons. If you do not specify any tags, the tags from the active template version will be reused, if available. To remove existing tags, use --provisioner-tag=\"-\".", Value: serpent.StringArrayOf(&provisionerTags), }, { diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index b8e4147e6bab4..e1a7e612f4ed6 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -602,7 +602,7 @@ func TestTemplatePush(t *testing.T) { templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID) - // Push new template version without provisioner tags. CLI should reuse tags from the previous version. + // Push new template version with different provisioner tags. source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ Parse: echo.ParseComplete, ProvisionApply: echo.ApplyComplete, @@ -639,6 +639,75 @@ func TestTemplatePush(t *testing.T) { require.EqualValues(t, map[string]string{"foobar": "foobaz", "owner": "", "scope": "organization"}, templateVersion.Job.Tags) }) + t.Run("DeleteTags", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + + // Start the first provisioner with no tags. + client, provisionerDocker, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + ProvisionerDaemonTags: map[string]string{}, + }) + defer provisionerDocker.Close() + + // Start the second provisioner with a tag set. + provisionerFoobar := coderdtest.NewTaggedProvisionerDaemon(t, api, "provisioner-foobar", map[string]string{ + "foobar": "foobaz", + }) + defer provisionerFoobar.Close() + + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + // Create the template with initial tagged template version. + templateVersion := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) { + ctvr.ProvisionerTags = map[string]string{ + "foobar": "foobaz", + } + }) + templateVersion = coderdtest.AwaitTemplateVersionJobCompleted(t, client, templateVersion.ID) + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, templateVersion.ID) + + // Stop the tagged provisioner daemon. + provisionerFoobar.Close() + + // Push new template version with no provisioner tags. + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + inv, root := clitest.New(t, "templates", "push", template.Name, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--name", template.Name, "--provisioner-tag=\"-\"") + clitest.SetupConfig(t, templateAdmin, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.WithContext(ctx).Run() + }() + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + pty.WriteLine(m.write) + } + + require.NoError(t, <-execDone) + + // Verify template version tags + template, err := client.Template(ctx, template.ID) + require.NoError(t, err) + + templateVersion, err = client.TemplateVersion(ctx, template.ActiveVersionID) + require.NoError(t, err) + require.EqualValues(t, map[string]string{"owner": "", "scope": "organization"}, templateVersion.Job.Tags) + }) + t.Run("DoNotChangeTags", func(t *testing.T) { t.Parallel() diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index eee0ad34ca925..edab61a3c55f1 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -33,7 +33,10 @@ OPTIONS: generated if not provided. --provisioner-tag string-array - Specify a set of tags to target provisioner daemons. + Specify a set of tags to target provisioner daemons. If you do not + specify any tags, the tags from the active template version will be + reused, if available. To remove existing tags, use + --provisioner-tag="-". --var string-array Alias of --variable. diff --git a/docs/reference/cli/templates_push.md b/docs/reference/cli/templates_push.md index 46687d3fc672e..8c7901e86e408 100644 --- a/docs/reference/cli/templates_push.md +++ b/docs/reference/cli/templates_push.md @@ -41,7 +41,7 @@ Alias of --variable. |------|---------------------------| | Type | string-array | -Specify a set of tags to target provisioner daemons. +Specify a set of tags to target provisioner daemons. If you do not specify any tags, the tags from the active template version will be reused, if available. To remove existing tags, use --provisioner-tag="-". ### --name From 7b273b0b8c356ed80881f1ab205292857d495157 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 3 Jun 2025 12:45:28 -0500 Subject: [PATCH 004/840] fix: add blink-so to contributors bypass (#18215) --- .github/workflows/contrib.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contrib.yaml b/.github/workflows/contrib.yaml index fccb6d3b1b50e..27dffe94f4000 100644 --- a/.github/workflows/contrib.yaml +++ b/.github/workflows/contrib.yaml @@ -42,7 +42,7 @@ jobs: # branch should not be protected branch: "main" # Some users have signed a corporate CLA with Coder so are exempt from signing our community one. - allowlist: "coryb,aaronlehmann,dependabot*,blink-so" + allowlist: "coryb,aaronlehmann,dependabot*,blink-so*" release-labels: runs-on: ubuntu-latest From cc89820d7ceec9ee2847b0526a22861e9adc4430 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:26:50 -0400 Subject: [PATCH 005/840] feat: add template export functionality to UI (#18214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds template export functionality to the Coder UI, addressing issue #17859. Users can now export templates directly from the web interface without requiring CLI access. ## Changes ### Frontend API - Added `downloadTemplateVersion` function to `site/src/api/api.ts` - Supports both TAR (default) and ZIP formats - Uses existing `/api/v2/files/{fileId}` endpoint with format parameter ### UI Enhancement - Added "Export as TAR" and "Export as ZIP" options to template dropdown menu - Positioned logically between "Duplicate" and "Delete" actions - Uses download icon from Lucide React for consistency ### User Experience - Files automatically named as `{templateName}-{templateVersion}.{extension}` - Immediate download trigger on click - Proper error handling with console logging - Clean blob URL management to prevent memory leaks ## Testing The implementation has been tested for: - ✅ TypeScript compilation - ✅ Proper function signatures and types - ✅ UI component integration - ✅ Error handling structure ## Screenshots The export options appear in the template dropdown menu: - Export as TAR (default format, compatible with `coder template pull`) - Export as ZIP (compressed format for easier handling) ## Fixes Closes #17859 ## Notes This enhancement makes template management more accessible for users who: - Don't have CLI access - Manage deployments on devices without Coder CLI - Prefer web-based workflows - Need to transfer templates between environments The implementation follows existing patterns in the codebase and maintains consistency with the current UI design. --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: Kyle Carberry --- site/src/api/api.ts | 25 ++++++++++++++ .../pages/TemplatePage/TemplatePageHeader.tsx | 34 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 81931c003c99d..5463ad7a44dd6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1084,6 +1084,31 @@ class ApiMethods { return response.data; }; + /** + * Downloads a template version as a tar or zip archive + * @param fileId The file ID from the template version's job + * @param format Optional format: "zip" for zip archive, empty/undefined for tar + * @returns Promise that resolves to a Blob containing the archive + */ + downloadTemplateVersion = async ( + fileId: string, + format?: "zip", + ): Promise => { + const params = new URLSearchParams(); + if (format) { + params.set("format", format); + } + + const response = await this.axios.get( + `/api/v2/files/${fileId}?${params.toString()}`, + { + responseType: "blob", + }, + ); + + return response.data; + }; + updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 54c3c04de8bdf..a7ebbf0ad00b1 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -1,5 +1,6 @@ import EditIcon from "@mui/icons-material/EditOutlined"; import Button from "@mui/material/Button"; +import { API } from "api/api"; import { workspaces } from "api/queries/workspaces"; import type { AuthorizationResponse, @@ -26,7 +27,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; -import { CopyIcon } from "lucide-react"; +import { CopyIcon, DownloadIcon } from "lucide-react"; import { EllipsisVertical, PlusIcon, @@ -46,6 +47,7 @@ type TemplateMenuProps = { templateName: string; templateVersion: string; templateId: string; + fileId: string; onDelete: () => void; }; @@ -54,6 +56,7 @@ const TemplateMenu: FC = ({ templateName, templateVersion, templateId, + fileId, onDelete, }) => { const dialogState = useDeletionDialogState(templateId, onDelete); @@ -68,6 +71,24 @@ const TemplateMenu: FC = ({ const templateLink = getLink(linkToTemplate(organizationName, templateName)); + const handleExport = async (format?: "zip") => { + try { + const blob = await API.downloadTemplateVersion(fileId, format); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + const extension = format === "zip" ? "zip" : "tar"; + link.download = `${templateName}-${templateVersion}.${extension}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error("Failed to export template:", error); + // TODO: Show user-friendly error message + } + }; + return ( <> @@ -102,6 +123,16 @@ const TemplateMenu: FC = ({ Duplicate… + + handleExport()}> + + Export as TAR + + + handleExport("zip")}> + + Export as ZIP + = ({ templateId={template.id} templateName={template.name} templateVersion={activeVersion.name} + fileId={activeVersion.job.file_id} onDelete={onDeleteTemplate} /> )} From 63adfa57bd6a9c9f3e526fadd90d01a1a052d156 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Tue, 3 Jun 2025 15:49:34 -0400 Subject: [PATCH 006/840] docs: add link to official jetbrains toolbox (#18128) Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/user-guides/workspace-access/jetbrains/toolbox.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user-guides/workspace-access/jetbrains/toolbox.md b/docs/user-guides/workspace-access/jetbrains/toolbox.md index 9473056022131..52de09330346a 100644 --- a/docs/user-guides/workspace-access/jetbrains/toolbox.md +++ b/docs/user-guides/workspace-access/jetbrains/toolbox.md @@ -2,6 +2,8 @@ JetBrains Toolbox helps you manage JetBrains products and includes remote development capabilities for connecting to Coder workspaces. +For more details, visit the [official JetBrains documentation](https://www.jetbrains.com/help/toolbox-app/manage-providers.html#shx3a8_18). + ## Install the Coder provider for Toolbox 1. Install [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/) version 2.6.0.40632 or later. From 13449b9e78c06b0684843137e840d1351e5ec8dd Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 3 Jun 2025 17:00:59 -0300 Subject: [PATCH 007/840] feat: embed chat ui in the task sidebar (#18216) **Demo:** Screenshot 2025-06-03 at 14 36 25 - Extract components to be reused and easier to reasoning about - When having cloude-code-web, embed the chat in the sidebar - The sidebar will be wider when having the chat to better fit that **Does not include:** - Sidebar width drag and drop control. The width is static but would be nice to have a control to customize it. --- site/src/pages/TaskPage/TaskAppIframe.tsx | 40 +++ site/src/pages/TaskPage/TaskApps.tsx | 169 +++++++++ site/src/pages/TaskPage/TaskPage.tsx | 396 +--------------------- site/src/pages/TaskPage/TaskSidebar.tsx | 230 +++++++++++++ site/src/pages/TaskPage/constants.ts | 1 + 5 files changed, 445 insertions(+), 391 deletions(-) create mode 100644 site/src/pages/TaskPage/TaskAppIframe.tsx create mode 100644 site/src/pages/TaskPage/TaskApps.tsx create mode 100644 site/src/pages/TaskPage/TaskSidebar.tsx create mode 100644 site/src/pages/TaskPage/constants.ts diff --git a/site/src/pages/TaskPage/TaskAppIframe.tsx b/site/src/pages/TaskPage/TaskAppIframe.tsx new file mode 100644 index 0000000000000..1f247bfeaab71 --- /dev/null +++ b/site/src/pages/TaskPage/TaskAppIframe.tsx @@ -0,0 +1,40 @@ +import type { WorkspaceApp } from "api/typesGenerated"; +import { useAppLink } from "modules/apps/useAppLink"; +import type { Task } from "modules/tasks/tasks"; +import type { FC } from "react"; +import { cn } from "utils/cn"; + +type TaskAppIFrameProps = { + task: Task; + app: WorkspaceApp; + active: boolean; +}; + +export const TaskAppIFrame: FC = ({ + task, + app, + active, +}) => { + const agent = task.workspace.latest_build.resources + .flatMap((r) => r.agents) + .filter((a) => !!a) + .find((a) => a.apps.some((a) => a.id === app.id)); + + if (!agent) { + throw new Error(`Agent for app ${app.id} not found in task workspace`); + } + + const link = useAppLink(app, { + agent, + workspace: task.workspace, + }); + + return ( +