diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e5fb019 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# This file is for unifying the coding style for different editors and IDEs. +# More information at http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c40cfd5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + - package-ecosystem: 'gomod' + directory: '/' + schedule: + interval: 'daily' + time: '08:00' + labels: + - 'dependencies' + commit-message: + prefix: 'chore' + include: 'scope' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + time: '08:00' + labels: + - 'dependencies' + commit-message: + prefix: 'chore' + include: 'scope' + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'daily' + time: '08:00' + labels: + - 'dependencies' + commit-message: + prefix: 'chore' + include: 'scope' diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..49fe629 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,22 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 14 + +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 + +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + +# Label to use when marking an issue as stale +staleLabel: wontfix + +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d5af5c9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,77 @@ +name: build + +on: + push: + branches: + - "main" + + pull_request: + paths: + - "go.*" + - "**/*.go" + - "Taskfile.yml" + - "Dockerfile" + - ".github/workflows/*.yml" + +permissions: + contents: read + +jobs: + govulncheck: + uses: caarlos0/meta/.github/workflows/govulncheck.yml@main + + semgrep: + uses: caarlos0/meta/.github/workflows/semgrep.yml@main + + ruleguard: + uses: caarlos0/meta/.github/workflows/ruleguard.yml@main + with: + args: "-disable largeloopcopy" + + test: + runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: arduino/setup-task@v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - name: setup-snapcraft + # FIXME: the mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715 + run: | + sudo apt-get update + sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft + mkdir -p $HOME/.cache/snapcraft/download + mkdir -p $HOME/.cache/snapcraft/stage-packages + - uses: crazy-max/ghaction-upx@v3 + with: + install-only: true + - uses: cachix/install-nix-action@v25 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v4 + with: + go-version: stable + - uses: sigstore/cosign-installer@v3.4.0 + - uses: anchore/sbom-action/download-syft@v0.15.8 + - name: setup-validate-krew-manifest + run: go install sigs.k8s.io/krew/cmd/validate-krew-manifest@latest + - name: setup-tparse + run: go install github.com/mfridman/tparse@latest + - name: setup + run: | + task setup + task build + - name: test + run: ./scripts/test.sh + - uses: codecov/codecov-action@e0b68c6749509c5f83f984dd99a76a1c1a231044 # v4 + with: + file: ./coverage.txt + - run: ./goreleaser check + - run: git diff diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..ca58f98 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,24 @@ +name: "codeql" + +on: + push: + branches: [main] + +jobs: + analyze: + name: analyze + runs-on: ubuntu-latest + + permissions: + security-events: write + actions: read + contents: read + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - uses: github/codeql-action/init@v2 + - uses: github/codeql-action/autobuild@v2 + - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..a06d186 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: dependency-review +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + allow-licenses: BSD-2-Clause, BSD-3-Clause, MIT, Apache-2.0, MPL-2.0 diff --git a/.github/workflows/generate.yml.disabled b/.github/workflows/generate.yml.disabled new file mode 100644 index 0000000..50673f6 --- /dev/null +++ b/.github/workflows/generate.yml.disabled @@ -0,0 +1,39 @@ +name: generate + +on: + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + - uses: actions/setup-go@v5 + with: + go-version: stable + cache: true + - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - run: "go install mvdan.cc/gofumpt@latest" + - run: "go install github.com/santhosh-tekuri/jsonschema/cmd/jv@latest" + - run: task docs:releases + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: task docs:generate + - run: task schema:generate + - run: task nix:licenses:generate + - run: task schema:validate + - uses: stefanzweifel/git-auto-commit-action@8756aa072ef5b4a080af5dc8fef36c5d586e521d # v5 + with: + commit_message: "chore: docs auto-update" + branch: main + commit_user_name: actions-user + commit_user_email: actions@github.com + commit_author: actions-user diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..e4b9d8b --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,23 @@ +name: gitleaks + +on: + push: + branches: ["main"] + tags: ["v*"] + pull_request: + +permissions: + contents: read + +jobs: + gitleaks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}} + # if: ${{ env.GITLEAKS_LICENSE != '' }} diff --git a/.github/workflows/grype.yml b/.github/workflows/grype.yml new file mode 100644 index 0000000..fa1f551 --- /dev/null +++ b/.github/workflows/grype.yml @@ -0,0 +1,25 @@ +name: "grype" + +on: + push: + branches: ["main"] + tags: ["v*"] + + pull_request: + +jobs: + scan-source: + name: scan-source + runs-on: ubuntu-latest + + permissions: + security-events: write + actions: read + contents: read + + steps: + - uses: actions/checkout@v4 + - uses: anchore/scan-action@v3 + with: + path: "." + fail-build: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..b02bcc5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: golangci-lint + +on: + push: + tags: + - v* + branches: + - main + pull_request: + +permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + +jobs: + golangci-lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + args: --timeout=5m + only-new-issues: true diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..af73971 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,57 @@ +name: nightly + +on: + workflow_dispatch: + schedule: + - cron: 0 0 * * 4 + +permissions: + contents: write + id-token: write + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 + with: + fetch-depth: 0 + - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v2 + - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v2 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v4 + with: + go-version: stable + - uses: sigstore/cosign-installer@v3.4.0 + - uses: anchore/sbom-action/download-syft@v0.15.8 + # - uses: crazy-max/ghaction-upx@v3 + # with: + # install-only: true + # - uses: cachix/install-nix-action@v25 + # with: + # github_access_token: ${{ secrets.GITHUB_TOKEN }} + # - name: dockerhub-login + # uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v2 + # with: + # username: ${{ secrets.DOCKER_USERNAME }} + # password: ${{ secrets.DOCKER_PASSWORD }} + - name: ghcr-login + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: goreleaser/goreleaser-action@v5 + with: + distribution: goreleaser-pro + version: nightly + args: release --clean --nightly -f .goreleaser-nightly.yaml --timeout 60m + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b6974e3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,138 @@ +name: release + +on: + push: + branches: + - "main" + tags: + - "v*" + +permissions: + contents: write + id-token: write + packages: write + +jobs: + trigger-generate: + runs-on: ubuntu-latest + needs: [goreleaser] + steps: + - uses: benc-uk/workflow-dispatch@v121 + if: startsWith(github.ref, 'refs/tags/v') + with: + repo: jippi/dottie + ref: main + token: ${{ secrets.GH_PAT }} + workflow: generate.yml + notify-goreleaser-cross: + runs-on: ubuntu-latest + needs: [trigger-generate] + steps: + - name: get version + if: startsWith(github.ref, 'refs/tags/v') + run: echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + - name: notify goreleaser-cross with new release + if: startsWith(github.ref, 'refs/tags/v') + uses: benc-uk/workflow-dispatch@v121 + with: + token: ${{ secrets.GH_PAT }} + repo: goreleaser/goreleaser-cross + workflow: goreleaser-bump + inputs: '{ "tag" : "${{ env.RELEASE_TAG }}" }' + goreleaser-check-pkgs: + runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + needs: [goreleaser] + if: github.ref == 'refs/heads/main' + strategy: + matrix: + format: [deb, rpm, apk] + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 + with: + fetch-depth: 0 + - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v2 + - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 + with: + path: | + ./dist/*.deb + ./dist/*.rpm + ./dist/*.apk + key: ${{ github.ref }} + - run: task goreleaser:test:${{ matrix.format }} + goreleaser: + runs-on: ubuntu-latest + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 + with: + fetch-depth: 0 + - uses: arduino/setup-task@e26d8975574116b0097a1161e0fe16ba75d84c1c # v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v2 + - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v2 + - name: setup-snapcraft + # FIXME: the mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715 + run: | + sudo apt-get update + sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft + mkdir -p $HOME/.cache/snapcraft/download + mkdir -p $HOME/.cache/snapcraft/stage-packages + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v4 + with: + go-version: stable + - uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4 + with: + path: | + ./dist/*.deb + ./dist/*.rpm + ./dist/*.apk + key: ${{ github.ref }} + - uses: sigstore/cosign-installer@v3.4.0 + - uses: anchore/sbom-action/download-syft@v0.15.8 + - uses: crazy-max/ghaction-upx@v3 + with: + install-only: true + - uses: cachix/install-nix-action@v25 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + - name: dockerhub-login + if: startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: ghcr-login + if: startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: snapcraft-login + if: startsWith(github.ref, 'refs/tags/v') + run: snapcraft login --with <(echo "${{ secrets.SNAPCRAFT_LOGIN }}") + - name: goreleaser-release + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + MASTODON_CLIENT_ID: ${{ secrets.MASTODON_CLIENT_ID }} + MASTODON_CLIENT_SECRET: ${{ secrets.MASTODON_CLIENT_SECRET }} + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + COSIGN_PWD: ${{ secrets.COSIGN_PWD }} + FURY_TOKEN: ${{ secrets.FURY_TOKEN }} + DISCORD_WEBHOOK_ID: ${{ secrets.DISCORD_WEBHOOK_ID }} + DISCORD_WEBHOOK_TOKEN: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} + AUR_KEY: ${{ secrets.AUR_KEY }} + run: task goreleaser diff --git a/.gitignore b/.gitignore index 2cef73e..4f364b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ .env .env.* !.env.example + +dist/ +bin/ +coverage.txt +dotti +dotti.exe +.task/ +.idea/ +.direnv diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..3fd0c1f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,254 @@ +run: + modules-download-mode: readonly + skip-dirs-use-default: true + +linters: + disable-all: true + enable: + # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] + - govet + + # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] + - ineffassign + + # (megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] + - staticcheck + + # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] + - typecheck + + # (gas): Inspects source code for security problems [fast: false, auto-fix: false] + - gosec + + # Simple linter to check that your code does not contain non-ASCII identifiers + - asciicheck + + # Checks for dangerous unicode character sequences + - bidichk + + # checks whether HTTP response body is closed successfully + - bodyclose + + # check the function whether use a non-inherited context + - contextcheck + + # check declaration order and count of types, constants, variables and functions + - decorder + + # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. + - errchkjson + + # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. + - errname + + # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - errorlint + + # check exhaustiveness of enum switch statements + - exhaustive + + # checks for pointers to enclosing loop variables + - exportloopref + + # finds forced type assertions + - forcetypeassert + + # Finds repeated strings that could be replaced by a constant + - goconst + + # Gofumpt checks whether code was gofumpt-ed. + - gofumpt + + # Allow and block list linter for direct Go module dependencies + - gomodguard + + # Reports deeply nested if statements + - nestif + + # Finds the code that returns nil even if it checks that the error is not nil. + - nilerr + + # Checks that there is no simultaneous return of nil error and an invalid value. + - nilnil + + # nlreturn checks for a new line before return and branch statements to increase code clarity + - nlreturn + + # noctx finds sending http request without context.Context + - noctx + + # paralleltest detects missing usage of t.Parallel() method in your Go test + - paralleltest + + # Finds slice declarations that could potentially be pre-allocated + - prealloc + + # find code that shadows one of Go's predeclared identifiers + - predeclared + + # Checks that package variables are not reassigned + - reassign + + # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - tenv + + # linter that makes you use a separate _test package + - testpackage + + # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - thelper + + # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes + - tparallel + + # Remove unnecessary type conversions + - unconvert + + # Reports unused function parameters + - unparam + + # wastedassign finds wasted assignment statements. + - wastedassign + + # Whitespace Linter - Forces you to use empty lines! + - wsl + + # A linter that detect the possibility to use variables/constants from the Go standard library. + - usestdlibvars + + # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) + - dogsled + + # check for two durations multiplied together + - durationcheck + + # check for pass []any as any in variadic func(...any) + - asasalint + + # Go linter that checks if package imports are in a list of acceptable packages + - depguard + + # Finds commonly misspelled English words in comments + - misspell + + # Align and sort struct tags consistently + - tagalign + +issues: + exclude-rules: + # ignoring this check from 'unused' since setID is indeed used, but + # the linter fail to recognize its being used through an interface + - path: internal/pkg/system/checks/ + text: setID` is unused + linters: [unused] + - path: internal/pkg/system/checks/ + text: asEntry()` is unused + linters: [unused] + - path: internal/pkg/system/checks/ + text: setState()` is unused + linters: [unused] + # ignore (nil, nil) return errors + - path: ".*" + text: "use a sentinel error instead" + linters: [nilnil] + # ignore logger not being referenced, as it's very likely that the logger is in fact used in a Go file + # that just happen to be for another GOOS than linux where CI runs (e.g. _windows.go file) + - path: ".*" + text: logger` is unused + linters: [unused] + # we're fine with forced typed assertions in tests + - path: ".*_test.go" + linters: [forcetypeassert, goconst] + # these tests can not run in parallel + - path: "internal/pkg/meta/version_test.go" + linters: [paralleltest] + - path: "internal/pkg/meta/version.go" + linters: [forcetypeassert] + # true/false as strings shouldn't need a constant + - path: ".*" + text: "string `true|false`" + linters: [goconst] + # "string" literal shouldn't need a constant + - path: ".*" + text: "string `string`" + linters: [goconst] + # MarshalText is an interface that we must implement + - path: ".*" + text: "MarshalText" + linters: [unparam] + # MarshalYAML is an interface that we must implement + - path: ".*" + text: "MarshalYAML" + linters: [unparam] + +linters-settings: + nestif: + min-complexity: 6 + + depguard: + rules: + DontUse: + deny: + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + + # See https://golangci-lint.run/usage/linters/#gomnd + gomnd: + ignored-functions: + - "cobra.RangeArgs" + + # See https://golangci-lint.run/usage/linters#gosec + gosec: + includes: [] + excludes: [] + + exhaustive: + # Presence of "default" case in switch statements satisfies exhaustiveness, even if all enum members are not listed. + # Default: false + default-signifies-exhaustive: true + + unparam: + check-exported: true + + tagalign: + # Align and sort can be used together or separately. + # + # Whether enable align. If true, the struct tags will be aligned. + # eg: + # type FooBar struct { + # Bar string `json:"bar" validate:"required"` + # FooFoo int8 `json:"foo_foo" validate:"required"` + # } + # will be formatted to: + # type FooBar struct { + # Bar string `json:"bar" validate:"required"` + # FooFoo int8 `json:"foo_foo" validate:"required"` + # } + # Default: true. + align: true + # Whether enable tags sort. + # If true, the tags will be sorted by name in ascending order. + # eg: `xml:"bar" json:"bar" validate:"required"` -> `json:"bar" validate:"required" xml:"bar"` + # Default: true + sort: true + # Specify the order of tags, the other tags will be sorted by name. + # This option will be ignored if `sort` is false. + # Default: [] + order: + # config/dependency injection + - default + - koanf + - wire + - letsgo + # encode and decode + - json + - yaml + - mapstructure + # misc + - validate + # Whether enable strict style. + # In this style, the tags will be sorted and aligned in the dictionary order, + # and the tags with the same name will be aligned together. + # Note: This option will be ignored if 'align' or 'sort' is false. + # Default: false + strict: true diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..b51ca46 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,20 @@ +dockers: + - image_templates: + - 'jippi/dottie:{{ .Tag }}-amd64' + use: buildx + build_flag_templates: + - '--pull' + - '--platform=linux/amd64' + - image_templates: + - 'myorg/myuser:{{ .Tag }}-arm64' + use: buildx + build_flag_templates: + - '--pull' + - '--platform=linux/arm64' + goarch: arm64 + +docker_manifests: + - name_template: 'jippi/dottie:{{ .Tag }}' + image_templates: + - 'jippi/dottie:{{ .Tag }}-amd64' + - 'jippi/dottie:{{ .Tag }}-arm64' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..caed418 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "files.eol": "\n", + "go.buildTags": "mage", + "go.formatTool": "gofumpt", + "go.formatFlags": [ + "-extra" + ], + "go.lintTool": "golangci-lint", + "go.lintOnSave": "package", + "go.lintFlags": [ + "--fast", + "--fix" + ], + "[go]": { + "editor.formatOnSave": true, + } +} diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..43a8383 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,172 @@ +# https://taskfile.dev + +version: "3" + +env: + GO111MODULE: on + GOPROXY: https://proxy.golang.org,direct + +tasks: + dev: + desc: Setup git hooks + cmds: + - cp -f scripts/pre-commit.sh .git/hooks/pre-commit + + setup: + desc: Install dependencies + cmds: + - go mod tidy + + build: + desc: Build the binary + sources: + - ./**/*.go + generates: + - ./dotti + cmds: + - go build -o dotti ./cmd/ + + test: + desc: Run tests + env: + LC_ALL: C + vars: + TEST_OPTIONS: '{{default "" .TEST_OPTIONS}}' + SOURCE_FILES: '{{default "./..." .SOURCE_FILES}}' + TEST_PATTERN: '{{default "." .TEST_PATTERN}}' + cmds: + - go test {{.TEST_OPTIONS}} -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt {{.SOURCE_FILES}} -run {{.TEST_PATTERN}} -timeout=5m + + cover: + desc: Open the cover tool + cmds: + - go tool cover -html=coverage.txt + + fmt: + desc: gofumpt all code + cmds: + - gofumpt -w -l . + + lint: + desc: Lint the code with golangci-lint + cmds: + - golangci-lint run --config ./.golangci.yaml ./... + + ci: + desc: Run all CI steps + cmds: + - task: setup + - task: build + - task: test + + default: + desc: Runs the default tasks + cmds: + - task: ci + + release: + desc: Create a new tag + vars: + NEXT: + sh: svu n + cmds: + - git tag {{.NEXT}} + - echo {{.NEXT}} + - git push origin --tags + + dotti:test:pkg: + desc: Test a package + cmds: + - docker run --platform linux/{{ .Platform }} --rm --workdir /tmp -v $PWD/dist:/tmp {{ .Image }} sh -c '{{ .Cmd }} && goreleaser --version' + + dotti:test:rpm: + desc: Tests rpm packages + vars: + rpm: "rpm --nodeps -ivh" + cmds: + - task: dotti:test:pkg + vars: + Platform: "386" + Image: centos:centos7 + Cmd: "{{.rpm}} goreleaser-*.i386.rpm" + - task: dotti:test:pkg + vars: + Platform: "amd64" + Image: fedora + Cmd: "{{.rpm}} goreleaser-*.x86_64.rpm" + - task: dotti:test:pkg + vars: + Platform: "arm64" + Image: fedora + Cmd: "{{.rpm}} goreleaser-*.aarch64.rpm" + + dotti:test:deb: + desc: Tests deb packages + vars: + dpkg: "dpkg --ignore-depends=git -i" + cmds: + - task: dotti:test:pkg + vars: + Platform: "amd64" + Image: ubuntu + Cmd: "{{.dpkg}} goreleaser*_amd64.deb" + - task: dotti:test:pkg + vars: + Platform: "arm64" + Image: ubuntu + Cmd: "{{.dpkg}} goreleaser*_arm64.deb" + - task: dotti:test:pkg + vars: + Platform: "arm/7" + Image: ubuntu + Cmd: "{{.dpkg}} goreleaser*_armhf.deb" + + dotti:test:apk: + desc: Tests apk packages + vars: + apk: "apk add --allow-untrusted -U" + cmds: + - task: dotti:test:pkg + vars: + Platform: "386" + Image: alpine + Cmd: "{{.apk}} goreleaser*_x86.apk" + - task: dotti:test:pkg + vars: + Platform: "amd64" + Image: alpine + Cmd: "{{.apk}} goreleaser*_x86_64.apk" + - task: dotti:test:pkg + vars: + Platform: "arm64" + Image: alpine + Cmd: "{{.apk}} goreleaser*_aarch64.apk" + - task: dotti:test:pkg + vars: + Platform: "arm/7" + Image: alpine + Cmd: "{{.apk}} goreleaser*_armv7.apk" + + dotti:test: + desc: Test built linux packages + cmds: + - task: dotti:test:apk + - task: dotti:test:deb + - task: dotti:test:rpm + + dotti: + desc: Run GoReleaser either in snapshot or release mode + deps: + - build + vars: + SNAPSHOT: + sh: 'if [[ $GITHUB_REF != refs/tags/v* ]]; then echo "--snapshot"; fi' + cmds: + - ./goreleaser release --clean --timeout 60m {{ .SNAPSHOT }} + + nightly: + cmds: + - gh run list --workflow=nightly-oss.yml + - gh workflow run nightly-oss.yml + - sleep 30 + - gh run watch diff --git a/cmd/cmd_json.go b/cmd/cmd_json.go index 3f36234..3e876f1 100644 --- a/cmd/cmd_json.go +++ b/cmd/cmd_json.go @@ -19,6 +19,7 @@ var jsonCommand = &cli.Command{ } fmt.Println(string(b)) + return nil }, } diff --git a/cmd/cmd_update.go b/cmd/cmd_update.go index 4e8b050..9e97d1d 100644 --- a/cmd/cmd_update.go +++ b/cmd/cmd_update.go @@ -82,8 +82,8 @@ var updateCommand = &cli.Command{ } fmt.Println() - fmt.Println("Saving .env.merged") + return pkg.Save(".env.merged", mergedEnv) }, } diff --git a/cmd/main.go b/cmd/main.go index 3911946..09589f5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,6 +52,7 @@ func main() { cli.HelpPrinterCustom = func(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { origHelpPrinterCustom(out, templ, data, customFuncs) + if data != app { origHelpPrinterCustom(app.Writer, globalOptionsTemplate, app, nil) } diff --git a/cmd/setup.go b/cmd/setup.go index af52b15..1764e70 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -11,6 +11,7 @@ import ( func setup(_ context.Context, cmd *cli.Command) error { var err error + env, err = pkg.Load(cmd.String("file")) if err != nil { return err diff --git a/pkg/ast/document.go b/pkg/ast/document.go index 28cfb10..c752fa5 100644 --- a/pkg/ast/document.go +++ b/pkg/ast/document.go @@ -131,10 +131,12 @@ func (doc *Document) Set(input *Assignment, options SetOptions) (bool, error) { before := options.Before var res []Statement + for _, stmt := range group.Statements { x, ok := stmt.(*Assignment) if !ok { res = append(res, stmt) + continue } diff --git a/pkg/ast/shared.go b/pkg/ast/shared.go index 3959d2c..ea973d8 100644 --- a/pkg/ast/shared.go +++ b/pkg/ast/shared.go @@ -27,15 +27,15 @@ func (p Position) String() string { } func renderStatements(statements []Statement, config RenderSettings) string { - var buf bytes.Buffer - var prev Statement - var line *Newline - - var printed bool + var ( + buf bytes.Buffer + prev Statement + line *Newline + printed bool + ) for _, stmt := range statements { switch val := stmt.(type) { - case *Group: output := val.Render(config) if len(output) == 0 { @@ -46,9 +46,10 @@ func renderStatements(statements []Statement, config RenderSettings) string { buf.WriteString("\n") } - printed = true buf.WriteString(output) + printed = true + case *Comment: printed = true @@ -65,19 +66,18 @@ func renderStatements(statements []Statement, config RenderSettings) string { // attempt to inject some new-lines to give them some space if config.WithBlankLines() && val.Is(prev) { switch { - // only allow cuddling of assignments if they both have no comments case val.HasComments() || assignmentHasComments(prev): buf.WriteString("\n") default: - // NOOP } } - printed = true buf.WriteString(output) + printed = true + case *Newline: output := val.Render(config) if len(output) == 0 { diff --git a/pkg/file.go b/pkg/file.go index 42c3f3c..0514c27 100644 --- a/pkg/file.go +++ b/pkg/file.go @@ -27,6 +27,7 @@ func Save(filename string, env *ast.Document) error { defer f.Close() _, err = f.WriteString(env.RenderFull()) + return err } diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 813ff9a..42bb31e 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -33,9 +33,11 @@ func New(scanner Scanner, filename string) *Parser { // Parse parses the .env file and returns an ast.Statement. func (p *Parser) Parse() (*ast.Document, error) { - var group *ast.Group - var comments []*ast.Comment - var previousStatement ast.Statement + var ( + group *ast.Group + comments []*ast.Comment + previousStatement ast.Statement + ) global := &ast.Document{} @@ -248,8 +250,10 @@ func (p *Parser) parseCommentStatement() (ast.Statement, error) { } func (p *Parser) parseRowStatement() (ast.Statement, error) { - var err error - var stmt *ast.Assignment + var ( + err error + stmt *ast.Assignment + ) name := p.token.Literal active := !p.token.Commented @@ -258,18 +262,24 @@ func (p *Parser) parseRowStatement() (ast.Statement, error) { switch p.token.Type { case token.NewLine, token.EOF: - stmt, err = p.parseNakedAssign(name) + stmt = p.parseNakedAssign(name) case token.Assign: p.nextToken() switch p.token.Type { case token.NewLine, token.EOF: - stmt, err = p.parseNakedAssign(name) + stmt = p.parseNakedAssign(name) case token.Value, token.RawValue: stmt, err = p.parseCompleteAssign(name) + + default: + _, err = p.unexpectedToken() } + + default: + _, err = p.unexpectedToken() } if err != nil { @@ -285,7 +295,7 @@ func (p *Parser) parseRowStatement() (ast.Statement, error) { return p.unexpectedToken() } -func (p *Parser) parseNakedAssign(name string) (*ast.Assignment, error) { +func (p *Parser) parseNakedAssign(name string) *ast.Assignment { defer p.nextToken() return &ast.Assignment{ @@ -297,7 +307,7 @@ func (p *Parser) parseNakedAssign(name string) (*ast.Assignment, error) { Line: p.token.LineNumber, LastLine: p.token.LineNumber, }, - }, nil + } } func (p *Parser) parseCompleteAssign(name string) (*ast.Assignment, error) { diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 3e6176a..5e90563 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -15,6 +15,8 @@ func TestParser_Parse(t *testing.T) { t.Parallel() t.Run("parse assigment successful", func(t *testing.T) { + t.Parallel() + tests := []struct { name string input string @@ -30,6 +32,7 @@ func TestParser_Parse(t *testing.T) { Literal: "value", Interpolated: "value", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -51,6 +54,7 @@ func TestParser_Parse(t *testing.T) { Literal: "value", Interpolated: "value", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -72,6 +76,7 @@ func TestParser_Parse(t *testing.T) { Literal: "value", Interpolated: "value", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -94,6 +99,7 @@ func TestParser_Parse(t *testing.T) { Interpolated: "", Active: true, Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -113,6 +119,7 @@ func TestParser_Parse(t *testing.T) { Literal: "", Interpolated: "", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -131,6 +138,7 @@ func TestParser_Parse(t *testing.T) { &ast.Newline{ Blank: true, Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -141,6 +149,7 @@ func TestParser_Parse(t *testing.T) { Literal: "", Interpolated: "", Position: ast.Position{ + File: "-", Line: 5, FirstLine: 5, LastLine: 5, @@ -152,6 +161,7 @@ func TestParser_Parse(t *testing.T) { &ast.Newline{ Blank: true, Position: ast.Position{ + File: "-", Line: 6, FirstLine: 6, LastLine: 6, @@ -173,6 +183,7 @@ func TestParser_Parse(t *testing.T) { Active: true, Quote: token.NoQuotes, Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -186,6 +197,7 @@ func TestParser_Parse(t *testing.T) { Active: true, Quote: token.NoQuotes, Position: ast.Position{ + File: "-", Line: 2, FirstLine: 2, LastLine: 2, @@ -199,6 +211,7 @@ func TestParser_Parse(t *testing.T) { Active: true, Quote: token.NoQuotes, Position: ast.Position{ + File: "-", Line: 3, FirstLine: 3, LastLine: 3, @@ -217,6 +230,7 @@ func TestParser_Parse(t *testing.T) { Literal: ":9090", Interpolated: ":9090", Position: ast.Position{ + File: "-", Line: 2, FirstLine: 1, LastLine: 2, @@ -228,6 +242,7 @@ func TestParser_Parse(t *testing.T) { { Value: "# comment 1", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -238,6 +253,7 @@ func TestParser_Parse(t *testing.T) { &ast.Comment{ Value: "# comment 2", Position: ast.Position{ + File: "-", Line: 3, FirstLine: 3, LastLine: 3, @@ -256,6 +272,7 @@ func TestParser_Parse(t *testing.T) { Literal: "bar\nbaz", Interpolated: "bar\nbaz", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -277,6 +294,7 @@ func TestParser_Parse(t *testing.T) { Literal: "bar\nbaz", Interpolated: "bar\nbaz", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -298,6 +316,7 @@ func TestParser_Parse(t *testing.T) { Literal: "'d'", Interpolated: "'d'", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -319,6 +338,7 @@ func TestParser_Parse(t *testing.T) { Literal: "foobar=", Interpolated: "foobar=", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -340,6 +360,7 @@ func TestParser_Parse(t *testing.T) { Literal: "bar # this is foo", Interpolated: "bar # this is foo", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -361,6 +382,7 @@ func TestParser_Parse(t *testing.T) { Literal: "bar#baz", Interpolated: "bar#baz", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, @@ -382,6 +404,7 @@ func TestParser_Parse(t *testing.T) { Literal: "bar#baz", Interpolated: "bar#baz", Position: ast.Position{ + File: "-", Line: 1, FirstLine: 1, LastLine: 1, diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go index 3e687f1..27a310d 100644 --- a/pkg/scanner/scanner.go +++ b/pkg/scanner/scanner.go @@ -270,6 +270,7 @@ func (s *Scanner) scanQuotedValue(tType token.Type, quote token.Quote) token.Tok for { if isEOF(s.rune) || isNewLine(s.rune) { tType = token.Illegal + break } @@ -332,6 +333,7 @@ func (s *Scanner) prev() rune { case s.prevOffset < len(s.input): r, _ := s.scanRune(s.prevOffset) + return r default: @@ -349,11 +351,11 @@ func (s *Scanner) scanRune(offset int) (rune, int) { // not ASCII r, width = utf8.DecodeRune([]byte(s.input[offset:])) if r == utf8.RuneError && width == 1 { - panic("illegal UTF-8 encoding on position " + strconv.Itoa(int(offset))) + panic("illegal UTF-8 encoding on position " + strconv.Itoa(offset)) } if r == bom && s.offset > 0 { - panic("illegal byte order mark on position " + strconv.Itoa(int(offset))) + panic("illegal byte order mark on position " + strconv.Itoa(offset)) } } @@ -394,6 +396,7 @@ func isSymbol(r rune) bool { case '_', '.', ',', '-': return true } + return false } diff --git a/pkg/validation/explain.go b/pkg/validation/explain.go index 606804e..72f8f44 100644 --- a/pkg/validation/explain.go +++ b/pkg/validation/explain.go @@ -75,16 +75,19 @@ func AskToCreateDirectory(path string) { Run() if err != nil { tui.Theme.Warning.StderrPrinter().Println(" Prompt cancelled: " + err.Error()) + return } if !confirm { tui.Theme.Warning.StderrPrinter().Println(" Prompt cancelled") + return } if err := os.MkdirAll(path, os.ModePerm); err != nil { tui.Theme.Danger.StderrPrinter().Println(" Could not create directory: " + err.Error()) + return } @@ -104,12 +107,14 @@ func AskToSetValue(env *ast.Document, assignment *ast.Assignment) { Run() if err != nil { tui.Theme.Warning.StderrPrinter().Println(" Prompt cancelled: " + err.Error()) + return } assignment.Literal = value if err := pkg.Save(assignment.Position.File, env); err != nil { tui.Theme.Danger.StderrPrinter().Println(" Could not update key with value [" + value + "]: " + err.Error()) + return } diff --git a/pkg/validation/validation.go b/pkg/validation/validation.go index 49a71fc..5093803 100644 --- a/pkg/validation/validation.go +++ b/pkg/validation/validation.go @@ -32,12 +32,14 @@ func Validate(d *ast.Document) []ValidationError { data[assignment.Name] = assignment.Interpolated rules[assignment.Name] = validationRules + fieldOrder = append(fieldOrder, assignment.Name) } errors := validator.New(validator.WithRequiredStructEnabled()).ValidateMap(data, rules) result := []ValidationError{} + for _, field := range fieldOrder { if err, ok := errors[field]; ok { result = append(result, ValidationError{