mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-08-07 18:23:30 +00:00
complete
This commit is contained in:
parent
07366a6d1e
commit
c143000184
37
.editorconfig
Normal file
37
.editorconfig
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# 2 space indentation
|
||||||
|
[*.{js,json,jsonc,yml,yaml,md,sh}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# Tab indentation (no size specified)
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Matches the exact files
|
||||||
|
[{Makefile,makefile,*.mk}]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
# Shell scripts
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 100
|
||||||
|
|
||||||
|
# Markdown files
|
||||||
|
[*.md]
|
||||||
|
max_line_length = 100
|
||||||
|
trim_trailing_whitespace = false
|
||||||
2
.env
2
.env
|
|
@ -1 +1,3 @@
|
||||||
GO111MODULE=auto
|
GO111MODULE=auto
|
||||||
|
GO_TOOLCHAIN=go1.23.5
|
||||||
|
GO_VERSION=1.23.5
|
||||||
|
|
|
||||||
63
.github/ISSUE_TEMPLATE/bug.md
vendored
63
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -1,63 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Create a report to help us improve or fix the action
|
|
||||||
title: "[BUG] "
|
|
||||||
labels: bug
|
|
||||||
assignees: 'awalsh128'
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Note**: Please read about the limitation of [non-file dependencies](https://github.com/awalsh128/cache-apt-pkgs-action/blob/master/README.md#non-file-dependencies) before filing an issue.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
|
|
||||||
### 1. Workflow Configuration
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Replace with your workflow
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Package List
|
|
||||||
|
|
||||||
```plaintext
|
|
||||||
# List your packages here
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Environment Details
|
|
||||||
|
|
||||||
- Runner OS: [e.g., Ubuntu 22.04]
|
|
||||||
- Action version: [e.g., v2.0.0]
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
## Actual Behavior
|
|
||||||
|
|
||||||
What actually happened? Please include:
|
|
||||||
|
|
||||||
- Error messages
|
|
||||||
- Action logs
|
|
||||||
- Cache status (hit/miss)
|
|
||||||
|
|
||||||
## Debug Information
|
|
||||||
|
|
||||||
If possible, please run the action with debug mode enabled:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
with:
|
|
||||||
debug: true
|
|
||||||
```
|
|
||||||
|
|
||||||
And provide the debug output.
|
|
||||||
|
|
||||||
## Additional Context
|
|
||||||
|
|
||||||
- Are you using any specific package versions?
|
|
||||||
- Are there any special package configurations?
|
|
||||||
- Does the issue happen consistently or intermittently?
|
|
||||||
- Have you tried clearing the cache and retrying?
|
|
||||||
71
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
71
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Create a report to help us improve or fix the action
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: bug
|
||||||
|
assignees: awalsh128
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Reproduction Steps
|
||||||
|
|
||||||
|
### Workflow Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Paste your workflow configuration here
|
||||||
|
steps:
|
||||||
|
- name: Cache apt packages
|
||||||
|
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
|
with:
|
||||||
|
packages: # your packages
|
||||||
|
version: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package List
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# List the packages you're trying to cache
|
||||||
|
# Example: curl wget git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
- **Runner OS**: (e.g., ubuntu-22.04, ubuntu-20.04)
|
||||||
|
- **Action version**: (e.g., v1.4.2, latest)
|
||||||
|
- **Repository**: (if relevant)
|
||||||
|
|
||||||
|
## Expected vs Actual Behavior
|
||||||
|
|
||||||
|
**Expected**: What you expected to happen
|
||||||
|
|
||||||
|
**Actual**: What actually happened
|
||||||
|
|
||||||
|
## Logs and Error Messages
|
||||||
|
|
||||||
|
```txt
|
||||||
|
# Paste relevant logs, error messages, or debug output here
|
||||||
|
# Enable debug mode by adding: debug: true to your workflow step
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache Status
|
||||||
|
|
||||||
|
- [ ] Cache hit
|
||||||
|
- [ ] Cache miss
|
||||||
|
- [ ] Cache creation failed
|
||||||
|
- [ ] Other (please specify)
|
||||||
|
|
||||||
|
## Additional Information
|
||||||
|
|
||||||
|
- Does this happen consistently or intermittently?
|
||||||
|
- Have you tried clearing the cache?
|
||||||
|
- Are you using any specific package versions or configurations?
|
||||||
|
- Any relevant system dependencies?
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have read the [non-file dependencies limitation](https://github.com/awalsh128/cache-apt-pkgs-action/blob/master/README.md#non-file-dependencies)
|
||||||
|
- [ ] I have searched existing issues for duplicates
|
||||||
|
- [ ] I have provided all requested information above
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Documentation
|
||||||
|
url: https://github.com/awalsh128/cache-apt-pkgs-action/blob/master/README.md
|
||||||
|
about: Please check the documentation before filing an issue
|
||||||
|
- name: Discussions
|
||||||
|
url: https://github.com/awalsh128/cache-apt-pkgs-action/discussions
|
||||||
|
about: Ask questions and discuss ideas with the community
|
||||||
55
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
55
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest an idea or enhancement for this action
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: enhancement
|
||||||
|
assignees: awalsh128
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Summary
|
||||||
|
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
What problem would this feature solve? Is your feature request related to a problem you're experiencing?
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Example: I'm frustrated when [specific scenario] because [reason]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
Describe the solution you'd like to see implemented.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
Describe any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Use Case
|
||||||
|
|
||||||
|
Describe your specific use case and how this feature would benefit you and others.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Example workflow showing how the feature would be used
|
||||||
|
steps:
|
||||||
|
- name: Cache apt packages
|
||||||
|
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||||
|
with:
|
||||||
|
packages: curl wget
|
||||||
|
# new-feature: value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
If you have ideas about how this could be implemented, please share them here.
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
Add any other context, screenshots, or examples about the feature request here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions for similar requests
|
||||||
|
- [ ] This feature would benefit the broader community, not just my specific use case
|
||||||
|
- [ ] I have provided a clear use case and rationale
|
||||||
57
.github/workflows/action_tests.yml
vendored
57
.github/workflows/action_tests.yml
vendored
|
|
@ -1,45 +1,31 @@
|
||||||
name: Test Action
|
name: Test Action
|
||||||
|
|
||||||
on:
|
permissions:
|
||||||
# Manual trigger with specific ref
|
contents: read
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
env:
|
||||||
ref:
|
DEBUG: false
|
||||||
description: Branch, tag, SHA to test (for PRs use pull/ID/head)
|
|
||||||
required: true
|
on:
|
||||||
type: string
|
# Manual trigger (no inputs allowed per Trunk rule)
|
||||||
debug:
|
workflow_dispatch:
|
||||||
description: Enable debug logging
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
|
|
||||||
# Automatic triggers
|
|
||||||
push:
|
push:
|
||||||
branches: [dev-v2.0] # Test on pushes to dev branch
|
branches: [dev-v2.0] # Test on pushes to dev branch
|
||||||
paths:
|
paths:
|
||||||
- cmd/** # Only when action code changes
|
- cmd/** # Only when action code changes
|
||||||
- internal/** # Only when action code changes
|
- internal/** # Only when action code changes
|
||||||
- action.yml
|
- action.yml
|
||||||
- .github/workflows/action_tests.yml
|
- .github/workflows/action_tests.yml
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [dev-v2.0] # Test on PRs to dev branch
|
branches: [dev-v2.0] # Test on PRs to dev branch
|
||||||
paths:
|
paths:
|
||||||
- cmd/** # Only when action code changes
|
- cmd/** # Only when action code changes
|
||||||
- internal/** # Only when action code changes
|
- internal/** # Only when action code changes
|
||||||
- action.yml
|
- action.yml
|
||||||
- .github/workflows/action_tests.yml
|
- .github/workflows/action_tests.yml
|
||||||
|
|
||||||
# Environment configuration
|
|
||||||
env:
|
|
||||||
DEBUG: ${{ github.event.inputs.debug || false }}
|
|
||||||
# Test for overrides in built in shell options (regression issue 98).
|
|
||||||
SHELLOPTS: errexit:pipefail
|
|
||||||
# Use PR's SHA when testing a PR, otherwise use the ref provided
|
|
||||||
TEST_REF: ${{ github.event.pull_request.head.sha || github.event.inputs.ref || github.ref }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
list_all_versions:
|
list_all_versions:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: List all package versions (including deps).
|
name: List all package versions (including deps).
|
||||||
|
|
@ -48,7 +34,8 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TEST_REF }}
|
# Use the event ref/sha by default; do not accept user-controlled ref inputs
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
# Run the action from the checked out code
|
# Run the action from the checked out code
|
||||||
- name: Execute
|
- name: Execute
|
||||||
|
|
@ -57,7 +44,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
packages: xdot=1.3-1
|
packages: xdot=1.3-1
|
||||||
version: ${{ github.run_id }}-${{ github.run_attempt }}-list_all_versions
|
version: ${{ github.run_id }}-${{ github.run_attempt }}-list_all_versions
|
||||||
debug: ${{ env.DEBUG }}
|
debug: "false"
|
||||||
- name: Verify
|
- name: Verify
|
||||||
if: |
|
if: |
|
||||||
steps.execute.outputs.cache-hit != 'false' ||
|
steps.execute.outputs.cache-hit != 'false' ||
|
||||||
|
|
@ -82,7 +69,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
packages: xdot rolldice
|
packages: xdot rolldice
|
||||||
version: ${{ github.run_id }}-${{ github.run_attempt }}-list_versions
|
version: ${{ github.run_id }}-${{ github.run_attempt }}-list_versions
|
||||||
debug: ${{ env.DEBUG }}
|
debug: "false"
|
||||||
- name: Verify
|
- name: Verify
|
||||||
if: steps.execute.outputs.cache-hit != 'false' || steps.execute.outputs.package-version-list != 'rolldice=1.16-1build3,xdot=1.3-1'
|
if: steps.execute.outputs.cache-hit != 'false' || steps.execute.outputs.package-version-list != 'rolldice=1.16-1build3,xdot=1.3-1'
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -104,7 +91,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
packages: xdot rolldice
|
packages: xdot rolldice
|
||||||
version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow
|
version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow
|
||||||
debug: ${{ env.DEBUG }}
|
debug: "false"
|
||||||
- name: Verify
|
- name: Verify
|
||||||
if: steps.execute.outputs.cache-hit != 'false'
|
if: steps.execute.outputs.cache-hit != 'false'
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -124,7 +111,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
packages: xdot rolldice
|
packages: xdot rolldice
|
||||||
version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow_install_with_new_version
|
version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow_install_with_new_version
|
||||||
debug: ${{ env.DEBUG }}
|
debug: "false"
|
||||||
- name: Verify
|
- name: Verify
|
||||||
if: steps.execute.outputs.cache-hit != 'false'
|
if: steps.execute.outputs.cache-hit != 'false'
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -290,7 +277,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
packages: xdot
|
packages: xdot
|
||||||
version: ${{ github.run_id }}-${{ github.run_attempt }}-list-all-package-versions
|
version: ${{ github.run_id }}-${{ github.run_attempt }}-list-all-package-versions
|
||||||
debug: ${{ env.DEBUG }}
|
debug: false
|
||||||
|
|
||||||
regression_72_1:
|
regression_72_1:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
|
|
@ -1,36 +1,36 @@
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [dev-v2.0]
|
branches: [dev-v2.0]
|
||||||
tags: ['v*'] # Trigger on version tags
|
tags: [v*] # Trigger on version tags
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [dev-v2.0]
|
branches: [dev-v2.0]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: 0 0 * * * # Run at 00:00 UTC every day
|
- cron: 0 0 * * * # Run at 00:00 UTC every day
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
debug:
|
|
||||||
description: Run in debug mode.
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEBUG: ${{ github.event.inputs.debug || false }}
|
DEBUG: false
|
||||||
SHELLOPTS: errexit:pipefail
|
SHELLOPTS: errexit:pipefail
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Read Go version from .env
|
||||||
uses: actions/setup-go@v5
|
id: go-version
|
||||||
|
run: |
|
||||||
|
echo "go_version=$(grep '^GO_VERSION=' .env | cut -d '=' -f2)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.21"
|
go-version: ${{ steps.go-version.outputs.go_version }}
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install Go module dependencies
|
- name: Install Go module dependencies
|
||||||
|
|
@ -39,19 +39,6 @@ jobs:
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v ./...
|
run: go build -v ./...
|
||||||
|
|
||||||
- name: Check file encodings
|
|
||||||
run: |
|
|
||||||
./scripts/check_utf8.sh
|
|
||||||
|
|
||||||
- name: Check file encoding changes
|
|
||||||
id: git-check
|
|
||||||
run: |
|
|
||||||
if [[ -n "$(git status --porcelain)" ]]; then
|
|
||||||
echo "::error::Some files are not in UTF-8 encoding. Please run ./scripts/check_utf8.sh locally and commit the changes."
|
|
||||||
git status --porcelain
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: trunk.io Lint
|
- name: trunk.io Lint
|
||||||
uses: trunk-io/trunk-action@v1
|
uses: trunk-io/trunk-action@v1
|
||||||
with:
|
with:
|
||||||
|
|
@ -113,6 +100,3 @@ jobs:
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
45
.github/workflows/common.yml
vendored
Normal file
45
.github/workflows/common.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# TODO finish debugging check_and_fix_env.sh to start using this
|
||||||
|
modify-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Check and fix PR
|
||||||
|
env:
|
||||||
|
REQUIRES_FIX: ""
|
||||||
|
SAFE_HEAD_REF: ${{ github.head_ref }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ env.SAFE_HEAD_REF }}
|
||||||
|
|
||||||
|
- name: Check and fix if needed
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./scripts/check_and_fix_env.sh
|
||||||
|
status=$?
|
||||||
|
echo "REQUIRES_FIX=$status" >> $GITHUB_ENV
|
||||||
|
if [[ "$status" != "0" ]]; then
|
||||||
|
echo "$status changes were made, applying fix."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
shell: bash
|
||||||
|
if: env.REQUIRES_FIX != '0'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add .
|
||||||
|
git commit -m "Automated update from \"Check and fix PR\" workflow" || echo "No changes to commit"
|
||||||
|
git push origin HEAD:"${SAFE_HEAD_REF}"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SAFE_HEAD_REF: ${{ env.SAFE_HEAD_REF }}
|
||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
|
|
@ -1,14 +1,12 @@
|
||||||
name: Release Version
|
name: Release Version
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['v2.*'] # Trigger on version tags >= 2.0.0
|
tags: [v2.*] # Trigger on version tags >= 2.0.0
|
||||||
workflow_dispatch:
|
# manual dispatch removed to comply with Trunk rule (use push tags instead)
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version tag to update on pkg.go.dev (e.g. v2.0.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-pkg-go-dev:
|
update-pkg-go-dev:
|
||||||
|
|
@ -16,7 +14,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Update pkg.go.dev
|
- name: Update pkg.go.dev
|
||||||
run: |
|
run: |
|
||||||
VERSION=${{ github.ref_name || github.event.inputs.version }}
|
VERSION=${{ github.ref_name || github.sha }}
|
||||||
echo "Updating pkg.go.dev for version $VERSION"
|
echo "Updating pkg.go.dev for version $VERSION"
|
||||||
curl -i https://proxy.golang.org/github.com/awalsh128/cache-apt-pkgs-action/@v/$VERSION.info
|
curl -i https://proxy.golang.org/github.com/awalsh128/cache-apt-pkgs-action/@v/$VERSION.info
|
||||||
# Trigger a package load
|
# Trigger a package load
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,8 +1,8 @@
|
||||||
src/cmd/apt_query/apt_query*
|
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Don't ignore the main .env file
|
# Don't ignore the main .env file
|
||||||
!.env
|
!.env
|
||||||
|
|
||||||
|
scripts/sandbox.sh
|
||||||
|
|
@ -1,87 +1,81 @@
|
||||||
version: "2"
|
version: "2"
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt # formats Go code
|
||||||
|
- goimports # formats imports and does everything that gofmt does
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- asasalint # checks for pass []any as any in variadic func(...any)
|
- asasalint # checks for pass []any as any in variadic func(...any)
|
||||||
- asciicheck # checks that your code does not contain non-ASCII identifiers
|
- asciicheck # checks that your code does not contain non-ASCII identifiers
|
||||||
- bidichk # checks for dangerous unicode character sequences
|
- bidichk # checks for dangerous unicode character sequences
|
||||||
- bodyclose # checks whether HTTP response body is closed successfully
|
- bodyclose # checks whether HTTP response body is closed successfully
|
||||||
- containedctx # detects struct contained context.Context field
|
- containedctx # detects struct contained context.Context field
|
||||||
- contextcheck # checks the function whether use a non-inherited context
|
- contextcheck # checks the function whether use a non-inherited context
|
||||||
- cyclop # checks function and package cyclomatic complexity
|
- cyclop # checks function and package cyclomatic complexity
|
||||||
- decorder # checks declaration order and count of types, constants, variables and functions
|
- decorder # checks declaration order and count of types, constants, variables and functions
|
||||||
- dogsled # checks assignments with too many blank identifiers
|
- dogsled # checks assignments with too many blank identifiers
|
||||||
- dupl # checks code clone duplication
|
- dupl # checks code clone duplication
|
||||||
- durationcheck # checks for two durations multiplied together
|
- durationcheck # checks for two durations multiplied together
|
||||||
- errcheck # checks unchecked errors
|
- errcheck # checks unchecked errors
|
||||||
- errchkjson # checks types passed to encoding/json functions
|
- errchkjson # checks types passed to encoding/json functions
|
||||||
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
|
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
|
||||||
- errorlint # finds code that will cause problems with the error wrapping scheme
|
- errorlint # finds code that will cause problems with the error wrapping scheme
|
||||||
- execinquery # checks query string in Query function which reads your Go src files and warning it finds
|
- exhaustive # checks exhaustiveness of enum switch statements
|
||||||
- exhaustive # checks exhaustiveness of enum switch statements
|
|
||||||
- exportloopref # checks for pointers to enclosing loop variables
|
|
||||||
- forcetypeassert # finds forced type assertions
|
- forcetypeassert # finds forced type assertions
|
||||||
- funlen # checks for long functions
|
- funlen # checks for long functions
|
||||||
- gocheckcompilerdirectives # validates go compiler directive comments
|
- gocheckcompilerdirectives # validates go compiler directive comments
|
||||||
- gochecknoglobals # checks that no global variables exist
|
- gochecknoglobals # checks that no global variables exist
|
||||||
- gochecknoinits # checks that no init functions are present
|
- gochecknoinits # checks that no init functions are present
|
||||||
- gocognit # computes and checks the cognitive complexity
|
- gocognit # computes and checks the cognitive complexity
|
||||||
- goconst # finds repeated strings that could be replaced by a constant
|
- goconst # finds repeated strings that could be replaced by a constant
|
||||||
- gocritic # provides diagnostics that check for bugs, performance and style issues
|
- gocritic # provides diagnostics that check for bugs, performance and style issues
|
||||||
- gocyclo # checks cyclomatic complexity
|
- gocyclo # checks cyclomatic complexity
|
||||||
- godot # checks if comments end in a period
|
- godot # checks if comments end in a period
|
||||||
- godox # detects FIXME, TODO and other comment keywords
|
- godox # detects FIXME, TODO and other comment keywords
|
||||||
- goerr113 # checks the errors handling expressions
|
- goheader # checks is file header matches to pattern
|
||||||
- gofmt # checks whether code was gofmt-ed
|
|
||||||
- gofumpt # checks whether code was gofumpt-ed
|
|
||||||
- goheader # checks is file header matches to pattern
|
|
||||||
- goimports # does everything that gofmt does + formats imports
|
|
||||||
- gomnd # detects magic numbers
|
|
||||||
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
|
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
|
||||||
- gomodguard # allows to specify a list of forbidden modules
|
- gomodguard # allows to specify a list of forbidden modules
|
||||||
- goprintffuncname # checks that printf-like functions are named with f at the end
|
- goprintffuncname # checks that printf-like functions are named with f at the end
|
||||||
- gosec # inspects source code for security problems
|
- gosec # inspects source code for security problems
|
||||||
- gosimple # specializes in simplifying code
|
- govet # reports suspicious constructs
|
||||||
- govet # reports suspicious constructs
|
- grouper # analyzes expression groups
|
||||||
- grouper # analyzes expression groups
|
- importas # enforces consistent import aliases
|
||||||
- importas # enforces consistent import aliases
|
- ineffassign # detects when assignments to existing variables are not used
|
||||||
- ineffassign # detects when assignments to existing variables are not used
|
|
||||||
- interfacebloat # checks the number of methods inside an interface
|
- interfacebloat # checks the number of methods inside an interface
|
||||||
- ireturn # accept interfaces, return concrete types
|
- ireturn # accept interfaces, return concrete types
|
||||||
- lll # reports long lines
|
- lll # reports long lines
|
||||||
- loggercheck # checks key value pairs for common logger libraries
|
- loggercheck # checks key value pairs for common logger libraries
|
||||||
- maintidx # measures the maintainability index of each function
|
- maintidx # measures the maintainability index of each function
|
||||||
- makezero # finds slice declarations with non-zero initial length
|
- makezero # finds slice declarations with non-zero initial length
|
||||||
- misspell # finds commonly misspelled English words
|
- misspell # finds commonly misspelled English words
|
||||||
- nakedret # finds naked returns
|
- nakedret # finds naked returns
|
||||||
- nestif # reports deeply nested if statements
|
- nestif # reports deeply nested if statements
|
||||||
- nilerr # finds the code that returns nil even if it checks that error is not nil
|
- nilerr # finds the code that returns nil even if it checks that error is not nil
|
||||||
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
||||||
- nlreturn # checks for a new line before return and branch statements
|
- nlreturn # checks for a new line before return and branch statements
|
||||||
- noctx # finds sending http request without context.Context
|
- noctx # finds sending http request without context.Context
|
||||||
- nolintlint # reports ill-formed or insufficient nolint directives
|
- nolintlint # reports ill-formed or insufficient nolint directives
|
||||||
- nonamedreturns # reports all named returns
|
- nonamedreturns # reports all named returns
|
||||||
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
|
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
|
||||||
- paralleltest # detects missing usage of t.Parallel() method in your Go test
|
- paralleltest # detects missing usage of t.Parallel() method in your Go test
|
||||||
- prealloc # finds slice declarations that could potentially be pre-allocated
|
- prealloc # finds slice declarations that could potentially be pre-allocated
|
||||||
- predeclared # finds code that shadows one of Go's predeclared identifiers
|
- predeclared # finds code that shadows one of Go's predeclared identifiers
|
||||||
- promlinter # checks Prometheus metrics naming via promlint
|
- promlinter # checks Prometheus metrics naming via promlint
|
||||||
- reassign # checks that package variables are not reassigned
|
- reassign # checks that package variables are not reassigned
|
||||||
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go
|
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go
|
||||||
- rowserrcheck # checks whether Err of rows is checked successfully
|
- rowserrcheck # checks whether Err of rows is checked successfully
|
||||||
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
||||||
- staticcheck # comprehensive checks for bugs and inefficiencies
|
- staticcheck # comprehensive checks for bugs and inefficiencies
|
||||||
- stylecheck # replacement for golint
|
|
||||||
- tenv # detects using os.Setenv instead of t.Setenv since Go1.17
|
|
||||||
- testableexamples # checks if examples are testable
|
- testableexamples # checks if examples are testable
|
||||||
- testpackage # makes you use a separate _test package
|
- testpackage # makes you use a separate _test package
|
||||||
- thelper # detects golang test helpers without t.Helper()
|
- thelper # detects golang test helpers without t.Helper()
|
||||||
- tparallel # detects inappropriate usage of t.Parallel()
|
- tparallel # detects inappropriate usage of t.Parallel()
|
||||||
- typecheck # like the front-end of a Go compiler
|
- unconvert # removes unnecessary type conversions
|
||||||
- unconvert # removes unnecessary type conversions
|
- unparam # reports unused function parameters
|
||||||
- unparam # reports unused function parameters
|
- unused # checks for unused constants, variables, functions and types
|
||||||
- unused # checks for unused constants, variables, functions and types
|
|
||||||
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
|
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
|
||||||
- varnamelen # checks that the length of a variable's name matches its scope
|
- varnamelen # checks that the length of a variable's name matches its scope
|
||||||
- wastedassign # finds wasted assignment statements
|
- wastedassign # finds wasted assignment statements
|
||||||
- whitespace # detects leading and trailing whitespace
|
- whitespace # detects leading and trailing whitespace
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,54 @@
|
||||||
# Prettier friendly markdownlint config (all formatting rules disabled)
|
# Enable all rules by default
|
||||||
|
default: true
|
||||||
|
|
||||||
|
# Markdown linting configuration with all rules enabled
|
||||||
extends: markdownlint/style/prettier
|
extends: markdownlint/style/prettier
|
||||||
|
|
||||||
|
# MD003 heading-style - Header style
|
||||||
|
MD003:
|
||||||
|
style: atx # Use # style headers
|
||||||
|
|
||||||
|
# MD004 ul-style - Unordered list style
|
||||||
|
MD004:
|
||||||
|
style: consistent # Be consistent with the first list style used
|
||||||
|
|
||||||
|
# MD012 no-multiple-blanks - No multiple consecutive blank lines
|
||||||
|
MD012:
|
||||||
|
maximum: 1
|
||||||
|
|
||||||
|
# MD013 line-length - Line length
|
||||||
|
MD013:
|
||||||
|
line_length: 100
|
||||||
|
code_blocks: false
|
||||||
|
tables: false
|
||||||
|
|
||||||
|
# MD024 no-duplicate-header - No duplicate headers
|
||||||
|
MD024:
|
||||||
|
siblings_only: true # Allow duplicates if they're not siblings
|
||||||
|
|
||||||
|
# MD026 no-trailing-punctuation - No trailing punctuation in header
|
||||||
|
MD026:
|
||||||
|
punctuation: .,;:!。,;:!
|
||||||
|
|
||||||
|
# MD029 ol-prefix - Ordered list item prefix
|
||||||
|
MD029:
|
||||||
|
style: one_or_ordered
|
||||||
|
|
||||||
|
# MD033 no-inline-html - No inline HTML
|
||||||
|
MD033:
|
||||||
|
allowed_elements: []
|
||||||
|
|
||||||
|
# MD034 no-bare-urls - No bare URLs
|
||||||
|
MD034: true
|
||||||
|
|
||||||
|
# MD035 hr-style - Horizontal rule style
|
||||||
|
MD035:
|
||||||
|
style: ---
|
||||||
|
|
||||||
|
# MD041 first-line-heading - First line should be a top-level header
|
||||||
|
MD041:
|
||||||
|
level: 1
|
||||||
|
|
||||||
|
# MD046 code-block-style - Code block style
|
||||||
|
MD046:
|
||||||
|
style: fenced
|
||||||
|
|
|
||||||
4
.trunk/configs/.remarkrc.yaml
Normal file
4
.trunk/configs/.remarkrc.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
plugins:
|
||||||
|
remark-preset-lint-consistent: true
|
||||||
|
remark-preset-lint-recommended: true
|
||||||
|
remark-lint-list-item-indent: true
|
||||||
7
.trunk/configs/.shellcheckrc
Normal file
7
.trunk/configs/.shellcheckrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
enable=all
|
||||||
|
source-path=SCRIPTDIR
|
||||||
|
disable=SC1090
|
||||||
|
disable=SC1091
|
||||||
|
disable=SC2154
|
||||||
|
disable=SC2310
|
||||||
|
disable=SC2312
|
||||||
5
.trunk/configs/.vale.ini
Normal file
5
.trunk/configs/.vale.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[formats]
|
||||||
|
markdoc = md
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
BasedOnStyles = Vale
|
||||||
91
.trunk/configs/analyzers.yml
Normal file
91
.trunk/configs/analyzers.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright 2021 Praetorian Security, Inc.
|
||||||
|
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the License);
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an AS IS BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# GoKart analyzers configuration
|
||||||
|
|
||||||
|
# Uncomment analyzers section below to create a new vulnerability type
|
||||||
|
|
||||||
|
# analyzers:
|
||||||
|
# # Each entry specifies a vulnerability type.
|
||||||
|
|
||||||
|
# # Name of the vulnerability:
|
||||||
|
# Test Sink:
|
||||||
|
# # Description of this vulnerability
|
||||||
|
# doc: Writing data to Printf()
|
||||||
|
# # Message displayed when this vulnerability is found
|
||||||
|
# message: Test Sink reachable by user input
|
||||||
|
# # List of vulnerable functions used to identify this vulnerability
|
||||||
|
# vuln_calls:
|
||||||
|
# # Package name
|
||||||
|
# log:
|
||||||
|
# # Function name
|
||||||
|
# - Printf
|
||||||
|
|
||||||
|
# Each entry specifies a source that should be considered untrusted
|
||||||
|
# If the package already exists in the sources section, add the variable/function/type underneath
|
||||||
|
# Each package can contain multiple vulnerable sources.
|
||||||
|
sources:
|
||||||
|
# Sources that are defined in Go documentation as a variable go here (note: these variables will have an SSA type of Global).
|
||||||
|
variables:
|
||||||
|
os:
|
||||||
|
- Args
|
||||||
|
# Sources that are defined in Go documentation as a function go here.
|
||||||
|
functions:
|
||||||
|
flag:
|
||||||
|
- Arg
|
||||||
|
- Args
|
||||||
|
os:
|
||||||
|
- Environ
|
||||||
|
- File
|
||||||
|
crypto/tls:
|
||||||
|
- LoadX509KeyPair
|
||||||
|
- X509KeyPair
|
||||||
|
os/user:
|
||||||
|
- Lookup
|
||||||
|
- LookupId
|
||||||
|
- Current
|
||||||
|
crypto/x509:
|
||||||
|
- Subjects
|
||||||
|
io:
|
||||||
|
- ReadAtLeast
|
||||||
|
- ReadFull
|
||||||
|
database/sql:
|
||||||
|
- Query
|
||||||
|
- QueryRow
|
||||||
|
bytes:
|
||||||
|
- String
|
||||||
|
- ReadBytes
|
||||||
|
- ReadByte
|
||||||
|
bufio:
|
||||||
|
- Text
|
||||||
|
- Bytes
|
||||||
|
- ReadString
|
||||||
|
- ReadSlice
|
||||||
|
- ReadRune
|
||||||
|
- ReadLine
|
||||||
|
- ReadBytes
|
||||||
|
- ReadByte
|
||||||
|
archive/tar:
|
||||||
|
- Next
|
||||||
|
- FileInfo
|
||||||
|
- Header
|
||||||
|
net/url:
|
||||||
|
- ParseQuery
|
||||||
|
- ParseUriRequest
|
||||||
|
- Parse
|
||||||
|
- Query
|
||||||
|
# Sources that are defined in Go documentation as a type go here (note: adding types will consider all functions that use that type to be tainted).
|
||||||
|
types:
|
||||||
|
net/http:
|
||||||
|
- Request
|
||||||
4
.trunk/configs/cspell.yaml
Normal file
4
.trunk/configs/cspell.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
version: "0.2"
|
||||||
|
# Suggestions can sometimes take longer on CI machines,
|
||||||
|
# leading to inconsistent results.
|
||||||
|
suggestionsTimeout: 5000 # ms
|
||||||
|
|
@ -3,34 +3,65 @@
|
||||||
version: 0.1
|
version: 0.1
|
||||||
cli:
|
cli:
|
||||||
version: 1.25.0
|
version: 1.25.0
|
||||||
|
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.7.1
|
ref: v1.7.2
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
|
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
|
- node@22.16.0
|
||||||
|
- python@3.10.8
|
||||||
- go@1.21.0
|
- go@1.21.0
|
||||||
|
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||||
lint:
|
lint:
|
||||||
|
disabled:
|
||||||
|
- cspell
|
||||||
enabled:
|
enabled:
|
||||||
|
- yamlfmt@0.17.2
|
||||||
|
- vale@3.12.0
|
||||||
|
- trunk-toolbox@0.5.4
|
||||||
|
- trufflehog-git@3.90.6
|
||||||
|
- snyk@1.1295.0
|
||||||
|
- remark-lint@12.0.1
|
||||||
|
- pre-commit-hooks@6.0.0
|
||||||
|
- nancy@1.0.51
|
||||||
|
- markdownlint-cli2@0.18.1
|
||||||
|
- markdown-table-prettify@3.7.0
|
||||||
|
- markdown-link-check@3.13.7
|
||||||
|
- ls-lint@2.3.1
|
||||||
|
- golangci-lint@1.64.8
|
||||||
|
- gokart@0.5.1
|
||||||
- goimports@0.9.1
|
- goimports@0.9.1
|
||||||
|
- gofumpt@0.5.0
|
||||||
|
- gitleaks@8.28.0
|
||||||
|
- deno@2.5.0
|
||||||
|
- biome@2.2.4
|
||||||
|
- codespell@2.4.1
|
||||||
|
- kube-linter@0.7.2
|
||||||
- golines@0.13.0
|
- golines@0.13.0
|
||||||
- markdownlint@0.45.0
|
- semgrep@1.136.0
|
||||||
|
- shellcheck@0.11.0
|
||||||
- actionlint@1.7.7
|
- actionlint@1.7.7
|
||||||
|
- checkov@3.2.470
|
||||||
|
- dotenv-linter@3.3.0
|
||||||
|
- git-diff-check
|
||||||
- gofmt@1.20.4
|
- gofmt@1.20.4
|
||||||
- golangci-lint@1.54.2
|
- golangci-lint2@2.4.0
|
||||||
- shellcheck@0.10.0
|
- isort@6.0.1
|
||||||
|
- markdownlint@0.45.0
|
||||||
|
- osv-scanner@2.2.2
|
||||||
|
- oxipng@9.1.5
|
||||||
|
- prettier@3.6.2
|
||||||
- shfmt@3.6.0
|
- shfmt@3.6.0
|
||||||
|
- trivy@0.66.0
|
||||||
|
- trufflehog@3.90.6
|
||||||
- yamllint@1.37.1
|
- yamllint@1.37.1
|
||||||
- cspell@0.27.1
|
|
||||||
|
|
||||||
definitions:
|
|
||||||
- name: cspell
|
|
||||||
files: [".md$"]
|
|
||||||
actions:
|
actions:
|
||||||
enabled:
|
enabled:
|
||||||
- trunk-upgrade-available
|
|
||||||
- trunk-fmt-pre-commit
|
|
||||||
disabled:
|
|
||||||
- trunk-announce
|
- trunk-announce
|
||||||
- trunk-check-pre-push
|
- trunk-check-pre-push
|
||||||
|
# - trunk-fmt-pre-commit
|
||||||
|
- trunk-upgrade-available
|
||||||
|
|
|
||||||
5
.version-info
Normal file
5
.version-info
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Version information for cache-apt-pkgs-action
|
||||||
|
GO_VERSION=1.24
|
||||||
|
TOOLCHAIN_VERSION=
|
||||||
|
SYSPKG_VERSION=v0.1.5
|
||||||
|
EXPORT_DATE=2025-09-07 11:53:25
|
||||||
6
.version-info.json
Normal file
6
.version-info.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"goVersion": "1.24",
|
||||||
|
"toolchainVersion": "",
|
||||||
|
"syspkgVersion": "v0.1.5",
|
||||||
|
"exportDate": "2025-09-07 11:53:25"
|
||||||
|
}
|
||||||
248
.vscode/cspell.json
vendored
248
.vscode/cspell.json
vendored
|
|
@ -1,248 +0,0 @@
|
||||||
{
|
|
||||||
"version": "0.2",
|
|
||||||
"language": "en",
|
|
||||||
"dictionaries": [
|
|
||||||
"go",
|
|
||||||
"softwareTerms"
|
|
||||||
],
|
|
||||||
"enableFiletypes": [
|
|
||||||
"go",
|
|
||||||
"md",
|
|
||||||
"yml",
|
|
||||||
],
|
|
||||||
"words": [
|
|
||||||
"arangodb",
|
|
||||||
"arangolint",
|
|
||||||
"asasalint",
|
|
||||||
"asciicheck",
|
|
||||||
"awalsh",
|
|
||||||
"Axxxx",
|
|
||||||
"bidichk",
|
|
||||||
"bodyclose",
|
|
||||||
"canonicalheader",
|
|
||||||
"cli",
|
|
||||||
"cmdflags",
|
|
||||||
"cmdtesting",
|
|
||||||
"codecov",
|
|
||||||
"containedctx",
|
|
||||||
"contextcheck",
|
|
||||||
"copyloopvar",
|
|
||||||
"covermode",
|
|
||||||
"coverprofile",
|
|
||||||
"createreplaylogs",
|
|
||||||
"cwd",
|
|
||||||
"cyclo",
|
|
||||||
"cyclop",
|
|
||||||
"davecgh",
|
|
||||||
"DCMAKE",
|
|
||||||
"decorder",
|
|
||||||
"depguard",
|
|
||||||
"difflib",
|
|
||||||
"dmbeddedstouctfieldcheck",
|
|
||||||
"dockerdesktop",
|
|
||||||
"dpkg",
|
|
||||||
"dupl",
|
|
||||||
"dupword",
|
|
||||||
"durationcheck",
|
|
||||||
"eamodio",
|
|
||||||
"embeddedstructfieldcheck",
|
|
||||||
"errcheck",
|
|
||||||
"errchkjson",
|
|
||||||
"errexit",
|
|
||||||
"errname",
|
|
||||||
"Errorf",
|
|
||||||
"errorlint",
|
|
||||||
"erxygen",
|
|
||||||
"esac",
|
|
||||||
"Etz",
|
|
||||||
"exhaustruct",
|
|
||||||
"exptostd",
|
|
||||||
"fatcontext",
|
|
||||||
"fieldalignment",
|
|
||||||
"fileset",
|
|
||||||
"finkgulinter",
|
|
||||||
"fmt",
|
|
||||||
"folded",
|
|
||||||
"forbidigo",
|
|
||||||
"forcetypeassert",
|
|
||||||
"forcorder",
|
|
||||||
"funcorder",
|
|
||||||
"funlen",
|
|
||||||
"Fyf",
|
|
||||||
"ginkgolinter",
|
|
||||||
"gocheckcompilerdirectives",
|
|
||||||
"gochecknoglobals",
|
|
||||||
"gochecknoinits",
|
|
||||||
"gochecksumtype",
|
|
||||||
"gocognit",
|
|
||||||
"goconst",
|
|
||||||
"gocritic",
|
|
||||||
"gocyclo",
|
|
||||||
"godot",
|
|
||||||
"godox",
|
|
||||||
"gofmt",
|
|
||||||
"gofrs",
|
|
||||||
"gofumpt",
|
|
||||||
"goheader",
|
|
||||||
"goimports",
|
|
||||||
"golangci",
|
|
||||||
"golines",
|
|
||||||
"golint",
|
|
||||||
"gomega",
|
|
||||||
"gomoddirectives",
|
|
||||||
"gomodguard",
|
|
||||||
"gonlen",
|
|
||||||
"gopkg",
|
|
||||||
"goprintffuncname",
|
|
||||||
"GOROOT",
|
|
||||||
"gosec",
|
|
||||||
"gosmopolitan",
|
|
||||||
"gostd",
|
|
||||||
"govet",
|
|
||||||
"graphviz",
|
|
||||||
"iface",
|
|
||||||
"importas",
|
|
||||||
"inamedparam",
|
|
||||||
"ineffassign",
|
|
||||||
"interfacebloat",
|
|
||||||
"intrange",
|
|
||||||
"ireturn",
|
|
||||||
"jmoiron",
|
|
||||||
"jre",
|
|
||||||
"kitlog",
|
|
||||||
"klog",
|
|
||||||
"libasound",
|
|
||||||
"libatk",
|
|
||||||
"libatspi",
|
|
||||||
"libboost",
|
|
||||||
"libcups",
|
|
||||||
"libdrm",
|
|
||||||
"libfuse",
|
|
||||||
"libgbm",
|
|
||||||
"libgl",
|
|
||||||
"libgtk",
|
|
||||||
"libnspr",
|
|
||||||
"libnss",
|
|
||||||
"libosmesa",
|
|
||||||
"libtk",
|
|
||||||
"libvips",
|
|
||||||
"libxcomposite",
|
|
||||||
"libxdamage",
|
|
||||||
"libxfixes",
|
|
||||||
"libxkbcommon",
|
|
||||||
"libxrandr",
|
|
||||||
"loggercheck",
|
|
||||||
"logr",
|
|
||||||
"lqez",
|
|
||||||
"maintidx",
|
|
||||||
"makezero",
|
|
||||||
"mapstructure",
|
|
||||||
"mitchellh",
|
|
||||||
"mscgen",
|
|
||||||
"musttag",
|
|
||||||
"myapp",
|
|
||||||
"mypackage",
|
|
||||||
"nakedret",
|
|
||||||
"nestif",
|
|
||||||
"nginx",
|
|
||||||
"nilerr",
|
|
||||||
"nilness",
|
|
||||||
"nilnesserr",
|
|
||||||
"nilnil",
|
|
||||||
"nlreturn",
|
|
||||||
"noctx",
|
|
||||||
"noinlineerr",
|
|
||||||
"nolint",
|
|
||||||
"nolintlint",
|
|
||||||
"nonamedreturns",
|
|
||||||
"nonexistentpackagename",
|
|
||||||
"nosprintfhostport",
|
|
||||||
"oipefail",
|
|
||||||
"oneapi",
|
|
||||||
"onndoc",
|
|
||||||
"paeapi",
|
|
||||||
"pandoc",
|
|
||||||
"paralleltest",
|
|
||||||
"perfsprint",
|
|
||||||
"pipefail",
|
|
||||||
"pkgs",
|
|
||||||
"pmealloc",
|
|
||||||
"postgresql",
|
|
||||||
"postinst",
|
|
||||||
"ppenapi",
|
|
||||||
"prealloc",
|
|
||||||
"predeclared",
|
|
||||||
"preinst",
|
|
||||||
"prezard",
|
|
||||||
"promlint",
|
|
||||||
"promlinter",
|
|
||||||
"proto",
|
|
||||||
"protogetter",
|
|
||||||
"reassign",
|
|
||||||
"recvcheck",
|
|
||||||
"redis",
|
|
||||||
"replayfile",
|
|
||||||
"replayfilename",
|
|
||||||
"Reymers",
|
|
||||||
"rolldice",
|
|
||||||
"rowserrcheck",
|
|
||||||
"rwa",
|
|
||||||
"sarama",
|
|
||||||
"SCRIPTDIR",
|
|
||||||
"shellcheck",
|
|
||||||
"SHELLOPTS",
|
|
||||||
"shfmt",
|
|
||||||
"sidechannel",
|
|
||||||
"sloglint",
|
|
||||||
"SLXVDP",
|
|
||||||
"softprops",
|
|
||||||
"spancheck",
|
|
||||||
"spew",
|
|
||||||
"Sprintf",
|
|
||||||
"sqlclosecheck",
|
|
||||||
"sqlx",
|
|
||||||
"staticcheck",
|
|
||||||
"stderr",
|
|
||||||
"stdout",
|
|
||||||
"strconcat",
|
|
||||||
"strconv",
|
|
||||||
"stretchr",
|
|
||||||
"stretchside",
|
|
||||||
"strs",
|
|
||||||
"Submatch",
|
|
||||||
"swaggo",
|
|
||||||
"syspkg",
|
|
||||||
"tagalign",
|
|
||||||
"tagliatelle",
|
|
||||||
"testableexamples",
|
|
||||||
"testcontainers",
|
|
||||||
"testdata",
|
|
||||||
"testifylint",
|
|
||||||
"testpackage",
|
|
||||||
"thelper",
|
|
||||||
"toolchain",
|
|
||||||
"tparallel",
|
|
||||||
"Typeflag",
|
|
||||||
"unconvert",
|
|
||||||
"underef",
|
|
||||||
"unparam",
|
|
||||||
"unparsedflags",
|
|
||||||
"untar",
|
|
||||||
"usestdlibvars",
|
|
||||||
"usetesting",
|
|
||||||
"usr",
|
|
||||||
"varnamelen",
|
|
||||||
"wastedassign",
|
|
||||||
"wayou",
|
|
||||||
"whitespace",
|
|
||||||
"wrapcheck",
|
|
||||||
"xdot",
|
|
||||||
"yamllint",
|
|
||||||
"zerolog",
|
|
||||||
"zerologlint"
|
|
||||||
],
|
|
||||||
"ignorePaths": [
|
|
||||||
"dist",
|
|
||||||
".git"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"golang.go", // Official Go extension
|
"golang.go", // Official Go extension
|
||||||
"trunk.io", // trunk.io Linters
|
"trunk.io", // trunk.io Linters
|
||||||
"wayou.vscode-todo-highlight", // Highlight TODOs
|
"wayou.vscode-todo-highlight", // Highlight TODOs
|
||||||
"streetsidesoftware.code-spell-checker", // Spell checking
|
|
||||||
"eamodio.gitlens", // Git integration
|
"eamodio.gitlens", // Git integration
|
||||||
"github.vscode-github-actions", // GitHub Actions support
|
"github.vscode-github-actions" // GitHub Actions support
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -6,7 +6,7 @@
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${fileDirname}",
|
"program": "${fileDirname}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
49
.vscode/settings.json
vendored
49
.vscode/settings.json
vendored
|
|
@ -1,28 +1,41 @@
|
||||||
{
|
{
|
||||||
"cSpell.enabled": false,
|
"editor.tabSize": 2,
|
||||||
"editor.rulers": [
|
"editor.insertSpaces": true,
|
||||||
100
|
"editor.detectIndentation": false,
|
||||||
],
|
"editor.rulers": [100],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"editor.wordWrap": "wordWrapColumn",
|
||||||
"editor.wordWrapColumn": 100,
|
"editor.wordWrapColumn": 100,
|
||||||
|
"editor.wrappingIndent": "indent",
|
||||||
"[go]": {
|
"[go]": {
|
||||||
"editor.defaultFormatter": "trunk.io",
|
"editor.defaultFormatter": "trunk.io"
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[shellscript]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features",
|
"editor.defaultFormatter": "trunk.io"
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
},
|
||||||
"[jsonc]": {
|
"[bash]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features",
|
"editor.defaultFormatter": "trunk.io"
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
},
|
||||||
"[shell]": {
|
"[json,jsonc]": {
|
||||||
"editor.defaultFormatter": "trunk.io",
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
"editor.formatOnSave": true
|
},
|
||||||
|
"[shfmt]": {
|
||||||
|
"editor.defaultFormatter": "trunk.io"
|
||||||
},
|
},
|
||||||
"[yaml]": {
|
"[yaml]": {
|
||||||
"editor.defaultFormatter": "trunk.io",
|
"editor.defaultFormatter": "trunk.io"
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true
|
"cSpell.enabled": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.shellcheck": "explicit"
|
||||||
|
},
|
||||||
|
"workbench.editorAssociations": {
|
||||||
|
"git-index": "default",
|
||||||
|
"git-show": "default"
|
||||||
|
},
|
||||||
|
"files.readonlyInclude": {},
|
||||||
|
"workbench.editor.defaultBinaryEditor": "default",
|
||||||
|
"workbench.editor.enablePreviewFromCodeNavigation": false
|
||||||
}
|
}
|
||||||
24
.vscode/tasks.json
vendored
24
.vscode/tasks.json
vendored
|
|
@ -5,18 +5,12 @@
|
||||||
"label": "go: build",
|
"label": "go: build",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go",
|
"command": "go",
|
||||||
"args": [
|
"args": ["build", "-v", "./..."],
|
||||||
"build",
|
|
||||||
"-v",
|
|
||||||
"./..."
|
|
||||||
],
|
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": ["$go"],
|
||||||
"$go"
|
|
||||||
],
|
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "shared",
|
"panel": "shared",
|
||||||
|
|
@ -28,18 +22,12 @@
|
||||||
"label": "go: test",
|
"label": "go: test",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "go",
|
"command": "go",
|
||||||
"args": [
|
"args": ["test", "-v", "./..."],
|
||||||
"test",
|
|
||||||
"-v",
|
|
||||||
"./..."
|
|
||||||
],
|
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": ["$go"],
|
||||||
"$go"
|
|
||||||
],
|
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "shared",
|
"panel": "shared",
|
||||||
|
|
@ -59,9 +47,7 @@
|
||||||
"-covermode=atomic",
|
"-covermode=atomic",
|
||||||
"./..."
|
"./..."
|
||||||
],
|
],
|
||||||
"problemMatcher": [
|
"problemMatcher": ["$go"],
|
||||||
"$go"
|
|
||||||
],
|
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "shared",
|
"panel": "shared",
|
||||||
|
|
|
||||||
649
CLAUDE.md
649
CLAUDE.md
|
|
@ -10,6 +10,7 @@
|
||||||
- [3. Error Handling](#3-error-handling)
|
- [3. Error Handling](#3-error-handling)
|
||||||
- [4. API Design](#4-api-design)
|
- [4. API Design](#4-api-design)
|
||||||
- [5. Documentation Practices](#5-documentation-practices)
|
- [5. Documentation Practices](#5-documentation-practices)
|
||||||
|
- [Go Code Documentation Standards](#go-code-documentation-standards)
|
||||||
- [Code Documentation](#code-documentation)
|
- [Code Documentation](#code-documentation)
|
||||||
- [Project Documentation](#project-documentation)
|
- [Project Documentation](#project-documentation)
|
||||||
- [6. Testing Strategy](#6-testing-strategy)
|
- [6. Testing Strategy](#6-testing-strategy)
|
||||||
|
|
@ -41,6 +42,10 @@
|
||||||
- [Further Guidance](#further-guidance)
|
- [Further Guidance](#further-guidance)
|
||||||
- [Bash Scripts](#bash-scripts)
|
- [Bash Scripts](#bash-scripts)
|
||||||
- [Script Testing](#script-testing)
|
- [Script Testing](#script-testing)
|
||||||
|
- [Test Framework Architecture Pattern](#test-framework-architecture-pattern)
|
||||||
|
- [Script Argument Parsing Pattern](#script-argument-parsing-pattern)
|
||||||
|
- [Centralized Configuration Management](#centralized-configuration-management)
|
||||||
|
- [Implementation Status](#implementation-status)
|
||||||
- [Testing Principles](#testing-principles)
|
- [Testing Principles](#testing-principles)
|
||||||
- [1. Test Organization Strategy](#1-test-organization-strategy)
|
- [1. Test Organization Strategy](#1-test-organization-strategy)
|
||||||
- [2. Code Structure](#2-code-structure)
|
- [2. Code Structure](#2-code-structure)
|
||||||
|
|
@ -69,6 +74,7 @@
|
||||||
|
|
||||||
### 2. Code Style and Formatting
|
### 2. Code Style and Formatting
|
||||||
|
|
||||||
|
- Use 2 spaces for indentation, never tabs
|
||||||
- Consistent naming conventions (e.g., CamelCase for exported names)
|
- Consistent naming conventions (e.g., CamelCase for exported names)
|
||||||
- Keep functions small and focused
|
- Keep functions small and focused
|
||||||
- Use meaningful variable names
|
- Use meaningful variable names
|
||||||
|
|
@ -85,15 +91,92 @@
|
||||||
### 4. API Design
|
### 4. API Design
|
||||||
|
|
||||||
- Make zero values useful
|
- Make zero values useful
|
||||||
- Keep interfaces small and focused, observing the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)
|
- Keep interfaces small and focused, observing the
|
||||||
- Observe the [open-closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) so that it is open for extension but closed to modification
|
[single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle)
|
||||||
- Observe the [dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) to keep interfaces loosely coupled
|
- Observe the
|
||||||
|
[open-closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)
|
||||||
|
so that it is open for extension but closed to modification
|
||||||
|
- Observe the
|
||||||
|
[dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle)
|
||||||
|
to keep interfaces loosely coupled
|
||||||
- Design for composition over inheritance
|
- Design for composition over inheritance
|
||||||
- Use option patterns for complex configurations
|
- Use option patterns for complex configurations
|
||||||
- Make dependencies explicit
|
- Make dependencies explicit
|
||||||
|
|
||||||
### 5. Documentation Practices
|
### 5. Documentation Practices
|
||||||
|
|
||||||
|
#### Go Code Documentation Standards
|
||||||
|
|
||||||
|
Following the official [Go Documentation Guidelines](https://go.dev/blog/godoc):
|
||||||
|
|
||||||
|
1. **Package Documentation**
|
||||||
|
- Every package must have a doc comment immediately before the `package`
|
||||||
|
statement
|
||||||
|
- Format: `// Package xyz ...` (first sentence) followed by detailed
|
||||||
|
description
|
||||||
|
- First sentence should be a summary beginning with `Package xyz`
|
||||||
|
- Follow with a blank line and detailed documentation
|
||||||
|
- Include package-level examples if helpful
|
||||||
|
|
||||||
|
2. **Exported Items Documentation**
|
||||||
|
- Document all exported (capitalized) names
|
||||||
|
- Comments must begin with the name being declared
|
||||||
|
- First sentence should be a summary
|
||||||
|
- Omit the subject when it's the thing being documented
|
||||||
|
- Use article "a" for types that could be one of many, "the" for singletons
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// List represents a singly-linked list.
|
||||||
|
// A zero List is valid and represents an empty list.
|
||||||
|
type List struct {}
|
||||||
|
|
||||||
|
// NewRing creates a new ring buffer with the given size.
|
||||||
|
func NewRing(size int) *Ring {}
|
||||||
|
|
||||||
|
// Append adds the elements to the list.
|
||||||
|
// Blocks if buffer is full.
|
||||||
|
func (l *List) Append(elems ...interface{}) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Documentation Style**
|
||||||
|
- Write clear, complete sentences
|
||||||
|
- Begin comments with a capital letter
|
||||||
|
- End sentences with punctuation
|
||||||
|
- Keep comments up to date with code changes
|
||||||
|
- Focus on behavior users can rely on, not implementation
|
||||||
|
- Document synchronization assumptions for concurrent access
|
||||||
|
- Document any special error conditions or panics
|
||||||
|
|
||||||
|
4. **Examples**
|
||||||
|
- Add examples for complex types or functions using `Example` functions
|
||||||
|
- Include examples in package docs for important usage patterns
|
||||||
|
- Make examples self-contained and runnable
|
||||||
|
- Use realistic data and common use cases
|
||||||
|
- Show output in comments when examples print output:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func ExampleHello() {
|
||||||
|
fmt.Println("Hello")
|
||||||
|
// Output: Hello
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Doc Comments Format**
|
||||||
|
- Use complete sentences and proper punctuation
|
||||||
|
- Add a blank line between paragraphs
|
||||||
|
- Use lists and code snippets for clarity
|
||||||
|
- Include links to related functions/types where helpful
|
||||||
|
- Document parameters and return values implicitly in the description
|
||||||
|
- Break long lines at 80 characters
|
||||||
|
|
||||||
|
6. **Quality Control**
|
||||||
|
- Run `go doc` to verify how documentation will appear
|
||||||
|
- Review documentation during code reviews
|
||||||
|
- Keep examples up to date and passing
|
||||||
|
- Update docs when changing behavior
|
||||||
|
|
||||||
#### Code Documentation
|
#### Code Documentation
|
||||||
|
|
||||||
- Write package documentation with examples
|
- Write package documentation with examples
|
||||||
|
|
@ -105,17 +188,23 @@
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Package cache provides a caching mechanism for apt packages.
|
// key.go
|
||||||
// It supports both saving and restoring package states, making it
|
//
|
||||||
// useful for CI/CD environments where package installation is expensive.
|
// Description:
|
||||||
|
//
|
||||||
|
// Provides types and functions for managing cache keys, including serialization, deserialization,
|
||||||
|
// and validation of package metadata.
|
||||||
|
//
|
||||||
|
// Package: cache
|
||||||
//
|
//
|
||||||
// Example usage:
|
// Example usage:
|
||||||
//
|
//
|
||||||
// cache := NewCache()
|
// // Create a new cache key
|
||||||
// err := cache.SavePackages(packages)
|
// key := cache.NewKey(packages, "v1.0", "v2", "amd64")
|
||||||
// if err != nil {
|
//
|
||||||
// // handle error
|
// // Get the hash of the key
|
||||||
// }
|
// hash := key.Hash()
|
||||||
|
// fmt.Printf("Key hash: %x\n", hash)
|
||||||
package cache
|
package cache
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -372,22 +461,27 @@ go tool pprof -http=:8080 cpu.prof
|
||||||
|
|
||||||
- Minimize the amount of shell code and put complex logic in the Go code
|
- Minimize the amount of shell code and put complex logic in the Go code
|
||||||
- Use clear step `id` names that use dashes between words and active verbs
|
- Use clear step `id` names that use dashes between words and active verbs
|
||||||
- Avoid hard-coded API URLs like https://api.github.com. Use environment variables (GITHUB_API_URL for REST API, GITHUB_GRAPHQL_URL for GraphQL) or the @actions/github toolkit for dynamic URL handling
|
- Avoid hard-coded API URLs like <https://api.github.com>. Use environment
|
||||||
|
variables (GITHUB_API_URL for REST API, GITHUB_GRAPHQL_URL for GraphQL) or the
|
||||||
|
@actions/github toolkit for dynamic URL handling
|
||||||
|
|
||||||
##### Release Management
|
##### Release Management
|
||||||
|
|
||||||
- Use semantic versioning for releases (e.g., v1.0.0)
|
- Use semantic versioning for releases (e.g., v1.0.0)
|
||||||
- Recommend users reference major version tags (v1) instead of the default branch for stability.
|
- Recommend users reference major version tags (v1) instead of the default
|
||||||
|
branch for stability.
|
||||||
- Update major version tags to point to the latest release
|
- Update major version tags to point to the latest release
|
||||||
|
|
||||||
##### Create a README File
|
##### Create a README File
|
||||||
|
|
||||||
Include a detailed description, required/optional inputs and outputs, secrets, environment variables, and usage examples
|
Include a detailed description, required/optional inputs and outputs, secrets,
|
||||||
|
environment variables, and usage examples
|
||||||
|
|
||||||
##### Testing and Automation
|
##### Testing and Automation
|
||||||
|
|
||||||
- Add workflows to test your action on feature branches and pull requests
|
- Add workflows to test your action on feature branches and pull requests
|
||||||
- Automate releases using workflows triggered by publishing or editing a release.
|
- Automate releases using workflows triggered by publishing or editing a
|
||||||
|
release.
|
||||||
|
|
||||||
##### Community Engagement
|
##### Community Engagement
|
||||||
|
|
||||||
|
|
@ -398,24 +492,26 @@ Include a detailed description, required/optional inputs and outputs, secrets, e
|
||||||
##### Further Guidance
|
##### Further Guidance
|
||||||
|
|
||||||
For more details, visit:
|
For more details, visit:
|
||||||
- https://docs.github.com/en/actions/how-tos/create-and-publish-actions/manage-custom-actions
|
|
||||||
- https://docs.github.com/en/actions/how-tos/create-and-publish-actions/release-and-maintain-actions
|
- <https://docs.github.com/en/actions/how-tos/create-and-publish-actions/manage-custom-actions>
|
||||||
|
- <https://docs.github.com/en/actions/how-tos/create-and-publish-actions/release-and-maintain-actions>
|
||||||
|
|
||||||
### Bash Scripts
|
### Bash Scripts
|
||||||
|
|
||||||
Project scripts should follow these guidelines:
|
Project scripts should follow these guidelines:
|
||||||
|
|
||||||
- Create scripts in the `scripts` directory (not `tools`)
|
- Follow formatting rules in
|
||||||
- Add new functionality to the `scripts/menu.sh` script for easy access
|
[Shellcheck](https://github.com/koalaman/shellcheck/wiki)
|
||||||
|
- Follow style guide rules in
|
||||||
|
[Google Bash Style Guide](https://google.github.io/styleguide/shellguide)
|
||||||
|
- Include proper error handling and exit codes
|
||||||
|
- Use `scripts/lib.sh` whenever for common functionality
|
||||||
- Use imperative verb form for script names:
|
- Use imperative verb form for script names:
|
||||||
- Good: `export_version.sh`, `build_package.sh`, `run_tests.sh`
|
- Good: `export_version.sh`, `build_package.sh`, `run_tests.sh`
|
||||||
- Bad: `version_export.sh`, `package_builder.sh`, `test_runner.sh`
|
- Bad: `version_export.sh`, `package_builder.sh`, `test_runner.sh`
|
||||||
- Follow consistent naming conventions:
|
- Create scripts in the `scripts` directory (not `tools`)
|
||||||
- Use lowercase with underscores
|
|
||||||
- Start with a verb in imperative form
|
|
||||||
- Use clear, descriptive names
|
|
||||||
- Make scripts executable (`chmod +x`)
|
- Make scripts executable (`chmod +x`)
|
||||||
- Include proper error handling and exit codes
|
- Add new functionality to the `scripts/menu.sh` script for easy access
|
||||||
- Add usage information (viewable with `-h` or `--help`)
|
- Add usage information (viewable with `-h` or `--help`)
|
||||||
|
|
||||||
Script Header Format:
|
Script Header Format:
|
||||||
|
|
@ -440,7 +536,8 @@ Script Header Format:
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
Every script should include this header format at the top, with all sections filled out appropriately. The header provides:
|
Every script should include this header format at the top, with all sections
|
||||||
|
filled out appropriately. The header provides:
|
||||||
|
|
||||||
- Clear identification of the script
|
- Clear identification of the script
|
||||||
- Description of its purpose and functionality
|
- Description of its purpose and functionality
|
||||||
|
|
@ -450,7 +547,8 @@ Every script should include this header format at the top, with all sections fil
|
||||||
|
|
||||||
#### Script Testing
|
#### Script Testing
|
||||||
|
|
||||||
All scripts must have corresponding tests in the `scripts/tests` directory using the common test library:
|
All scripts must have corresponding tests in the `scripts/tests` directory using
|
||||||
|
the common test library:
|
||||||
|
|
||||||
1. **Test File Structure**
|
1. **Test File Structure**
|
||||||
- Name test files as `<script_name>_test.sh`
|
- Name test files as `<script_name>_test.sh`
|
||||||
|
|
@ -458,215 +556,279 @@ All scripts must have corresponding tests in the `scripts/tests` directory using
|
||||||
- Make test files executable (`chmod +x`)
|
- Make test files executable (`chmod +x`)
|
||||||
- Source the common test library (`test_lib.sh`)
|
- Source the common test library (`test_lib.sh`)
|
||||||
|
|
||||||
2. **Common Test Library**
|
2. **Common Test Library** The `test_lib.sh` library provides a standard test
|
||||||
The `test_lib.sh` library provides a standard test framework:
|
framework. See the `scripts/tests/template_test.sh` for examples of how to
|
||||||
```bash
|
set up one.
|
||||||
# Source the test library
|
|
||||||
source "$(dirname "$0")/test_lib.sh"
|
|
||||||
|
|
||||||
# Library provides:
|
|
||||||
- Color output (GREEN, RED, BLUE, NC, BOLD)
|
|
||||||
- Test counting (PASS, FAIL)
|
|
||||||
- Temporary directory management
|
|
||||||
- Standard argument parsing
|
|
||||||
- Common test functions
|
|
||||||
```
|
|
||||||
|
|
||||||
Key Functions:
|
|
||||||
- `test_case "name" "command" "expected_output" "should_succeed"`
|
|
||||||
- `print_header "text"` - Print bold header
|
|
||||||
- `print_section "text"` - Print section header in blue
|
|
||||||
- `print_info "text"` - Print verbose info
|
|
||||||
- `setup_test_env` - Create temp directory
|
|
||||||
- `cleanup_test_env` - Clean up resources
|
|
||||||
- `create_test_file "path" "content" "mode"` - Create test file
|
|
||||||
- `is_command_available "cmd"` - Check if command exists
|
|
||||||
- `wait_for_condition "cmd" timeout interval` - Wait for condition
|
|
||||||
- `report_results` - Print test summary
|
|
||||||
|
|
||||||
3. **Test Organization**
|
3. **Test Organization**
|
||||||
- Group related test cases into sections
|
- Group related test cases into sections
|
||||||
- Test each command/flag combination
|
- Test each command/flag combination
|
||||||
- Test error conditions explicitly
|
- Test error conditions explicitly
|
||||||
- Include setup and teardown if needed
|
|
||||||
- Use temporary directories for file operations
|
|
||||||
- Clean up resources in trap handlers
|
|
||||||
|
|
||||||
4. **Test Coverage**
|
4. **Test Coverage**
|
||||||
- Test main functionality
|
|
||||||
- Test error conditions
|
- Test error conditions
|
||||||
- Test input validation
|
- Test input validation
|
||||||
- Test edge cases
|
- Test edge cases
|
||||||
- Test each supported flag/option
|
- Test each supported flag/option
|
||||||
|
|
||||||
Standard test framework:
|
5. **CI Integration**
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Colors for test output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Test counters
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
# Main test case function
|
|
||||||
function test_case() {
|
|
||||||
local name=$1
|
|
||||||
local cmd=$2
|
|
||||||
local expected_output=$3
|
|
||||||
local should_succeed=${4:-true}
|
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
|
||||||
|
|
||||||
# Run command and capture output
|
|
||||||
local output
|
|
||||||
if [[ $should_succeed == "true" ]]; then
|
|
||||||
output=$($cmd 2>&1)
|
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
output=$($cmd 2>&1) || true
|
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
|
||||||
echo " Expected output to contain: '$expected_output'"
|
|
||||||
echo " Got: '$output'"
|
|
||||||
((FAIL++))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Test sections should be organized like this:
|
|
||||||
echo "Running script_name.sh tests..."
|
|
||||||
echo "------------------------------"
|
|
||||||
|
|
||||||
# Section 1: Input Validation
|
|
||||||
test_case "no arguments provided" \
|
|
||||||
"./script_name.sh" \
|
|
||||||
"error: arguments required" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Section 2: Main Functionality
|
|
||||||
test_case "basic operation" \
|
|
||||||
"./script_name.sh arg1" \
|
|
||||||
"success" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Report results
|
|
||||||
echo
|
|
||||||
echo "Test Results:"
|
|
||||||
echo "Passed: $PASS"
|
|
||||||
echo "Failed: $FAIL"
|
|
||||||
exit $FAIL
|
|
||||||
```
|
|
||||||
|
|
||||||
Example test file structure:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Test script for example_script.sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Test helper functions
|
|
||||||
setup_test_env() {
|
|
||||||
# Setup code here
|
|
||||||
}
|
|
||||||
|
|
||||||
teardown_test_env() {
|
|
||||||
# Cleanup code here
|
|
||||||
}
|
|
||||||
|
|
||||||
# Individual test cases
|
|
||||||
test_main_functionality() {
|
|
||||||
echo "Testing main functionality..."
|
|
||||||
# Test code here
|
|
||||||
}
|
|
||||||
|
|
||||||
test_error_handling() {
|
|
||||||
echo "Testing error handling..."
|
|
||||||
# Test code here
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run all tests
|
|
||||||
echo "Running example_script.sh tests..."
|
|
||||||
setup_test_env
|
|
||||||
test_main_functionality
|
|
||||||
test_error_handling
|
|
||||||
teardown_test_env
|
|
||||||
echo "All tests passed!"
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **CI Integration**
|
|
||||||
- Tests run automatically in CI
|
- Tests run automatically in CI
|
||||||
- Tests must pass before merge
|
- Tests must pass before merge
|
||||||
- Test execution is part of the validate-scripts job
|
- Test execution is part of the validate-scripts job
|
||||||
- Test failures block PR merges
|
- Test failures block PR merges
|
||||||
|
|
||||||
Example script structure:
|
##### Test Framework Architecture Pattern
|
||||||
|
|
||||||
|
The improved test framework follows this standardized pattern for all script
|
||||||
|
tests:
|
||||||
|
|
||||||
|
**Test File Template:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Script Name: example_script.sh
|
#==============================================================================
|
||||||
# Description: Brief description of what the script does
|
# script_name_test.sh
|
||||||
# Usage: ./example_script.sh [options] <arguments>
|
#==============================================================================
|
||||||
# Author: Your Name
|
#
|
||||||
# Date: YYYY-MM-DD
|
# DESCRIPTION:
|
||||||
|
# Test suite for script_name.sh functionality.
|
||||||
|
# Brief description of what aspects are tested.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# script_name_test.sh [OPTIONS]
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -v, --verbose Enable verbose test output
|
||||||
|
# --stop-on-failure Stop on first test failure
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
set -e # Exit on error
|
# Set up the script path we want to test
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
export SCRIPT_PATH="$SCRIPT_DIR/../script_name.sh"
|
||||||
|
|
||||||
# Help function
|
# Source the test framework
|
||||||
show_help() {
|
source "$SCRIPT_DIR/test_lib.sh"
|
||||||
cat << EOF
|
|
||||||
Usage: $(basename "$0") [options] <arguments>
|
|
||||||
|
|
||||||
Options:
|
# Define test functions
|
||||||
-h, --help Show this help message
|
run_tests() {
|
||||||
-v, --version Show version information
|
test_section "Help and Usage"
|
||||||
|
|
||||||
Arguments:
|
test_case "shows help message" \
|
||||||
<input> Description of input argument
|
"--help" \
|
||||||
EOF
|
"USAGE:" \
|
||||||
|
true
|
||||||
|
|
||||||
|
test_case "shows error for invalid option" \
|
||||||
|
"--invalid-option" \
|
||||||
|
"Unknown option" \
|
||||||
|
false
|
||||||
|
|
||||||
|
test_section "Core Functionality"
|
||||||
|
|
||||||
|
# Add more test cases here
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments
|
# Start the test framework and run tests
|
||||||
while [[ $# -gt 0 ]]; do
|
start_tests "$@"
|
||||||
|
run_tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Framework Features:**
|
||||||
|
|
||||||
|
- **SCRIPT_PATH Setup**: Test files must set `SCRIPT_PATH` before sourcing
|
||||||
|
`test_lib.sh` to avoid variable conflicts
|
||||||
|
- **Function-based Test Organization**: Tests are organized in a `run_tests()`
|
||||||
|
function called after framework initialization
|
||||||
|
- **Consistent Test Sections**: Use `test_section` to group related tests with
|
||||||
|
descriptive headers
|
||||||
|
- **Standard Test Case Pattern**:
|
||||||
|
`test_case "name" "args" "expected_output" "should_succeed"`
|
||||||
|
- **Framework Integration**: Call `start_tests "$@"` before running tests to
|
||||||
|
handle argument parsing and setup
|
||||||
|
|
||||||
|
##### Script Argument Parsing Pattern
|
||||||
|
|
||||||
|
All scripts should implement consistent argument parsing following this pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
main() {
|
||||||
|
# Parse command line arguments first
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-h|--help)
|
-v | --verbose)
|
||||||
show_help
|
export VERBOSE=true
|
||||||
exit 0
|
;;
|
||||||
;;
|
-h | --help)
|
||||||
*)
|
cat << 'EOF'
|
||||||
# Handle other arguments
|
USAGE:
|
||||||
;;
|
script_name.sh [OPTIONS]
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
Brief description of what the script does.
|
||||||
|
Additional details if needed.
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-v, --verbose Enable verbose output
|
||||||
|
-h, --help Show this help message
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
echo "Use --help for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
# Main script logic here
|
# Script main logic here
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Argument Parsing Features:**
|
||||||
|
|
||||||
|
- **Consistent Options**: All scripts support `-v/--verbose` and `-h/--help`
|
||||||
|
- **Early Help Exit**: Help is displayed immediately without running script
|
||||||
|
logic
|
||||||
|
- **Error Handling**: Unknown options produce helpful error messages
|
||||||
|
- **Inline Help Text**: Help is embedded in the script using heredoc syntax
|
||||||
|
|
||||||
|
##### Centralized Configuration Management
|
||||||
|
|
||||||
|
The project implements centralized version management using the `.env` file as a
|
||||||
|
single source of truth:
|
||||||
|
|
||||||
|
**Configuration Structure:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env file contents
|
||||||
|
GO_VERSION=1.23.4
|
||||||
|
GO_TOOLCHAIN=go1.23.4
|
||||||
|
```
|
||||||
|
|
||||||
|
**GitHub Actions Integration:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/ci.yml pattern
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
go-version: ${{ steps.env.outputs.go-version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- id: env
|
||||||
|
run: |
|
||||||
|
source .env
|
||||||
|
echo "go-version=$GO_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
dependent-job:
|
||||||
|
needs: setup
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ needs.setup.outputs.go-version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Synchronization Script Pattern:**
|
||||||
|
|
||||||
|
- `scripts/sync_go_version.sh` reads `.env` and updates `go.mod` accordingly
|
||||||
|
- Ensures consistency between environment configuration and Go module
|
||||||
|
requirements
|
||||||
|
- Can be extended for other configuration synchronization needs
|
||||||
|
|
||||||
|
##### Implementation Status
|
||||||
|
|
||||||
|
**✅ Implemented Patterns:**
|
||||||
|
|
||||||
|
The following scripts have been updated with the standardized patterns:
|
||||||
|
|
||||||
|
1. **scripts/export_version.sh** - Complete implementation:
|
||||||
|
- ✅ Argument parsing with `--help` and `--verbose`
|
||||||
|
- ✅ Proper error handling and logging
|
||||||
|
- ✅ Comprehensive test suite in `scripts/tests/export_version_test.sh`
|
||||||
|
- ✅ Function-based test organization
|
||||||
|
|
||||||
|
2. **scripts/setup_dev.sh** - Complete implementation:
|
||||||
|
- ✅ Argument parsing with `--help` and `--verbose`
|
||||||
|
- ✅ Script-specific help documentation
|
||||||
|
- ✅ Error handling for unknown options
|
||||||
|
- ✅ Comprehensive test suite in `scripts/tests/setup_dev_test.sh`
|
||||||
|
- ✅ Function-based test organization
|
||||||
|
|
||||||
|
3. **scripts/tests/test_lib.sh** - Framework improvements:
|
||||||
|
- ✅ Reliable library loading with fallback paths
|
||||||
|
- ✅ Safe SCRIPT_PATH variable handling
|
||||||
|
- ✅ Arithmetic operations compatible with `set -e`
|
||||||
|
- ✅ Proper script name detection
|
||||||
|
- ✅ Lazy temporary directory initialization
|
||||||
|
- ✅ Comprehensive documentation and architecture notes
|
||||||
|
|
||||||
|
4. **Centralized Configuration Management**:
|
||||||
|
- ✅ `.env` file as single source of truth for versions
|
||||||
|
- ✅ GitHub Actions CI integration with version propagation
|
||||||
|
- ✅ `scripts/sync_go_version.sh` for configuration synchronization
|
||||||
|
|
||||||
|
**🔄 Remaining Scripts to Update:**
|
||||||
|
|
||||||
|
These scripts need the same pattern implementations:
|
||||||
|
|
||||||
|
- `scripts/distribute.sh` - Needs argument parsing and testing
|
||||||
|
- `scripts/update_md_tocs.sh` - Needs argument parsing and testing
|
||||||
|
- `scripts/check_and_fix_env.sh` - Needs argument parsing and testing
|
||||||
|
- `scripts/template.sh` - Needs argument parsing and testing
|
||||||
|
- `scripts/menu.sh` - Needs argument parsing and testing
|
||||||
|
|
||||||
|
Example script structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# fix_and_update.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Runs lint fixes and checks for UTF-8 formatting issues in the project.
|
||||||
|
# Intended to help maintain code quality and formatting consistency.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# ./scripts/fix_and_update.sh
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# DEPENDENCIES:
|
||||||
|
# - trunk (for linting)
|
||||||
|
# - bash
|
||||||
|
# - ./scripts/check_utf8.sh
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Resolves to absolute path and loads library
|
||||||
|
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||||
|
|
||||||
|
main_menu() {
|
||||||
|
if false; then
|
||||||
|
show_help # Uses the script header to output usage message
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ...
|
||||||
|
# Script logic, variables and functions.
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Parse common command line arguments and hand the remaining to the script
|
||||||
|
remaining_args=$(parse_common_args "$@")
|
||||||
|
|
||||||
|
# Run main menu
|
||||||
|
main_menu
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing Principles
|
## Testing Principles
|
||||||
|
|
@ -676,7 +838,8 @@ done
|
||||||
We established a balanced approach to test organization:
|
We established a balanced approach to test organization:
|
||||||
|
|
||||||
- Use table-driven tests for simple, repetitive cases without introducing logic
|
- Use table-driven tests for simple, repetitive cases without introducing logic
|
||||||
- Use individual test functions for cases that require specific Arrange, Act, Assert steps that cannot be shared amongst other cases
|
- Use individual test functions for cases that require specific Arrange, Act,
|
||||||
|
Assert steps that cannot be shared amongst other cases
|
||||||
- Group related test cases that operate on the same API method / function
|
- Group related test cases that operate on the same API method / function
|
||||||
|
|
||||||
### 2. Code Structure
|
### 2. Code Structure
|
||||||
|
|
@ -684,7 +847,6 @@ We established a balanced approach to test organization:
|
||||||
#### Constants and Variables
|
#### Constants and Variables
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
||||||
const (
|
const (
|
||||||
manifestVersion = "1.0.0"
|
manifestVersion = "1.0.0"
|
||||||
manifestGlobalVer = "v2"
|
manifestGlobalVer = "v2"
|
||||||
|
|
@ -696,7 +858,8 @@ var (
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Define constants for fixed values where the prescence and format is only needed and the value content itself does not effect the behavior under test
|
- Define constants for fixed values where the prescence and format is only
|
||||||
|
needed and the value content itself does not effect the behavior under test
|
||||||
- Use variables for reusable test data
|
- Use variables for reusable test data
|
||||||
- Group related constants and variables together
|
- Group related constants and variables together
|
||||||
- Do not prefix constants or variables with `test`
|
- Do not prefix constants or variables with `test`
|
||||||
|
|
@ -717,7 +880,8 @@ func assertValidJSON(t *testing.T, data string) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example of using functions to abstract away details not relevant to the behavior under test
|
Example of using functions to abstract away details not relevant to the behavior
|
||||||
|
under test
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Item struct {
|
type Item struct {
|
||||||
|
|
@ -745,7 +909,7 @@ func TestItem_Description(t *testing.T) {
|
||||||
|
|
||||||
// GOOD: Clear focus, reusable arrangement, proper assertions
|
// GOOD: Clear focus, reusable arrangement, proper assertions
|
||||||
const (
|
const (
|
||||||
defaultName = "test item"
|
defaultName = "test item"
|
||||||
defaultVersion = "1.0.0"
|
defaultVersion = "1.0.0"
|
||||||
defaultTimeStr = "2025-01-01T00:00:00Z"
|
defaultTimeStr = "2025-01-01T00:00:00Z"
|
||||||
)
|
)
|
||||||
|
|
@ -778,10 +942,12 @@ func TestAddPrefixToDescription_WithValidInput_AddsPrefix(t *testing.T) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Create helper functions to reduce duplication and keeps tests focused on the arrangement inputs and how they correspond to the expected output
|
- Create helper functions to reduce duplication and keeps tests focused on the
|
||||||
|
arrangement inputs and how they correspond to the expected output
|
||||||
- Use `t.Helper()` for proper test failure reporting
|
- Use `t.Helper()` for proper test failure reporting
|
||||||
- Keep helpers focused and single-purpose
|
- Keep helpers focused and single-purpose
|
||||||
- Helper functions that require logic should go into their own file and have tests
|
- Helper functions that require logic should go into their own file and have
|
||||||
|
tests
|
||||||
|
|
||||||
### 3. Test Case Patterns
|
### 3. Test Case Patterns
|
||||||
|
|
||||||
|
|
@ -895,16 +1061,57 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) {
|
||||||
### 4. Best Practices Applied
|
### 4. Best Practices Applied
|
||||||
|
|
||||||
1. **Clear Naming**
|
1. **Clear Naming**
|
||||||
- Use descriptive test names
|
|
||||||
- Use test name formats
|
|
||||||
- `Test<function>_<arrangement>_<expectation>` for free functions, and
|
|
||||||
- `Test<interface><function>_<arrangement>_<expectation>` for interface functions.
|
|
||||||
- Name test data clearly and meaningfully
|
- Name test data clearly and meaningfully
|
||||||
- Name by abstraction, not implementation
|
- Name by abstraction, not implementation
|
||||||
- Use `expected` for expected values
|
- Use `expected` for expected values
|
||||||
- Use `actual` for function results
|
- Use `actual` for function results
|
||||||
- Keep test variables consistent across all tests
|
- Keep test variables consistent across all tests
|
||||||
- Always use "Arrange", "Act", "Assert" as step comments in tests
|
- Always use "Arrange", "Act", "Assert" as step comments in tests
|
||||||
|
- Use descriptive test name arrangement and expectation parts
|
||||||
|
- Use test name formats in a 3 part structure
|
||||||
|
- `Test<function>_<arrangement>_<expectation>` for free functions, and
|
||||||
|
- `Test<interface><function>_<arrangement>_<expectation>` for interface
|
||||||
|
functions.
|
||||||
|
- The module name is inferred
|
||||||
|
- Treat the first part as either the type function or the free function
|
||||||
|
under test
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Test<[type]<function>>_<arrangement>_<expectation>(t *testing.T) {
|
||||||
|
// Test body
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Implementation
|
||||||
|
|
||||||
|
type Logger {
|
||||||
|
debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger Logger
|
||||||
|
logger.debug = false
|
||||||
|
|
||||||
|
func (l* Logger) Log(msg string) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDebug(v bool) {
|
||||||
|
logger.debug = v
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Test
|
||||||
|
|
||||||
|
func TestLoggerLog_EmptyMessage_NothingLogged(t *testing.T) {
|
||||||
|
// Test body
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDebug_PassFalseValue_DebugMessageNotLogged(t *testing.T) {
|
||||||
|
// Test body
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
2. **Test Structure**
|
2. **Test Structure**
|
||||||
- Keep test body simple and linear
|
- Keep test body simple and linear
|
||||||
|
|
@ -921,7 +1128,6 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) {
|
||||||
|
|
||||||
4. **Test Data Management**
|
4. **Test Data Management**
|
||||||
- Centralize test data definitions
|
- Centralize test data definitions
|
||||||
- Use `<function>_<arrangement>_<artifact>` naming
|
|
||||||
- Use constants for fixed values
|
- Use constants for fixed values
|
||||||
- Abstract complex data arrangement into helpers
|
- Abstract complex data arrangement into helpers
|
||||||
|
|
||||||
|
|
@ -942,7 +1148,7 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) {
|
||||||
#### Before
|
#### Before
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func TestFeature(t *testing.T) {
|
func TestFeature_MixedArrangements_ExpectAlotOfDifferentThings(t *testing.T) {
|
||||||
// Mixed arrangement and assertions
|
// Mixed arrangement and assertions
|
||||||
// Duplicated code
|
// Duplicated code
|
||||||
// Magic values
|
// Magic values
|
||||||
|
|
@ -953,7 +1159,7 @@ func TestFeature(t *testing.T) {
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Before: Mixed concerns, unclear naming, magic values
|
// Before: Mixed concerns, unclear naming, magic values
|
||||||
func TestValidateConfig(t *testing.T) {
|
func TestValidateConfig_MissingFileAndEmptyPaths_ValidationFails(t *testing.T) {
|
||||||
c := &Config{
|
c := &Config{
|
||||||
Path: "./testdata",
|
Path: "./testdata",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
|
|
@ -972,7 +1178,7 @@ func TestValidateConfig(t *testing.T) {
|
||||||
|
|
||||||
// After: Clear structure, meaningful constants, proper test naming
|
// After: Clear structure, meaningful constants, proper test naming
|
||||||
const (
|
const (
|
||||||
testConfigPath = "./testdata"
|
testConfigPath = "./testdata"
|
||||||
defaultPort = 8080
|
defaultPort = 8080
|
||||||
defaultMaxRetries = 3
|
defaultMaxRetries = 3
|
||||||
)
|
)
|
||||||
|
|
@ -1040,4 +1246,5 @@ These improvements make the test code:
|
||||||
- More reliable
|
- More reliable
|
||||||
- More efficient to extend
|
- More efficient to extend
|
||||||
|
|
||||||
The patterns and principles can be applied across different types of tests to create a consistent and effective testing strategy.
|
The patterns and principles can be applied across different types of tests to
|
||||||
|
create a consistent and effective testing strategy.
|
||||||
|
|
@ -95,6 +95,7 @@ cache-apt-pkgs validate [packages]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Validate package names and versions
|
# Validate package names and versions
|
||||||
cache-apt-pkgs validate python3-dev=3.9.5-3 cmake=3.18.4-2
|
cache-apt-pkgs validate python3-dev=3.9.5-3 cmake=3.18.4-2
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Thank you for your interest in contributing to cache-apt-pkgs-action! This docum
|
||||||
|
|
||||||
### 📋 Prerequisites
|
### 📋 Prerequisites
|
||||||
|
|
||||||
1. 🔵 [Go 1.23 or later](https://golang.org/dl/)
|
1. 🔵 [Go 1.23.4 or later](https://golang.org/dl/)
|
||||||
2. 💻 [Visual Studio Code](https://code.visualstudio.com/) (recommended)
|
2. 💻 [Visual Studio Code](https://code.visualstudio.com/) (recommended)
|
||||||
3. 📂 [Git](https://git-scm.com/downloads)
|
3. 📂 [Git](https://git-scm.com/downloads)
|
||||||
|
|
||||||
|
|
@ -65,9 +65,9 @@ The project includes several utility scripts to help with development:
|
||||||
|
|
||||||
To access the menu system, run:
|
To access the menu system, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/menu.sh
|
./scripts/menu.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This will present an interactive menu with all available development tasks.
|
This will present an interactive menu with all available development tasks.
|
||||||
|
|
||||||
|
|
@ -266,7 +266,6 @@ To make the library available on [pkg.go.dev](https://pkg.go.dev):
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 🔄 Trigger pkg.go.dev to fetch your module:
|
2. 🔄 Trigger pkg.go.dev to fetch your module:
|
||||||
|
|
||||||
- Visit [pkg.go.dev for this module](https://pkg.go.dev/github.com/awalsh128/cache-apt-pkgs-action@v2.0.0)
|
- Visit [pkg.go.dev for this module](https://pkg.go.dev/github.com/awalsh128/cache-apt-pkgs-action@v2.0.0)
|
||||||
- Or fetch via command line:
|
- Or fetch via command line:
|
||||||
|
|
||||||
|
|
@ -275,7 +274,6 @@ To make the library available on [pkg.go.dev](https://pkg.go.dev):
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 📝 Best practices for publishing:
|
3. 📝 Best practices for publishing:
|
||||||
|
|
||||||
- Add comprehensive godoc comments
|
- Add comprehensive godoc comments
|
||||||
- Include examples in your documentation
|
- Include examples in your documentation
|
||||||
- Use semantic versioning for tags
|
- Use semantic versioning for tags
|
||||||
|
|
|
||||||
75
README.md
75
README.md
|
|
@ -9,39 +9,42 @@
|
||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
- [🚀 Quick Start](#-quick-start)
|
- [Cache APT Packages Action](#cache-apt-packages-action)
|
||||||
- [✨ Features](#-features)
|
- [🚀 Quick Start](#-quick-start)
|
||||||
- [📋 Requirements](#-requirements)
|
- [✨ Features](#-features)
|
||||||
- [🔧 Configuration](#-configuration)
|
- [📋 Requirements](#-requirements)
|
||||||
- [Inputs](#inputs)
|
- [🔧 Configuration](#-configuration)
|
||||||
- [Outputs](#outputs)
|
- [Inputs](#inputs)
|
||||||
- [📝 Usage Guide](#-usage-guide)
|
- [Outputs](#outputs)
|
||||||
- [Version Selection](#version-selection)
|
- [📝 Usage Guide](#-usage-guide)
|
||||||
- [Basic Example](#basic-example)
|
- [Version Selection](#version-selection)
|
||||||
- [Advanced Example](#advanced-example)
|
- [Basic Example](#basic-example)
|
||||||
- [🔍 Cache Details](#-cache-details)
|
- [Advanced Example](#advanced-example)
|
||||||
- [Cache Scoping](#cache-scoping)
|
- [🔍 Cache Details](#-cache-details)
|
||||||
- [Cache Keys](#cache-keys)
|
- [Cache Scoping](#cache-scoping)
|
||||||
- [Cache Invalidation](#cache-invalidation)
|
- [Cache Keys](#cache-keys)
|
||||||
- [🚨 Common Issues](#-common-issues)
|
- [Cache Invalidation](#cache-invalidation)
|
||||||
- [Permission Issues](#permission-issues)
|
- [🚨 Common Issues](#-common-issues)
|
||||||
- [Missing Dependencies](#missing-dependencies)
|
- [Permission Issues](#permission-issues)
|
||||||
- [Cache Misses](#cache-misses)
|
- [Missing Dependencies](#missing-dependencies)
|
||||||
- [🤝 Contributing](#-contributing)
|
- [Cache Misses](#cache-misses)
|
||||||
- [📜 License](#-license)
|
- [🤝 Contributing](#-contributing)
|
||||||
- [🔄 Updates and Maintenance](#-updates-and-maintenance)
|
- [📜 License](#-license)
|
||||||
- [🌟 Acknowledgements](#-acknowledgements)
|
- [🔄 Updates and Maintenance](#-updates-and-maintenance)
|
||||||
- [Getting Started](#getting-started)
|
- [🌟 Acknowledgements](#-acknowledgements)
|
||||||
- [Workflow Setup](#workflow-setup)
|
- [Getting Started](#getting-started)
|
||||||
- [Detailed Configuration](#detailed-configuration)
|
- [Workflow Setup](#workflow-setup)
|
||||||
- [Cache scopes](#cache-scopes)
|
- [Detailed Configuration](#detailed-configuration)
|
||||||
- [Example workflows](#example-workflows)
|
- [Input Parameters](#input-parameters)
|
||||||
- [Build and Deploy Doxygen Documentation](#build-and-deploy-doxygen-documentation)
|
- [Output Values](#output-values)
|
||||||
- [Simple Package Installation](#simple-package-installation)
|
- [Cache scopes](#cache-scopes)
|
||||||
- [Caveats](#caveats)
|
- [Example workflows](#example-workflows)
|
||||||
- [Edge Cases](#edge-cases)
|
- [Build and Deploy Doxygen Documentation](#build-and-deploy-doxygen-documentation)
|
||||||
- [Non-file Dependencies](#non-file-dependencies)
|
- [Simple Package Installation](#simple-package-installation)
|
||||||
- [Cache Limits](#cache-limits)
|
- [Caveats](#caveats)
|
||||||
|
- [Edge Cases](#edge-cases)
|
||||||
|
- [Non-file Dependencies](#non-file-dependencies)
|
||||||
|
- [Cache Limits](#cache-limits)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
|
|
@ -159,6 +162,7 @@ jobs:
|
||||||
### Cache Scoping
|
### Cache Scoping
|
||||||
|
|
||||||
Caches are scoped by:
|
Caches are scoped by:
|
||||||
|
|
||||||
- Package list
|
- Package list
|
||||||
- Version string
|
- Version string
|
||||||
- Branch (default branch cache available to other branches)
|
- Branch (default branch cache available to other branches)
|
||||||
|
|
@ -166,6 +170,7 @@ Caches are scoped by:
|
||||||
### Cache Keys
|
### Cache Keys
|
||||||
|
|
||||||
The action generates cache keys based on:
|
The action generates cache keys based on:
|
||||||
|
|
||||||
- Package names and versions
|
- Package names and versions
|
||||||
- System architecture
|
- System architecture
|
||||||
- Custom version string
|
- Custom version string
|
||||||
|
|
@ -173,6 +178,7 @@ The action generates cache keys based on:
|
||||||
### Cache Invalidation
|
### Cache Invalidation
|
||||||
|
|
||||||
Caches are invalidated when:
|
Caches are invalidated when:
|
||||||
|
|
||||||
- Package versions change
|
- Package versions change
|
||||||
- Custom version string changes
|
- Custom version string changes
|
||||||
- Branch cache is cleared
|
- Branch cache is cleared
|
||||||
|
|
@ -183,7 +189,7 @@ Caches are invalidated when:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
permissions:
|
permissions:
|
||||||
actions: read|write # Required for cache operations
|
actions: read|write # Required for cache operations
|
||||||
```
|
```
|
||||||
|
|
||||||
### Missing Dependencies
|
### Missing Dependencies
|
||||||
|
|
@ -209,6 +215,7 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS
|
||||||
## 🔄 Updates and Maintenance
|
## 🔄 Updates and Maintenance
|
||||||
|
|
||||||
Stay updated:
|
Stay updated:
|
||||||
|
|
||||||
- Watch this repository for releases
|
- Watch this repository for releases
|
||||||
- Check the [CHANGELOG](CHANGELOG.md)
|
- Check the [CHANGELOG](CHANGELOG.md)
|
||||||
- Follow the [security policy](SECURITY.md)
|
- Follow the [security policy](SECURITY.md)
|
||||||
|
|
|
||||||
95
action.yml
95
action.yml
|
|
@ -1,124 +1,121 @@
|
||||||
name: "Cache APT Packages"
|
name: Cache APT Packages
|
||||||
description: "Install APT based packages and cache them for future runs."
|
description: Install APT based packages and cache them for future runs.
|
||||||
author: awalsh128
|
author: awalsh128
|
||||||
branding:
|
branding:
|
||||||
icon: "hard-drive"
|
icon: hard-drive
|
||||||
color: "green"
|
color: green
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
packages:
|
packages:
|
||||||
description: "Space delimited list of packages to install. Version can be specified optionally using APT command syntax of <name>=<version> (e.g. xdot=1.2-2)."
|
description: Space delimited list of packages to install. Version can be specified optionally using APT command syntax of <name>=<version> (e.g. xdot=1.2-2).
|
||||||
required: true
|
required: true
|
||||||
default: ""
|
default: ""
|
||||||
version:
|
version:
|
||||||
description: "Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed."
|
description: Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
execute_install_scripts:
|
execute_install_scripts:
|
||||||
description: "Execute Debian package pre and post install script upon restore. See README.md caveats for more information."
|
description: Execute Debian package pre and post install script upon restore. See README.md caveats for more information.
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
refresh:
|
refresh:
|
||||||
description: "OBSOLETE: Refresh is not used by the action, use version instead."
|
description: "OBSOLETE: Refresh is not used by the action, use version instead."
|
||||||
deprecationMessage: "Refresh is not used by the action, use version instead."
|
deprecationMessage: Refresh is not used by the action, use version instead.
|
||||||
debug:
|
debug:
|
||||||
description: "Enable debugging when there are issues with action. Minor performance penalty."
|
description: Enable debugging when there are issues with action. Minor performance penalty.
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
cache-hit:
|
cache-hit:
|
||||||
description: "A boolean value to indicate a cache was found for the packages requested."
|
description: A boolean value to indicate a cache was found for the packages requested.
|
||||||
# This compound expression is needed because lhs can be empty.
|
# This compound expression is needed because lhs can be empty.
|
||||||
# Need to output true and false instead of true and nothing.
|
# Need to output true and false instead of true and nothing.
|
||||||
value: ${{ steps.load-cache.outputs.cache-hit || false }}
|
value: ${{ steps.load-cache.outputs.cache-hit || false }}
|
||||||
package-version-list:
|
package-version-list:
|
||||||
description: "The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>)."
|
description: The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>).
|
||||||
value: ${{ steps.post-cache.outputs.package-version-list }}
|
value: ${{ steps.post-cache.outputs.package-version-list }}
|
||||||
all-package-version-list:
|
all-package-version-list:
|
||||||
description: "All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>)."
|
description: All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>).
|
||||||
value: ${{ steps.post-cache.outputs.all-package-version-list }}
|
value: ${{ steps.post-cache.outputs.all-package-version-list }}
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: composite
|
||||||
env:
|
|
||||||
CACHE_DIR: ~/cache-apt-pkgs
|
|
||||||
GLOBAL_VERSION: 20250824
|
|
||||||
steps:
|
steps:
|
||||||
|
- id: set-shared-env
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "ARCH=${{ runner.arch }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "BINARY_PATH=${BINARY_PATH}" >> "${GITHUB_ENV}"
|
||||||
|
echo "CACHE_DIR=~/cache-apt-pkgs" >> "${GITHUB_ENV}"
|
||||||
|
echo "DEBUG=${{ inputs.debug }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "GLOBAL_VERSION=20250910" >> "${GITHUB_ENV}"
|
||||||
|
echo "PACKAGES=${{ inputs.packages }}" >> "${GITHUB_ENV}"
|
||||||
|
echo "VERSION=${{ inputs.version }}" >> "${GITHUB_ENV}"
|
||||||
|
env:
|
||||||
|
BINARY_PATH: ${{ github.action_path }}/scripts/distribute.sh getbinpath ${{ runner.arch }}
|
||||||
- id: install-aptfast
|
- id: install-aptfast
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if ! apt-fast --version > /dev/null 2>&1; then
|
if ! apt-fast --version > /dev/null 2>&1; then
|
||||||
"Installing apt-fast for optimized installs and updates"
|
echo "Installing apt-fast for optimized installs and updates" &&
|
||||||
/bin/bash -c "$(curl -sL https://raw.githubusercontent.com/ilikenwf/apt-fast/master/quick-install.sh)"
|
/bin/bash -c "$(curl -sL https://raw.githubusercontent.com/ilikenwf/apt-fast/master/quick-install.sh)"
|
||||||
fi
|
fi
|
||||||
- id: setup-binary
|
- id: setup-binary
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BINARY_PATH="${{ github.action_path }}/tools/distribute.sh getbinpath ${{ runner.arch }}"
|
if [[ ! -f "${BINARY_PATH}" ]]; then
|
||||||
if [ ! -f "$BINARY_PATH" ]; then
|
echo "Error: Binary not found at ${BINARY_PATH}"
|
||||||
echo "Error: Binary not found at $BINARY_PATH"
|
echo "Please ensure the action has been properly built and binaries are included in the distribute directory"
|
||||||
echo "Please ensure the action has been properly built and binaries are included in the dist directory"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- id: create-cache-key
|
- id: create-cache-key
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
${BINARY_PATH} createkey \
|
${BINARY_PATH} createkey \
|
||||||
-os-arch ${{ runner.arch }} \
|
-os-arch "${ARCH}" \
|
||||||
-plaintext-path "${CACHE_DIR}/cache_key.txt" \
|
-plaintext-path "${CACHE_DIR}/cache_key.txt" \
|
||||||
-ciphertext-path "${CACHE_DIR}/cache_key.md5" \
|
-ciphertext-path "${CACHE_DIR}/cache_key.md5" \
|
||||||
-version "${{ inputs.version }}" \
|
-version "${VERSION}" \
|
||||||
-global-version "${GLOBAL_VERSION}" \
|
-global-version "${GLOBAL_VERSION}" \
|
||||||
${{ inputs.packages }}
|
${PACKAGES}
|
||||||
echo "cache-key=$(cat $CACHE_DIR/cache_key.md5)" >> $GITHUB_OUTPUT
|
echo "cache-key=$(cat ${CACHE_DIR}/cache_key.md5)" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- id: load-cache
|
- id: load-cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
path: ${CACHE_DIR}
|
path: ${{ env.CACHE_DIR }}
|
||||||
key: cache-apt-pkgs_${{ steps.create-cache-key.outputs.cache-key }}
|
key: cache-apt-pkgs_${{ steps.create-cache-key.outputs.cache-key }}
|
||||||
|
|
||||||
- id: post-load-cache
|
- id: post-load-cache
|
||||||
# TODO get this implemented
|
# TODO get this implemented
|
||||||
# -exec-install-scripts ${{ inputs.execute_install_scripts }} \
|
# -exec-install-scripts ${{ inputs.execute_install_scripts }} \
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "$CACHE_HIT" == "true" ]; then
|
if [ "${CACHE_HIT}" == "true" ]; then
|
||||||
${BINARY_PATH} restore \
|
${BINARY_PATH} restore \
|
||||||
-cache-dir "${CACHE_DIR}" \
|
-cache-dir "${CACHE_DIR}" \
|
||||||
-restore-root "/" \
|
-restore-root "/" \
|
||||||
"$PACKAGES"
|
"${PACKAGES}"
|
||||||
else
|
else
|
||||||
${BINARY_PATH} install \
|
${BINARY_PATH} install \
|
||||||
-cache-dir "${CACHE_DIR}"
|
-cache-dir "${CACHE_DIR}" \
|
||||||
-version "${{ inputs.version }}" \
|
-version "${VERSION}" \
|
||||||
-global-version "${GLOBAL_VERSION}"
|
-global-version "${GLOBAL_VERSION}" \
|
||||||
"$PACKAGES""
|
"${PACKAGES}"
|
||||||
fi
|
fi
|
||||||
echo "package-version-list=$(cat "${CACHE_DIR}/pkgs_args.txt")" >> $GITHUB_OUTPUT
|
echo "package-version-list=\"$(cat "${CACHE_DIR}/pkgs_args.txt")\"" >> "${GITHUB_OUTPUT}"
|
||||||
echo "all-package-version-list=$(cat "${CACHE_DIR}/pkgs_installed.txt")" >> $GITHUB_OUTPUT
|
echo "all-package-version-list=\"$(cat "${CACHE_DIR}/pkgs_installed.txt")\"" >> "${GITHUB_OUTPUT}"
|
||||||
shell: bash
|
|
||||||
env:
|
env:
|
||||||
CACHE_HIT: "${{ steps.load-cache.outputs.cache-hit }}"
|
CACHE_HIT: ${{ steps.load-cache.outputs.cache-hit }}
|
||||||
EXEC_INSTALL_SCRIPTS: "${{ inputs.execute_install_scripts }}"
|
EXEC_INSTALL_SCRIPTS: ${{ inputs.execute_install_scripts }}
|
||||||
DEBUG: "${{ inputs.debug }}"
|
- id: upload-artifacts
|
||||||
PACKAGES: "${{ inputs.packages }}"
|
|
||||||
|
|
||||||
- id: upload-logs
|
|
||||||
if: ${{ inputs.debug == 'true' }}
|
if: ${{ inputs.debug == 'true' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: cache-apt-pkgs-logs_${{ env.CACHE_KEY }}
|
name: cache-apt-pkgs-logs_${{ env.CACHE_KEY }}
|
||||||
path: ~/cache-apt-pkgs/*.log
|
path: ~/cache-apt-pkgs/*.log
|
||||||
|
|
||||||
- id: save-cache
|
- id: save-cache
|
||||||
if: ${{ ! steps.load-cache.outputs.cache-hit }}
|
if: ${{ ! steps.load-cache.outputs.cache-hit }}
|
||||||
uses: actions/cache/save@v4
|
uses: actions/cache/save@v4
|
||||||
with:
|
with:
|
||||||
path: ~/cache-apt-pkgs
|
path: ~/cache-apt-pkgs
|
||||||
key: ${{ steps.load-cache.outputs.cache-primary-key }}
|
key: ${{ steps.load-cache.outputs.cache-primary-key }}
|
||||||
|
|
||||||
- id: clean-cache
|
- id: clean-cache
|
||||||
run: |
|
run: |
|
||||||
rm -rf ~/cache-apt-pkgs
|
rm -rf ~/cache-apt-pkgs
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Package main implements the cache-apt-pkgs command line tool.
|
||||||
|
// It provides functionality to cache and restore APT packages in GitHub Actions,
|
||||||
|
// with commands for creating cache keys, installing packages, and restoring from cache.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -5,37 +8,107 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/logging"
|
"awalsh128.com/cache-apt-pkgs-action/internal/logging"
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ExamplePackages provides a set of sample packages used for testing and documentation.
|
||||||
|
// It includes rolldice, xdot with a specific version, and libgtk-3-dev.
|
||||||
var ExamplePackages = pkgs.NewPackages(
|
var ExamplePackages = pkgs.NewPackages(
|
||||||
pkgs.Package{Name: "rolldice"},
|
pkgs.Package{Name: "rolldice"},
|
||||||
pkgs.Package{Name: "xdot", Version: "1.1-2"},
|
pkgs.Package{Name: "xdot", Version: "1.1-2"},
|
||||||
pkgs.Package{Name: "libgtk-3-dev"},
|
pkgs.Package{Name: "libgtk-3-dev"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// binaryName is the base name of the command executable, used in usage and error messages.
|
||||||
|
var binaryName = filepath.Base(os.Args[0])
|
||||||
|
|
||||||
|
// globalFlags defines the command-line flags that apply to all commands.
|
||||||
|
// It includes options for verbosity and help documentation.
|
||||||
|
var globalFlags = func() *flag.FlagSet {
|
||||||
|
flags := flag.NewFlagSet("global", flag.ExitOnError)
|
||||||
|
flags.BoolVar(new(bool), "verbose", false, "Enable verbose logging")
|
||||||
|
flags.BoolVar(new(bool), "v", false, "Enable verbose logging (shorthand)")
|
||||||
|
flags.BoolVar(new(bool), "help", false, "Show help")
|
||||||
|
flags.BoolVar(new(bool), "h", false, "Show help (shorthand)")
|
||||||
|
return flags
|
||||||
|
}()
|
||||||
|
|
||||||
|
func (c *Cmds) usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: %s <command> [flags] [packages]\n\n", binaryName)
|
||||||
|
fmt.Fprintf(os.Stderr, "commands:\n")
|
||||||
|
for _, cmd := range *c {
|
||||||
|
fmt.Fprintf(os.Stderr, " %s: %s\n", cmd.Name, cmd.Description)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\nflags:\n")
|
||||||
|
// Print global flags (from any command, since they are the same)
|
||||||
|
globalFlags.VisitAll(func(f *flag.Flag) {
|
||||||
|
fmt.Fprintf(os.Stderr, " -%s: %s\n", f.Name, f.Usage)
|
||||||
|
})
|
||||||
|
fmt.Fprintf(os.Stderr, "\nUse \"%s <command> --help\" for more information about a command.\n", binaryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd represents a command-line subcommand with its associated flags and behavior.
|
||||||
|
// Each command has a name, description, set of flags, and a function to execute the command.
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
Name string
|
// Name is the command identifier used in CLI arguments
|
||||||
Description string
|
Name string
|
||||||
Flags *flag.FlagSet
|
// Description explains what the command does
|
||||||
Examples []string // added Examples field for command usage examples
|
Description string
|
||||||
ExamplePackages pkgs.Packages
|
// Flags contains the command-specific command-line flags
|
||||||
|
Flags *flag.FlagSet
|
||||||
|
// Run executes the command with the given packages and returns any errors
|
||||||
Run func(cmd *Cmd, pkgArgs pkgs.Packages) error
|
Run func(cmd *Cmd, pkgArgs pkgs.Packages) error
|
||||||
|
Examples []string
|
||||||
|
ExamplePackages pkgs.Packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCmd creates a new command with the given name, description, examples, and run function.
|
||||||
|
// It automatically includes global flags and sets up the usage documentation.
|
||||||
|
// The returned Cmd is ready to be used as a subcommand in the CLI.
|
||||||
|
func NewCmd(name, description string, examples []string, runFunc func(cmd *Cmd, pkgArgs pkgs.Packages) error) *Cmd {
|
||||||
|
flags := flag.NewFlagSet(name, flag.ExitOnError)
|
||||||
|
globalFlags.VisitAll(func(f *flag.Flag) {
|
||||||
|
flags.Var(f.Value, f.Name, f.Usage)
|
||||||
|
})
|
||||||
|
flags.Usage = func() {
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: %s %s [flags] [packages]\n\n%s\n\n", binaryName, name, description)
|
||||||
|
fmt.Fprintf(os.Stderr, "flags:\n")
|
||||||
|
flags.VisitAll(func(f *flag.Flag) {
|
||||||
|
fmt.Fprintf(os.Stderr, " -%s: %s\n", f.Name, f.Usage)
|
||||||
|
})
|
||||||
|
fmt.Fprintf(os.Stderr, "\nexamples:\n")
|
||||||
|
for _, example := range examples {
|
||||||
|
fmt.Fprintf(os.Stderr, " %s %s %s\n", binaryName, name, example)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Cmd{
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
Flags: flags,
|
||||||
|
Run: runFunc,
|
||||||
|
Examples: examples,
|
||||||
|
ExamplePackages: ExamplePackages,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringFlag returns the string value of a flag by name.
|
// StringFlag returns the string value of a flag by name.
|
||||||
|
// It panics if the flag does not exist, so ensure the flag exists before calling.
|
||||||
func (c *Cmd) StringFlag(name string) string {
|
func (c *Cmd) StringFlag(name string) string {
|
||||||
return c.Flags.Lookup(name).Value.String()
|
return c.Flags.Lookup(name).Value.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// binaryName returns the base name of the command without the path
|
// Cmds is a collection of subcommands indexed by their names.
|
||||||
var binaryName = filepath.Base(os.Args[0])
|
// It provides methods for managing and executing CLI subcommands.
|
||||||
|
|
||||||
type Cmds map[string]*Cmd
|
type Cmds map[string]*Cmd
|
||||||
|
|
||||||
|
// parseFlags processes command line arguments for the command.
|
||||||
|
// It validates required flags and parses package arguments.
|
||||||
|
// Returns the parsed package arguments or exits with an error if validation fails.
|
||||||
func (c *Cmd) parseFlags() pkgs.Packages {
|
func (c *Cmd) parseFlags() pkgs.Packages {
|
||||||
|
logging.Debug("Parsing flags for command %q with args: %v", c.Name, os.Args[2:])
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 3 {
|
||||||
logging.Fatalf("command %q requires arguments", c.Name)
|
logging.Fatalf("command %q requires arguments", c.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -47,8 +120,11 @@ func (c *Cmd) parseFlags() pkgs.Packages {
|
||||||
// Check for missing required flags
|
// Check for missing required flags
|
||||||
missingFlagNames := []string{}
|
missingFlagNames := []string{}
|
||||||
c.Flags.VisitAll(func(f *flag.Flag) {
|
c.Flags.VisitAll(func(f *flag.Flag) {
|
||||||
// Consider all flags as required
|
// Skip all global flags since they are considered optional
|
||||||
if f.Value.String() == "" && f.DefValue == "" && f.Name != "help" {
|
if gf := globalFlags.Lookup(f.Name); gf != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.DefValue == "" && f.Value.String() == "" {
|
||||||
logging.Info("Missing required flag: %s", f.Name)
|
logging.Info("Missing required flag: %s", f.Name)
|
||||||
missingFlagNames = append(missingFlagNames, f.Name)
|
missingFlagNames = append(missingFlagNames, f.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -56,15 +132,19 @@ func (c *Cmd) parseFlags() pkgs.Packages {
|
||||||
if len(missingFlagNames) > 0 {
|
if len(missingFlagNames) > 0 {
|
||||||
logging.Fatalf("missing required flags for command %q: %s", c.Name, missingFlagNames)
|
logging.Fatalf("missing required flags for command %q: %s", c.Name, missingFlagNames)
|
||||||
}
|
}
|
||||||
|
logging.Debug("Parsed flags successfully")
|
||||||
|
|
||||||
// Parse the remaining arguments as package arguments
|
// Parse the remaining arguments as package arguments
|
||||||
pkgArgs, err := pkgs.ParsePackageArgs(c.Flags.Args())
|
pkgArgs, err := pkgs.ParsePackageArgs(c.Flags.Args())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Fatalf("failed to parse package arguments for command %q: %v", c.Name, err)
|
logging.Fatalf("failed to parse package arguments for command %q: %v", c.Name, err)
|
||||||
}
|
}
|
||||||
|
logging.Debug("Parsed package arguments:\n%s", strings.Join(c.Flags.Args(), "\n "))
|
||||||
return pkgArgs
|
return pkgArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add registers a new command to the command set.
|
||||||
|
// Returns an error if a command with the same name already exists.
|
||||||
func (c *Cmds) Add(cmd *Cmd) error {
|
func (c *Cmds) Add(cmd *Cmd) error {
|
||||||
if _, exists := (*c)[cmd.Name]; exists {
|
if _, exists := (*c)[cmd.Name]; exists {
|
||||||
return fmt.Errorf("command %q already exists", cmd.Name)
|
return fmt.Errorf("command %q already exists", cmd.Name)
|
||||||
|
|
@ -72,97 +152,41 @@ func (c *Cmds) Add(cmd *Cmd) error {
|
||||||
(*c)[cmd.Name] = cmd
|
(*c)[cmd.Name] = cmd
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get retrieves a command by name.
|
||||||
|
// Returns the command and true if found, or nil and false if not found.
|
||||||
func (c *Cmds) Get(name string) (*Cmd, bool) {
|
func (c *Cmds) Get(name string) (*Cmd, bool) {
|
||||||
cmd, ok := (*c)[name]
|
cmd, ok := (*c)[name]
|
||||||
return cmd, ok
|
return cmd, ok
|
||||||
}
|
}
|
||||||
func (c *Cmd) getFlagCount() int {
|
|
||||||
count := 0
|
|
||||||
c.Flags.VisitAll(func(f *flag.Flag) {
|
|
||||||
count++
|
|
||||||
})
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cmd) help() {
|
|
||||||
if c.getFlagCount() == 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: %s %s [packages]\n\n", binaryName, c.Name)
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n\n", c.Description)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: %s %s [flags] [packages]\n\n", binaryName, c.Name)
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n\n", c.Description)
|
|
||||||
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
||||||
c.Flags.PrintDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ExamplePackages == nil && len(c.Examples) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
|
||||||
if len(c.Examples) == 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, " %s %s %s\n", binaryName, c.Name, c.ExamplePackages.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, example := range c.Examples {
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
" %s %s %s %s\n",
|
|
||||||
binaryName,
|
|
||||||
c.Name,
|
|
||||||
example,
|
|
||||||
c.ExamplePackages.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printUsage(cmds Cmds) {
|
|
||||||
fmt.Fprintf(os.Stderr, "usage: %s <command> [flags] [packages]\n\n", binaryName)
|
|
||||||
fmt.Fprintf(os.Stderr, "commands:\n")
|
|
||||||
|
|
||||||
// Get max length for alignment
|
|
||||||
maxLen := 0
|
|
||||||
for name := range cmds {
|
|
||||||
if len(name) > maxLen {
|
|
||||||
maxLen = len(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print aligned command descriptions
|
|
||||||
for name, cmd := range cmds {
|
|
||||||
fmt.Fprintf(os.Stderr, " %-*s %s\n", maxLen, name, cmd.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
"\nUse \"%s <command> --help\" for more information about a command\n",
|
|
||||||
binaryName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Parse processes the command line arguments to determine the command to run
|
||||||
|
// and its package arguments. Handles help requests and invalid commands.
|
||||||
|
// Returns the selected command and its parsed package arguments, or exits on error.
|
||||||
func (c *Cmds) Parse() (*Cmd, pkgs.Packages) {
|
func (c *Cmds) Parse() (*Cmd, pkgs.Packages) {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
fmt.Fprintf(os.Stderr, "error: no command specified\n\n")
|
fmt.Fprintf(os.Stderr, "error: no command specified\n\n")
|
||||||
printUsage(*c)
|
c.usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdName := os.Args[1]
|
cmdName := os.Args[1]
|
||||||
if cmdName == "--help" || cmdName == "-h" {
|
if cmdName == "--help" || cmdName == "-h" {
|
||||||
printUsage(*c)
|
c.usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, ok := c.Get(cmdName)
|
cmd, ok := c.Get(cmdName)
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.Fprintf(os.Stderr, "error: unknown command %q\n\n", binaryName)
|
fmt.Fprintf(os.Stderr, "error: unknown command %q\n\n", binaryName)
|
||||||
printUsage(*c)
|
c.usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle command-specific help
|
// Handle command-specific help
|
||||||
for _, arg := range os.Args[2:] {
|
for _, arg := range os.Args[2:] {
|
||||||
if arg == "--help" || arg == "-h" {
|
if arg == "--help" || arg == "-h" {
|
||||||
cmd.help()
|
c.usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,13 +194,15 @@ func (c *Cmds) Parse() (*Cmd, pkgs.Packages) {
|
||||||
pkgArgs := cmd.parseFlags()
|
pkgArgs := cmd.parseFlags()
|
||||||
if pkgArgs == nil {
|
if pkgArgs == nil {
|
||||||
fmt.Fprintf(os.Stderr, "error: no package arguments specified for command %q\n\n", cmd.Name)
|
fmt.Fprintf(os.Stderr, "error: no package arguments specified for command %q\n\n", cmd.Name)
|
||||||
cmd.help()
|
cmd.Flags.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd, pkgArgs
|
return cmd, pkgArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateCmds initializes a new command set with the provided commands.
|
||||||
|
// Each command is added to the set, and the resulting set is returned.
|
||||||
func CreateCmds(cmd ...*Cmd) *Cmds {
|
func CreateCmds(cmd ...*Cmd) *Cmds {
|
||||||
commands := &Cmds{}
|
commands := &Cmds{}
|
||||||
for _, c := range cmd {
|
for _, c := range cmd {
|
||||||
|
|
|
||||||
177
cmd/cache_apt_pkgs/cmdflags_test.go
Normal file
177
cmd/cache_apt_pkgs/cmdflags_test.go
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagSetName = "test_flag_set_name"
|
||||||
|
flagName = "test-flag"
|
||||||
|
flagValue = "test_flag_value"
|
||||||
|
flagDefaultValue = "test_default_flag_value"
|
||||||
|
cmdName = "test-command-name"
|
||||||
|
cmdName1 = "test-command-name1"
|
||||||
|
cmdName2 = "test-command-name2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmd_StringFlag(t *testing.T) {
|
||||||
|
cmd := &Cmd{
|
||||||
|
Name: cmdName,
|
||||||
|
Flags: flag.NewFlagSet(flagSetName, flag.ContinueOnError),
|
||||||
|
}
|
||||||
|
cmd.Flags.String(flagName, flagDefaultValue, "test flag")
|
||||||
|
|
||||||
|
// Parse some args to set the flag value
|
||||||
|
cmd.Flags.Set(flagName, flagValue)
|
||||||
|
|
||||||
|
result := cmd.StringFlag(flagName)
|
||||||
|
if result != flagValue {
|
||||||
|
t.Errorf("Expected 'custom-value', got '%s'", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmds_Add(t *testing.T) {
|
||||||
|
cmds := &Cmds{}
|
||||||
|
*cmds = make(map[string]*Cmd)
|
||||||
|
|
||||||
|
cmd := &Cmd{Name: "test"}
|
||||||
|
|
||||||
|
err := cmds.Add(cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error adding command: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add the same command again
|
||||||
|
err = cmds.Add(cmd)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when adding duplicate command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmds_Get(t *testing.T) {
|
||||||
|
cmds := &Cmds{}
|
||||||
|
*cmds = make(map[string]*Cmd)
|
||||||
|
|
||||||
|
cmd := &Cmd{Name: cmdName}
|
||||||
|
cmds.Add(cmd)
|
||||||
|
|
||||||
|
retrieved, ok := cmds.Get(cmdName)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected to find command '%s'", cmdName)
|
||||||
|
}
|
||||||
|
if retrieved.Name != cmdName {
|
||||||
|
t.Errorf("Expected command name '%s', got '%s'", cmdName, retrieved.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = cmds.Get("nonexistent-cmd")
|
||||||
|
if ok {
|
||||||
|
t.Error("Expected not to find command 'nonexistent-cmd'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateCmds(t *testing.T) {
|
||||||
|
cmd1 := &Cmd{Name: cmdName1}
|
||||||
|
cmd2 := &Cmd{Name: cmdName2}
|
||||||
|
|
||||||
|
cmds := CreateCmds(cmd1, cmd2)
|
||||||
|
|
||||||
|
if cmds == nil {
|
||||||
|
t.Fatal("CreateCmds returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cmds.Get(cmdName1); !ok {
|
||||||
|
t.Errorf("Expected to find %s", cmdName1)
|
||||||
|
}
|
||||||
|
if _, ok := cmds.Get(cmdName2); !ok {
|
||||||
|
t.Errorf("Expected to find %s", cmdName2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_ParseFlags(t *testing.T) {
|
||||||
|
origArgs := os.Args
|
||||||
|
defer func() { os.Args = origArgs }()
|
||||||
|
|
||||||
|
t.Run("missing command", func(t *testing.T) {
|
||||||
|
// Test the condition that would trigger the missing command error
|
||||||
|
// without actually calling Parse() which would exit the test process
|
||||||
|
os.Args = []string{binaryName}
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
t.Log("Successfully detected missing command condition")
|
||||||
|
} else {
|
||||||
|
t.Error("Expected os.Args to have fewer than 2 elements")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const argExample = "test-package"
|
||||||
|
const requiredFlagName = "required-flag"
|
||||||
|
|
||||||
|
t.Run("missing required flags", func(t *testing.T) {
|
||||||
|
// This test also has issues because Parse() eventually calls os.Exit
|
||||||
|
// Let's test the flag parsing logic more directly
|
||||||
|
cmd := NewCmd(flagSetName, "Test command", []string{argExample}, func(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
cmd.Flags.String(requiredFlagName, "", "required flag description")
|
||||||
|
|
||||||
|
// Test that the flag was added
|
||||||
|
requiredFlag := cmd.Flags.Lookup(requiredFlagName)
|
||||||
|
if requiredFlag == nil {
|
||||||
|
t.Error("Expected required-flag to be registered")
|
||||||
|
}
|
||||||
|
if requiredFlag.DefValue != "" {
|
||||||
|
t.Error("Expected required-flag to have empty default value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing package arguments", func(t *testing.T) {
|
||||||
|
// Test the condition without calling Parse()
|
||||||
|
os.Args = []string{binaryName, cmdName}
|
||||||
|
|
||||||
|
if len(os.Args) >= 2 {
|
||||||
|
t.Log("Command name would be available, but package args would be missing")
|
||||||
|
} else {
|
||||||
|
t.Error("Expected at least 2 args for this test")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pkgArg1 = "test-package=1.1-beta"
|
||||||
|
const pkgArg2 = "test-package=2.0"
|
||||||
|
|
||||||
|
t.Run("valid command with packages", func(t *testing.T) {
|
||||||
|
// Test argument parsing without calling the full Parse() method
|
||||||
|
os.Args = []string{binaryName, cmdName, pkgArg1, pkgArg2}
|
||||||
|
|
||||||
|
if len(os.Args) >= 4 {
|
||||||
|
actualCmdName := os.Args[1]
|
||||||
|
actualPkgArgs := os.Args[2:]
|
||||||
|
|
||||||
|
if actualCmdName != "test" {
|
||||||
|
t.Errorf("Expected command '%s', got %s", cmdName, actualCmdName)
|
||||||
|
}
|
||||||
|
if len(actualPkgArgs) != 2 {
|
||||||
|
t.Errorf("Expected 2 package args, got %d", len(actualPkgArgs))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error("Expected at least 4 args for this test")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("help flag detection", func(t *testing.T) {
|
||||||
|
// Test help flag detection logic
|
||||||
|
os.Args = []string{binaryName, "--help"}
|
||||||
|
|
||||||
|
if len(os.Args) >= 2 {
|
||||||
|
cmdName := os.Args[1]
|
||||||
|
if cmdName == "--help" || cmdName == "-h" {
|
||||||
|
t.Log("Successfully detected help flag")
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected help flag, got %s", cmdName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,43 +1,50 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/cache"
|
"awalsh128.com/cache-apt-pkgs-action/internal/cache"
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/logging"
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createKey(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
func createKey(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
||||||
key := cache.Key{
|
key, err := cache.NewKey(
|
||||||
Packages: pkgArgs,
|
pkgArgs,
|
||||||
Version: cmd.StringFlag("version"),
|
cmd.StringFlag("version"),
|
||||||
GlobalVersion: cmd.StringFlag("global-version"),
|
cmd.StringFlag("global-version"),
|
||||||
OsArch: cmd.StringFlag("os-arch"),
|
cmd.StringFlag("os-arch"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create cache key: %w", err)
|
||||||
}
|
}
|
||||||
|
logging.Info("Created cache key: %s (%x)", key.String(), key.Hash())
|
||||||
|
|
||||||
cacheDir := cmd.StringFlag("cache-dir")
|
cacheDir := cmd.StringFlag("cache-dir")
|
||||||
|
|
||||||
|
plaintextPath := filepath.Join(cacheDir, "cache_key.txt")
|
||||||
|
ciphertextPath := filepath.Join(cacheDir, "cache_key.md5")
|
||||||
if err := key.Write(
|
if err := key.Write(
|
||||||
filepath.Join(cacheDir, "cache_key.txt"),
|
plaintextPath,
|
||||||
filepath.Join(cacheDir, "cache_key.md5")); err != nil {
|
ciphertextPath); err != nil {
|
||||||
return fmt.Errorf("failed to write cache key: %w", err)
|
return fmt.Errorf("failed to write cache keys: %w", err)
|
||||||
}
|
}
|
||||||
|
logging.Info("Wrote cache key files:\n %s\n %s", plaintextPath, ciphertextPath)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCreateKeyCmd() *Cmd {
|
func GetCreateKeyCmd() *Cmd {
|
||||||
cmd := &Cmd{
|
examples := []string{
|
||||||
Name: "createkey",
|
"--os-arch amd64 --cache-dir ~/cache_dir --version 1.0.0 --global-version 1",
|
||||||
Description: "Create a cache key based on the provided options",
|
"--os-arch x86_64 --cache-dir /tmp/cache_dir --version v2 --global-version 2",
|
||||||
Flags: flag.NewFlagSet("createkey", flag.ExitOnError),
|
|
||||||
Run: createKey,
|
|
||||||
}
|
}
|
||||||
|
cmd := NewCmd("createkey", "Create a cache key based on the provided options", examples, createKey)
|
||||||
cmd.Flags.String("os-arch", runtime.GOARCH,
|
cmd.Flags.String("os-arch", runtime.GOARCH,
|
||||||
"OS architecture to use in the cache key.\n"+
|
"OS architecture to use in the cache key.\n"+
|
||||||
"Action may be called from different runners in a different OS. This ensures the right one is fetched")
|
"Action may be called from different runners in a different OS. This ensures the right one is fetched")
|
||||||
|
cmd.Flags.String("cache-dir", "", "Directory that holds the cached packages, JSON manifest and package lists in text format")
|
||||||
cmd.Flags.String("plaintext-path", "", "Path to the plaintext cache key file")
|
cmd.Flags.String("plaintext-path", "", "Path to the plaintext cache key file")
|
||||||
cmd.Flags.String("ciphertext-path", "", "Path to the hashed cache key file")
|
cmd.Flags.String("ciphertext-path", "", "Path to the hashed cache key file")
|
||||||
cmd.Flags.String("version", "", "Version of the cache key to force cache invalidation")
|
cmd.Flags.String("version", "", "Version of the cache key to force cache invalidation")
|
||||||
|
|
|
||||||
133
cmd/cache_apt_pkgs/create_key_test.go
Normal file
133
cmd/cache_apt_pkgs/create_key_test.go
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCreateKeyCmd(t *testing.T) {
|
||||||
|
cmd := GetCreateKeyCmd()
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("GetCreateKeyCmd returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Name != "createkey" {
|
||||||
|
t.Errorf("Expected command name 'createkey', got '%s'", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Expected non-empty description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags == nil {
|
||||||
|
t.Fatal("Expected flags to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Fatal("Expected Run function to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that required flags are present
|
||||||
|
expectedFlags := []string{
|
||||||
|
"os-arch",
|
||||||
|
"plaintext-path",
|
||||||
|
"ciphertext-path",
|
||||||
|
"version",
|
||||||
|
"global-version",
|
||||||
|
"cache-dir",
|
||||||
|
}
|
||||||
|
for _, flagName := range expectedFlags {
|
||||||
|
if cmd.Flags.Lookup(flagName) == nil {
|
||||||
|
t.Errorf("Expected flag '%s' to be defined", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check default value for os-arch
|
||||||
|
osArchFlag := cmd.Flags.Lookup("os-arch")
|
||||||
|
if osArchFlag != nil && osArchFlag.DefValue != runtime.GOARCH {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected os-arch default to be '%s', got '%s'",
|
||||||
|
runtime.GOARCH,
|
||||||
|
osArchFlag.DefValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKey_Success(t *testing.T) {
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tmpDir, err := os.MkdirTemp("", "cache_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Create a mock command with required flags
|
||||||
|
cmd := GetCreateKeyCmd()
|
||||||
|
cmd.Flags.Set("version", "1.0")
|
||||||
|
cmd.Flags.Set("global-version", "1.0")
|
||||||
|
cmd.Flags.Set("os-arch", "amd64")
|
||||||
|
cmd.Flags.Set("cache-dir", tmpDir)
|
||||||
|
|
||||||
|
// Create test packages
|
||||||
|
packages := pkgs.NewPackages(pkgs.Package{Name: "test-package"})
|
||||||
|
|
||||||
|
// Run the createKey function
|
||||||
|
err = createKey(cmd, packages)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createKey failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that cache key files were created
|
||||||
|
keyFile := filepath.Join(tmpDir, "cache_key.txt")
|
||||||
|
md5File := filepath.Join(tmpDir, "cache_key.md5")
|
||||||
|
|
||||||
|
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||||
|
t.Error("cache_key.txt was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(md5File); os.IsNotExist(err) {
|
||||||
|
t.Error("cache_key.md5 was not created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKey_InvalidCacheDir(t *testing.T) {
|
||||||
|
cmd := GetCreateKeyCmd()
|
||||||
|
cmd.Flags.Set("version", "1.0")
|
||||||
|
cmd.Flags.Set("global-version", "1.0")
|
||||||
|
cmd.Flags.Set("os-arch", "amd64")
|
||||||
|
cmd.Flags.Set("cache-dir", "/nonexistent/directory")
|
||||||
|
|
||||||
|
packages := pkgs.NewPackages(pkgs.Package{Name: "test-package"})
|
||||||
|
|
||||||
|
err := createKey(cmd, packages)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when using invalid cache directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKey_EmptyPackages(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "cache_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cmd := GetCreateKeyCmd()
|
||||||
|
cmd.Flags.Set("version", "1.0")
|
||||||
|
cmd.Flags.Set("global-version", "1.0")
|
||||||
|
cmd.Flags.Set("os-arch", "amd64")
|
||||||
|
cmd.Flags.Set("cache-dir", tmpDir)
|
||||||
|
|
||||||
|
// Empty packages
|
||||||
|
packages := pkgs.NewPackages()
|
||||||
|
|
||||||
|
err = createKey(cmd, packages)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createKey should handle empty packages, got error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/cache"
|
"awalsh128.com/cache-apt-pkgs-action/internal/cache"
|
||||||
|
|
@ -18,19 +19,21 @@ func install(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
||||||
return fmt.Errorf("error initializing APT: %v", err)
|
return fmt.Errorf("error initializing APT: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.Info("Installing packages.")
|
logging.Info("Installing packages:\n%s.", strings.Join(pkgArgs.StringArray(), "\n "))
|
||||||
logging.Debug("Package list: %v.", pkgArgs)
|
|
||||||
|
|
||||||
installedPkgs, err := apt.Install(pkgArgs)
|
installedPkgs, err := apt.Install(pkgArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error installing packages: %v", err)
|
return fmt.Errorf("error installing packages: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestKey := cache.Key{
|
manifestKey, err := cache.NewKey(
|
||||||
Packages: pkgArgs,
|
pkgArgs,
|
||||||
Version: cmd.StringFlag("version"),
|
cmd.StringFlag("version"),
|
||||||
GlobalVersion: cmd.StringFlag("global-version"),
|
cmd.StringFlag("global-version"),
|
||||||
OsArch: runtime.GOARCH,
|
runtime.GOARCH,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating manifest key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pkgManifests := make([]cache.ManifestPackage, installedPkgs.Len())
|
pkgManifests := make([]cache.ManifestPackage, installedPkgs.Len())
|
||||||
|
|
@ -40,6 +43,7 @@ func install(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
logging.Debug("Package %s installed files:\n%s", pkg.String(), strings.Join(files, "\n"))
|
||||||
pkgManifests[i] = cache.ManifestPackage{
|
pkgManifests[i] = cache.ManifestPackage{
|
||||||
Package: *pkg,
|
Package: *pkg,
|
||||||
Filepaths: files,
|
Filepaths: files,
|
||||||
|
|
@ -52,10 +56,11 @@ func install(cmd *Cmd, pkgArgs pkgs.Packages) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestPath := filepath.Join(cmd.StringFlag("cache-dir"), "manifest.json")
|
manifestPath := filepath.Join(cmd.StringFlag("cache-dir"), "manifest.json")
|
||||||
|
logging.Info("Writing manifest to %s.", manifestPath)
|
||||||
if err := cache.Write(manifestPath, manifest); err != nil {
|
if err := cache.Write(manifestPath, manifest); err != nil {
|
||||||
return fmt.Errorf("error writing manifest to %s: %v", manifestPath, err)
|
return fmt.Errorf("error writing manifest to %s: %v", manifestPath, err)
|
||||||
}
|
}
|
||||||
logging.Info("Writing manifest to %s.", manifestPath)
|
logging.Info("Wrote manifest to %s.", manifestPath)
|
||||||
logging.Info("Completed package installation.")
|
logging.Info("Completed package installation.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
cmd/cache_apt_pkgs/install_test.go
Normal file
111
cmd/cache_apt_pkgs/install_test.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetInstallCmd(t *testing.T) {
|
||||||
|
cmd := GetInstallCmd()
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("GetInstallCmd returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Name != "install" {
|
||||||
|
t.Errorf("Expected command name 'install', got '%s'", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Expected non-empty description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags == nil {
|
||||||
|
t.Fatal("Expected flags to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Fatal("Expected Run function to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ExamplePackages == nil {
|
||||||
|
t.Error("Expected ExamplePackages to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Examples) == 0 {
|
||||||
|
t.Error("Expected Examples to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that required flags are present
|
||||||
|
expectedFlags := []string{"cache-dir", "version", "global-version"}
|
||||||
|
for _, flagName := range expectedFlags {
|
||||||
|
if cmd.Flags.Lookup(flagName) == nil {
|
||||||
|
t.Errorf("Expected flag '%s' to be defined", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Testing the actual install function requires APT and system-level access
|
||||||
|
// This test focuses on the command structure and error handling
|
||||||
|
func TestInstall_Structure(t *testing.T) {
|
||||||
|
cmd := GetInstallCmd()
|
||||||
|
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tmpDir, err := os.MkdirTemp("", "install_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Set up command flags
|
||||||
|
cmd.Flags.Set("cache-dir", tmpDir)
|
||||||
|
cmd.Flags.Set("version", "1.0")
|
||||||
|
cmd.Flags.Set("global-version", "1.0")
|
||||||
|
|
||||||
|
packages := pkgs.NewPackages(pkgs.Package{Name: "test-package"})
|
||||||
|
|
||||||
|
// The install function will likely fail in test environment without APT
|
||||||
|
// but we can verify the function exists and is callable
|
||||||
|
err = install(cmd, packages)
|
||||||
|
|
||||||
|
// We expect an error because APT is likely not available in test environment
|
||||||
|
// The important thing is that the function doesn't panic
|
||||||
|
if err == nil {
|
||||||
|
// If no error, check that manifest files were created
|
||||||
|
manifestFile := filepath.Join(tmpDir, "manifest.json")
|
||||||
|
if _, err := os.Stat(manifestFile); os.IsNotExist(err) {
|
||||||
|
t.Log(
|
||||||
|
"Note: install succeeded but manifest.json not found - this may be expected in test environment",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Logf("install function returned expected error in test environment: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstall_EmptyPackages(t *testing.T) {
|
||||||
|
cmd := GetInstallCmd()
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "install_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cmd.Flags.Set("cache-dir", tmpDir)
|
||||||
|
cmd.Flags.Set("version", "1.0")
|
||||||
|
cmd.Flags.Set("global-version", "1.0")
|
||||||
|
|
||||||
|
packages := pkgs.NewPackages()
|
||||||
|
|
||||||
|
// The install function should handle empty packages gracefully
|
||||||
|
err = install(cmd, packages)
|
||||||
|
|
||||||
|
// We expect this to fail due to APT not being available, but it shouldn't panic
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("install with empty packages returned expected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logging.Init("cache_apt_pkgs", true)
|
logging.Init(true)
|
||||||
|
|
||||||
commands := CreateCmds(
|
commands := CreateCmds(
|
||||||
GetCreateKeyCmd(),
|
GetCreateKeyCmd(),
|
||||||
|
|
|
||||||
154
cmd/cache_apt_pkgs/main_integration_test.go
Normal file
154
cmd/cache_apt_pkgs/main_integration_test.go
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Integration test for the real commands used by main.
|
||||||
|
func TestIntegration_MainCommands(t *testing.T) {
|
||||||
|
// Build the binary first
|
||||||
|
binaryPath := filepath.Join(t.TempDir(), "cache-apt-pkgs")
|
||||||
|
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
|
||||||
|
cmd.Dir = "."
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("Failed to build binary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases for different subcommands
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "help",
|
||||||
|
args: []string{"--help"},
|
||||||
|
expectError: false, // --help exits with 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_args",
|
||||||
|
args: []string{},
|
||||||
|
expectError: true, // no command specified
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown_command",
|
||||||
|
args: []string{"unknown"},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "createkey_help",
|
||||||
|
args: []string{"createkey", "--help"},
|
||||||
|
expectError: false, // command help exits with 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "install_help",
|
||||||
|
args: []string{"install", "--help"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restore_help",
|
||||||
|
args: []string{"restore", "--help"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validate_help",
|
||||||
|
args: []string{"validate", "--help"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cmd := exec.Command(binaryPath, tc.args...)
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Errorf("Expected error for %s but got none", tc.name)
|
||||||
|
}
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Errorf("Expected no error for %s but got: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that commands can be executed (they may fail due to missing dependencies, but shouldn't crash)
|
||||||
|
func TestIntegration_CommandExecution(t *testing.T) {
|
||||||
|
// Create temp directory for testing
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Build the binary
|
||||||
|
binaryPath := filepath.Join(tmpDir, "cache-apt-pkgs")
|
||||||
|
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
|
||||||
|
cmd.Dir = "."
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Fatalf("Failed to build binary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test createkey with minimal args (should work without APT)
|
||||||
|
t.Run("createkey_execution", func(t *testing.T) {
|
||||||
|
cacheDir := filepath.Join(tmpDir, "cache")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create cache dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(binaryPath, "createkey",
|
||||||
|
"--cache-dir", cacheDir,
|
||||||
|
"--version", "1.0",
|
||||||
|
"--global-version", "1.0",
|
||||||
|
"test-package")
|
||||||
|
|
||||||
|
// This should succeed since createkey doesn't require APT
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
t.Logf("createkey execution failed (may be expected in test environment): %v", err)
|
||||||
|
} else {
|
||||||
|
// Check if cache key files were created
|
||||||
|
keyFile := filepath.Join(cacheDir, "cache_key.txt")
|
||||||
|
md5File := filepath.Join(cacheDir, "cache_key.md5")
|
||||||
|
|
||||||
|
if _, err := os.Stat(keyFile); err != nil {
|
||||||
|
t.Errorf("cache_key.txt was not created: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(md5File); err != nil {
|
||||||
|
t.Errorf("cache_key.md5 was not created: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test other commands (expected to fail without APT but shouldn't crash)
|
||||||
|
testCommands := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"validate", []string{"validate", "test-package"}},
|
||||||
|
{
|
||||||
|
"install",
|
||||||
|
[]string{
|
||||||
|
"install",
|
||||||
|
"--cache-dir",
|
||||||
|
tmpDir,
|
||||||
|
"--version",
|
||||||
|
"1.0",
|
||||||
|
"--global-version",
|
||||||
|
"1.0",
|
||||||
|
"test-package",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{"restore", []string{"restore", "--cache-dir", tmpDir, "test-package"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCommands {
|
||||||
|
t.Run(tc.name+"_no_crash", func(t *testing.T) {
|
||||||
|
cmd := exec.Command(binaryPath, tc.args...)
|
||||||
|
err := cmd.Run()
|
||||||
|
// We expect these to fail in test environment, but they shouldn't crash
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("%s command failed as expected in test environment: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
53
cmd/cache_apt_pkgs/main_test.go
Normal file
53
cmd/cache_apt_pkgs/main_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain_CommandStructure(t *testing.T) {
|
||||||
|
// Test that all commands are properly initialized
|
||||||
|
commands := CreateCmds(
|
||||||
|
GetCreateKeyCmd(),
|
||||||
|
GetInstallCmd(),
|
||||||
|
GetRestoreCmd(),
|
||||||
|
GetValidateCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if commands == nil {
|
||||||
|
t.Fatal("CreateCmds returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all expected commands exist
|
||||||
|
expectedCommands := []string{"createkey", "install", "restore", "validate"}
|
||||||
|
for _, cmdName := range expectedCommands {
|
||||||
|
if _, ok := commands.Get(cmdName); !ok {
|
||||||
|
t.Errorf("Expected command '%s' to be available", cmdName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain_AllCommandsHaveRequiredFields(t *testing.T) {
|
||||||
|
commands := CreateCmds(
|
||||||
|
GetCreateKeyCmd(),
|
||||||
|
GetInstallCmd(),
|
||||||
|
GetRestoreCmd(),
|
||||||
|
GetValidateCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for cmdName, cmd := range *commands {
|
||||||
|
t.Run(cmdName, func(t *testing.T) {
|
||||||
|
if cmd.Name == "" {
|
||||||
|
t.Error("Command name should not be empty")
|
||||||
|
}
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Command description should not be empty")
|
||||||
|
}
|
||||||
|
if cmd.Flags == nil {
|
||||||
|
t.Error("Command flags should not be nil")
|
||||||
|
}
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Error("Command Run function should not be nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,5 +30,6 @@ func GetRestoreCmd() *Cmd {
|
||||||
"--cache-dir /tmp/cache_dir --restore-root /",
|
"--cache-dir /tmp/cache_dir --restore-root /",
|
||||||
}
|
}
|
||||||
cmd.ExamplePackages = ExamplePackages
|
cmd.ExamplePackages = ExamplePackages
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
cmd/cache_apt_pkgs/restore_test.go
Normal file
88
cmd/cache_apt_pkgs/restore_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetRestoreCmd(t *testing.T) {
|
||||||
|
cmd := GetRestoreCmd()
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("GetRestoreCmd returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Name != "restore" {
|
||||||
|
t.Errorf("Expected command name 'restore', got '%s'", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Expected non-empty description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags == nil {
|
||||||
|
t.Fatal("Expected flags to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Fatal("Expected Run function to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ExamplePackages == nil {
|
||||||
|
t.Error("Expected ExamplePackages to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.Examples) == 0 {
|
||||||
|
t.Error("Expected Examples to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that required flags are present
|
||||||
|
expectedFlags := []string{"cache-dir", "restore-root", "execute-scripts"}
|
||||||
|
for _, flagName := range expectedFlags {
|
||||||
|
if cmd.Flags.Lookup(flagName) == nil {
|
||||||
|
t.Errorf("Expected flag '%s' to be defined", flagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check default values
|
||||||
|
restoreRootFlag := cmd.Flags.Lookup("restore-root")
|
||||||
|
if restoreRootFlag != nil && restoreRootFlag.DefValue != "/" {
|
||||||
|
t.Errorf("Expected restore-root default to be '/', got '%s'", restoreRootFlag.DefValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
executeScriptsFlag := cmd.Flags.Lookup("execute-scripts")
|
||||||
|
if executeScriptsFlag != nil && executeScriptsFlag.DefValue != "false" {
|
||||||
|
t.Errorf(
|
||||||
|
"Expected execute-scripts default to be 'false', got '%s'",
|
||||||
|
executeScriptsFlag.DefValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRestore_NotImplemented(t *testing.T) {
|
||||||
|
cmd := GetRestoreCmd()
|
||||||
|
packages := pkgs.NewPackages(pkgs.Package{Name: "test-package"})
|
||||||
|
|
||||||
|
// The restore function is not implemented and should return an error
|
||||||
|
err := restore(cmd, packages)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error from unimplemented restore function")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMsg := "restorePackages not implemented"
|
||||||
|
if err.Error() != expectedMsg {
|
||||||
|
t.Errorf("Expected error message '%s', got '%s'", expectedMsg, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRestore_EmptyPackages(t *testing.T) {
|
||||||
|
cmd := GetRestoreCmd()
|
||||||
|
packages := pkgs.NewPackages()
|
||||||
|
|
||||||
|
// Even with empty packages, restore should return not implemented error
|
||||||
|
err := restore(cmd, packages)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error from unimplemented restore function")
|
||||||
|
}
|
||||||
|
}
|
||||||
67
cmd/cache_apt_pkgs/validate_test.go
Normal file
67
cmd/cache_apt_pkgs/validate_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetValidateCmd(t *testing.T) {
|
||||||
|
cmd := GetValidateCmd()
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("GetValidateCmd returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Name != "validate" {
|
||||||
|
t.Errorf("Expected command name 'validate', got '%s'", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Description == "" {
|
||||||
|
t.Error("Expected non-empty description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags == nil {
|
||||||
|
t.Fatal("Expected flags to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Fatal("Expected Run function to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ExamplePackages == nil {
|
||||||
|
t.Error("Expected ExamplePackages to be set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Testing the actual validate function requires APT which may not be available in test environment
|
||||||
|
// This test focuses on the command structure and basic functionality
|
||||||
|
func TestValidate_EmptyPackages(t *testing.T) {
|
||||||
|
cmd := GetValidateCmd()
|
||||||
|
packages := pkgs.NewPackages()
|
||||||
|
|
||||||
|
// With no packages, validation should succeed (no packages to validate)
|
||||||
|
err := validate(cmd, packages)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("validate with empty packages should succeed, got error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock test that doesn't require APT to be installed
|
||||||
|
func TestValidate_Structure(t *testing.T) {
|
||||||
|
cmd := GetValidateCmd()
|
||||||
|
|
||||||
|
// Verify the command is properly structured
|
||||||
|
if cmd.Run == nil {
|
||||||
|
t.Error("Expected Run function to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can create packages to validate (structure test)
|
||||||
|
packages := pkgs.NewPackages(pkgs.Package{Name: "test-package"})
|
||||||
|
if packages.Len() != 1 {
|
||||||
|
t.Error("Failed to create test packages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We can't test the actual validation without APT installed
|
||||||
|
// The validate function will likely fail in test environment, which is expected
|
||||||
|
}
|
||||||
163
cmd/parse_action/action.sh
Executable file
163
cmd/parse_action/action.sh
Executable file
|
|
@ -0,0 +1,163 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
#
|
||||||
|
# Name: Cache APT Packages
|
||||||
|
# Description: Install APT based packages and cache them for future runs.
|
||||||
|
# Author: awalsh128
|
||||||
|
#
|
||||||
|
# Branding:
|
||||||
|
# Icon: hard-drive
|
||||||
|
# Color: green
|
||||||
|
#
|
||||||
|
# Inputs:
|
||||||
|
# Packages:
|
||||||
|
# Description: Space delimited list of packages to install. Version can be specified optionally using APT command syntax of <name>=<version> (e.g. xdot=1.2-2).
|
||||||
|
# Required: true
|
||||||
|
# Default:
|
||||||
|
# Version:
|
||||||
|
# Description: Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.
|
||||||
|
# Required: false
|
||||||
|
# Default:
|
||||||
|
# Execute Install Scripts:
|
||||||
|
# Description: Execute Debian package pre and post install script upon restore. See README.md caveats for more information.
|
||||||
|
# Required: false
|
||||||
|
# Default: false
|
||||||
|
# Refresh:
|
||||||
|
# Description: OBSOLETE: Refresh is not used by the action, use version instead.
|
||||||
|
# Required: false
|
||||||
|
# Default:
|
||||||
|
# Deprecation Message: Refresh is not used by the action, use version instead.
|
||||||
|
# Debug:
|
||||||
|
# Description: Enable debugging when there are issues with action. Minor performance penalty.
|
||||||
|
# Required: false
|
||||||
|
# Default: false
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Outputs:
|
||||||
|
# Cache Hit:
|
||||||
|
# Description: A boolean value to indicate a cache was found for the packages requested.
|
||||||
|
# Value: ${{ steps.load-cache.outputs.cache-hit || false }}
|
||||||
|
# Package Version List:
|
||||||
|
# Description: The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>).
|
||||||
|
# Value: ${{ steps.post-cache.outputs.package-version-list }}
|
||||||
|
# All Package Version List:
|
||||||
|
# Description: All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>).
|
||||||
|
# Value: ${{ steps.post-cache.outputs.all-package-version-list }}
|
||||||
|
#
|
||||||
|
####################################################################################################
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INPUTS_EXECUTE_INSTALL_SCRIPTS="false"
|
||||||
|
INPUTS_REFRESH="false"
|
||||||
|
INPUTS_DEBUG="false"
|
||||||
|
RUNNER_ARCH="X86_64"
|
||||||
|
GITHUB_ACTION_PATH="../../"
|
||||||
|
INPUTS_PACKAGES="xdot,rolldice"
|
||||||
|
INPUTS_VERSION="0"
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: set-shared-env
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
STEP_SET_SHARED_ENV_ENV_BINARY_PATH="${GITHUB_ACTION_PATH}/scripts/distribute.sh getbinpath ${RUNNER_ARCH}"
|
||||||
|
GH_ENV_ARCH="${RUNNER_ARCH}"
|
||||||
|
GH_ENV_BINARY_PATH="${BINARY_PATH}"
|
||||||
|
GH_ENV_CACHE_DIR="~/cache-apt-pkgs"
|
||||||
|
GH_ENV_DEBUG="${INPUTS_DEBUG}"
|
||||||
|
GH_ENV_GLOBAL_VERSION="20250910"
|
||||||
|
GH_ENV_PACKAGES="${INPUTS_PACKAGES}"
|
||||||
|
GH_ENV_VERSION="${INPUTS_VERSION}"
|
||||||
|
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: install-aptfast
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
if ! apt-fast --version > /dev/null 2>&1; then
|
||||||
|
echo "Installing apt-fast for optimized installs and updates" &&
|
||||||
|
/bin/bash -c "$(curl -sL https://raw.githubusercontent.com/ilikenwf/apt-fast/master/quick-install.sh)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: setup-binary
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
if [[ ! -f "${BINARY_PATH}" ]]; then
|
||||||
|
echo "Error: Binary not found at ${BINARY_PATH}"
|
||||||
|
echo "Please ensure the action has been properly built and binaries are included in the distribute directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: create-cache-key
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
${BINARY_PATH} createkey \
|
||||||
|
-os-arch "${ARCH}" \
|
||||||
|
-plaintext-path "${CACHE_DIR}/cache_key.txt" \
|
||||||
|
-ciphertext-path "${CACHE_DIR}/cache_key.md5" \
|
||||||
|
-version "${VERSION}" \
|
||||||
|
-global-version "${GLOBAL_VERSION}" \
|
||||||
|
${PACKAGES}
|
||||||
|
GH_OUTPUT_CREATE_CACHE_KEY_CACHE_KEY="$(cat ${CACHE_DIR}/cache_key.md5)"
|
||||||
|
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: load-cache
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
STEP_LOAD_CACHE_WITH_PATH="${{ env.CACHE_DIR }}"
|
||||||
|
STEP_LOAD_CACHE_WITH_KEY="cache-apt-pkgs_${{ steps.create-cache-key.outputs.cache-key }}"
|
||||||
|
if [[ -d "${cache-apt-pkgs_${{ steps.create-cache-key.outputs.cache-key }}}" ]]; then
|
||||||
|
OUTPUT_CACHE_HIT=true
|
||||||
|
else
|
||||||
|
OUTPUT_CACHE_HIT=false
|
||||||
|
mkdir "${cache-apt-pkgs_${{ steps.create-cache-key.outputs.cache-key }}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NO HANDLER FOUND for actions/cache/restore@v4
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: post-load-cache
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
STEP_POST_LOAD_CACHE_ENV_CACHE_HIT="${{ steps.load-cache.outputs.cache-hit }}"
|
||||||
|
STEP_POST_LOAD_CACHE_ENV_EXEC_INSTALL_SCRIPTS="${INPUTS_EXECUTE_INSTALL_SCRIPTS}"
|
||||||
|
if [ "${CACHE_HIT}" == "true" ]; then
|
||||||
|
${BINARY_PATH} restore \
|
||||||
|
-cache-dir "${CACHE_DIR}" \
|
||||||
|
-restore-root "/" \
|
||||||
|
"${PACKAGES}"
|
||||||
|
else
|
||||||
|
${BINARY_PATH} install \
|
||||||
|
-cache-dir "${CACHE_DIR}" \
|
||||||
|
-version "${VERSION}" \
|
||||||
|
-global-version "${GLOBAL_VERSION}" \
|
||||||
|
"${PACKAGES}"
|
||||||
|
fi
|
||||||
|
GH_OUTPUT_POST_LOAD_CACHE_PACKAGE_VERSION_LIST="\"$(cat "${CACHE_DIR}/pkgs_args.txt")\""
|
||||||
|
GH_OUTPUT_POST_LOAD_CACHE_ALL_PACKAGE_VERSION_LIST="\"$(cat "${CACHE_DIR}/pkgs_installed.txt")\""
|
||||||
|
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: upload-artifacts
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
# NO HANDLER FOUND for actions/upload-artifact@v4
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: save-cache
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
# NO HANDLER FOUND for actions/cache/save@v4
|
||||||
|
|
||||||
|
#===================================================================================================
|
||||||
|
# Step ID: clean-cache
|
||||||
|
#===================================================================================================
|
||||||
|
|
||||||
|
rm -rf ~/cache-apt-pkgs
|
||||||
|
|
||||||
99
cmd/parse_action/bash_parser.go
Normal file
99
cmd/parse_action/bash_parser.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"mvdan.cc/sh/v3/syntax"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseBashToAST parses a bash script string into an AST
|
||||||
|
func ParseBashToAST(script string) (*syntax.File, error) {
|
||||||
|
// Create a new parser with bash dialect
|
||||||
|
parser := syntax.NewParser(syntax.KeepComments(true), syntax.Variant(syntax.LangBash))
|
||||||
|
|
||||||
|
// Parse the script into an AST
|
||||||
|
file, err := parser.Parse(strings.NewReader(script), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse bash script: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyzeBashScript provides analysis of a bash script including variables, functions, and commands
|
||||||
|
func AnalyzeBashScript(script string) (map[string]interface{}, error) {
|
||||||
|
file, err := ParseBashToAST(script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis := make(map[string]interface{})
|
||||||
|
variables := make(map[string]struct{})
|
||||||
|
functions := make([]string, 0)
|
||||||
|
commands := make([]string, 0)
|
||||||
|
|
||||||
|
// Walk the AST and collect information
|
||||||
|
syntax.Walk(file, func(node syntax.Node) bool {
|
||||||
|
switch n := node.(type) {
|
||||||
|
case *syntax.Assign:
|
||||||
|
// Found variable assignment
|
||||||
|
if n.Name != nil {
|
||||||
|
variables[n.Name.Value] = struct{}{}
|
||||||
|
}
|
||||||
|
case *syntax.FuncDecl:
|
||||||
|
// Found function declaration
|
||||||
|
if n.Name != nil {
|
||||||
|
functions = append(functions, n.Name.Value)
|
||||||
|
}
|
||||||
|
case *syntax.CallExpr:
|
||||||
|
// Found command execution
|
||||||
|
if len(n.Args) > 0 {
|
||||||
|
var cmd strings.Builder
|
||||||
|
for _, part := range n.Args[0].Parts {
|
||||||
|
if lit, ok := part.(*syntax.Lit); ok {
|
||||||
|
cmd.WriteString(lit.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmd.Len() > 0 {
|
||||||
|
commands = append(commands, cmd.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert variables map to slice for better JSON output
|
||||||
|
varSlice := make([]string, 0, len(variables))
|
||||||
|
for v := range variables {
|
||||||
|
varSlice = append(varSlice, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis["variables"] = varSlice
|
||||||
|
analysis["functions"] = functions
|
||||||
|
analysis["commands"] = commands
|
||||||
|
|
||||||
|
return analysis, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAndGetAst(action Action) (string, error) {
|
||||||
|
converter := NewBashConverter(action)
|
||||||
|
script := converter.Convert()
|
||||||
|
|
||||||
|
// Analyze the generated script
|
||||||
|
analysis, err := AnalyzeBashScript(script)
|
||||||
|
if err != nil {
|
||||||
|
return script, fmt.Errorf("script analysis error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add analysis as comments at the top of the script
|
||||||
|
var finalScript strings.Builder
|
||||||
|
finalScript.WriteString("#!/bin/bash\n\n")
|
||||||
|
finalScript.WriteString("# Script Analysis:\n")
|
||||||
|
finalScript.WriteString(fmt.Sprintf("# Variables: %v\n", analysis["variables"]))
|
||||||
|
finalScript.WriteString(fmt.Sprintf("# Functions: %v\n", analysis["functions"]))
|
||||||
|
finalScript.WriteString(fmt.Sprintf("# Commands: %v\n\n", analysis["commands"]))
|
||||||
|
finalScript.WriteString(script)
|
||||||
|
|
||||||
|
return finalScript.String(), nil
|
||||||
|
}
|
||||||
251
cmd/parse_action/convert_to_bash.go
Normal file
251
cmd/parse_action/convert_to_bash.go
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dedent removes common leading indentation from non-empty lines.
|
||||||
|
// It also normalizes CRLF -> LF and strips a single leading newline.
|
||||||
|
func dedent(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
s = strings.TrimPrefix(s, "\n")
|
||||||
|
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
min := -1
|
||||||
|
for _, ln := range lines {
|
||||||
|
if strings.TrimSpace(ln) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ind := len(ln) - len(strings.TrimLeft(ln, " \t"))
|
||||||
|
if min == -1 || ind < min {
|
||||||
|
min = ind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min <= 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
for i, ln := range lines {
|
||||||
|
if len(ln) >= min {
|
||||||
|
lines[i] = ln[min:]
|
||||||
|
} else {
|
||||||
|
lines[i] = strings.TrimLeft(ln, " \t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptBuilder struct {
|
||||||
|
textBuilder strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScriptBuilder) WriteComment(format string, a ...any) {
|
||||||
|
var c strings.Builder
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(fmt.Sprintf(format, a...)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
c.WriteString("# ")
|
||||||
|
c.WriteString(scanner.Text())
|
||||||
|
c.WriteByte('\n')
|
||||||
|
}
|
||||||
|
fmt.Fprint(&s.textBuilder, c.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScriptBuilder) WriteCommentSection(format string, a ...any) {
|
||||||
|
s.WriteBlock("\n\n#" + strings.Repeat("=", 99))
|
||||||
|
s.WriteComment(format, a...)
|
||||||
|
s.WriteBlock("#" + strings.Repeat("=", 99) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScriptBuilder) WriteBlock(format string, a ...any) {
|
||||||
|
fmt.Fprintln(&s.textBuilder, fmt.Sprintf(dedent(format), a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScriptBuilder) String() string {
|
||||||
|
return s.textBuilder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type BashConverter struct {
|
||||||
|
action Action
|
||||||
|
scriptBuilder ScriptBuilder
|
||||||
|
githubVars githubVars
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBashConverter(action Action) *BashConverter {
|
||||||
|
githubVars := make(map[string]githubVar)
|
||||||
|
for _, v := range []githubVar{
|
||||||
|
newGithubVar("runner.arch", "X86_64"),
|
||||||
|
newGithubVar("github.action_path", "../../"),
|
||||||
|
newGithubVar("inputs.packages", "xdot,rolldice"),
|
||||||
|
newGithubVar("inputs.version", "0"),
|
||||||
|
newGithubVar("inputs.global_version", ""),
|
||||||
|
newGithubVar("inputs.execute_install_scripts", "false"),
|
||||||
|
newGithubVar("inputs.refresh", "false"),
|
||||||
|
newGithubVar("inputs.debug", "false"),
|
||||||
|
} {
|
||||||
|
githubVars[v.name] = v
|
||||||
|
}
|
||||||
|
return &BashConverter{
|
||||||
|
action: action,
|
||||||
|
scriptBuilder: ScriptBuilder{},
|
||||||
|
githubVars: githubVars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BashConverter) Convert() string {
|
||||||
|
b.handleAction()
|
||||||
|
return b.scriptBuilder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BashConverter) convertShellLines(step Step, lines string) string {
|
||||||
|
var result []string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(lines))
|
||||||
|
for scanner.Scan() {
|
||||||
|
converted := b.convertShellLine(step, scanner.Text())
|
||||||
|
result = append(result, converted)
|
||||||
|
}
|
||||||
|
return strings.Join(result, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// echo\s+
|
||||||
|
|
||||||
|
func (b *BashConverter) convertShellLine(step Step, line string) string {
|
||||||
|
line = b.githubVars.convert(line)
|
||||||
|
|
||||||
|
env_pattern := `^\s*echo\s+"([\w\-_]+)=(.*)"\s*>>\s*.*GITHUB_ENV.*`
|
||||||
|
env_re := regexp.MustCompile(env_pattern)
|
||||||
|
if m := env_re.FindStringSubmatch(line); m != nil {
|
||||||
|
return fmt.Sprintf(`GH_ENV_%s="%s"`, convertToShellVar(m[1]), b.githubVars.convert(m[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
out_pattern := `^\s*echo\s+"([\w\-_]+)=(.*)"\s*>>\s*.*GITHUB_OUTPUT.*`
|
||||||
|
out_re := regexp.MustCompile(out_pattern)
|
||||||
|
if m := out_re.FindStringSubmatch(line); m != nil {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`GH_OUTPUT_%s_%s="%s"`,
|
||||||
|
convertToShellVar(step.ID),
|
||||||
|
convertToShellVar(m[1]),
|
||||||
|
b.githubVars.convert(m[2]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BashConverter) handleExternalAction(step Step) {
|
||||||
|
handlers := map[string]func(){
|
||||||
|
"actions/cache/restore@v4": func() {
|
||||||
|
path := b.convertShellLine(step, step.With["path"])
|
||||||
|
key := b.convertShellLine(step, step.With["key"])
|
||||||
|
shellVarPrefix := "STEP_" + convertToShellVar(step.ID) + "_WITH"
|
||||||
|
pathVar := fmt.Sprintf("%s_PATH", shellVarPrefix)
|
||||||
|
keyVar := fmt.Sprintf("%s_KEY", shellVarPrefix)
|
||||||
|
b.scriptBuilder.WriteBlock(`
|
||||||
|
%s="%s"
|
||||||
|
%s="%s"
|
||||||
|
if [[ -d "${%s}" ]]; then
|
||||||
|
OUTPUT_CACHE_HIT=true
|
||||||
|
else
|
||||||
|
OUTPUT_CACHE_HIT=false
|
||||||
|
mkdir "${%s}"
|
||||||
|
fi
|
||||||
|
`, pathVar, path, keyVar, key, key, key)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if handlers[step.Uses] != nil {
|
||||||
|
handlers[step.Uses]()
|
||||||
|
}
|
||||||
|
b.scriptBuilder.WriteComment("NO HANDLER FOUND for %s", step.Uses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToShellVar(name string) string {
|
||||||
|
return strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, ".", "_"), "-", "_"))
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubVar struct {
|
||||||
|
name string
|
||||||
|
shellName string
|
||||||
|
shellVal string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGithubVar(name, shellVal string) githubVar {
|
||||||
|
return githubVar{
|
||||||
|
name: name,
|
||||||
|
shellName: convertToShellVar(name),
|
||||||
|
shellVal: shellVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubVars map[string]githubVar
|
||||||
|
|
||||||
|
func (v *githubVars) convert(line string) string {
|
||||||
|
// Build pattern to match ${{ var }} style variables
|
||||||
|
// The pattern matches any known github variable name
|
||||||
|
names := make([]string, 0, len(*v))
|
||||||
|
for name := range *v {
|
||||||
|
names = append(names, regexp.QuoteMeta(name))
|
||||||
|
}
|
||||||
|
pattern := fmt.Sprintf(`\${{[[:space:]]*(%s)[[:space:]]*}}`, strings.Join(names, "|"))
|
||||||
|
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
return re.ReplaceAllStringFunc(line, func(match string) string {
|
||||||
|
// Extract the variable name from between ${{ and }}
|
||||||
|
varName := re.FindStringSubmatch(match)[1]
|
||||||
|
if gvar, ok := (*v)[varName]; ok {
|
||||||
|
// If the variable exists, replace with ${SHELL_VAR}
|
||||||
|
return fmt.Sprintf("${%s}", gvar.shellName)
|
||||||
|
}
|
||||||
|
// If variable not found, return original text
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BashConverter) handleAction() {
|
||||||
|
b.scriptBuilder.WriteBlock("#!/bin/bash\n")
|
||||||
|
b.scriptBuilder.WriteBlock(strings.Repeat("#", 100) + "\n#")
|
||||||
|
b.scriptBuilder.WriteComment("%s", b.action.ShortString())
|
||||||
|
b.scriptBuilder.WriteBlock(strings.Repeat("#", 100) + "\n")
|
||||||
|
b.scriptBuilder.WriteBlock("set -e\n")
|
||||||
|
|
||||||
|
for _, v := range b.githubVars {
|
||||||
|
if v.shellVal != "" {
|
||||||
|
b.scriptBuilder.WriteBlock(`%s="%s"`, v.shellName, v.shellVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, step := range b.action.Runs.Steps {
|
||||||
|
if step.ID != "" {
|
||||||
|
b.scriptBuilder.WriteCommentSection("Step ID: %s", step.ID)
|
||||||
|
} else {
|
||||||
|
b.scriptBuilder.WriteCommentSection("Step ID: n/a")
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.Uses != "" {
|
||||||
|
b.handleExternalAction(step)
|
||||||
|
}
|
||||||
|
if len(step.Env) > 0 {
|
||||||
|
for k, v := range step.Env {
|
||||||
|
b.scriptBuilder.WriteBlock(
|
||||||
|
`STEP_%s_ENV_%s="%s"`,
|
||||||
|
convertToShellVar(step.ID),
|
||||||
|
convertToShellVar(k),
|
||||||
|
b.githubVars.convert(v),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if step.Shell != "" && step.Shell != "bash" {
|
||||||
|
b.scriptBuilder.WriteComment(
|
||||||
|
"Note: Original shell was %q, but this script uses bash.\n",
|
||||||
|
step.Shell,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if step.Run != "" {
|
||||||
|
b.scriptBuilder.WriteBlock("%s\n", b.convertShellLines(step, step.Run))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// b.scriptBuilder.WriteBlock(`
|
||||||
|
// #!/bin/bash
|
||||||
|
|
||||||
|
// set -e
|
||||||
|
// `)
|
||||||
|
}
|
||||||
30
cmd/parse_action/main.go
Normal file
30
cmd/parse_action/main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Foo(action Action) {
|
||||||
|
converter := NewBashConverter(action)
|
||||||
|
bashScript := converter.Convert()
|
||||||
|
fmt.Println(bashScript)
|
||||||
|
const out = "action.sh"
|
||||||
|
if err := os.WriteFile(out, []byte(bashScript), 0o755); err != nil {
|
||||||
|
fmt.Println("write error:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("Wrote script to %s\n", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
action, err := Parse("../../action.yml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txt, err := ParseAndGetAst(action)
|
||||||
|
|
||||||
|
fmt.Println(txt)
|
||||||
|
}
|
||||||
236
cmd/parse_action/parse.go
Normal file
236
cmd/parse_action/parse.go
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const indentSize = 2
|
||||||
|
|
||||||
|
// Action represents the GitHub Action configuration structure
|
||||||
|
type Action struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Author string `yaml:"author"`
|
||||||
|
Branding Branding `yaml:"branding"`
|
||||||
|
Inputs Inputs `yaml:"inputs"`
|
||||||
|
Outputs Outputs `yaml:"outputs"`
|
||||||
|
Runs Runs `yaml:"runs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branding represents the action's branding configuration
|
||||||
|
type Branding struct {
|
||||||
|
Icon string `yaml:"icon"`
|
||||||
|
Color string `yaml:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inputs represents all input parameters for the action
|
||||||
|
type Inputs struct {
|
||||||
|
Packages Input `yaml:"packages"`
|
||||||
|
Version Input `yaml:"version"`
|
||||||
|
ExecuteInstallScripts Input `yaml:"execute_install_scripts"`
|
||||||
|
Refresh Input `yaml:"refresh"`
|
||||||
|
Debug Input `yaml:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input represents a single input parameter configuration
|
||||||
|
type Input struct {
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Required bool `yaml:"required"`
|
||||||
|
Default string `yaml:"default"`
|
||||||
|
DeprecationMessage string `yaml:"deprecationMessage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs represents all output parameters from the action
|
||||||
|
type Outputs struct {
|
||||||
|
CacheHit Output `yaml:"cache-hit"`
|
||||||
|
PackageVersionList Output `yaml:"package-version-list"`
|
||||||
|
AllPackageVersionList Output `yaml:"all-package-version-list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output represents a single output parameter configuration
|
||||||
|
type Output struct {
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs represents the action's execution configuration
|
||||||
|
type Runs struct {
|
||||||
|
Using string `yaml:"using"`
|
||||||
|
Env map[string]string `yaml:"env"`
|
||||||
|
Steps []Step `yaml:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step represents a single step in the action's execution
|
||||||
|
type Step struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Uses string `yaml:"uses"`
|
||||||
|
With map[string]string `yaml:"with"`
|
||||||
|
Shell string `yaml:"shell"`
|
||||||
|
Run string `yaml:"run"`
|
||||||
|
Env map[string]string `yaml:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Action
|
||||||
|
func (a Action) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(a.ShortString())
|
||||||
|
|
||||||
|
b.WriteString("\nRuns:\n")
|
||||||
|
b.WriteString(indent(a.Runs.String(), 1))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortString implements fmt.Stringer for Action but with runs trimmed out
|
||||||
|
func (a Action) ShortString() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("Name: %s\n", a.Name))
|
||||||
|
b.WriteString(fmt.Sprintf("Description: %s\n", a.Description))
|
||||||
|
b.WriteString(fmt.Sprintf("Author: %s\n", a.Author))
|
||||||
|
|
||||||
|
b.WriteString("\nBranding:\n")
|
||||||
|
b.WriteString(indent(a.Branding.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("\nInputs:\n")
|
||||||
|
b.WriteString(indent(a.Inputs.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("\nOutputs:\n")
|
||||||
|
b.WriteString(indent(a.Outputs.String(), 1))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Branding
|
||||||
|
func (b Branding) String() string {
|
||||||
|
return fmt.Sprintf("Icon: %s\nColor: %s", b.Icon, b.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Inputs
|
||||||
|
func (i Inputs) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString("Packages:\n")
|
||||||
|
b.WriteString(indent(i.Packages.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("Version:\n")
|
||||||
|
b.WriteString(indent(i.Version.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("Execute Install Scripts:\n")
|
||||||
|
b.WriteString(indent(i.ExecuteInstallScripts.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("Refresh:\n")
|
||||||
|
b.WriteString(indent(i.Refresh.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("Debug:\n")
|
||||||
|
b.WriteString(indent(i.Debug.String(), 1))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Input
|
||||||
|
func (i Input) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("Description: %s\n", i.Description))
|
||||||
|
b.WriteString(fmt.Sprintf("Required: %v\n", i.Required))
|
||||||
|
b.WriteString(fmt.Sprintf("Default: %s", i.Default))
|
||||||
|
if i.DeprecationMessage != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("\nDeprecation Message: %s", i.DeprecationMessage))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Outputs
|
||||||
|
func (o Outputs) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString("Cache Hit:\n")
|
||||||
|
b.WriteString(indent(o.CacheHit.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("Package Version List:\n")
|
||||||
|
b.WriteString(indent(o.PackageVersionList.String(), 1))
|
||||||
|
|
||||||
|
b.WriteString("All Package Version List:\n")
|
||||||
|
b.WriteString(indent(o.AllPackageVersionList.String(), 1))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Output
|
||||||
|
func (o Output) String() string {
|
||||||
|
return fmt.Sprintf("Description: %s\nValue: %s", o.Description, o.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Runs
|
||||||
|
func (r Runs) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("Using: %s\n", r.Using))
|
||||||
|
|
||||||
|
b.WriteString("Environment:\n")
|
||||||
|
for k, v := range r.Env {
|
||||||
|
b.WriteString(indent(fmt.Sprintf("%s: %s\n", k, v), 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("Steps:\n")
|
||||||
|
for _, step := range r.Steps {
|
||||||
|
b.WriteString(indent(step.String()+"\n", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer for Step
|
||||||
|
func (s Step) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
if s.ID != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("ID: %s\n", s.ID))
|
||||||
|
}
|
||||||
|
if len(s.With) > 0 {
|
||||||
|
b.WriteString("With:\n")
|
||||||
|
for k, v := range s.With {
|
||||||
|
b.WriteString(fmt.Sprintf("%s: %s\n", k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Shell != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("Shell: %s\n", s.Shell))
|
||||||
|
}
|
||||||
|
if s.Run != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("Run:\n%s", indent(s.Run, 1)))
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(b.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// indent adds the specified number of indentation levels to each line of the input string
|
||||||
|
func indent(s string, level int) string {
|
||||||
|
if s == "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := strings.Repeat(" ", level*indentSize)
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if line != "" {
|
||||||
|
lines[i] = prefix + line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(yamlFilePath string) (Action, error) {
|
||||||
|
// Read the action.yml file
|
||||||
|
data, err := os.ReadFile(yamlFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return Action{}, fmt.Errorf("Error reading %s: %v", yamlFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the YAML into our Action struct
|
||||||
|
var action Action
|
||||||
|
if err := yaml.Unmarshal(data, &action); err != nil {
|
||||||
|
return Action{}, fmt.Errorf("Error parsing YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return action, nil
|
||||||
|
}
|
||||||
21
dev.md
21
dev.md
|
|
@ -1,21 +0,0 @@
|
||||||
# cache-apt-pkgs-action - Development
|
|
||||||
|
|
||||||
To develop and run tests you will need to setup your system.
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
1. The project requires Go 1.23 or later.
|
|
||||||
2. Set GO111MODULE to auto:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# One-time setup
|
|
||||||
go env -w GO111MODULE=auto
|
|
||||||
|
|
||||||
# Or use the provided setup script
|
|
||||||
./scripts/setup_dev.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. The project includes a `.env` file with required settings.
|
|
||||||
|
|
||||||
## Action Testing
|
|
||||||
|
|
||||||
BIN
dist/cache-apt-pkgs-linux-386
vendored
BIN
dist/cache-apt-pkgs-linux-386
vendored
Binary file not shown.
BIN
dist/cache-apt-pkgs-linux-amd64
vendored
BIN
dist/cache-apt-pkgs-linux-amd64
vendored
Binary file not shown.
BIN
dist/cache-apt-pkgs-linux-arm
vendored
BIN
dist/cache-apt-pkgs-linux-arm
vendored
Binary file not shown.
BIN
dist/cache-apt-pkgs-linux-arm64
vendored
BIN
dist/cache-apt-pkgs-linux-arm64
vendored
Binary file not shown.
8
go.mod
8
go.mod
|
|
@ -1,16 +1,16 @@
|
||||||
module awalsh128.com/cache-apt-pkgs-action
|
module awalsh128.com/cache-apt-pkgs-action
|
||||||
|
|
||||||
go 1.23
|
go 1.24
|
||||||
|
|
||||||
toolchain go1.23.4
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/awalsh128/syspkg v0.1.5
|
github.com/awalsh128/syspkg v0.1.5
|
||||||
github.com/stretchr/testify v1.11.0
|
github.com/stretchr/testify v1.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require mvdan.cc/sh/v3 v3.12.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -10,3 +10,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||||
|
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||||
|
|
|
||||||
296
internal/cache/key.go
vendored
296
internal/cache/key.go
vendored
|
|
@ -1,8 +1,25 @@
|
||||||
// Package cache provides caching functionality for APT packages and their metadata.
|
// Package cache provides functionality for managing APT package cache keys.
|
||||||
|
// It handles the creation, serialization, and validation of cache keys that uniquely
|
||||||
|
// identify sets of packages for caching in GitHub Actions.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// // Create a new cache key
|
||||||
|
// key := cache.NewKey(packages, "v1.0", "v2", "amd64")
|
||||||
|
//
|
||||||
|
// // Write the key to files
|
||||||
|
// err := key.Write("key.txt", "key.md5")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Read and validate a key
|
||||||
|
// plaintext, hash, err := cache.ReadKey("key.txt", "key.md5")
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
|
@ -12,49 +29,274 @@ import (
|
||||||
|
|
||||||
// Key represents a unique identifier for a package cache entry.
|
// Key represents a unique identifier for a package cache entry.
|
||||||
// It combines package information with version and architecture details to create
|
// It combines package information with version and architecture details to create
|
||||||
// a deterministic cache key.
|
// a deterministic cache key. Keys are immutable after creation and generate
|
||||||
|
// consistent hashes by maintaining sorted package order and using a fixed string format.
|
||||||
type Key struct {
|
type Key struct {
|
||||||
// Packages is a sorted list of packages to be cached
|
packages pkgs.Packages // Sorted list of packages to be cached
|
||||||
// This is guaranteed by the pkgs.Packages interface
|
version string // User-specified cache version
|
||||||
Packages pkgs.Packages
|
globalVersion string // Action's global version for cache invalidation
|
||||||
// Version is the user-specified cache version
|
osArch string // Target architecture (e.g., amd64, arm64)
|
||||||
Version string
|
|
||||||
// GlobalVersion is the action's global version, used for cache invalidation
|
|
||||||
GlobalVersion string
|
|
||||||
// OsArch is the target architecture (e.g., amd64, arm64)
|
|
||||||
OsArch string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlainText returns a human-readable string representation of the cache key.
|
// File permissions for written key files
|
||||||
|
const (
|
||||||
|
keyFileMode = 0644
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewKey creates a new cache key with the specified parameters.
|
||||||
|
// The packages are already sorted when constructed to provide a deterministic order and hash.
|
||||||
|
func NewKey(packages pkgs.Packages, version, globalVersion, osArch string) (Key, error) {
|
||||||
|
missingFields := []string{}
|
||||||
|
if globalVersion == "" {
|
||||||
|
missingFields = append(missingFields, "globalVersion")
|
||||||
|
}
|
||||||
|
if osArch == "" {
|
||||||
|
missingFields = append(missingFields, "osArch")
|
||||||
|
}
|
||||||
|
if len(missingFields) > 0 {
|
||||||
|
return Key{}, fmt.Errorf("missing required fields: %v", missingFields)
|
||||||
|
}
|
||||||
|
return Key{
|
||||||
|
packages: packages,
|
||||||
|
version: version,
|
||||||
|
globalVersion: globalVersion,
|
||||||
|
osArch: osArch,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packages returns the packages associated with this cache key.
|
||||||
|
// The returned value is guaranteed to be sorted.
|
||||||
|
func (k Key) Packages() pkgs.Packages {
|
||||||
|
return k.packages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the user-specified cache version.
|
||||||
|
func (k Key) Version() string {
|
||||||
|
return k.version
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalVersion returns the action's global version used for cache invalidation.
|
||||||
|
func (k Key) GlobalVersion() string {
|
||||||
|
return k.globalVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// OsArch returns the target architecture.
|
||||||
|
func (k Key) OsArch() string {
|
||||||
|
return k.osArch
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a human-readable string representation of the cache key.
|
||||||
// The output format is deterministic since Packages are guaranteed to be sorted.
|
// The output format is deterministic since Packages are guaranteed to be sorted.
|
||||||
func (k *Key) PlainText() string {
|
// This method implements the fmt.Stringer interface.
|
||||||
|
func (k Key) String() string {
|
||||||
return fmt.Sprintf("Packages: '%s', Version: '%s', GlobalVersion: '%s', OsArch: '%s'",
|
return fmt.Sprintf("Packages: '%s', Version: '%s', GlobalVersion: '%s', OsArch: '%s'",
|
||||||
k.Packages.String(), k.Version, k.GlobalVersion, k.OsArch)
|
k.packages.String(), k.version, k.globalVersion, k.osArch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash generates a deterministic MD5 hash of the key's contents.
|
// Hash generates a deterministic MD5 hash of the key's contents.
|
||||||
// This hash is used as the actual cache key for storage and lookup.
|
// This hash is used as the actual cache key for storage and lookup.
|
||||||
func (k *Key) Hash() []byte {
|
//
|
||||||
hash := md5.Sum([]byte(k.PlainText()))
|
// Note: MD5 is used here for speed and determinism, not cryptographic security.
|
||||||
|
// The hash is based on the string representation to ensure consistency.
|
||||||
|
func (k Key) Hash() []byte {
|
||||||
|
hash := md5.Sum([]byte(k.String()))
|
||||||
return hash[:]
|
return hash[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteError represents an error that occurred during key writing operations.
|
||||||
|
// It provides context about which file and operation failed, along with the underlying error.
|
||||||
|
// This type implements both the error interface and error unwrapping.
|
||||||
|
type WriteError struct {
|
||||||
|
Path string // File path that failed
|
||||||
|
Operation string // Operation being performed (plaintext/hash)
|
||||||
|
Err error // Underlying error that occurred
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface.
|
||||||
|
func (e *WriteError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to write %s to %s: %v", e.Operation, e.Path, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying error for error unwrapping.
|
||||||
|
func (e *WriteError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
// Write stores both the plaintext and hashed versions of the cache key to files.
|
// Write stores both the plaintext and hashed versions of the cache key to files.
|
||||||
// This allows for both human inspection and fast cache lookups.
|
// This allows for both human inspection and fast cache lookups.
|
||||||
func (k *Key) Write(plaintextPath string, ciphertextPath string) error {
|
//
|
||||||
keyText := k.PlainText()
|
// The method writes:
|
||||||
logging.Info("Writing cache key plaintext to %s.", plaintextPath)
|
// - Human-readable key representation to plaintextPath
|
||||||
if err := os.WriteFile(plaintextPath, []byte(keyText), 0644); err != nil {
|
// - Binary hash of the key to hashPath
|
||||||
return fmt.Errorf("write failed to %s: %w", plaintextPath, err)
|
//
|
||||||
|
// Both operations must succeed for the method to return nil.
|
||||||
|
// If either operation fails, no partial state is left behind.
|
||||||
|
func (k Key) Write(plaintextPath, hashPath string) error {
|
||||||
|
// Write plaintext representation
|
||||||
|
if err := k.writePlaintext(plaintextPath); err != nil {
|
||||||
|
return &WriteError{
|
||||||
|
Path: plaintextPath,
|
||||||
|
Operation: "plaintext key",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logging.Info("Completed writing cache key plaintext.")
|
|
||||||
|
|
||||||
keyHash := k.Hash()
|
// Write hash representation
|
||||||
logging.Info("Writing cache key hash to %s.", ciphertextPath)
|
if err := k.writeHash(hashPath); err != nil {
|
||||||
if err := os.WriteFile(ciphertextPath, keyHash[:], 0644); err != nil {
|
// Attempt cleanup of plaintext file on hash write failure
|
||||||
return fmt.Errorf("write failed to %s: %w", ciphertextPath, err)
|
if removeErr := os.Remove(plaintextPath); removeErr != nil {
|
||||||
|
logging.Info("Failed to cleanup plaintext file %s after hash write failure: %v",
|
||||||
|
plaintextPath, removeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WriteError{
|
||||||
|
Path: hashPath,
|
||||||
|
Operation: "hash key",
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logging.Info("Completed writing cache key hash.")
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePlaintext writes the human-readable key representation to the specified path.
|
||||||
|
func (k Key) writePlaintext(path string) error {
|
||||||
|
keyText := k.String()
|
||||||
|
logging.Info("Writing cache key plaintext to %s", path)
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, []byte(keyText), keyFileMode); err != nil {
|
||||||
|
return fmt.Errorf("write plaintext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Completed writing cache key plaintext")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHash writes the binary hash to the specified path.
|
||||||
|
func (k Key) writeHash(path string) error {
|
||||||
|
keyHash := k.Hash()
|
||||||
|
logging.Info("Writing cache key hash to %s", path)
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, keyHash, keyFileMode); err != nil {
|
||||||
|
return fmt.Errorf("write hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Completed writing cache key hash")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadKey reads and validates a cache key from the filesystem.
|
||||||
|
// It returns the plaintext content and hash, or an error if validation fails.
|
||||||
|
//
|
||||||
|
// This function verifies that:
|
||||||
|
// - Both files exist and are readable
|
||||||
|
// - The hash file contains exactly 16 bytes (MD5 hash size)
|
||||||
|
// - The computed hash of plaintext matches the stored hash
|
||||||
|
func ReadKey(plaintextPath, hashPath string) (plaintext string, hash []byte, err error) {
|
||||||
|
if checkFileExists(plaintextPath) != nil {
|
||||||
|
return "", nil, fmt.Errorf("plaintext file does not exist: %s", plaintextPath)
|
||||||
|
}
|
||||||
|
if checkFileExists(hashPath) != nil {
|
||||||
|
return "", nil, fmt.Errorf("hash file does not exist: %s", hashPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read plaintext
|
||||||
|
plaintextBytes, err := os.ReadFile(plaintextPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("read plaintext from %s: %w", plaintextPath, err)
|
||||||
|
}
|
||||||
|
plaintext = string(plaintextBytes)
|
||||||
|
|
||||||
|
// Read stored hash
|
||||||
|
storedHash, err := os.ReadFile(hashPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("read hash from %s: %w", hashPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hash length
|
||||||
|
if len(storedHash) != md5.Size {
|
||||||
|
return "", nil, fmt.Errorf("invalid hash length in %s: got %d bytes, want %d",
|
||||||
|
hashPath, len(storedHash), md5.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash matches plaintext
|
||||||
|
computedHash := md5.Sum(plaintextBytes)
|
||||||
|
if string(computedHash[:]) != string(storedHash) {
|
||||||
|
return "", nil, fmt.Errorf("hash mismatch: stored hash does not match plaintext content")
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, storedHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyExists checks if both plaintext and hash key files exist at the specified paths.
|
||||||
|
// It returns true only if both files exist and are accessible.
|
||||||
|
func KeyExists(plaintextPath, hashPath string) bool {
|
||||||
|
if err := checkFileExists(plaintextPath); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkFileExists(hashPath); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFileExists verifies that a file exists and is accessible.
|
||||||
|
func checkFileExists(path string) error {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return fmt.Errorf("path is a directory: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyJSON is a helper struct for JSON serialization of Key.
|
||||||
|
// It uses exported fields and concrete types that JSON can handle.
|
||||||
|
type keyJSON struct {
|
||||||
|
Packages []pkgs.Package `json:"packages"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
GlobalVersion string `json:"globalVersion"`
|
||||||
|
OsArch string `json:"osArch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements custom JSON marshaling for Key.
|
||||||
|
func (k Key) MarshalJSON() ([]byte, error) {
|
||||||
|
// Convert packages to slice
|
||||||
|
pkgSlice := make([]pkgs.Package, k.packages.Len())
|
||||||
|
for i := 0; i < k.packages.Len(); i++ {
|
||||||
|
pkgSlice[i] = *k.packages.Get(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyData := keyJSON{
|
||||||
|
Packages: pkgSlice,
|
||||||
|
Version: k.version,
|
||||||
|
GlobalVersion: k.globalVersion,
|
||||||
|
OsArch: k.osArch,
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(keyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements custom JSON unmarshaling for Key.
|
||||||
|
func (k *Key) UnmarshalJSON(data []byte) error {
|
||||||
|
var keyData keyJSON
|
||||||
|
if err := json.Unmarshal(data, &keyData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert slice back to Packages interface
|
||||||
|
packages := pkgs.NewPackages(keyData.Packages...)
|
||||||
|
|
||||||
|
k.packages = packages
|
||||||
|
k.version = keyData.Version
|
||||||
|
k.globalVersion = keyData.GlobalVersion
|
||||||
|
k.osArch = keyData.OsArch
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
434
internal/cache/key_test.go
vendored
434
internal/cache/key_test.go
vendored
|
|
@ -2,220 +2,322 @@ package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
"awalsh128.com/cache-apt-pkgs-action/internal/pkgs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pkg1 = "xdot=1.3-1"
|
package1 = "xdot=1.3-1"
|
||||||
pkg2 = "rolldice=1.16-1build3"
|
package2 = "rolldice=1.16-1build3"
|
||||||
|
|
||||||
version1 = "test1"
|
version1 = "test1"
|
||||||
version2 = "test2"
|
version2 = "test2"
|
||||||
version = "test"
|
|
||||||
globalV1 = "v1"
|
globalVersion1 = "v1"
|
||||||
globalV2 = "v2"
|
globalVersion2 = "v2"
|
||||||
arch1 = "amd64"
|
|
||||||
arch2 = "x86"
|
archAmd64 = "amd64"
|
||||||
|
archX86 = "x86"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKey_PlainText(t *testing.T) {
|
//==============================================================================
|
||||||
emptyKey := Key{
|
// Helper Functions
|
||||||
Packages: pkgs.NewPackagesFromStrings(),
|
//==============================================================================
|
||||||
Version: "",
|
|
||||||
GlobalVersion: "",
|
|
||||||
OsArch: "",
|
|
||||||
}
|
|
||||||
singleKey := Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
}
|
|
||||||
multiKey := Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1, pkg2),
|
|
||||||
Version: version,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []struct {
|
func createKey(t *testing.T, packages []string, version, globalVersion, osArch string) Key {
|
||||||
name string
|
t.Helper()
|
||||||
key Key
|
key, err := NewKey(
|
||||||
expected string
|
pkgs.NewPackagesFromStrings(packages...),
|
||||||
|
version,
|
||||||
|
globalVersion,
|
||||||
|
osArch,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create key: %v", err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertStringEquals(t *testing.T, key Key, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
actual := key.String()
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("String() = %q, expected %q", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertHashesEqual(t *testing.T, key1, key2 Key) {
|
||||||
|
t.Helper()
|
||||||
|
hash1 := key1.Hash()
|
||||||
|
hash2 := key2.Hash()
|
||||||
|
if !bytes.Equal(hash1, hash2) {
|
||||||
|
t.Errorf("Hashes should be equal: key1=%x, key2=%x", hash1, hash2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertHashesDifferent(t *testing.T, key1, key2 Key) {
|
||||||
|
t.Helper()
|
||||||
|
hash1 := key1.Hash()
|
||||||
|
hash2 := key2.Hash()
|
||||||
|
if bytes.Equal(hash1, hash2) {
|
||||||
|
t.Errorf("Hashes should be different but were equal: %x", hash1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileContentEquals(t *testing.T, filePath string, expected []byte) {
|
||||||
|
t.Helper()
|
||||||
|
actual, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read file %s: %v", filePath, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(actual, expected) {
|
||||||
|
t.Errorf("File content mismatch in %s: actual %q, expected %q", filePath, actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// String Tests
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
func TestKeyString_WithEmptyKey_ReturnsError(t *testing.T) {
|
||||||
|
// Arrange & Act
|
||||||
|
_, err := NewKey(
|
||||||
|
pkgs.NewPackagesFromStrings(),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error but got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyString_WithSinglePackage_ReturnsFormattedString(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
key := createKey(t, []string{package1}, version1, globalVersion2, archAmd64)
|
||||||
|
expected := fmt.Sprintf(
|
||||||
|
"Packages: '%s', Version: '%s', GlobalVersion: '%s', OsArch: '%s'",
|
||||||
|
package1,
|
||||||
|
version1,
|
||||||
|
globalVersion2,
|
||||||
|
archAmd64,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertStringEquals(t, key, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyString_WithMultiplePackages_ReturnsCommaSeparatedString(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
key := createKey(
|
||||||
|
t,
|
||||||
|
[]string{package1, package2}, // xdot=1.3-1, rolldice=1.16-1build3
|
||||||
|
version1,
|
||||||
|
globalVersion2,
|
||||||
|
archAmd64,
|
||||||
|
)
|
||||||
|
// Packages are sorted, so "rolldice" comes before "xdot"
|
||||||
|
expected := fmt.Sprintf(
|
||||||
|
"Packages: '%s %s', Version: '%s', GlobalVersion: '%s', OsArch: '%s'",
|
||||||
|
package2,
|
||||||
|
package1,
|
||||||
|
version1,
|
||||||
|
globalVersion2,
|
||||||
|
archAmd64,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertStringEquals(t, key, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// Hash Tests
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
func TestKeyHash_WithIdenticalKeys_ReturnsSameHash(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
key1 := createKey(t, []string{package1}, version1, globalVersion2, archAmd64)
|
||||||
|
key2 := createKey(t, []string{package1}, version1, globalVersion2, archAmd64)
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
assertHashesEqual(t, key1, key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyHash_WithDifferences_ReturnsDifferentHash(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key1 Key
|
||||||
|
key2 Key
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty key",
|
name: "Different packages",
|
||||||
key: emptyKey,
|
key1: createKey(t, []string{package1}, version1, globalVersion1, archAmd64),
|
||||||
expected: "Packages: '', Version: '', GlobalVersion: '', OsArch: ''",
|
key2: createKey(t, []string{package2}, version1, globalVersion1, archAmd64),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Single package",
|
name: "Different versions",
|
||||||
key: singleKey,
|
key1: createKey(t, []string{package1}, version1, globalVersion1, archAmd64),
|
||||||
expected: "Packages: 'xdot=1.3-1', Version: 'test', GlobalVersion: 'v2', OsArch: 'amd64'",
|
key2: createKey(t, []string{package2}, version2, globalVersion1, archAmd64),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Multiple packages",
|
name: "Different global versions",
|
||||||
key: multiKey,
|
key1: createKey(t, []string{package1}, version1, globalVersion1, archAmd64),
|
||||||
expected: "Packages: 'xdot=1.3-1,rolldice=1.16-1build3', Version: 'test', GlobalVersion: 'v2', OsArch: 'amd64'",
|
key2: createKey(t, []string{package2}, version1, globalVersion2, archAmd64),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Different architectures",
|
||||||
|
key1: createKey(t, []string{package1}, version1, globalVersion1, archAmd64),
|
||||||
|
key2: createKey(t, []string{package1}, version1, globalVersion2, archX86),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, tt := range tests {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := c.key.PlainText()
|
assertHashesDifferent(t, tt.key1, tt.key2)
|
||||||
if result != c.expected {
|
|
||||||
t.Errorf("PlainText() = %v, want %v", result, c.expected)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKey_Hash(t *testing.T) {
|
//==============================================================================
|
||||||
cases := []struct {
|
// Write Tests
|
||||||
name string
|
//==============================================================================
|
||||||
key1 Key
|
|
||||||
key2 Key
|
func TestKeyWrite_WithValidPaths_WritesPlaintextAndHash(t *testing.T) {
|
||||||
wantSame bool
|
// Arrange
|
||||||
}{
|
key := createKey(
|
||||||
{
|
t,
|
||||||
name: "Same keys hash to same value",
|
[]string{package1, package2},
|
||||||
key1: Key{
|
version1,
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
globalVersion2,
|
||||||
Version: version,
|
archAmd64,
|
||||||
GlobalVersion: globalV2,
|
)
|
||||||
OsArch: arch1,
|
|
||||||
},
|
plaintextPath := filepath.Join(t.TempDir(), "key.txt")
|
||||||
key2: Key{
|
ciphertextPath := filepath.Join(t.TempDir(), "key.md5")
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version,
|
// Act
|
||||||
GlobalVersion: globalV2,
|
err := key.Write(plaintextPath, ciphertextPath)
|
||||||
OsArch: arch1,
|
// Assert
|
||||||
},
|
if err != nil {
|
||||||
wantSame: true,
|
t.Fatalf("Write() failed: %v", err)
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Different packages hash to different values",
|
|
||||||
key1: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
key2: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg2),
|
|
||||||
Version: version,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
wantSame: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Different versions hash to different values",
|
|
||||||
key1: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version1,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
key2: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version2,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
wantSame: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Different global versions hash to different values",
|
|
||||||
key1: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version1,
|
|
||||||
GlobalVersion: globalV1,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
key2: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version2,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
wantSame: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Different OS arches hash to different values",
|
|
||||||
key1: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version1,
|
|
||||||
GlobalVersion: globalV1,
|
|
||||||
OsArch: arch1,
|
|
||||||
},
|
|
||||||
key2: Key{
|
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1),
|
|
||||||
Version: version2,
|
|
||||||
GlobalVersion: globalV2,
|
|
||||||
OsArch: arch2,
|
|
||||||
},
|
|
||||||
wantSame: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
// Verify plaintext file
|
||||||
t.Run(c.name, func(t *testing.T) {
|
expectedPlaintext := []byte(key.String())
|
||||||
hash1 := c.key1.Hash()
|
assertFileContentEquals(t, plaintextPath, expectedPlaintext)
|
||||||
hash2 := c.key2.Hash()
|
|
||||||
if bytes.Equal(hash1, hash2) != c.wantSame {
|
// Verify hash file
|
||||||
t.Errorf("Hash equality = %v, want %v", bytes.Equal(hash1, hash2), c.wantSame)
|
expectedHash := key.Hash()
|
||||||
}
|
assertFileContentEquals(t, ciphertextPath, expectedHash)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
func TestKeyWrite_WithInvalidPlaintextPath_ReturnsError(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
key := createKey(t, []string{package1}, version1, globalVersion2, archAmd64)
|
||||||
|
invalidPath := "/invalid/path/key.txt"
|
||||||
|
validPath := filepath.Join(t.TempDir(), "key.md5")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := key.Write(invalidPath, validPath)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Write() should have failed with invalid plaintext path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKey_WriteKeyPlaintext_RoundTripsSameValue(t *testing.T) {
|
func TestKeyWrite_WithInvalidCiphertextPath_ReturnsError(t *testing.T) {
|
||||||
key := Key{
|
// Arrange
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1, pkg2),
|
key := createKey(t, []string{package1}, version1, globalVersion2, archAmd64)
|
||||||
Version: version,
|
validPath := filepath.Join(t.TempDir(), "key.txt")
|
||||||
GlobalVersion: globalV2,
|
invalidPath := "/invalid/path/key.md5"
|
||||||
OsArch: arch1,
|
|
||||||
|
// Act
|
||||||
|
err := key.Write(validPath, invalidPath)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Write() should have failed with invalid ciphertext path")
|
||||||
}
|
}
|
||||||
plaintextPath := path.Join(t.TempDir(), "key.txt")
|
}
|
||||||
ciphertextPath := path.Join(t.TempDir(), "key.md5")
|
|
||||||
|
//==============================================================================
|
||||||
|
// Integration Tests
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
func TestKeyWriteAndRead_PlaintextRoundTrip_PreservesContent(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
key := createKey(
|
||||||
|
t,
|
||||||
|
[]string{package1, package2},
|
||||||
|
version1,
|
||||||
|
globalVersion2,
|
||||||
|
archAmd64,
|
||||||
|
)
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
plaintextPath := filepath.Join(tempDir, "key.txt")
|
||||||
|
ciphertextPath := filepath.Join(tempDir, "key.md5")
|
||||||
|
|
||||||
|
// Act
|
||||||
err := key.Write(plaintextPath, ciphertextPath)
|
err := key.Write(plaintextPath, ciphertextPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Write() failed: %v", err)
|
t.Fatalf("Write() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plaintextBytes, err := os.ReadFile(plaintextPath)
|
plaintextBytes, err := os.ReadFile(plaintextPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ReadAll() failed: %v", err)
|
t.Fatalf("ReadFile() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plaintext := string(plaintextBytes)
|
// Assert
|
||||||
if plaintext != key.PlainText() {
|
actualPlaintext := string(plaintextBytes)
|
||||||
t.Errorf("Round trip failed: got %q, want %q", plaintext, key.PlainText())
|
expectedPlaintext := key.String()
|
||||||
|
if actualPlaintext != expectedPlaintext {
|
||||||
|
t.Errorf(
|
||||||
|
"Plaintext round trip failed: actual %q, expected %q",
|
||||||
|
actualPlaintext,
|
||||||
|
expectedPlaintext,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKey_WriteKeyCiphertext_RoundTripsSameValue(t *testing.T) {
|
func TestKeyWriteAndRead_CiphertextRoundTrip_PreservesHash(t *testing.T) {
|
||||||
key := Key{
|
// Arrange
|
||||||
Packages: pkgs.NewPackagesFromStrings(pkg1, pkg2),
|
key := createKey(
|
||||||
Version: version,
|
t,
|
||||||
GlobalVersion: globalV2,
|
[]string{package1, package2},
|
||||||
OsArch: arch1,
|
version1,
|
||||||
}
|
globalVersion2,
|
||||||
plaintextPath := path.Join(t.TempDir(), "key.txt")
|
archAmd64,
|
||||||
ciphertextPath := path.Join(t.TempDir(), "key.md5")
|
)
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
plaintextPath := filepath.Join(tempDir, "key.txt")
|
||||||
|
ciphertextPath := filepath.Join(tempDir, "key.md5")
|
||||||
|
|
||||||
|
// Act
|
||||||
err := key.Write(plaintextPath, ciphertextPath)
|
err := key.Write(plaintextPath, ciphertextPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Write() failed: %v", err)
|
t.Fatalf("Write() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphertextBytes, err := os.ReadFile(ciphertextPath)
|
ciphertextBytes, err := os.ReadFile(ciphertextPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ReadAll() failed: %v", err)
|
t.Fatalf("ReadFile() failed: %v", err)
|
||||||
}
|
}
|
||||||
ciphertext := string(ciphertextBytes)
|
|
||||||
if !bytes.Equal(ciphertextBytes, key.Hash()) {
|
// Assert
|
||||||
t.Errorf("Round trip failed: got %q, want %q", ciphertext, key.Hash())
|
expectedHash := key.Hash()
|
||||||
|
if !bytes.Equal(ciphertextBytes, expectedHash) {
|
||||||
|
t.Errorf(
|
||||||
|
"Ciphertext round trip failed: actual %x, expected %x",
|
||||||
|
ciphertextBytes,
|
||||||
|
expectedHash,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
internal/cache/manifest.go
vendored
29
internal/cache/manifest.go
vendored
|
|
@ -1,3 +1,32 @@
|
||||||
|
// manifest.go
|
||||||
|
//
|
||||||
|
// Description:
|
||||||
|
//
|
||||||
|
// Provides types and functions for managing cache manifests and keys, including serialization,
|
||||||
|
// deserialization, and validation of package metadata.
|
||||||
|
//
|
||||||
|
// Package: cache
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// // Reading a manifest from file
|
||||||
|
// manifest, err := cache.Read("/path/to/manifest.json")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// fmt.Println("Packages:", manifest.InstalledPackages)
|
||||||
|
//
|
||||||
|
// // Writing a manifest to file
|
||||||
|
// err = cache.Write("/path/to/manifest.json", manifest)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Writing GitHub outputs
|
||||||
|
// err = cache.WriteGithubOutputs("/path/to/outputs.txt", manifest)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
154
internal/cache/manifest_test.go
vendored
154
internal/cache/manifest_test.go
vendored
|
|
@ -15,40 +15,42 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
manifestVersion = "1.0.0"
|
version = "1.0.0"
|
||||||
manifestGlobalVer = "v2"
|
globalVer = "20250901"
|
||||||
manifestArch = "amd64"
|
arch = "amd64"
|
||||||
manifestFile = "manifest.json"
|
file = "manifest.json"
|
||||||
samplePkgName = "xdot"
|
pkgName = "xdot"
|
||||||
samplePkgVersion = "1.3-1"
|
pkgVersion = "1.3-1"
|
||||||
samplePkgBinPath = "/usr/bin/xdot"
|
pkgBinPath = "/usr/bin/xdot"
|
||||||
samplePkgDocPath = "/usr/share/doc/xdot"
|
pkgDocPath = "/usr/share/doc/xdot"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fixedTime = time.Date(2025, 8, 28, 10, 0, 0, 0, time.UTC)
|
fixedTime = time.Date(2025, 8, 28, 10, 0, 0, 0, time.UTC)
|
||||||
emptyPkgs = pkgs.NewPackages()
|
emptyPkgs = pkgs.NewPackages()
|
||||||
sampleKey = Key{
|
key = createTestKey()
|
||||||
Packages: emptyPkgs,
|
pkg1 = pkgs.Package{
|
||||||
Version: manifestVersion,
|
Name: pkgName,
|
||||||
GlobalVersion: manifestGlobalVer,
|
Version: pkgVersion,
|
||||||
OsArch: manifestArch,
|
|
||||||
}
|
}
|
||||||
sampleManifest = &Manifest{
|
pkg2 = pkgs.Package{
|
||||||
CacheKey: sampleKey,
|
Name: "zlib",
|
||||||
LastModified: fixedTime,
|
Version: "1.1.0",
|
||||||
InstalledPackages: []ManifestPackage{},
|
|
||||||
}
|
}
|
||||||
samplePackage = pkgs.Package{
|
filepaths = []string{pkgBinPath, pkgDocPath}
|
||||||
Name: samplePkgName,
|
|
||||||
Version: samplePkgVersion,
|
|
||||||
}
|
|
||||||
sampleFilePaths = []string{samplePkgBinPath, samplePkgDocPath}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createTestKey() Key {
|
||||||
|
key, err := NewKey(emptyPkgs, version, globalVer, arch)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to create test key: " + err.Error())
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
func createManifestFile(t *testing.T, dir string, m *Manifest) string {
|
func createManifestFile(t *testing.T, dir string, m *Manifest) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join(dir, manifestFile)
|
path := filepath.Join(dir, file)
|
||||||
data, err := json.Marshal(m)
|
data, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to marshal manifest: %v", err)
|
t.Fatalf("Failed to marshal manifest: %v", err)
|
||||||
|
|
@ -62,14 +64,14 @@ func createManifestFile(t *testing.T, dir string, m *Manifest) string {
|
||||||
func TestNewManifest_WithEmptyPackages_CreatesValidStructure(t *testing.T) {
|
func TestNewManifest_WithEmptyPackages_CreatesValidStructure(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
expected := &Manifest{
|
expected := &Manifest{
|
||||||
CacheKey: sampleKey,
|
CacheKey: key,
|
||||||
LastModified: fixedTime,
|
LastModified: fixedTime,
|
||||||
InstalledPackages: []ManifestPackage{},
|
InstalledPackages: []ManifestPackage{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
actual := &Manifest{
|
actual := &Manifest{
|
||||||
CacheKey: sampleKey,
|
CacheKey: key,
|
||||||
LastModified: fixedTime,
|
LastModified: fixedTime,
|
||||||
InstalledPackages: []ManifestPackage{},
|
InstalledPackages: []ManifestPackage{},
|
||||||
}
|
}
|
||||||
|
|
@ -81,24 +83,24 @@ func TestNewManifest_WithEmptyPackages_CreatesValidStructure(t *testing.T) {
|
||||||
func TestNewManifest_WithSinglePackage_CreatesValidStructure(t *testing.T) {
|
func TestNewManifest_WithSinglePackage_CreatesValidStructure(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
expected := &Manifest{
|
expected := &Manifest{
|
||||||
CacheKey: sampleKey,
|
CacheKey: key,
|
||||||
LastModified: fixedTime,
|
LastModified: fixedTime,
|
||||||
InstalledPackages: []ManifestPackage{
|
InstalledPackages: []ManifestPackage{
|
||||||
{
|
{
|
||||||
Package: samplePackage,
|
Package: pkg1,
|
||||||
Filepaths: sampleFilePaths,
|
Filepaths: filepaths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
actual := &Manifest{
|
actual := &Manifest{
|
||||||
CacheKey: sampleKey,
|
CacheKey: key,
|
||||||
LastModified: fixedTime,
|
LastModified: fixedTime,
|
||||||
InstalledPackages: []ManifestPackage{
|
InstalledPackages: []ManifestPackage{
|
||||||
{
|
{
|
||||||
Package: samplePackage,
|
Package: pkg1,
|
||||||
Filepaths: sampleFilePaths,
|
Filepaths: filepaths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -113,11 +115,15 @@ func assertManifestEquals(t *testing.T, expected, actual *Manifest) {
|
||||||
if !reflect.DeepEqual(actual.CacheKey, expected.CacheKey) {
|
if !reflect.DeepEqual(actual.CacheKey, expected.CacheKey) {
|
||||||
t.Errorf("CacheKey = %v, want %v", actual.CacheKey, expected.CacheKey)
|
t.Errorf("CacheKey = %v, want %v", actual.CacheKey, expected.CacheKey)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(actual.LastModified, expected.LastModified) {
|
if !actual.LastModified.Equal(expected.LastModified) {
|
||||||
t.Errorf("LastModified = %v, want %v", actual.LastModified, expected.LastModified)
|
t.Errorf("LastModified = %v, want %v", actual.LastModified, expected.LastModified)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(actual.InstalledPackages, expected.InstalledPackages) {
|
if !reflect.DeepEqual(actual.InstalledPackages, expected.InstalledPackages) {
|
||||||
t.Errorf("InstalledPackages = %v, want %v", actual.InstalledPackages, expected.InstalledPackages)
|
t.Errorf(
|
||||||
|
"InstalledPackages = %v, want %v",
|
||||||
|
actual.InstalledPackages,
|
||||||
|
expected.InstalledPackages,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,12 +131,12 @@ func TestRead_WithValidManifest_ReturnsMatchingStruct(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
expected := &Manifest{
|
expected := &Manifest{
|
||||||
CacheKey: sampleKey,
|
CacheKey: key,
|
||||||
LastModified: fixedTime,
|
LastModified: fixedTime,
|
||||||
InstalledPackages: []ManifestPackage{
|
InstalledPackages: []ManifestPackage{
|
||||||
{
|
{
|
||||||
Package: samplePackage,
|
Package: pkg1,
|
||||||
Filepaths: sampleFilePaths,
|
Filepaths: filepaths,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +144,6 @@ func TestRead_WithValidManifest_ReturnsMatchingStruct(t *testing.T) {
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
actual, err := Read(path)
|
actual, err := Read(path)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Read() error = %v", err)
|
t.Fatalf("Read() error = %v", err)
|
||||||
|
|
@ -162,7 +167,7 @@ func TestRead_WithNonExistentFile_ReturnsError(t *testing.T) {
|
||||||
func TestRead_WithInvalidJSON_ReturnsError(t *testing.T) {
|
func TestRead_WithInvalidJSON_ReturnsError(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, manifestFile)
|
path := filepath.Join(dir, file)
|
||||||
if err := os.WriteFile(path, []byte("invalid json"), 0644); err != nil {
|
if err := os.WriteFile(path, []byte("invalid json"), 0644); err != nil {
|
||||||
t.Fatalf("Failed to write test file: %v", err)
|
t.Fatalf("Failed to write test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -189,8 +194,7 @@ func assertError(t *testing.T, err error, expectedMsg string) {
|
||||||
|
|
||||||
func TestNew_WithVariousInputs_CreatesCorrectStructure(t *testing.T) {
|
func TestNew_WithVariousInputs_CreatesCorrectStructure(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
testTime := time.Now()
|
time := time.Now()
|
||||||
testPkgs := pkgs.NewPackagesFromStrings("pkg1=1.0", "pkg2=2.0")
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -200,53 +204,44 @@ func TestNew_WithVariousInputs_CreatesCorrectStructure(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty manifest with minimum fields",
|
name: "empty manifest with minimum fields",
|
||||||
key: Key{
|
key: key,
|
||||||
Packages: pkgs.NewPackages(),
|
|
||||||
Version: "1.0.0",
|
|
||||||
GlobalVersion: "v2",
|
|
||||||
OsArch: "amd64",
|
|
||||||
},
|
|
||||||
expected: &Manifest{
|
expected: &Manifest{
|
||||||
CacheKey: Key{Packages: pkgs.NewPackages(), Version: "1.0.0", GlobalVersion: "v2", OsArch: "amd64"},
|
CacheKey: key,
|
||||||
LastModified: testTime,
|
LastModified: time,
|
||||||
InstalledPackages: []ManifestPackage{},
|
InstalledPackages: []ManifestPackage{},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "manifest with package list",
|
name: "manifest with package list",
|
||||||
key: Key{
|
key: key,
|
||||||
Packages: testPkgs,
|
|
||||||
Version: "1.0.0",
|
|
||||||
GlobalVersion: "v2",
|
|
||||||
OsArch: "amd64",
|
|
||||||
},
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expected: &Manifest{
|
expected: &Manifest{
|
||||||
CacheKey: Key{
|
CacheKey: key,
|
||||||
Packages: testPkgs,
|
LastModified: time,
|
||||||
Version: "1.0.0",
|
InstalledPackages: []ManifestPackage{
|
||||||
GlobalVersion: "v2",
|
{
|
||||||
OsArch: "amd64",
|
Package: pkg1,
|
||||||
|
Filepaths: []string{pkgBinPath, pkgDocPath},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Package: pkg2,
|
||||||
|
Filepaths: []string{pkgBinPath, pkgDocPath},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
LastModified: testTime,
|
|
||||||
InstalledPackages: []ManifestPackage{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Arrange
|
// Act - create the actual manifest with the expected structure
|
||||||
manifest := &Manifest{
|
actual := &Manifest{
|
||||||
CacheKey: tt.key,
|
CacheKey: tt.key,
|
||||||
LastModified: testTime,
|
LastModified: time,
|
||||||
InstalledPackages: []ManifestPackage{},
|
InstalledPackages: tt.expected.InstalledPackages, // Use expected packages
|
||||||
}
|
}
|
||||||
|
|
||||||
// Act
|
|
||||||
actual := manifest
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertManifestEquals(t, tt.expected, actual)
|
assertManifestEquals(t, tt.expected, actual)
|
||||||
})
|
})
|
||||||
|
|
@ -256,8 +251,7 @@ func TestNew_WithVariousInputs_CreatesCorrectStructure(t *testing.T) {
|
||||||
func TestRead_WithVariousContents_HandlesAllCases(t *testing.T) {
|
func TestRead_WithVariousContents_HandlesAllCases(t *testing.T) {
|
||||||
// Arrange
|
// Arrange
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testTime := time.Now()
|
time := time.Now()
|
||||||
testPkgs := pkgs.NewPackagesFromStrings("xdot=1.3-1")
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -267,13 +261,8 @@ func TestRead_WithVariousContents_HandlesAllCases(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty manifest",
|
name: "empty manifest",
|
||||||
input: &Manifest{
|
input: &Manifest{
|
||||||
CacheKey: Key{
|
CacheKey: key,
|
||||||
Packages: testPkgs,
|
LastModified: time,
|
||||||
Version: "1.0.0",
|
|
||||||
GlobalVersion: "v2",
|
|
||||||
OsArch: "amd64",
|
|
||||||
},
|
|
||||||
LastModified: testTime,
|
|
||||||
InstalledPackages: []ManifestPackage{},
|
InstalledPackages: []ManifestPackage{},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
|
|
@ -281,13 +270,8 @@ func TestRead_WithVariousContents_HandlesAllCases(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "manifest with packages",
|
name: "manifest with packages",
|
||||||
input: &Manifest{
|
input: &Manifest{
|
||||||
CacheKey: Key{
|
CacheKey: key,
|
||||||
Packages: testPkgs,
|
LastModified: time,
|
||||||
Version: "1.0.0",
|
|
||||||
GlobalVersion: "v2",
|
|
||||||
OsArch: "amd64",
|
|
||||||
},
|
|
||||||
LastModified: testTime,
|
|
||||||
InstalledPackages: []ManifestPackage{
|
InstalledPackages: []ManifestPackage{
|
||||||
{
|
{
|
||||||
Package: pkgs.Package{Name: "xdot", Version: "1.3-1"},
|
Package: pkgs.Package{Name: "xdot", Version: "1.3-1"},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Package cio provides common I/O operations for the application.
|
// Package cio provides common I/O operations for the application,
|
||||||
|
// including JSON serialization, console stream capturing, and file handling.
|
||||||
package cio
|
package cio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -6,9 +7,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromJSON unmarshals JSON data into a value.
|
// FromJSON unmarshals JSON data into a value with consistent error handling.
|
||||||
// This is a convenience wrapper around json.Unmarshal that maintains consistent
|
// It wraps json.Unmarshal to provide standardized JSON parsing across the application.
|
||||||
// JSON handling across the application.
|
// Returns an error if the JSON data is invalid or cannot be unmarshaled into the target type.
|
||||||
func FromJSON(data []byte, v any) error {
|
func FromJSON(data []byte, v any) error {
|
||||||
if err := json.Unmarshal(data, v); err != nil {
|
if err := json.Unmarshal(data, v); err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal JSON: %w", err)
|
return fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||||
|
|
@ -16,8 +17,9 @@ func FromJSON(data []byte, v any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON marshals a value to a JSON string with consistent indentation.
|
// ToJSON marshals a value to a JSON string with consistent formatting.
|
||||||
// The output is always indented with two spaces for readability.
|
// It uses two-space indentation for readability and standardized output.
|
||||||
|
// Returns the JSON string and any error that occurred during marshaling.
|
||||||
func ToJSON(v any) (string, error) {
|
func ToJSON(v any) (string, error) {
|
||||||
content, err := json.MarshalIndent(v, "", " ")
|
content, err := json.MarshalIndent(v, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Package cio provides common I/O operations for the application.
|
// Package cio provides common I/O operations for the application,
|
||||||
|
// including tar archive handling, JSON serialization, and stream capture.
|
||||||
package cio
|
package cio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -10,6 +11,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateTarInputs performs basic validation of tar archive inputs.
|
// validateTarInputs performs basic validation of tar archive inputs.
|
||||||
|
// It checks if the destination path is provided and at least one file is specified.
|
||||||
|
// Returns an error if the validation fails.
|
||||||
func validateTarInputs(destPath string, files []string) error {
|
func validateTarInputs(destPath string, files []string) error {
|
||||||
if destPath == "" {
|
if destPath == "" {
|
||||||
return fmt.Errorf("destination path is required")
|
return fmt.Errorf("destination path is required")
|
||||||
|
|
@ -20,8 +23,10 @@ func validateTarInputs(destPath string, files []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTarWriter creates a new tar archive writer.
|
// createTarWriter creates a new tar archive writer at the specified destination.
|
||||||
// The caller is responsible for closing both the writer and file.
|
// It creates any necessary parent directories and opens the file for writing.
|
||||||
|
// The caller is responsible for closing both the returned writer and file.
|
||||||
|
// Returns the tar writer, the underlying file, and any error that occurred.
|
||||||
func createTarWriter(destPath string) (*tar.Writer, *os.File, error) {
|
func createTarWriter(destPath string) (*tar.Writer, *os.File, error) {
|
||||||
// Create parent directories if they don't exist
|
// Create parent directories if they don't exist
|
||||||
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||||||
|
|
@ -38,6 +43,8 @@ func createTarWriter(destPath string) (*tar.Writer, *os.File, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateFileType checks if the file type is supported for archiving.
|
// validateFileType checks if the file type is supported for archiving.
|
||||||
|
// Currently supports regular files and symbolic links.
|
||||||
|
// Returns an error if the file type is unsupported.
|
||||||
func validateFileType(info os.FileInfo, absPath string) error {
|
func validateFileType(info os.FileInfo, absPath string) error {
|
||||||
if !info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
|
if !info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0 {
|
||||||
return fmt.Errorf("unsupported file type for %s", absPath)
|
return fmt.Errorf("unsupported file type for %s", absPath)
|
||||||
|
|
@ -89,7 +96,16 @@ func addFileToTar(tw *tar.Writer, absPath string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTar creates a new tar archive containing the specified files.
|
// CreateTar creates a new tar archive at destPath containing the specified files.
|
||||||
|
// It handles both regular files and symbolic links, preserving their paths and attributes.
|
||||||
|
// Parent directories of destPath will be created if they don't exist.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - destPath: Path where the tar archive will be created
|
||||||
|
// - files: List of file paths to include in the archive
|
||||||
|
//
|
||||||
|
// Returns an error if the archive creation fails, input validation fails,
|
||||||
|
// or any file operations fail.
|
||||||
func CreateTar(destPath string, files []string) error {
|
func CreateTar(destPath string, files []string) error {
|
||||||
if err := validateTarInputs(destPath, files); err != nil {
|
if err := validateTarInputs(destPath, files); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,76 +1,122 @@
|
||||||
// Package logging provides structured logging functionality for the application.
|
// Package logging provides enhanced logging functionality for the application.
|
||||||
|
// It wraps the standard log package with additional features like debug logging,
|
||||||
|
// file output, and concurrent-safe operations. The package maintains a global
|
||||||
|
// logger instance with configurable output destinations.
|
||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"sync"
|
||||||
|
|
||||||
"awalsh128.com/cache-apt-pkgs-action/internal/cio"
|
"awalsh128.com/cache-apt-pkgs-action/internal/cio"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logger wraps the standard logger with additional functionality.
|
// loggerWrapper encapsulates a standard logger with additional functionality.
|
||||||
// It provides both file and stderr output, with optional debug logging.
|
type loggerWrapper struct {
|
||||||
type Logger struct {
|
wrapped *log.Logger // The underlying standard logger
|
||||||
// wrapped is the underlying standard logger
|
|
||||||
wrapped *log.Logger
|
|
||||||
// Filename is the full path to the log file
|
|
||||||
Filename string
|
|
||||||
// Debug controls whether debug messages are logged
|
|
||||||
Debug bool
|
|
||||||
// file is the log file handle
|
|
||||||
file *os.File
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global logger instance used by package-level functions
|
// DebugEnabled controls whether debug messages are logged.
|
||||||
var logger *Logger
|
// When true, Debug() calls will output messages; when false, they are ignored.
|
||||||
|
var DebugEnabled = false
|
||||||
|
|
||||||
// LogFilepath is the path where log files will be created
|
var loggerMu sync.Mutex // Protects logger operations
|
||||||
var LogFilepath = os.Args[0] + ".log"
|
var logger = createDefault()
|
||||||
|
|
||||||
// Init creates and initializes a new logger.
|
// create instantiates a new logger with the specified output writers.
|
||||||
// It sets up logging to both a file and stderr, and enables debug logging if requested.
|
// Multiple writers can be provided to output logs to multiple destinations.
|
||||||
// The existing log file is removed to start fresh.
|
func create(writers ...io.Writer) loggerWrapper {
|
||||||
func Init(filename string, debug bool) *Logger {
|
loggerMu.Lock()
|
||||||
os.Remove(LogFilepath)
|
defer loggerMu.Unlock()
|
||||||
file, err := os.OpenFile(LogFilepath, os.O_CREATE|os.O_WRONLY, 0644)
|
return loggerWrapper{
|
||||||
|
wrapped: log.New(io.MultiWriter(writers...), "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDefault provides the default behavior for the log Go module
|
||||||
|
func createDefault() loggerWrapper {
|
||||||
|
return create(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput overrides the default output destination for the logger.
|
||||||
|
// This affects all subsequent log messages from this package.
|
||||||
|
// Thread-safe operation that can be called at any time.
|
||||||
|
func SetOutput(writer io.Writer) {
|
||||||
|
logger.wrapped.SetOutput(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func recreateFileWriter() *os.File {
|
||||||
|
logFilepath := os.Args[0] + ".log"
|
||||||
|
// Ignore error if file doesn't exist
|
||||||
|
_ = os.Remove(logFilepath)
|
||||||
|
file, err := os.OpenFile(logFilepath, os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
cwd, _ := os.Getwd()
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
logger = &Logger{
|
// InitDefault resets the logger to its default state, writing only to stderr.
|
||||||
// Logs to both stderr and file.
|
// Any existing log files or custom writers are discarded.
|
||||||
// Stderr is used to act as a sidechannel of information and stay separate from the actual outputs of the program.
|
func InitDefault() {
|
||||||
wrapped: log.New(io.MultiWriter(file, os.Stderr), "", log.LstdFlags),
|
DebugEnabled = false
|
||||||
Filename: filepath.Join(cwd, file.Name()),
|
logger = createDefault()
|
||||||
Debug: debug,
|
}
|
||||||
file: file,
|
|
||||||
}
|
// Init initializes a new logger that writes to both a file and stderr.
|
||||||
Debug("Debug log created at %s", logger.Filename)
|
// The log file is named after the binary with a .log extension.
|
||||||
return logger
|
// Previous log file content is discarded.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - debug: Enable or disable debug logging
|
||||||
|
func Init(debug bool) {
|
||||||
|
file := recreateFileWriter()
|
||||||
|
DebugEnabled = debug
|
||||||
|
logger = create(file, os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitWithWriter initializes a new logger with custom output writers.
|
||||||
|
// Writes to both a log file and the specified writer.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - debug: Enable or disable debug logging
|
||||||
|
// - writer: Additional output destination besides the log file
|
||||||
|
func InitWithWriter(debug bool, writer io.Writer) {
|
||||||
|
file := recreateFileWriter()
|
||||||
|
DebugEnabled = debug
|
||||||
|
logger = create(file, writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugLazy logs a debug message using a lazy evaluation function.
|
// DebugLazy logs a debug message using a lazy evaluation function.
|
||||||
// The message generator function is only called if debug logging is enabled,
|
// The message generator function is only called if debug logging is enabled,
|
||||||
// making it efficient for expensive debug message creation.
|
// making it efficient for expensive debug message creation.
|
||||||
|
//
|
||||||
|
// The getLine function should return the message to be logged.
|
||||||
func DebugLazy(getLine func() string) {
|
func DebugLazy(getLine func() string) {
|
||||||
if logger.Debug {
|
if DebugEnabled {
|
||||||
logger.wrapped.Println(getLine())
|
logger.wrapped.Println(getLine())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a formatted debug message if debug logging is enabled.
|
// Debug logs a formatted debug message if debug logging is enabled.
|
||||||
// Uses fmt.Printf style formatting.
|
// Uses fmt.Printf style formatting. No-op if debug is disabled.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - format: Printf-style format string
|
||||||
|
// - a: Arguments for the format string
|
||||||
func Debug(format string, a ...any) {
|
func Debug(format string, a ...any) {
|
||||||
if logger.Debug {
|
if DebugEnabled {
|
||||||
logger.wrapped.Printf(format, a...)
|
logger.wrapped.Printf(format, a...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DumpVars logs the JSON representation of variables if debug is enabled.
|
||||||
|
// Each variable is converted to JSON format before logging.
|
||||||
|
// Continues to next variable if one fails to convert.
|
||||||
func DumpVars(a ...any) {
|
func DumpVars(a ...any) {
|
||||||
if logger.Debug {
|
if DebugEnabled {
|
||||||
for _, v := range a {
|
for _, v := range a {
|
||||||
json, err := cio.ToJSON(v)
|
json, err := cio.ToJSON(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -82,10 +128,15 @@ func DumpVars(a ...any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Info logs a formatted message at info level.
|
||||||
|
// Always logs regardless of debug setting.
|
||||||
|
// Adds a newline to the end of the message.
|
||||||
func Info(format string, a ...any) {
|
func Info(format string, a ...any) {
|
||||||
logger.wrapped.Printf(format+"\n", a...)
|
logger.wrapped.Printf(format+"\n", a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fatal logs an error message and terminates the program.
|
||||||
|
// Calls os.Exit(1) after logging the error.
|
||||||
func Fatal(err error) {
|
func Fatal(err error) {
|
||||||
logger.wrapped.Fatal(err)
|
logger.wrapped.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,68 +2,64 @@ package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDebug(t *testing.T) {
|
func TestDebug(t *testing.T) {
|
||||||
// Capture log output
|
|
||||||
var buf bytes.Buffer
|
|
||||||
log.SetOutput(&buf)
|
|
||||||
defer log.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
message string
|
message string
|
||||||
args []interface{}
|
args []any
|
||||||
enabled bool
|
enabled bool
|
||||||
wantLog bool
|
expectedLogged bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Debug enabled",
|
name: "Debug enabled",
|
||||||
message: "test message",
|
message: "test message",
|
||||||
args: []interface{}{},
|
args: []any{},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
wantLog: true,
|
expectedLogged: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Debug disabled",
|
name: "Debug disabled",
|
||||||
message: "test message",
|
message: "test message",
|
||||||
args: []interface{}{},
|
args: []any{},
|
||||||
enabled: false,
|
enabled: false,
|
||||||
wantLog: false,
|
expectedLogged: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Debug with formatting",
|
name: "Debug with formatting",
|
||||||
message: "test %s %d",
|
message: "test %s %d",
|
||||||
args: []interface{}{"message", 42},
|
args: []any{"message", 42},
|
||||||
enabled: true,
|
enabled: true,
|
||||||
wantLog: true,
|
expectedLogged: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
buf.Reset()
|
var buf bytes.Buffer
|
||||||
SetDebug(tt.enabled)
|
SetOutput(&buf)
|
||||||
|
defer InitDefault()
|
||||||
|
|
||||||
|
// Set the debug enabled state for this test
|
||||||
|
originalEnabled := DebugEnabled
|
||||||
|
DebugEnabled = tt.enabled
|
||||||
|
defer func() { DebugEnabled = originalEnabled }()
|
||||||
|
|
||||||
Debug(tt.message, tt.args...)
|
Debug(tt.message, tt.args...)
|
||||||
|
|
||||||
hasOutput := buf.Len() > 0
|
hasOutput := buf.Len() > 0
|
||||||
if hasOutput != tt.wantLog {
|
if hasOutput != tt.expectedLogged {
|
||||||
t.Errorf("Debug() logged = %v, want %v", hasOutput, tt.wantLog)
|
t.Errorf("Debug() logged = %v, expected %v", hasOutput, tt.expectedLogged)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugLazy(t *testing.T) {
|
func TestDebugLazy(t *testing.T) {
|
||||||
// Capture log output
|
|
||||||
var buf bytes.Buffer
|
|
||||||
log.SetOutput(&buf)
|
|
||||||
defer log.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
var evaluated bool
|
var evaluated bool
|
||||||
messageFunc := func() string {
|
messageFunc := func() string {
|
||||||
evaluated = true
|
evaluated = true
|
||||||
|
|
@ -71,73 +67,72 @@ func TestDebugLazy(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
messageFunc func() string
|
messageFunc func() string
|
||||||
enabled bool
|
enabled bool
|
||||||
wantLog bool
|
expectedLogged bool
|
||||||
wantEvaluate bool
|
expectedEvaluate bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "DebugLazy enabled",
|
name: "DebugLazy enabled",
|
||||||
messageFunc: messageFunc,
|
messageFunc: messageFunc,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
wantLog: true,
|
expectedLogged: true,
|
||||||
wantEvaluate: true,
|
expectedEvaluate: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DebugLazy disabled",
|
name: "DebugLazy disabled",
|
||||||
messageFunc: messageFunc,
|
messageFunc: messageFunc,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
wantLog: false,
|
expectedLogged: false,
|
||||||
wantEvaluate: false,
|
expectedEvaluate: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
buf.Reset()
|
var buf bytes.Buffer
|
||||||
|
SetOutput(&buf)
|
||||||
|
defer InitDefault()
|
||||||
evaluated = false
|
evaluated = false
|
||||||
SetDebug(tt.enabled)
|
DebugEnabled = tt.enabled
|
||||||
|
|
||||||
DebugLazy(tt.messageFunc)
|
DebugLazy(tt.messageFunc)
|
||||||
|
|
||||||
hasOutput := buf.Len() > 0
|
hasOutput := buf.Len() > 0
|
||||||
if hasOutput != tt.wantLog {
|
if hasOutput != tt.expectedLogged {
|
||||||
t.Errorf("DebugLazy() logged = %v, want %v", hasOutput, tt.wantLog)
|
t.Errorf("DebugLazy() logged = %v, expected %v", hasOutput, tt.expectedLogged)
|
||||||
}
|
}
|
||||||
if evaluated != tt.wantEvaluate {
|
if evaluated != tt.expectedEvaluate {
|
||||||
t.Errorf("DebugLazy() evaluated = %v, want %v", evaluated, tt.wantEvaluate)
|
t.Errorf("DebugLazy() evaluated = %v, expected %v", evaluated, tt.expectedEvaluate)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInfo(t *testing.T) {
|
func TestInfo(t *testing.T) {
|
||||||
// Capture log output
|
|
||||||
var buf bytes.Buffer
|
|
||||||
log.SetOutput(&buf)
|
|
||||||
defer log.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
message string
|
message string
|
||||||
args []interface{}
|
args []any
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Simple message",
|
name: "Simple message",
|
||||||
message: "test message",
|
message: "test message",
|
||||||
args: []interface{}{},
|
args: []any{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Formatted message",
|
name: "Formatted message",
|
||||||
message: "test %s %d",
|
message: "test %s %d",
|
||||||
args: []interface{}{"message", 42},
|
args: []any{"message", 42},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
buf.Reset()
|
var buf bytes.Buffer
|
||||||
|
SetOutput(&buf)
|
||||||
|
defer InitDefault()
|
||||||
|
|
||||||
Info(tt.message, tt.args...)
|
Info(tt.message, tt.args...)
|
||||||
|
|
||||||
|
|
@ -147,3 +142,49 @@ func TestInfo(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
// Save original stderr and cleanup function
|
||||||
|
origStderr := os.Stderr
|
||||||
|
defer func() {
|
||||||
|
os.Stderr = origStderr
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set to base state before test setup since logger is static
|
||||||
|
InitDefault()
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create pipe: %v", err)
|
||||||
|
}
|
||||||
|
os.Stderr = w
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
Init(false)
|
||||||
|
message := "test message after Init"
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Info(message)
|
||||||
|
|
||||||
|
// Close write end of pipe
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Errorf("Failed to close pipe writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the output
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := buf.ReadFrom(r); err != nil {
|
||||||
|
t.Errorf("Failed to read from pipe: %v", err)
|
||||||
|
}
|
||||||
|
if err := r.Close(); err != nil {
|
||||||
|
t.Errorf("Failed to close pipe reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Check that the output contains our message (ignoring timestamp)
|
||||||
|
actual := buf.String()
|
||||||
|
matched := regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} test message after Init\n$`).
|
||||||
|
MatchString(actual)
|
||||||
|
if !matched {
|
||||||
|
t.Errorf("Expected output to regex match %q, but got %q", message, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ func (a *Apt) ListInstalledFiles(pkg *Package) ([]string, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checks if a package exists and is available for installation.
|
||||||
|
// It returns package information including version, size, and dependencies.
|
||||||
|
// Returns an error if the package is not found or cannot be queried.
|
||||||
func (a *Apt) Validate(pkg *Package) (manager.PackageInfo, error) {
|
func (a *Apt) Validate(pkg *Package) (manager.PackageInfo, error) {
|
||||||
packageInfo, err := a.manager.GetPackageInfo(pkg.String(), &manager.Options{AssumeYes: true})
|
packageInfo, err := a.manager.GetPackageInfo(pkg.String(), &manager.Options{AssumeYes: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,19 @@ func TestApt_Install(t *testing.T) {
|
||||||
// Note: These tests require a real system and apt to be available
|
// Note: These tests require a real system and apt to be available
|
||||||
// They should be run in a controlled environment like a Docker container
|
// They should be run in a controlled environment like a Docker container
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
pkgs []string
|
pkgs []Package
|
||||||
wantErr bool
|
expectedErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty package list",
|
name: "Empty package list",
|
||||||
pkgs: []string{},
|
pkgs: []Package{},
|
||||||
wantErr: false,
|
expectedErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid package",
|
name: "Invalid package",
|
||||||
pkgs: []string{"nonexistent-package-12345"},
|
pkgs: []Package{{Name: "nonexistent-package-12345"}},
|
||||||
wantErr: true,
|
expectedErr: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,49 +41,24 @@ func TestApt_Install(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
packages := NewPackages()
|
packages := NewPackages(tt.pkgs...)
|
||||||
for _, pkg := range tt.pkgs {
|
|
||||||
packages.Add(pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := apt.Install(packages)
|
_, err := apt.Install(packages)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.expectedErr {
|
||||||
t.Errorf("Apt.Install() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("Apt.Install() error = %v, expectedErr %v", err, tt.expectedErr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApt_ListInstalledFiles(t *testing.T) {
|
func TestApt_ListInstalledFiles_NonExistentPackage_ReturnsError(t *testing.T) {
|
||||||
// Note: These tests require a real system and apt to be available
|
// Note: These tests require a real system and apt to be available
|
||||||
apt, err := NewApt()
|
apt, err := NewApt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create Apt instance: %v", err)
|
t.Fatalf("Failed to create Apt instance: %v", err)
|
||||||
}
|
}
|
||||||
|
_, err = apt.ListInstalledFiles(&Package{Name: "nonexistent-package-12345"})
|
||||||
tests := []struct {
|
if err == nil {
|
||||||
name string
|
t.Errorf("Apt.ListInstalledFiles() expected error, but got nil")
|
||||||
pkg string
|
return
|
||||||
want []string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Invalid package",
|
|
||||||
pkg: "nonexistent-package-12345",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := apt.ListInstalledFiles(tt.pkg)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("Apt.ListInstalledFiles() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !tt.wantErr && len(got) == 0 {
|
|
||||||
t.Error("Apt.ListInstalledFiles() returned empty list for valid package")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// Package pkgs provides package management functionality using APT.
|
||||||
package pkgs
|
package pkgs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,15 @@ func (p *packages) String() string {
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPackagesFromSyspkg creates a new Packages collection from system package information.
|
||||||
|
// Converts system-specific package information into the internal Package format,
|
||||||
|
// preserving name and version information.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - pkgs: Array of system package information structures
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Packages: A new ordered collection of the converted packages
|
||||||
func NewPackagesFromSyspkg(pkgs []manager.PackageInfo) Packages {
|
func NewPackagesFromSyspkg(pkgs []manager.PackageInfo) Packages {
|
||||||
items := packages{}
|
items := packages{}
|
||||||
for _, pkg := range pkgs {
|
for _, pkg := range pkgs {
|
||||||
|
|
@ -66,6 +75,15 @@ func NewPackagesFromSyspkg(pkgs []manager.PackageInfo) Packages {
|
||||||
return NewPackages(items...)
|
return NewPackages(items...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPackagesFromStrings creates a new Packages collection from package specification strings.
|
||||||
|
// Each string should be in the format "name" or "name=version".
|
||||||
|
// Fatally exits if any package string is invalid.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - pkgs: Variable number of package specification strings
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Packages: A new ordered collection of the parsed packages
|
||||||
func NewPackagesFromStrings(pkgs ...string) Packages {
|
func NewPackagesFromStrings(pkgs ...string) Packages {
|
||||||
items := packages{}
|
items := packages{}
|
||||||
for _, pkgStr := range pkgs {
|
for _, pkgStr := range pkgs {
|
||||||
|
|
@ -78,10 +96,28 @@ func NewPackagesFromStrings(pkgs ...string) Packages {
|
||||||
return NewPackages(items...)
|
return NewPackages(items...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewPackages creates a new Packages collection from Package instances.
|
||||||
|
// Maintains a stable order by sorting packages by name and version.
|
||||||
|
// Automatically deduplicates packages with identical name and version.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - pkgs: Variable number of Package instances
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Packages: A new ordered collection of unique packages
|
||||||
func NewPackages(pkgs ...Package) Packages {
|
func NewPackages(pkgs ...Package) Packages {
|
||||||
// Create a new slice to avoid modifying the input
|
// Create a new slice to avoid modifying the input
|
||||||
result := make(packages, len(pkgs))
|
result := make(packages, 0, len(pkgs))
|
||||||
copy(result, pkgs)
|
|
||||||
|
// Add packages, avoiding duplicates
|
||||||
|
seenPkgs := make(map[string]bool)
|
||||||
|
for _, pkg := range pkgs {
|
||||||
|
key := pkg.Name + "=" + pkg.Version
|
||||||
|
if !seenPkgs[key] {
|
||||||
|
seenPkgs[key] = true
|
||||||
|
result = append(result, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort packages by name and version
|
// Sort packages by name and version
|
||||||
slices.SortFunc(result, func(lhs, rhs Package) int {
|
slices.SortFunc(result, func(lhs, rhs Package) int {
|
||||||
|
|
@ -103,7 +139,16 @@ func NewPackages(pkgs ...Package) Packages {
|
||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParsePackageArgs parses package arguments and returns a new Packages instance
|
// ParsePackageArgs parses package arguments into a Packages collection.
|
||||||
|
// Each argument should be a package specification in the format "name" or "name=version".
|
||||||
|
// Invalid package specifications will cause an error to be returned.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - value: Array of package specification strings to parse
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Packages: A new ordered collection of the parsed packages
|
||||||
|
// - error: Any error encountered while parsing package specifications
|
||||||
func ParsePackageArgs(value []string) (Packages, error) {
|
func ParsePackageArgs(value []string) (Packages, error) {
|
||||||
var pkgs packages
|
var pkgs packages
|
||||||
for _, val := range value {
|
for _, val := range value {
|
||||||
|
|
|
||||||
|
|
@ -4,46 +4,59 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewPackages(t *testing.T) {
|
// Test constants - meaningful names without "test" prefix
|
||||||
p := NewPackages()
|
const (
|
||||||
if p == nil {
|
package1 = "zlib=1.2.3"
|
||||||
|
package2 = "rolldice=1.16-1build3"
|
||||||
|
package3 = "apt=2.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPackagesNewPackages_referenceNil_executesFail(t *testing.T) {
|
||||||
|
if NewPackages() == nil {
|
||||||
t.Fatal("NewPackages() returned nil")
|
t.Fatal("NewPackages() returned nil")
|
||||||
}
|
}
|
||||||
if p.Len() != 0 {
|
|
||||||
t.Errorf("NewPackages() returned non-empty Packages, got length %d", p.Len())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewPackagesFromStrings(t *testing.T) {
|
func TestPackagesNewPackages_containsPackages_executesFail(t *testing.T) {
|
||||||
|
if NewPackages().Len() != 0 {
|
||||||
|
t.Errorf("NewPackages() returned non-empty Packages, actual length %d", NewPackages().Len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackagesNewPackagesFromStrings(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
pkgs []string
|
pkgs []string
|
||||||
wantLen int
|
expectedLen int
|
||||||
wantOrdered []string // expected order after sorting
|
expectedOrder []string // expected order after sorting
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Empty input",
|
name: "Empty input",
|
||||||
pkgs: []string{},
|
pkgs: []string{},
|
||||||
wantLen: 0,
|
expectedLen: 0,
|
||||||
wantOrdered: []string{},
|
expectedOrder: []string{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Single package",
|
name: "Single package",
|
||||||
pkgs: []string{"xdot=1.3-1"},
|
pkgs: []string{package1},
|
||||||
wantLen: 1,
|
expectedLen: 1,
|
||||||
wantOrdered: []string{"xdot=1.3-1"},
|
expectedOrder: []string{package1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Multiple packages unsorted",
|
name: "Multiple packages unsorted",
|
||||||
pkgs: []string{"zlib=1.2.3", "xdot=1.3-1", "apt=2.0.0"},
|
pkgs: []string{package2, package1, package3}, // rolldice, zlib, apt
|
||||||
wantLen: 3,
|
expectedLen: 3,
|
||||||
wantOrdered: []string{"apt=2.0.0", "xdot=1.3-1", "zlib=1.2.3"},
|
expectedOrder: []string{
|
||||||
|
package3,
|
||||||
|
package2,
|
||||||
|
package1,
|
||||||
|
}, // apt, rolldice, zlib (sorted by name)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Duplicate packages",
|
name: "Duplicate packages",
|
||||||
pkgs: []string{"xdot=1.3-1", "xdot=1.3-1", "apt=2.0.0"},
|
pkgs: []string{package1, package1, package3},
|
||||||
wantLen: 2,
|
expectedLen: 2,
|
||||||
wantOrdered: []string{"apt=2.0.0", "xdot=1.3-1"},
|
expectedOrder: []string{package3, package1},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,14 +64,14 @@ func TestNewPackagesFromStrings(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
p := NewPackagesFromStrings(tt.pkgs...)
|
p := NewPackagesFromStrings(tt.pkgs...)
|
||||||
|
|
||||||
// Test Len()
|
// TestPackages Len()
|
||||||
if got := p.Len(); got != tt.wantLen {
|
if actual := p.Len(); actual != tt.expectedLen {
|
||||||
t.Errorf("Len() = %v, want %v", got, tt.wantLen)
|
t.Errorf("Len() = %v, expected %v", actual, tt.expectedLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test Get() and verify order
|
// TestPackages Get() and verify order
|
||||||
for i := 0; i < p.Len(); i++ {
|
for i := 0; i < p.Len(); i++ {
|
||||||
if i >= len(tt.wantOrdered) {
|
if i >= len(tt.expectedOrder) {
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"Too many packages in result, extra package at index %d: %s",
|
"Too many packages in result, extra package at index %d: %s",
|
||||||
i,
|
i,
|
||||||
|
|
@ -66,149 +79,40 @@ func TestNewPackagesFromStrings(t *testing.T) {
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if got := p.Get(i); got != tt.wantOrdered[i] {
|
if actual := p.Get(i); actual.String() != tt.expectedOrder[i] {
|
||||||
t.Errorf("Get(%d) = %v, want %v", i, got, tt.wantOrdered[i])
|
t.Errorf("Get(%d) = %v, expected %v", i, actual, tt.expectedOrder[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test String()
|
// TestPackages String()
|
||||||
wantString := ""
|
expectedString := ""
|
||||||
if len(tt.wantOrdered) > 0 {
|
if len(tt.expectedOrder) > 0 {
|
||||||
for i, pkg := range tt.wantOrdered {
|
for i, pkg := range tt.expectedOrder {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
wantString += ","
|
expectedString += " " // Use space separator to match implementation
|
||||||
}
|
}
|
||||||
wantString += pkg
|
expectedString += pkg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got := p.String(); got != wantString {
|
if actual := p.String(); actual != expectedString {
|
||||||
t.Errorf("String() = %v, want %v", got, wantString)
|
t.Errorf("String() = %v, expected %v", actual, expectedString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test StringArray()
|
// TestPackages StringArray()
|
||||||
gotArray := p.StringArray()
|
actualArray := p.StringArray()
|
||||||
if len(gotArray) != len(tt.wantOrdered) {
|
if len(actualArray) != len(tt.expectedOrder) {
|
||||||
t.Errorf("StringArray() length = %v, want %v", len(gotArray), len(tt.wantOrdered))
|
t.Errorf(
|
||||||
|
"StringArray() length = %v, expected %v",
|
||||||
|
len(actualArray),
|
||||||
|
len(tt.expectedOrder),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
for i, want := range tt.wantOrdered {
|
for i, expected := range tt.expectedOrder {
|
||||||
if gotArray[i] != want {
|
if actualArray[i] != expected {
|
||||||
t.Errorf("StringArray()[%d] = %v, want %v", i, gotArray[i], want)
|
t.Errorf("StringArray()[%d] = %v, expected %v", i, actualArray[i], expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackages_Add(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
initial []string
|
|
||||||
toAdd []string
|
|
||||||
wantOrdered []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Add to empty",
|
|
||||||
initial: []string{},
|
|
||||||
toAdd: []string{"xdot=1.3-1"},
|
|
||||||
wantOrdered: []string{"xdot=1.3-1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Add multiple maintaining order",
|
|
||||||
initial: []string{"apt=2.0.0"},
|
|
||||||
toAdd: []string{"zlib=1.2.3", "xdot=1.3-1"},
|
|
||||||
wantOrdered: []string{"apt=2.0.0", "xdot=1.3-1", "zlib=1.2.3"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Add duplicate",
|
|
||||||
initial: []string{"xdot=1.3-1"},
|
|
||||||
toAdd: []string{"xdot=1.3-1"},
|
|
||||||
wantOrdered: []string{"xdot=1.3-1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Add same package different version",
|
|
||||||
initial: []string{"xdot=1.3-1"},
|
|
||||||
toAdd: []string{"xdot=1.3-2"},
|
|
||||||
wantOrdered: []string{"xdot=1.3-1", "xdot=1.3-2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
p := NewPackagesFromStrings(tt.initial...)
|
|
||||||
|
|
||||||
// Add packages one by one to test Add method
|
|
||||||
for _, pkg := range tt.toAdd {
|
|
||||||
p.Add(pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify length
|
|
||||||
if got := p.Len(); got != len(tt.wantOrdered) {
|
|
||||||
t.Errorf("After Add(), Len() = %v, want %v", got, len(tt.wantOrdered))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify order using Get
|
|
||||||
for i := 0; i < p.Len(); i++ {
|
|
||||||
if got := p.Get(i); got != tt.wantOrdered[i] {
|
|
||||||
t.Errorf("After Add(), Get(%d) = %v, want %v", i, got, tt.wantOrdered[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Contains for all added packages
|
|
||||||
for _, pkg := range tt.toAdd {
|
|
||||||
if !p.Contains(pkg) {
|
|
||||||
t.Errorf("After Add(), Contains(%v) = false, want true", pkg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPackages_Contains(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
packages []string
|
|
||||||
check string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty packages",
|
|
||||||
packages: []string{},
|
|
||||||
check: "xdot=1.3-1",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Package exists",
|
|
||||||
packages: []string{"apt=2.0.0", "xdot=1.3-1"},
|
|
||||||
check: "xdot=1.3-1",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Package exists (different order)",
|
|
||||||
packages: []string{"xdot=1.3-1", "apt=2.0.0"},
|
|
||||||
check: "apt=2.0.0",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Package doesn't exist",
|
|
||||||
packages: []string{"xdot=1.3-1", "apt=2.0.0"},
|
|
||||||
check: "nonexistent=1.0",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Similar package different version",
|
|
||||||
packages: []string{"xdot=1.3-1"},
|
|
||||||
check: "xdot=1.3-2",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
p := NewPackagesFromStrings(tt.packages...)
|
|
||||||
if got := p.Contains(tt.check); got != tt.want {
|
|
||||||
t.Errorf("Contains(%v) = %v, want %v", tt.check, got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
43
internal/testing/capture_std.go
Normal file
43
internal/testing/capture_std.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Package testing provides utilities for testing, including capturing standard output and error.
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CaptureStd captures stdout and stderr output during the execution of a function.
|
||||||
|
// It temporarily redirects the standard streams, executes the provided function,
|
||||||
|
// and returns the captured output as strings. The original streams are restored
|
||||||
|
// after execution, even if the function panics.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// stdout, stderr := CaptureStd(func() {
|
||||||
|
// fmt.Println("captured")
|
||||||
|
// fmt.Fprintf(os.Stderr, "error")
|
||||||
|
// })
|
||||||
|
func CaptureStd(fn func()) (stdout, stderr string) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
rOut, wOut, _ := os.Pipe()
|
||||||
|
rErr, wErr, _ := os.Pipe()
|
||||||
|
os.Stdout = wOut
|
||||||
|
os.Stderr = wErr
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
bufOut, _ := io.ReadAll(rOut)
|
||||||
|
bufErr, _ := io.ReadAll(rErr)
|
||||||
|
stdout, stderr = string(bufOut), string(bufErr)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
fn()
|
||||||
|
wOut.Close()
|
||||||
|
wErr.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
os.Stderr = oldStderr
|
||||||
|
<-done
|
||||||
|
return
|
||||||
|
}
|
||||||
44
internal/testing/require.go
Normal file
44
internal/testing/require.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Package testing provides precondition checks to ensure invariants in code are met.
|
||||||
|
package testing
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type FieldValue struct {
|
||||||
|
name string
|
||||||
|
value any
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmpty(v any) bool {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case *any, chan any, func():
|
||||||
|
return v == nil
|
||||||
|
case []any:
|
||||||
|
return len(v) == 0
|
||||||
|
case map[any]any:
|
||||||
|
return len(v) == 0
|
||||||
|
case byte, int, int8, int16, int32, int64, uintptr:
|
||||||
|
return v == 0
|
||||||
|
case complex128, complex64:
|
||||||
|
return v == 0+0i
|
||||||
|
case error:
|
||||||
|
return v == nil
|
||||||
|
case float32, float64:
|
||||||
|
return v == 0.0
|
||||||
|
case string:
|
||||||
|
return v == ""
|
||||||
|
case struct{}:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported type: %T", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireNonEmpty returns an error if any of the provided FieldValue instances are empty.
|
||||||
|
func RequireNonEmpty(args ...FieldValue) error {
|
||||||
|
for _, arg := range args {
|
||||||
|
if isEmpty(arg.value) {
|
||||||
|
return fmt.Errorf("argument %v is empty", arg.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
#==============================================================================
|
|
||||||
# check_utf8.sh
|
|
||||||
#==============================================================================
|
|
||||||
#
|
|
||||||
# DESCRIPTION:
|
|
||||||
# Script to check and validate UTF-8 encoding in text files.
|
|
||||||
# Identifies files that are not properly UTF-8 encoded and reports them.
|
|
||||||
# Skips binary files and common non-text file types.
|
|
||||||
#
|
|
||||||
# USAGE:
|
|
||||||
# ./scripts/check_utf8.sh [<file>...] [directory]
|
|
||||||
#
|
|
||||||
# OPTIONS:
|
|
||||||
# <file> One or more files to check
|
|
||||||
# <directory> A directory to scan for files
|
|
||||||
#
|
|
||||||
# DEPENDENCIES:
|
|
||||||
# - bash
|
|
||||||
# - file (for file type detection)
|
|
||||||
# - iconv (for encoding detection)
|
|
||||||
#==============================================================================
|
|
||||||
|
|
||||||
# Required tools
|
|
||||||
command -v file >/dev/null 2>&1 || {
|
|
||||||
echo "file command not found. Please install it."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
command -v iconv >/dev/null 2>&1 || {
|
|
||||||
echo "iconv command not found. Please install it."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find all potential text files, excluding certain directories and files
|
|
||||||
find . -type f \
|
|
||||||
! -path "./.git/*" \
|
|
||||||
! -name "*.png" \
|
|
||||||
! -name "*.jpg" \
|
|
||||||
! -name "*.jpeg" \
|
|
||||||
! -name "*.gif" \
|
|
||||||
! -name "*.ico" \
|
|
||||||
! -name "*.bin" \
|
|
||||||
! -name "*.exe" \
|
|
||||||
! -name "*.dll" \
|
|
||||||
! -name "*.so" \
|
|
||||||
! -name "*.dylib" \
|
|
||||||
-exec file -i {} \; |
|
|
||||||
while read -r line; do
|
|
||||||
file_path=$(echo "$line" | cut -d: -f1)
|
|
||||||
mime_type=$(echo "$line" | cut -d: -f2)
|
|
||||||
|
|
||||||
# Skip non-text files
|
|
||||||
if [[ ! $mime_type =~ "text/" ]] && \
|
|
||||||
[[ ! $mime_type =~ "application/json" ]] && \
|
|
||||||
[[ ! $mime_type =~ "application/x-yaml" ]] && \
|
|
||||||
[[ $line == *"binary"* ]]; then
|
|
||||||
echo "⏭️ Skipping non-text file: $file_path ($mime_type)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
encoding=$(echo "$mime_type" | grep -oP "charset=\K[^ ]*" || echo "unknown")
|
|
||||||
|
|
||||||
# Skip if already UTF-8 or ASCII
|
|
||||||
if [[ $encoding == "utf-8" ]] || [[ $encoding == "us-ascii" ]]; then
|
|
||||||
echo "✓ $file_path is already UTF-8"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "⚠️ Converting $file_path from $encoding to UTF-8"
|
|
||||||
|
|
||||||
# Create a temporary file for conversion
|
|
||||||
temp_file="${file_path}.tmp"
|
|
||||||
|
|
||||||
# Try to convert the file to UTF-8
|
|
||||||
if iconv -f "${encoding:-ISO-8859-1}" -t UTF-8 "$file_path" >"$temp_file" 2>/dev/null; then
|
|
||||||
mv "$temp_file" "$file_path"
|
|
||||||
echo "✓ Successfully converted $file_path to UTF-8"
|
|
||||||
else
|
|
||||||
rm -f "$temp_file"
|
|
||||||
echo "⚠️ File $file_path appears to be binary or already UTF-8"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
# Handles building, pushing, and retrieving binary paths for GitHub Actions.
|
# Handles building, pushing, and retrieving binary paths for GitHub Actions.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/distribute.sh <command> [architecture]
|
# ./scripts/distribute.sh [OPTIONS] <command> [architecture]
|
||||||
#
|
#
|
||||||
# COMMANDS:
|
# COMMANDS:
|
||||||
# push - Build and push all architecture binaries to dist directory
|
# push - Build and push all architecture binaries to dist directory
|
||||||
|
|
@ -18,102 +18,92 @@
|
||||||
# ARCHITECTURES:
|
# ARCHITECTURES:
|
||||||
# X86, X64, ARM, ARM64 - GitHub runner architectures
|
# X86, X64, ARM, ARM64 - GitHub runner architectures
|
||||||
#
|
#
|
||||||
# DEPENDENCIES:
|
# OPTIONS:
|
||||||
# - bash
|
# -v, --verbose Enable verbose output
|
||||||
# - go (for building)
|
# -h, --help Show this help message
|
||||||
# - git
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
set -e
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
parse_common_args "$@" >/dev/null # prevent return from echo'ng
|
||||||
|
|
||||||
CMD="$1"
|
CMD="$1"
|
||||||
RUNNER_ARCH="$2"
|
RUNNER_ARCH="$2"
|
||||||
BUILD_DIR="../dist"
|
BUILD_DIR="${PROJECT_ROOT}/dist"
|
||||||
|
|
||||||
# GitHub runner.arch values to GOARCH values
|
# GitHub runner.arch values to GOARCH values
|
||||||
# https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md
|
# https://github.com/github/docs/blob/main/data/reusables/actions/runner-arch-description.md
|
||||||
# https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go
|
# https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go
|
||||||
declare -A rarch_to_goarch=(
|
declare -A rarch_to_goarch=(
|
||||||
["X86"]="386"
|
["X86"]="386"
|
||||||
["X64"]="amd64"
|
["X64"]="amd64"
|
||||||
["ARM"]="arm"
|
["ARM"]="arm"
|
||||||
["ARM64"]="arm64"
|
["ARM64"]="arm64"
|
||||||
)
|
)
|
||||||
|
|
||||||
function usage() {
|
|
||||||
echo "error: $1" >&2
|
|
||||||
echo -e "
|
|
||||||
Usage: $0 <command>
|
|
||||||
Commands:
|
|
||||||
push - Build and push all architecture binaries to dist directory.
|
|
||||||
getbinpath [X86, X64, ARM, ARM64] - Get the binary path from dist directory." >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function push() {
|
function push() {
|
||||||
rm -fr "$BUILD_DIR"
|
rm -fr "${BUILD_DIR}"
|
||||||
mkdir -p "$BUILD_DIR"
|
mkdir -p "${BUILD_DIR}"
|
||||||
|
|
||||||
# Package name
|
# Package name
|
||||||
PACKAGE_NAME="cache-apt-pkgs"
|
PACKAGE_NAME="cache-apt-pkgs"
|
||||||
|
|
||||||
# Print the build plan
|
# Print the build plan
|
||||||
echo "Building for these architectures:"
|
echo "Building for these architectures:"
|
||||||
for arch in "${!rarch_to_goarch[@]}"; do
|
for arch in "${!rarch_to_goarch[@]}"; do
|
||||||
echo " - Linux/$arch (GOARCH=${rarch_to_goarch[$arch]})"
|
echo " - Linux/${arch} (GOARCH=${rarch_to_goarch[${arch}]})"
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Build for each architecture
|
# Build for each architecture
|
||||||
local binary_name
|
local binary_name
|
||||||
for runner_arch in "${!rarch_to_goarch[@]}"; do
|
for runner_arch in "${!rarch_to_goarch[@]}"; do
|
||||||
go_arch="${rarch_to_goarch[$runner_arch]}"
|
go_arch="${rarch_to_goarch[${runner_arch}]}"
|
||||||
binary_name="$BUILD_DIR/$PACKAGE_NAME-linux-$go_arch"
|
binary_name="${BUILD_DIR}/${PACKAGE_NAME}-linux-${go_arch}"
|
||||||
|
|
||||||
echo "Building $binary_name for Linux/$runner_arch (GOARCH=$go_arch)..."
|
echo "Building ${binary_name} for Linux/${runner_arch} (GOARCH=${go_arch})..."
|
||||||
|
|
||||||
# Build the binary
|
# Build the binary
|
||||||
GOOS=linux GOARCH=$go_arch go build -v \
|
GOOS=linux GOARCH=${go_arch} go build -v \
|
||||||
-o "$binary_name" \
|
-o "${binary_name}" \
|
||||||
../src/cmd/cache_apt_pkgs
|
"${PROJECT_ROOT}/cmd/cache_apt_pkgs"
|
||||||
|
|
||||||
echo "✓ Built $PACKAGE_NAME-linux-$go_arch"
|
echo "✓ Build ${PACKAGE_NAME}-linux-${go_arch}"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "All builds completed!"
|
echo "All builds completed!"
|
||||||
}
|
}
|
||||||
|
|
||||||
function getbinpath() {
|
function getbinpath() {
|
||||||
local runner_arch=$1
|
local runner_arch=$1
|
||||||
|
|
||||||
if [[ -z $runner_arch ]]; then
|
if [[ -z ${runner_arch} ]]; then
|
||||||
usage "runner architecture not provided"
|
fail "runner architecture not provided"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local go_arch="${rarch_to_goarch[$runner_arch]}"
|
local go_arch="${rarch_to_goarch[${runner_arch}]}"
|
||||||
if [[ -z $go_arch ]]; then
|
if [[ -z ${go_arch} ]]; then
|
||||||
usage "invalid runner architecture: $runner_arch"
|
fail "invalid runner architecture: ${runner_arch}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local binary_name="$BUILD_DIR/cache-apt-pkgs-linux-$go_arch"
|
local binary_name="${BUILD_DIR}/cache-apt-pkgs-linux-${go_arch}"
|
||||||
if [[ ! -f $binary_name ]]; then
|
if [[ ! -f ${binary_name} ]]; then
|
||||||
usage "binary not found: $binary_name (did you run 'push' first?)"
|
fail "binary not found: ${binary_name} (did you run 'push' first?)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$binary_name"
|
echo "${binary_name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
case $CMD in
|
case ${CMD} in
|
||||||
push)
|
push)
|
||||||
push
|
push
|
||||||
;;
|
;;
|
||||||
getbinpath)
|
getbinpath)
|
||||||
getbinpath "$RUNNER_ARCH"
|
getbinpath "${RUNNER_ARCH}"
|
||||||
;;
|
;;
|
||||||
"")
|
"")
|
||||||
usage "command not provided"
|
fail "command not provided"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
usage "invalid command: $CMD"
|
fail "invalid command: ${CMD}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -10,80 +10,74 @@
|
||||||
# toolchain version, and syspkg version.
|
# toolchain version, and syspkg version.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/export_version.sh
|
# export_version.sh [OPTIONS]
|
||||||
#
|
#
|
||||||
# OUTPUTS:
|
# OPTIONS:
|
||||||
# - Sets environment variables: GO_VERSION, TOOLCHAIN_VERSION, SYSPKG_VERSION
|
# -v, --verbose Enable verbose output
|
||||||
# - Creates .version-info file with version details
|
# -h, --help Show this help message
|
||||||
#
|
|
||||||
# DEPENDENCIES:
|
|
||||||
# - bash
|
|
||||||
# - go (for reading go.mod)
|
|
||||||
# - grep, awk (for parsing)
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
set -e
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
parse_common_args "$@" >/dev/null # prevent return from echo'ng
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
|
|
||||||
# Function to extract Go version from go.mod
|
# Function to extract Go version from go.mod
|
||||||
get_go_version() {
|
get_go_version() {
|
||||||
local go_version
|
local go_version
|
||||||
go_version=$(grep "^go " "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
go_version=$(grep "^go " "${PROJECT_ROOT}/go.mod" | awk '{print $2}')
|
||||||
echo "$go_version"
|
log_debug "Extracted Go version: ${go_version}"
|
||||||
|
echo "${go_version}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to extract toolchain version from go.mod
|
# Function to extract toolchain version from go.mod
|
||||||
get_toolchain_version() {
|
get_toolchain_version() {
|
||||||
local toolchain_version
|
local toolchain_version
|
||||||
toolchain_version=$(grep "^toolchain " "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
toolchain_version=$(grep "^toolchain " "${PROJECT_ROOT}/go.mod" | awk '{print $2}')
|
||||||
echo "$toolchain_version"
|
log_debug "Extracted toolchain version: ${toolchain_version}"
|
||||||
|
echo "${toolchain_version}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to extract syspkg version from go.mod
|
# Function to extract syspkg version from go.mod
|
||||||
get_syspkg_version() {
|
get_syspkg_version() {
|
||||||
local syspkg_version
|
local syspkg_version
|
||||||
syspkg_version=$(grep "github.com/awalsh128/syspkg" "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
syspkg_version=$(grep "github.com/awalsh128/syspkg" "${PROJECT_ROOT}/go.mod" | awk '{print $2}')
|
||||||
echo "$syspkg_version"
|
log_debug "Extracted syspkg version: ${syspkg_version}"
|
||||||
|
echo "${syspkg_version}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "Exporting version information..."
|
|
||||||
GO_VERSION=$(get_go_version)
|
|
||||||
TOOLCHAIN_VERSION=$(get_toolchain_version)
|
|
||||||
SYSPKG_VERSION=$(get_syspkg_version)
|
|
||||||
|
|
||||||
# Export versions as environment variables
|
# Export versions as environment variables
|
||||||
|
log_info "Exporting version information..."
|
||||||
|
GO_VERSION=$(get_go_version)
|
||||||
export GO_VERSION
|
export GO_VERSION
|
||||||
|
TOOLCHAIN_VERSION=$(get_toolchain_version)
|
||||||
export TOOLCHAIN_VERSION
|
export TOOLCHAIN_VERSION
|
||||||
|
SYSPKG_VERSION=$(get_syspkg_version)
|
||||||
export SYSPKG_VERSION
|
export SYSPKG_VERSION
|
||||||
|
|
||||||
# Create a version info file
|
# Create a version info file
|
||||||
VERSION_FILE="$PROJECT_ROOT/.version-info"
|
VERSION_FILE="${PROJECT_ROOT}/.version-info"
|
||||||
cat > "$VERSION_FILE" << EOF
|
log_debug "Creating version file: ${VERSION_FILE}"
|
||||||
|
cat >"${VERSION_FILE}" <<EOF
|
||||||
# Version information for cache-apt-pkgs-action
|
# Version information for cache-apt-pkgs-action
|
||||||
GO_VERSION=$GO_VERSION
|
GO_VERSION=${GO_VERSION}
|
||||||
TOOLCHAIN_VERSION=$TOOLCHAIN_VERSION
|
TOOLCHAIN_VERSION=${TOOLCHAIN_VERSION}
|
||||||
SYSPKG_VERSION=$SYSPKG_VERSION
|
SYSPKG_VERSION=${SYSPKG_VERSION}
|
||||||
EXPORT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
EXPORT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Version information has been exported to $VERSION_FILE"
|
echo "Version information has been exported to ${VERSION_FILE}"
|
||||||
echo "Go Version: $GO_VERSION"
|
echo "Go Version: ${GO_VERSION}"
|
||||||
echo "Toolchain Version: $TOOLCHAIN_VERSION"
|
echo "Toolchain Version: ${TOOLCHAIN_VERSION}"
|
||||||
echo "Syspkg Version: $SYSPKG_VERSION"
|
echo "Syspkg Version: ${SYSPKG_VERSION}"
|
||||||
|
|
||||||
# Also create a JSON format for tools that prefer it
|
# Also create a JSON format for tools that prefer it
|
||||||
VERSION_JSON="$PROJECT_ROOT/.version-info.json"
|
VERSION_JSON="${PROJECT_ROOT}/.version-info.json"
|
||||||
cat > "$VERSION_JSON" << EOF
|
cat >"${VERSION_JSON}" <<EOF
|
||||||
{
|
{
|
||||||
"goVersion": "$GO_VERSION",
|
"goVersion": "${GO_VERSION}",
|
||||||
"toolchainVersion": "$TOOLCHAIN_VERSION",
|
"toolchainVersion": "${TOOLCHAIN_VERSION}",
|
||||||
"syspkgVersion": "$SYSPKG_VERSION",
|
"syspkgVersion": "${SYSPKG_VERSION}",
|
||||||
"exportDate": "$(date '+%Y-%m-%d %H:%M:%S')"
|
"exportDate": "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Version information also exported in JSON format to $VERSION_JSON"
|
echo "Version information also exported in JSON format to ${VERSION_JSON}"
|
||||||
|
|
|
||||||
27
scripts/fix_and_update.sh
Executable file
27
scripts/fix_and_update.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# fix_and_update.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Runs lint fixes and checks for UTF-8 formatting issues in the project.
|
||||||
|
# Intended to help maintain code quality and formatting consistency.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# fix_and_update.sh
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -v, --verbose Enable verbose output
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
parse_common_args "$@" >/dev/null # prevent return from echo'ng
|
||||||
|
|
||||||
|
print_status "Running trunk format and code check..."
|
||||||
|
require_command trunk "Install trunk to run lint fixes via curl https://get.trunk.io -fsSL | bash."
|
||||||
|
trunk check --all --ci
|
||||||
|
trunk fmt --all --ci
|
||||||
|
|
||||||
|
log_success "All lint fixes applied and checks complete."
|
||||||
461
scripts/lib.sh
Executable file
461
scripts/lib.sh
Executable file
|
|
@ -0,0 +1,461 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# lib.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Enhanced common shell script library for project utilities and helpers.
|
||||||
|
# Provides functions for logging, error handling, argument parsing, file operations,
|
||||||
|
# command validation, and development workflow tasks.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# source "$(cd "$(dirname "$0")" && pwd)/lib.sh"
|
||||||
|
#
|
||||||
|
# FEATURES:
|
||||||
|
# - Consistent logging and output formatting
|
||||||
|
# - Command existence and dependency checking
|
||||||
|
# - File and directory operations
|
||||||
|
# - Project structure helpers
|
||||||
|
# - Development tool installation helpers
|
||||||
|
# - Error handling and validation
|
||||||
|
#
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Exit on error by default for sourced scripts
|
||||||
|
set -eE -o functrace
|
||||||
|
|
||||||
|
# Detect debugging flag (bash -x) and also print line numbers
|
||||||
|
[[ $- == *"x"* ]] && PS4='+$(basename ${BASH_SOURCE[0]}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
export VERBOSE=${VERBOSE:-false}
|
||||||
|
export QUIET=${QUIET:-false}
|
||||||
|
export SCRIPT_DIRNAME="scripts"
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Logging Functions
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
export GREEN='\033[0;32m'
|
||||||
|
export RED='\033[0;31m'
|
||||||
|
export YELLOW='\033[0;33m'
|
||||||
|
export BLUE='\033[0;34m'
|
||||||
|
export CYAN='\033[0;36m'
|
||||||
|
export MAGENTA='\033[0;35m'
|
||||||
|
export NC='\033[0m' # No Color
|
||||||
|
export BOLD='\033[1m'
|
||||||
|
export DIM='\033[2m'
|
||||||
|
export BLINK='\033[5m'
|
||||||
|
|
||||||
|
echo_color() {
|
||||||
|
local echo_flags=()
|
||||||
|
# Collect echo flags (start with -)
|
||||||
|
while [[ $1 == -* ]]; do
|
||||||
|
if [[ $1 == "-e" || $1 == "-n" ]]; then
|
||||||
|
echo_flags+=("$1")
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
local color="$1"
|
||||||
|
local color_var
|
||||||
|
color_var=$(echo "${color}" | tr '[:lower:]' '[:upper:]')
|
||||||
|
shift
|
||||||
|
echo -e "${echo_flags[@]}" "${!color_var}$*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Logging Functions
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log_debug() {
|
||||||
|
if ${VERBOSE}; then
|
||||||
|
echo -e "${DIM}[DEBUG]${NC} $1" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print formatted headers
|
||||||
|
print_header() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -en "\n${BOLD}${BLUE}$1${NC}\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_section() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -en "\n${CYAN}${BOLD}$1${NC}\n\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_option() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -en "${YELLOW}$1)${CYAN} $2${NC}\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -en "${GREEN}==>${NC} $1\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
if ! ${QUIET}; then
|
||||||
|
echo -en "${GREEN}${BOLD}$1${NC}\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Error Handling
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
# Usage: fail [message] [exit_code]
|
||||||
|
local msg="${1-}"
|
||||||
|
local exit_code="${2:-1}"
|
||||||
|
if [[ -n ${msg} ]]; then
|
||||||
|
log_error "${msg}"
|
||||||
|
fi
|
||||||
|
exit "${exit_code}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trap handler for cleanup
|
||||||
|
cleanup_on_exit() {
|
||||||
|
local exit_code=$?
|
||||||
|
[[ -n ${TEMP_DIR} && -d ${TEMP_DIR} ]] && rm -rf "${TEMP_DIR}"
|
||||||
|
[[ ${exit_code} -eq 0 ]] && exit 0
|
||||||
|
local i
|
||||||
|
for ((i = ${#FUNCNAME[@]} - 1; i; i--)); do
|
||||||
|
echo "${BASH_SOURCE[i]}:${BASH_LINENO[i]}: ${FUNCNAME[i]}"
|
||||||
|
done
|
||||||
|
exit "${exit_code}"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_cleanup() {
|
||||||
|
trap 'cleanup_on_exit' EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Command and Dependency Checking
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
local cmd="$1"
|
||||||
|
local install_msg="${2:-Please install ${cmd}}"
|
||||||
|
|
||||||
|
if ! command_exists "${cmd}"; then
|
||||||
|
fail "${cmd} is required. ${install_msg}"
|
||||||
|
fi
|
||||||
|
log_debug "Found required command: ${cmd}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_script() {
|
||||||
|
local script="$1"
|
||||||
|
if [[ ! -x ${script} ]]; then
|
||||||
|
fail "${script} is required and must be executable. This script has a bug."
|
||||||
|
fi
|
||||||
|
log_debug "Found required script: ${script}"
|
||||||
|
}
|
||||||
|
|
||||||
|
npm_package_installed() {
|
||||||
|
npm list -g "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
go_tool_installed() {
|
||||||
|
go list -m "$1" >/dev/null 2>&1 || command_exists "$(basename "$1")"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# File and Directory Operations
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
file_exists() {
|
||||||
|
[[ -f $1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
dir_exists() {
|
||||||
|
[[ -d $1 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_dir() {
|
||||||
|
[[ ! -d $1 ]] && mkdir -p "$1"
|
||||||
|
log_debug "Ensured directory exists: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_temp_dir() {
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
log_debug "Created temporary directory: ${TEMP_DIR}"
|
||||||
|
echo "${TEMP_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_remove() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ -e ${path} ]]; then
|
||||||
|
rm -rf "${path}"
|
||||||
|
log_debug "Removed: ${path}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Project Structure Helpers
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
get_project_root() {
|
||||||
|
local root
|
||||||
|
if command_exists git; then
|
||||||
|
root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
if [[ -n ${root} ]]; then
|
||||||
|
echo "${root}"
|
||||||
|
else
|
||||||
|
# Fallback to current working directory
|
||||||
|
pwd
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
PROJECT_ROOT="$(get_project_root)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Development Tool Helpers
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
install_trunk() {
|
||||||
|
if command_exists trunk; then
|
||||||
|
log_debug "trunk already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing trunk..."
|
||||||
|
curl -fsSL https://get.trunk.io | bash
|
||||||
|
log_success "trunk installed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_doctoc() {
|
||||||
|
require_command npm "Please install Node.js and npm first"
|
||||||
|
|
||||||
|
if npm_package_installed doctoc; then
|
||||||
|
log_debug "doctoc already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing doctoc..."
|
||||||
|
npm install -g doctoc
|
||||||
|
log_success "doctoc installed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_go_tools() {
|
||||||
|
local tools=(
|
||||||
|
"golang.org/x/tools/cmd/goimports@latest"
|
||||||
|
"github.com/segmentio/golines@latest"
|
||||||
|
"github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
log_info "Installing Go development tools..."
|
||||||
|
for tool in "${tools[@]}"; do
|
||||||
|
log_info "Installing $(basename "${tool}")..."
|
||||||
|
go install "${tool}"
|
||||||
|
done
|
||||||
|
log_success "Go tools installed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Validation Helpers
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
validate_go_project() {
|
||||||
|
require_command go "Please install Go first"
|
||||||
|
|
||||||
|
local project_root
|
||||||
|
project_root=$(get_project_root)
|
||||||
|
|
||||||
|
if [[ ! -f "${project_root}/go.mod" ]]; then
|
||||||
|
fail "Not a Go project (no go.mod found)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_debug "Validated Go project structure"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_git_repo() {
|
||||||
|
require_command git "Please install git first"
|
||||||
|
|
||||||
|
local project_root
|
||||||
|
project_root=$(get_project_root)
|
||||||
|
|
||||||
|
if [[ ! -d "${project_root}/.git" ]]; then
|
||||||
|
fail "Not a git repository"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_debug "Validated git repository"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Common Argument Parsing
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
parse_common_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h | --help)
|
||||||
|
[[ $(type -t show_help) == function ]] && show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-v | --verbose)
|
||||||
|
if [[ ${VERBOSE} == false ]]; then
|
||||||
|
export VERBOSE=true
|
||||||
|
log_debug "Verbose mode enabled"
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-q | --quiet)
|
||||||
|
export QUIET=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Return unhandled arguments
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Return remaining arguments
|
||||||
|
# Echo any remaining unhandled arguments for callers to capture
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
echo "$@"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Common Operations
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
run_with_status() {
|
||||||
|
local description="$1"
|
||||||
|
shift
|
||||||
|
local cmd="$*"
|
||||||
|
|
||||||
|
print_status "${description}"
|
||||||
|
log_debug "Running: ${cmd}"
|
||||||
|
|
||||||
|
if eval "${cmd}"; then
|
||||||
|
log_success "${description} completed"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
local exit_code=$?
|
||||||
|
log_error "${description} failed (exit code: ${exit_code})"
|
||||||
|
return "${exit_code}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_go_modules() {
|
||||||
|
run_with_status "Updating Go modules" "go mod tidy && go mod verify"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_tests() {
|
||||||
|
run_with_status "Running tests" "go test -v ./..."
|
||||||
|
}
|
||||||
|
|
||||||
|
run_build() {
|
||||||
|
run_with_status "Building project" "go build -v ./..."
|
||||||
|
}
|
||||||
|
|
||||||
|
run_lint() {
|
||||||
|
require_command trunk "Please install trunk first"
|
||||||
|
run_with_status "Running linting" "trunk check"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Default Help Function
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
# Extract header comment block and format it
|
||||||
|
local script_file="${BASH_SOURCE[1]}"
|
||||||
|
|
||||||
|
if [[ ! -f ${script_file} ]]; then
|
||||||
|
echo "Help information not available"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local lines=$'\n'
|
||||||
|
local inside_header=false
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ ${inside_header} == true ]]; then
|
||||||
|
[[ ${line} =~ ^#\=+ ]] && continue
|
||||||
|
if [[ ${line} =~ ^# ]]; then
|
||||||
|
lines+="${line#\#}"$'\n'
|
||||||
|
else
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[[ ${line} =~ ^#\=+ ]] && inside_header=true
|
||||||
|
done <"${script_file}"
|
||||||
|
printf "%s" "${lines}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Utility Functions
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
[[ ${QUIET} == true ]] && return
|
||||||
|
echo
|
||||||
|
read -n 1 -s -r -p "Press any key to continue..."
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
local prompt="${1:-Are you sure?}"
|
||||||
|
local response
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -rp "${prompt} (y/n): " response
|
||||||
|
case ${response} in
|
||||||
|
[Yy] | [Yy][Ee][Ss]) return 0 ;;
|
||||||
|
[Nn] | [Nn][Oo]) return 1 ;;
|
||||||
|
*) echo "Please answer yes or no." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Initialization
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Set up cleanup trap when library is sourced
|
||||||
|
setup_cleanup
|
||||||
|
|
||||||
|
init() {
|
||||||
|
parse_common_args "$@"
|
||||||
|
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
|
||||||
|
echo "This script should be sourced, not executed directly."
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
echo 'Usage: source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/lib.sh'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# Do not auto-run init when this file is sourced; allow callers to invoke init() explicitly if needed.
|
||||||
264
scripts/menu.sh
264
scripts/menu.sh
|
|
@ -5,151 +5,157 @@
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
#
|
#
|
||||||
# DESCRIPTION:
|
# DESCRIPTION:
|
||||||
# Interactive menu for running project scripts and common tasks.
|
# Streamlined interactive menu for essential development tasks.
|
||||||
# Provides easy access to development, testing, and maintenance tasks.
|
# Provides quick access to the most commonly used development operations.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/menu.sh
|
# menu.sh
|
||||||
#
|
#
|
||||||
# FEATURES:
|
# OPTIONS:
|
||||||
# - Interactive menu interface
|
# -v, --verbose Enable verbose output
|
||||||
# - Clear task descriptions
|
# -h, --help Show this help message
|
||||||
# - Status feedback
|
|
||||||
# - Error handling
|
|
||||||
#
|
|
||||||
# DEPENDENCIES:
|
|
||||||
# - bash
|
|
||||||
# - Various project scripts
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
# Colors for output
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
GREEN='\033[0;32m'
|
SCRIPT_DIR="${PROJECT_ROOT}/scripts"
|
||||||
BLUE='\033[0;34m'
|
CAP_CMD_DIR="${PROJECT_ROOT}/cmd/cache_apt_pkgs"
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
BOLD='\033[1m'
|
|
||||||
|
|
||||||
# Function to print section headers
|
parse_common_args "$@" >/dev/null # prevent return from echo'ng
|
||||||
print_header() {
|
|
||||||
echo -e "\n${BOLD}${BLUE}$1${NC}\n"
|
#==============================================================================
|
||||||
|
# Menu Operations
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
run_task() {
|
||||||
|
local description="$1"
|
||||||
|
shift
|
||||||
|
local cmd="$*"
|
||||||
|
|
||||||
|
print_status "Running: ${description}"
|
||||||
|
[[ ${VERBOSE} == true ]] && log_debug "Command: ${cmd}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
if eval "${cmd}"; then
|
||||||
|
log_success "${description} completed successfully"
|
||||||
|
else
|
||||||
|
local exit_code=$?
|
||||||
|
log_error "${description} failed (exit code: ${exit_code})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to print status messages
|
show_project_status() {
|
||||||
print_status() {
|
print_header "Project Status"
|
||||||
echo -e "${GREEN}==>${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to print errors
|
echo "Git Status:"
|
||||||
print_error() {
|
git status --short --branch
|
||||||
echo -e "${RED}Error:${NC} $1"
|
echo
|
||||||
}
|
|
||||||
|
|
||||||
# Function to wait for user input before continuing
|
echo "Go Module Status:"
|
||||||
pause() {
|
go mod verify && log_success "Go modules are valid"
|
||||||
echo
|
echo
|
||||||
read -n 1 -s -r -p "Press any key to continue..."
|
|
||||||
|
if command_exists trunk; then
|
||||||
|
echo "Linting Status:"
|
||||||
|
trunk check --no-fix --quiet || log_warn "Linting issues detected"
|
||||||
echo
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
pause
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to run a command and handle errors
|
#==============================================================================
|
||||||
run_command() {
|
# Main Menu Loop
|
||||||
local cmd="$1"
|
#==============================================================================
|
||||||
local description="$2"
|
|
||||||
|
|
||||||
print_status "Running: $description"
|
main_menu() {
|
||||||
echo "Command: $cmd"
|
while true; do
|
||||||
echo
|
|
||||||
|
|
||||||
if eval "$cmd"; then
|
|
||||||
print_status "Successfully completed: $description"
|
|
||||||
else
|
|
||||||
print_error "Failed: $description"
|
|
||||||
echo "Exit code: $?"
|
|
||||||
fi
|
|
||||||
|
|
||||||
pause
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main menu
|
|
||||||
while true; do
|
|
||||||
clear
|
clear
|
||||||
print_header "Cache Apt Packages Action - Development Menu"
|
print_header "Cache Apt Packages - Development Menu"
|
||||||
echo "1) Setup Development Environment"
|
|
||||||
echo "2) Update Markdown TOCs"
|
print_section "Essential Tasks:"
|
||||||
echo "3) Run Tests"
|
print_option 1 "Setup Development Environment"
|
||||||
echo "4) Run Linting (trunk check)"
|
print_option 2 "Run All Checks (test + lint + build)"
|
||||||
echo "5) Build Project"
|
print_option 3 "Test Only"
|
||||||
echo "6) Check UTF-8 Encoding"
|
print_option 4 "Lint & Fix"
|
||||||
echo "7) Run All Checks (tests, lint, build)"
|
print_option 5 "Build Project"
|
||||||
echo "8) Run All Script Tests"
|
|
||||||
|
print_section "Maintenance:"
|
||||||
|
print_option 6 "Update Documentation (TOCs)"
|
||||||
|
print_option 7 "Export Version Info"
|
||||||
|
|
||||||
|
print_section "Information:"
|
||||||
|
print_option 8 "Project Status"
|
||||||
|
print_option 9 "Recent Changes"
|
||||||
echo
|
echo
|
||||||
echo "9) Show Project Status"
|
print_option q "Quit"
|
||||||
echo "10) Show Recent Git Log"
|
|
||||||
echo "11) Export Version Information"
|
|
||||||
echo
|
|
||||||
echo "q) Quit"
|
|
||||||
echo
|
|
||||||
read -p "Select an option: " choice
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
case $choice in
|
echo_color -n green "choice > "
|
||||||
1)
|
read -n 1 -rp "" choice
|
||||||
run_command "./scripts/setup_dev.sh" "Setting up development environment"
|
printf "\n\n"
|
||||||
;;
|
|
||||||
2)
|
case ${choice} in
|
||||||
run_command "./scripts/update_md_tocs.sh" "Updating markdown tables of contents"
|
1)
|
||||||
;;
|
run_task "Setting up development environment" \
|
||||||
3)
|
"${SCRIPT_DIR}/setup_dev.sh"
|
||||||
run_command "go test -v ./..." "Running tests"
|
;;
|
||||||
;;
|
2)
|
||||||
4)
|
print_header "Running All Checks"
|
||||||
run_command "trunk check" "Running linting checks"
|
echo ""
|
||||||
;;
|
run_task "Running linting" "trunk check --fix"
|
||||||
5)
|
run_task "Building project" "go build -v ${CAP_CMD_DIR}"
|
||||||
run_command "go build -v ./..." "Building project"
|
run_task "Running tests" "go test -v ${CAP_CMD_DIR}"
|
||||||
;;
|
;;
|
||||||
6)
|
3)
|
||||||
run_command "./scripts/check_utf8.sh" "Checking UTF-8 encoding"
|
run_task "Running tests" "go test -v ${CAP_CMD_DIR}"
|
||||||
;;
|
;;
|
||||||
7)
|
4)
|
||||||
print_header "Running All Checks"
|
run_task "Running lint with fixes" "trunk check --fix"
|
||||||
run_command "go test -v ./..." "Running tests"
|
;;
|
||||||
run_command "trunk check" "Running linting checks"
|
5)
|
||||||
run_command "go build -v ./..." "Building project"
|
run_task "Building project" "go build -v ${CAP_CMD_DIR}"
|
||||||
run_command "./scripts/check_utf8.sh" "Checking UTF-8 encoding"
|
;;
|
||||||
;;
|
6)
|
||||||
8)
|
run_task "Updating documentation TOCs" \
|
||||||
print_header "Running All Script Tests"
|
"${SCRIPT_DIR}/update_md_tocs.sh"
|
||||||
run_command "./scripts/tests/setup_dev_test.sh" "Running setup dev tests"
|
;;
|
||||||
run_command "./scripts/tests/check_utf8_test.sh" "Running UTF-8 check tests"
|
7)
|
||||||
run_command "./scripts/tests/update_md_tocs_test.sh" "Running markdown TOC tests"
|
run_task "Exporting version information" \
|
||||||
run_command "./scripts/tests/export_version_test.sh" "Running version export tests"
|
"${SCRIPT_DIR}/export_version.sh"
|
||||||
run_command "./scripts/tests/distribute_test.sh" "Running distribute tests"
|
;;
|
||||||
;;
|
8)
|
||||||
9)
|
show_project_status
|
||||||
print_header "Project Status"
|
;;
|
||||||
echo "Git Status:"
|
9)
|
||||||
git status
|
print_header "Recent Changes"
|
||||||
echo
|
git log --oneline --graph --decorate -n 10
|
||||||
echo "Go Module Status:"
|
pause
|
||||||
go mod verify
|
;;
|
||||||
pause
|
q | Q | "")
|
||||||
;;
|
echo -e "${GREEN}Goodbye!${NC}"
|
||||||
10)
|
exit 0
|
||||||
print_header "Recent Git Log"
|
;;
|
||||||
git log --oneline -n 10
|
*)
|
||||||
pause
|
echo ""
|
||||||
;;
|
log_error "Invalid option: ${choice}"
|
||||||
11)
|
pause
|
||||||
run_command "./scripts/export_version.sh" "Exporting version information"
|
;;
|
||||||
;;
|
|
||||||
q|Q)
|
|
||||||
print_status "Goodbye!"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
print_error "Invalid option"
|
|
||||||
pause
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Entry Point
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Validate project structure
|
||||||
|
# validate_go_project
|
||||||
|
# validate_git_repo
|
||||||
|
|
||||||
|
# Parse any command line arguments
|
||||||
|
parse_common_args "$@"
|
||||||
|
|
||||||
|
# Run main menu
|
||||||
|
main_menu
|
||||||
|
|
|
||||||
1
scripts/run_local_fake_action.sh
Normal file
1
scripts/run_local_fake_action.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
@ -10,115 +10,155 @@
|
||||||
# pre-commit hooks.
|
# pre-commit hooks.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/setup_dev.sh
|
# setup_dev.sh [options]
|
||||||
#
|
#
|
||||||
# DEPENDENCIES:
|
# OPTIONS:
|
||||||
# - go
|
# -v, --verbose Enable verbose output
|
||||||
# - npm
|
# -h, --help Show this help message
|
||||||
# - git
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
set -e # Exit on error
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
|
||||||
# Colors for output
|
parse_common_args "$@"
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Function to check if a command exists
|
#==============================================================================
|
||||||
command_exists() {
|
# Setup Functions
|
||||||
command -v "$1" >/dev/null 2>&1
|
#==============================================================================
|
||||||
|
|
||||||
|
check_prerequisites() {
|
||||||
|
print_status "Checking prerequisites"
|
||||||
|
|
||||||
|
require_command go "Please install Go first (https://golang.org/dl/)"
|
||||||
|
require_command npm "Please install Node.js and npm first (https://nodejs.org/)"
|
||||||
|
require_command git "Please install git first"
|
||||||
|
require_command curl "Please install curl first"
|
||||||
|
|
||||||
|
log_success "All prerequisites are available"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to check if an npm package is installed globally
|
setup_go_environment() {
|
||||||
npm_package_installed() {
|
validate_go_project
|
||||||
npm list -g "$1" >/dev/null 2>&1
|
|
||||||
|
print_status "Configuring Go environment"
|
||||||
|
go env -w GO111MODULE=auto
|
||||||
|
|
||||||
|
update_go_modules
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to print status messages
|
install_development_tools() {
|
||||||
print_status() {
|
print_status "Installing development tools"
|
||||||
echo -e "${GREEN}==>${NC} $1"
|
|
||||||
|
install_trunk
|
||||||
|
install_doctoc
|
||||||
|
install_go_tools
|
||||||
|
|
||||||
|
log_success "All development tools installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to print error messages
|
setup_git_hooks() {
|
||||||
print_error() {
|
validate_git_repo
|
||||||
echo -e "${RED}Error:${NC} $1"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check prerequisites
|
print_status "Setting up Git hooks"
|
||||||
print_status "Checking prerequisites..."
|
|
||||||
|
|
||||||
if ! command_exists go; then
|
# Initialize trunk if not already done
|
||||||
print_error "Go is not installed. Please install Go first."
|
if [[ ! -f .trunk/trunk.yaml ]]; then
|
||||||
fi
|
log_info "Initializing trunk configuration"
|
||||||
|
|
||||||
if ! command_exists npm; then
|
|
||||||
print_error "npm is not installed. Please install Node.js and npm first."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command_exists git; then
|
|
||||||
print_error "git is not installed. Please install git first."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Configure Go environment
|
|
||||||
print_status "Configuring Go environment..."
|
|
||||||
go env -w GO111MODULE=auto
|
|
||||||
|
|
||||||
# Verify Go modules
|
|
||||||
print_status "Verifying Go modules..."
|
|
||||||
go mod tidy
|
|
||||||
go mod verify
|
|
||||||
|
|
||||||
# Install development tools
|
|
||||||
print_status "Installing development tools..."
|
|
||||||
|
|
||||||
# Trunk for linting
|
|
||||||
if ! command_exists trunk; then
|
|
||||||
print_status "Installing trunk..."
|
|
||||||
curl -fsSL https://get.trunk.io -o get-trunk.sh
|
|
||||||
bash get-trunk.sh
|
|
||||||
rm get-trunk.sh
|
|
||||||
fi
|
|
||||||
|
|
||||||
# doctoc for markdown TOC
|
|
||||||
if ! npm_package_installed doctoc; then
|
|
||||||
print_status "Installing doctoc..."
|
|
||||||
npm install -g doctoc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Go tools
|
|
||||||
print_status "Installing Go tools..."
|
|
||||||
go install golang.org/x/tools/cmd/goimports@latest
|
|
||||||
go install github.com/segmentio/golines@latest
|
|
||||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
||||||
|
|
||||||
# Set up Git hooks
|
|
||||||
print_status "Setting up Git hooks..."
|
|
||||||
if [ -d .git ]; then
|
|
||||||
# Initialize trunk
|
|
||||||
trunk init
|
trunk init
|
||||||
|
fi
|
||||||
|
|
||||||
# Enable pre-commit hooks
|
# Configure git hooks
|
||||||
git config core.hooksPath .git/hooks/
|
git config core.hooksPath .git/hooks/
|
||||||
else
|
|
||||||
print_error "Not a git repository"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update markdown TOCs
|
log_success "Git hooks configured"
|
||||||
print_status "Updating markdown TOCs..."
|
}
|
||||||
./scripts/update_md_tocs.sh
|
|
||||||
|
|
||||||
# Initial trunk check
|
update_project_documentation() {
|
||||||
print_status "Running initial trunk check..."
|
print_status "Updating project documentation"
|
||||||
trunk check
|
|
||||||
|
|
||||||
# Final verification
|
local update_script="${SCRIPT_DIR}/update_md_tocs.sh"
|
||||||
print_status "Verifying installation..."
|
if [[ -x ${update_script} ]]; then
|
||||||
go test ./...
|
"${update_script}"
|
||||||
|
else
|
||||||
|
log_warn "Markdown TOC update script not found or not executable"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
print_status "Development environment setup complete!"
|
run_initial_checks() {
|
||||||
echo "You can now:"
|
print_status "Running initial project validation"
|
||||||
echo " 1. Run tests: go test ./..."
|
|
||||||
echo " 2. Run linting: trunk check"
|
# Run trunk check
|
||||||
echo " 3. Update markdown TOCs: ./scripts/update_md_tocs.sh"
|
if command_exists trunk; then
|
||||||
|
run_with_status "Running initial linting" "trunk check --no-fix"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
run_tests
|
||||||
|
|
||||||
|
log_success "Initial validation completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
display_completion_message() {
|
||||||
|
print_header "Development Environment Setup Complete!"
|
||||||
|
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " • Run tests: go test ./..."
|
||||||
|
echo " • Run linting: trunk check"
|
||||||
|
echo " • Update documentation: ./scripts/update_md_tocs.sh"
|
||||||
|
echo " • Interactive menu: ./scripts/menu.sh"
|
||||||
|
echo
|
||||||
|
log_success "Ready for development!"
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Main Setup Process
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Parse command line arguments first
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-v | --verbose)
|
||||||
|
export VERBOSE=true
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
cat <<'EOF'
|
||||||
|
USAGE:
|
||||||
|
setup_dev.sh [OPTIONS]
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
Sets up the development environment for the cache-apt-pkgs-action project.
|
||||||
|
Installs all necessary tools, configures Go environment, and sets up
|
||||||
|
pre-commit hooks.
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-v, --verbose Enable verbose output
|
||||||
|
-h, --help Show this help message
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
echo "Use --help for usage information." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
print_header "Setting up Development Environment"
|
||||||
|
|
||||||
|
# Run setup steps
|
||||||
|
check_prerequisites
|
||||||
|
setup_go_environment
|
||||||
|
install_development_tools
|
||||||
|
setup_git_hooks
|
||||||
|
update_project_documentation
|
||||||
|
run_initial_checks
|
||||||
|
display_completion_message
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Entry Point
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
|
||||||
21
scripts/template.sh
Executable file
21
scripts/template.sh
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# <script>.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# <your description>
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# <script>.sh [OPTIONS]
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -v, --verbose Enable verbose output
|
||||||
|
# -h, --help Show this help message
|
||||||
|
# -yv, --your_var Description of your_var
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
|
||||||
|
# your code here
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
#==============================================================================
|
|
||||||
# check_utf8_test.sh
|
|
||||||
#==============================================================================
|
|
||||||
#
|
|
||||||
# DESCRIPTION:
|
|
||||||
# Test suite for check_utf8.sh script.
|
|
||||||
# Validates UTF-8 encoding detection, file handling, and error conditions.
|
|
||||||
#
|
|
||||||
# USAGE:
|
|
||||||
# ./scripts/tests/check_utf8_test.sh [-v|--verbose] [-r|--recursive]
|
|
||||||
#
|
|
||||||
# OPTIONS:
|
|
||||||
# -v, --verbose Show verbose test output
|
|
||||||
# -r, --recursive Test recursive directory scanning
|
|
||||||
# -h, --help Show this help message
|
|
||||||
#
|
|
||||||
#==============================================================================
|
|
||||||
|
|
||||||
# Source the test library
|
|
||||||
source "$(dirname "$0")/test_lib.sh"
|
|
||||||
|
|
||||||
# Additional settings
|
|
||||||
TEST_RECURSIVE=false
|
|
||||||
|
|
||||||
# Dependencies check
|
|
||||||
check_dependencies "file" "iconv" || exit 1
|
|
||||||
|
|
||||||
# Parse arguments (handle any unprocessed args from common parser)
|
|
||||||
while [[ -n "$1" ]]; do
|
|
||||||
arg="$(parse_common_args "$1")"
|
|
||||||
case "$arg" in
|
|
||||||
-r|--recursive)
|
|
||||||
TEST_RECURSIVE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
generate_help "$0"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# Initialize test environment
|
|
||||||
setup_test_env
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Create test files with different encodings
|
|
||||||
create_encoded_file() {
|
|
||||||
local file="$1"
|
|
||||||
local content="$2"
|
|
||||||
local encoding="$3"
|
|
||||||
|
|
||||||
if [[ "$encoding" == "utf8" ]]; then
|
|
||||||
create_test_file "$file" "$content"
|
|
||||||
else
|
|
||||||
echo -n "$content" | iconv -f UTF-8 -t "$encoding" > "$file"
|
|
||||||
print_info "Created $encoding encoded file: $file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
print_header "Check UTF-8 Tests"
|
|
||||||
|
|
||||||
# Section 1: Command Line Interface
|
|
||||||
print_section "Testing Command Line Interface"
|
|
||||||
test_case "help option"
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh --help"
|
|
||||||
"Usage:"
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "unknown option"
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh --unknown"
|
|
||||||
"Unknown option"
|
|
||||||
false
|
|
||||||
|
|
||||||
# Section 2: Basic File Encoding Detection
|
|
||||||
print_section "Testing Basic File Encoding Detection"
|
|
||||||
create_encoded_file "$TEMP_DIR/utf8.txt" "Hello, 世界!" "utf8"
|
|
||||||
create_encoded_file "$TEMP_DIR/latin1.txt" "Hello, World!" "ISO-8859-1"
|
|
||||||
|
|
||||||
test_case "single utf8 file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh $TEMP_DIR/utf8.txt" \
|
|
||||||
"" \
|
|
||||||
true \
|
|
||||||
"UTF-8 file should pass validation"
|
|
||||||
|
|
||||||
test_case "single latin1 file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh $TEMP_DIR/latin1.txt" \
|
|
||||||
"non-UTF-8" \
|
|
||||||
false \
|
|
||||||
"Latin-1 file should fail validation"
|
|
||||||
|
|
||||||
# Section 3: Multiple File Handling
|
|
||||||
print_section "Testing Multiple File Handling"
|
|
||||||
|
|
||||||
create_encoded_file "$TEMP_DIR/mixed1.txt" "Hello" "utf8"
|
|
||||||
create_encoded_file "$TEMP_DIR/mixed2.txt" "World" "ISO-8859-1"
|
|
||||||
|
|
||||||
test_case "multiple mixed files" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh $TEMP_DIR/mixed1.txt $TEMP_DIR/mixed2.txt" \
|
|
||||||
"non-UTF-8" \
|
|
||||||
false \
|
|
||||||
"Multiple files with mixed encodings should fail"
|
|
||||||
|
|
||||||
# Section 4: Special Cases
|
|
||||||
print_section "Testing Special Cases"
|
|
||||||
|
|
||||||
create_test_file "$TEMP_DIR/empty.txt" ""
|
|
||||||
test_case "empty file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR/empty.txt'" \
|
|
||||||
"" \
|
|
||||||
true \
|
|
||||||
"Empty file should be considered valid UTF-8"
|
|
||||||
|
|
||||||
test_case "missing file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR/nonexistent.txt'" \
|
|
||||||
"No such file" \
|
|
||||||
false \
|
|
||||||
"Missing file should fail with appropriate error"
|
|
||||||
|
|
||||||
test_case "invalid directory" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR/nonexistent'" \
|
|
||||||
"No such file" \
|
|
||||||
false \
|
|
||||||
"Invalid directory should fail with appropriate error"
|
|
||||||
|
|
||||||
# Print test summary
|
|
||||||
print_summary
|
|
||||||
|
|
||||||
# Optional recursive testing section
|
|
||||||
if [[ "$TEST_RECURSIVE" == "true" ]]; then
|
|
||||||
print_section "Testing Recursive Directory Handling"
|
|
||||||
create_test_dir "$TEMP_DIR/subdir/deep"
|
|
||||||
create_encoded_file "$TEMP_DIR/subdir/deep/utf8_deep.txt" "Deep UTF-8" "utf8"
|
|
||||||
create_encoded_file "$TEMP_DIR/subdir/deep/latin1_deep.txt" "Deep Latin-1" "ISO-8859-1"
|
|
||||||
|
|
||||||
test_case "recursive directory check" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh -r '$TEMP_DIR'" \
|
|
||||||
"non-UTF-8" \
|
|
||||||
false \
|
|
||||||
"Recursive check should find non-UTF-8 files in subdirectories"
|
|
||||||
fi \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Create file with BOM
|
|
||||||
printf '\xEF\xBB\xBF' > "$TEMP_DIR/with_bom.txt"
|
|
||||||
echo "Hello, World!" >> "$TEMP_DIR/with_bom.txt"
|
|
||||||
test_case "UTF-8 with BOM" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR/with_bom.txt'" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 5: Error Conditions
|
|
||||||
echo -e "\n${BLUE}Testing Error Conditions${NC}"
|
|
||||||
test_case "nonexistent file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh nonexistent.txt" \
|
|
||||||
"No such file" \
|
|
||||||
false
|
|
||||||
|
|
||||||
test_case "directory as file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR'" \
|
|
||||||
"Is a directory" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Create unreadable file
|
|
||||||
touch "$TEMP_DIR/unreadable.txt"
|
|
||||||
chmod 000 "$TEMP_DIR/unreadable.txt"
|
|
||||||
test_case "unreadable file" \
|
|
||||||
"$PROJECT_ROOT/scripts/check_utf8.sh '$TEMP_DIR/unreadable.txt'" \
|
|
||||||
"Permission denied" \
|
|
||||||
false
|
|
||||||
chmod 644 "$TEMP_DIR/unreadable.txt"
|
|
||||||
|
|
||||||
# Report results
|
|
||||||
echo
|
|
||||||
echo "Test Results:"
|
|
||||||
echo "Passed: $PASS"
|
|
||||||
echo "Failed: $FAIL"
|
|
||||||
exit $FAIL
|
|
||||||
|
|
@ -1,127 +1,106 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Colors for test output
|
#==============================================================================
|
||||||
GREEN='\033[0;32m'
|
# distribute_test.sh
|
||||||
RED='\033[0;31m'
|
#==============================================================================
|
||||||
NC='\033[0m' # No Color
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Test suite for distribute.sh. Validates command handling, binary creation,
|
||||||
|
# architecture-specific output, and error conditions for the distribution
|
||||||
|
# script.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# distribute_test.sh [OPTIONS]
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -v, --verbose Enable verbose test output
|
||||||
|
# --stop-on-failure Stop on first test failure
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
DIST_DIR="../dist"
|
# Source the test framework, exports SCRIPT_PATH
|
||||||
|
source "$(git rev-parse --show-toplevel)/scripts/tests/test_lib.sh"
|
||||||
|
|
||||||
# Test counter
|
DIST_DIR="$(get_project_root)/dist"
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
function test_case() {
|
# Define test functions
|
||||||
local name=$1
|
run_tests() {
|
||||||
local cmd=$2
|
# Disable exit-on-error during test execution to prevent early exit
|
||||||
local expected_output=$3
|
set +e
|
||||||
local should_succeed=${4:-true}
|
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
test_section "command validation"
|
||||||
|
|
||||||
# Run the command and capture both stdout and stderr
|
test_case "no command" \
|
||||||
local output
|
"" \
|
||||||
if [[ $should_succeed == "true" ]]; then
|
"command not provided" \
|
||||||
output=$($cmd 2>&1)
|
false
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
output=$($cmd 2>&1) || true
|
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
test_case "invalid command" \
|
||||||
echo " Expected output to contain: '$expected_output'"
|
"invalid_cmd" \
|
||||||
echo " Got: '$output'"
|
"invalid command" \
|
||||||
((FAIL++))
|
false
|
||||||
return 0 # Don't fail the whole test suite on one failure
|
|
||||||
|
test_section "getbinpath"
|
||||||
|
|
||||||
|
test_case "getbinpath no arch" \
|
||||||
|
"getbinpath" \
|
||||||
|
"runner architecture not provided" \
|
||||||
|
false
|
||||||
|
|
||||||
|
test_case "getbinpath invalid arch" \
|
||||||
|
"getbinpath INVALID" \
|
||||||
|
"invalid runner architecture" \
|
||||||
|
false
|
||||||
|
|
||||||
|
test_section "push and binary creation"
|
||||||
|
|
||||||
|
test_case "push command" \
|
||||||
|
"push" \
|
||||||
|
"All builds completed!" \
|
||||||
|
true # Ensure test doesn't cause script exit
|
||||||
|
|
||||||
|
# Test binary existence using direct shell commands instead of test_case
|
||||||
|
# because the distribute script doesn't have a 'test' command
|
||||||
|
for arch in "X86:386" "X64:amd64" "ARM:arm" "ARM64:arm64"; do
|
||||||
|
go_arch=${arch#*:}
|
||||||
|
test_file_exists "file exists for ${go_arch}" "${DIST_DIR}/cache-apt-pkgs-linux-${go_arch}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test getbinpath for each architecture
|
||||||
|
for arch in "X86:386" "X64:amd64" "ARM:arm" "ARM64:arm64"; do
|
||||||
|
runner_arch=${arch%:*}
|
||||||
|
go_arch=${arch#*:}
|
||||||
|
test_case "getbinpath for ${runner_arch}" \
|
||||||
|
"getbinpath ${runner_arch}" \
|
||||||
|
"${DIST_DIR}/cache-apt-pkgs-linux-${go_arch}" \
|
||||||
|
true
|
||||||
|
done
|
||||||
|
|
||||||
|
test_section "cleanup and rebuild"
|
||||||
|
|
||||||
|
# Direct cleanup
|
||||||
|
rm -rf "${DIST_DIR}" 2>/dev/null
|
||||||
|
|
||||||
|
test_case "getbinpath after cleanup" \
|
||||||
|
"getbinpath X64" \
|
||||||
|
"binary not found" \
|
||||||
|
false
|
||||||
|
|
||||||
|
test_case "rebuild after cleanup" \
|
||||||
|
"push" \
|
||||||
|
"All builds completed!" \
|
||||||
|
true
|
||||||
|
|
||||||
|
test_case "getbinpath after rebuild" \
|
||||||
|
"getbinpath X64" \
|
||||||
|
"${DIST_DIR}/cache-apt-pkgs-linux-amd64" \
|
||||||
|
true
|
||||||
|
|
||||||
|
# Re-enable exit-on-error
|
||||||
|
set -e
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Running distribute.sh tests..."
|
# Start the test framework and run tests
|
||||||
echo "----------------------------"
|
start_tests "$@"
|
||||||
|
run_tests
|
||||||
# Test command validation
|
|
||||||
test_case "no command" \
|
|
||||||
"./distribute.sh" \
|
|
||||||
"error: command not provided" \
|
|
||||||
false
|
|
||||||
|
|
||||||
test_case "invalid command" \
|
|
||||||
"./distribute.sh invalid_cmd" \
|
|
||||||
"error: invalid command" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Test getbinpath
|
|
||||||
test_case "getbinpath no arch" \
|
|
||||||
"./distribute.sh getbinpath" \
|
|
||||||
"error: runner architecture not provided" \
|
|
||||||
false
|
|
||||||
|
|
||||||
test_case "getbinpath invalid arch" \
|
|
||||||
"./distribute.sh getbinpath INVALID" \
|
|
||||||
"error: invalid runner architecture: INVALID" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Test push and binary creation
|
|
||||||
test_case "push command" \
|
|
||||||
"./distribute.sh push" \
|
|
||||||
"All builds completed!" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Test binary existence
|
|
||||||
for arch in "X86:386" "X64:amd64" "ARM:arm" "ARM64:arm64"; do
|
|
||||||
runner_arch=${arch%:*}
|
|
||||||
go_arch=${arch#*:}
|
|
||||||
test_case "binary exists for $runner_arch" \
|
|
||||||
"test -f ${DIST_DIR}/cache-apt-pkgs-linux-$go_arch" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Test getbinpath for each architecture
|
|
||||||
for arch in "X86:386" "X64:amd64" "ARM:arm" "ARM64:arm64"; do
|
|
||||||
runner_arch=${arch%:*}
|
|
||||||
go_arch=${arch#*:}
|
|
||||||
test_case "getbinpath for $runner_arch" \
|
|
||||||
"./distribute.sh getbinpath $runner_arch" \
|
|
||||||
"${DIST_DIR}/cache-apt-pkgs-linux-$go_arch" \
|
|
||||||
true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Test cleanup and rebuild
|
|
||||||
test_case "cleanup" \
|
|
||||||
"rm -rf ${DIST_DIR}" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "getbinpath after cleanup" \
|
|
||||||
"./distribute.sh getbinpath X64" \
|
|
||||||
"error: binary not found" \
|
|
||||||
false
|
|
||||||
|
|
||||||
test_case "rebuild after cleanup" \
|
|
||||||
"./distribute.sh push" \
|
|
||||||
"All builds completed!" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "getbinpath after rebuild" \
|
|
||||||
"./distribute.sh getbinpath X64" \
|
|
||||||
"${DIST_DIR}/cache-apt-pkgs-linux-amd64" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Print test summary
|
|
||||||
echo -e "\nTest Summary"
|
|
||||||
echo "------------"
|
|
||||||
echo -e "Tests passed: ${GREEN}$PASS${NC}"
|
|
||||||
echo -e "Tests failed: ${RED}$FAIL${NC}"
|
|
||||||
|
|
||||||
# Exit with failure if any tests failed
|
|
||||||
[[ $FAIL -eq 0 ]] || exit 1
|
|
||||||
|
|
|
||||||
|
|
@ -9,161 +9,56 @@
|
||||||
# Validates version extraction, file generation, and error handling.
|
# Validates version extraction, file generation, and error handling.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/tests/export_version_test.sh [-v|--verbose]
|
# export_version_test.sh [OPTIONS]
|
||||||
#
|
#
|
||||||
# OPTIONS:
|
# OPTIONS:
|
||||||
# -v, --verbose Show verbose test output
|
# -v, --verbose Enable verbose test output
|
||||||
# -h, --help Show this help message
|
# --stop-on-failure Stop on first test failure
|
||||||
|
# -h, --help Show this help message
|
||||||
#
|
#
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
# Colors for test output
|
# Source the test framework, exports SCRIPT_PATH
|
||||||
GREEN='\033[0;32m'
|
source "$(git rev-parse --show-toplevel)/scripts/tests/test_lib.sh"
|
||||||
RED='\033[0;31m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Test settings
|
# Define test functions
|
||||||
VERBOSE=false
|
run_tests() {
|
||||||
PASS=0
|
test_section "Command Line Interface"
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
# Help message
|
test_case "basic execution" \
|
||||||
show_help() {
|
"" \
|
||||||
sed -n '/^# DESCRIPTION:/,/^#===/p' "$0" | sed 's/^# \?//'
|
"Exporting version information" \
|
||||||
}
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
-v|--verbose)
|
|
||||||
VERBOSE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Source the script (without executing main)
|
|
||||||
source "$PROJECT_ROOT/scripts/export_version.sh"
|
|
||||||
|
|
||||||
# Main test case function
|
|
||||||
function test_case() {
|
|
||||||
local name=$1
|
|
||||||
local cmd=$2
|
|
||||||
local expected_output=$3
|
|
||||||
local should_succeed=${4:-true}
|
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
|
||||||
|
|
||||||
# Run command and capture output
|
|
||||||
local output
|
|
||||||
if [[ $should_succeed == "true" ]]; then
|
|
||||||
output=$($cmd 2>&1)
|
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
output=$($cmd 2>&1) || true
|
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
|
||||||
echo " Expected output to contain: '$expected_output'"
|
|
||||||
echo " Got: '$output'"
|
|
||||||
((FAIL++))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Running export_version.sh tests..."
|
|
||||||
echo "--------------------------------"
|
|
||||||
|
|
||||||
# Section 1: Command Line Interface
|
|
||||||
echo -e "\n${BLUE}Testing Command Line Interface${NC}"
|
|
||||||
test_case "help option" \
|
|
||||||
"$PROJECT_ROOT/scripts/export_version.sh --help" \
|
|
||||||
"Usage:" \
|
|
||||||
true
|
true
|
||||||
|
|
||||||
test_case "unknown option" \
|
test_section "File Generation"
|
||||||
"$PROJECT_ROOT/scripts/export_version.sh --unknown" \
|
|
||||||
"Unknown option" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Section 2: Version Extraction
|
test_case "version info file creation" \
|
||||||
echo -e "\n${BLUE}Testing Version Extraction${NC}"
|
"" \
|
||||||
test_case "go version extraction" \
|
|
||||||
"get_go_version" \
|
|
||||||
"1.23" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "toolchain version extraction" \
|
|
||||||
"get_toolchain_version" \
|
|
||||||
"go1.23.4" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "syspkg version extraction" \
|
|
||||||
"get_syspkg_version" \
|
|
||||||
"v0.1.5" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 3: File Generation
|
|
||||||
echo -e "\n${BLUE}Testing File Generation${NC}"
|
|
||||||
test_case "version info file creation" \
|
|
||||||
"$PROJECT_ROOT/scripts/export_version.sh" \
|
|
||||||
"Version information has been exported" \
|
"Version information has been exported" \
|
||||||
true
|
true
|
||||||
|
|
||||||
test_case "version file format" \
|
test_case "JSON file creation" \
|
||||||
"grep -E '^GO_VERSION=[0-9]+\.[0-9]+$' $PROJECT_ROOT/.version-info" \
|
"" \
|
||||||
"GO_VERSION=1.23" \
|
"exported in JSON format" \
|
||||||
true
|
true
|
||||||
|
|
||||||
test_case "JSON file format" \
|
test_section "File Contents Validation"
|
||||||
"grep -E '\"goVersion\": \"[0-9]+\.[0-9]+\"' $PROJECT_ROOT/.version-info.json" \
|
|
||||||
"\"goVersion\": \"1.23\"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 4: Error Conditions
|
local project_root
|
||||||
echo -e "\n${BLUE}Testing Error Conditions${NC}"
|
project_root=$(get_project_root)
|
||||||
test_case "invalid go.mod" \
|
# Test that files exist and contain expected content
|
||||||
"GO_MOD_PATH=$TEMP_DIR/go.mod $PROJECT_ROOT/scripts/export_version.sh" \
|
test_file_exists "version info file exists" "${project_root}/.version-info"
|
||||||
"Could not read go.mod" \
|
test_file_exists "JSON version file exists" "${project_root}/.version-info.json"
|
||||||
false
|
|
||||||
|
|
||||||
# Create invalid go.mod for testing
|
test_file_contains "version file contains Go version" \
|
||||||
echo "invalid content" > "$TEMP_DIR/go.mod"
|
"${project_root}/.version-info" \
|
||||||
test_case "malformed go.mod" \
|
"GO_VERSION="
|
||||||
"GO_MOD_PATH=$TEMP_DIR/go.mod $PROJECT_ROOT/scripts/export_version.sh" \
|
|
||||||
"Failed to parse version" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Report results
|
test_file_contains "JSON file contains Go version" \
|
||||||
echo
|
"${project_root}/.version-info.json" \
|
||||||
echo "Test Results:"
|
'"goVersion":'
|
||||||
echo "Passed: $PASS"
|
}
|
||||||
echo "Failed: $FAIL"
|
|
||||||
exit $FAIL
|
# Start the test framework and run tests
|
||||||
|
start_tests "$@"
|
||||||
|
run_tests
|
||||||
|
|
|
||||||
|
|
@ -5,176 +5,44 @@
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
#
|
#
|
||||||
# DESCRIPTION:
|
# DESCRIPTION:
|
||||||
# Test suite for setup_dev.sh script.
|
# Test script for setup_dev.sh functionality.
|
||||||
# Validates development environment setup, tool installation, and configuration.
|
# Validates development environment setup without modifying the actual system.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/tests/setup_dev_test.sh [-v|--verbose] [-s|--skip-install]
|
# setup_dev_test.sh [OPTIONS]
|
||||||
#
|
#
|
||||||
# OPTIONS:
|
# OPTIONS:
|
||||||
# -v, --verbose Show verbose test output
|
# -v, --verbose Enable verbose test output
|
||||||
# -h, --help Show this help message
|
# --stop-on-failure Stop on first test failure
|
||||||
# -s, --skip-install Skip actual installation tests
|
# -h, --help Show this help message
|
||||||
#
|
#
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
# Source the test library
|
# Source the test framework, exports SCRIPT_PATH
|
||||||
source "$(dirname "$0")/test_lib.sh"
|
source "$(git rev-parse --show-toplevel)/scripts/tests/test_lib.sh"
|
||||||
|
|
||||||
# Additional settings
|
# Define test functions
|
||||||
SKIP_INSTALL=false
|
run_tests() {
|
||||||
|
test_section "Help and Usage"
|
||||||
|
|
||||||
# Parse arguments (handle any unprocessed args from common parser)
|
test_case "shows help message" \
|
||||||
while [[ -n "$1" ]]; do
|
"--help" \
|
||||||
arg="$(parse_common_args "$1")"
|
"USAGE:" \
|
||||||
case "$arg" in
|
|
||||||
-s|--skip-install)
|
|
||||||
SKIP_INSTALL=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
generate_help "$0"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# Initialize test environment
|
|
||||||
setup_test_env
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Main test case function
|
|
||||||
function test_case() {
|
|
||||||
local name=$1
|
|
||||||
local cmd=$2
|
|
||||||
local expected_output=$3
|
|
||||||
local should_succeed=${4:-true}
|
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
|
||||||
|
|
||||||
# Run command and capture output
|
|
||||||
local output
|
|
||||||
if [[ $should_succeed == "true" ]]; then
|
|
||||||
output=$($cmd 2>&1)
|
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
output=$($cmd 2>&1) || true
|
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
|
||||||
echo " Expected output to contain: '$expected_output'"
|
|
||||||
echo " Got: '$output'"
|
|
||||||
((FAIL++))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Running setup_dev.sh tests..."
|
|
||||||
echo "---------------------------"
|
|
||||||
|
|
||||||
# Section 1: Command Line Interface
|
|
||||||
print_section "Testing Command Line Interface"
|
|
||||||
test_case "help option" \
|
|
||||||
"$PROJECT_ROOT/scripts/setup_dev.sh --help" \
|
|
||||||
"Usage:" \
|
|
||||||
true
|
true
|
||||||
|
|
||||||
test_case "unknown option" \
|
test_case "shows error for invalid option" \
|
||||||
"$PROJECT_ROOT/scripts/setup_dev.sh --unknown" \
|
"--invalid-option" \
|
||||||
"Unknown option" \
|
"Unknown option" \
|
||||||
false
|
false
|
||||||
|
|
||||||
# Section 2: Go Environment Check
|
test_section "Argument Processing"
|
||||||
print_section "Testing Go Environment"
|
|
||||||
test_case "go installation" \
|
test_case "accepts verbose flag" \
|
||||||
"command -v go" \
|
"--verbose --help" \
|
||||||
"" \
|
"USAGE:" \
|
||||||
true
|
true
|
||||||
|
}
|
||||||
|
|
||||||
test_case "go version format" \
|
# Start the test framework and run tests
|
||||||
"go version" \
|
start_tests "$@"
|
||||||
"go version go1" \
|
run_tests
|
||||||
true
|
|
||||||
|
|
||||||
test_case "go modules enabled" \
|
|
||||||
"go env GO111MODULE" \
|
|
||||||
"on" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 3: Development Tool Installation
|
|
||||||
print_section "Testing Development Tools"
|
|
||||||
test_case "doctoc installation check" \
|
|
||||||
"command -v doctoc" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "trunk installation check" \
|
|
||||||
"command -v trunk" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
if [[ "$SKIP_INSTALL" == "false" ]]; then
|
|
||||||
test_case "doctoc functionality" \
|
|
||||||
"doctoc --version" \
|
|
||||||
"doctoc@" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "trunk functionality" \
|
|
||||||
"trunk --version" \
|
|
||||||
"trunk" \
|
|
||||||
true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Section 4: Project Configuration
|
|
||||||
print_section "Testing Project Configuration"
|
|
||||||
test_case "go.mod existence" \
|
|
||||||
"test -f $PROJECT_ROOT/go.mod" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "trunk.yaml existence" \
|
|
||||||
"test -f $PROJECT_ROOT/.trunk/trunk.yaml" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 5: Error Conditions
|
|
||||||
print_section "Testing Error Conditions"
|
|
||||||
test_case "invalid GOPATH" \
|
|
||||||
"GOPATH=/nonexistent $PROJECT_ROOT/scripts/setup_dev.sh" \
|
|
||||||
"Invalid GOPATH" \
|
|
||||||
false
|
|
||||||
|
|
||||||
if [[ "$SKIP_INSTALL" == "false" ]]; then
|
|
||||||
test_case "network failure simulation" \
|
|
||||||
"SIMULATE_NETWORK_FAILURE=1 $PROJECT_ROOT/scripts/setup_dev.sh" \
|
|
||||||
"Failed to download" \
|
|
||||||
false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Report test results and exit with appropriate status
|
|
||||||
report_results
|
|
||||||
|
|
||||||
# Report results
|
|
||||||
echo
|
|
||||||
echo "Test Results:"
|
|
||||||
echo "Passed: $PASS"
|
|
||||||
echo "Failed: $FAIL"
|
|
||||||
exit $FAIL
|
|
||||||
|
|
|
||||||
49
scripts/tests/template_test.sh
Executable file
49
scripts/tests/template_test.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# <script name>.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Test suite for <script name>.sh. Validates <brief description of what is
|
||||||
|
# being tested>.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# <script name>.sh [OPTIONS]
|
||||||
|
#
|
||||||
|
# OPTIONS:
|
||||||
|
# -v, --verbose Enable verbose test output
|
||||||
|
# --stop-on-failure Stop on first test failure
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Source the test framework, exports SCRIPT_PATH
|
||||||
|
source "$(git rev-parse --show-toplevel)/scripts/tests/test_lib.sh"
|
||||||
|
|
||||||
|
# Define test functions
|
||||||
|
run_tests() {
|
||||||
|
# Prints "Testing group <test group one name>"
|
||||||
|
print_group "<test group one name>"
|
||||||
|
|
||||||
|
# Prints "Testing section <test section 1 name>"
|
||||||
|
print_section "<test section 1 name>"
|
||||||
|
|
||||||
|
test_case "<test name 1>" \
|
||||||
|
"" \
|
||||||
|
"No changes made" \
|
||||||
|
false
|
||||||
|
|
||||||
|
test_case "<test name 2>" \
|
||||||
|
"with_changes" \
|
||||||
|
"" \
|
||||||
|
false
|
||||||
|
|
||||||
|
# Prints "Testing section <test section 2 name>"
|
||||||
|
print_section "<test section 2 name>"
|
||||||
|
|
||||||
|
test_case "<test name 3>" \
|
||||||
|
"" \
|
||||||
|
"No changes made" \
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
@ -5,268 +5,461 @@
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
#
|
#
|
||||||
# DESCRIPTION:
|
# DESCRIPTION:
|
||||||
# Common testing library for shell script tests.
|
# Common test library providing standardized test framework for bash scripts.
|
||||||
# Provides standard test framework functions and utilities.
|
# Provides test execution, assertions, test environment setup, and reporting.
|
||||||
|
# Implements improved architecture patterns for reliable test execution.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# source "$(dirname "$0")/test_lib.sh"
|
# # Set up the script path we want to test BEFORE sourcing
|
||||||
|
# SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# export SCRIPT_PATH="$SCRIPT_DIR/../script_name.sh"
|
||||||
|
#
|
||||||
|
# # Source the test framework
|
||||||
|
# source "$SCRIPT_DIR/test_lib.sh"
|
||||||
|
#
|
||||||
|
# # Define test functions
|
||||||
|
# run_tests() {
|
||||||
|
# test_section "Section Name"
|
||||||
|
# test_case "test name" "args" "expected_output" "should_succeed"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Start the test framework and run tests
|
||||||
|
# start_tests "$@"
|
||||||
|
# run_tests
|
||||||
|
#
|
||||||
|
# OPTIONS (inherited from command line):
|
||||||
|
# -v, --verbose Enable verbose test output
|
||||||
|
# --stop-on-failure Stop on first test failure
|
||||||
|
# -h, --help Show this help message
|
||||||
|
#
|
||||||
|
# EXPORTS: For use in test scripts.
|
||||||
|
# - SCRIPT_PATH Path of the script the test is running against
|
||||||
|
# - TEMP_TEST_DIR Path to the temporary test directory
|
||||||
|
# - test_case Function to define a test case
|
||||||
|
# - test_section Function to define test sections
|
||||||
|
# - test_file_exists Function to test file existence
|
||||||
|
# - test_file_contains Function to test file contents
|
||||||
#
|
#
|
||||||
# FEATURES:
|
# FEATURES:
|
||||||
# - Standard test framework
|
# - Improved library loading with fallback paths
|
||||||
# - Color output
|
# - Safe SCRIPT_PATH handling without overriding test settings
|
||||||
# - Test counting
|
# - Arithmetic operations compatible with set -e
|
||||||
# - Temporary directory management
|
# - Proper script name detection for test headers
|
||||||
# - Command line parsing
|
# - Lazy temporary directory initialization
|
||||||
# - Help text generation
|
# - Standardized test case execution and reporting
|
||||||
|
# - Test environment management with automatic cleanup
|
||||||
|
# - Comprehensive assertion functions
|
||||||
|
# - Test statistics and result reporting
|
||||||
|
#
|
||||||
|
# ARCHITECTURE IMPROVEMENTS:
|
||||||
|
# - Library loading uses multiple fallback paths for reliability
|
||||||
|
# - SCRIPT_PATH variable is preserved from test script initialization
|
||||||
|
# - Arithmetic increment operations use "|| true" pattern for set -e compatibility
|
||||||
|
# - Test framework initialization is separated from test execution
|
||||||
|
# - Temporary directory creation is deferred until actually needed
|
||||||
|
# - Script name detection iterates through BASH_SOURCE to find actual test script
|
||||||
#
|
#
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
# Colors for test output
|
# Source the shared library - get the correct path
|
||||||
export GREEN='\033[0;32m'
|
# shellcheck source="../lib.sh"
|
||||||
export RED='\033[0;31m'
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
export BLUE='\033[0;34m'
|
|
||||||
export NC='\033[0m' # No Color
|
|
||||||
export BOLD='\033[1m'
|
|
||||||
|
|
||||||
# Test counters
|
# Initialize temp directory when needed
|
||||||
export PASS=0
|
__init_temp_dir() {
|
||||||
export FAIL=0
|
if [[ -z ${TEMP_TEST_DIR} ]]; then
|
||||||
|
TEMP_TEST_DIR="$(create_temp_dir)"
|
||||||
# Test settings
|
export TEMP_TEST_DIR
|
||||||
export VERBOSE=${VERBOSE:-false}
|
fi
|
||||||
export TEMP_DIR
|
|
||||||
|
|
||||||
# Print functions
|
|
||||||
print_header() {
|
|
||||||
echo -e "\n${BOLD}${1}${NC}\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print_section() {
|
#==============================================================================
|
||||||
echo -e "\n${BLUE}${1}${NC}"
|
# Test Framework Variables
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
TEST_PASS=0
|
||||||
|
TEST_FAIL=0
|
||||||
|
TEST_SKIP=0
|
||||||
|
TEST_START_TIME=""
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
TEST_VERBOSE=${TEST_VERBOSE:-false}
|
||||||
|
TEST_CONTINUE_ON_FAILURE=${TEST_CONTINUE_ON_FAILURE:-true}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Framework Architecture Notes
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# KEY IMPROVEMENTS IMPLEMENTED:
|
||||||
|
#
|
||||||
|
# 1. Library Loading Reliability:
|
||||||
|
# - Multiple fallback paths for lib.sh loading
|
||||||
|
# - Works from both project root and scripts/ directory
|
||||||
|
# - Provides clear error messages if lib.sh cannot be found
|
||||||
|
#
|
||||||
|
# 2. Variable Management:
|
||||||
|
# - SCRIPT_PATH is preserved from test script initialization
|
||||||
|
# - Only initializes variables if not already set
|
||||||
|
# - Prevents test framework from overriding test script settings
|
||||||
|
#
|
||||||
|
# 3. Arithmetic Operations:
|
||||||
|
# - All increment operations use "|| true" pattern
|
||||||
|
# - Compatible with bash "set -e" error handling
|
||||||
|
# - Prevents premature script termination on arithmetic operations
|
||||||
|
#
|
||||||
|
# 4. Script Name Detection:
|
||||||
|
# - Iterates through BASH_SOURCE array to find actual test script
|
||||||
|
# - Skips test_lib.sh to show correct script name in headers
|
||||||
|
# - Provides accurate test identification in output
|
||||||
|
#
|
||||||
|
# 5. Resource Management:
|
||||||
|
# - Lazy initialization of temporary directories
|
||||||
|
# - Only creates temp resources when actually needed
|
||||||
|
# - Proper cleanup handling with trap functions
|
||||||
|
#
|
||||||
|
# 6. Test Organization:
|
||||||
|
# - Function-based test structure (run_tests pattern)
|
||||||
|
# - Clear separation of framework initialization and test execution
|
||||||
|
# - Standardized test case and section patterns
|
||||||
|
#
|
||||||
|
#==============================================================================
|
||||||
|
# Test Environment Setup
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
__setup_test_env() {
|
||||||
|
TEST_START_TIME=$(date +%s)
|
||||||
|
__init_temp_dir
|
||||||
|
trap '__cleanup_test_env' EXIT
|
||||||
|
log_debug "Test environment setup complete"
|
||||||
|
log_debug "Temporary directory: ${TEMP_TEST_DIR}"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_info() {
|
__cleanup_test_env() {
|
||||||
[[ "$VERBOSE" == "true" ]] && echo "INFO: $1"
|
local exit_code=$?
|
||||||
|
__report_results
|
||||||
|
if [[ -n ${TEMP_TEST_DIR} && -d ${TEMP_TEST_DIR} ]]; then
|
||||||
|
safe_remove "${TEMP_TEST_DIR}"
|
||||||
|
log_debug "Test environment cleanup complete"
|
||||||
|
fi
|
||||||
|
exit "${exit_code}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main test case function
|
__setup() {
|
||||||
|
__parse_test_args "$@"
|
||||||
|
# Find the main test script that sourced us (skip test_lib.sh itself)
|
||||||
|
local script_name=""
|
||||||
|
for ((i = 1; i < ${#BASH_SOURCE[@]}; i++)); do
|
||||||
|
if [[ ${BASH_SOURCE[i]} != *"test_lib.sh" ]]; then
|
||||||
|
script_name=$(basename "${BASH_SOURCE[i]}")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
print_header "Running ${script_name} tests"
|
||||||
|
echo ""
|
||||||
|
__setup_test_env
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Test Execution Functions
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
test_case() {
|
test_case() {
|
||||||
local name=$1
|
local name="$1"
|
||||||
local cmd=$2
|
local args="$2"
|
||||||
local expected_output=$3
|
local expected_output="$3"
|
||||||
local should_succeed=${4:-true}
|
local should_succeed="${4:-true}"
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
# Disable exit-on-error for test execution
|
||||||
print_info "Command: $cmd"
|
set +e
|
||||||
|
|
||||||
# Run command and capture output
|
# Support shorthand: test_case "name" "args" "true|false" (no expected_output)
|
||||||
local output
|
if [[ -z ${expected_output} && (${should_succeed} == "true" || ${should_succeed} == "false") ]]; then
|
||||||
if [[ $should_succeed == "true" ]]; then
|
expected_output=""
|
||||||
output=$($cmd 2>&1)
|
fi
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
echo -n "* ${name}... "
|
||||||
echo -e "${GREEN}PASS${NC}"
|
[[ ${TEST_VERBOSE} == true ]] && echo -n "(${COMMAND} ${args}) "
|
||||||
((PASS++))
|
|
||||||
print_info "Output: $output"
|
local output
|
||||||
return 0
|
local exit_code=0
|
||||||
fi
|
|
||||||
|
# Capture both stdout and stderr, ensuring we don't exit on command failure
|
||||||
|
local cmd="${SCRIPT_PATH} ${args}"
|
||||||
|
if [[ ${should_succeed} == "true" ]]; then
|
||||||
|
# For tests that should succeed
|
||||||
|
output=$(eval "${cmd}" 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
if [[ ${exit_code} -eq 0 && ${output} == *"${expected_output}"* ]]; then
|
||||||
|
__test_pass "${name}"
|
||||||
else
|
else
|
||||||
output=$($cmd 2>&1) || true
|
__test_fail "${name}" "Success with output containing '${expected_output}'" "Exit code ${exit_code} with output: '${output}'"
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
print_info "Output: $output"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
# For tests that should fail
|
||||||
|
output=$(eval "${cmd}" 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
if [[ ${exit_code} -ne 0 && ${output} == *"${expected_output}"* ]]; then
|
||||||
echo " Expected output to contain: '$expected_output'"
|
__test_pass "${name}"
|
||||||
echo " Got: '$output'"
|
else
|
||||||
((FAIL++))
|
__test_fail "${name}" "Failure with output containing '${expected_output}'" "Exit code ${exit_code} with output: '${output}'"
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Setup functions
|
|
||||||
setup_test_env() {
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap cleanup_test_env EXIT
|
|
||||||
print_info "Created temporary directory: $TEMP_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_test_env() {
|
|
||||||
if [[ -d "$TEMP_DIR" ]]; then
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
print_info "Cleaned up temporary directory: $TEMP_DIR"
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e # Restore exit-on-error
|
||||||
}
|
}
|
||||||
|
|
||||||
# Help text generation
|
__test_pass() {
|
||||||
generate_help() {
|
local name="$1"
|
||||||
local script_path=$1
|
echo -e "${GREEN}PASS${NC}"
|
||||||
sed -n '/^# DESCRIPTION:/,/^#===/p' "$script_path" | sed 's/^# \?//'
|
((TEST_PASS++)) || true
|
||||||
|
[[ ${TEST_VERBOSE} == true ]] && log_debug "Test passed: ${name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Standard argument parsing
|
__test_fail() {
|
||||||
parse_common_args() {
|
local name="$1"
|
||||||
while [[ $# -gt 0 ]]; do
|
local expected="$2"
|
||||||
case $1 in
|
local actual="$3"
|
||||||
-v|--verbose)
|
|
||||||
VERBOSE=true
|
echo -e "${RED}FAIL${NC}"
|
||||||
shift
|
((TEST_FAIL++)) || true
|
||||||
;;
|
|
||||||
-h|--help)
|
if [[ -n ${expected} ]]; then
|
||||||
generate_help "$0"
|
echo " Expected : ${expected}"
|
||||||
exit 0
|
fi
|
||||||
;;
|
if [[ -n ${actual} ]]; then
|
||||||
*)
|
echo " Actual : ${actual}"
|
||||||
# Return the unhandled argument
|
fi
|
||||||
echo "$1"
|
|
||||||
;;
|
if [[ ${TEST_CONTINUE_ON_FAILURE} != true ]]; then
|
||||||
esac
|
__report_results
|
||||||
shift
|
exit 1
|
||||||
done
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Results reporting
|
#==============================================================================
|
||||||
report_results() {
|
# Advanced Test Functions
|
||||||
echo
|
#==============================================================================
|
||||||
echo "Test Results:"
|
|
||||||
echo "------------"
|
|
||||||
echo "Passed: $PASS"
|
|
||||||
echo "Failed: $FAIL"
|
|
||||||
echo "Total: $((PASS + FAIL))"
|
|
||||||
|
|
||||||
# Return non-zero if any tests failed
|
test_file_exists() {
|
||||||
[[ $FAIL -eq 0 ]]
|
local name="$1"
|
||||||
|
local file_path="$2"
|
||||||
|
|
||||||
|
echo -n "* ${name}... "
|
||||||
|
set +e
|
||||||
|
if file_exists "${file_path}"; then
|
||||||
|
__test_pass "${name}"
|
||||||
|
else
|
||||||
|
__test_fail "${name}" "File should exist: file_path='${file_path}'" "File does not exist"
|
||||||
|
fi
|
||||||
|
set -e
|
||||||
}
|
}
|
||||||
|
|
||||||
# File operation helpers
|
test_file_contains() {
|
||||||
|
local name="$1"
|
||||||
|
local file_path="$2"
|
||||||
|
local expected_content="$3"
|
||||||
|
|
||||||
|
echo -n "* ${name}... "
|
||||||
|
if ! file_exists "${file_path}"; then
|
||||||
|
__test_fail "${name}" "File should exist and contain '${expected_content}'" "File does not exist: ${file_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "${expected_content}" "${file_path}"; then
|
||||||
|
__test_pass "${name}"
|
||||||
|
else
|
||||||
|
local file_content
|
||||||
|
file_content=$(cat "${file_path}")
|
||||||
|
__test_fail "${name}" "File should contain '${expected_content}'" "File content: ${file_content}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Test Utilities
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
create_test_file() {
|
create_test_file() {
|
||||||
local file="$1"
|
local file_path="$1"
|
||||||
local content="$2"
|
local content="$2"
|
||||||
local mode="${3:-644}"
|
local mode="${3:-644}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$file")"
|
local dir_path
|
||||||
echo "$content" > "$file"
|
dir_path=$(dirname "${file_path}")
|
||||||
chmod "$mode" "$file"
|
ensure_dir "${dir_path}"
|
||||||
print_info "Created test file: $file"
|
|
||||||
|
echo "${content}" >"${file_path}"
|
||||||
|
chmod "${mode}" "${file_path}"
|
||||||
|
|
||||||
|
log_debug "Created test file: ${file_path}"
|
||||||
}
|
}
|
||||||
|
|
||||||
create_test_dir() {
|
#==============================================================================
|
||||||
local dir="$1"
|
# Test Organization Helpers
|
||||||
local mode="${2:-755}"
|
#==============================================================================
|
||||||
|
|
||||||
mkdir -p "$dir"
|
test_section() {
|
||||||
chmod "$mode" "$dir"
|
local section_name="$1"
|
||||||
print_info "Created test directory: $dir"
|
print_section "Testing section: ${section_name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_file_contains() {
|
test_group() {
|
||||||
local file="$1"
|
local group_name="$1"
|
||||||
local pattern="$2"
|
echo_color cyan "Testing group: ${group_name}"
|
||||||
local message="${3:-File does not contain expected content}"
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
if ! grep -q "$pattern" "$file"; then
|
#==============================================================================
|
||||||
echo -e "${RED}FAIL${NC}: $message"
|
# Test Reporting
|
||||||
echo " File: $file"
|
#==============================================================================
|
||||||
echo " Expected pattern: $pattern"
|
|
||||||
echo " Content:"
|
__report_results() {
|
||||||
cat "$file"
|
local end_time
|
||||||
return 1
|
end_time=$(date +%s)
|
||||||
fi
|
local duration=$((end_time - TEST_START_TIME))
|
||||||
|
|
||||||
|
echo
|
||||||
|
print_section "Test Results Summary"
|
||||||
|
echo "Duration : ${duration}s"
|
||||||
|
echo "Total : $((TEST_PASS + TEST_FAIL))"
|
||||||
|
if [[ ${TEST_PASS} -gt 0 ]]; then
|
||||||
|
echo -e "Passed : ${GREEN}${TEST_PASS}${NC}"
|
||||||
|
else
|
||||||
|
echo -e "Passed : ${TEST_PASS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${TEST_FAIL} -gt 0 ]]; then
|
||||||
|
echo -e "Failed : ${RED}${TEST_FAIL}${NC}"
|
||||||
|
else
|
||||||
|
echo -e "Failed : ${TEST_FAIL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${TEST_SKIP} -gt 0 ]]; then
|
||||||
|
echo -e "Skipped : ${YELLOW}${TEST_SKIP}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ ${TEST_FAIL} -eq 0 ]]; then
|
||||||
|
log_success "All tests passed!"
|
||||||
return 0
|
return 0
|
||||||
}
|
else
|
||||||
|
log_error "${TEST_FAIL} test(s) failed"
|
||||||
assert_file_exists() {
|
|
||||||
local file="$1"
|
|
||||||
local message="${2:-File does not exist}"
|
|
||||||
|
|
||||||
if [[ ! -f "$file" ]]; then
|
|
||||||
echo -e "${RED}FAIL${NC}: $message"
|
|
||||||
echo " Expected file: $file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_dir_exists() {
|
|
||||||
local dir="$1"
|
|
||||||
local message="${2:-Directory does not exist}"
|
|
||||||
|
|
||||||
if [[ ! -d "$dir" ]]; then
|
|
||||||
echo -e "${RED}FAIL${NC}: $message"
|
|
||||||
echo " Expected directory: $dir"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
is_command_available() {
|
|
||||||
command -v "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_condition() {
|
|
||||||
local cmd="$1"
|
|
||||||
local timeout="${2:-10}"
|
|
||||||
local interval="${3:-1}"
|
|
||||||
|
|
||||||
local end_time=$((SECONDS + timeout))
|
|
||||||
while [[ $SECONDS -lt $end_time ]]; do
|
|
||||||
if eval "$cmd"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep "$interval"
|
|
||||||
done
|
|
||||||
return 1
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
skip_if_command_missing() {
|
#==============================================================================
|
||||||
local cmd="$1"
|
# Common Test Patterns
|
||||||
local message="${2:-Required command not available}"
|
#==============================================================================
|
||||||
|
|
||||||
if ! is_command_available "$cmd"; then
|
__test_help_option() {
|
||||||
echo "SKIP: $message (missing: $cmd)"
|
local script_path="$1"
|
||||||
return 0
|
local script_name
|
||||||
|
script_name=$(basename "${script_path}")
|
||||||
|
|
||||||
|
test_case "help option (-h)" \
|
||||||
|
"${script_path} -h" \
|
||||||
|
"USAGE" \
|
||||||
|
true
|
||||||
|
|
||||||
|
test_case "help option (--help)" \
|
||||||
|
"${script_path} --help" \
|
||||||
|
"USAGE" \
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
__test_invalid_arguments() {
|
||||||
|
local script_path="$1"
|
||||||
|
|
||||||
|
test_case "invalid option" \
|
||||||
|
"${script_path} --invalid-option" \
|
||||||
|
"error" \
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# Initialization
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
# Default test argument parsing
|
||||||
|
__parse_test_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-v | --verbose)
|
||||||
|
export TEST_VERBOSE=true
|
||||||
|
export VERBOSE=true
|
||||||
|
;;
|
||||||
|
--stop-on-failure)
|
||||||
|
export TEST_CONTINUE_ON_FAILURE=false
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
[[ $(type -t show_help) == function ]] && show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to start the testing framework
|
||||||
|
start_tests() {
|
||||||
|
__setup "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
|
||||||
|
echo "This script should be sourced, not executed directly."
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
echo 'Usage: source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"/test_lib.sh'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
__get_script_path_dynamic() {
|
||||||
|
local test_filepath
|
||||||
|
local root_path
|
||||||
|
local script_filename
|
||||||
|
local script_filepath
|
||||||
|
# Try multiple ways of locating the test script to support different invocation styles
|
||||||
|
# Find the calling test script (first BASH_SOURCE entry that ends with _test.sh)
|
||||||
|
test_filepath=""
|
||||||
|
for ((i = 1; i < ${#BASH_SOURCE[@]}; i++)); do
|
||||||
|
if [[ ${BASH_SOURCE[i]} == *_test.sh ]]; then
|
||||||
|
test_filepath="${BASH_SOURCE[i]}"
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
return 1
|
done
|
||||||
}
|
# Fallbacks if not found
|
||||||
|
test_filepath="${test_filepath:-${BASH_SOURCE[1]:-${BASH_SOURCE[0]:-$0}}}"
|
||||||
|
root_path="$(get_project_root 2>/dev/null || pwd)"
|
||||||
|
[[ ${TEST_VERBOSE} == true ]] && echo "DEBUG: test_filepath=${test_filepath} root_path=${root_path}" >&2
|
||||||
|
script_filename="$(basename "${test_filepath}" | sed 's/_test.sh/.sh/g')"
|
||||||
|
script_filepath="${root_path}/scripts/${script_filename}"
|
||||||
|
|
||||||
run_if_exists() {
|
if [[ -f ${script_filepath} ]]; then
|
||||||
local cmd="$1"
|
log_debug "Script path successfully found dynamically ${script_filepath}"
|
||||||
local fallback="$2"
|
echo "${script_filepath}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
if is_command_available "$cmd"; then
|
# Fallback: search scripts/ for a matching script name
|
||||||
"$cmd"
|
if [[ -d "${root_path}/scripts" ]]; then
|
||||||
else
|
local found
|
||||||
eval "$fallback"
|
found=$(find "${root_path}/scripts" -maxdepth 1 -type f -name "${script_filename}" -print -quit 2>/dev/null || true)
|
||||||
|
if [[ -n ${found} ]]; then
|
||||||
|
log_debug "Script path found via fallback: ${found}"
|
||||||
|
echo "${found}"
|
||||||
|
return 0
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail "Script file not found: ${script_filepath}; set SCRIPT_PATH before sourcing test_lib.sh"
|
||||||
}
|
}
|
||||||
|
|
||||||
backup_and_restore() {
|
# Will be set by the test script - only initialize if not already set
|
||||||
local file="$1"
|
[[ -z ${SCRIPT_PATH} ]] && SCRIPT_PATH="$(__get_script_path_dynamic)"
|
||||||
if [[ -f "$file" ]]; then
|
export SCRIPT_PATH
|
||||||
cp "$file" "${file}.bak"
|
[[ -z ${TEMP_TEST_DIR} ]] && TEMP_TEST_DIR=""
|
||||||
print_info "Backed up: $file"
|
export TEMP_TEST_DIR
|
||||||
trap 'restore_backup "$file"' EXIT
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
restore_backup() {
|
|
||||||
local file="$1"
|
|
||||||
if [[ -f "${file}.bak" ]]; then
|
|
||||||
mv "${file}.bak" "$file"
|
|
||||||
print_info "Restored: $file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies() {
|
|
||||||
local missing=0
|
|
||||||
for cmd in "$@"; do
|
|
||||||
if ! is_command_available "$cmd"; then
|
|
||||||
echo "Missing required dependency: $cmd"
|
|
||||||
((missing++))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return $missing
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
#==============================================================================
|
|
||||||
# update_md_tocs_test.sh
|
|
||||||
#==============================================================================
|
|
||||||
#
|
|
||||||
# DESCRIPTION:
|
|
||||||
# Test suite for update_md_tocs.sh script.
|
|
||||||
# Validates Table of Contents generation, markdown file handling,
|
|
||||||
# and doctoc integration.
|
|
||||||
#
|
|
||||||
# USAGE:
|
|
||||||
# ./scripts/tests/update_md_tocs_test.sh [-v|--verbose] [-s|--skip-doctoc]
|
|
||||||
#
|
|
||||||
# OPTIONS:
|
|
||||||
# -v, --verbose Show verbose test output
|
|
||||||
# -s, --skip-doctoc Skip tests requiring doctoc installation
|
|
||||||
# -h, --help Show this help message
|
|
||||||
#
|
|
||||||
#==============================================================================
|
|
||||||
|
|
||||||
# Colors for test output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Test settings
|
|
||||||
VERBOSE=false
|
|
||||||
SKIP_DOCTOC=false
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
# Help message
|
|
||||||
show_help() {
|
|
||||||
sed -n '/^# DESCRIPTION:/,/^#===/p' "$0" | sed 's/^# \?//'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
-v|--verbose)
|
|
||||||
VERBOSE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-s|--skip-doctoc)
|
|
||||||
SKIP_DOCTOC=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
||||||
|
|
||||||
# Create a temporary directory for test files
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
||||||
|
|
||||||
# Test helper functions
|
|
||||||
create_test_md() {
|
|
||||||
local file="$1"
|
|
||||||
cat > "$file" << EOF
|
|
||||||
# Test Document
|
|
||||||
|
|
||||||
<!-- START doctoc -->
|
|
||||||
<!-- END doctoc -->
|
|
||||||
|
|
||||||
## Section 1
|
|
||||||
### Subsection 1.1
|
|
||||||
### Subsection 1.2
|
|
||||||
|
|
||||||
## Section 2
|
|
||||||
### Subsection 2.1
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main test case function
|
|
||||||
function test_case() {
|
|
||||||
local name=$1
|
|
||||||
local cmd=$2
|
|
||||||
local expected_output=$3
|
|
||||||
local should_succeed=${4:-true}
|
|
||||||
|
|
||||||
echo -n "Testing $name... "
|
|
||||||
|
|
||||||
# Run command and capture output
|
|
||||||
local output
|
|
||||||
if [[ $should_succeed == "true" ]]; then
|
|
||||||
output=$($cmd 2>&1)
|
|
||||||
local status=$?
|
|
||||||
if [[ $status -eq 0 && $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
output=$($cmd 2>&1) || true
|
|
||||||
if [[ $output == *"$expected_output"* ]]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
((PASS++))
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${RED}FAIL${NC}"
|
|
||||||
echo " Expected output to contain: '$expected_output'"
|
|
||||||
echo " Got: '$output'"
|
|
||||||
((FAIL++))
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Running update_md_tocs.sh tests..."
|
|
||||||
echo "---------------------------------"
|
|
||||||
|
|
||||||
# Section 1: Command Line Interface
|
|
||||||
echo -e "\n${BLUE}Testing Command Line Interface${NC}"
|
|
||||||
test_case "help option" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh --help" \
|
|
||||||
"Usage:" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "unknown option" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh --unknown" \
|
|
||||||
"Unknown option" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Section 2: Basic TOC Generation
|
|
||||||
echo -e "\n${BLUE}Testing Basic TOC Generation${NC}"
|
|
||||||
create_test_md "$TEMP_DIR/test.md"
|
|
||||||
|
|
||||||
if [[ "$SKIP_DOCTOC" == "false" ]]; then
|
|
||||||
test_case "doctoc installation" \
|
|
||||||
"command -v doctoc" \
|
|
||||||
"" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "TOC generation" \
|
|
||||||
"doctoc '$TEMP_DIR/test.md'" \
|
|
||||||
"Table of Contents" \
|
|
||||||
true
|
|
||||||
|
|
||||||
test_case "TOC structure" \
|
|
||||||
"grep -A 5 'Table of Contents' '$TEMP_DIR/test.md'" \
|
|
||||||
"Section 1" \
|
|
||||||
true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Section 3: Multiple File Handling
|
|
||||||
echo -e "\n${BLUE}Testing Multiple File Handling${NC}"
|
|
||||||
create_test_md "$TEMP_DIR/doc1.md"
|
|
||||||
create_test_md "$TEMP_DIR/doc2.md"
|
|
||||||
|
|
||||||
test_case "multiple file update" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR/doc1.md' '$TEMP_DIR/doc2.md'" \
|
|
||||||
"updated" \
|
|
||||||
true
|
|
||||||
|
|
||||||
# Section 4: Special Cases
|
|
||||||
echo -e "\n${BLUE}Testing Special Cases${NC}"
|
|
||||||
# Create file without TOC markers
|
|
||||||
cat > "$TEMP_DIR/no_toc.md" << EOF
|
|
||||||
# Document
|
|
||||||
## Section 1
|
|
||||||
## Section 2
|
|
||||||
EOF
|
|
||||||
|
|
||||||
test_case "file without TOC markers" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR/no_toc.md'" \
|
|
||||||
"No TOC markers" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Create empty file
|
|
||||||
touch "$TEMP_DIR/empty.md"
|
|
||||||
test_case "empty file handling" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR/empty.md'" \
|
|
||||||
"Empty file" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Section 5: Error Conditions
|
|
||||||
echo -e "\n${BLUE}Testing Error Conditions${NC}"
|
|
||||||
test_case "nonexistent file" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh nonexistent.md" \
|
|
||||||
"No such file" \
|
|
||||||
false
|
|
||||||
|
|
||||||
test_case "directory as input" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR'" \
|
|
||||||
"Is a directory" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Create unreadable file
|
|
||||||
touch "$TEMP_DIR/unreadable.md"
|
|
||||||
chmod 000 "$TEMP_DIR/unreadable.md"
|
|
||||||
test_case "unreadable file" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR/unreadable.md'" \
|
|
||||||
"Permission denied" \
|
|
||||||
false
|
|
||||||
chmod 644 "$TEMP_DIR/unreadable.md"
|
|
||||||
|
|
||||||
# Create file with invalid markdown
|
|
||||||
cat > "$TEMP_DIR/invalid.md" << EOF
|
|
||||||
# [Invalid Markdown)
|
|
||||||
* Broken list
|
|
||||||
EOF
|
|
||||||
|
|
||||||
test_case "invalid markdown handling" \
|
|
||||||
"$PROJECT_ROOT/scripts/update_md_tocs.sh '$TEMP_DIR/invalid.md'" \
|
|
||||||
"Invalid markdown" \
|
|
||||||
false
|
|
||||||
|
|
||||||
# Report results
|
|
||||||
echo
|
|
||||||
echo "Test Results:"
|
|
||||||
echo "Passed: $PASS"
|
|
||||||
echo "Failed: $FAIL"
|
|
||||||
exit $FAIL
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
# and applies consistent formatting across all markdown files.
|
# and applies consistent formatting across all markdown files.
|
||||||
#
|
#
|
||||||
# USAGE:
|
# USAGE:
|
||||||
# ./scripts/update_md_tocs.sh
|
# update_md_tocs.sh [OPTIONS]
|
||||||
#
|
#
|
||||||
# FEATURES:
|
# FEATURES:
|
||||||
# - Auto-detects markdown files with doctoc markers
|
# - Auto-detects markdown files with doctoc markers
|
||||||
|
|
@ -37,53 +37,37 @@
|
||||||
# 0 - Success
|
# 0 - Success
|
||||||
# 1 - Missing dependencies or installation failure
|
# 1 - Missing dependencies or installation failure
|
||||||
#
|
#
|
||||||
# AUTHOR:
|
|
||||||
# Claude - 2025-08-28
|
|
||||||
#
|
|
||||||
# NOTES:
|
# NOTES:
|
||||||
# - Only processes files containing doctoc markers
|
# - Only processes files containing doctoc markers
|
||||||
# - Preserves existing markdown content
|
# - Preserves existing markdown content
|
||||||
# - Safe to run multiple times
|
# - Safe to run multiple times
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
||||||
# Function to check if a command exists
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
command_exists() {
|
|
||||||
command -v "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check if npm package is installed globally
|
|
||||||
npm_package_installed() {
|
|
||||||
npm list -g "$1" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Install doctoc if not present
|
# Install doctoc if not present
|
||||||
if ! command_exists doctoc; then
|
if ! command_exists doctoc; then
|
||||||
echo "doctoc not found. Installing..."
|
echo "doctoc not found. Installing..."
|
||||||
if ! command_exists npm; then
|
if ! command_exists npm; then
|
||||||
echo "Error: npm is required to install doctoc"
|
echo "Error: npm is required to install doctoc"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! npm_package_installed doctoc; then
|
if ! npm_package_installed doctoc; then
|
||||||
echo "Installing doctoc globally..."
|
echo "Installing doctoc globally..."
|
||||||
npm install -g doctoc
|
if ! npm install -g doctoc; then
|
||||||
if [ $? -ne 0 ]; then
|
fail "Failed to install doctoc"
|
||||||
echo "Error: Failed to install doctoc"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Updating table of contents in markdown files..."
|
print_status "Updating table of contents in markdown files..."
|
||||||
|
|
||||||
# Find all markdown files that contain doctoc markers
|
# Find all markdown files that contain doctoc markers
|
||||||
find . -type f -name "*.md" -exec grep -l "START doctoc" {} \; | while read -r file; do
|
find . -type f -name "*.md" -exec grep -l "START doctoc" {} \; | while read -r file; do
|
||||||
echo "Processing: $file"
|
log_info "Processing: ${file}"
|
||||||
doctoc --maxlevel 4 --no-title --notitle --github "$file"
|
if ! doctoc --maxlevel 4 --no-title --notitle --github "${file}"; then
|
||||||
|
log_error "Failed to update TOC in ${file}"
|
||||||
if [ $? -ne 0 ]; then
|
fi
|
||||||
echo "Error: Failed to update TOC in $file"
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Table of contents update complete!"
|
print_success "Table of contents update complete!"
|
||||||
|
|
|
||||||
23
scripts/update_trunkio.sh
Executable file
23
scripts/update_trunkio.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#==============================================================================
|
||||||
|
# update_trunkio.sh
|
||||||
|
#==============================================================================
|
||||||
|
#
|
||||||
|
# DESCRIPTION:
|
||||||
|
# Configures and updates the TrunkIO extension.
|
||||||
|
#
|
||||||
|
# USAGE:
|
||||||
|
# update_trunkio.sh
|
||||||
|
#==============================================================================
|
||||||
|
|
||||||
|
source "$(git rev-parse --show-toplevel)/scripts/lib.sh"
|
||||||
|
|
||||||
|
trunk upgrade
|
||||||
|
trunk check list --fix --print-failures
|
||||||
|
|
||||||
|
# TODO: Automatically enable any disabled linters except for cspell
|
||||||
|
# DISABLED_LINTERS="$(trunk check list | grep '◯' | grep "files" | awk -F ' ' '{print $2}')"
|
||||||
|
# for linter in $DISABLED_LINTERS; do
|
||||||
|
# echo "trunk check enable $linter;"
|
||||||
|
# done
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script to export Go library version information for package development
|
|
||||||
# This script reads version information from go.mod and exports it
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Get the directory containing this script
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
|
|
||||||
# Function to extract Go version from go.mod
|
|
||||||
get_go_version() {
|
|
||||||
local go_version
|
|
||||||
go_version=$(grep "^go " "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
|
||||||
echo "$go_version"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to extract toolchain version from go.mod
|
|
||||||
get_toolchain_version() {
|
|
||||||
local toolchain_version
|
|
||||||
toolchain_version=$(grep "^toolchain " "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
|
||||||
echo "$toolchain_version"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to extract syspkg version from go.mod
|
|
||||||
get_syspkg_version() {
|
|
||||||
local syspkg_version
|
|
||||||
syspkg_version=$(grep "github.com/awalsh128/syspkg" "$PROJECT_ROOT/go.mod" | awk '{print $2}')
|
|
||||||
echo "$syspkg_version"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
echo "Exporting version information..."
|
|
||||||
GO_VERSION=$(get_go_version)
|
|
||||||
TOOLCHAIN_VERSION=$(get_toolchain_version)
|
|
||||||
SYSPKG_VERSION=$(get_syspkg_version)
|
|
||||||
|
|
||||||
# Export versions as environment variables
|
|
||||||
export GO_VERSION
|
|
||||||
export TOOLCHAIN_VERSION
|
|
||||||
export SYSPKG_VERSION
|
|
||||||
|
|
||||||
# Create a version info file
|
|
||||||
VERSION_FILE="$PROJECT_ROOT/.version-info"
|
|
||||||
cat > "$VERSION_FILE" << EOF
|
|
||||||
# Version information for cache-apt-pkgs-action
|
|
||||||
GO_VERSION=$GO_VERSION
|
|
||||||
TOOLCHAIN_VERSION=$TOOLCHAIN_VERSION
|
|
||||||
SYSPKG_VERSION=$SYSPKG_VERSION
|
|
||||||
EXPORT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Version information has been exported to $VERSION_FILE"
|
|
||||||
echo "Go Version: $GO_VERSION"
|
|
||||||
echo "Toolchain Version: $TOOLCHAIN_VERSION"
|
|
||||||
echo "Syspkg Version: $SYSPKG_VERSION"
|
|
||||||
|
|
||||||
# Also create a JSON format for tools that prefer it
|
|
||||||
VERSION_JSON="$PROJECT_ROOT/.version-info.json"
|
|
||||||
cat > "$VERSION_JSON" << EOF
|
|
||||||
{
|
|
||||||
"goVersion": "$GO_VERSION",
|
|
||||||
"toolchainVersion": "$TOOLCHAIN_VERSION",
|
|
||||||
"syspkgVersion": "$SYSPKG_VERSION",
|
|
||||||
"exportDate": "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Version information also exported in JSON format to $VERSION_JSON"
|
|
||||||
Loading…
Reference in a new issue