Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"image": "mcr.microsoft.com/devcontainers/go:latest",
"features": {
"ghcr.io/devcontainers-extra/features/pre-commit:2": {}
"ghcr.io/devcontainers-extra/features/prek:1": {}
}
}
4 changes: 2 additions & 2 deletions .github/workflows/devcontainer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ on:
runCmd:
description: "Command to run inside the devcontainer"
required: false
default: test -x "$(command -v pre-commit)"
default: test -x "$(command -v prek)"

permissions:
contents: read

env:
DEFAULT_RUN_CMD: test -x "$(command -v pre-commit)"
DEFAULT_RUN_CMD: test -x "$(command -v prek)"

jobs:
build:
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
- '.goreleaser.yml'
- 'e2e/**'
- '.github/workflows/pr-ci.yml'
- 'hack/licenses/**'
- 'THIRD_PARTY_LICENSES.md'

precommit:
name: Pre-commit
Expand Down Expand Up @@ -116,6 +118,21 @@ jobs:
name: devsy-${{ steps.os.outputs.runner_os }}
path: dist/devsy-${{ steps.os.outputs.runner_os }}_*/devsy-${{ steps.os.outputs.runner_os }}-*

licenses:
name: Third-party licenses
needs: [changes, precommit, lint]
if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version-file: go.mod

- name: Check license policy and attribution
run: go run ./hack/licenses --check

Comment thread
coderabbitai[bot] marked this conversation as resolved.
integration-tests-unprivileged:
name: Test ${{ matrix.label }} on ${{ matrix.runner }}
needs: [changes, build-cli]
Expand Down Expand Up @@ -609,6 +626,7 @@ jobs:
- changes
- precommit
- lint
- licenses
- build-cli
- integration-tests-unprivileged
- integration-tests
Expand All @@ -619,6 +637,7 @@ jobs:
if [[ "${{ needs.changes.result }}" == "failure" ]] || \
[[ "${{ needs.precommit.result }}" == "failure" ]] || \
[[ "${{ needs.lint.result }}" == "failure" ]] || \
[[ "${{ needs.licenses.result }}" == "failure" ]] || \
[[ "${{ needs.build-cli.result }}" == "failure" ]] || \
[[ "${{ needs.integration-tests-unprivileged.result }}" == "failure" ]] || \
[[ "${{ needs.integration-tests.result }}" == "failure" ]]; then
Expand Down
3 changes: 3 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ archives:
format_overrides:
- goos: windows
formats: [zip]
files:
- LICENSE
- THIRD_PARTY_LICENSES.md
checksum:
name_template: checksums.txt
report_sizes: true
323 changes: 323 additions & 0 deletions THIRD_PARTY_LICENSES.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ tasks:
desc: tidy go.mod and go.sum
cmd: go mod tidy

cli:licenses:
desc: regenerate THIRD_PARTY_LICENSES.md and enforce the license allowlist
cmd: go run ./hack/licenses

cli:licenses:check:
desc: verify THIRD_PARTY_LICENSES.md is in sync and all licenses are allowed (CI)
cmd: go run ./hack/licenses --check

cli:lint:
desc: lint go code using golangci-lint
cmd: golangci-lint run ./...
Expand Down
225 changes: 225 additions & 0 deletions hack/licenses/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"maps"
"os"
"os/exec"
"path/filepath"
"sort"
)

const detectorVersion = "v0.10.0"

const (
archAMD64 = "amd64"
archARM64 = "arm64"
)

var releaseTargets = []struct{ os, arch string }{
{"linux", archAMD64},
{"linux", archARM64},
{"darwin", archAMD64},
{"darwin", archARM64},
{"windows", archAMD64},
{"windows", archARM64},
}

func main() {
check := flag.Bool("check", false,
"verify THIRD_PARTY_LICENSES.md is in sync instead of writing it")
flag.Parse()

if err := run(*check); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}

func run(check bool) error {
root, err := repoRoot()
if err != nil {
return err
}

hackDir := filepath.Join(root, "hack", "licenses")
outFile := filepath.Join(root, "THIRD_PARTY_LICENSES.md")

deps, err := moduleListJSON(root)
if err != nil {
return fmt.Errorf("collecting module dependencies: %w", err)
}

if check {
return checkInSync(hackDir, outFile, deps)
}

fmt.Fprintln(os.Stderr, "Generating", filepath.Base(outFile)+"...")
if err := runDetector(hackDir, deps, outFile); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Wrote", outFile)
return nil
}

func checkInSync(hackDir, outFile string, deps []byte) error {
tmp, err := os.CreateTemp("", "third-party-licenses-*.md")
if err != nil {
return err
}
target := tmp.Name()
if err := tmp.Close(); err != nil {
return err
}
defer func() { _ = os.Remove(target) }()

fmt.Fprintln(os.Stderr, "Generating", filepath.Base(outFile)+"...")
if err := runDetector(hackDir, deps, target); err != nil {
return err
}

current, err := os.ReadFile(outFile)
if err != nil {
return fmt.Errorf("reading %s: %w", outFile, err)
}
generated, err := os.ReadFile(target)
if err != nil {
return err
}
if !bytes.Equal(current, generated) {
diff := exec.Command("git", "--no-pager", "diff", "--no-index", "--", outFile, target)
diff.Stdout = os.Stderr
diff.Stderr = os.Stderr
_ = diff.Run()
return fmt.Errorf(
"THIRD_PARTY_LICENSES.md is out of date; run 'task cli:licenses' and commit the result",
)
}

fmt.Fprintln(os.Stderr, "THIRD_PARTY_LICENSES.md is up to date.")
return nil
}

func repoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", fmt.Errorf("could not locate go.mod above %q", dir)
}
dir = parent
}
}

type goModule struct {
Path string
Version string
Main bool
Dir string
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

type goPackage struct {
Module *goModule
}

func moduleListJSON(dir string) ([]byte, error) {
modules := map[string]goModule{}
for _, t := range releaseTargets {
mods, err := buildModules(dir, t.os, t.arch)
if err != nil {
return nil, fmt.Errorf("listing deps for %s/%s: %w", t.os, t.arch, err)
}
maps.Copy(modules, mods)
}

paths := make([]string, 0, len(modules))
for p := range modules {
paths = append(paths, p)
}
sort.Strings(paths)

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", "\t")
for _, p := range paths {
if err := enc.Encode(modules[p]); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}

func buildModules(dir, goos, goarch string) (map[string]goModule, error) {
cmd := exec.Command("go", "list", "-deps", "-mod=readonly", "-json", "./...")
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, err
}

modules := map[string]goModule{}
dec := json.NewDecoder(bytes.NewReader(out))
for {
var pkg goPackage
if err := dec.Decode(&pkg); err != nil {
if err == io.EOF {
break
}
return nil, err
}
m := pkg.Module
if m == nil || m.Main || m.Path == "" {
continue
}
modules[m.Path] = *m
}
return modules, nil
}

func runDetector(hackDir string, deps []byte, outPath string) error {
tmpDir, err := os.MkdirTemp("", "go-licence-detector-")
if err != nil {
return err
}
defer func() { _ = os.RemoveAll(tmpDir) }()

cmd := exec.Command("go", "run",
"go.elastic.co/go-licence-detector@"+detectorVersion,
"-rules", filepath.Join(hackDir, "rules.json"),
"-overrides", filepath.Join(hackDir, "overrides.ndjson"),
"-depsTemplate", filepath.Join(hackDir, "third-party-licenses.md.tmpl"),
"-depsOut", outPath,
)
cmd.Dir = tmpDir
cmd.Stdin = bytes.NewReader(deps)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("go-licence-detector: %w", err)
}

return normalizeTrailingNewline(outPath)
}

func normalizeTrailingNewline(path string) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
normalized := append(bytes.TrimRight(content, "\n"), '\n')
if bytes.Equal(content, normalized) {
return nil
}
return os.WriteFile(path, normalized, 0o644)
}
Empty file added hack/licenses/overrides.ndjson
Empty file.
16 changes: 16 additions & 0 deletions hack/licenses/rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"allowlist": [
"Apache-2.0",
"MIT",
"BSD-2-Clause",
"BSD-2-Clause-Views",
"BSD-3-Clause",
"ISC",
"MPL-2.0",
"CC-BY-4.0",
"CC0-1.0",
"Unlicense",
"0BSD",
"Zlib"
]
}
21 changes: 21 additions & 0 deletions hack/licenses/third-party-licenses.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{- define "depTable" -}}
| Dependency | Version | License |
| ---------- | ------- | ------- |
{{ range $i, $dep := . -}}
| [{{ $dep.Name }}]({{ with $dep.URL }}{{ . }}{{ else }}https://pkg.go.dev/{{ $dep.Name }}{{ end }}) | {{ with $dep.Version }}`{{ . }}`{{ else }}—{{ end }} | {{ $dep.LicenceType }} |
{{ end -}}
{{- end -}}
<!-- Generated by hack/licenses (go-licence-detector). DO NOT EDIT BY HAND. -->
<!-- Regenerate with: task cli:licenses -->

# Third-Party Licenses

The Devsy CLI is built with the open-source Go modules listed below. Each entry
links to the module and notes the license it is distributed under. Some
dependencies are only included on certain operating systems or architectures.

To regenerate this file after changing dependencies, run `task cli:licenses`.

## Dependencies

{{ template "depTable" .Direct }}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ For non-machine providers, Devsy can automatically kill the container its runnin

### Machine Providers

For machine providers, killing just the container within the remote machine is typically not enough as VMs still generate costs even if they are unused.
Instead, Devsy will install itself as a Daemon into the remote VM and track the activity from there. If there wasn't activity for a given amount of time, Devsy will automatically shutdown the machine or even delete it, based on what's cheaper for the given cloud provider.
Then when the developer wants to resume development, Devsy will restart or recreate the virtual machine.
For machine providers, killing just the container within the remote machine is typically not enough as VMs still generate costs even if they are unused. Instead, Devsy will install itself as a Daemon into the remote VM and track the activity from there. If there wasn't activity for a given amount of time, Devsy will automatically shutdown the machine or even delete it, based on whichever is more cost-effective for the given cloud provider. Then when the developer wants to resume development, Devsy restarts or recreates the virtual machine.

:::info
See [agent's development guide](../developing-providers/agent.mdx#machine-providers) to learn more about how inactivity-timeout works on the provider side.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ title: What are Workspaces?
sidebar_label: What are Workspaces?
---

A workspace in Devsy is a containerized development environment, that holds the source code of a project as well as the dependencies to work on that project, such as a compiler and debugger.
The underlying environment where the container runs will be created and managed through a Devsy provider. This allows Devsy to provide a consistent development experience no matter where the container is actually running, which can be a remote machine in a public cloud, localhost or even a Kubernetes cluster.
A workspace in Devsy is a containerized development environment that holds a project's source code along with the dependencies needed to work on it, such as a compiler and debugger. The underlying environment where the container runs is created and managed through a Devsy provider. This gives every engineer a consistent development experience no matter where the container actually runs — a remote machine in a public cloud, localhost, or a Kubernetes cluster.

To configure the development container, Devsy reuses the [devcontainer.json](https://containers.dev/) specification, which is also used by other popular tools, such as [VS Code dev containers](https://code.visualstudio.com/docs/devcontainers/containers) or [GitHub Codespaces](https://github.com/features/codespaces).
This means you can already reuse projects that use this configuration to spin up a workspace in Devsy. If no configuration is found, Devsy will automatically try to find out what programming language is used and provide an appropriate template.
To configure the development container, Devsy reuses the [devcontainer.json](https://containers.dev/) specification, which is also used by other popular tools such as [VS Code dev containers](https://code.visualstudio.com/docs/devcontainers/containers) and [GitHub Codespaces](https://github.com/features/codespaces). This means any project that already uses this configuration can spin up a workspace in Devsy with no extra setup. If no configuration is found, Devsy detects the project's programming language and provides an appropriate template.

A workspace in Devsy can be stopped and restarted without losing its state. This allows you to install additional programs or change configuration without the need to reconfigure the container.
Depending on the Provider, Devsy will also automatically determine when a workspace is currently not be used and shutdown any unused resources to save costs.
A workspace can be stopped and restarted without losing its state, so you can install additional programs or change configuration without reconfiguring the container. Depending on the provider, Devsy also detects when a workspace is no longer in use and shuts down idle resources to keep infrastructure costs down.
Loading
Loading