diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ac35121..16f9bf5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,44 +1,44 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/go { - "name": "Go", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", - "customizations": { - "vscode": { - "extensions": [ - "GitHub.codespaces", - "github.vscode-github-actions", - "GitHub.copilot", - "GitHub.copilot-chat", - "github.copilot-workspace", - "GitHub.vscode-pull-request-github", - "GitHub.remotehub", - "golang.Go" - ] - } - }, - "tasks": { - "build": "go build .", - "test": "go test ./...", - "run": "go run ." - }, + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", + "customizations": { + "vscode": { + "extensions": [ + "GitHub.codespaces", + "github.vscode-github-actions", + "GitHub.copilot", + "GitHub.copilot-chat", + "github.copilot-workspace", + "GitHub.vscode-pull-request-github", + "GitHub.remotehub", + "golang.Go" + ] + } + }, + "tasks": { + "build": "go build .", + "test": "go test ./...", + "run": "go run ." + }, - // Features to add to the dev container. More info: https://containers.dev/features. + // Features to add to the dev container. More info: https://containers.dev/features. - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {} - }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "go install -v golang.org/x/tools/cmd/goimports@latest" + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "go install -v golang.org/x/tools/cmd/goimports@latest" - // Configure tool-specific properties. - // "customizations": {}, + // Configure tool-specific properties. + // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2d76a1b..d6ab863 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,9 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + cooldown: + default-days: 7 + semver-major-days: 7 allow: - dependency-type: "direct" - dependency-type: "indirect" @@ -20,6 +23,8 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" + cooldown: + default-days: 7 groups: github-actions: patterns: diff --git a/.github/instructions/dependabot.instructions.md b/.github/instructions/dependabot.instructions.md new file mode 100644 index 0000000..8655a2e --- /dev/null +++ b/.github/instructions/dependabot.instructions.md @@ -0,0 +1,196 @@ +--- +applyTo: "**/*dependabot.yml" +description: Dependabot configuration patterns and best practices +--- + +# Dependabot Configuration + +Guidelines for configuring Dependabot version updates. + +## Basic Structure + +```yaml +version: 2 +updates: + - package-ecosystem: "gomod" # or npm, pip, docker, github-actions, etc. + directory: "/" + schedule: + interval: "weekly" # daily, weekly, or monthly +``` + +## Cooldown Settings + +Configure cooldown periods to delay updates until packages have matured. This helps avoid churn from rapid releases. Cooldown only applies to version updates, not security updates. + +```yaml +cooldown: + default-days: 7 # Default cooldown for all updates + semver-major-days: 7 # Major version updates wait 7 days + semver-minor-days: 3 # Minor version updates wait 3 days + semver-patch-days: 1 # Patch version updates wait 1 day + include: + - "some-package*" # Only apply cooldown to matching packages + exclude: + - "critical-pkg*" # Skip cooldown for these packages +``` + +**Parameters:** + +| Parameter | Description | +| ------------------- | ---------------------------------------------------------------- | +| `default-days` | Default cooldown period for all dependencies | +| `semver-major-days` | Cooldown for major version updates | +| `semver-minor-days` | Cooldown for minor version updates | +| `semver-patch-days` | Cooldown for patch version updates | +| `include` | List of dependencies to apply cooldown (supports wildcards) | +| `exclude` | List of dependencies excluded from cooldown (supports wildcards) | + +**Notes:** + +- If semver-specific days aren't defined, `default-days` is used +- `exclude` takes precedence over `include` +- Security updates automatically bypass cooldown + +### SemVer Cooldown Support by Ecosystem + +**IMPORTANT:** The `semver-major-days`, `semver-minor-days`, and `semver-patch-days` options are NOT supported by all package ecosystems. For unsupported ecosystems, use only `default-days`. + +| Ecosystem | SemVer Cooldown Supported | +| ---------------- | ------------------------------- | +| `gomod` | ✅ Yes | +| `npm` | ✅ Yes | +| `pip` | ✅ Yes | +| `bundler` | ✅ Yes | +| `cargo` | ✅ Yes | +| `composer` | ✅ Yes | +| `maven` | ✅ Yes | +| `gradle` | ✅ Yes | +| `nuget` | ✅ Yes | +| `docker` | ✅ Yes | +| `github-actions` | ❌ No - use `default-days` only | +| `gitsubmodule` | ❌ No - use `default-days` only | +| `terraform` | ❌ No - use `default-days` only | + +**Example for github-actions (no semver support):** + +```yaml +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 # Only default-days is supported +``` + +## Grouping Updates + +Group related dependencies into single PRs to reduce noise: + +```yaml +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + groups: + go-dependencies: + patterns: + - "*" # Group all Go dependencies +``` + +You can create multiple groups with specific patterns: + +```yaml +groups: + aws-sdk: + patterns: + - "github.com/aws/*" + testing: + patterns: + - "*test*" + - "*mock*" + dependency-type: "development" +``` + +Group parameters: + +- `patterns`: Include dependencies matching these patterns +- `exclude-patterns`: Exclude dependencies matching these patterns +- `dependency-type`: Limit to `development` or `production` +- `update-types`: Limit to `minor`, `patch`, or `major` + +## Filtering Dependencies + +### Allow specific dependency types + +```yaml +allow: + - dependency-type: "direct" # Only direct dependencies + - dependency-type: "indirect" # Include transitive dependencies + - dependency-type: "all" # All dependencies +``` + +### Ignore specific dependencies + +```yaml +ignore: + - dependency-name: "lodash" + versions: ["4.x"] # Ignore lodash 4.x updates + - dependency-name: "aws-sdk" + update-types: ["version-update:semver-major"] # Ignore major updates +``` + +## Common Ecosystems + +| Ecosystem | `package-ecosystem` value | +| -------------- | ------------------------- | +| Go modules | `gomod` | +| npm/Yarn | `npm` | +| Python pip | `pip` | +| Docker | `docker` | +| GitHub Actions | `github-actions` | +| Terraform | `terraform` | +| Cargo (Rust) | `cargo` | +| NuGet (.NET) | `nuget` | + +## Complete Example + +```yaml +version: 2 +updates: + # Go modules - supports semver cooldown + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + semver-major-days: 7 + allow: + - dependency-type: "direct" + - dependency-type: "indirect" + groups: + go-dependencies: + patterns: + - "*" + + # GitHub Actions - does NOT support semver cooldown + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + groups: + github-actions: + patterns: + - "*" +``` + +--- + +## References + +- [Dependabot Configuration Options](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) - Full configuration reference +- [Supported Ecosystems](https://docs.github.com/en/code-security/dependabot/ecosystems-supported-by-dependabot/supported-ecosystems-and-repositories) - List of supported package ecosystems +- [Optimizing PR Creation](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates) - Cooldown and grouping strategies diff --git a/.github/instructions/github-actions.instructions.md b/.github/instructions/github-actions.instructions.md new file mode 100644 index 0000000..6e9a5ca --- /dev/null +++ b/.github/instructions/github-actions.instructions.md @@ -0,0 +1,176 @@ +--- +applyTo: ".github/workflows/**/*.{yml,yaml}" +description: GitHub Actions workflow development patterns and security recommended practices +--- + +# GitHub Actions Workflow Development + +Guidelines for secure and maintainable GitHub Actions workflows. + +## Security Best Practices + +### Minimal Permissions + +```yaml +# ✅ Minimal at workflow level +permissions: + contents: read + +# Increase per-job only when needed +jobs: + deploy: + permissions: + contents: read + deployments: write +``` + +### SHA Pinning (CRITICAL) + +**Always pin third-party actions to full commit SHA and use the latest release:** + +```yaml +# ✅ Pin to commit SHA - immutable and secure +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +``` + +**Before submitting workflow changes:** + +1. **Find Latest Release**: Check the action's releases page (e.g., `https://github.com/actions/checkout/releases`) +2. **Get Commit SHA**: Use GitHub MCP tools or the releases page to get the full 40-character commit SHA +3. **Include Version Comment**: Always include the version tag as a trailing comment (e.g., `# v6.0.2`) + +### Credential Security + +**Always set `persist-credentials: false` on checkout actions:** + +```yaml +# ✅ Secure - credentials not persisted +- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false +``` + +This prevents credentials from being stored in the Git config. + +### Script Injection Prevention + +```yaml +# ✅ Safe - environment variable +- name: Check PR title + env: + TITLE: ${{ github.event.pull_request.title }} + run: | + if [[ "$TITLE" =~ ^feat ]]; then + echo "Valid feature PR" + fi + +# ❌ Unsafe - direct interpolation +- run: | + if [[ "${{ github.event.pull_request.title }}" =~ ^feat ]]; then +``` + +### Secrets Handling + +```yaml +# ✅ Reference secrets properly +env: + API_KEY: ${{ secrets.API_KEY }} + +# Mask generated sensitive values +- run: | + TOKEN=$(generate-token) + echo "::add-mask::$TOKEN" + echo "TOKEN=$TOKEN" >> $GITHUB_ENV +``` + +## Workflow Structure + +### YAML Document Start + +Always begin workflow files with `---` for proper YAML parsing: + +```yaml +--- +name: CI +on: + push: + branches: [main] +``` + +### Environment Consistency + +**Prefer configuration files over hardcoded versions:** + +```yaml +# Go projects - use go.mod +- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + with: + go-version-file: go.mod + +# Node.js projects - use .node-version or .nvmrc +- uses: actions/setup-node@v4 + with: + node-version-file: ".node-version" +``` + +### Conditional Execution + +```yaml +# Run only on main +- run: ./deploy.sh + if: github.ref == 'refs/heads/main' + +# Continue on error +- run: ./optional-step.sh + continue-on-error: true + +# Run even if previous failed +- run: ./cleanup.sh + if: always() +``` + +## Verification Checklist (MANDATORY) + +Before finalizing any workflow changes: + +1. **Verify Each Action's Commit SHA:** + - Use GitHub MCP tools (`get_commit` with the tag) to retrieve the correct 40-character SHA + - **Do NOT assume** the SHA in comments or existing workflows is correct + - **Always use the latest release** - check the releases page for each action + +2. **Validate Action Existence:** + - After updating SHAs, confirm the action exists at that commit + - If the commit lookup fails or returns 404, the SHA is invalid + +3. **Security Checks:** + - Ensure `persist-credentials: false` is set on all checkout actions + - Verify minimal permissions are configured at workflow level + - Check that no secrets are directly interpolated in run commands + +4. **Test YAML Syntax:** + - Verify the workflow has valid YAML syntax + - Ensure the file starts with `---` + +## Before Making Changes + +1. Check existing `.github/workflows/` for established patterns +2. Check `.github/dependabot.yml` for dependency automation settings +3. Verify action versions via releases pages using GitHub MCP tools +4. Consider CI time and complexity trade-offs + +## Keeping Instructions Up-to-Date + +**IMPORTANT**: When making changes to workflow files that introduce new patterns or security practices, update this instruction file to reflect those changes. + +--- + +## Antipatterns + +| Antipattern | Why It's Problematic | Better Approach | +| ---------------------------- | -------------------------------------------- | ------------------------------------------- | +| Using version tags (`v4`) | Tags can be moved/deleted; supply chain risk | Pin to full 40-char commit SHA | +| Direct string interpolation | Script injection vulnerability | Use environment variables | +| Workflow-level `write` perms | Excessive access if job compromised | Minimal perms at workflow, increase per-job | +| Hardcoded language versions | Drift between local and CI | Use version files (go.mod, .node-version) | +| Assuming SHA validity | Outdated SHAs break workflows | Verify SHA against latest release | +| Missing YAML document start | Parser warnings and inconsistency | Always start with `---` | diff --git a/.github/linters/.textlintignore b/.github/linters/.textlintignore new file mode 100644 index 0000000..5fb5513 --- /dev/null +++ b/.github/linters/.textlintignore @@ -0,0 +1,2 @@ +# Ignore instruction files that contain technical terms in code examples +.github/instructions/**/*.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a9c909..01663dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,4 @@ +--- # .github/workflows/build.yml name: Build @@ -15,10 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: go.mod @@ -32,7 +35,7 @@ jobs: go tool cover -func=coverage.out > coverage.txt - name: Upload Coverage Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: coverage-report path: | diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2a52548..20ce7e5 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,3 +1,4 @@ +--- name: Lint Code Base permissions: @@ -19,17 +20,18 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + persist-credentials: false - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: go.mod - name: Run Super-Linter - uses: super-linter/super-linter/slim@5119dcd8011e92182ce8219d9e9efc82f16fddb6 # v8.0.0 + uses: super-linter/super-linter/slim@d5b0a2ab116623730dd094f15ddc1b6b25bf7b99 # v8.3.2 env: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: "main" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2df551f..34e9550 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,4 @@ +--- name: release on: push: @@ -12,7 +13,9 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: cli/gh-extension-precompile@9e2237c30f869ad3bcaed6a4be2cd43564dd421b # v2.1.0 with: release_android: true diff --git a/.textlintignore b/.textlintignore new file mode 100644 index 0000000..5fb5513 --- /dev/null +++ b/.textlintignore @@ -0,0 +1,2 @@ +# Ignore instruction files that contain technical terms in code examples +.github/instructions/**/*.md diff --git a/.vscode/settings.json b/.vscode/settings.json index ee878d0..e8702bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,19 +1,19 @@ { - // Use golangci-lint for linting - "go.lintTool": "golangci-lint", + // Use golangci-lint for linting + "go.lintTool": "golangci-lint", - // Configure linter flags - "go.lintFlags": ["--fast"], + // Configure linter flags + "go.lintFlags": ["--fast"], - "go.formatTool": "goimports", - "go.useLanguageServer": true, - "go.testOnSave": true, + "go.formatTool": "goimports", + "go.useLanguageServer": true, + "go.testOnSave": true, - // Editor settings optimized for Go development - "[go]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "always" - } - } + // Editor settings optimized for Go development + "[go]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } + } } diff --git a/go.mod b/go.mod index f5bff99..8b5b340 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/github/gh-skyline -go 1.24.1 +go 1.25.0 require ( - github.com/cli/go-gh/v2 v2.12.2 + github.com/cli/go-gh/v2 v2.13.0 github.com/fogleman/gg v1.3.0 - github.com/spf13/cobra v1.9.1 + github.com/spf13/cobra v1.10.1 ) require ( @@ -18,15 +18,15 @@ require ( github.com/henvic/httpretty v0.1.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/thlib/go-timezone-local v0.0.7 // indirect - golang.org/x/image v0.30.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/image v0.32.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e091470..26e473e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cli/go-gh/v2 v2.12.2 h1:EtocmDAH7dKrH2PscQOQVo7PbFD5G6uYx4rSKY2w1SY= -github.com/cli/go-gh/v2 v2.12.2/go.mod h1:g2IjwHEo27fgItlS9wUbRaXPYurZEXPp1jrxf3piC6g= +github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys= +github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= @@ -28,8 +28,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= @@ -42,27 +42,26 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII= github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= -golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= -golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/ascii/generator.go b/internal/ascii/generator.go index dad02ec..3c18211 100644 --- a/internal/ascii/generator.go +++ b/internal/ascii/generator.go @@ -56,15 +56,21 @@ func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, sortedDays, nonZeroCount := sortContributionDays(week, now) // Fill the column for this week - for dayIdx, day := range sortedDays { + // Limit iteration to valid asciiGrid indices (max 7 rows for days of week) + maxDayIdx := len(asciiGrid) + if len(sortedDays) < maxDayIdx { + maxDayIdx = len(sortedDays) + } + for dayIdx := 0; dayIdx < maxDayIdx; dayIdx++ { + day := sortedDays[dayIdx] if day.ContributionCount == -1 { - asciiGrid[dayIdx][weekIdx] = FutureBlock + asciiGrid[dayIdx][weekIdx] = FutureBlock // #nosec G602 -- bounds checked by maxDayIdx calculation above } else { normalized := 0.0 if maxContributions != 0 { normalized = float64(day.ContributionCount) / float64(maxContributions) } - asciiGrid[dayIdx][weekIdx] = getBlock(normalized, dayIdx, nonZeroCount) + asciiGrid[dayIdx][weekIdx] = getBlock(normalized, dayIdx, nonZeroCount) // #nosec G602 -- bounds checked by maxDayIdx calculation above } } } diff --git a/internal/errors/errors.go b/internal/errors/errors.go index fc52f8a..06665ba 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,4 +1,6 @@ // Package errors provides custom error types and utilities for the Skyline application. +// +//nolint:revive // Package name is intentional for clear error semantics within this project package errors import ( diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index 05ee43e..1613011 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -1,20 +1,22 @@ -package errors +package errors_test import ( "errors" "testing" + + skylineerrors "github.com/github/gh-skyline/internal/errors" ) func TestSkylineError_Error(t *testing.T) { tests := []struct { name string - err *SkylineError + err *skylineerrors.SkylineError want string }{ { name: "error with underlying error", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, Message: "invalid input", Err: errors.New("value out of range"), }, @@ -22,8 +24,8 @@ func TestSkylineError_Error(t *testing.T) { }, { name: "error without underlying error", - err: &SkylineError{ - Type: STLError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.STLError, Message: "failed to process STL", }, want: "[STL] failed to process STL", @@ -61,8 +63,8 @@ func TestWrap(t *testing.T) { }, { name: "wrap SkylineError preserves type", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, Message: "original message", Err: errors.New("base error"), }, @@ -73,7 +75,7 @@ func TestWrap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := Wrap(tt.err, tt.message) + got := skylineerrors.Wrap(tt.err, tt.message) if tt.wantNil { if got != nil { t.Errorf("Wrap() = %v, want nil", got) @@ -93,21 +95,21 @@ func TestWrap(t *testing.T) { func TestNew(t *testing.T) { tests := []struct { name string - errType ErrorType + errType skylineerrors.ErrorType message string err error want string }{ { name: "new error without underlying error", - errType: ValidationError, + errType: skylineerrors.ValidationError, message: "validation failed", err: nil, want: "[VALIDATION] validation failed", }, { name: "new error with underlying error", - errType: NetworkError, + errType: skylineerrors.NetworkError, message: "network timeout", err: errors.New("connection refused"), want: "[NETWORK] network timeout: connection refused", @@ -116,7 +118,7 @@ func TestNew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := New(tt.errType, tt.message, tt.err) + got := skylineerrors.New(tt.errType, tt.message, tt.err) if got.Error() != tt.want { t.Errorf("New() error = %v, want %v", got.Error(), tt.want) } @@ -136,34 +138,34 @@ func TestNew(t *testing.T) { func TestSkylineError_Is(t *testing.T) { tests := []struct { name string - err *SkylineError + err *skylineerrors.SkylineError target error want bool }{ { name: "matching error types", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, }, - target: &SkylineError{ - Type: ValidationError, + target: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, }, want: true, }, { name: "different error types", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, }, - target: &SkylineError{ - Type: NetworkError, + target: &skylineerrors.SkylineError{ + Type: skylineerrors.NetworkError, }, want: false, }, { name: "non-SkylineError target", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, }, target: errors.New("standard error"), want: false, @@ -183,13 +185,13 @@ func TestSkylineError_Unwrap(t *testing.T) { baseErr := errors.New("base error") tests := []struct { name string - err *SkylineError + err *skylineerrors.SkylineError wantErr error }{ { name: "with underlying error", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, Message: "test message", Err: baseErr, }, @@ -197,8 +199,8 @@ func TestSkylineError_Unwrap(t *testing.T) { }, { name: "without underlying error", - err: &SkylineError{ - Type: ValidationError, + err: &skylineerrors.SkylineError{ + Type: skylineerrors.ValidationError, Message: "test message", }, wantErr: nil,