diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json
new file mode 100644
index 0000000..11027ba
--- /dev/null
+++ b/.devcontainer/devcontainer-lock.json
@@ -0,0 +1,19 @@
+{
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "version": "2.17.0",
+ "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c",
+ "integrity": "sha256:25b9f05705ffba7dbe503230ac76081419306f8c8bc88e0ce78c4ecd99a0c78c"
+ },
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "1.3.4",
+ "resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032",
+ "integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032"
+ },
+ "ghcr.io/devcontainers/features/node:1": {
+ "version": "1.7.1",
+ "resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
+ "integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..81b7867
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,33 @@
+{
+ "name": "Corteca CLI",
+ "image": "mcr.microsoft.com/devcontainers/go:1.24",
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {
+ "version": "latest",
+ "moby": false
+ },
+ "ghcr.io/devcontainers/features/node:1": {},
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "none",
+ "golangciLintVersion": "latest"
+ }
+ },
+ "postCreateCommand": "sudo apt-get update && sudo apt-get install -y msitools ",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "streetsidesoftware.code-spell-checker",
+ "golang.Go",
+ "davidanson.vscode-markdownlint",
+ "github.vscode-github-actions",
+ "ms-azuretools.vscode-containers",
+ "ms-vscode.makefile-tools"
+ ]
+ }
+ },
+ "remoteEnv": {
+ "PODMAN_USERS": "keep-id",
+ "IN_DEVCONTAINER": "1"
+ },
+ "containerUser": "vscode"
+}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 600c3cd..519b92c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,14 +11,68 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0 # required for git describe --tags used in VERSION
- name: Set up Go
- uses: actions/setup-go@v5
+ uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build
run: make
+
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0 # required for git describe --tags used in VERSION
+
+ - name: Set up Go
+ uses: actions/setup-go@v6
+ with:
+ go-version-file: go.mod
+
+ - name: Run tests
+ run: make test
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: ./coverage.out
+ flags: unittests
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Set up Go
+ uses: actions/setup-go@v6
+ with:
+ go-version-file: go.mod
+
+ - name: Go lint
+ uses: golangci/golangci-lint-action@v7
+ with:
+ version: latest
+
+ - name: YAML lint
+ run: |
+ pip install --quiet yamllint
+ yamllint -c .yamllint.yaml .
+
+ - name: Markdown lint
+ uses: DavidAnson/markdownlint-cli2-action@v18
+ with:
+ config: .markdownlint.yaml
+ globs: '**/*.md'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..6b9145f
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,72 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ release:
+ name: Build and publish
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # create GitHub Release and upload assets
+ packages: write # push to GHCR
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0 # required for git describe --tags
+
+ - name: Set up Go
+ uses: actions/setup-go@v6
+ with:
+ go-version-file: go.mod
+
+ - name: Install nfpm
+ run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.40.0
+
+ - name: Get version
+ id: version
+ run: echo "version=$(git describe --tags)" >> "$GITHUB_OUTPUT"
+
+ - name: Build packages
+ run: |
+ make deb DESTARCH=amd64
+ make deb DESTARCH=arm64
+ make rpm DESTARCH=amd64
+ make rpm DESTARCH=arm64
+ make osx DESTARCH=amd64
+ make osx DESTARCH=arm64
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/packages/*
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push container image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: corteca-cli.Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ build-args: |
+ VERSION=${{ steps.version.outputs.version }}
+ tags: |
+ ghcr.io/${{ github.repository }}:latest
+ ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 3d35284..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Tests
-
-on:
- push:
- pull_request:
-
-jobs:
- test:
- name: Tests
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0 # required for git describe --tags used in VERSION
-
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
-
- - name: Run tests
- run: make test
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v5
- with:
- files: ./coverage.out
- flags: unittests
- token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 7df794d..91ff407 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@
/dist
nfpm.yaml
coverage.out
+
+.DS_Store
+.claude
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..1e98b6d
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,12 @@
+version: "2"
+
+run:
+ timeout: 5m
+
+linters:
+ enable:
+ - errcheck
+ - govet
+ - ineffassign
+ - staticcheck
+ - unused
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000..553fde5
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,3 @@
+default: true
+MD013: false # line length — not enforced
+MD033: false # inline HTML — used in README (logo div)
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
index 0000000..794a3dc
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,7 @@
+extends: default
+
+rules:
+ line-length:
+ max: 120
+ truthy:
+ allowed-values: ['true', 'false']
diff --git a/README.md b/README.md
index 45db6b7..27ca2a0 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
+
+
# Corteca Developer Toolkit
[](https://github.com/nokia/corteca-cli/actions/workflows/build.yml)
-[](https://github.com/nokia/corteca-cli/actions/workflows/test.yml)
[](https://codecov.io/gh/nokia/corteca-cli)
Corteca Developer Toolkit is a command-line tool for building, packaging, and
@@ -69,95 +70,24 @@ supported:
Devices and the sequences to run on them are configured in `corteca.yaml` and
can be targeted by name when running `corteca exec`.
-
-
-## Prerequisites
-
-| Requirement | Version | Notes |
-| ----------- | -------- | ---------------------------------------------- |
-| Go | ≥ 1.21 | Required to build from source |
-| Docker | ≥ 23.0 | Required to build application container images |
-| Docker BuildKit | ≥ 0.11 | Required for `docker build --output` |
-| make | any | Used to drive the build and install targets |
-
## Build
-Clone the repository and run:
-
-```bash
-$ make
-```
-
-The compiled binary is placed in the `dist/` directory.
-
-### Build using Docker
-
-If you do not have a local Go toolchain, you can build entirely inside Docker
-(BuildKit is required):
-
-```bash
-$ docker build --output ./dist .
-```
-
-#### Installing Docker BuildKit
-
-On Docker Engine < 23.0, BuildKit must be enabled manually. On Ubuntu 22.04:
-
-```bash
-$ sudo apt-get install docker-buildx-plugin
-```
-
-Then either prefix each `docker build` invocation with `DOCKER_BUILDKIT=1`, or
-follow the [official instructions](https://docs.docker.com/build/buildkit/#getting-started)
-to enable it globally.
-
-> For the full build guide see [doc/BUILD.md](doc/BUILD.md).
+For build instructions see [doc/BUILD.md](doc/BUILD.md).
## Install
-### Install manually from source
-
-You can run the below command to build the application and install the binary to
-the `/usr/bin` folder and the default configuration files to the `/etc/corteca`
-folder:
-
-```bash
-$ sudo make install
-```
-
-To remove a previous manual installation
-
-```bash
-$ sudo make uninstall
-```
-
-You can customize the destination folder by overriding the `$DESTDIR`
-environment variable:
-
-```bash
-# no sudo required; will be installed to ~/.local/share/usr/bin
-$ DESTDIR=~/.local/share make install
-```
-
-### Install with package manager
-
-If you are using debian/ubuntu or redhat-based distributions, you can create a relevant package and let your package manager handle installation. E.g. for ubuntu:
-
-```bash
-$ make deb
-$ make rpm
-```
+For installation instructions see [doc/INSTALL.md](doc/INSTALL.md).
## Getting Started
The fastest way to get up and running is to create a project, build it, and
publish it to a local registry in three commands:
-```bash
-$ corteca create my-app # scaffold a new application project
-$ cd my-app
-$ corteca build aarch64 # build an OCI image for aarch64
-$ corteca publish localRegistry # push it to a local OCI registry
+```shell
+corteca create my-app # scaffold a new application project
+cd my-app
+corteca build aarch64 # build an OCI image for aarch64
+corteca publish localRegistry # push it to a local OCI registry
```
For a step-by-step walkthrough — including how to configure a device target and
@@ -180,7 +110,7 @@ directory, so `corteca` commands work from any subdirectory of a project.
The `corteca config` command can be used to inspect or modify any value
without editing YAML by hand:
-```bash
+```shell
corteca config get publish # show all publish targets
corteca config set app.version 1.1 # update a value
```
@@ -198,14 +128,36 @@ reference](doc/Configuration.md).
| [`corteca publish`](doc/reference/corteca_publish.md) | Upload or serve the build artifact via a configured publish target |
| [`corteca exec`](doc/reference/corteca_exec.md) | Run a named deployment sequence on a configured device |
| [`corteca config`](doc/reference/corteca_config.md) | Inspect or modify configuration values |
+| [`corteca config add`](doc/reference/corteca_config_add.md) | Add a configuration value |
+| [`corteca config get`](doc/reference/corteca_config_get.md) | Get a configuration value |
+| [`corteca config set`](doc/reference/corteca_config_set.md) | Set a configuration value |
| [`corteca regen`](doc/reference/corteca_regen.md) | Regenerate template-derived project files |
-For a broader overview of all commands, flags, and usage patterns see
-[doc/USAGE.md](doc/USAGE.md).
-
Every command also accepts `--help` for inline usage information:
-```bash
+```shell
corteca --help
corteca build --help
```
+
+## Application Templates
+
+`corteca` scans for available templates in a `templates/` folder that resides in the global config folder (defaults to `$HOME/.config/corteca`). You can override the global config folder using the `--configRoot` flag.
+
+Each subfolder containing a `.template-info.yaml` file will be treated as a template and will be rendered with all configuration variables available for rendering. For more information, see the [Configuration](#configuration) section above.
+
+Here is an overview of the content of `.template-info.yaml`:
+
+```yaml
+# Name of the template
+name: "foobar"
+description: "Short template description"
+dependencies:
+ compile: [ foo ] # Dependencies needed during compile
+ runtime: [ bar ] # Dependencies needed during runtime
+# Dynamic files that need regeneration during the project's lifecycle
+regenFiles:
+ .corteca/foo.template: foo # Mapping template filepath to regenerated filepath
+# A collection of custom options available for rendering
+options:
+```
diff --git a/cmd/build.go b/cmd/build.go
index 515ae2f..420c11f 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -96,7 +96,7 @@ func doBuildApp(selectedArchitecture string) {
if rootfsPath == "" {
assertOperation(fmt.Sprintf("building '%s'", selectedArchitecture), builder.BuildRootFS(projectRoot, rootfsBuildPath, settings, config.App, config.Build))
} else {
- fsutil.ExtractTarball(rootfsPath, rootfsBuildPath)
+ assertOperation(fmt.Sprintf("extracting rootfs from '%s'", rootfsPath), fsutil.ExtractTarball(rootfsPath, rootfsBuildPath))
}
// STEP 2: validate rootfs
diff --git a/cmd/config.go b/cmd/config.go
index 83103f7..e165072 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -113,7 +113,7 @@ func doGetConfigValue(key string) {
assertOperation("retrieving config value", err)
enc := yaml.NewEncoder(os.Stdout)
enc.SetIndent(configuration.YamlIndentation)
- enc.Encode(field)
+ assertOperation("encoding config value", enc.Encode(field))
}
func doSetConfigValue(key, value string, append bool) {
@@ -144,7 +144,7 @@ func doEvalConfigValue(expr string) {
field := configuration.T(expr)
enc := yaml.NewEncoder(os.Stdout)
enc.SetIndent(configuration.YamlIndentation)
- enc.Encode(field.String())
+ assertOperation("encoding config value", enc.Encode(field.String()))
}
func validConfigArgsFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
diff --git a/cmd/doc.go b/cmd/doc.go
index 36107a2..2b1e001 100644
--- a/cmd/doc.go
+++ b/cmd/doc.go
@@ -48,7 +48,7 @@ func doGenerateDocumentation(cmdNames []string) {
continue
}
cmdFile := createMdFile(cmd)
- defer cmdFile.Close()
+ defer func() { _ = cmdFile.Close() }()
err = generateMarkdown(cmd, cmdFile, false)
assertOperation("generating documentation for corteca commands", err)
}
@@ -67,7 +67,7 @@ func doGenerateDocumentation(cmdNames []string) {
index--
cmdFile := createMdFile(cmd)
- defer cmdFile.Close()
+ defer func() { _ = cmdFile.Close() }()
err = generateMarkdown(cmd, cmdFile, false)
assertOperation("generating documentation for specific corteca command(s)", err)
}
@@ -82,30 +82,38 @@ func doGenerateDocumentation(cmdNames []string) {
if len(notMatchedCmds) == 0 {
fmt.Println("Cortecacli's commands documentation has generated successfuly!")
} else {
- fmt.Fprintf(os.Stdout, "The following commands are not part of cortecacli: %s\n", notMatchedCmds)
+ _, _ = fmt.Fprintf(os.Stdout, "The following commands are not part of cortecacli: %s\n", notMatchedCmds)
}
} else {
- fmt.Fprintf(os.Stdout, "No command documentation was generated\nThe following commands are not part of cortecacli: %s\n", notMatchedCmds)
+ _, _ = fmt.Fprintf(os.Stdout, "No command documentation was generated\nThe following commands are not part of cortecacli: %s\n", notMatchedCmds)
}
}
func generateMarkdown(cmd *cobra.Command, cmdFile *os.File, isSubcmd bool) error {
// Write command usage
if isSubcmd {
- _, writeToFileErr = cmdFile.WriteString(fmt.Sprintf("### Corteca %s %s\n\n%s\n\n", cmd.Parent().Name(), cmd.Name(), cmd.Long))
+ _, writeToFileErr = fmt.Fprintf(cmdFile, "### Corteca %s %s\n\n%s\n\n", cmd.Parent().Name(), cmd.Name(), cmd.Long)
} else {
- _, writeToFileErr = cmdFile.WriteString(fmt.Sprintf("## Corteca %s\n\n%s\n\n", cmd.Name(), cmd.Long))
+ _, writeToFileErr = fmt.Fprintf(cmdFile, "## Corteca %s\n\n%s\n\n", cmd.Name(), cmd.Long)
}
if writeToFileErr != nil {
return writeToFileErr
}
- cmdFile.WriteString(fmt.Sprintf("### Usage:\n\n```\n%s\n```\n\n", cmd.UseLine()))
- cmdFile.WriteString(fmt.Sprintf("### Flags:\n\n```\n%s%s\n```\n\n", cmd.InheritedFlags().FlagUsages(), cmd.LocalFlags().FlagUsages()))
- cmdFile.WriteString(fmt.Sprintf("### Example:\n\n```\n%s\n```\n\n", cmd.Example))
+ if _, err := fmt.Fprintf(cmdFile, "### Usage:\n\n```\n%s\n```\n\n", cmd.UseLine()); err != nil {
+ return err
+ }
+ if _, err := fmt.Fprintf(cmdFile, "### Flags:\n\n```\n%s%s\n```\n\n", cmd.InheritedFlags().FlagUsages(), cmd.LocalFlags().FlagUsages()); err != nil {
+ return err
+ }
+ if _, err := fmt.Fprintf(cmdFile, "### Example:\n\n```\n%s\n```\n\n", cmd.Example); err != nil {
+ return err
+ }
// Append subcommands to parent command's documentation, if any
if len(cmd.Commands()) > 0 {
- cmdFile.WriteString("## Subcommands:\n\n")
+ if _, err := fmt.Fprintf(cmdFile, "## Subcommands:\n\n"); err != nil {
+ return err
+ }
for _, subCmd := range cmd.Commands() {
// Recursive call for nested subcommands
err := generateMarkdown(subCmd, cmdFile, true)
@@ -113,7 +121,9 @@ func generateMarkdown(cmd *cobra.Command, cmdFile *os.File, isSubcmd bool) error
}
}
- cmdFile.WriteString("\n")
+ if _, err := fmt.Fprintf(cmdFile, "\n"); err != nil {
+ return err
+ }
return nil
}
diff --git a/cmd/exec.go b/cmd/exec.go
index 1c0bf36..d54d2aa 100644
--- a/cmd/exec.go
+++ b/cmd/exec.go
@@ -33,7 +33,7 @@ var logFile string
var publishTargetName string
func init() {
- execCmd.RegisterFlagCompletionFunc("artifact", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ _ = execCmd.RegisterFlagCompletionFunc("artifact", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"tar.gz, tar"}, cobra.ShellCompDirectiveFilterFileExt
})
rootCmd.AddCommand(execCmd)
diff --git a/cmd/publish.go b/cmd/publish.go
index ecc0a94..3f8f44e 100644
--- a/cmd/publish.go
+++ b/cmd/publish.go
@@ -21,11 +21,6 @@ import (
"github.com/spf13/cobra"
)
-const (
- ociSuffix = "oci"
- rootfsSuffix = "rootfs"
- artifactNotFoundMessage = "No build artifact found for [%s,%s]"
-)
var publishCmd = &cobra.Command{
Use: "publish TARGET",
@@ -49,7 +44,7 @@ type RegistryConfig struct {
func init() {
publishCmd.PersistentFlags().BoolVar(&skipLocalConfig, "global", false, "Affect global config & ignore any project-local configuration")
publishCmd.PersistentFlags().StringVarP(&artifact, "artifact", "a", "", "Specify the path to a an artifact to publish")
- publishCmd.RegisterFlagCompletionFunc("artifact", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ _ = publishCmd.RegisterFlagCompletionFunc("artifact", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"tar.gz"}, cobra.ShellCompDirectiveFilterFileExt
})
rootCmd.AddCommand(publishCmd)
@@ -65,19 +60,19 @@ func doPublishApp(targetName string, wait bool) {
switch target.Method {
case "listen":
serverConfig := configuration.HttpServerEndpoint{}
- target.Decode(&serverConfig)
+ assertOperation("decoding publish config", target.Decode(&serverConfig))
handleListen(serverConfig, wait)
case "put":
clientConfig := configuration.HttpClientEndpoint{}
- target.Decode(&clientConfig)
+ assertOperation("decoding publish config", target.Decode(&clientConfig))
handlePut(clientConfig, artifact)
case "push":
clientConfig := configuration.HttpClientEndpoint{}
- target.Decode(&clientConfig)
+ assertOperation("decoding publish config", target.Decode(&clientConfig))
handlePush(clientConfig, artifact)
case "registry-v2":
registryConfig := RegistryConfig{}
- target.Decode(®istryConfig)
+ assertOperation("decoding publish config", target.Decode(®istryConfig))
handleRegistry(registryConfig, wait)
default:
failOperation(fmt.Sprintf("unknown publish method '%v'", target.Method))
@@ -93,7 +88,7 @@ func handleListen(target configuration.HttpServerEndpoint, wait bool) {
assertOperation("starting server", err)
if wait {
waitForInterruptSignal()
- srv.Shutdown(context.Background())
+ assertOperation("shutting down server", srv.Shutdown(context.Background()))
} else {
fmt.Printf("Serving %v on %v\n", serverRoot, u.String())
}
diff --git a/cmd/root.go b/cmd/root.go
index b8acbd3..0717185 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -61,20 +61,22 @@ func init() {
rootCmd.SetVersionTemplate("{{.Short}} v{{.Version}}\n")
rootCmd.CompletionOptions.HiddenDefaultCmd = true
rootCmd.PersistentFlags().StringArrayVarP(&configOverrides, "config", "c", []string{}, "Override a configuration value in the form of a 'key=value' pair")
- rootCmd.RegisterFlagCompletionFunc("config", validConfigArgsFunc)
+ _ = rootCmd.RegisterFlagCompletionFunc("config", validConfigArgsFunc)
rootCmd.PersistentFlags().StringVarP(&systemConfigRoot, "configRoot", "r", systemConfigRoot, "Override configuration root folder")
- rootCmd.RegisterFlagCompletionFunc("configRoot", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ _ = rootCmd.RegisterFlagCompletionFunc("configRoot", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
})
rootCmd.PersistentFlags().StringVarP(&projectRoot, "projectRoot", "C", projectRoot, "Specify project root folder")
- rootCmd.RegisterFlagCompletionFunc("projectRoot", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ _ = rootCmd.RegisterFlagCompletionFunc("projectRoot", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
})
rootCmd.PersistentFlags().BoolVar(&tui.DisableColoredOutput, "no-color", false, "Disables colored stdout|stderr output")
}
func Execute() {
- rootCmd.Execute()
+ if err := rootCmd.Execute(); err != nil {
+ os.Exit(1)
+ }
}
func readLocalConfiguration() {
@@ -206,16 +208,6 @@ func requireProjectContext() {
configuration.GetCmdContext().Build = &config.Build
}
-func getAppNameFromArtifact(artifactPath string) string {
- artifactName := filepath.Base(artifactPath)
- splitedArtifactName := strings.SplitN(artifactName, "-", 2)
- // If artifact name doesn't contain hyphens we consider the form "appName.tar.gz", else we consider App.Name the string up to the first hyphen.
- if len(splitedArtifactName) == 1 {
- return strings.TrimSuffix(splitedArtifactName[0], ".tar.gz")
- } else {
- return splitedArtifactName[0]
- }
-}
func requireBuildArtifact() {
if artifact != "" {
diff --git a/corteca-cli.Dockerfile b/corteca-cli.Dockerfile
new file mode 100644
index 0000000..5853f33
--- /dev/null
+++ b/corteca-cli.Dockerfile
@@ -0,0 +1,25 @@
+# syntax=docker/dockerfile:1
+#
+# Runtime container image for corteca CLI.
+# Includes the corteca binary and Docker CLI (required for `docker buildx build`).
+#
+# Requires pre-built binaries in dist/bin/ (produced by `make`).
+# See doc/BUILD.md for local build and test instructions.
+
+ARG DOCKER_VERSION=27
+
+FROM docker:${DOCKER_VERSION}-cli
+
+LABEL org.opencontainers.image.title="corteca-cli" \
+ org.opencontainers.image.description="Corteca Developer Toolkit — CLI for building and deploying Corteca applications" \
+ org.opencontainers.image.source="https://github.com/nokia/corteca-cli" \
+ org.opencontainers.image.licenses="BSD-3-Clause"
+
+RUN apk add --no-cache ca-certificates
+
+ARG TARGETARCH
+ARG VERSION
+COPY dist/bin/corteca-linux-${TARGETARCH}-${VERSION} /usr/local/bin/corteca
+COPY data/ /etc/corteca/
+
+ENTRYPOINT ["corteca"]
diff --git a/data/templates/c/.template-info.yaml b/data/templates/c/.template-info.yaml
index f91aeb6..7c62732 100644
--- a/data/templates/c/.template-info.yaml
+++ b/data/templates/c/.template-info.yaml
@@ -1,6 +1,6 @@
name: "c"
description: "An application template for the C language."
dependencies:
- compile: [ gcc, make, g++ ]
- runtime: [ json-c ]
+ compile: [gcc, make, g++]
+ runtime: [json-c]
base: _base
diff --git a/data/templates/cpp/.template-info.yaml b/data/templates/cpp/.template-info.yaml
index 8d36e24..9c71500 100644
--- a/data/templates/cpp/.template-info.yaml
+++ b/data/templates/cpp/.template-info.yaml
@@ -1,6 +1,6 @@
name: "cpp"
description: "An application template for the C++ language."
dependencies:
- compile: [ g++, make ]
- runtime: [ libstdc++, json-c ]
+ compile: [g++, make]
+ runtime: [libstdc++, json-c]
base: _base
diff --git a/data/templates/go/.template-info.yaml b/data/templates/go/.template-info.yaml
index 7ef3b0b..9bc13ed 100644
--- a/data/templates/go/.template-info.yaml
+++ b/data/templates/go/.template-info.yaml
@@ -1,6 +1,6 @@
name: "go"
description: "An application template for the Go language."
dependencies:
- compile: [ go, make ]
- runtime: [ json-c ]
+ compile: [go, make]
+ runtime: [json-c]
base: _base
diff --git a/doc/BUILD.md b/doc/BUILD.md
new file mode 100644
index 0000000..135047d
--- /dev/null
+++ b/doc/BUILD.md
@@ -0,0 +1,85 @@
+# Build
+
+## Prerequisites
+
+| Requirement | Version | Notes |
+| ----------- | -------- | ---------------------------------------------- |
+| Go | ≥ 1.21 | Required to build from source |
+| Docker | ≥ 23.0 | Required to build application container images |
+| Docker BuildKit | ≥ 0.11 | Required for `docker build --output` |
+| make | any | Used to drive the build and install targets |
+
+## Build locally
+
+Clone the repository and run:
+
+```shell
+make
+```
+
+The compiled binary is placed in the `dist/` directory.
+
+## Build using Docker
+
+If you do not have a local Go toolchain, you can build entirely inside Docker
+(BuildKit is required):
+
+```shell
+docker build --output ./dist .
+```
+
+### Installing Docker BuildKit
+
+On Docker Engine < 23.0, BuildKit must be enabled manually. On Ubuntu 22.04:
+
+```shell
+sudo apt-get install docker-buildx-plugin
+```
+
+Then either prefix each `docker build` invocation with `DOCKER_BUILDKIT=1`, or
+follow the [official instructions](https://docs.docker.com/build/buildkit/#getting-started)
+to enable it globally.
+
+## Build the corteca-cli container image
+
+`corteca-cli.Dockerfile` produces a self-contained runtime image that bundles
+the corteca binary together with the Docker CLI (required for
+`docker buildx build`). This is the image published to GHCR on every merge to
+`main`.
+
+### Build the image locally
+
+The image expects pre-built binaries in `dist/bin/`. Build them first, then
+build the image:
+
+```shell
+VERSION=$(git describe --tags 2>/dev/null || echo "dev")
+
+# Build the Linux binary for your host architecture (amd64 or arm64)
+make DESTOS=linux DESTARCH=amd64
+
+docker build \
+ -f corteca-cli.Dockerfile \
+ --platform linux/amd64 \
+ --build-arg VERSION=$VERSION \
+ -t corteca-cli:local .
+```
+
+### Test the image
+
+Corteca needs access to the host Docker daemon at runtime, so mount the Docker
+socket:
+
+```shell
+# Verify the version reported by the binary
+docker run --rm \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ corteca-cli:local --version
+
+# Open an interactive session with a project directory mounted
+docker run --rm -it \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -v $(pwd):/workspace \
+ -w /workspace \
+ corteca-cli:local
+```
diff --git a/doc/Configuration.md b/doc/Configuration.md
index 977cc35..1436cf0 100644
--- a/doc/Configuration.md
+++ b/doc/Configuration.md
@@ -79,7 +79,7 @@ app:
runtime: {} # OCI runtime spec overrides (see OCI Runtime Specification)
```
-### Fields
+### App Fields
| Field | Type | Description |
| ------- | ------ | ------------- |
@@ -119,7 +119,7 @@ build:
args: [ , … ] # Arguments passed to the QEMU helper
```
-### Fields
+### Build Fields
| Field | Type | Default | Description |
| ------- | ------ | --------- | ------------- |
@@ -275,10 +275,10 @@ meaningful.
The fields shared by all device types are:
-| Field | Type | Description |
-| ------- | ------ | ------------- |
+| Field | Type | Description |
+| ------- | ------ | ------------- |
| `addr` | string (template) | **(Required)** Connection URL. Its scheme (`ssh://`, `cwmp://`, `cwmps://`) selects the device type. |
-| `architecture` | string | Architecture identifier matching an entry in `build.architectures`. |
+| `architecture` | string | Architecture identifier matching an entry in `build.architectures`. |
---
@@ -299,13 +299,13 @@ devices:
privateKeyFile: # Path to a PEM-encoded private key
```
-| Field | Type | Required | Description |
-| ------- | ------ | -------- | ------------- |
+| Field | Type | Required | Description |
+| ------- | ------ | -------- | ------------- |
| `addr` | string (template) | Yes | SSH connection URL. Must use the `ssh://` scheme. Username and password may be embedded (e.g., `ssh://user:pass@host`). |
-| `username` | string (template) | No | SSH username. Overrides any username embedded in `addr`. |
-| `password` | string (template) | No | SSH password. Falls back to the password in `addr`, then prompts interactively if absent. |
-| `password2` | string (template) | No | Secondary (escalation) password required by certain device firmware (e.g., for Quagga deactivation). |
-| `privateKeyFile` | string (template) | No | Path to a PEM-encoded private key file. When provided, public-key authentication is attempted first. |
+| `username` | string (template) | No | SSH username. Overrides any username embedded in `addr`. |
+| `password` | string (template) | No | SSH password. Falls back to the password in `addr`, then prompts interactively if absent. |
+| `password2` | string (template) | No | Secondary (escalation) password required by certain device firmware (e.g., for Quagga deactivation). |
+| `privateKeyFile` | string (template) | No | Path to a PEM-encoded private key file. When provided, public-key authentication is attempted first. |
---
@@ -371,7 +371,7 @@ sequences:
| Field | Type | Default | Description |
| ------- | ------ | --------- | ------------- |
| `cmd` | string (template) | — | **(Required)** Command to execute. Interpreted differently per device type; see below. |
-| `delay` | string (duration) | `0` | Time to wait after the step (or each retry) completes. Refer to [this](https://pkg.go.dev/time#ParseDuration) for syntax. |
+| `delay` | string (duration) | `0` | Time to wait after the step (or each retry) completes. Refer to [this](https://pkg.go.dev/time#ParseDuration) for syntax. |
| `timeout` | string (duration) | 5 minutes | Maximum execution time. If exceeded, the step is considered failed. Same syntax as `delay`. |
| `retries` | uint | `0` | How many additional attempts to make if the step fails. |
| `ignoreFailure` | bool | `false` | When `true`, a failing step does not abort the sequence. |
@@ -434,7 +434,7 @@ templates:
Each key is a path to a Go template file (relative to the `.corteca` directory inside the project), and the corresponding value is the output path where the rendered result should be written.
-### Fields
+### Templates Fields
| Field | Description |
| ------- | ------------- |
@@ -456,7 +456,7 @@ ${ ["":].[:[:]] }
```
| Part | Description |
-|------|-------------|
+| --- | --- |
| `"":` | Optional literal string prepended to each resolved value. |
| `.` | Dot-separated path into the context (e.g., `.app.name`, `.env.HOME`). |
| `:` | For **map** fields: separator between each key and its value (default: space). |
@@ -465,7 +465,7 @@ ${ ["":].[:[:]] }
Examples:
| Expression | Result |
-|------------|--------|
+| --- | --- |
| `${ .app.name }` | Value of `app.name` |
| `${ .app.version }` | Value of `app.version` |
| `${ "v":.app.version }` | `v` prepended to `app.version` (e.g., `v1.2.0`) |
@@ -481,7 +481,7 @@ Template fields can reference other template fields; circular references will ca
### `.app` — Application Settings
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.app.name` | string | Application name. |
| `.app.author` | string | Application author. |
| `.app.version` | string | Application version string. |
@@ -494,7 +494,7 @@ Template fields can reference other template fields; circular references will ca
### `.build` — Build Settings
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.build.default` | string | Default target architecture name. |
| `.build.options.outputType` | string | Build output type (`rootfs`, `oci`, or `docker`). |
| `.build.options.debug` | bool | Whether debug mode is enabled. |
@@ -507,14 +507,14 @@ Template fields can reference other template fields; circular references will ca
### `.arch` and `.platform` — Current Architecture
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.arch` | string | Name of the target architecture currently being built (e.g., `aarch64`). Set during `build`, `regen`, and `exec`. |
| `.platform` | string | Docker platform string for the current architecture (e.g., `linux/arm64`). Set alongside `.arch`. |
### `.artifact` — Build Artifact
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.artifact` | string | Absolute path to the current build artifact (e.g., `/project/dist/app-1.0-aarch64-oci.tar`). Set during `publish` and `exec`. |
### `.publish` — Active Publish Target
@@ -522,7 +522,7 @@ Template fields can reference other template fields; circular references will ca
Populated when a publish target is selected (during `publish` or `exec` with a publish target).
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.publish.name` | string | Alias name of the active publish target. |
| `.publish.method` | string | Delivery method of the active publish target. |
@@ -531,7 +531,7 @@ Populated when a publish target is selected (during `publish` or `exec` with a p
Populated when a device is selected (during `exec`).
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.device.name` | string | Alias name of the active device. |
| `.device.addr` | string (template) | Connection URL of the device. |
| `.device.architecure` | string | Architecture identifier of the device. |
@@ -539,7 +539,7 @@ Populated when a device is selected (during `exec`).
### `.env` — Host Environment Variables
| Field | Type | Description |
-|-------|------|-------------|
+| --- | --- | --- |
| `.env` | map | All host environment variables at the time the command was invoked. Access individual variables as `.env.` (e.g., `.env.HOME`, `.env.PATH`). |
---
diff --git a/doc/GettingStarted.md b/doc/GettingStarted.md
index 416bf92..3b25fed 100644
--- a/doc/GettingStarted.md
+++ b/doc/GettingStarted.md
@@ -1,10 +1,11 @@
# Getting Started
## Install corteca cli
+
To install or build from source, you can follow the [relevant instructions](../README.md#install) in the main README file. To make sure everything is in place, run the following command in any folder.
-```bash
-$ corteca config get
+```shell
+corteca config get
```
You should see your default system-wide configuration.
@@ -18,8 +19,8 @@ on how to write templates, refer to the [relevant guide](templates.md). We will
Change to the parent folder where you want your application directory to be
created and run the following command:
-```bash
-$ corteca create test_app
+```shell
+corteca create test_app
```
Follow the prompt instructions to populate the application settings. A
@@ -27,7 +28,7 @@ folder named after the given parameter (test_app) in the above command will be
created with all the generated files inside. The structure of the folder should
be similar to the below:
-```bash
+```shell
test_app
├── .corteca
│ ├── ADF.template
@@ -67,20 +68,22 @@ generated.
Corteca cli provides a publish functionality to help users upload their produced artifacts in an OCI registry. There are some registries set up by default in corteca that can be used as a template from the user to set up the target registry. You can check the current configuration for publish using
-```bash
-$ corteca config get publish
+```shell
+corteca config get publish
```
#### Publish in corteca local registry
+
User can use the already set up local registry.
`corteca publish localRegistry armv7l`
-Corteca will set up a local oci registry and will upload the artifact. (check with https://localhost:8080/v2/_catalog). If this registry is needed to be visible outside the host device, publicURL needs to be changed to the device IP.
+Corteca will set up a local oci registry and will upload the artifact. (check with [localhost:8080/v2/_catalog](https://localhost:8080/v2/_catalog)). If this registry is needed to be visible outside the host device, publicURL needs to be changed to the device IP.
`corteca config set publish.localRegistry.publicURL http://192.168.18.2:8080`
#### Publish in custom registry
+
User can add his own registry using *corteca config*
`corteca config add publish "{myregistry: { addr: https://my-registry.com, auth: basic, username: user1, password: pass1, method: push}}"`
@@ -94,9 +97,8 @@ After the registry is set up, the application can be uploaded.
For more info on corteca publish check [corteca publish](reference/corteca_publish.md) guide.
## Configuring application
-Application configuration is noted in the corteca.yaml file placed in application's folder when created. The *app* section -related to the create command- specifies app settings you can alter during the build process.
-The complete set of settings for Corteca Cli is created by combining this file with /etc/corteca/corteca.yaml.
-Corteca cli provides methods to show or edit these settings using the *config* method. You can also check or edit the settings values using the auto complete of Corteca cli by pressing key twice like in a shell.
+
+Application configuration is noted in the corteca.yaml file placed in application's folder when created. The *app* section -related to the create command- specifies app settings you can alter during the build process. The complete set of settings for Corteca Cli is created by combining this file with /etc/corteca/corteca.yaml. Corteca cli provides methods to show or edit these settings using the *config* method. You can also check or edit the settings values using the auto complete of Corteca cli by pressing key twice like in a shell.
`corteca config get app`
@@ -108,6 +110,4 @@ To set a value
`corteca config set app.version 1.0.1`
-
-
For more detailed guide on config get/set/add please refer to [corteca config](reference/corteca_config.md) guide.
diff --git a/doc/INSTALL.md b/doc/INSTALL.md
new file mode 100644
index 0000000..123b687
--- /dev/null
+++ b/doc/INSTALL.md
@@ -0,0 +1,34 @@
+# Install
+
+## Install manually from source
+
+You can run the below command to build the application and install the binary to
+the `/usr/bin` folder and the default configuration files to the `/etc/corteca`
+folder:
+
+```shell
+sudo make install
+```
+
+To remove a previous manual installation
+
+```shell
+sudo make uninstall
+```
+
+You can customize the destination folder by overriding the `$DESTDIR`
+environment variable:
+
+```shell
+# no sudo required; will be installed to ~/.local/share/usr/bin
+DESTDIR=~/.local/share make install
+```
+
+## Install with package manager
+
+If you are using debian/ubuntu or redhat-based distributions, you can create a relevant package and let your package manager handle installation. E.g. for ubuntu:
+
+```shell
+make deb
+make rpm
+```
diff --git a/doc/USAGE.md b/doc/USAGE.md
deleted file mode 100644
index 873b5d2..0000000
--- a/doc/USAGE.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Corteca Command Line Interface
-
-- [Corteca Command Line Interface](#corteca-command-line-interface)
- - [Available commands](#available-commands)
- - [Configuration](#configuration)
- - [Application Templates](#application-templates)
-
-## Available commands
-
-- [`corteca build`](reference/corteca_build.md)
-- [`corteca config`](reference/corteca_config.md)
- - [`corteca config add`](reference/corteca_config_add.md)
- - [`corteca config get`](reference/corteca_config_get.md)
- - [`corteca config set`](reference/corteca_config_set.md)
-- [`corteca create`](reference/corteca_create.md)
-- [`corteca exec`](reference/corteca_exec.md)
-- [`corteca publish`](reference/corteca_publish.md)
-- [`corteca regen`](reference/corteca_regen.md)
-
-**Note**: For essential guidance on using the Corteca CLI, refer to `corteca --help` for general information, or `corteca --help` for details on specific commands.
-
-## Configuration
-
-Configuration values are read cascadingly from the following sources:
-
-1. System-wide configuration: `/etc/corteca/corteca.yaml`
-1. User global configuration: `$HOME/.config/corteca/corteca.yaml`
-1. user local (project) configuration: `./corteca.yaml`
-
-The last is searched in the working directory; if not found, it will continue searching in the parent folder(s), until it finds a configuration file or reaches filesystem root. Thus, provided that a local configuration file exists in the project root folder, `corteca` commands can be run from any subfolder inside that.
-
-## Application Templates
-
-`corteca` scans for available templates in a `templates/` folder that resides in the global config folder (defaults to `$HOME/.config/corteca`). You can override the global config folder using the `--configRoot` flag.
-
-Each subfolder containing a `.template-info.yaml` file will be treated as a template and will be rendered with all configuration variables available for rendering. For more information, see [configuration section](#configuration) above.
-
-Here is an overview of the content of `.template-info.yaml`
-
-```yaml
- # Name of the template
- name: "foobar"
- description: "Short template description"
- dependencies:
- compile: [ foo ] # Dependencies needed during compile
- runtime: [ bar ] # Dependencies needed during runtime
- # Dynamic files that need regeneration during the project's lifecycle
- regenFiles:
- .corteca/foo.template: foo # Mapping template filepath to regenerated filepath
- # A collection of custom options available for rendering
- options:
-```
diff --git a/doc/reference/corteca_create.md b/doc/reference/corteca_create.md
index 62ff129..b147d30 100644
--- a/doc/reference/corteca_create.md
+++ b/doc/reference/corteca_create.md
@@ -1,6 +1,6 @@
# `create`
-The create command is used to generate a new application skeleton/template based on a selected programming language (see details about templates [here](../USAGE.md#application-templates)). This command can be configured interactively or through specified flags.
+The create command is used to generate a new application skeleton/template based on a selected programming language (see details about [application templates](../USAGE.md#application-templates)). This command can be configured interactively or through specified flags.
## Usage
diff --git a/doc/templates.md b/doc/templates.md
index b627d1e..2616a27 100644
--- a/doc/templates.md
+++ b/doc/templates.md
@@ -2,7 +2,7 @@
`corteca` scans for available templates in a `templates/` folder that resides in the global config folder (defaults to `$HOME/.config/corteca`). You can override the global config folder using the `--configRoot` flag.
-Each subfolder containing a `.template-info.yaml` file will be treated as a template and will be rendered with all configuration variables available for rendering. For more information, see [configuration section](#configuration) above.
+Each subfolder containing a `.template-info.yaml` file will be treated as a template and will be rendered with all configuration variables available for rendering. For more information, see the configuration section above.
Here is an overview of the content of `.template-info.yaml`
diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go
index 07ce055..a89ef8f 100755
--- a/internal/configuration/configuration.go
+++ b/internal/configuration/configuration.go
@@ -359,9 +359,8 @@ func (conf *Settings) ReadFromFile(path string) error {
if err != nil {
return err
}
- defer in.Close()
+ defer func() { _ = in.Close() }()
if err = ReadYamlInto(conf, in); err != nil {
- in.Close()
return err
}
@@ -810,7 +809,7 @@ func readTemplateInfo(fullPath string) (info TemplateInfo, err error) {
if err != nil {
return TemplateInfo{}, err
}
- defer yamlData.Close()
+ defer func() { _ = yamlData.Close() }()
if err = ReadYamlInto(&info, yamlData); err != nil {
return
diff --git a/internal/configuration/sequence.go b/internal/configuration/sequence.go
index 6e76064..ad0ae25 100644
--- a/internal/configuration/sequence.go
+++ b/internal/configuration/sequence.go
@@ -129,7 +129,9 @@ func (sm *SequenceMap) executeSequenceSteps(executor CommandExecutor, seqName st
// TODO: provide option to suppress output
tui.SetOutputColor(tui.CBlue, os.Stdout)
enc := yaml.NewEncoder(os.Stdout)
- enc.Encode(res)
+ if err := enc.Encode(res); err != nil {
+ return fmt.Errorf("failed to encode step result: %w", err)
+ }
tui.ResetOutputColor(os.Stdout)
}
return nil
diff --git a/internal/configuration/templating/templating.go b/internal/configuration/templating/templating.go
index 09688df..ade8fe1 100644
--- a/internal/configuration/templating/templating.go
+++ b/internal/configuration/templating/templating.go
@@ -94,7 +94,7 @@ func RenderTemplateToFile(fs afero.Fs, srcFile, destFile string, context any) er
if err != nil {
return err
}
- defer out.Close()
+ defer func() { _ = out.Close() }()
if err := fileTmpl.Execute(out, context); err != nil {
return err
diff --git a/internal/configuration/templating/templating_test.go b/internal/configuration/templating/templating_test.go
index f9bd9bb..8455159 100644
--- a/internal/configuration/templating/templating_test.go
+++ b/internal/configuration/templating/templating_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
)
func TestRenderTemplateFile(t *testing.T) {
@@ -18,8 +19,8 @@ func TestRenderTemplateFile(t *testing.T) {
templateContent := "Hello, {{.Name}}!"
templateDir := "/project"
templateFile := "template.txt"
- fs.MkdirAll(templateDir, 0755)
- afero.WriteFile(fs, filepath.Join(templateDir, templateFile), []byte(templateContent), 0644)
+ require.NoError(t, fs.MkdirAll(templateDir, 0755))
+ require.NoError(t, afero.WriteFile(fs, filepath.Join(templateDir, templateFile), []byte(templateContent), 0644))
testCases := []struct {
name string
diff --git a/internal/device/cwmp/cwmpdevice.go b/internal/device/cwmp/cwmpdevice.go
index 015845f..847d8f5 100644
--- a/internal/device/cwmp/cwmpdevice.go
+++ b/internal/device/cwmp/cwmpdevice.go
@@ -197,9 +197,9 @@ func (c *CWMPDevice) sendConnectionRequest(cpe *configuration.HttpClientEndpoint
if err != nil {
return fmt.Errorf("error sending connection request: %w", err)
}
- defer resp.Body.Close()
- c.log.Write(fmt.Appendf([]byte(""), "[%s] Connection Request (response: %s)\n", time.Now().Format(time.DateTime), resp.Status))
- io.Copy(io.Discard, resp.Body)
+ defer func() { _ = resp.Body.Close() }()
+ _, _ = c.log.Write(fmt.Appendf([]byte(""), "[%s] Connection Request (response: %s)\n", time.Now().Format(time.DateTime), resp.Status))
+ _, _ = io.Copy(io.Discard, resp.Body)
tui.LogNormal("Connection Request sent to %s; status code: %d", url.String(), resp.StatusCode)
return nil
}
@@ -227,9 +227,10 @@ func (d *CWMPDevice) initServer(config *configuration.HttpServerEndpoint) error
// Run server in a goroutine
go func() {
var err error
- if u.Scheme == "http" {
+ switch u.Scheme {
+ case "http":
err = d.server.ListenAndServe()
- } else if u.Scheme == "https" {
+ case "https":
err = d.server.ListenAndServeTLS(config.Certificate.String(), config.Key.String())
}
if err != nil && err != http.ErrServerClosed {
@@ -248,7 +249,7 @@ func (d *CWMPDevice) handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
return
}
- d.log.Write(fmt.Appendf([]byte(""), "[%s] IN: %s %s %s\n",
+ _, _ = d.log.Write(fmt.Appendf([]byte(""), "[%s] IN: %s %s %s\n",
time.Now().Format(time.DateTime),
r.Method,
r.RequestURI,
@@ -264,7 +265,7 @@ func (d *CWMPDevice) handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
return
}
} else {
- d.log.Write([]byte("\n"))
+ _, _ = d.log.Write([]byte("\n"))
if len(env.Body.Messages) > 1 {
tui.LogNormal("%d messages received; ignoring all but first", len(env.Body.Messages))
} else if len(env.Body.Messages) == 0 {
@@ -276,10 +277,10 @@ func (d *CWMPDevice) handleHTTPRequest(w http.ResponseWriter, r *http.Request) {
d.SetSessionID(env.GetID())
}
- d.log.Write(fmt.Appendf([]byte(""), "[%s] OUT:\n", time.Now().Format(time.DateTime)))
+ _, _ = d.log.Write(fmt.Appendf([]byte(""), "[%s] OUT:\n", time.Now().Format(time.DateTime)))
resp := <-d.out
d.writeHTTPResponse(w, http.StatusOK, resp)
- d.log.Write([]byte("\n--------------------------------------------------------------------------------\n"))
+ _, _ = d.log.Write([]byte("\n--------------------------------------------------------------------------------\n"))
}
// write a reply to the response
diff --git a/internal/device/cwmp/messages/ChangeDUState_test.go b/internal/device/cwmp/messages/ChangeDUState_test.go
index cc49e39..5cfb686 100644
--- a/internal/device/cwmp/messages/ChangeDUState_test.go
+++ b/internal/device/cwmp/messages/ChangeDUState_test.go
@@ -6,12 +6,12 @@ package messages_test
import (
"bytes"
- "github.com/nokia/corteca-cli/internal/configuration"
- c "github.com/nokia/corteca-cli/internal/configuration"
- "github.com/nokia/corteca-cli/internal/device/cwmp/messages"
"encoding/xml"
"testing"
+ c "github.com/nokia/corteca-cli/internal/configuration"
+ "github.com/nokia/corteca-cli/internal/device/cwmp/messages"
+
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
@@ -91,10 +91,10 @@ var ChangeDUStateInputMsg = messages.ChangeDUState{
}
func setupContext() {
- configuration.ResetContext()
- configuration.GetCmdContext().App.Name = "image"
- configuration.GetCmdContext().App.Version = "1.0.0"
- configuration.GetCmdContext().App.DUID = "c0c4328b-18a4-4b3b-b1da-e8ea8d8f457d"
+ c.ResetContext()
+ c.GetCmdContext().App.Name = "image"
+ c.GetCmdContext().App.Version = "1.0.0"
+ c.GetCmdContext().App.DUID = "c0c4328b-18a4-4b3b-b1da-e8ea8d8f457d"
}
func TestChangeDUStateParseFromXML(t *testing.T) {
diff --git a/internal/device/ssh/mock_ssh_server_test.go b/internal/device/ssh/mock_ssh_server_test.go
index 9252f92..786e2b4 100644
--- a/internal/device/ssh/mock_ssh_server_test.go
+++ b/internal/device/ssh/mock_ssh_server_test.go
@@ -86,7 +86,7 @@ func startTestServer(t *testing.T, expectedUsername, password string, authorized
if err != nil {
t.Fatalf("startTestServer: listen: %v", err)
}
- t.Cleanup(func() { ln.Close() })
+ t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
@@ -106,12 +106,12 @@ func serveConn(conn net.Conn, cfg *ssh.ServerConfig, handler cmdHandlerFunc) {
if err != nil {
return // auth failure or protocol error — nothing to do
}
- defer sshConn.Close()
+ defer func() { _ = sshConn.Close() }()
go ssh.DiscardRequests(reqs)
for newChan := range chans {
if newChan.ChannelType() != "session" {
- newChan.Reject(ssh.UnknownChannelType, "unsupported channel type")
+ _ = newChan.Reject(ssh.UnknownChannelType, "unsupported channel type")
continue
}
ch, requests, err := newChan.Accept()
@@ -127,22 +127,22 @@ func serveSession(ch ssh.Channel, reqs <-chan *ssh.Request, handler cmdHandlerFu
// exec request, the channel is closed. This causes the client-side
// s.wait goroutine's "for msg := range reqs" loop to exit, which populates
// s.exitStatus and unblocks session.Wait() — preventing a deadlock.
- defer ch.Close()
+ defer func() { _ = ch.Close() }()
for req := range reqs {
switch req.Type {
case "exec":
if len(req.Payload) < 4 {
- req.Reply(false, nil)
+ _ = req.Reply(false, nil)
continue
}
n := binary.BigEndian.Uint32(req.Payload[:4])
if uint32(len(req.Payload)) < 4+n {
- req.Reply(false, nil)
+ _ = req.Reply(false, nil)
continue
}
cmd := string(req.Payload[4 : 4+n])
- req.Reply(true, nil)
+ _ = req.Reply(true, nil)
// Call the handler synchronously. For normal commands this returns
// quickly. For the context-cancellation test the handler blocks on
@@ -166,7 +166,7 @@ func serveSession(ch ssh.Channel, reqs <-chan *ssh.Request, handler cmdHandlerFu
// other channel requests the client may send while the handler is
// running or between commands.
if req.WantReply {
- req.Reply(false, nil)
+ _ = req.Reply(false, nil)
}
}
}
diff --git a/internal/device/ssh/sshdevice.go b/internal/device/ssh/sshdevice.go
index ed899e3..2c79b0e 100644
--- a/internal/device/ssh/sshdevice.go
+++ b/internal/device/ssh/sshdevice.go
@@ -20,7 +20,6 @@ import (
"sync"
"time"
- "golang.org/x/crypto/ssh"
stdssh "golang.org/x/crypto/ssh"
)
@@ -41,10 +40,6 @@ const (
deactivateQuaggaCmd = "sed -i 's#/usr/bin/vtysh#/bin/ash#' /etc/passwd"
maxNumRetries = 3
defaultSSHPort = "22"
-
- authSSHPassword = "password"
- authSSHPublicKey = "publicKey"
- cmdCPUArch = "uname -m"
)
type SSHDevice struct {
@@ -64,7 +59,9 @@ func NewSSHDevice(c *configuration.DeviceConfig, log io.Writer) (device.Device,
err error
sshconfig configuration.SSHClientEndpoint
)
- c.Decode(&sshconfig)
+ if err = c.Decode(&sshconfig); err != nil {
+ return nil, err
+ }
if err = d.connectSSHClient(&sshconfig); err != nil {
return nil, err
@@ -78,7 +75,7 @@ func NewSSHDevice(c *configuration.DeviceConfig, log io.Writer) (device.Device,
if err := deactivateQuagga(d.client, passwd2); err != nil {
return nil, err
}
- d.client.Close()
+ _ = d.client.Close()
tui.LogNormal("Need to reconnect...")
return NewSSHDevice(c, log)
}
@@ -115,7 +112,7 @@ func (d *SSHDevice) executeCommandString(ctx context.Context, cmd string) (any,
if err != nil {
return nil, fmt.Errorf("cannot start SSH command session: %w", err)
}
- defer session.Close()
+ defer func() { _ = session.Close() }()
output := bytes.NewBuffer(make([]byte, 0, 512))
session.Stdout = io.MultiWriter(d.log, output)
session.Stderr = d.log
@@ -141,7 +138,7 @@ func (d *SSHDevice) executeCommandString(ctx context.Context, cmd string) (any,
}
case <-ctx.Done():
- _ = session.Signal(ssh.SIGKILL)
+ _ = session.Signal(stdssh.SIGKILL)
return nil, ctx.Err()
}
}
@@ -213,7 +210,7 @@ func (d *SSHDevice) connectSSHClient(sshconfig *configuration.SSHClientEndpoint)
// connect to stdssh server
d.client, err = stdssh.Dial("tcp", u.Host, config)
if err != nil {
- fmt.Fprintf(d.log, "\n=== New connection to %s at %s ===\n", u.Host, time.Now().Format(time.DateTime))
+ _, _ = fmt.Fprintf(d.log, "\n=== New connection to %s at %s ===\n", u.Host, time.Now().Format(time.DateTime))
}
return err
}
@@ -223,7 +220,7 @@ func hasQuagga(client *stdssh.Client) (bool, error) {
if err != nil {
return false, err
}
- defer session.Close()
+ defer func() { _ = session.Close() }()
var output bytes.Buffer
session.Stdout = &output
@@ -248,7 +245,7 @@ func deactivateQuagga(client *stdssh.Client, password2 string) error {
if err != nil {
return err
}
- defer session.Close()
+ defer func() { _ = session.Close() }()
if err := session.RequestPty("xterm", 80, 40, stdssh.TerminalModes{}); err != nil {
return err
@@ -275,5 +272,5 @@ func deactivateQuagga(client *stdssh.Client, password2 string) error {
}
func (d *SSHDevice) Close() {
- d.client.Close()
+ _ = d.client.Close()
}
diff --git a/internal/device/ssh/sshdevice_test.go b/internal/device/ssh/sshdevice_test.go
index 1df3ccb..feefc5b 100644
--- a/internal/device/ssh/sshdevice_test.go
+++ b/internal/device/ssh/sshdevice_test.go
@@ -179,7 +179,7 @@ func TestSSHDevice_PublicKeyAuth(t *testing.T) {
if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil {
t.Fatalf("write PEM key: %v", err)
}
- keyFile.Close()
+ _ = keyFile.Close()
// Start a server that only accepts the generated public key (no password).
addr := startTestServer(t, "testuser", "", sshPubKey, withQuaggaProbe(func(cmd string) (string, uint32) {
diff --git a/internal/fsutil/fsutil.go b/internal/fsutil/fsutil.go
index 5be3529..0c3455f 100644
--- a/internal/fsutil/fsutil.go
+++ b/internal/fsutil/fsutil.go
@@ -26,7 +26,7 @@ func CopyFile(src, dst string) (int64, error) {
if err != nil {
return 0, err
}
- defer source.Close()
+ defer func() { _ = source.Close() }()
destDir := filepath.Dir(dst)
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
@@ -37,7 +37,7 @@ func CopyFile(src, dst string) (int64, error) {
if err != nil {
return 0, err
}
- defer destination.Close()
+ defer func() { _ = destination.Close() }()
nBytes, err := io.Copy(destination, source)
if err != nil {
return nBytes, err
@@ -78,13 +78,13 @@ func TarAndGzip(basePath, targetTarGzPath string, includePaths []string) error {
if err != nil {
return err
}
- defer tarFile.Close()
+ defer func() { _ = tarFile.Close() }()
gzipWriter := gzip.NewWriter(tarFile)
- defer gzipWriter.Close()
+ defer func() { _ = gzipWriter.Close() }()
tarWriter := tar.NewWriter(gzipWriter)
- defer tarWriter.Close()
+ defer func() { _ = tarWriter.Close() }()
for _, includePath := range includePaths {
fullPath := filepath.Join(basePath, includePath)
@@ -150,7 +150,7 @@ func addFileToTarWriter(tarWriter *tar.Writer, file string, fi os.FileInfo, base
if err != nil {
return err
}
- defer fileContent.Close()
+ defer func() { _ = fileContent.Close() }()
if _, err := io.Copy(tarWriter, fileContent); err != nil {
return err
@@ -166,14 +166,14 @@ func ExtractTarball(src, dest string) error {
if err != nil {
return fmt.Errorf("could not open source file: %v", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
// Create a gzip reader
gzipReader, err := gzip.NewReader(file)
if err != nil {
return fmt.Errorf("could not create gzip reader: %v", err)
}
- defer gzipReader.Close()
+ defer func() { _ = gzipReader.Close() }()
// Create a tar reader
tarReader := tar.NewReader(gzipReader)
@@ -207,7 +207,7 @@ func ExtractTarball(src, dest string) error {
if err != nil {
return fmt.Errorf("could not create file: %v", err)
}
- defer outFile.Close()
+ defer func() { _ = outFile.Close() }()
err = os.Chmod(targetPath, header.FileInfo().Mode())
if err != nil {
@@ -242,16 +242,16 @@ func CreateTarArchive(fileName, archivePath string) error {
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
archive, err := os.Create(archivePath)
if err != nil {
return fmt.Errorf("failed to create archive file: %w", err)
}
- defer archive.Close()
+ defer func() { _ = archive.Close() }()
writer := tar.NewWriter(archive)
- defer writer.Close()
+ defer func() { _ = writer.Close() }()
fileInfo, err := file.Stat()
if err != nil {
@@ -292,13 +292,13 @@ func CreateTarArchiveFromFolder(srcDir, destTarGzPath string) error {
if err != nil {
return fmt.Errorf("failed to create archive file: %w", err)
}
- defer archive.Close()
+ defer func() { _ = archive.Close() }()
gzipWriter := gzip.NewWriter(archive)
- defer gzipWriter.Close()
+ defer func() { _ = gzipWriter.Close() }()
tarWriter := tar.NewWriter(gzipWriter)
- defer tarWriter.Close()
+ defer func() { _ = tarWriter.Close() }()
baseDir := filepath.Clean(srcDir)
return filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error {
@@ -333,7 +333,7 @@ func CreateTarArchiveFromFolder(srcDir, destTarGzPath string) error {
if err != nil {
return err
}
- defer fileContent.Close()
+ defer func() { _ = fileContent.Close() }()
if _, err := io.Copy(tarWriter, fileContent); err != nil {
return err
diff --git a/internal/packager/oci.go b/internal/packager/oci.go
index 41d4df8..932f926 100644
--- a/internal/packager/oci.go
+++ b/internal/packager/oci.go
@@ -31,7 +31,7 @@ func writeBuildInfoJson(buildInfo map[string]string, rootfsPath string) error {
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
_, err = file.Write(data)
if err != nil {
@@ -50,7 +50,7 @@ func writeRuntimeSpecToJSON(appSettings configuration.AppSettings, filePath stri
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
_, err = file.Write(data)
if err != nil {
diff --git a/internal/packager/validation.go b/internal/packager/validation.go
index 8d7bc1b..931f2b2 100644
--- a/internal/packager/validation.go
+++ b/internal/packager/validation.go
@@ -44,7 +44,7 @@ func isBinary(entrypointPath string) (bool, string, error) {
if err != nil {
return false, "", fmt.Errorf("failed to open file: %v", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
is_bin := false
const chunkSize = 4
buf := make([]byte, chunkSize)
@@ -73,7 +73,7 @@ func isScript(entrypointPath string) (bool, string, error) {
if err != nil {
return false, "", fmt.Errorf("failed to open file: %v", err)
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
reader := bufio.NewReader(file)
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
@@ -186,7 +186,7 @@ func verifyMachineCompatibility(path, targetArch string) error {
if err != nil {
return fmt.Errorf("ELF validation failed: %w", err)
}
- defer f.Close()
+ defer func() { _ = f.Close() }()
if f.Type != elf.ET_EXEC && f.Type != elf.ET_DYN {
return fmt.Errorf("invalid ELF type: %s", f.Type)
diff --git a/internal/packager/validation_test.go b/internal/packager/validation_test.go
index a2c79c4..605e4d0 100644
--- a/internal/packager/validation_test.go
+++ b/internal/packager/validation_test.go
@@ -295,30 +295,18 @@ func createSymlink(t *testing.T, target, link string) {
func TestDiscoverEntrypointPath_SymlinkToBinary(t *testing.T) {
- tmpDir := t.TempDir()
- bin := createBinaryFile(t, "", []byte{0x7F, 0x45, 0x4C, 0x46})
- tmpPathToBinary := filepath.Join(tmpDir, bin)
- link := "link"
- tmpPathToLink := filepath.Join(tmpDir, link)
- createSymlink(t, bin, link)
+ tmpDir := t.TempDir()
+ bin := createBinaryFile(t, tmpDir, []byte{0x7F, 0x45, 0x4C, 0x46})
+ tmpPathToLink := filepath.Join(tmpDir, "link")
+ createSymlink(t, "binaryfile", tmpPathToLink)
- err := os.Rename(bin, tmpPathToBinary)
+ path, err := discoverEntrypointPath(tmpPathToLink, tmpDir)
if err != nil {
- t.Fatalf("failed to move file: %v", err)
+ t.Fatalf("unexpected error: %v", err)
}
-
- err = os.Rename(link, tmpPathToLink)
- if err != nil {
- t.Fatalf("failed to move file: %v", err)
+ if path != bin {
+ t.Errorf("expected path to be '%s', got '%s'", bin, path)
}
-
- path, err := discoverEntrypointPath(tmpPathToLink, tmpDir)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if path != tmpPathToBinary {
- t.Errorf("expected path to be '%s', got '%s'", bin, path)
- }
}
func TestDiscoverEntrypointPath_ScriptWithShebang(t *testing.T) {
@@ -337,32 +325,19 @@ func TestDiscoverEntrypointPath_ScriptWithShebang(t *testing.T) {
}
func TestDiscoverEntrypointPath_ChainSymlinkScriptBinary(t *testing.T) {
- tmpDir := t.TempDir()
- bin := createBinaryFile(t, tmpDir, []byte{0x7F, 0x45, 0x4C, 0x46})
- script := createFile(t, "", "testfile.sh", "#!/binaryfile\n")
- tmpPathToScript := filepath.Join(tmpDir, script)
- link := "linkfile"
- tmpPathToLink := filepath.Join(tmpDir, link)
- createSymlink(t, script, link)
+ tmpDir := t.TempDir()
+ bin := createBinaryFile(t, tmpDir, []byte{0x7F, 0x45, 0x4C, 0x46})
+ _ = createFile(t, tmpDir, "testfile.sh", "#!/binaryfile\n")
+ tmpPathToLink := filepath.Join(tmpDir, "linkfile")
+ createSymlink(t, "testfile.sh", tmpPathToLink)
- err := os.Rename(script, tmpPathToScript)
+ path, err := discoverEntrypointPath(tmpPathToLink, tmpDir)
if err != nil {
- t.Fatalf("failed to move file: %v", err)
+ t.Fatalf("unexpected error: %v", err)
}
-
- err = os.Rename(link, tmpPathToLink)
- if err != nil {
- t.Fatalf("failed to move file: %v", err)
+ if path != bin {
+ t.Errorf("expected path to be '%s', got '%s'", bin, path)
}
-
- path, err := discoverEntrypointPath(tmpPathToLink, tmpDir)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- expected := bin
- if path != expected {
- t.Errorf("expected path to be '%s', got '%s'", expected, path)
- }
}
func TestDiscoverEntrypointPath_UnknownFileType(t *testing.T) {
diff --git a/internal/publish/listen.go b/internal/publish/listen.go
index 0c1da5b..08f7830 100644
--- a/internal/publish/listen.go
+++ b/internal/publish/listen.go
@@ -30,7 +30,7 @@ func ListenAsync(serverRoot string, addr *url.URL) (*http.Server, error) {
Handler: http.FileServer(http.Dir(serverRoot)),
}
go func() {
- srv.Serve(l)
+ _ = srv.Serve(l)
}()
return srv, nil
default:
diff --git a/internal/publish/push.go b/internal/publish/push.go
index c47a83f..a96fe80 100644
--- a/internal/publish/push.go
+++ b/internal/publish/push.go
@@ -39,7 +39,7 @@ func GzipOpener(path string) tarball.Opener {
gz, err := gzip.NewReader(f)
if err != nil {
- f.Close()
+ _ = f.Close()
return nil, err
}
diff --git a/internal/publish/put.go b/internal/publish/put.go
index 3d97b3e..8443577 100644
--- a/internal/publish/put.go
+++ b/internal/publish/put.go
@@ -68,7 +68,7 @@ func HttpPut(filePath string, url url.URL, token string) error {
if err != nil {
return err
}
- defer file.Close()
+ defer func() { _ = file.Close() }()
prog := tui.PromptForProgress(fmt.Sprintf("Uploading %s", fileName))
defer close(prog)
@@ -98,7 +98,7 @@ func HttpPut(filePath string, url url.URL, token string) error {
return err
}
- defer resp.Body.Close()
+ defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("server returned non-successful status: %s", resp.Status)
diff --git a/internal/publish/put_test.go b/internal/publish/put_test.go
index cabb93b..c5e3cb5 100644
--- a/internal/publish/put_test.go
+++ b/internal/publish/put_test.go
@@ -101,13 +101,13 @@ func TestHttpPut(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create temporary file: %v", err)
}
- defer os.Remove(tmpFile.Name())
+ defer func() { _ = os.Remove(tmpFile.Name()) }()
_, err = tmpFile.Write([]byte("test content"))
if err != nil {
t.Fatalf("Failed to write to temporary file: %v", tmpFile)
}
- tmpFile.Close()
+ _ = tmpFile.Close()
var server *httptest.Server
if tc.setupServer != nil {
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 136a0ff..0815a0f 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -37,7 +37,7 @@ var (
// If no-color flag is set or no terminal found then remove colored output
func DefineOutputColor() {
- if DisableColoredOutput || !(term.IsTerminal(int(os.Stdout.Fd())) && term.IsTerminal(int(os.Stderr.Fd()))) {
+ if DisableColoredOutput || !term.IsTerminal(int(os.Stdout.Fd())) || !term.IsTerminal(int(os.Stderr.Fd())) {
DisableColoredOutput = true
pterm.DisableColor()
}
@@ -64,13 +64,13 @@ func LogError(format string, args ...any) {
func SetOutputColor(colorSeq string, out io.Writer) {
if !DisableColoredOutput {
- fmt.Fprintf(out, colorSeq)
+ _, _ = fmt.Fprint(out, colorSeq)
}
}
func ResetOutputColor(out io.Writer) {
if !DisableColoredOutput {
- fmt.Fprintf(out, CsReset)
+ _, _ = fmt.Fprint(out, CsReset)
}
}
@@ -117,7 +117,7 @@ func PromptForProgress(label string) chan<- ProgressUpdate {
diff := current - bar.Current
bar.Add(diff)
}
- bar.Stop()
+ _, _ = bar.Stop()
}()
return ch
}
diff --git a/scripts/check.sh b/scripts/check.sh
new file mode 100755
index 0000000..b86cbe8
--- /dev/null
+++ b/scripts/check.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# Run the same checks as build.yml locally.
+set -euo pipefail
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+BOLD='\033[1m'
+RESET='\033[0m'
+
+pass() { echo -e "${GREEN}✓ $1${RESET}"; }
+fail() { echo -e "${RED}✗ $1${RESET}"; exit 1; }
+warn() { echo -e "${YELLOW}⚠ $1${RESET}"; }
+header() { echo -e "\n${BOLD}=== $1 ===${RESET}"; }
+
+# ── Devcontainer guard ───────────────────────────────────────────────────────
+# Re-run inside the devcontainer if we're not already in one.
+if [ -z "${IN_DEVCONTAINER:-}" ] && [ -z "${REMOTE_CONTAINERS:-}" ] && [ -z "${CODESPACES:-}" ]; then
+ header "Not inside devcontainer — spinning it up"
+ if ! command -v devcontainer &>/dev/null; then
+ fail "devcontainer CLI not found.\n Install: npm install -g @devcontainers/cli\n Or open this repo in VS Code and use 'Reopen in Container'."
+ fi
+ WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
+ devcontainer up --workspace-folder "$WORKSPACE" || fail "devcontainer up failed"
+ exec devcontainer exec --workspace-folder "$WORKSPACE" bash scripts/check.sh
+fi
+
+# ── Build ────────────────────────────────────────────────────────────────────
+header "Build"
+make || fail "build failed"
+pass "build"
+
+# ── Tests ────────────────────────────────────────────────────────────────────
+header "Tests"
+make test || fail "tests failed"
+pass "tests"
+
+# ── Go lint ──────────────────────────────────────────────────────────────────
+header "Go lint (golangci-lint)"
+if ! command -v golangci-lint &>/dev/null; then
+ fail "golangci-lint not found — install it or open the devcontainer"
+fi
+golangci-lint run ./... || fail "Go lint failed"
+pass "Go lint"
+
+# ── YAML lint ────────────────────────────────────────────────────────────────
+header "YAML lint (yamllint)"
+if ! command -v yamllint &>/dev/null; then
+ echo "yamllint not found, installing via apt..."
+ sudo apt-get install -y -qq yamllint
+fi
+yamllint -c .yamllint.yaml . || fail "YAML lint failed"
+pass "YAML lint"
+
+# ── Markdown lint ────────────────────────────────────────────────────────────
+header "Markdown lint (markdownlint-cli2)"
+if ! command -v npx &>/dev/null; then
+ warn "npx / Node.js not found — skipping markdown lint (runs in CI via GitHub Actions)"
+else
+ npx --yes markdownlint-cli2 --config .markdownlint.yaml "**/*.md" || fail "Markdown lint failed"
+ pass "Markdown lint"
+fi
+
+# ─────────────────────────────────────────────────────────────────────────────
+echo -e "\n${GREEN}${BOLD}All available checks passed.${RESET}"