mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 11:14:04 +00:00
chore: even more linting, test fixes (#24)
* chore(lint): funcorder * chore(lint): yamlfmt, ignored broken test yaml files * chore(tests): tests do not output garbage, add coverage * chore(lint): fix editorconfig violations * chore(lint): move from eclint to editorconfig-checker * chore(lint): add pre-commit, run and fix * chore(ci): we use renovate to manage updates
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
# Ignore patterns for eclint
|
||||
|
||||
# Build artifacts and binaries
|
||||
gh-action-readme
|
||||
dist/
|
||||
coverage.out
|
||||
*.exe
|
||||
*.bin
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Git directory
|
||||
.git/
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
|
||||
# Cache directories
|
||||
.cache/
|
||||
.npm/
|
||||
@@ -31,3 +31,6 @@ max_line_length = 200
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
[{go.sum,go.mod}]
|
||||
max_line_length = 300
|
||||
|
||||
36
.editorconfig-checker.json
Normal file
36
.editorconfig-checker.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Verbose": false,
|
||||
"Debug": false,
|
||||
"IgnoreDefaults": false,
|
||||
"SpacesAfterTabs": false,
|
||||
"NoColor": false,
|
||||
"Exclude": [
|
||||
"\\.git",
|
||||
"node_modules",
|
||||
"coverage\\.out",
|
||||
"coverage\\.html",
|
||||
"\\.DS_Store",
|
||||
"Thumbs\\.db",
|
||||
"\\.vscode",
|
||||
"\\.idea",
|
||||
"\\.cache",
|
||||
"\\.npm",
|
||||
"\\.tmp$",
|
||||
"\\.temp$",
|
||||
"\\.log$",
|
||||
"\\.exe$",
|
||||
"\\.bin$",
|
||||
"\\.dll$",
|
||||
"\\.so$",
|
||||
"\\.dylib$",
|
||||
"gh-action-readme$",
|
||||
"dist/",
|
||||
"testutil\\.test$",
|
||||
"test_.*",
|
||||
"testdata/yaml-fixtures/validation/invalid-yaml\\.yml$",
|
||||
"testdata/yaml-fixtures/validation/missing-.*",
|
||||
"testdata/yaml-fixtures/actions/invalid/.*",
|
||||
"testdata/yaml-fixtures/invalid-action\\.yml$",
|
||||
"testdata/yaml-fixtures/.*-template\\.yml$"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# Repository-level configuration for gh-action-readme
|
||||
organization: "ivuorinen"
|
||||
repository: "gh-action-readme"
|
||||
|
||||
66
.github/dependabot.yml
vendored
66
.github/dependabot.yml
vendored
@@ -1,66 +0,0 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
# Go modules
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- "ivuorinen"
|
||||
assignees:
|
||||
- "ivuorinen"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "security"
|
||||
# Group security updates
|
||||
groups:
|
||||
security-updates:
|
||||
patterns:
|
||||
- "*"
|
||||
update-types:
|
||||
- "security-update"
|
||||
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "ivuorinen"
|
||||
assignees:
|
||||
- "ivuorinen"
|
||||
commit-message:
|
||||
prefix: "fix(github-action)"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
|
||||
# Docker
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 3
|
||||
reviewers:
|
||||
- "ivuorinen"
|
||||
assignees:
|
||||
- "ivuorinen"
|
||||
commit-message:
|
||||
prefix: "fix(docker)"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "docker"
|
||||
8
.github/renovate.json
vendored
8
.github/renovate.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>ivuorinen/renovate-config"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>ivuorinen/renovate-config"
|
||||
]
|
||||
}
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Release
|
||||
|
||||
on:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@ go.sum
|
||||
testdata/**/*.md
|
||||
testdata/**/*.html
|
||||
testdata/**/*.json
|
||||
coverage.*
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
|
||||
version: "2"
|
||||
|
||||
@@ -19,7 +20,7 @@ linters:
|
||||
- errname
|
||||
- exhaustive
|
||||
- forcetypeassert
|
||||
# - funcorder
|
||||
- funcorder
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# GoReleaser configuration for gh-action-readme
|
||||
# See: https://goreleaser.com
|
||||
|
||||
@@ -52,12 +53,7 @@ archives:
|
||||
- goos: windows
|
||||
format: zip
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
{{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE*
|
||||
|
||||
@@ -31,5 +31,5 @@ MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.json
|
||||
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
||||
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
||||
|
||||
FILTER_REGEX_EXCLUDE: >
|
||||
FILTER_REGEX_EXCLUDE: >-
|
||||
(node_modules|\.automation/test|docs/json-schemas|\.github/workflows)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
repos:
|
||||
# Built-in pre-commit hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: requirements-txt-fixer
|
||||
- id: detect-private-key
|
||||
- id: trailing-whitespace
|
||||
args: [--markdown-linebreak-ext=md]
|
||||
exclude: "^testdata/"
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
@@ -16,48 +17,54 @@ repos:
|
||||
- id: check-xml
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
exclude: "^testdata/"
|
||||
- id: end-of-file-fixer
|
||||
exclude: "^testdata/"
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=auto]
|
||||
- id: pretty-format-json
|
||||
args: [--autofix, --no-sort-keys]
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.44.0
|
||||
# YAML formatting with yamlfmt (replaces yamllint for formatting)
|
||||
- repo: https://github.com/google/yamlfmt
|
||||
rev: v0.17.2
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
args: [-c, .markdownlint.json, --fix]
|
||||
- id: yamlfmt
|
||||
exclude: "^testdata/"
|
||||
|
||||
- repo: https://github.com/adrienverge/yamllint
|
||||
rev: v1.37.0
|
||||
# Markdown linting with markdownlint-cli2 (excluding legacy files)
|
||||
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
||||
rev: v0.18.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- id: markdownlint-cli2
|
||||
exclude: '^(testdata/|CHANGELOG\.md|TODO\.md|\.github/|CLAUDE\.md|README\.md|SECURITY\.md)'
|
||||
|
||||
# EditorConfig checking
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker
|
||||
rev: v3.3.0
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
alias: ec
|
||||
|
||||
# Go formatting, imports, and linting
|
||||
- repo: https://github.com/TekWizely/pre-commit-golang
|
||||
rev: v1.0.0-rc.2
|
||||
hooks:
|
||||
- id: go-imports-repo
|
||||
args: [-w]
|
||||
- id: go-mod-tidy
|
||||
- id: golangci-lint-repo-mod
|
||||
args: [--fix]
|
||||
|
||||
# Shell formatting and linting
|
||||
- repo: https://github.com/scop/pre-commit-shfmt
|
||||
rev: v3.11.0-1
|
||||
rev: v3.12.0-2
|
||||
hooks:
|
||||
- id: shfmt
|
||||
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.10.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: ['--severity=warning']
|
||||
|
||||
# GitHub Actions linting
|
||||
- repo: https://github.com/rhysd/actionlint
|
||||
rev: v1.7.7
|
||||
hooks:
|
||||
- id: actionlint
|
||||
args: ['-shellcheck=']
|
||||
|
||||
- repo: https://github.com/renovatebot/pre-commit-hooks
|
||||
rev: 39.227.2
|
||||
hooks:
|
||||
- id: renovate-config-validator
|
||||
|
||||
- repo: https://github.com/bridgecrewio/checkov.git
|
||||
rev: '3.2.400'
|
||||
hooks:
|
||||
- id: checkov
|
||||
args:
|
||||
- '--quiet'
|
||||
args: ["-shellcheck="]
|
||||
|
||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
testdata/yaml-fixtures/validation/invalid-yaml.yml
|
||||
testdata/yaml-fixtures/validation/missing-*
|
||||
testdata/yaml-fixtures/actions/invalid/*
|
||||
testdata/yaml-fixtures/invalid-action.yml
|
||||
testdata/yaml-fixtures/*-template.yml
|
||||
18
.yamlfmt.yml
Normal file
18
.yamlfmt.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/yamlfmt/main/schema.json
|
||||
doublestar: true
|
||||
formatter:
|
||||
type: basic
|
||||
include_document_start: true
|
||||
retain_line_breaks: true
|
||||
retain_line_breaks_single: true
|
||||
trim_trailing_whitespace: true
|
||||
eof_newline: true
|
||||
max_line_length: 300
|
||||
gitignore_excludes: true
|
||||
exclude:
|
||||
- testdata/yaml-fixtures/validation/invalid-yaml.yml
|
||||
- testdata/yaml-fixtures/validation/missing-*
|
||||
- testdata/yaml-fixtures/actions/invalid/*
|
||||
- testdata/yaml-fixtures/invalid-action.yml
|
||||
- testdata/yaml-fixtures/*-template.yml
|
||||
@@ -1 +1,5 @@
|
||||
|
||||
testdata/yaml-fixtures/validation/invalid-yaml.yml
|
||||
testdata/yaml-fixtures/validation/missing-*
|
||||
testdata/yaml-fixtures/actions/invalid/*
|
||||
testdata/yaml-fixtures/invalid-action.ym
|
||||
testdata/yaml-fixtures/*-template.yml
|
||||
|
||||
156
Makefile
156
Makefile
@@ -1,5 +1,6 @@
|
||||
.PHONY: help test lint run example clean readme config-verify security vulncheck audit snyk trivy gitleaks \
|
||||
editorconfig editorconfig-fix format
|
||||
.PHONY: help test test-coverage test-coverage-html lint build run example \
|
||||
clean readme config-verify security vulncheck audit snyk trivy gitleaks \
|
||||
editorconfig editorconfig-fix format devtools pre-commit-install pre-commit-update
|
||||
|
||||
all: help
|
||||
|
||||
@@ -10,18 +11,66 @@ help: ## Show this help message
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "Common workflows:"
|
||||
@echo " make test lint # Run tests and linting"
|
||||
@echo " make format # Format code and fix EditorConfig issues"
|
||||
@echo " make security # Run all security scans"
|
||||
@echo " make devtools # Install all development tools"
|
||||
@echo " make pre-commit-install # Install pre-commit hooks (run once)"
|
||||
@echo " make build # Build the application binary"
|
||||
@echo " make test lint # Run tests and all linters via pre-commit"
|
||||
@echo " make test-coverage # Run tests with coverage analysis"
|
||||
@echo " make pre-commit-update # Update pre-commit hooks to latest versions"
|
||||
@echo " make security # Run all security scans"
|
||||
|
||||
test: ## Run all tests
|
||||
go test ./...
|
||||
|
||||
lint: format ## Run linter (after formatting)
|
||||
golangci-lint run \
|
||||
--max-issues-per-linter 100 \
|
||||
--max-same-issues 50 \
|
||||
--output.tab.path stdout || true
|
||||
test-coverage: ## Run tests with coverage and display in CLI
|
||||
@echo "Running tests with coverage analysis..."
|
||||
@go test ./... -coverprofile=coverage.out -covermode=atomic
|
||||
@echo ""
|
||||
@echo "=== Coverage Summary ==="
|
||||
@go tool cover -func=coverage.out | tail -1
|
||||
@echo ""
|
||||
@echo "=== Package Coverage Details ==="
|
||||
@go tool cover -func=coverage.out | grep -v "total:" | \
|
||||
awk '{printf "%-50s %s\n", $$1, $$3}' | \
|
||||
sort -k2 -nr
|
||||
@echo ""
|
||||
@echo "Coverage report saved to: coverage.out"
|
||||
@echo "Run 'make test-coverage-html' to generate HTML report"
|
||||
|
||||
test-coverage-html: test-coverage ## Generate HTML coverage report and open in browser
|
||||
@echo "Generating HTML coverage report..."
|
||||
@go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "HTML coverage report generated: coverage.html"
|
||||
@if command -v open >/dev/null 2>&1; then \
|
||||
echo "Opening coverage report in browser..."; \
|
||||
open coverage.html; \
|
||||
elif command -v xdg-open >/dev/null 2>&1; then \
|
||||
echo "Opening coverage report in browser..."; \
|
||||
xdg-open coverage.html; \
|
||||
else \
|
||||
echo "Open coverage.html in your browser to view detailed coverage"; \
|
||||
fi
|
||||
|
||||
lint: ## Run all linters via pre-commit
|
||||
@echo "Running all linters via pre-commit..."
|
||||
@command -v pre-commit >/dev/null 2>&1 || \
|
||||
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
|
||||
pre-commit run --all-files
|
||||
|
||||
pre-commit-install: ## Install pre-commit hooks
|
||||
@echo "Installing pre-commit hooks..."
|
||||
@command -v pre-commit >/dev/null 2>&1 || \
|
||||
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
|
||||
pre-commit install
|
||||
|
||||
pre-commit-update: ## Update pre-commit hooks to latest versions
|
||||
@echo "Updating pre-commit hooks..."
|
||||
@command -v pre-commit >/dev/null 2>&1 || \
|
||||
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
|
||||
pre-commit autoupdate
|
||||
|
||||
build: ## Build the application
|
||||
go build -o gh-action-readme .
|
||||
|
||||
config-verify: ## Verify golangci-lint configuration
|
||||
golangci-lint config verify --verbose
|
||||
@@ -37,6 +86,7 @@ readme: ## Generate project README
|
||||
|
||||
clean: ## Clean build artifacts
|
||||
rm -rf dist/
|
||||
rm -f gh-action-readme coverage.out coverage.html
|
||||
|
||||
# Code formatting and EditorConfig targets
|
||||
format: editorconfig-fix ## Format code and fix EditorConfig issues
|
||||
@@ -48,43 +98,59 @@ format: editorconfig-fix ## Format code and fix EditorConfig issues
|
||||
|
||||
editorconfig: ## Check EditorConfig compliance
|
||||
@echo "Checking EditorConfig compliance..."
|
||||
@command -v eclint >/dev/null 2>&1 || \
|
||||
{ echo "Please install eclint: npm install -g eclint"; exit 1; }
|
||||
@echo "Checking files for EditorConfig compliance..."
|
||||
@find . -type f \( \
|
||||
-name "*.go" -o \
|
||||
-name "*.yml" -o \
|
||||
-name "*.yaml" -o \
|
||||
-name "*.json" -o \
|
||||
-name "*.md" -o \
|
||||
-name "Makefile" -o \
|
||||
-name ".snyk" -o \
|
||||
-name "*.tmpl" -o \
|
||||
-name "*.adoc" -o \
|
||||
-name "*.sh" \
|
||||
\) -not -path "./gh-action-readme" -not -path "./coverage*" \
|
||||
-not -path "./testutil.test" -not -path "./test_*" | \
|
||||
xargs eclint check
|
||||
@command -v editorconfig-checker >/dev/null 2>&1 || \
|
||||
{ echo "Please install editorconfig-checker or run 'make devtools'"; exit 1; }
|
||||
editorconfig-checker
|
||||
|
||||
editorconfig-fix: ## Fix EditorConfig violations
|
||||
@echo "Fixing EditorConfig violations..."
|
||||
@command -v eclint >/dev/null 2>&1 || \
|
||||
{ echo "Please install eclint: npm install -g eclint"; exit 1; }
|
||||
@echo "Fixing files for EditorConfig compliance..."
|
||||
@find . -type f \( \
|
||||
-name "*.go" -o \
|
||||
-name "*.yml" -o \
|
||||
-name "*.yaml" -o \
|
||||
-name "*.json" -o \
|
||||
-name "*.md" -o \
|
||||
-name "Makefile" -o \
|
||||
-name ".snyk" -o \
|
||||
-name "*.tmpl" -o \
|
||||
-name "*.adoc" -o \
|
||||
-name "*.sh" \
|
||||
\) -not -path "./gh-action-readme" -not -path "./coverage*" \
|
||||
-not -path "./testutil.test" -not -path "./test_*" | \
|
||||
xargs eclint fix
|
||||
@echo "EditorConfig violations cannot be automatically fixed by editorconfig-checker"
|
||||
@echo "Please fix the reported issues manually or use your editor's EditorConfig plugin"
|
||||
@echo "Running check to show issues..."
|
||||
@command -v editorconfig-checker >/dev/null 2>&1 || \
|
||||
{ echo "Please install editorconfig-checker or run 'make devtools'"; exit 1; }
|
||||
editorconfig-checker
|
||||
|
||||
# Development tools installation
|
||||
devtools: ## Install all development tools
|
||||
@echo "Installing development tools..."
|
||||
@echo ""
|
||||
@echo "=== Go Tools ==="
|
||||
@command -v golangci-lint >/dev/null 2>&1 || \
|
||||
{ echo "Installing golangci-lint..."; \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
|
||||
sh -s -- -b $(go env GOPATH)/bin; }
|
||||
@command -v govulncheck >/dev/null 2>&1 || \
|
||||
{ echo "Installing govulncheck..."; go install golang.org/x/vuln/cmd/govulncheck@latest; }
|
||||
@command -v editorconfig-checker >/dev/null 2>&1 || \
|
||||
{ echo "Installing editorconfig-checker..."; \
|
||||
go install github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@latest; }
|
||||
@command -v yamlfmt >/dev/null 2>&1 || \
|
||||
{ echo "Installing yamlfmt..."; go install github.com/google/yamlfmt/cmd/yamlfmt@latest; }
|
||||
@echo "✓ Go tools installed"
|
||||
@echo ""
|
||||
@echo "=== Node.js Tools ==="
|
||||
@command -v npm >/dev/null 2>&1 || \
|
||||
{ echo "❌ npm not found. Please install Node.js first."; exit 1; }
|
||||
@command -v snyk >/dev/null 2>&1 || \
|
||||
{ echo "Installing snyk..."; npm install -g snyk; }
|
||||
@echo "✓ Node.js tools installed"
|
||||
@echo ""
|
||||
@echo "=== Python Tools ==="
|
||||
@command -v python3 >/dev/null 2>&1 || \
|
||||
{ echo "❌ python3 not found. Please install Python 3 first."; exit 1; }
|
||||
@command -v pre-commit >/dev/null 2>&1 || \
|
||||
{ echo "Installing pre-commit..."; pip install pre-commit; }
|
||||
@echo "✓ Python tools installed"
|
||||
@echo ""
|
||||
@echo "=== System Tools ==="
|
||||
@command -v trivy >/dev/null 2>&1 || \
|
||||
{ echo "❌ trivy not found. Please install manually: https://aquasecurity.github.io/trivy/"; }
|
||||
@command -v gitleaks >/dev/null 2>&1 || \
|
||||
{ echo "❌ gitleaks not found. Please install manually: https://github.com/gitleaks/gitleaks"; }
|
||||
@echo "✓ System tools check completed"
|
||||
@echo ""
|
||||
@echo "🎉 Development tools installation completed!"
|
||||
@echo " Run 'make test lint' to verify everything works."
|
||||
|
||||
# Security targets
|
||||
security: vulncheck snyk trivy gitleaks ## Run all security scans
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# Default configuration for gh-action-readme
|
||||
defaults:
|
||||
name: "GitHub Action"
|
||||
|
||||
38
internal/cache/cache.go
vendored
38
internal/cache/cache.go
vendored
@@ -196,6 +196,25 @@ func (c *Cache) Close() error {
|
||||
return c.saveToDisk()
|
||||
}
|
||||
|
||||
// GetOrSet retrieves a value from cache or sets it if not found.
|
||||
func (c *Cache) GetOrSet(key string, getter func() (any, error)) (any, error) {
|
||||
// Try to get from cache first
|
||||
if value, exists := c.Get(key); exists {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Not in cache, get from source
|
||||
value, err := getter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
_ = c.Set(key, value) // Log error but don't fail - we have the value
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// cleanupLoop runs periodically to remove expired entries.
|
||||
func (c *Cache) cleanupLoop() {
|
||||
for {
|
||||
@@ -289,22 +308,3 @@ func (c *Cache) estimateSize(value any) int64 {
|
||||
|
||||
return int64(len(jsonData))
|
||||
}
|
||||
|
||||
// GetOrSet retrieves a value from cache or sets it if not found.
|
||||
func (c *Cache) GetOrSet(key string, getter func() (any, error)) (any, error) {
|
||||
// Try to get from cache first
|
||||
if value, exists := c.Get(key); exists {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Not in cache, get from source
|
||||
value, err := getter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
_ = c.Set(key, value) // Log error but don't fail - we have the value
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -108,6 +108,77 @@ func (cl *ConfigurationLoader) LoadConfiguration(configFile, repoRoot, actionDir
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// LoadGlobalConfig loads only the global configuration.
|
||||
func (cl *ConfigurationLoader) LoadGlobalConfig(configFile string) (*AppConfig, error) {
|
||||
return cl.loadGlobalConfig(configFile)
|
||||
}
|
||||
|
||||
// ValidateConfiguration validates a configuration for consistency and required values.
|
||||
func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
|
||||
if config == nil {
|
||||
return errors.New("configuration cannot be nil")
|
||||
}
|
||||
|
||||
// Validate output format
|
||||
validFormats := []string{"md", "html", "json", "asciidoc"}
|
||||
if !containsString(validFormats, config.OutputFormat) {
|
||||
return fmt.Errorf("invalid output format '%s', must be one of: %s",
|
||||
config.OutputFormat, strings.Join(validFormats, ", "))
|
||||
}
|
||||
|
||||
// Validate theme (if set)
|
||||
if config.Theme != "" {
|
||||
if err := cl.validateTheme(config.Theme); err != nil {
|
||||
return fmt.Errorf("invalid theme: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate output directory
|
||||
if config.OutputDir == "" {
|
||||
return errors.New("output directory cannot be empty")
|
||||
}
|
||||
|
||||
// Validate mutually exclusive flags
|
||||
if config.Verbose && config.Quiet {
|
||||
return errors.New("verbose and quiet flags are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsString checks if a slice contains a string.
|
||||
func containsString(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetConfigurationSources returns the currently enabled configuration sources.
|
||||
func (cl *ConfigurationLoader) GetConfigurationSources() []ConfigurationSource {
|
||||
var sources []ConfigurationSource
|
||||
for source, enabled := range cl.sources {
|
||||
if enabled {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
// EnableSource enables a specific configuration source.
|
||||
func (cl *ConfigurationLoader) EnableSource(source ConfigurationSource) {
|
||||
cl.sources[source] = true
|
||||
}
|
||||
|
||||
// DisableSource disables a specific configuration source.
|
||||
func (cl *ConfigurationLoader) DisableSource(source ConfigurationSource) {
|
||||
cl.sources[source] = false
|
||||
}
|
||||
|
||||
// loadDefaultsStep loads default configuration values.
|
||||
func (cl *ConfigurationLoader) loadDefaultsStep(config *AppConfig) {
|
||||
if cl.sources[SourceDefaults] {
|
||||
@@ -177,44 +248,6 @@ func (cl *ConfigurationLoader) loadEnvironmentStep(config *AppConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// LoadGlobalConfig loads only the global configuration.
|
||||
func (cl *ConfigurationLoader) LoadGlobalConfig(configFile string) (*AppConfig, error) {
|
||||
return cl.loadGlobalConfig(configFile)
|
||||
}
|
||||
|
||||
// ValidateConfiguration validates a configuration for consistency and required values.
|
||||
func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
|
||||
if config == nil {
|
||||
return errors.New("configuration cannot be nil")
|
||||
}
|
||||
|
||||
// Validate output format
|
||||
validFormats := []string{"md", "html", "json", "asciidoc"}
|
||||
if !containsString(validFormats, config.OutputFormat) {
|
||||
return fmt.Errorf("invalid output format '%s', must be one of: %s",
|
||||
config.OutputFormat, strings.Join(validFormats, ", "))
|
||||
}
|
||||
|
||||
// Validate theme (if set)
|
||||
if config.Theme != "" {
|
||||
if err := cl.validateTheme(config.Theme); err != nil {
|
||||
return fmt.Errorf("invalid theme: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate output directory
|
||||
if config.OutputDir == "" {
|
||||
return errors.New("output directory cannot be empty")
|
||||
}
|
||||
|
||||
// Validate mutually exclusive flags
|
||||
if config.Verbose && config.Quiet {
|
||||
return errors.New("verbose and quiet flags are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadGlobalConfig initializes and loads the global configuration using Viper.
|
||||
func (cl *ConfigurationLoader) loadGlobalConfig(configFile string) (*AppConfig, error) {
|
||||
v := viper.New()
|
||||
@@ -396,39 +429,6 @@ func (cl *ConfigurationLoader) validateTheme(theme string) error {
|
||||
theme, strings.Join(supportedThemes, ", "))
|
||||
}
|
||||
|
||||
// containsString checks if a slice contains a string.
|
||||
func containsString(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetConfigurationSources returns the currently enabled configuration sources.
|
||||
func (cl *ConfigurationLoader) GetConfigurationSources() []ConfigurationSource {
|
||||
var sources []ConfigurationSource
|
||||
for source, enabled := range cl.sources {
|
||||
if enabled {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
// EnableSource enables a specific configuration source.
|
||||
func (cl *ConfigurationLoader) EnableSource(source ConfigurationSource) {
|
||||
cl.sources[source] = true
|
||||
}
|
||||
|
||||
// DisableSource disables a specific configuration source.
|
||||
func (cl *ConfigurationLoader) DisableSource(source ConfigurationSource) {
|
||||
cl.sources[source] = false
|
||||
}
|
||||
|
||||
// String returns a string representation of a ConfigurationSource.
|
||||
func (s ConfigurationSource) String() string {
|
||||
switch s {
|
||||
|
||||
@@ -168,6 +168,85 @@ func (a *Analyzer) AnalyzeActionFileWithProgress(
|
||||
return a.processCompositeSteps(action.Runs.Steps, progressCallback)
|
||||
}
|
||||
|
||||
// CheckOutdated analyzes dependencies and finds those with newer versions available.
|
||||
func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error) {
|
||||
var outdated []OutdatedDependency
|
||||
|
||||
for _, dep := range deps {
|
||||
if dep.IsShellScript || dep.IsLocalAction {
|
||||
continue // Skip shell scripts and local actions
|
||||
}
|
||||
|
||||
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
|
||||
if owner == "" || repo == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
latestVersion, latestSHA, err := a.getLatestVersion(owner, repo)
|
||||
if err != nil {
|
||||
continue // Skip on error, don't fail the whole operation
|
||||
}
|
||||
|
||||
updateType := a.compareVersions(currentVersion, latestVersion)
|
||||
if updateType != updateTypeNone {
|
||||
outdated = append(outdated, OutdatedDependency{
|
||||
Current: dep,
|
||||
LatestVersion: latestVersion,
|
||||
LatestSHA: latestSHA,
|
||||
UpdateType: updateType,
|
||||
IsSecurityUpdate: updateType == updateTypeMajor, // Assume major updates might be security
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return outdated, nil
|
||||
}
|
||||
|
||||
// GeneratePinnedUpdate creates a pinned update for a dependency.
|
||||
func (a *Analyzer) GeneratePinnedUpdate(
|
||||
actionPath string,
|
||||
dep Dependency,
|
||||
latestVersion, latestSHA string,
|
||||
) (*PinnedUpdate, error) {
|
||||
if latestSHA == "" {
|
||||
return nil, fmt.Errorf("no commit SHA available for %s", dep.Uses)
|
||||
}
|
||||
|
||||
// Create the new pinned uses string: "owner/repo@sha # version"
|
||||
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
|
||||
newUses := fmt.Sprintf("%s/%s@%s # %s", owner, repo, latestSHA, latestVersion)
|
||||
|
||||
updateType := a.compareVersions(currentVersion, latestVersion)
|
||||
|
||||
return &PinnedUpdate{
|
||||
FilePath: actionPath,
|
||||
OldUses: dep.Uses,
|
||||
NewUses: newUses,
|
||||
CommitSHA: latestSHA,
|
||||
Version: latestVersion,
|
||||
UpdateType: updateType,
|
||||
LineNumber: 0, // Will be determined during file update
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApplyPinnedUpdates applies pinned updates to action files.
|
||||
func (a *Analyzer) ApplyPinnedUpdates(updates []PinnedUpdate) error {
|
||||
// Group updates by file path
|
||||
updatesByFile := make(map[string][]PinnedUpdate)
|
||||
for _, update := range updates {
|
||||
updatesByFile[update.FilePath] = append(updatesByFile[update.FilePath], update)
|
||||
}
|
||||
|
||||
// Apply updates to each file
|
||||
for filePath, fileUpdates := range updatesByFile {
|
||||
if err := a.updateActionFile(filePath, fileUpdates); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAndCheckComposite validates action type and checks if it's composite.
|
||||
func (a *Analyzer) validateAndCheckComposite(
|
||||
action *ActionWithComposite,
|
||||
@@ -244,8 +323,6 @@ func (a *Analyzer) processStep(step CompositeStep, stepNumber int) *Dependency {
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCompositeAction is implemented in parser.go
|
||||
|
||||
// analyzeActionDependency analyzes a single action dependency.
|
||||
func (a *Analyzer) analyzeActionDependency(step CompositeStep, _ int) (*Dependency, error) {
|
||||
// Parse the uses statement
|
||||
@@ -405,40 +482,6 @@ func (a *Analyzer) convertWithParams(with map[string]any) map[string]string {
|
||||
return params
|
||||
}
|
||||
|
||||
// CheckOutdated analyzes dependencies and finds those with newer versions available.
|
||||
func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error) {
|
||||
var outdated []OutdatedDependency
|
||||
|
||||
for _, dep := range deps {
|
||||
if dep.IsShellScript || dep.IsLocalAction {
|
||||
continue // Skip shell scripts and local actions
|
||||
}
|
||||
|
||||
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
|
||||
if owner == "" || repo == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
latestVersion, latestSHA, err := a.getLatestVersion(owner, repo)
|
||||
if err != nil {
|
||||
continue // Skip on error, don't fail the whole operation
|
||||
}
|
||||
|
||||
updateType := a.compareVersions(currentVersion, latestVersion)
|
||||
if updateType != updateTypeNone {
|
||||
outdated = append(outdated, OutdatedDependency{
|
||||
Current: dep,
|
||||
LatestVersion: latestVersion,
|
||||
LatestSHA: latestSHA,
|
||||
UpdateType: updateType,
|
||||
IsSecurityUpdate: updateType == updateTypeMajor, // Assume major updates might be security
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return outdated, nil
|
||||
}
|
||||
|
||||
// getLatestVersion fetches the latest release/tag for a repository.
|
||||
func (a *Analyzer) getLatestVersion(owner, repo string) (version, sha string, err error) {
|
||||
if a.GitHubClient == nil {
|
||||
@@ -584,51 +627,6 @@ func (a *Analyzer) determineUpdateType(currentParts, latestParts []string) strin
|
||||
return updateTypeNone
|
||||
}
|
||||
|
||||
// GeneratePinnedUpdate creates a pinned update for a dependency.
|
||||
func (a *Analyzer) GeneratePinnedUpdate(
|
||||
actionPath string,
|
||||
dep Dependency,
|
||||
latestVersion, latestSHA string,
|
||||
) (*PinnedUpdate, error) {
|
||||
if latestSHA == "" {
|
||||
return nil, fmt.Errorf("no commit SHA available for %s", dep.Uses)
|
||||
}
|
||||
|
||||
// Create the new pinned uses string: "owner/repo@sha # version"
|
||||
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
|
||||
newUses := fmt.Sprintf("%s/%s@%s # %s", owner, repo, latestSHA, latestVersion)
|
||||
|
||||
updateType := a.compareVersions(currentVersion, latestVersion)
|
||||
|
||||
return &PinnedUpdate{
|
||||
FilePath: actionPath,
|
||||
OldUses: dep.Uses,
|
||||
NewUses: newUses,
|
||||
CommitSHA: latestSHA,
|
||||
Version: latestVersion,
|
||||
UpdateType: updateType,
|
||||
LineNumber: 0, // Will be determined during file update
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ApplyPinnedUpdates applies pinned updates to action files.
|
||||
func (a *Analyzer) ApplyPinnedUpdates(updates []PinnedUpdate) error {
|
||||
// Group updates by file path
|
||||
updatesByFile := make(map[string][]PinnedUpdate)
|
||||
for _, update := range updates {
|
||||
updatesByFile[update.FilePath] = append(updatesByFile[update.FilePath], update)
|
||||
}
|
||||
|
||||
// Apply updates to each file
|
||||
for filePath, fileUpdates := range updatesByFile {
|
||||
if err := a.updateActionFile(filePath, fileUpdates); err != nil {
|
||||
return fmt.Errorf("failed to update %s: %w", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateActionFile applies updates to a single action file.
|
||||
func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) error {
|
||||
// Read the file
|
||||
|
||||
@@ -34,9 +34,39 @@ type Generator struct {
|
||||
Progress ProgressManager
|
||||
}
|
||||
|
||||
// isUnitTestEnvironment detects if we're running unit tests (not integration tests).
|
||||
func isUnitTestEnvironment() bool {
|
||||
// Only enable for unit tests, not integration tests
|
||||
// Integration tests need real output to verify CLI behavior
|
||||
|
||||
// Check if we're in the internal package tests
|
||||
if strings.Contains(os.Args[0], "internal.test") ||
|
||||
strings.Contains(os.Args[0], "T/go-build") && strings.Contains(os.Args[0], "internal") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for explicit unit test environment variable
|
||||
if os.Getenv("UNIT_TEST_MODE") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NewGenerator creates a new generator instance with the provided configuration.
|
||||
// This constructor maintains backward compatibility by using concrete implementations.
|
||||
// In unit test environments, it automatically uses NullOutput to suppress output.
|
||||
func NewGenerator(config *AppConfig) *Generator {
|
||||
// Use null output in unit test environments to keep tests clean
|
||||
// Integration tests need real output to verify CLI behavior
|
||||
if isUnitTestEnvironment() {
|
||||
return NewGeneratorWithDependencies(
|
||||
config,
|
||||
NewNullOutput(),
|
||||
NewNullProgressManager(),
|
||||
)
|
||||
}
|
||||
|
||||
return NewGeneratorWithDependencies(
|
||||
config,
|
||||
NewColoredOutput(config.Quiet),
|
||||
@@ -115,76 +145,116 @@ func (g *Generator) GenerateFromFile(actionPath string) error {
|
||||
return g.generateByFormat(action, outputDir, actionPath)
|
||||
}
|
||||
|
||||
// parseAndValidateAction parses and validates an action.yml file.
|
||||
func (g *Generator) parseAndValidateAction(actionPath string) (*ActionYML, error) {
|
||||
action, err := ParseActionYML(actionPath)
|
||||
// DiscoverActionFiles finds action.yml and action.yaml files in the given directory
|
||||
// using the centralized parser function and adds verbose logging.
|
||||
func (g *Generator) DiscoverActionFiles(dir string, recursive bool) ([]string, error) {
|
||||
actionFiles, err := DiscoverActionFiles(dir, recursive)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse action file %s: %w", actionPath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validationResult := ValidateActionYML(action)
|
||||
if len(validationResult.MissingFields) > 0 {
|
||||
// Check for critical validation errors that cannot be fixed with defaults
|
||||
for _, field := range validationResult.MissingFields {
|
||||
// All core required fields should cause validation failure
|
||||
if field == "name" || field == "description" || field == "runs" || field == "runs.using" {
|
||||
// Required fields missing - cannot be fixed with defaults, must fail
|
||||
return nil, fmt.Errorf(
|
||||
"action file %s has invalid configuration, missing required field(s): %v",
|
||||
actionPath,
|
||||
validationResult.MissingFields,
|
||||
)
|
||||
// Add verbose logging
|
||||
if g.Config.Verbose {
|
||||
for _, file := range actionFiles {
|
||||
if recursive {
|
||||
g.Output.Info("Discovered action file: %s", file)
|
||||
} else {
|
||||
g.Output.Info("Found action file: %s", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.Config.Verbose {
|
||||
g.Output.Warning("Missing fields in %s: %v", actionPath, validationResult.MissingFields)
|
||||
}
|
||||
FillMissing(action, g.Config.Defaults)
|
||||
if g.Config.Verbose {
|
||||
g.Output.Info("Applied default values for missing fields")
|
||||
return actionFiles, nil
|
||||
}
|
||||
|
||||
// DiscoverActionFilesWithValidation discovers action files with centralized error handling and validation.
|
||||
// This function consolidates the duplicated file discovery logic across the codebase.
|
||||
func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool, context string) ([]string, error) {
|
||||
// Discover action files
|
||||
actionFiles, err := g.DiscoverActionFiles(dir, recursive)
|
||||
if err != nil {
|
||||
g.Output.ErrorWithContext(
|
||||
errCodes.ErrCodeFileNotFound,
|
||||
"failed to discover action files for "+context,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
ContextKeyError: err.Error(),
|
||||
},
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if any files were found
|
||||
if len(actionFiles) == 0 {
|
||||
contextMsg := "no GitHub Action files found for " + context
|
||||
g.Output.ErrorWithContext(
|
||||
errCodes.ErrCodeNoActionFiles,
|
||||
contextMsg,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
"suggestion": "Please run this command in a directory containing GitHub Action files (action.yml or action.yaml)",
|
||||
},
|
||||
)
|
||||
|
||||
return nil, fmt.Errorf("no action files found in directory: %s", dir)
|
||||
}
|
||||
|
||||
return actionFiles, nil
|
||||
}
|
||||
|
||||
// ProcessBatch processes multiple action.yml files.
|
||||
func (g *Generator) ProcessBatch(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return errors.New("no action files to process")
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Processing files", paths)
|
||||
errors, successCount := g.processFiles(paths, bar)
|
||||
g.Progress.FinishProgressBarWithNewline(bar)
|
||||
g.reportResults(successCount, errors)
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("encountered %d errors during batch processing", len(errors))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateFiles validates multiple action.yml files and reports results.
|
||||
func (g *Generator) ValidateFiles(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return errors.New("no action files to validate")
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Validating files", paths)
|
||||
allResults, errors := g.validateFiles(paths, bar)
|
||||
g.Progress.FinishProgressBarWithNewline(bar)
|
||||
|
||||
if !g.Config.Quiet {
|
||||
g.reportValidationResults(allResults, errors)
|
||||
}
|
||||
|
||||
// Count validation failures (files with missing required fields)
|
||||
validationFailures := 0
|
||||
for _, result := range allResults {
|
||||
// Each result starts with "file: <path>" so check if there are actual missing fields beyond that
|
||||
if len(result.MissingFields) > 1 {
|
||||
validationFailures++
|
||||
}
|
||||
}
|
||||
|
||||
return action, nil
|
||||
}
|
||||
if len(errors) > 0 || validationFailures > 0 {
|
||||
totalFailures := len(errors) + validationFailures
|
||||
|
||||
// determineOutputDir calculates the output directory for generated files.
|
||||
func (g *Generator) determineOutputDir(actionPath string) string {
|
||||
if g.Config.OutputDir == "" || g.Config.OutputDir == "." {
|
||||
return filepath.Dir(actionPath)
|
||||
return fmt.Errorf("validation failed for %d files", totalFailures)
|
||||
}
|
||||
|
||||
return g.Config.OutputDir
|
||||
}
|
||||
|
||||
// resolveOutputPath resolves the final output path, considering custom filename.
|
||||
func (g *Generator) resolveOutputPath(outputDir, defaultFilename string) string {
|
||||
if g.Config.OutputFilename != "" {
|
||||
if filepath.IsAbs(g.Config.OutputFilename) {
|
||||
return g.Config.OutputFilename
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, g.Config.OutputFilename)
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, defaultFilename)
|
||||
}
|
||||
|
||||
// generateByFormat generates documentation in the specified format.
|
||||
func (g *Generator) generateByFormat(action *ActionYML, outputDir, actionPath string) error {
|
||||
switch g.Config.OutputFormat {
|
||||
case "md":
|
||||
return g.generateMarkdown(action, outputDir, actionPath)
|
||||
case OutputFormatHTML:
|
||||
return g.generateHTML(action, outputDir, actionPath)
|
||||
case OutputFormatJSON:
|
||||
return g.generateJSON(action, outputDir)
|
||||
case OutputFormatASCIIDoc:
|
||||
return g.generateASCIIDoc(action, outputDir, actionPath)
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", g.Config.OutputFormat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateMarkdown creates a README.md file using the template.
|
||||
@@ -311,86 +381,6 @@ func (g *Generator) generateASCIIDoc(action *ActionYML, outputDir, actionPath st
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscoverActionFiles finds action.yml and action.yaml files in the given directory
|
||||
// using the centralized parser function and adds verbose logging.
|
||||
func (g *Generator) DiscoverActionFiles(dir string, recursive bool) ([]string, error) {
|
||||
actionFiles, err := DiscoverActionFiles(dir, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add verbose logging
|
||||
if g.Config.Verbose {
|
||||
for _, file := range actionFiles {
|
||||
if recursive {
|
||||
g.Output.Info("Discovered action file: %s", file)
|
||||
} else {
|
||||
g.Output.Info("Found action file: %s", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actionFiles, nil
|
||||
}
|
||||
|
||||
// DiscoverActionFilesWithValidation discovers action files with centralized error handling and validation.
|
||||
// This function consolidates the duplicated file discovery logic across the codebase.
|
||||
func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool, context string) ([]string, error) {
|
||||
// Discover action files
|
||||
actionFiles, err := g.DiscoverActionFiles(dir, recursive)
|
||||
if err != nil {
|
||||
g.Output.ErrorWithContext(
|
||||
errCodes.ErrCodeFileNotFound,
|
||||
"failed to discover action files for "+context,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
ContextKeyError: err.Error(),
|
||||
},
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if any files were found
|
||||
if len(actionFiles) == 0 {
|
||||
contextMsg := "no GitHub Action files found for " + context
|
||||
g.Output.ErrorWithContext(
|
||||
errCodes.ErrCodeNoActionFiles,
|
||||
contextMsg,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
"suggestion": "Please run this command in a directory containing GitHub Action files (action.yml or action.yaml)",
|
||||
},
|
||||
)
|
||||
|
||||
return nil, fmt.Errorf("no action files found in directory: %s", dir)
|
||||
}
|
||||
|
||||
return actionFiles, nil
|
||||
}
|
||||
|
||||
// ProcessBatch processes multiple action.yml files.
|
||||
func (g *Generator) ProcessBatch(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return errors.New("no action files to process")
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Processing files", paths)
|
||||
errors, successCount := g.processFiles(paths, bar)
|
||||
g.Progress.FinishProgressBarWithNewline(bar)
|
||||
g.reportResults(successCount, errors)
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("encountered %d errors during batch processing", len(errors))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processFiles processes each file and tracks results.
|
||||
func (g *Generator) processFiles(paths []string, bar *progressbar.ProgressBar) ([]string, int) {
|
||||
var errors []string
|
||||
@@ -429,36 +419,76 @@ func (g *Generator) reportResults(successCount int, errors []string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateFiles validates multiple action.yml files and reports results.
|
||||
func (g *Generator) ValidateFiles(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return errors.New("no action files to validate")
|
||||
// parseAndValidateAction parses and validates an action.yml file.
|
||||
func (g *Generator) parseAndValidateAction(actionPath string) (*ActionYML, error) {
|
||||
action, err := ParseActionYML(actionPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse action file %s: %w", actionPath, err)
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Validating files", paths)
|
||||
allResults, errors := g.validateFiles(paths, bar)
|
||||
g.Progress.FinishProgressBarWithNewline(bar)
|
||||
validationResult := ValidateActionYML(action)
|
||||
if len(validationResult.MissingFields) > 0 {
|
||||
// Check for critical validation errors that cannot be fixed with defaults
|
||||
for _, field := range validationResult.MissingFields {
|
||||
// All core required fields should cause validation failure
|
||||
if field == "name" || field == "description" || field == "runs" || field == "runs.using" {
|
||||
// Required fields missing - cannot be fixed with defaults, must fail
|
||||
return nil, fmt.Errorf(
|
||||
"action file %s has invalid configuration, missing required field(s): %v",
|
||||
actionPath,
|
||||
validationResult.MissingFields,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !g.Config.Quiet {
|
||||
g.reportValidationResults(allResults, errors)
|
||||
}
|
||||
|
||||
// Count validation failures (files with missing required fields)
|
||||
validationFailures := 0
|
||||
for _, result := range allResults {
|
||||
// Each result starts with "file: <path>" so check if there are actual missing fields beyond that
|
||||
if len(result.MissingFields) > 1 {
|
||||
validationFailures++
|
||||
if g.Config.Verbose {
|
||||
g.Output.Warning("Missing fields in %s: %v", actionPath, validationResult.MissingFields)
|
||||
}
|
||||
FillMissing(action, g.Config.Defaults)
|
||||
if g.Config.Verbose {
|
||||
g.Output.Info("Applied default values for missing fields")
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 || validationFailures > 0 {
|
||||
totalFailures := len(errors) + validationFailures
|
||||
return action, nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("validation failed for %d files", totalFailures)
|
||||
// determineOutputDir calculates the output directory for generated files.
|
||||
func (g *Generator) determineOutputDir(actionPath string) string {
|
||||
if g.Config.OutputDir == "" || g.Config.OutputDir == "." {
|
||||
return filepath.Dir(actionPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
return g.Config.OutputDir
|
||||
}
|
||||
|
||||
// resolveOutputPath resolves the final output path, considering custom filename.
|
||||
func (g *Generator) resolveOutputPath(outputDir, defaultFilename string) string {
|
||||
if g.Config.OutputFilename != "" {
|
||||
if filepath.IsAbs(g.Config.OutputFilename) {
|
||||
return g.Config.OutputFilename
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, g.Config.OutputFilename)
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, defaultFilename)
|
||||
}
|
||||
|
||||
// generateByFormat generates documentation in the specified format.
|
||||
func (g *Generator) generateByFormat(action *ActionYML, outputDir, actionPath string) error {
|
||||
switch g.Config.OutputFormat {
|
||||
case "md":
|
||||
return g.generateMarkdown(action, outputDir, actionPath)
|
||||
case OutputFormatHTML:
|
||||
return g.generateHTML(action, outputDir, actionPath)
|
||||
case OutputFormatJSON:
|
||||
return g.generateJSON(action, outputDir)
|
||||
case OutputFormatASCIIDoc:
|
||||
return g.generateASCIIDoc(action, outputDir, actionPath)
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", g.Config.OutputFormat)
|
||||
}
|
||||
}
|
||||
|
||||
// validateFiles processes each file for validation.
|
||||
|
||||
@@ -563,19 +563,18 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
t.Parallel()
|
||||
themes := []string{"default", "github", "gitlab", "minimal", "professional"}
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
// Set up test templates
|
||||
testutil.SetupTestTemplates(t, tmpDir)
|
||||
|
||||
actionPath := filepath.Join(tmpDir, "action.yml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
for _, theme := range themes {
|
||||
t.Run("theme_"+theme, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Templates are now embedded, no working directory changes needed
|
||||
// Create separate temp directory for each theme test
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
// Set up test templates for this theme test
|
||||
testutil.SetupTestTemplates(t, tmpDir)
|
||||
|
||||
actionPath := filepath.Join(tmpDir, "action.yml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
config := &AppConfig{
|
||||
Theme: theme,
|
||||
@@ -596,11 +595,6 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
if len(readmeFiles) == 0 {
|
||||
t.Errorf("no output file was created for theme %s", theme)
|
||||
}
|
||||
|
||||
// Clean up for next test
|
||||
for _, file := range readmeFiles {
|
||||
_ = os.Remove(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,35 +78,34 @@ func TestProgressBarManager_CreateProgressBarForFiles(t *testing.T) {
|
||||
|
||||
func TestProgressBarManager_FinishProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
// Use quiet mode to avoid cluttering test output
|
||||
pm := NewProgressBarManager(true)
|
||||
|
||||
// Test with nil bar (should not panic)
|
||||
pm.FinishProgressBar(nil)
|
||||
|
||||
// Test with actual bar
|
||||
// Test with actual bar (will be nil in quiet mode)
|
||||
bar := pm.CreateProgressBar("Test", 5)
|
||||
if bar != nil {
|
||||
pm.FinishProgressBar(bar)
|
||||
}
|
||||
pm.FinishProgressBar(bar) // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func TestProgressBarManager_UpdateProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
// Use quiet mode to avoid cluttering test output
|
||||
pm := NewProgressBarManager(true)
|
||||
|
||||
// Test with nil bar (should not panic)
|
||||
pm.UpdateProgressBar(nil)
|
||||
|
||||
// Test with actual bar
|
||||
// Test with actual bar (will be nil in quiet mode)
|
||||
bar := pm.CreateProgressBar("Test", 5)
|
||||
if bar != nil {
|
||||
pm.UpdateProgressBar(bar)
|
||||
}
|
||||
pm.UpdateProgressBar(bar) // Should handle nil gracefully
|
||||
}
|
||||
|
||||
func TestProgressBarManager_ProcessWithProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
// Use NullProgressManager to avoid cluttering test output
|
||||
pm := NewNullProgressManager()
|
||||
items := []string{"item1", "item2", "item3"}
|
||||
|
||||
processedItems := make([]string, 0)
|
||||
|
||||
120
internal/testoutput.go
Normal file
120
internal/testoutput.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/internal/errors"
|
||||
)
|
||||
|
||||
// NullOutput is a no-op implementation of CompleteOutput for testing.
|
||||
// All methods are no-ops to prevent cluttering test output.
|
||||
type NullOutput struct{}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ MessageLogger = (*NullOutput)(nil)
|
||||
_ ErrorReporter = (*NullOutput)(nil)
|
||||
_ ErrorFormatter = (*NullOutput)(nil)
|
||||
_ ProgressReporter = (*NullOutput)(nil)
|
||||
_ OutputConfig = (*NullOutput)(nil)
|
||||
_ CompleteOutput = (*NullOutput)(nil)
|
||||
)
|
||||
|
||||
// NewNullOutput creates a new null output instance for testing.
|
||||
func NewNullOutput() *NullOutput {
|
||||
return &NullOutput{}
|
||||
}
|
||||
|
||||
// IsQuiet returns true as null output is always quiet.
|
||||
func (no *NullOutput) IsQuiet() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Success is a no-op.
|
||||
func (no *NullOutput) Success(_ string, _ ...any) {}
|
||||
|
||||
// Error is a no-op.
|
||||
func (no *NullOutput) Error(_ string, _ ...any) {}
|
||||
|
||||
// Warning is a no-op.
|
||||
func (no *NullOutput) Warning(_ string, _ ...any) {}
|
||||
|
||||
// Info is a no-op.
|
||||
func (no *NullOutput) Info(_ string, _ ...any) {}
|
||||
|
||||
// Progress is a no-op.
|
||||
func (no *NullOutput) Progress(_ string, _ ...any) {}
|
||||
|
||||
// Bold is a no-op.
|
||||
func (no *NullOutput) Bold(_ string, _ ...any) {}
|
||||
|
||||
// Printf is a no-op.
|
||||
func (no *NullOutput) Printf(_ string, _ ...any) {}
|
||||
|
||||
// Fprintf is a no-op.
|
||||
func (no *NullOutput) Fprintf(_ *os.File, _ string, _ ...any) {}
|
||||
|
||||
// ErrorWithSuggestions is a no-op.
|
||||
func (no *NullOutput) ErrorWithSuggestions(_ *errors.ContextualError) {}
|
||||
|
||||
// ErrorWithContext is a no-op.
|
||||
func (no *NullOutput) ErrorWithContext(
|
||||
_ errors.ErrorCode,
|
||||
_ string,
|
||||
_ map[string]string,
|
||||
) {
|
||||
}
|
||||
|
||||
// ErrorWithSimpleFix is a no-op.
|
||||
func (no *NullOutput) ErrorWithSimpleFix(_, _ string) {}
|
||||
|
||||
// FormatContextualError returns empty string.
|
||||
func (no *NullOutput) FormatContextualError(_ *errors.ContextualError) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// NullProgressManager is a no-op implementation of ProgressManager for testing.
|
||||
type NullProgressManager struct{}
|
||||
|
||||
// Compile-time interface check.
|
||||
var _ ProgressManager = (*NullProgressManager)(nil)
|
||||
|
||||
// NewNullProgressManager creates a new null progress manager for testing.
|
||||
func NewNullProgressManager() *NullProgressManager {
|
||||
return &NullProgressManager{}
|
||||
}
|
||||
|
||||
// CreateProgressBar returns nil to suppress progress bars.
|
||||
func (npm *NullProgressManager) CreateProgressBar(_ string, _ int) *progressbar.ProgressBar {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateProgressBarForFiles returns nil to suppress progress bars.
|
||||
func (npm *NullProgressManager) CreateProgressBarForFiles(
|
||||
_ string,
|
||||
_ []string,
|
||||
) *progressbar.ProgressBar {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FinishProgressBar is a no-op.
|
||||
func (npm *NullProgressManager) FinishProgressBar(_ *progressbar.ProgressBar) {}
|
||||
|
||||
// FinishProgressBarWithNewline is a no-op.
|
||||
func (npm *NullProgressManager) FinishProgressBarWithNewline(_ *progressbar.ProgressBar) {}
|
||||
|
||||
// UpdateProgressBar is a no-op.
|
||||
func (npm *NullProgressManager) UpdateProgressBar(_ *progressbar.ProgressBar) {}
|
||||
|
||||
// ProcessWithProgressBar executes the function for each item without progress display.
|
||||
func (npm *NullProgressManager) ProcessWithProgressBar(
|
||||
_ string,
|
||||
items []string,
|
||||
processFunc func(item string, bar *progressbar.ProgressBar),
|
||||
) {
|
||||
for _, item := range items {
|
||||
processFunc(item, nil)
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,32 @@ func (e *ConfigExporter) ExportConfig(config *internal.AppConfig, format ExportF
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedFormats returns the list of supported export formats.
|
||||
func (e *ConfigExporter) GetSupportedFormats() []ExportFormat {
|
||||
return []ExportFormat{FormatYAML, FormatJSON, FormatTOML}
|
||||
}
|
||||
|
||||
// GetDefaultOutputPath returns the default output path for a given format.
|
||||
func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, error) {
|
||||
configPath, err := internal.GetConfigPath()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(configPath)
|
||||
|
||||
switch format {
|
||||
case FormatYAML:
|
||||
return filepath.Join(dir, "config.yaml"), nil
|
||||
case FormatJSON:
|
||||
return filepath.Join(dir, "config.json"), nil
|
||||
case FormatTOML:
|
||||
return filepath.Join(dir, "config.toml"), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// exportYAML exports configuration as YAML.
|
||||
func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath string) error {
|
||||
// Create a clean config without sensitive data for export
|
||||
@@ -260,29 +286,3 @@ func (e *ConfigExporter) writeVariablesSection(file *os.File, config *internal.A
|
||||
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedFormats returns the list of supported export formats.
|
||||
func (e *ConfigExporter) GetSupportedFormats() []ExportFormat {
|
||||
return []ExportFormat{FormatYAML, FormatJSON, FormatTOML}
|
||||
}
|
||||
|
||||
// GetDefaultOutputPath returns the default output path for a given format.
|
||||
func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, error) {
|
||||
configPath, err := internal.GetConfigPath()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(configPath)
|
||||
|
||||
switch format {
|
||||
case FormatYAML:
|
||||
return filepath.Join(dir, "config.yaml"), nil
|
||||
case FormatJSON:
|
||||
return filepath.Join(dir, "config.json"), nil
|
||||
case FormatTOML:
|
||||
return filepath.Join(dir, "config.toml"), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,33 @@ func (v *ConfigValidator) ValidateField(fieldName, value string) *ValidationResu
|
||||
return result
|
||||
}
|
||||
|
||||
// DisplayValidationResult displays validation results to the user.
|
||||
func (v *ConfigValidator) DisplayValidationResult(result *ValidationResult) {
|
||||
if result.Valid {
|
||||
v.output.Success("✅ Configuration is valid")
|
||||
} else {
|
||||
v.output.Error("❌ Configuration has errors")
|
||||
}
|
||||
|
||||
// Display errors
|
||||
for _, err := range result.Errors {
|
||||
v.output.Error(" • %s: %s (value: %s)", err.Field, err.Message, err.Value)
|
||||
}
|
||||
|
||||
// Display warnings
|
||||
for _, warning := range result.Warnings {
|
||||
v.output.Warning(" ⚠️ %s: %s", warning.Field, warning.Message)
|
||||
}
|
||||
|
||||
// Display suggestions
|
||||
if len(result.Suggestions) > 0 {
|
||||
v.output.Info("\nSuggestions:")
|
||||
for _, suggestion := range result.Suggestions {
|
||||
v.output.Printf(" 💡 %s", suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateOrganization validates the organization field.
|
||||
func (v *ConfigValidator) validateOrganization(org string, result *ValidationResult) {
|
||||
if org == "" {
|
||||
@@ -478,30 +505,3 @@ func (v *ConfigValidator) isValidVariableName(name string) bool {
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// DisplayValidationResult displays validation results to the user.
|
||||
func (v *ConfigValidator) DisplayValidationResult(result *ValidationResult) {
|
||||
if result.Valid {
|
||||
v.output.Success("✅ Configuration is valid")
|
||||
} else {
|
||||
v.output.Error("❌ Configuration has errors")
|
||||
}
|
||||
|
||||
// Display errors
|
||||
for _, err := range result.Errors {
|
||||
v.output.Error(" • %s: %s (value: %s)", err.Field, err.Message, err.Value)
|
||||
}
|
||||
|
||||
// Display warnings
|
||||
for _, warning := range result.Warnings {
|
||||
v.output.Warning(" ⚠️ %s: %s", warning.Field, warning.Message)
|
||||
}
|
||||
|
||||
// Display suggestions
|
||||
if len(result.Suggestions) > 0 {
|
||||
v.output.Info("\nSuggestions:")
|
||||
for _, suggestion := range result.Suggestions {
|
||||
v.output.Printf(" 💡 %s", suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
scripts/release.sh
Normal file → Executable file
8
scripts/release.sh
Normal file → Executable file
@@ -44,13 +44,13 @@ fi
|
||||
|
||||
# Get version from command line or prompt
|
||||
VERSION="$1"
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
if [[ -z $VERSION ]]; then
|
||||
echo -n "Enter version (e.g., v1.0.0): "
|
||||
read -r VERSION
|
||||
fi
|
||||
|
||||
# Validate version format
|
||||
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
log_error "Version must be in format vX.Y.Z (e.g., v1.0.0)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -59,11 +59,11 @@ log_info "Preparing release $VERSION"
|
||||
|
||||
# Check if we're on main branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ "$CURRENT_BRANCH" != "main" ]]; then
|
||||
if [[ $CURRENT_BRANCH != "main" ]]; then
|
||||
log_warning "You're not on the main branch (current: $CURRENT_BRANCH)"
|
||||
echo -n "Continue anyway? (y/N): "
|
||||
read -r CONTINUE
|
||||
if [[ "$CONTINUE" != "y" && "$CONTINUE" != "Y" ]]; then
|
||||
if [[ $CONTINUE != "y" && $CONTINUE != "Y" ]]; then
|
||||
log_info "Aborted"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
1
testdata/composite-action/action.yml
vendored
1
testdata/composite-action/action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Composite Example Action
|
||||
description: 'Test Composite Action for gh-action-readme dependency analysis'
|
||||
inputs:
|
||||
|
||||
1
testdata/example-action/action.yml
vendored
1
testdata/example-action/action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Example Action
|
||||
description: 'Test Action for gh-action-readme'
|
||||
inputs:
|
||||
|
||||
1
testdata/example-action/config.yaml
vendored
1
testdata/example-action/config.yaml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
# Action-specific configuration
|
||||
theme: "github"
|
||||
variables:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Basic Composite Action'
|
||||
description: 'A simple composite action with basic steps'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Complex Composite Workflow'
|
||||
description: 'A complex composite action demonstrating advanced features'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Composite Action with Dependencies'
|
||||
description: 'A composite action that uses external actions'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Basic Docker Action'
|
||||
description: 'A simple Docker-based action'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Docker Action with Environment'
|
||||
description: 'Docker action with environment variables and advanced configuration'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Node.js 16 Action'
|
||||
description: 'JavaScript action running on Node.js 16'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Simple JavaScript Action'
|
||||
description: 'A simple JavaScript action for testing'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Comprehensive JavaScript Action'
|
||||
description: 'A JavaScript action with all possible fields for testing'
|
||||
author: 'Test Author <test@example.com>'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: default
|
||||
output_format: md
|
||||
repo_overrides:
|
||||
|
||||
1
testdata/yaml-fixtures/composite-action.yml
vendored
1
testdata/yaml-fixtures/composite-action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Composite Action'
|
||||
description: 'A composite action with multiple steps'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: professional
|
||||
output_format: html
|
||||
output_dir: docs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: default
|
||||
output_format: md
|
||||
output_dir: .
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: github
|
||||
output_format: md
|
||||
output_dir: docs
|
||||
|
||||
1
testdata/yaml-fixtures/docker-action.yml
vendored
1
testdata/yaml-fixtures/docker-action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Docker Action'
|
||||
description: 'A Docker-based GitHub Action'
|
||||
inputs:
|
||||
|
||||
1
testdata/yaml-fixtures/global-config.yml
vendored
1
testdata/yaml-fixtures/global-config.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: default
|
||||
output_format: md
|
||||
github_token: global-token
|
||||
|
||||
1
testdata/yaml-fixtures/minimal-action.yml
vendored
1
testdata/yaml-fixtures/minimal-action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Minimal Action'
|
||||
description: 'Minimal test action'
|
||||
runs:
|
||||
|
||||
1
testdata/yaml-fixtures/minimal-config.yml
vendored
1
testdata/yaml-fixtures/minimal-config.yml
vendored
@@ -1,2 +1,3 @@
|
||||
---
|
||||
theme: minimal
|
||||
github_token: config-token
|
||||
|
||||
1
testdata/yaml-fixtures/my-new-action.yml
vendored
1
testdata/yaml-fixtures/my-new-action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'My New Action'
|
||||
description: 'A brand new GitHub Action'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: professional
|
||||
output_format: html
|
||||
output_dir: docs
|
||||
|
||||
1
testdata/yaml-fixtures/repo-config.yml
vendored
1
testdata/yaml-fixtures/repo-config.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
theme: github
|
||||
output_format: md
|
||||
output_dir: docs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
scenarios:
|
||||
# JavaScript Action Scenarios
|
||||
- id: "simple-javascript"
|
||||
|
||||
1
testdata/yaml-fixtures/simple-action.yml
vendored
1
testdata/yaml-fixtures/simple-action.yml
vendored
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Simple Action'
|
||||
description: 'A simple GitHub Action for testing'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Test Composite Action'
|
||||
description: 'Test action for update testing'
|
||||
runs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: 'Test Project Action'
|
||||
description: 'A GitHub Action for testing project functionality'
|
||||
inputs:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
name: Test Action
|
||||
description: A test action
|
||||
runs:
|
||||
|
||||
Reference in New Issue
Block a user