Rework CI validation workflow and makefile (#460)
* rework CI validation workflow and makefile * enable push * fix job names * fix license check * fix snapshot builds * fix acceptance tests * fix linting * disable pull request event * rework windows runner caching * disable release pipeline and add issue templates
This commit is contained in:
parent
42925c1b05
commit
d5e8a92968
12
.bouncer.yaml
Normal file
12
.bouncer.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
permit:
|
||||
- BSD.*
|
||||
- MIT.*
|
||||
- Apache.*
|
||||
- MPL.*
|
||||
- ISC
|
||||
- WTFPL
|
||||
|
||||
ignore-packages:
|
||||
# crypto/internal/boring is released under the openSSL license as a part of the Golang Standard Library
|
||||
- crypto/internal/boring
|
||||
|
@ -1,59 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
run-static-analyses:
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
working_directory: /home/circleci/app
|
||||
docker:
|
||||
- image: cimg/go:<< parameters.version >>
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- golang-<< parameters.version >>-{{ checksum "go.sum" }}
|
||||
- run: make ci-install-go-tools
|
||||
- save_cache:
|
||||
key: golang-<< parameters.version >>-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "/go/pkg/mod"
|
||||
- run:
|
||||
name: run static analysis
|
||||
command: make ci-static-analysis
|
||||
|
||||
run-tests:
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
working_directory: /home/circleci/app
|
||||
docker:
|
||||
- image: cimg/go:<< parameters.version >>
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- golang-<< parameters.version >>-{{ checksum "go.sum" }}
|
||||
- run: make ci-install-go-tools
|
||||
- save_cache:
|
||||
key: golang-<< parameters.version >>-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "/go/pkg/mod"
|
||||
- run:
|
||||
name: run unit tests
|
||||
command: make ci-unit-test
|
||||
|
||||
|
||||
workflows:
|
||||
commit:
|
||||
jobs:
|
||||
- run-static-analyses:
|
||||
version: "1.19"
|
||||
- run-tests:
|
||||
version: "1.19"
|
||||
- run-tests:
|
||||
version: "1.19"
|
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1,2 +1 @@
|
||||
github: ['wagoodman']
|
||||
custom: ['https://www.paypal.me/wagoodman']
|
||||
|
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something isn't working as expected
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
||||
**Environment**:
|
||||
- OS version
|
||||
- Docker version (if applicable)
|
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Got an idea for a new feature? Let us know!
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What would you like to be added**:
|
||||
|
||||
**Why is this needed**:
|
||||
|
||||
**Additional context**:
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
76
.github/actions/bootstrap/action.yaml
vendored
Normal file
76
.github/actions/bootstrap/action.yaml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
name: "Bootstrap"
|
||||
description: "Bootstrap all tools and dependencies"
|
||||
inputs:
|
||||
go-version:
|
||||
description: "Go version to install"
|
||||
required: true
|
||||
default: "1.20.x"
|
||||
use-go-cache:
|
||||
description: "Restore go cache"
|
||||
required: true
|
||||
default: "true"
|
||||
cache-key-prefix:
|
||||
description: "Prefix all cache keys with this value"
|
||||
required: true
|
||||
default: "efa04b89c1b1"
|
||||
build-cache-key-prefix:
|
||||
description: "Prefix build cache key with this value"
|
||||
required: true
|
||||
default: "f8b6d31dea"
|
||||
bootstrap-apt-packages:
|
||||
description: "Space delimited list of tools to install via apt"
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
- name: Restore tool cache
|
||||
id: tool-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.tmp
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||
|
||||
# note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in
|
||||
# some installations of project tools.
|
||||
- name: Restore go module cache
|
||||
id: go-mod-cache
|
||||
if: inputs.use-go-cache == 'true'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap project tools
|
||||
shell: bash
|
||||
if: steps.tool-cache.outputs.cache-hit != 'true'
|
||||
run: make bootstrap-tools
|
||||
|
||||
- name: Restore go build cache
|
||||
id: go-cache
|
||||
if: inputs.use-go-cache == 'true'
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
key: ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-
|
||||
|
||||
- name: (cache-miss) Bootstrap go dependencies
|
||||
shell: bash
|
||||
if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true'
|
||||
run: make bootstrap-go
|
||||
|
||||
- name: Install apt packages
|
||||
if: inputs.bootstrap-apt-packages != ''
|
||||
shell: bash
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}
|
11
.github/scripts/ci-check.sh
vendored
Executable file
11
.github/scripts/ci-check.sh
vendored
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
red=$(tput setaf 1)
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
# assert we are running in CI (or die!)
|
||||
if [[ -z "$CI" ]]; then
|
||||
echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}"
|
||||
exit 1
|
||||
fi
|
36
.github/scripts/coverage.py
vendored
Executable file
36
.github/scripts/coverage.py
vendored
Executable file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import sys
|
||||
import shlex
|
||||
|
||||
|
||||
class bcolors:
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
||||
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: coverage.py [threshold] [go-coverage-report]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
threshold = float(sys.argv[1])
|
||||
report = sys.argv[2]
|
||||
|
||||
|
||||
args = shlex.split(f"go tool cover -func {report}")
|
||||
p = subprocess.run(args, capture_output=True, text=True)
|
||||
|
||||
percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", ""))
|
||||
print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}")
|
||||
|
||||
if percent_coverage < threshold:
|
||||
print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}")
|
||||
sys.exit(1)
|
31
.github/scripts/go-mod-tidy-check.sh
vendored
Executable file
31
.github/scripts/go-mod-tidy-check.sh
vendored
Executable file
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||
|
||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
|
||||
echo "Capturing original state of files..."
|
||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
|
||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
||||
go mod tidy
|
||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
echo ""
|
||||
|
||||
set +e
|
||||
|
||||
# Detect difference between the git HEAD state and the go mod tidy state
|
||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||
|
||||
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
|
||||
echo "go.mod diff:"
|
||||
echo "${DIFF_MOD}"
|
||||
echo "go.sum diff:"
|
||||
echo "${DIFF_SUM}"
|
||||
echo ""
|
||||
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
|
||||
exit 1
|
||||
fi
|
50
.github/scripts/trigger-release.sh
vendored
Executable file
50
.github/scripts/trigger-release.sh
vendored
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
bold=$(tput bold)
|
||||
normal=$(tput sgr0)
|
||||
|
||||
if ! [ -x "$(command -v gh)" ]; then
|
||||
echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh auth status
|
||||
|
||||
# we need all of the git state to determine the next version. Since tagging is done by
|
||||
# the release pipeline it is possible to not have all of the tags from previous releases.
|
||||
git fetch --tags
|
||||
|
||||
# populates the CHANGELOG.md and VERSION files
|
||||
echo "${bold}Generating changelog...${normal}"
|
||||
make changelog 2> /dev/null
|
||||
|
||||
NEXT_VERSION=$(cat VERSION)
|
||||
|
||||
if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then
|
||||
echo "Could not determine the next version to release. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn
|
||||
case $yn in
|
||||
[Yy]* ) echo; break;;
|
||||
[Nn]* ) echo; echo "Cancelling release..."; exit;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
|
||||
echo
|
||||
gh workflow run release.yaml -f version=${NEXT_VERSION}
|
||||
|
||||
echo
|
||||
echo "${bold}Waiting for release to start...${normal}"
|
||||
sleep 10
|
||||
|
||||
set +e
|
||||
|
||||
echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
|
||||
id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
|
||||
gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed)
|
180
.github/workflows/pipeline.yml
vendored
180
.github/workflows/pipeline.yml
vendored
@ -1,180 +0,0 @@
|
||||
name: 'app-pipeline'
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
types: [ opened, reopened ]
|
||||
env:
|
||||
DOCKER_CLI_VERSION: "19.03.1"
|
||||
jobs:
|
||||
unit-test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
# todo: support windows
|
||||
platform: [ubuntu-latest, macos-latest]
|
||||
# platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Cache go dependencies
|
||||
id: unit-cache-go-dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-${{ matrix.go-version }}-
|
||||
|
||||
- name: Install go dependencies
|
||||
if: steps.unit-cache-go-dependencies.outputs.cache-hit != 'true'
|
||||
run: go get ./...
|
||||
|
||||
- name: Test
|
||||
run: make ci-unit-test
|
||||
|
||||
build-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.19.x'
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Install tooling
|
||||
run: |
|
||||
make ci-install-go-tools
|
||||
make ci-install-ci-tools
|
||||
|
||||
- name: Cache go dependencies
|
||||
id: package-cache-go-dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-prod-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-prod-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.package-cache-go-dependencies.outputs.cache-hit != 'true'
|
||||
run: go get ./...
|
||||
|
||||
- name: Linting, formatting, and other static code analyses
|
||||
run: make ci-static-analysis
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
run: make ci-build-snapshot-packages
|
||||
|
||||
- run: docker images wagoodman/dive
|
||||
|
||||
# todo: compare against known json output in shared volume
|
||||
- name: Test production image
|
||||
run: make ci-test-production-image
|
||||
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist
|
||||
|
||||
|
||||
test-linux-artifacts:
|
||||
needs: [ build-artifacts ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist
|
||||
|
||||
- name: Test linux run
|
||||
run: make ci-test-linux-run
|
||||
|
||||
- name: Test DEB package installation
|
||||
run: make ci-test-deb-package-install
|
||||
|
||||
- name: Test RPM package installation
|
||||
run: make ci-test-rpm-package-install
|
||||
|
||||
|
||||
test-mac-artifacts:
|
||||
needs: [ build-artifacts ]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist
|
||||
|
||||
- name: Test darwin run
|
||||
run: make ci-test-mac-run
|
||||
|
||||
|
||||
test-windows-artifacts:
|
||||
needs: [ build-artifacts ]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: actions/download-artifact@master
|
||||
with:
|
||||
name: artifacts
|
||||
path: dist
|
||||
|
||||
- name: Test windows run
|
||||
run: make ci-test-windows-run
|
||||
|
||||
|
||||
release:
|
||||
needs: [ unit-test, build-artifacts, test-linux-artifacts, test-mac-artifacts, test-windows-artifacts ]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.19.x'
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Install tooling
|
||||
run: make ci-install-ci-tools
|
||||
|
||||
- name: Cache go dependencies
|
||||
id: release-cache-go-dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-prod-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-prod-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.release-cache-go-dependencies.outputs.cache-hit != 'true'
|
||||
run: go get ./...
|
||||
|
||||
- name: Docker login
|
||||
run: make ci-docker-login
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Publish GitHub release
|
||||
run: make ci-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker logout
|
||||
run: make ci-docker-logout
|
||||
|
||||
- name: Smoke test published image
|
||||
run: make ci-test-production-image
|
114
.github/workflows/release.yaml
vendored
Normal file
114
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
name: "Release"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: tag the latest commit on main with the given version (prefixed with v)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
environment: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Check if tag already exists
|
||||
# note: this will fail if the tag already exists
|
||||
run: |
|
||||
[[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1)
|
||||
git tag ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Check static analysis results
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: static-analysis
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
|
||||
checkName: "Static analysis"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check unit test results
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: unit
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
|
||||
checkName: "Unit tests"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (linux)
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: acceptance-linux
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
|
||||
checkName: "Acceptance tests (Linux)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (mac)
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: acceptance-mac
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
|
||||
checkName: "Acceptance tests (Mac)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Check acceptance test results (windows)
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: acceptance-windows
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This check name is defined as the github action job name (in .github/workflows/validations.yaml)
|
||||
checkName: "Acceptance tests (Windows)"
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
|
||||
- name: Quality gate
|
||||
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success' || steps.acceptance-windows.outputs.conclusion != 'success'
|
||||
run: |
|
||||
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
|
||||
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
|
||||
echo "Acceptance Test (Windows) Status: ${{ steps.acceptance-windows.outputs.conclusion }}"
|
||||
|
||||
false
|
||||
|
||||
# TODO: uncomment this when we have a release process tested and ready to go
|
||||
# release:
|
||||
# needs: [quality-gate]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# # for tagging
|
||||
# contents: write
|
||||
# steps:
|
||||
#
|
||||
# - uses: actions/checkout@v3
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Bootstrap environment
|
||||
# uses: ./.github/actions/bootstrap
|
||||
#
|
||||
# - name: Tag release
|
||||
# run: |
|
||||
# git tag ${{ github.event.inputs.version }}
|
||||
# git push origin --tags
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
#
|
||||
# - name: Login to Docker Hub
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
#
|
||||
# - name: Build & publish release artifacts
|
||||
# run: make ci-release
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
#
|
||||
# - name: Smoke test published image
|
||||
# run: make ci-test-docker-image
|
135
.github/workflows/validations.yaml
vendored
Normal file
135
.github/workflows/validations.yaml
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
name: "Validations"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
Static-Analysis:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Static analysis"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Run static analysis
|
||||
run: make static-analysis
|
||||
|
||||
|
||||
Unit-Test:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Unit tests"
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- ubuntu-latest
|
||||
# - macos-latest # todo: mac runners are expensive minute-wise
|
||||
# - windows-latest # todo: support windows
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit
|
||||
|
||||
|
||||
Build-Snapshot-Artifacts:
|
||||
name: "Build snapshot artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bootstrap environment
|
||||
uses: ./.github/actions/bootstrap
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
run: make snapshot
|
||||
|
||||
- run: docker images wagoodman/dive
|
||||
|
||||
# todo: compare against known json output in shared volume
|
||||
- name: Test production image
|
||||
run: make ci-test-docker-image
|
||||
|
||||
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||
- name: Upload snapshot artifacts
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
# ... however the cache trick doesn't work on windows :(
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows-artifacts
|
||||
path: snapshot/dive_windows_amd64_v1/dive.exe
|
||||
|
||||
|
||||
Acceptance-Linux:
|
||||
name: "Acceptance tests (Linux)"
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Download snapshot build
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
- name: Test linux run
|
||||
run: make ci-test-linux-run
|
||||
|
||||
- name: Test DEB package installation
|
||||
run: make ci-test-deb-package-install
|
||||
|
||||
- name: Test RPM package installation
|
||||
run: make ci-test-rpm-package-install
|
||||
|
||||
|
||||
Acceptance-Mac:
|
||||
name: "Acceptance tests (Mac)"
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Download snapshot build
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: snapshot
|
||||
key: snapshot-build-${{ github.run_id }}
|
||||
|
||||
- name: Test darwin run
|
||||
run: make ci-test-mac-run
|
||||
|
||||
|
||||
Acceptance-Windows:
|
||||
name: "Acceptance tests (Windows)"
|
||||
needs: [ Build-Snapshot-Artifacts ]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows-artifacts
|
||||
|
||||
- name: Test windows run
|
||||
run: make ci-test-windows-run
|
24
.gitignore
vendored
24
.gitignore
vendored
@ -1,6 +1,25 @@
|
||||
# misc
|
||||
/.image
|
||||
*.log
|
||||
CHANGELOG.md
|
||||
VERSION
|
||||
|
||||
# IDEs
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
# tooling
|
||||
/bin
|
||||
/.tool-versions
|
||||
/.tmp
|
||||
|
||||
# builds
|
||||
/dist
|
||||
/snapshot
|
||||
|
||||
# testing
|
||||
.cover
|
||||
coverage.txt
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
@ -18,8 +37,3 @@
|
||||
/build
|
||||
/_vendor*
|
||||
/vendor
|
||||
/.image
|
||||
*.log
|
||||
/dist
|
||||
.cover
|
||||
coverage.txt
|
||||
|
74
.golangci.yaml
Normal file
74
.golangci.yaml
Normal file
@ -0,0 +1,74 @@
|
||||
# TODO: enable this when we have coverage on docstring comments
|
||||
#issues:
|
||||
# # The list of ids of default excludes to include or disable.
|
||||
# include:
|
||||
# - EXC0002 # disable excluding of issues about comments from golint
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 60
|
||||
# TODO: drop this down over time...
|
||||
lines: 110
|
||||
# Checks the number of statements in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 40
|
||||
statements: 60
|
||||
|
||||
# TODO: use the default linters for now, but include these over time
|
||||
#linters:
|
||||
# # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
# disable-all: true
|
||||
# enable:
|
||||
# - asciicheck
|
||||
# - bodyclose
|
||||
# - depguard
|
||||
# - dogsled
|
||||
# - dupl
|
||||
# - errcheck
|
||||
# - exportloopref
|
||||
# - funlen
|
||||
# - gocognit
|
||||
# - goconst
|
||||
# - gocritic
|
||||
# - gocyclo
|
||||
# - gofmt
|
||||
# - goimports
|
||||
# - goprintffuncname
|
||||
# - gosec
|
||||
# - gosimple
|
||||
# - govet
|
||||
# - ineffassign
|
||||
# - misspell
|
||||
# - nakedret
|
||||
# - nolintlint
|
||||
# - revive
|
||||
# - staticcheck
|
||||
# - stylecheck
|
||||
# - typecheck
|
||||
# - unconvert
|
||||
# - unparam
|
||||
# - unused
|
||||
# - whitespace
|
||||
|
||||
# do not enable...
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits # this is too aggressive
|
||||
# - godot
|
||||
# - godox
|
||||
# - goerr113
|
||||
# - golint # deprecated
|
||||
# - gomnd # this is too aggressive
|
||||
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
|
||||
# - lll # without a way to specify per-line exception cases, this is not usable
|
||||
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
||||
# - nestif
|
||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||
# - scopelint # deprecated
|
||||
# - testpackage
|
||||
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
||||
# - varcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
# - deadcode # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
# - structcheck # deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused.
|
||||
# - rowserrcheck # we're not using sql.Rows at all in the codebase
|
@ -1,5 +1,10 @@
|
||||
release:
|
||||
prerelease: false
|
||||
# If set to auto, will mark the release as not ready for production in case there is an indicator for this in the
|
||||
# tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.
|
||||
prerelease: auto
|
||||
|
||||
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
|
||||
draft: false
|
||||
|
||||
builds:
|
||||
- binary: dive
|
||||
@ -11,6 +16,7 @@ builds:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.buildTime={{.Date}}`.
|
||||
|
||||
brews:
|
||||
@ -18,7 +24,7 @@ brews:
|
||||
owner: wagoodman
|
||||
name: homebrew-dive
|
||||
homepage: "https://github.com/wagoodman/dive/"
|
||||
description: "A tool for exploring each layer in a docker image"
|
||||
description: "A tool for exploring layers in a docker image"
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
@ -30,7 +36,7 @@ nfpms:
|
||||
- license: MIT
|
||||
maintainer: Alex Goodman
|
||||
homepage: https://github.com/wagoodman/dive/
|
||||
description: "A tool for exploring each layer in a docker image"
|
||||
description: "A tool for exploring layers in a docker image"
|
||||
formats:
|
||||
- rpm
|
||||
- deb
|
@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
BOLD=$(tput bold)
|
||||
NORMAL=$(tput sgr0)
|
||||
|
||||
echo "${BOLD}Tagging${NORMAL}"
|
||||
|
||||
#get highest tag number
|
||||
VERSION=`git describe --abbrev=0 --tags`
|
||||
|
||||
#replace . with space so can split into an array
|
||||
VERSION_BITS=(${VERSION//./ })
|
||||
|
||||
#get number parts and increase last one by 1
|
||||
VNUM1=${VERSION_BITS[0]}
|
||||
VNUM2=${VERSION_BITS[1]}
|
||||
VNUM3=${VERSION_BITS[2]}
|
||||
VNUM3=$((VNUM3+1))
|
||||
|
||||
#create new tag
|
||||
NEW_TAG="$VNUM1.$VNUM2.$VNUM3"
|
||||
|
||||
echo "Updating $VERSION to $NEW_TAG"
|
||||
|
||||
#get current hash and see if it already has a tag
|
||||
GIT_COMMIT=`git rev-parse HEAD`
|
||||
NEEDS_TAG=`git describe --contains $GIT_COMMIT`
|
||||
|
||||
#only tag if no tag already (would be better if the git describe command above could have a silent option)
|
||||
if [ -z "$NEEDS_TAG" ]; then
|
||||
echo "Tagged with $NEW_TAG (Ignoring fatal:cannot describe - this means commit is untagged) "
|
||||
git tag $NEW_TAG
|
||||
git push --tags
|
||||
else
|
||||
echo "Already a tag on this commit"
|
||||
fi
|
@ -1,55 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Generate test coverage statistics for Go packages.
|
||||
#
|
||||
# Works around the fact that `go test -coverprofile` currently does not work
|
||||
# with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909
|
||||
#
|
||||
# Usage: script/coverage [--html|--coveralls]
|
||||
#
|
||||
# --html Additionally create HTML report and open it in browser
|
||||
# --coveralls Push coverage statistics to coveralls.io
|
||||
#
|
||||
# Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage
|
||||
|
||||
set -e
|
||||
|
||||
workdir=.cover
|
||||
profile="$workdir/cover.out"
|
||||
mode=count
|
||||
|
||||
generate_cover_data() {
|
||||
rm -rf "$workdir"
|
||||
mkdir "$workdir"
|
||||
|
||||
for pkg in "$@"; do
|
||||
f="$workdir/$(echo $pkg | tr / -).cover"
|
||||
go test -v -covermode="$mode" -coverprofile="$f" "$pkg"
|
||||
done
|
||||
|
||||
echo "mode: $mode" >"$profile"
|
||||
grep -h -v "^mode:" "$workdir"/*.cover >>"$profile"
|
||||
}
|
||||
|
||||
show_cover_report() {
|
||||
go tool cover -${1}="$profile"
|
||||
}
|
||||
|
||||
push_to_coveralls() {
|
||||
echo "Pushing coverage statistics to coveralls.io"
|
||||
goveralls -coverprofile="$profile"
|
||||
}
|
||||
|
||||
generate_cover_data $(go list ./...)
|
||||
case "$1" in
|
||||
"")
|
||||
show_cover_report func
|
||||
;;
|
||||
--html)
|
||||
show_cover_report html
|
||||
;;
|
||||
--coveralls)
|
||||
push_to_coveralls
|
||||
;;
|
||||
*)
|
||||
echo >&2 "error: invalid option: $1"; exit 1 ;;
|
||||
esac
|
324
Makefile
324
Makefile
@ -1,65 +1,201 @@
|
||||
BIN = dive
|
||||
BUILD_DIR = ./dist/dive_linux_amd64
|
||||
BUILD_PATH = $(BUILD_DIR)/$(BIN)
|
||||
TEMP_DIR = ./.tmp
|
||||
PWD := ${CURDIR}
|
||||
PRODUCTION_REGISTRY = docker.io
|
||||
SHELL = /bin/bash -o pipefail
|
||||
TEST_IMAGE = busybox:latest
|
||||
|
||||
all: gofmt clean build
|
||||
# Tool versions #################################
|
||||
GOLANG_CI_VERSION = v1.52.2
|
||||
GOBOUNCER_VERSION = v0.4.0
|
||||
GORELEASER_VERSION = v1.19.1
|
||||
GOSIMPORTS_VERSION = v0.3.8
|
||||
CHRONICLE_VERSION = v0.6.0
|
||||
GLOW_VERSION = v1.5.0
|
||||
DOCKER_CLI_VERSION = 23.0.6
|
||||
|
||||
## For CI
|
||||
# Command templates #################################
|
||||
LINT_CMD = $(TEMP_DIR)/golangci-lint run --tests=false --timeout=2m --config .golangci.yaml
|
||||
GOIMPORTS_CMD = $(TEMP_DIR)/gosimports -local github.com/wagoodman
|
||||
RELEASE_CMD = DOCKER_CLI_VERSION=$(DOCKER_CLI_VERSION) $(TEMP_DIR)/goreleaser release --clean
|
||||
SNAPSHOT_CMD = $(RELEASE_CMD) --skip-publish --snapshot --skip-sign
|
||||
CHRONICLE_CMD = $(TEMP_DIR)/chronicle
|
||||
GLOW_CMD = $(TEMP_DIR)/glow
|
||||
|
||||
ci-unit-test:
|
||||
go test -cover -v -race ./...
|
||||
# Formatting variables #################################
|
||||
BOLD := $(shell tput -T linux bold)
|
||||
PURPLE := $(shell tput -T linux setaf 5)
|
||||
GREEN := $(shell tput -T linux setaf 2)
|
||||
CYAN := $(shell tput -T linux setaf 6)
|
||||
RED := $(shell tput -T linux setaf 1)
|
||||
RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
|
||||
ci-static-analysis:
|
||||
grep -R 'const allowTestDataCapture = false' runtime/ui/viewmodel
|
||||
go vet ./...
|
||||
gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
|
||||
golangci-lint run
|
||||
# Test variables #################################
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 55
|
||||
|
||||
ci-install-go-tools:
|
||||
mkdir -p ${HOME}/.local/bin
|
||||
curl -sfL https://goreleaser.com/static/run > ${HOME}/.local/bin/goreleaser
|
||||
chmod +x ${HOME}/.local/bin/goreleaser
|
||||
## Build variables #################################
|
||||
DIST_DIR = dist
|
||||
SNAPSHOT_DIR = snapshot
|
||||
OS=$(shell uname | tr '[:upper:]' '[:lower:]')
|
||||
SNAPSHOT_BIN=$(realpath $(shell pwd)/$(SNAPSHOT_DIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
|
||||
CHANGELOG := CHANGELOG.md
|
||||
VERSION=$(shell git describe --dirty --always --tags)
|
||||
|
||||
ci-install-ci-tools:
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ${HOME}/.local/bin/
|
||||
ifeq "$(strip $(VERSION))" ""
|
||||
override VERSION = $(shell git describe --always --tags --dirty)
|
||||
endif
|
||||
|
||||
ci-docker-login:
|
||||
echo '${DOCKER_PASSWORD}' | docker login -u '${DOCKER_USERNAME}' --password-stdin '${PRODUCTION_REGISTRY}'
|
||||
## Variable assertions
|
||||
|
||||
ci-docker-logout:
|
||||
docker logout '${PRODUCTION_REGISTRY}'
|
||||
ifndef TEMP_DIR
|
||||
$(error TEMP_DIR is not set)
|
||||
endif
|
||||
|
||||
ci-publish-release:
|
||||
goreleaser --clean
|
||||
ifndef DIST_DIR
|
||||
$(error DIST_DIR is not set)
|
||||
endif
|
||||
|
||||
ci-build-snapshot-packages:
|
||||
goreleaser \
|
||||
--snapshot \
|
||||
--skip-publish \
|
||||
--clean
|
||||
ifndef SNAPSHOT_DIR
|
||||
$(error SNAPSHOT_DIR is not set)
|
||||
endif
|
||||
|
||||
ci-release:
|
||||
goreleaser release --clean
|
||||
define title
|
||||
@printf '$(TITLE)$(1)$(RESET)\n'
|
||||
endef
|
||||
|
||||
|
||||
.PHONY: all
|
||||
all: clean static-analysis test ## Run all static analysis and tests
|
||||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
||||
|
||||
.PHONY: test
|
||||
test: unit ## Run all tests (currently unit and cli tests)
|
||||
|
||||
$(TEMP_DIR):
|
||||
mkdir -p $(TEMP_DIR)
|
||||
|
||||
|
||||
## Bootstrapping targets #################################
|
||||
|
||||
.PHONY: bootstrap-tools
|
||||
bootstrap-tools: $(TEMP_DIR)
|
||||
$(call title,Bootstrapping tools)
|
||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANG_CI_VERSION)
|
||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(GOBOUNCER_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
|
||||
GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION)
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
$(call title,Bootstrapping go dependencies)
|
||||
go mod download
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
|
||||
|
||||
## Development targets ###################################
|
||||
|
||||
#run: build
|
||||
# $(BUILD_PATH) build -t dive-example:latest -f .data/Dockerfile.example .
|
||||
#
|
||||
#run-large: build
|
||||
# $(BUILD_PATH) amir20/clashleaders:latest
|
||||
#
|
||||
#run-podman: build
|
||||
# podman build -t dive-example:latest -f .data/Dockerfile.example .
|
||||
# $(BUILD_PATH) localhost/dive-example:latest --engine podman
|
||||
#
|
||||
#run-podman-large: build
|
||||
# $(BUILD_PATH) docker.io/amir20/clashleaders:latest --engine podman
|
||||
#
|
||||
#run-ci: build
|
||||
# CI=true $(BUILD_PATH) dive-example:latest --ci-config .data/.dive-ci
|
||||
#
|
||||
#dev:
|
||||
# docker run -ti --rm -v $(PWD):/app -w /app -v dive-pkg:/go/pkg/ golang:1.13 bash
|
||||
#
|
||||
#build: gofmt
|
||||
# go build -o $(BUILD_PATH)
|
||||
|
||||
.PHONY: generate-test-data
|
||||
generate-test-data:
|
||||
docker build -t dive-test:latest -f .data/Dockerfile.test-image . && docker image save -o .data/test-docker-image.tar dive-test:latest && echo 'Exported test data!'
|
||||
|
||||
|
||||
## Static analysis targets #################################
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: lint check-go-mod-tidy check-licenses
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
$(call title,Running linters)
|
||||
# ensure there are no go fmt differences
|
||||
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
|
||||
@test -z "$(shell gofmt -l -s .)"
|
||||
|
||||
# run all golangci-lint rules
|
||||
$(LINT_CMD)
|
||||
@[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
|
||||
|
||||
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
|
||||
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
|
||||
@bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
|
||||
|
||||
.PHONY: format
|
||||
format: ## Auto-format all source code
|
||||
$(call title,Running formatters)
|
||||
gofmt -w -s .
|
||||
$(GOIMPORTS_CMD) -w .
|
||||
go mod tidy
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: format ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
$(LINT_CMD) --fix
|
||||
|
||||
.PHONY: check-licenses
|
||||
check-licenses:
|
||||
$(TEMP_DIR)/bouncer check ./...
|
||||
|
||||
check-go-mod-tidy:
|
||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||
|
||||
|
||||
## Testing targets #################################
|
||||
|
||||
.PHONY: unit
|
||||
unit: $(TEMP_DIR) ## Run unit tests (with coverage)
|
||||
$(call title,Running unit tests)
|
||||
go test -race -coverprofile $(TEMP_DIR)/unit-coverage-details.txt ./...
|
||||
@.github/scripts/coverage.py $(COVERAGE_THRESHOLD) $(TEMP_DIR)/unit-coverage-details.txt
|
||||
|
||||
|
||||
## Acceptance testing targets (CI only) #################################
|
||||
|
||||
# todo: add --pull=never when supported by host box
|
||||
ci-test-production-image:
|
||||
.PHONY: ci-test-docker-image
|
||||
ci-test-docker-image:
|
||||
docker run \
|
||||
--rm \
|
||||
-t \
|
||||
-v //var/run/docker.sock://var/run/docker.sock \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
'${PRODUCTION_REGISTRY}/wagoodman/dive:latest' \
|
||||
'${TEST_IMAGE}' \
|
||||
--ci
|
||||
|
||||
.PHONY: ci-test-deb-package-install
|
||||
ci-test-deb-package-install:
|
||||
docker run \
|
||||
-v //var/run/docker.sock://var/run/docker.sock \
|
||||
-v /${PWD}://src \
|
||||
-w //src \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /${PWD}:/src \
|
||||
-w /src \
|
||||
ubuntu:latest \
|
||||
/bin/bash -x -c "\
|
||||
apt update && \
|
||||
@ -68,78 +204,120 @@ ci-test-deb-package-install:
|
||||
tar -vxzf - docker/docker --strip-component=1 && \
|
||||
mv docker /usr/local/bin/ &&\
|
||||
docker version && \
|
||||
apt install ./dist/dive_*_linux_amd64.deb -y && \
|
||||
apt install ./snapshot/dive_*_linux_amd64.deb -y && \
|
||||
dive --version && \
|
||||
dive '${TEST_IMAGE}' --ci \
|
||||
"
|
||||
|
||||
.PHONY: ci-test-deb-package-install
|
||||
ci-test-rpm-package-install:
|
||||
docker run \
|
||||
-v //var/run/docker.sock://var/run/docker.sock \
|
||||
-v /${PWD}://src \
|
||||
-w //src \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /${PWD}:/src \
|
||||
-w /src \
|
||||
fedora:latest \
|
||||
/bin/bash -x -c "\
|
||||
curl -L 'https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_CLI_VERSION}.tgz' | \
|
||||
tar -vxzf - docker/docker --strip-component=1 && \
|
||||
mv docker /usr/local/bin/ &&\
|
||||
docker version && \
|
||||
dnf install ./dist/dive_*_linux_amd64.rpm -y && \
|
||||
dnf install ./snapshot/dive_*_linux_amd64.rpm -y && \
|
||||
dive --version && \
|
||||
dive '${TEST_IMAGE}' --ci \
|
||||
"
|
||||
|
||||
.PHONY: ci-test-linux-run
|
||||
ci-test-linux-run:
|
||||
ls -la ./dist
|
||||
ls -la ./dist/dive_linux_amd64_v1
|
||||
chmod 755 ./dist/dive_linux_amd64_v1/dive && \
|
||||
./dist/dive_linux_amd64_v1/dive '${TEST_IMAGE}' --ci && \
|
||||
./dist/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
|
||||
ls -la $(SNAPSHOT_DIR)
|
||||
ls -la $(SNAPSHOT_DIR)/dive_linux_amd64_v1
|
||||
chmod 755 $(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive && \
|
||||
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive '${TEST_IMAGE}' --ci && \
|
||||
$(SNAPSHOT_DIR)/dive_linux_amd64_v1/dive --source docker-archive .data/test-kaniko-image.tar --ci --ci-config .data/.dive-ci
|
||||
|
||||
# we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI.
|
||||
.PHONY: ci-test-mac-run
|
||||
ci-test-mac-run:
|
||||
chmod 755 ./dist/dive_darwin_amd64_v1/dive && \
|
||||
./dist/dive_darwin_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
|
||||
chmod 755 $(SNAPSHOT_DIR)/dive_darwin_amd64_v1/dive && \
|
||||
$(SNAPSHOT_DIR)/dive_darwin_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
|
||||
|
||||
# we're not attempting to test docker, just our ability to run on these systems. This avoids setting up docker in CI.
|
||||
.PHONY: ci-test-windows-run
|
||||
ci-test-windows-run:
|
||||
./dist/dive_windows_amd64_v1/dive --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
|
||||
dive.exe --source docker-archive .data/test-docker-image.tar --ci --ci-config .data/.dive-ci
|
||||
|
||||
|
||||
## Build-related targets #################################
|
||||
|
||||
## For development
|
||||
.PHONY: build
|
||||
build: $(SNAPSHOT_DIR) ## Build release snapshot binaries and packages
|
||||
|
||||
run: build
|
||||
$(BUILD_PATH) build -t dive-example:latest -f .data/Dockerfile.example .
|
||||
$(SNAPSHOT_DIR): ## Build snapshot release binaries and packages
|
||||
$(call title,Building snapshot artifacts)
|
||||
|
||||
run-large: build
|
||||
$(BUILD_PATH) amir20/clashleaders:latest
|
||||
@# create a config with the dist dir overridden
|
||||
@echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml
|
||||
@cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
run-podman: build
|
||||
podman build -t dive-example:latest -f .data/Dockerfile.example .
|
||||
$(BUILD_PATH) localhost/dive-example:latest --engine podman
|
||||
@# build release snapshots
|
||||
@bash -c "\
|
||||
VERSION=$(VERSION:v%=%) \
|
||||
$(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml \
|
||||
"
|
||||
|
||||
run-podman-large: build
|
||||
$(BUILD_PATH) docker.io/amir20/clashleaders:latest --engine podman
|
||||
.PHONY: cli
|
||||
cli: $(SNAPSHOT_DIR) ## Run CLI tests
|
||||
chmod 755 "$(SNAPSHOT_BIN)"
|
||||
$(SNAPSHOT_BIN) version
|
||||
go test -count=1 -timeout=15m -v ./test/cli
|
||||
|
||||
run-ci: build
|
||||
CI=true $(BUILD_PATH) dive-example:latest --ci-config .data/.dive-ci
|
||||
.PHONY: changelog
|
||||
changelog: clean-changelog ## Generate and show the changelog for the current unreleased version
|
||||
$(CHRONICLE_CMD) -vvv -n --version-file VERSION > $(CHANGELOG)
|
||||
@$(GLOW_CMD) $(CHANGELOG)
|
||||
|
||||
build: gofmt
|
||||
go build -o $(BUILD_PATH)
|
||||
$(CHANGELOG):
|
||||
$(CHRONICLE_CMD) -vvv > $(CHANGELOG)
|
||||
|
||||
generate-test-data:
|
||||
docker build -t dive-test:latest -f .data/Dockerfile.test-image . && docker image save -o .data/test-docker-image.tar dive-test:latest && echo 'Exported test data!'
|
||||
.PHONY: release
|
||||
release: ## Cut a new release
|
||||
@.github/scripts/trigger-release.sh
|
||||
|
||||
test: gofmt
|
||||
./.scripts/test-coverage.sh
|
||||
.PHONY: release
|
||||
ci-release: ci-check clean-dist $(CHANGELOG)
|
||||
$(call title,Publishing release artifacts)
|
||||
|
||||
dev:
|
||||
docker run -ti --rm -v $(PWD):/app -w /app -v dive-pkg:/go/pkg/ golang:1.13 bash
|
||||
# create a config with the dist dir overridden
|
||||
echo "dist: $(DIST_DIR)" > $(TEMP_DIR)/goreleaser.yaml
|
||||
cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
go clean
|
||||
bash -c "$(RELEASE_CMD) --release-notes <(cat CHANGELOG.md) --config $(TEMP_DIR)/goreleaser.yaml"
|
||||
|
||||
.PHONY: ci-check
|
||||
ci-check:
|
||||
@.github/scripts/ci-check.sh
|
||||
|
||||
|
||||
## Cleanup targets #################################
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-dist clean-snapshot ## Remove previous builds, result reports, and test cache
|
||||
|
||||
.PHONY: clean-snapshot
|
||||
clean-snapshot:
|
||||
rm -rf $(SNAPSHOT_DIR) $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-dist
|
||||
clean-dist: clean-changelog
|
||||
rm -rf $(DIST_DIR) $(TEMP_DIR)/goreleaser.yaml
|
||||
|
||||
.PHONY: clean-changelog
|
||||
clean-changelog:
|
||||
rm -f $(CHANGELOG) VERSION
|
||||
|
||||
|
||||
## Halp! #################################
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
||||
|
||||
gofmt:
|
||||
go fmt -x ./...
|
||||
|
@ -1,6 +1,8 @@
|
||||
# dive
|
||||
[](https://github.com/wagoodman/dive/releases/latest)
|
||||
[](https://github.com/wagoodman/dive/actions/workflows/validations.yaml)
|
||||
[](https://goreportcard.com/report/github.com/wagoodman/dive)
|
||||
[](https://circleci.com/gh/wagoodman/dive)
|
||||
[](https://github.com/wagoodman/dive/blob/main/LICENSE)
|
||||
[](https://www.paypal.me/wagoodman)
|
||||
|
||||
**A tool for exploring a docker image, layer contents, and discovering ways to shrink the size of your Docker/OCI image.**
|
||||
|
@ -2,19 +2,19 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/runtime"
|
||||
)
|
||||
|
||||
// doAnalyzeCmd takes a docker image tag, digest, or id and displays the
|
||||
// image analysis to the screen
|
||||
func doAnalyzeCmd(cmd *cobra.Command, args []string) {
|
||||
|
||||
if len(args) == 0 {
|
||||
printVersionFlag, err := cmd.PersistentFlags().GetBool("version")
|
||||
if err == nil && printVersionFlag {
|
||||
|
@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/runtime"
|
||||
)
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func configureCi() (bool, *viper.Viper, error) {
|
||||
|
||||
isCiFromEnv, _ := strconv.ParseBool(os.Getenv("CI"))
|
||||
isCi = isCi || isCiFromEnv
|
||||
|
||||
|
@ -7,13 +7,13 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
@ -2,6 +2,7 @@ package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -52,8 +53,8 @@ func (cmp *Comparer) GetPathErrors(key TreeIndexKey) ([]PathError, error) {
|
||||
}
|
||||
|
||||
func (cmp *Comparer) GetTree(key TreeIndexKey) (*FileTree, error) {
|
||||
//func (cmp *Comparer) GetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, []PathError, error) {
|
||||
//key := TreeIndexKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
|
||||
// func (cmp *Comparer) GetTree(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) (*FileTree, []PathError, error) {
|
||||
// key := TreeIndexKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
|
||||
|
||||
if value, exists := cmp.trees[key]; exists {
|
||||
return value, nil
|
||||
@ -114,7 +115,6 @@ func (cmp *Comparer) NaturalIndexes() <-chan TreeIndexKey {
|
||||
}
|
||||
}()
|
||||
return indexes
|
||||
|
||||
}
|
||||
|
||||
// case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)
|
||||
@ -146,7 +146,6 @@ func (cmp *Comparer) AggregatedIndexes() <-chan TreeIndexKey {
|
||||
}
|
||||
}()
|
||||
return indexes
|
||||
|
||||
}
|
||||
|
||||
func (cmp *Comparer) BuildCache() (errors []error) {
|
||||
|
@ -85,7 +85,6 @@ func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
sizeBytes = node.Data.FileInfo.Size
|
||||
}
|
||||
|
@ -2,10 +2,11 @@ package filetree
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"github.com/cespare/xxhash"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/cespare/xxhash"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// FileInfo contains tar metadata for a specific FileNode
|
||||
@ -56,7 +57,6 @@ func NewFileInfo(realPath, path string, info os.FileInfo) FileInfo {
|
||||
if err != nil {
|
||||
logrus.Panic("unable to read link:", realPath, err)
|
||||
}
|
||||
|
||||
} else if info.IsDir() {
|
||||
fileType = tar.TypeDir
|
||||
} else {
|
||||
|
@ -6,11 +6,10 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/fatih/color"
|
||||
"github.com/phayes/permbits"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -277,7 +277,6 @@ func (tree *FileTree) AddPath(filepath string, data FileInfo) (*FileNode, []*Fil
|
||||
if idx == len(nodeNames)-1 {
|
||||
node.Data.FileInfo = data
|
||||
}
|
||||
|
||||
}
|
||||
return node, addedNodes, nil
|
||||
}
|
||||
|
@ -2,11 +2,12 @@ package dive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"github.com/wagoodman/dive/dive/image/podman"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -56,7 +57,6 @@ func DeriveImageSource(image string) (ImageSource, string) {
|
||||
return SourceDockerArchive, imageSource
|
||||
case "docker-tar":
|
||||
return SourceDockerArchive, imageSource
|
||||
|
||||
}
|
||||
return SourceUnknown, ""
|
||||
}
|
||||
|
@ -2,8 +2,9 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"os"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
type archiveResolver struct{}
|
||||
|
@ -2,9 +2,10 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
||||
// runDockerCmd runs a given Docker command in the current tty
|
||||
|
@ -2,6 +2,7 @@ package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -2,7 +2,6 @@ package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -11,6 +10,8 @@ import (
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
type engineResolver struct{}
|
||||
@ -20,7 +21,6 @@ func NewResolverFromEngine() *engineResolver {
|
||||
}
|
||||
|
||||
func (r *engineResolver) Fetch(id string) (*image.Image, error) {
|
||||
|
||||
reader, err := r.fetchArchive(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -46,7 +46,6 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
|
||||
|
||||
// some layer tars can be relative layer symlinks to other layer tars
|
||||
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
|
||||
|
||||
if strings.HasSuffix(name, ".tar") {
|
||||
currentLayer++
|
||||
layerReader := tar.NewReader(tarReader)
|
||||
@ -57,7 +56,6 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
|
||||
|
||||
// add the layer to the image
|
||||
img.layerMap[tree.Name] = tree
|
||||
|
||||
} else if strings.HasSuffix(name, ".tar.gz") || strings.HasSuffix(name, "tgz") {
|
||||
currentLayer++
|
||||
|
||||
@ -78,7 +76,6 @@ func NewImageArchive(tarFile io.ReadCloser) (*ImageArchive, error) {
|
||||
|
||||
// add the layer to the image
|
||||
img.layerMap[tree.Name] = tree
|
||||
|
||||
} else if strings.HasSuffix(name, ".json") || strings.HasPrefix(name, "sha256:") {
|
||||
fileBuffer, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
@ -206,5 +203,4 @@ func (img *ImageArchive) ToImage() (*image.Image, error) {
|
||||
Trees: trees,
|
||||
Layers: layers,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"strings"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
// Layer represents a Docker image layer and metadata
|
||||
|
@ -2,6 +2,7 @@ package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
func TestLoadArchive(tarPath string) (*ImageArchive, error) {
|
||||
|
@ -10,7 +10,6 @@ type Image struct {
|
||||
}
|
||||
|
||||
func (img *Image) Analyze() (*AnalysisResult, error) {
|
||||
|
||||
efficiency, inefficiencies := filetree.Efficiency(img.Trees)
|
||||
var sizeBytes, userSizeBytes uint64
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
)
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package podman
|
||||
|
@ -1,13 +1,15 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package podman
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
||||
// runPodmanCmd runs a given Podman command in the current tty
|
||||
|
@ -1,3 +1,4 @@
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package podman
|
||||
@ -5,6 +6,7 @@ package podman
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
//go:build !linux && !darwin
|
||||
// +build !linux,!darwin
|
||||
|
||||
package podman
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
|
@ -2,16 +2,16 @@ package ci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
||||
type CiEvaluator struct {
|
||||
@ -67,7 +67,6 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
||||
message: "test",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if !canEvaluate {
|
||||
@ -111,7 +110,6 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
||||
status: status,
|
||||
message: message,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ci.Tally.Total = len(ci.Results)
|
||||
@ -174,7 +172,6 @@ func (ci *CiEvaluator) Report() string {
|
||||
|
||||
if ci.Misconfigured {
|
||||
fmt.Fprintln(&sb, aurora.Red("CI Misconfigured"))
|
||||
|
||||
} else {
|
||||
summary := fmt.Sprintf("Result:%s [Total:%d] [Passed:%d] [Failed:%d] [Warn:%d] [Skipped:%d]", status, ci.Tally.Total, ci.Tally.Pass, ci.Tally.Fail, ci.Tally.Warn, ci.Tally.Skip)
|
||||
if ci.Pass {
|
||||
|
@ -1,11 +1,12 @@
|
||||
package ci
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
)
|
||||
|
||||
func Test_Evaluator(t *testing.T) {
|
||||
|
@ -2,13 +2,13 @@ package ci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -2,6 +2,7 @@ package export
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
diveImage "github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package export
|
||||
|
||||
import (
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"testing"
|
||||
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
)
|
||||
|
||||
func Test_Export(t *testing.T) {
|
||||
|
@ -2,6 +2,7 @@ package runtime
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
)
|
||||
|
||||
|
@ -2,9 +2,13 @@ package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
@ -12,8 +16,6 @@ import (
|
||||
"github.com/wagoodman/dive/runtime/export"
|
||||
"github.com/wagoodman/dive/runtime/ui"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func run(enableUi bool, options Options, imageResolver image.Resolver, events eventChannel, filesystem afero.Fs) {
|
||||
@ -84,7 +86,6 @@ func run(enableUi bool, options Options, imageResolver image.Resolver, events ev
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
} else {
|
||||
events.message(utils.TitleFormat("Building cache..."))
|
||||
treeStack := filetree.NewComparer(analysis.RefTrees)
|
||||
|
@ -2,14 +2,16 @@ package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type defaultResolver struct{}
|
||||
|
@ -2,16 +2,15 @@ package ui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"github.com/wagoodman/dive/runtime/ui/layout"
|
||||
"github.com/wagoodman/dive/runtime/ui/layout/compound"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
@ -51,7 +50,7 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
|
||||
lm.Add(controller.views.Debug, layout.LocationColumn)
|
||||
}
|
||||
gui.Cursor = false
|
||||
//g.Mouse = true
|
||||
// g.Mouse = true
|
||||
gui.SetManagerFunc(lm.Layout)
|
||||
|
||||
// var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook)
|
||||
@ -105,7 +104,6 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return appSingleton, err
|
||||
@ -129,22 +127,12 @@ func newApp(gui *gocui.Gui, imageName string, analysis *image.AnalysisResult, ca
|
||||
|
||||
// quit is the gocui callback invoked when the user hits Ctrl+C
|
||||
func (a *app) quit() error {
|
||||
|
||||
// profileObj.Stop()
|
||||
// onExit()
|
||||
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
|
||||
// handle ctrl+z
|
||||
func handle_ctrl_z(g *gocui.Gui, v *gocui.View) error {
|
||||
gocui.Suspend()
|
||||
if err := syscall.Kill(syscall.Getpid(), syscall.SIGSTOP); err != nil {
|
||||
return err
|
||||
}
|
||||
return gocui.Resume()
|
||||
}
|
||||
|
||||
// Run is the UI entrypoint.
|
||||
func Run(imageName string, analysis *image.AnalysisResult, treeStack filetree.Comparer) error {
|
||||
var err error
|
||||
|
@ -1,13 +1,15 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/runtime/ui/view"
|
||||
"github.com/wagoodman/dive/runtime/ui/viewmodel"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
@ -141,6 +143,7 @@ func (c *Controller) Render() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (c *Controller) NextPane() (err error) {
|
||||
v := c.gui.CurrentView()
|
||||
if v == nil {
|
||||
@ -165,6 +168,7 @@ func (c *Controller) NextPane() (err error) {
|
||||
return c.UpdateAndRender()
|
||||
}
|
||||
|
||||
//nolint:dupl
|
||||
func (c *Controller) PrevPane() (err error) {
|
||||
v := c.gui.CurrentView()
|
||||
if v == nil {
|
||||
|
@ -2,23 +2,24 @@ package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
//selectedLeftBracketStr = " "
|
||||
//selectedRightBracketStr = " "
|
||||
//selectedFillStr = " "
|
||||
// selectedLeftBracketStr = " "
|
||||
// selectedRightBracketStr = " "
|
||||
// selectedFillStr = " "
|
||||
//
|
||||
//leftBracketStr = "▏"
|
||||
//rightBracketStr = "▕"
|
||||
//fillStr = "─"
|
||||
|
||||
//selectedLeftBracketStr = " "
|
||||
//selectedRightBracketStr = " "
|
||||
//selectedFillStr = "━"
|
||||
// selectedLeftBracketStr = " "
|
||||
// selectedRightBracketStr = " "
|
||||
// selectedFillStr = "━"
|
||||
//
|
||||
//leftBracketStr = "▏"
|
||||
//rightBracketStr = "▕"
|
||||
@ -33,7 +34,7 @@ const (
|
||||
fillStr = "─"
|
||||
|
||||
selectStr = " ● "
|
||||
//selectStr = " "
|
||||
// selectStr = " "
|
||||
)
|
||||
|
||||
var (
|
||||
@ -74,8 +75,8 @@ func RenderHeader(title string, width int, selected bool) string {
|
||||
repeatCount = 0
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s%s\n", selectedLeftBracketStr, body, selectedRightBracketStr, strings.Repeat(selectedFillStr, repeatCount))
|
||||
//return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2)))
|
||||
//return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2))
|
||||
// return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), Selected(strings.Repeat(selectedFillStr, width-bodyLen-2)))
|
||||
// return fmt.Sprintf("%s%s%s%s\n", Selected(selectedLeftBracketStr), body, Selected(selectedRightBracketStr), strings.Repeat(selectedFillStr, width-bodyLen-2))
|
||||
}
|
||||
body := Header(fmt.Sprintf(" %s ", title))
|
||||
bodyLen := len(vtclean.Clean(body, false))
|
||||
|
13
runtime/ui/job_control_other.go
Normal file
13
runtime/ui/job_control_other.go
Normal file
@ -0,0 +1,13 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
// handle ctrl+z not supported on windows
|
||||
func handle_ctrl_z(_ *gocui.Gui, _ *gocui.View) error {
|
||||
return nil
|
||||
}
|
19
runtime/ui/job_control_unix.go
Normal file
19
runtime/ui/job_control_unix.go
Normal file
@ -0,0 +1,19 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
// handle ctrl+z
|
||||
func handle_ctrl_z(g *gocui.Gui, v *gocui.View) error {
|
||||
gocui.Suspend()
|
||||
if err := syscall.Kill(syscall.Getpid(), syscall.SIGSTOP); err != nil {
|
||||
return err
|
||||
}
|
||||
return gocui.Resume()
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/awesome-gocui/keybinding"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
)
|
||||
|
||||
|
@ -3,6 +3,7 @@ package compound
|
||||
import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/runtime/ui/view"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
@ -51,7 +51,6 @@ func (lm *Manager) planAndLayoutHeaders(g *gocui.Gui, area Area) (Area, error) {
|
||||
|
||||
// restrict the available screen real estate
|
||||
area.minY += height
|
||||
|
||||
}
|
||||
}
|
||||
return area, nil
|
||||
@ -141,7 +140,6 @@ func (lm *Manager) planAndLayoutColumns(g *gocui.Gui, area Area) (Area, error) {
|
||||
|
||||
// move left to right, scratching off real estate as it is taken
|
||||
area.minX += width
|
||||
|
||||
}
|
||||
}
|
||||
return area, nil
|
||||
|
@ -1,8 +1,9 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"testing"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
type testElement struct {
|
||||
|
@ -2,6 +2,7 @@ package view
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
@ -406,7 +407,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
|
||||
logrus.Tracef("view.Layout(minX: %d, minY: %d, maxX: %d, maxY: %d) %s", minX, minY, maxX, maxY, v.Name())
|
||||
attributeRowSize := 0
|
||||
|
||||
// make the layout responsive to the available realestate. Make more room for the main content by hiding auxillary
|
||||
// make the layout responsive to the available realestate. Make more room for the main content by hiding auxiliary
|
||||
// content when there is not enough room
|
||||
if maxX-minX < 60 {
|
||||
v.vm.ConstrainLayout()
|
||||
@ -436,7 +437,7 @@ func (v *FileTree) Layout(g *gocui.Gui, minX, minY, maxX, maxY int) error {
|
||||
}
|
||||
|
||||
func (v *FileTree) RequestedSize(available int) *int {
|
||||
//var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))
|
||||
//return &requestedWidth
|
||||
// var requestedWidth = int(float64(available) * (1.0 - v.requestedWidthRatio))
|
||||
// return &requestedWidth
|
||||
return nil
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
)
|
||||
|
@ -2,14 +2,16 @@ package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ImageDetails struct {
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
@ -319,7 +320,6 @@ func (v *Layer) Render() error {
|
||||
// update contents
|
||||
v.body.Clear()
|
||||
for idx, layer := range v.vm.Layers {
|
||||
|
||||
var layerStr string
|
||||
if v.constrainedRealEstate {
|
||||
layerStr = fmt.Sprintf("%-4d", layer.Index)
|
||||
@ -339,7 +339,6 @@ func (v *Layer) Render() error {
|
||||
logrus.Debug("unable to write to buffer: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
@ -2,12 +2,14 @@ package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LayerDetails struct {
|
||||
|
@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"github.com/wagoodman/dive/runtime/ui/key"
|
||||
"github.com/wagoodman/dive/utils"
|
||||
|
||||
"github.com/awesome-gocui/gocui"
|
||||
)
|
||||
|
||||
// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel
|
||||
|
@ -2,6 +2,7 @@ package view
|
||||
|
||||
import (
|
||||
"github.com/awesome-gocui/gocui"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image"
|
||||
)
|
||||
|
@ -3,14 +3,15 @@ package viewmodel
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lunixbochs/vtclean"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
)
|
||||
|
||||
// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
|
||||
|
@ -2,8 +2,6 @@ package viewmodel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@ -11,7 +9,10 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
|
||||
"github.com/wagoodman/dive/dive/filetree"
|
||||
"github.com/wagoodman/dive/dive/image/docker"
|
||||
"github.com/wagoodman/dive/runtime/ui/format"
|
||||
)
|
||||
|
||||
const allowTestDataCapture = false
|
||||
|
@ -1,8 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/logrusorgru/aurora"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
func TitleFormat(s string) string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user