Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75dc54b78 |
+2
-1
@@ -9,7 +9,8 @@ RUN go mod download
|
||||
COPY . .
|
||||
|
||||
RUN make fmt-check
|
||||
RUN make lint
|
||||
# Not make lint: script/lint is a docker build, which cannot run in here.
|
||||
RUN golangci-lint run --config .golangci.yml ./...
|
||||
|
||||
# Build stage — tests and compilation
|
||||
# golang 1.24.13-alpine (2026-03-10)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Lint image, built by script/lint: golangci-lint runs as a build step, so a
|
||||
# successful build is a clean lint. Works where the docker daemon is remote
|
||||
# and bind mounts are impossible.
|
||||
|
||||
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
||||
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS deps
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# script/lint rebuilds this stage on every run, by this name; the module
|
||||
# download above stays cached.
|
||||
FROM deps AS lint
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN golangci-lint run --config .golangci.yml ./...
|
||||
@@ -496,15 +496,18 @@ standard: normalized scripts in `script/` are the entrypoints for the
|
||||
development workflow, and the Makefile targets are thin shims that call
|
||||
them. We provide:
|
||||
|
||||
- `script/bootstrap` — install all dependencies (Go, golangci-lint, Go
|
||||
module download), idempotently
|
||||
- `script/bootstrap` — install all dependencies (Go, Go module
|
||||
download), idempotently; golangci-lint is not installed, it runs in
|
||||
docker
|
||||
- `script/setup` — make a fresh clone ready for development: runs
|
||||
`script/bootstrap`, then `script/install-precommit`
|
||||
- `script/projectname` — output the project name (`secret`); used by
|
||||
other scripts such as `script/docker`
|
||||
- `script/test` — run `go vet` and the test suite (verbose rerun on
|
||||
failure)
|
||||
- `script/lint` — run `golangci-lint`
|
||||
- `script/lint` — run `golangci-lint` in docker only: builds
|
||||
`Dockerfile.lint`, where the linter is a build step that runs on every
|
||||
call, also on an unchanged tree
|
||||
- `script/fmt` — format all Go code (writes)
|
||||
- `script/fmt-check` — check formatting without writing
|
||||
- `script/check` — run `script/test`, `script/lint`, and
|
||||
|
||||
@@ -25,13 +25,13 @@ Bring the repo into policy compliance in one commit:
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-03: The checks run before changing a vault now stop with an
|
||||
error naming the path and cause when they cannot read what they
|
||||
inspect, instead of reading the failure as "nothing there": the
|
||||
duplicate check before `unlocker add pgp` (an unreadable
|
||||
`unlockers.d`), the secret count that guards removing the last
|
||||
unlocker and removing a vault, and the existing long-term key check
|
||||
before `vault import`.
|
||||
- 2026-10-04: Lint runs only in docker: `script/lint` builds
|
||||
`Dockerfile.lint`, where golangci-lint is a build step rebuilt on
|
||||
every run (`--no-cache-filter`), so an unchanged tree is linted too;
|
||||
the module download stays cached. `script/bootstrap` no longer
|
||||
installs golangci-lint, and the `Dockerfile` lint stage calls it
|
||||
directly instead of `make lint`. `golangci-lint config verify` is not
|
||||
run: it fetches its schema live over unpinned HTTPS.
|
||||
- 2026-10-03: `version rm`, `version promote` and `get --version`
|
||||
accept a version only if it is one of the versions `version list`
|
||||
lists for that secret, compared as typed before any path is built
|
||||
|
||||
+31
-32
@@ -49,6 +49,7 @@ var (
|
||||
"is already added as an unlocker")
|
||||
errUnsupportedUnlockerType = errors.New("unsupported unlocker type")
|
||||
errLastUnlocker = errors.New("refusing to remove last unlocker")
|
||||
errUnlockerExists = errors.New("unlocker already exists")
|
||||
)
|
||||
|
||||
// UnlockerInfo represents unlocker information for display
|
||||
@@ -345,10 +346,9 @@ func unlockerIDFromDir(
|
||||
// stored metadata matches the given type and creation time and returns
|
||||
// the matching unlocker's ID. It returns ("", nil) when the directory is
|
||||
// readable but holds no match, and a non-nil error when the directory
|
||||
// itself cannot be read, which means the unlocker's ID cannot be known.
|
||||
// `unlocker list` then skips the entry rather than show a made-up ID; the
|
||||
// duplicate check before adding an unlocker must stop instead, because
|
||||
// the skipped entry may be the duplicate.
|
||||
// itself cannot be read. Callers must distinguish the two: an unreadable
|
||||
// directory means the unlocker's real ID is unknowable, so the entry has
|
||||
// to be skipped rather than reported under a synthesized ID.
|
||||
func findUnlockerIDByMetadata(
|
||||
fs afero.Fs, unlockersDir string, metadata secret.UnlockerMetadata,
|
||||
includeSecureEnclave bool,
|
||||
@@ -691,15 +691,8 @@ func (cli *Instance) addPGPUnlocker(cmd *cobra.Command) error {
|
||||
// Check if this GPG key is already added
|
||||
expectedID := "pgp-" + fingerprint
|
||||
|
||||
exists, err := cli.checkUnlockerExists(vlt, expectedID)
|
||||
err = cli.checkUnlockerExists(vlt, expectedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not check whether GPG key %s is already an unlocker: %w",
|
||||
gpgKeyID, err,
|
||||
)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("GPG key %s %w", gpgKeyID, errGPGKeyAlreadyUnlocker)
|
||||
}
|
||||
|
||||
@@ -779,38 +772,44 @@ func (cli *Instance) UnlockerSelect(unlockerID string) error {
|
||||
return vlt.SelectUnlocker(unlockerID)
|
||||
}
|
||||
|
||||
// checkUnlockerExists reports whether the vault already has an unlocker
|
||||
// with the given ID. It returns an error, and no answer, when unlockers.d
|
||||
// cannot be read; the caller must then not create the unlocker.
|
||||
func (cli *Instance) checkUnlockerExists(
|
||||
vlt *vault.Vault, unlockerID string,
|
||||
) (bool, error) {
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// checkUnlockerExists checks if an unlocker with the given ID exists
|
||||
func (cli *Instance) checkUnlockerExists(vlt *vault.Vault, unlockerID string) error {
|
||||
// Get the list of unlockers and check if any match the ID
|
||||
unlockers, err := vlt.ListUnlockers()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf(
|
||||
"failed to list unlockers in %s: %w", unlockersDir, err,
|
||||
)
|
||||
secret.Warn("Could not list unlockers during duplicate check", "error", err)
|
||||
|
||||
return nil // If we can't list unlockers, assume it doesn't exist
|
||||
}
|
||||
|
||||
// Get vault directory to construct unlocker instances
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Warn("Could not get vault directory during duplicate check",
|
||||
"error", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check each unlocker's ID
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
for _, metadata := range unlockers {
|
||||
// Construct the unlocker matching this metadata to get its ID
|
||||
id, err := findUnlockerIDByMetadata(cli.fs, unlockersDir, metadata, true)
|
||||
if err != nil {
|
||||
// Unlike `unlocker list`, never skip here: a skipped entry may be the duplicate.
|
||||
return false, err
|
||||
secret.Warn(
|
||||
"Could not read unlockers directory during duplicate check, "+
|
||||
"skipping unlocker",
|
||||
"unlockers_dir", unlockersDir, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if id != "" && id == unlockerID {
|
||||
return true, nil
|
||||
return errUnlockerExists
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
// Unreadable Directory Tests
|
||||
//
|
||||
// The checks that guard adding a PGP unlocker (is this key already an
|
||||
// unlocker?), removing the last unlocker and removing a vault (does the
|
||||
// vault hold secrets?), and importing a mnemonic (does the vault already
|
||||
// have a long-term key?) each look at the vault on disk before acting.
|
||||
// When that look fails they must refuse to act, not read the failure as
|
||||
// "nothing there" and go ahead.
|
||||
|
||||
//nolint:testpackage // white-box test of unexported internals
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
// unreadableTestGPGUserID is the user ID of the throwaway GPG key the
|
||||
// PGP unlocker tests generate, and the --keyid they pass.
|
||||
unreadableTestGPGUserID = "unlocker-test@example.com"
|
||||
|
||||
// unreadableTestSecretName is the secret stored in the vaults the
|
||||
// removal tests remove from.
|
||||
unreadableTestSecretName = "api-key"
|
||||
|
||||
// unreadableTestOtherVault is a second vault for the vault removal
|
||||
// test, since the last vault can never be removed.
|
||||
unreadableTestOtherVault = "work"
|
||||
|
||||
// unreadableTestSecretsDirName is the directory holding a vault's
|
||||
// secrets, and unreadableTestCurrentFileName the per-secret file
|
||||
// naming its current version.
|
||||
unreadableTestSecretsDirName = "secrets.d"
|
||||
unreadableTestCurrentFileName = "current"
|
||||
)
|
||||
|
||||
// errStatFailed is returned by statFailFs in place of a successful stat.
|
||||
var errStatFailed = errors.New("input/output error")
|
||||
|
||||
// statFailFs fails every Stat of one path, as an I/O or permission error
|
||||
// on that path would.
|
||||
type statFailFs struct {
|
||||
afero.Fs
|
||||
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *statFailFs) Stat(name string) (os.FileInfo, error) {
|
||||
if name == f.path {
|
||||
return nil, errStatFailed
|
||||
}
|
||||
|
||||
return f.Fs.Stat(name)
|
||||
}
|
||||
|
||||
// testVaultDir returns the directory of the named vault in the synthetic
|
||||
// state directory built by newListTestVault.
|
||||
func testVaultDir(vaultName string) string {
|
||||
return filepath.Join(listTestStateDir, "vaults.d", vaultName)
|
||||
}
|
||||
|
||||
// newTestInstance returns a CLI instance on fs whose output is discarded.
|
||||
func newTestInstance(fs afero.Fs) (*Instance, *cobra.Command) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
return &Instance{fs: fs, stateDir: listTestStateDir, cmd: cmd}, cmd
|
||||
}
|
||||
|
||||
// assertDirEntries asserts that dir holds exactly the named entries.
|
||||
func assertDirEntries(t *testing.T, fs afero.Fs, dir string, want ...string) {
|
||||
t.Helper()
|
||||
|
||||
entries, err := afero.ReadDir(fs, dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, want, names)
|
||||
}
|
||||
|
||||
// newTestGPGKey points GNUPGHOME at a fresh directory, generates a GPG key
|
||||
// without a passphrase there, and returns the key's fingerprint.
|
||||
func newTestGPGKey(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("GNUPGHOME", t.TempDir())
|
||||
|
||||
t.Cleanup(func() {
|
||||
// Stop the gpg-agent that key generation starts. t.Context is
|
||||
// already canceled when cleanup runs.
|
||||
ctx := context.WithoutCancel(t.Context())
|
||||
_ = exec.CommandContext(ctx, "gpgconf", "--kill", "gpg-agent").Run()
|
||||
})
|
||||
|
||||
output, err := exec.CommandContext(t.Context(), "gpg", "--batch",
|
||||
"--pinentry-mode", "loopback", "--passphrase", "",
|
||||
"--quick-gen-key", unreadableTestGPGUserID, "ed25519", "sign", "never",
|
||||
).CombinedOutput()
|
||||
require.NoError(t, err, "generating the test GPG key: %s", output)
|
||||
|
||||
fingerprint, err := secret.ResolveGPGKeyFingerprint(unreadableTestGPGUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
return fingerprint
|
||||
}
|
||||
|
||||
// addTestPGPUnlocker runs `secret unlocker add pgp` for the test key
|
||||
// against fs.
|
||||
func addTestPGPUnlocker(fs afero.Fs) error {
|
||||
instance, cmd := newTestInstance(fs)
|
||||
cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
||||
|
||||
return instance.addPGPUnlocker(cmd)
|
||||
}
|
||||
|
||||
// TestAddPGPUnlockerDuplicateCheck asserts that adding a PGP unlocker
|
||||
// fails, and creates no unlocker directory, when unlockers.d cannot be
|
||||
// read for the duplicate check; and, as the control case, that a readable
|
||||
// unlockers.d holding the same key is still refused as a duplicate.
|
||||
//
|
||||
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
||||
func TestAddPGPUnlockerDuplicateCheck(t *testing.T) {
|
||||
fingerprint := newTestGPGKey(t)
|
||||
unlockersDir := filepath.Join(
|
||||
testVaultDir(listTestVaultName), listTestUnlockersDirName)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
openBudget int
|
||||
}{
|
||||
// The vault's own enumeration of unlockers.d fails.
|
||||
{name: "listing fails", openBudget: 0},
|
||||
// The enumeration succeeds; the rescan that resolves IDs fails.
|
||||
{name: "rescan fails", openBudget: 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
base := newListTestVault(t, 1)
|
||||
fs := &unlockersDirFailFs{Fs: base, openBudget: tt.openBudget}
|
||||
|
||||
err := addTestPGPUnlocker(fs)
|
||||
|
||||
require.ErrorIs(t, err, errUnlockersDirUnreadable)
|
||||
require.NotErrorIs(t, err, errGPGKeyAlreadyUnlocker)
|
||||
assert.Contains(t, err.Error(), unlockersDir,
|
||||
"the error must name the directory it could not read")
|
||||
assertDirEntries(t, base, unlockersDir, listTestUnlockerDirOne)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("duplicate refused", func(t *testing.T) {
|
||||
base := newListTestVault(t, 1)
|
||||
writePGPUnlocker(t, base, unlockersDir, listTestUnlockerDirTwo,
|
||||
time.Date(2026, time.August, 10, 12, 30, 0, 0, time.UTC),
|
||||
fingerprint)
|
||||
|
||||
err := addTestPGPUnlocker(base)
|
||||
|
||||
require.ErrorIs(t, err, errGPGKeyAlreadyUnlocker)
|
||||
assertDirEntries(t, base, unlockersDir,
|
||||
listTestUnlockerDirOne, listTestUnlockerDirTwo)
|
||||
})
|
||||
}
|
||||
|
||||
// writeTestSecret stores a secret with a current-version pointer, which is
|
||||
// what makes it count as a secret, in the given vault directory.
|
||||
func writeTestSecret(t *testing.T, fs afero.Fs, vaultDir string) {
|
||||
t.Helper()
|
||||
|
||||
secretDir := filepath.Join(
|
||||
vaultDir, unreadableTestSecretsDirName, unreadableTestSecretName)
|
||||
require.NoError(t, fs.MkdirAll(secretDir, listTestDirPerm))
|
||||
require.NoError(t, afero.WriteFile(fs,
|
||||
filepath.Join(secretDir, unreadableTestCurrentFileName),
|
||||
[]byte("20260809.001"), listTestFilePerm))
|
||||
}
|
||||
|
||||
// TestRemoveLastUnlockerAbortsWhenSecretsUnreadable asserts that the last
|
||||
// unlocker is kept when the secrets it protects cannot be counted.
|
||||
func TestRemoveLastUnlockerAbortsWhenSecretsUnreadable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vaultDir := testVaultDir(listTestVaultName)
|
||||
unlockersDir := filepath.Join(vaultDir, listTestUnlockersDirName)
|
||||
secretsDir := filepath.Join(vaultDir, unreadableTestSecretsDirName)
|
||||
|
||||
for _, path := range []string{
|
||||
secretsDir,
|
||||
filepath.Join(secretsDir, unreadableTestSecretName,
|
||||
unreadableTestCurrentFileName),
|
||||
} {
|
||||
t.Run(filepath.Base(path), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := newListTestVault(t, 1)
|
||||
writeTestSecret(t, base, vaultDir)
|
||||
instance, cmd := newTestInstance(&statFailFs{Fs: base, path: path})
|
||||
|
||||
err := instance.UnlockersRemove(
|
||||
"pgp-"+listTestGPGKeyID+"A", false, cmd)
|
||||
|
||||
require.ErrorIs(t, err, errStatFailed)
|
||||
assertDirEntries(t, base, unlockersDir, listTestUnlockerDirOne)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRemoveVaultAbortsWhenSecretsDirUnreadable asserts that a vault is
|
||||
// kept when whether it holds secrets cannot be determined.
|
||||
func TestRemoveVaultAbortsWhenSecretsDirUnreadable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := newListTestVault(t, 1)
|
||||
vaultDir := testVaultDir(unreadableTestOtherVault)
|
||||
writeTestSecret(t, base, vaultDir)
|
||||
instance, cmd := newTestInstance(&statFailFs{
|
||||
Fs: base, path: filepath.Join(vaultDir, unreadableTestSecretsDirName),
|
||||
})
|
||||
|
||||
err := instance.RemoveVault(cmd, unreadableTestOtherVault, false)
|
||||
|
||||
require.ErrorIs(t, err, errStatFailed)
|
||||
|
||||
exists, err := afero.DirExists(base, vaultDir)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists, "the vault must not be removed")
|
||||
}
|
||||
|
||||
// TestVaultImportAbortsWhenPubKeyUnreadable asserts that a mnemonic import
|
||||
// stops when whether the vault already has a long-term key cannot be
|
||||
// determined.
|
||||
func TestVaultImportAbortsWhenPubKeyUnreadable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := newListTestVault(t, 1)
|
||||
instance, cmd := newTestInstance(&statFailFs{
|
||||
Fs: base, path: filepath.Join(testVaultDir(listTestVaultName), "pub.age"),
|
||||
})
|
||||
|
||||
err := instance.VaultImport(cmd, listTestVaultName)
|
||||
|
||||
require.ErrorIs(t, err, errStatFailed)
|
||||
}
|
||||
+7
-23
@@ -388,12 +388,8 @@ func (cli *Instance) vaultImportPreflight(
|
||||
// Check if vault already has a public key
|
||||
pubKeyPath := vaultDir + "/pub.age"
|
||||
|
||||
exists, err = afero.Exists(cli.fs, pubKeyPath)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to check %s: %w", pubKeyPath, err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
_, err = cli.fs.Stat(pubKeyPath)
|
||||
if err == nil {
|
||||
return "", "", "", fmt.Errorf("vault '%s' %w",
|
||||
vaultName, errVaultHasLongTermKey)
|
||||
}
|
||||
@@ -540,26 +536,17 @@ func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
||||
}
|
||||
|
||||
// vaultHasSecrets reports whether the vault directory contains any secrets
|
||||
func (cli *Instance) vaultHasSecrets(vaultDir string) (bool, error) {
|
||||
func (cli *Instance) vaultHasSecrets(vaultDir string) bool {
|
||||
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
||||
|
||||
exists, err := afero.DirExists(cli.fs, secretsDir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check secrets directory %s: %w",
|
||||
secretsDir, err)
|
||||
}
|
||||
|
||||
exists, _ := afero.DirExists(cli.fs, secretsDir)
|
||||
if !exists {
|
||||
return false, nil
|
||||
return false
|
||||
}
|
||||
|
||||
entries, err := afero.ReadDir(cli.fs, secretsDir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read secrets directory %s: %w",
|
||||
secretsDir, err)
|
||||
}
|
||||
|
||||
return len(entries) > 0, nil
|
||||
return err == nil && len(entries) > 0
|
||||
}
|
||||
|
||||
// switchAwayFromVault selects another vault as current before removal
|
||||
@@ -623,10 +610,7 @@ func (cli *Instance) RemoveVault(cmd *cobra.Command, name string, force bool) er
|
||||
}
|
||||
|
||||
// Check if vault has secrets
|
||||
hasSecrets, err := cli.vaultHasSecrets(vaultDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hasSecrets := cli.vaultHasSecrets(vaultDir)
|
||||
|
||||
// Require --force if vault has secrets
|
||||
if hasSecrets && !force {
|
||||
|
||||
@@ -138,12 +138,7 @@ func (v *Vault) NumSecrets() (int, error) {
|
||||
|
||||
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
||||
|
||||
exists, err := afero.DirExists(v.fs, secretsDir)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check secrets directory %s: %w",
|
||||
secretsDir, err)
|
||||
}
|
||||
|
||||
exists, _ := afero.DirExists(v.fs, secretsDir)
|
||||
if !exists {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -167,7 +162,7 @@ func (v *Vault) NumSecrets() (int, error) {
|
||||
|
||||
exists, err := afero.Exists(v.fs, currentFile)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check %s: %w", currentFile, err)
|
||||
continue // Skip directories we can't read
|
||||
}
|
||||
|
||||
if exists {
|
||||
|
||||
+1
-6
@@ -6,6 +6,7 @@
|
||||
# make, node, yarn, go, or python). Node is used directly if installed;
|
||||
# otherwise a pinned version is installed via nvm (installing nvm
|
||||
# itself first, from a hash-verified release archive, never curl | sh).
|
||||
# golangci-lint is never installed: script/lint runs it in docker.
|
||||
#
|
||||
# Uncomment the language sections in main() that apply to this repo.
|
||||
set -eu
|
||||
@@ -136,12 +137,6 @@ main() {
|
||||
|
||||
# ---- Go repos ----
|
||||
if missing go; then pkg_install go golang go go; fi
|
||||
# golangci-lint: packaged in nix, brew, and apk. On apt there is no
|
||||
# package: download a specific release archive from GitHub and
|
||||
# verify its hash (verify_sha256), never curl | sh.
|
||||
if missing golangci-lint; then
|
||||
pkg_install golangci-lint golangci-lint golangci-lint golangci-lint
|
||||
fi
|
||||
go mod download
|
||||
|
||||
# ---- Python repos ----
|
||||
|
||||
+14
-4
@@ -1,14 +1,24 @@
|
||||
#!/bin/sh
|
||||
# script/lint: run the linter.
|
||||
# script/lint: run the linter, in docker only. Builds Dockerfile.lint,
|
||||
# where golangci-lint runs as a build step.
|
||||
#
|
||||
# A cached build lints nothing, so --no-cache-filter rebuilds the lint
|
||||
# stage on every run, an unchanged tree included. It ignores a stage name
|
||||
# that does not exist, so --target names the same stage: a rename then
|
||||
# fails the build instead of serving the lint from cache. cacheonly keeps
|
||||
# no image; only the build's success matters.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
# CGO is required (Makefile exports this too)
|
||||
export CGO_ENABLED=1
|
||||
golangci-lint run --timeout 5m
|
||||
docker build \
|
||||
--progress=plain \
|
||||
--target lint \
|
||||
--no-cache-filter=lint \
|
||||
--output=type=cacheonly \
|
||||
-f Dockerfile.lint .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user