From 364380201108a907ac354a97c1657d9530d76998 Mon Sep 17 00:00:00 2001 From: awalsh128 Date: Mon, 5 Jan 2026 16:36:23 -0800 Subject: [PATCH] More stuff --- .editorconfig | 37 - .env | 4 +- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/config.yml | 3 - .github/ISSUE_TEMPLATE/feature_request.md | 11 +- .../{action_tests.yml => action-tests.yml} | 224 +++--- .github/workflows/build-distribute.yml | 116 +++ .github/workflows/pr.yml | 23 + .trunk/check-ignore | 1 - .trunk/configs/.golangci.yaml | 79 -- .trunk/configs/.markdownlint.yaml | 41 +- .trunk/configs/.remarkrc.yaml | 8 - .trunk/configs/.shellcheckrc | 8 +- .trunk/configs/.vale.ini | 15 +- .trunk/configs/.yamllint.yaml | 34 +- .trunk/configs/Vale/Vocab/Project/accept.txt | 6 - .trunk/configs/analyzers.yml | 83 --- .trunk/scripts/fix_vale_suggestions.py | 0 .trunk/trunk.yaml | 45 +- .trunk/valestyles/Common/AllowedWords.txt | 22 + .trunk/valestyles/Microsoft/AMPM.yml | 9 + .trunk/valestyles/Microsoft/Accessibility.yml | 30 + .trunk/valestyles/Microsoft/Acronyms.yml | 64 ++ .trunk/valestyles/Microsoft/Adverbs.yml | 272 +++++++ .trunk/valestyles/Microsoft/Auto.yml | 11 + .trunk/valestyles/Microsoft/Avoid.yml | 14 + .trunk/valestyles/Microsoft/Contractions.yml | 50 ++ .trunk/valestyles/Microsoft/Dashes.yml | 13 + .trunk/valestyles/Microsoft/DateFormat.yml | 10 + .trunk/valestyles/Microsoft/DateNumbers.yml | 40 + .trunk/valestyles/Microsoft/DateOrder.yml | 8 + .trunk/valestyles/Microsoft/Ellipses.yml | 9 + .trunk/valestyles/Microsoft/FirstPerson.yml | 16 + .trunk/valestyles/Microsoft/Foreign.yml | 13 + .trunk/valestyles/Microsoft/Gender.yml | 8 + .trunk/valestyles/Microsoft/GenderBias.yml | 42 ++ .trunk/valestyles/Microsoft/GeneralURL.yml | 11 + .../Microsoft/HeadingAcronyms.yml.disabled | 7 + .trunk/valestyles/Microsoft/HeadingColons.yml | 8 + .../Microsoft/HeadingPunctuation.yml.disabled | 13 + .../Microsoft/Headings.yml.disabled | 28 + .trunk/valestyles/Microsoft/Hyphens.yml | 14 + .trunk/valestyles/Microsoft/Microsoft.ini | 2 + .trunk/valestyles/Microsoft/Negative.yml | 13 + .trunk/valestyles/Microsoft/Ordinal.yml | 13 + .trunk/valestyles/Microsoft/OxfordComma.yml | 8 + .trunk/valestyles/Microsoft/Passive.yml | 183 +++++ .trunk/valestyles/Microsoft/Percentages.yml | 7 + .trunk/valestyles/Microsoft/Plurals.yml | 7 + .trunk/valestyles/Microsoft/Quotes.yml | 7 + .trunk/valestyles/Microsoft/RangeTime.yml | 13 + .trunk/valestyles/Microsoft/Semicolon.yml | 8 + .../valestyles/Microsoft/SentenceLength.yml | 6 + .trunk/valestyles/Microsoft/Spacing.yml | 8 + .trunk/valestyles/Microsoft/Suspended.yml | 7 + .trunk/valestyles/Microsoft/Terms.yml | 42 ++ .trunk/valestyles/Microsoft/URLFormat.yml | 9 + .trunk/valestyles/Microsoft/Units.yml | 16 + .trunk/valestyles/Microsoft/Vocab.yml | 25 + .trunk/valestyles/Microsoft/We.yml | 11 + .trunk/valestyles/Microsoft/Wordiness.yml | 127 ++++ .trunk/valestyles/Microsoft/meta.json | 4 + .trunk/valestyles/WriteGood/Cliches.yml | 702 ++++++++++++++++++ .trunk/valestyles/WriteGood/E-Prime.yml | 32 + .trunk/valestyles/WriteGood/Illusions.yml | 11 + .trunk/valestyles/WriteGood/Passive.yml | 183 +++++ .trunk/valestyles/WriteGood/README.md | 28 + .trunk/valestyles/WriteGood/So.yml | 5 + .trunk/valestyles/WriteGood/ThereIs.yml | 6 + .trunk/valestyles/WriteGood/TooWordy.yml | 221 ++++++ .trunk/valestyles/WriteGood/Weasel.yml | 29 + .trunk/valestyles/WriteGood/WriteGood.ini | 3 + .trunk/valestyles/WriteGood/meta.json | 4 + .vscode/extensions.json | 3 +- .vscode/launch.json | 2 + .vscode/settings.json | 47 +- .vscode/tasks.json | 9 +- CLAUDE.md | 171 ++--- COMMANDS.md | 9 +- CONTRIBUTING.md | 94 +-- README.md | 79 +- action.yml | 25 +- cmd/cache_apt_pkgs/cmdflags/cmd.go | 117 +++ cmd/cache_apt_pkgs/cmdflags/cmd_test.go | 105 +++ cmd/cache_apt_pkgs/cmdflags/cmds.go | 87 +++ cmd/cache_apt_pkgs/cmdflags/cmds_test.go | 141 ++++ cmd/cache_apt_pkgs/cmdflags/doc.go | 6 + cmd/cache_apt_pkgs/cmdflags/shared.go | 48 ++ cmd/cache_apt_pkgs/doc.go | 4 + cmd/cache_apt_pkgs/main_integration_test.go | 33 + internal/actions/action.go | 357 +++++++++ internal/actions/cache_actions_test.go | 172 +++++ internal/actions/cache_restore.go | 113 +++ internal/actions/cache_save.go | 57 ++ internal/actions/doc.go | 70 ++ internal/cio/ghio.go | 94 +++ internal/cio/print_scanner.go | 148 ++++ internal/cio/print_scanner_test.go | 95 +++ scripts/dev/export_version.sh | 83 +++ scripts/dev/fix_and_update.sh | 36 + scripts/dev/lib.sh | 370 +++++++++ scripts/dev/menu.sh | 161 ++++ scripts/dev/run_local_fake_action.sh | 1 + scripts/dev/setup_dev.sh | 152 ++++ scripts/dev/tests/export_version_test.sh | 64 ++ scripts/dev/tests/setup_dev_test.sh | 48 ++ scripts/dev/update_md_tocs.sh | 81 ++ scripts/dev/update_trunkio.sh | 23 + scripts/distribute.sh | 157 ++++ scripts/lib.sh | 184 ++--- scripts/setup.sh | 146 ++++ scripts/template.sh | 59 +- scripts/template_test.sh | 49 ++ scripts/test_lib.sh | 468 ++++++++++++ 114 files changed, 6573 insertions(+), 789 deletions(-) delete mode 100644 .editorconfig rename .github/workflows/{action_tests.yml => action-tests.yml} (72%) create mode 100644 .github/workflows/build-distribute.yml create mode 100644 .github/workflows/pr.yml delete mode 100644 .trunk/check-ignore delete mode 100644 .trunk/configs/.golangci.yaml delete mode 100644 .trunk/configs/.remarkrc.yaml delete mode 100644 .trunk/configs/Vale/Vocab/Project/accept.txt delete mode 100644 .trunk/configs/analyzers.yml create mode 100644 .trunk/scripts/fix_vale_suggestions.py create mode 100644 .trunk/valestyles/Common/AllowedWords.txt create mode 100644 .trunk/valestyles/Microsoft/AMPM.yml create mode 100644 .trunk/valestyles/Microsoft/Accessibility.yml create mode 100644 .trunk/valestyles/Microsoft/Acronyms.yml create mode 100644 .trunk/valestyles/Microsoft/Adverbs.yml create mode 100644 .trunk/valestyles/Microsoft/Auto.yml create mode 100644 .trunk/valestyles/Microsoft/Avoid.yml create mode 100644 .trunk/valestyles/Microsoft/Contractions.yml create mode 100644 .trunk/valestyles/Microsoft/Dashes.yml create mode 100644 .trunk/valestyles/Microsoft/DateFormat.yml create mode 100644 .trunk/valestyles/Microsoft/DateNumbers.yml create mode 100644 .trunk/valestyles/Microsoft/DateOrder.yml create mode 100644 .trunk/valestyles/Microsoft/Ellipses.yml create mode 100644 .trunk/valestyles/Microsoft/FirstPerson.yml create mode 100644 .trunk/valestyles/Microsoft/Foreign.yml create mode 100644 .trunk/valestyles/Microsoft/Gender.yml create mode 100644 .trunk/valestyles/Microsoft/GenderBias.yml create mode 100644 .trunk/valestyles/Microsoft/GeneralURL.yml create mode 100644 .trunk/valestyles/Microsoft/HeadingAcronyms.yml.disabled create mode 100644 .trunk/valestyles/Microsoft/HeadingColons.yml create mode 100644 .trunk/valestyles/Microsoft/HeadingPunctuation.yml.disabled create mode 100644 .trunk/valestyles/Microsoft/Headings.yml.disabled create mode 100644 .trunk/valestyles/Microsoft/Hyphens.yml create mode 100644 .trunk/valestyles/Microsoft/Microsoft.ini create mode 100644 .trunk/valestyles/Microsoft/Negative.yml create mode 100644 .trunk/valestyles/Microsoft/Ordinal.yml create mode 100644 .trunk/valestyles/Microsoft/OxfordComma.yml create mode 100644 .trunk/valestyles/Microsoft/Passive.yml create mode 100644 .trunk/valestyles/Microsoft/Percentages.yml create mode 100644 .trunk/valestyles/Microsoft/Plurals.yml create mode 100644 .trunk/valestyles/Microsoft/Quotes.yml create mode 100644 .trunk/valestyles/Microsoft/RangeTime.yml create mode 100644 .trunk/valestyles/Microsoft/Semicolon.yml create mode 100644 .trunk/valestyles/Microsoft/SentenceLength.yml create mode 100644 .trunk/valestyles/Microsoft/Spacing.yml create mode 100644 .trunk/valestyles/Microsoft/Suspended.yml create mode 100644 .trunk/valestyles/Microsoft/Terms.yml create mode 100644 .trunk/valestyles/Microsoft/URLFormat.yml create mode 100644 .trunk/valestyles/Microsoft/Units.yml create mode 100644 .trunk/valestyles/Microsoft/Vocab.yml create mode 100644 .trunk/valestyles/Microsoft/We.yml create mode 100644 .trunk/valestyles/Microsoft/Wordiness.yml create mode 100644 .trunk/valestyles/Microsoft/meta.json create mode 100644 .trunk/valestyles/WriteGood/Cliches.yml create mode 100644 .trunk/valestyles/WriteGood/E-Prime.yml create mode 100644 .trunk/valestyles/WriteGood/Illusions.yml create mode 100644 .trunk/valestyles/WriteGood/Passive.yml create mode 100644 .trunk/valestyles/WriteGood/README.md create mode 100644 .trunk/valestyles/WriteGood/So.yml create mode 100644 .trunk/valestyles/WriteGood/ThereIs.yml create mode 100644 .trunk/valestyles/WriteGood/TooWordy.yml create mode 100644 .trunk/valestyles/WriteGood/Weasel.yml create mode 100644 .trunk/valestyles/WriteGood/WriteGood.ini create mode 100644 .trunk/valestyles/WriteGood/meta.json create mode 100644 cmd/cache_apt_pkgs/cmdflags/cmd.go create mode 100644 cmd/cache_apt_pkgs/cmdflags/cmd_test.go create mode 100644 cmd/cache_apt_pkgs/cmdflags/cmds.go create mode 100644 cmd/cache_apt_pkgs/cmdflags/cmds_test.go create mode 100644 cmd/cache_apt_pkgs/cmdflags/doc.go create mode 100644 cmd/cache_apt_pkgs/cmdflags/shared.go create mode 100644 cmd/cache_apt_pkgs/doc.go create mode 100644 internal/actions/action.go create mode 100644 internal/actions/cache_actions_test.go create mode 100644 internal/actions/cache_restore.go create mode 100644 internal/actions/cache_save.go create mode 100644 internal/actions/doc.go create mode 100644 internal/cio/ghio.go create mode 100644 internal/cio/print_scanner.go create mode 100644 internal/cio/print_scanner_test.go create mode 100755 scripts/dev/export_version.sh create mode 100755 scripts/dev/fix_and_update.sh create mode 100755 scripts/dev/lib.sh create mode 100755 scripts/dev/menu.sh create mode 100644 scripts/dev/run_local_fake_action.sh create mode 100755 scripts/dev/setup_dev.sh create mode 100755 scripts/dev/tests/export_version_test.sh create mode 100755 scripts/dev/tests/setup_dev_test.sh create mode 100755 scripts/dev/update_md_tocs.sh create mode 100755 scripts/dev/update_trunkio.sh create mode 100644 scripts/distribute.sh create mode 100644 scripts/setup.sh create mode 100755 scripts/template_test.sh create mode 100755 scripts/test_lib.sh diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f730e63..0000000 --- a/.editorconfig +++ /dev/null @@ -1,37 +0,0 @@ -# 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 diff --git a/.env b/.env index a9dff7d..8973709 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ GO111MODULE=auto -GO_TOOLCHAIN=go1.23.5 -GO_VERSION=1.23.5 +GO_TOOLCHAIN=go1.24 +GO_VERSION=1.24 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7d5512c..2a16509 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,8 +33,8 @@ steps: ### Environment -- **Runner OS**: (e.g., ubuntu-22.04, ubuntu-20.04) -- **Action version**: (e.g., v1.4.2, latest) +- **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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d1bb109..78511a5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,3 @@ 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 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b2ec8a4..76598bb 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,11 +1,9 @@ --- - name: Feature Request about: Suggest an idea or enhancement for this action -title: "\[FEATURE] " +title: "[FEATURE] " labels: enhancement assignees: awalsh128 - --- ## Feature Summary @@ -14,8 +12,8 @@ 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? +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] @@ -31,8 +29,7 @@ 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. +Describe your specific use case and how this feature would benefit you and others. ```yaml # Example workflow showing how the feature would be used diff --git a/.github/workflows/action_tests.yml b/.github/workflows/action-tests.yml similarity index 72% rename from .github/workflows/action_tests.yml rename to .github/workflows/action-tests.yml index 67d54f0..dd9bdde 100644 --- a/.github/workflows/action_tests.yml +++ b/.github/workflows/action-tests.yml @@ -1,46 +1,38 @@ -name: Test Action -permissions: - contents: read -env: - DEBUG: false +name: Action Tests on: - # Manual trigger (no inputs allowed per Trunk rule) workflow_dispatch: + inputs: + debug: + description: "Run in debug mode." + type: boolean + required: false + default: true + repository_dispatch: push: - branches: [dev-v2.0] # Test on pushes to dev branch - paths: - - cmd/** # Only when action code changes - - internal/** # Only when action code changes - - action.yml - - .github/workflows/action_tests.yml pull_request: - branches: [dev-v2.0] # Test on PRs to dev branch - paths: - - cmd/** # Only when action code changes - - internal/** # Only when action code changes - - action.yml - - .github/workflows/action_tests.yml + +env: + DEBUG: ${{ github.event.inputs.debug || false }} + # Test for overrides in built in shell options (regression issue 98). + SHELLOPTS: errexit:pipefail + jobs: list_all_versions: runs-on: ubuntu-latest name: List all package versions (including deps). steps: - # Checkout the code we want to test - - name: Checkout - uses: actions/checkout@v4 - with: - # 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 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot=1.3-1 version: ${{ github.run_id }}-${{ github.run_attempt }}-list_all_versions - debug: false + debug: ${{ env.DEBUG }} - name: Verify - if: "steps.execute.outputs.cache-hit != 'false' || \nsteps.execute.outputs.all-package-version-list != 'fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.2,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1'\n" + if: | + steps.execute.outputs.cache-hit != 'false' || + steps.execute.outputs.all-package-version-list != 'fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.2,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1' run: | echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" echo "package-version-list = ${{ steps.execute.outputs.package-version-list }}" @@ -49,68 +41,81 @@ jobs: diff <(echo "${{ steps.execute.outputs.all-package-version-list }}" ) <(echo "fonts-liberation2=1:2.1.5-3,gir1.2-atk-1.0=2.52.0-1build1,gir1.2-freedesktop=1.80.1-1,gir1.2-gdkpixbuf-2.0=2.42.10+dfsg-3ubuntu3.2,gir1.2-gtk-3.0=3.24.41-4ubuntu1.3,gir1.2-harfbuzz-0.0=8.3.0-2build2,gir1.2-pango-1.0=1.52.1+ds-1build1,graphviz=2.42.2-9ubuntu0.1,libann0=1.1.2+doc-9build1,libblas3=3.12.0-3build1.1,libcdt5=2.42.2-9ubuntu0.1,libcgraph6=2.42.2-9ubuntu0.1,libgts-0.7-5t64=0.7.6+darcs121130-5.2build1,libgts-bin=0.7.6+darcs121130-5.2build1,libgvc6=2.42.2-9ubuntu0.1,libgvpr2=2.42.2-9ubuntu0.1,libharfbuzz-gobject0=8.3.0-2build2,liblab-gamut1=2.42.2-9ubuntu0.1,liblapack3=3.12.0-3build1.1,libpangoxft-1.0-0=1.52.1+ds-1build1,libpathplan4=2.42.2-9ubuntu0.1,python3-cairo=1.25.1-2build2,python3-gi-cairo=3.48.2-1,python3-numpy=1:1.26.4+ds-6ubuntu1,xdot=1.3-1") exit 1 shell: bash + list_versions: runs-on: ubuntu-latest name: List package versions. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice version: ${{ github.run_id }}-${{ github.run_attempt }}-list_versions - debug: false + debug: ${{ env.DEBUG }} - name: Verify - if: steps.execute.outputs.cache-hit != 'false' || steps.execute.outputs.package-version-list != 'rolldice=1.16-1build3,xdot=1.3-1' - run: "echo \"cache-hit = ${{ steps.execute.outputs.cache-hit }}\" \necho \"package-version-list = ${{ steps.execute.outputs.package-version-list }}\"\necho \"diff package-version-list\"\ndiff <(echo \"${{ steps.execute.outputs.package-version-list }}\" ) <(echo \"rolldice=1.16-1build3,xdot=1.3-1\")\nexit 1\n" + if: + steps.execute.outputs.cache-hit != 'false' || steps.execute.outputs.package-version-list + != 'rolldice=1.16-1build3,xdot=1.3-1' + run: | + echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" + echo "package-version-list = ${{ steps.execute.outputs.package-version-list }}" + echo "diff package-version-list" + diff <(echo "${{ steps.execute.outputs.package-version-list }}" ) <(echo "rolldice=1.16-1build3,xdot=1.3-1") + exit 1 shell: bash + standard_workflow_install: runs-on: ubuntu-latest name: Standard workflow install package and cache. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow - debug: false + debug: ${{ env.DEBUG }} - name: Verify if: steps.execute.outputs.cache-hit != 'false' run: | echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + standard_workflow_install_with_new_version: needs: standard_workflow_install runs-on: ubuntu-latest name: Standard workflow packages with new version. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice - version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow_install_with_new_version - debug: false + version: + ${{ github.run_id }}-${{ github.run_attempt + }}-standard_workflow_install_with_new_version + debug: ${{ env.DEBUG }} - name: Verify if: steps.execute.outputs.cache-hit != 'false' run: | echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + standard_workflow_restore: needs: standard_workflow_install runs-on: ubuntu-latest name: Standard workflow restore cached packages. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow @@ -121,15 +126,16 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + standard_workflow_restore_with_packages_out_of_order: needs: standard_workflow_install runs-on: ubuntu-latest name: Standard workflow restore with packages out of order. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: rolldice xdot version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow @@ -140,15 +146,16 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + standard_workflow_add_package: needs: standard_workflow_install runs-on: ubuntu-latest name: Standard workflow add another package. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice distro-info-data version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow @@ -159,15 +166,16 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + standard_workflow_restore_add_package: needs: standard_workflow_add_package runs-on: ubuntu-latest name: Standard workflow restore added package. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice distro-info-data version: ${{ github.run_id }}-${{ github.run_attempt }}-standard_workflow @@ -178,13 +186,14 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + no_packages: runs-on: ubuntu-latest name: No packages passed. steps: - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: "" continue-on-error: true @@ -192,13 +201,14 @@ jobs: if: steps.execute.outcome == 'failure' run: exit 0 shell: bash + package_not_found: runs-on: ubuntu-latest name: Package not found. steps: - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: package_that_doesnt_exist continue-on-error: true @@ -206,13 +216,14 @@ jobs: if: steps.execute.outcome == 'failure' run: exit 0 shell: bash + version_contains_spaces: runs-on: ubuntu-latest name: Version contains spaces. steps: - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot version: 123 abc @@ -222,61 +233,67 @@ jobs: if: steps.execute.outcome == 'failure' run: exit 0 shell: bash + regression_36: runs-on: ubuntu-latest name: "Reinstall existing package (regression issue #36)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libgtk-3-dev version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_36 debug: ${{ env.DEBUG }} + regression_37: runs-on: ubuntu-latest name: "Install with reported package dependencies not installed (regression issue #37)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libosmesa6-dev libgl1-mesa-dev python3-tk pandoc git-restore-mtime version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_37 debug: ${{ env.DEBUG }} + debug_disabled: runs-on: ubuntu-latest name: Debug disabled. steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot version: ${{ github.run_id }}-${{ github.run_attempt }}-list-all-package-versions - debug: false + debug: ${{ env.DEBUG }} + regression_72_1: runs-on: ubuntu-latest name: "Cache Java CA certs package v1 (regression issue #72)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: openjdk-11-jre version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_72 debug: ${{ env.DEBUG }} + regression_72_2: runs-on: ubuntu-latest name: "Cache Java CA certs package v2 (regression issue #72)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: default-jre version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_72 debug: ${{ env.DEBUG }} + regression_76: runs-on: ubuntu-latest name: "Cache empty archive (regression issue #76)." steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - run: | sudo wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null; echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | sudo tee /etc/apt/sources.list.d/oneAPI.list; @@ -284,52 +301,57 @@ jobs: sudo apt-get install -y intel-oneapi-runtime-libs intel-oneapi-runtime-opencl; sudo apt-get install -y opencl-headers ocl-icd-opencl-dev; sudo apt-get install -y libsundials-dev; - - uses: ./ + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: intel-oneapi-runtime-libs version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_76 debug: ${{ env.DEBUG }} + regression_79: runs-on: ubuntu-latest name: "Tar error with libboost-dev (regression issue #79)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libboost-dev version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_79 debug: ${{ env.DEBUG }} + regression_81: runs-on: ubuntu-latest name: "Tar error with alsa-ucm-conf (regression issue #81)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: - packages: libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcups2 libdrm2 libgbm1 libnspr4 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 + packages: + libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcups2 libdrm2 libgbm1 + libnspr4 libnss3 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_81 debug: ${{ env.DEBUG }} + regression_84_literal_block_install: runs-on: ubuntu-latest name: "Install multiline package listing using literal block style (regression issue #84)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: > xdot rolldice distro-info-data - version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_84_literal_block debug: ${{ env.DEBUG }} + regression_84_literal_block_restore: needs: regression_84_literal_block_install runs-on: ubuntu-latest name: "Restore multiline package listing using literal block style (regression issue #84)." steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice distro-info-data version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_84_literal_block @@ -340,27 +362,29 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + regression_84_folded_block_install: runs-on: ubuntu-latest name: "Install multiline package listing using literal block style (regression issue #84)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: | xdot \ rolldice distro-info-data version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_84_folded_block debug: ${{ env.DEBUG }} + regression_84_folded_block_restore: needs: regression_84_folded_block_install runs-on: ubuntu-latest name: "Restore multiline package listing using literal block style (regression issue #84)." steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3.1.0 - name: Execute id: execute - uses: ./ + uses: awalsh128/cache-apt-pkgs-action@master with: packages: xdot rolldice distro-info-data version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_84_folded_block @@ -371,73 +395,69 @@ jobs: echo "cache-hit = ${{ steps.execute.outputs.cache-hit }}" exit 1 shell: bash + regression_89: runs-on: ubuntu-latest name: "Upload logs artifact name (regression issue #89)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libgtk-3-dev:amd64 version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_89 debug: ${{ env.DEBUG }} + regression_98: runs-on: ubuntu-latest name: "Install error due to SHELLOPTS override (regression issue #98)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: git-restore-mtime libgl1-mesa-dev libosmesa6-dev pandoc version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_98 debug: ${{ env.DEBUG }} + regression_106_install: runs-on: ubuntu-latest name: "Stale apt repo not finding package on restore, install phase (regression issue #106)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libtk8.6 version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_106 debug: ${{ env.DEBUG }} + regression_106_restore: needs: regression_106_install runs-on: ubuntu-latest name: "Stale apt repo not finding package on restore, restore phase (regression issue #106)." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libtk8.6 version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_106 debug: ${{ env.DEBUG }} - regression_159_install: - runs-on: ubuntu-latest - name: apt-show false positive parsing Package line (regression issue #159). - steps: - - uses: actions/checkout@v4 - - uses: ./ - with: - packages: texlive-latex-extra - version: ${{ github.run_id }}-${{ github.run_attempt }}-regression_159 - debug: ${{ env.DEBUG }} + multi_arch_cache_key: runs-on: ubuntu-latest - name: Cache packages with multi-arch cache key. + name: "Cache packages with multi-arch cache key." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libfuse2 version: ${{ github.run_id }}-${{ github.run_attempt }}-multi_arch_cache_key debug: ${{ env.DEBUG }} + virtual_package: runs-on: ubuntu-latest - name: Cache virtual package. + name: "Cache virtual package." steps: - - uses: actions/checkout@v4 - - uses: ./ + - uses: actions/checkout@v3.1.0 + - uses: awalsh128/cache-apt-pkgs-action@master with: packages: libvips version: ${{ github.run_id }}-${{ github.run_attempt }}-virtual_package diff --git a/.github/workflows/build-distribute.yml b/.github/workflows/build-distribute.yml new file mode 100644 index 0000000..dacacfb --- /dev/null +++ b/.github/workflows/build-distribute.yml @@ -0,0 +1,116 @@ +name: Build and Release Distribute Artifacts +on: + push: + tags: + - "v2.*.*" + branches: + - dev-v2 +permissions: + contents: write + id-token: write +env: + VERSION_PREFIX: "commit" +jobs: + build-and-release: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + arch: x64 + - goos: linux + goarch: arm64 + arch: arm64 + - goos: linux + goarch: arm + goarch_variant: "6" + arch: arm + - goos: linux + goarch: "386" + arch: x86 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.24 + - name: Generate version from commit SHA + id: version + run: ./scripts/distribute.sh generate-version + - name: Create distribute directory + run: ./scripts/distribute.sh create-distribute-directory "${{ matrix.arch }}" + - name: Clone apt-fast repository + run: ./scripts/distribute.sh clone-apt-fast + - name: Build binary for ${{ matrix.goos }}/${{ matrix.goarch }} + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + GOARM: ${{ matrix.goarch_variant }} + CGO_ENABLED: 0 + run: | + ./scripts/distribute.sh build-binary \ + "${{ matrix.goos }}" \ + "${{ matrix.goarch }}" \ + "${{ matrix.goarch_variant }}" \ + "${{ matrix.arch }}" + - name: Generate checksums + run: ./scripts/distribute.sh generate-checksums "${{ matrix.arch }}" + - name: Verify build + run: | + ./scripts/distribute.sh verify-build "${{ matrix.arch }}" + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: cache-apt-pkgs-${{ matrix.arch }}-${{ steps.version.outputs.commit_sha }} + path: distribute/${{ matrix.arch }}/* + retention-days: 30 + create-release: + needs: build-and-release + runs-on: ubuntu-latest + if: success() + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Generate version from commit SHA + id: version + run: | + COMMIT_SHA="${GITHUB_SHA:0:8}" + VERSION="${{ env.VERSION_PREFIX }}-${COMMIT_SHA}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: distribute-artifacts + - name: Reorganize artifacts + run: | + ./scripts/distribute.sh reorganize-artifacts + - name: Create or update release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.version.outputs.version }} + name: "${{ steps.version.outputs.version }}" + generate_release_notes: true + files: | + distribute/x64/* + distribute/arm64/* + distribute/arm/* + distribute/x86/* + draft: false + prerelease: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Clean up old pre-releases + run: | + echo "Cleaning up old commit-based releases..." + # Keep last 10 commit-based releases, delete older ones + gh release list --limit 50 --json tagName,isPrerelease | \ + jq -r '.[] | select(.isPrerelease == true and (.tagName | startswith("commit-"))) | .tagName' | \ + tail -n +11 | \ + xargs -I {} gh release delete {} --yes --cleanup-tag || true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..4ffbcc7 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,23 @@ +name: Pull Request +# trunk-ignore(yamllint/truthy) +on: [push, pull_request] +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: read-all + +jobs: + trunk_check: + name: Trunk Code Quality Runner + runs-on: ubuntu-latest + permissions: + checks: write # For trunk to post annotations + contents: read # For repo checkout + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Trunk Code Quality + uses: trunk-io/trunk-action@v1 diff --git a/.trunk/check-ignore b/.trunk/check-ignore deleted file mode 100644 index 795d828..0000000 --- a/.trunk/check-ignore +++ /dev/null @@ -1 +0,0 @@ -dist/**/* diff --git a/.trunk/configs/.golangci.yaml b/.trunk/configs/.golangci.yaml deleted file mode 100644 index b272247..0000000 --- a/.trunk/configs/.golangci.yaml +++ /dev/null @@ -1,79 +0,0 @@ -version: "2" -formatters: - enable: - - gofumpt # formats Go code - - goimports # formats imports and does everything that gofmt does -linters: - enable: - - asasalint # checks for pass []any as any in variadic func(...any) - - asciicheck # checks that your code does not contain non-ASCII identifiers - - bidichk # checks for dangerous unicode character sequences - - bodyclose # checks whether HTTP response body is closed successfully - - containedctx # detects struct contained context.Context field - - contextcheck # checks the function whether use a non-inherited context - - cyclop # checks function and package cyclomatic complexity - - decorder # checks declaration order and count of types, constants, variables and functions - - dogsled # checks assignments with too many blank identifiers - - dupl # checks code clone duplication - - durationcheck # checks for two durations multiplied together - - errcheck # checks unchecked errors - - 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 - - errorlint # finds code that will cause problems with the error wrapping scheme - - exhaustive # checks exhaustiveness of enum switch statements - - forcetypeassert # finds forced type assertions - - funlen # checks for long functions - - gocheckcompilerdirectives # validates go compiler directive comments - - gochecknoglobals # checks that no global variables exist - - gochecknoinits # checks that no init functions are present - - gocognit # computes and checks the cognitive complexity - - goconst # finds repeated strings that could be replaced by a constant - - gocritic # provides diagnostics that check for bugs, performance and style issues - - gocyclo # checks cyclomatic complexity - - godot # checks if comments end in a period - - godox # detects FIXME, TODO and other comment keywords - - goheader # checks is file header matches to pattern - - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - - gomodguard # allows to specify a list of forbidden modules - - goprintffuncname # checks that printf-like functions are named with f at the end - - gosec # inspects source code for security problems - - govet # reports suspicious constructs - - grouper # analyzes expression groups - - importas # enforces consistent import aliases - - ineffassign # detects when assignments to existing variables are not used - - interfacebloat # checks the number of methods inside an interface - - ireturn # accept interfaces, return concrete types - - lll # reports long lines - - loggercheck # checks key value pairs for common logger libraries - - maintidx # measures the maintainability index of each function - - makezero # finds slice declarations with non-zero initial length - - misspell # finds commonly misspelled English words - - nakedret # finds naked returns - - nestif # reports deeply nested if statements - - 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 - - nlreturn # checks for a new line before return and branch statements - - noctx # finds sending http request without context.Context - - nolintlint # reports ill-formed or insufficient nolint directives - - nonamedreturns # reports all named returns - - 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 - - prealloc # finds slice declarations that could potentially be pre-allocated - - predeclared # finds code that shadows one of Go's predeclared identifiers - - promlinter # checks Prometheus metrics naming via promlint - - reassign # checks that package variables are not reassigned - - revive # fast, configurable, extensible, flexible, and beautiful linter for Go - - rowserrcheck # checks whether Err of rows is checked successfully - - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - staticcheck # comprehensive checks for bugs and inefficiencies - - testableexamples # checks if examples are testable - - testpackage # makes you use a separate _test package - - thelper # detects golang test helpers without t.Helper() - - tparallel # detects inappropriate usage of t.Parallel() - - unconvert # removes unnecessary type conversions - - unparam # reports unused function parameters - - unused # checks for unused constants, variables, functions and types - - 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 - - wastedassign # finds wasted assignment statements - - whitespace # detects leading and trailing whitespace diff --git a/.trunk/configs/.markdownlint.yaml b/.trunk/configs/.markdownlint.yaml index 870502e..b40ee9d 100644 --- a/.trunk/configs/.markdownlint.yaml +++ b/.trunk/configs/.markdownlint.yaml @@ -1,41 +1,2 @@ -# Enable all rules by default -default: true -# Markdown linting configuration with all rules enabled +# Prettier friendly markdownlint config (all formatting rules disabled) 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 diff --git a/.trunk/configs/.remarkrc.yaml b/.trunk/configs/.remarkrc.yaml deleted file mode 100644 index e8c7321..0000000 --- a/.trunk/configs/.remarkrc.yaml +++ /dev/null @@ -1,8 +0,0 @@ -plugins: - remark-preset-lint-consistent: true - remark-preset-lint-recommended: true - remark-lint-list-item-indent: true - # Allow ATX-style headings (using #). Previously Trunk/remark was expecting - # setext-style for certain heading levels which caused the "Unexpected ATX - # heading, expected setext" errors. Setting this rule to 'atx' relaxes that. - remark-lint-heading-style: [true, "atx"] diff --git a/.trunk/configs/.shellcheckrc b/.trunk/configs/.shellcheckrc index 11ce192..8c7b1ad 100644 --- a/.trunk/configs/.shellcheckrc +++ b/.trunk/configs/.shellcheckrc @@ -1,7 +1,7 @@ enable=all source-path=SCRIPTDIR -disable=SC1090 -disable=SC1091 disable=SC2154 -disable=SC2310 -disable=SC2312 \ No newline at end of file + +# If you're having issues with shellcheck following source, disable the errors via: +# disable=SC1090 +# disable=SC1091 diff --git a/.trunk/configs/.vale.ini b/.trunk/configs/.vale.ini index b19f154..558d340 100644 --- a/.trunk/configs/.vale.ini +++ b/.trunk/configs/.vale.ini @@ -1,11 +1,12 @@ -[formats] -markdoc = md +# Repo-root Vale configuration for Trunk +StylesPath = .trunk/valestyles +MinAlertLevel = suggestion [*.md] -BasedOnStyles = Vale +BasedOnStyles = Common, WriteGood, Microsoft +Ignore = .github/ISSUE_TEMPLATE/** CLAUDE.md [*] -vocab = Project - -# Disable spelling checks for technical terms -Vale.Spelling = NO +# Apply Code style to other files for comments +BasedOnStyles = Common +Ignore = **/.git/** .trunk/** diff --git a/.trunk/configs/.yamllint.yaml b/.trunk/configs/.yamllint.yaml index 39b8b7a..ef9ffaa 100644 --- a/.trunk/configs/.yamllint.yaml +++ b/.trunk/configs/.yamllint.yaml @@ -1,9 +1,35 @@ +extends: default + rules: - quoted-strings: disable - key-duplicates: {} - octal-values: - forbid-implicit-octal: true + line-length: + max: 100 + level: warning + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + + # Other customizations + comments: + min-spaces-from-content: 1 + + document-start: + present: false + + indentation: + spaces: 2 + indent-sequences: consistent + empty-lines: max: 2 max-start: 0 max-end: 0 + + quoted-strings: disable + key-duplicates: {} + octal-values: + forbid-implicit-octal: true + + # Strict whitespace rules + trailing-spaces: enable + new-line-at-end-of-file: enable + new-lines: + type: unix diff --git a/.trunk/configs/Vale/Vocab/Project/accept.txt b/.trunk/configs/Vale/Vocab/Project/accept.txt deleted file mode 100644 index d0ae16b..0000000 --- a/.trunk/configs/Vale/Vocab/Project/accept.txt +++ /dev/null @@ -1,6 +0,0 @@ -Goroutine -goroutines -Mutex -mutexes -heredoc -Profiler \ No newline at end of file diff --git a/.trunk/configs/analyzers.yml b/.trunk/configs/analyzers.yml deleted file mode 100644 index 76eeefc..0000000 --- a/.trunk/configs/analyzers.yml +++ /dev/null @@ -1,83 +0,0 @@ -# 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 diff --git a/.trunk/scripts/fix_vale_suggestions.py b/.trunk/scripts/fix_vale_suggestions.py new file mode 100644 index 0000000..e69de29 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 473d73f..1d3e7cb 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -6,61 +6,40 @@ cli: # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) plugins: sources: + - id: configs + ref: v1.1.1 + uri: https://github.com/trunk-io/configs - id: trunk ref: v1.7.3 uri: https://github.com/trunk-io/plugins # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) runtimes: enabled: + - go@1.21.0 - node@22.16.0 - python@3.10.8 - - go@1.21.0 # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) lint: disabled: - - deno - - remark-lint - - markdown-table-prettify - - biome + - cspell + - codespell enabled: + - vale@3.12.0 + - checkov@3.2.477 + - dotenv-linter@3.3.0 + - git-diff-check - gofmt@1.20.4 - - gokart@0.5.1 - golangci-lint2@2.5.0 - markdownlint@0.45.0 - osv-scanner@2.2.3 - - actionlint@1.7.7 - - checkov@3.2.477 - - cspell - - dotenv-linter@3.3.0 - - git-diff-check - - gitleaks@8.28.0 - - golangci-lint@1.64.8 - - isort@6.1.0 - - kube-linter@0.7.2 - - ls-lint@2.3.1 - - markdown-link-check@3.13.7 - - markdownlint-cli2@0.18.1 - - oxipng@9.1.5 - - pre-commit-hooks@6.0.0 - prettier@3.6.2 - - semgrep@1.139.0 - - shellcheck@0.11.0 - shfmt@3.6.0 - - snyk@1.1295.0 - - trivy@0.67.1 - - trunk-toolbox@0.5.4 - trufflehog@3.90.8 - - vale@3.12.0 - - yamlfmt@0.17.2 - yamllint@1.37.1 - ignore: - - linters: [markdownlint-cli2] - paths: [".github/ISSUE_TEMPLATE/**"] actions: disabled: - - trunk-fmt-pre-commit - enabled: - trunk-announce - trunk-check-pre-push - # - trunk-fmt-pre-commit + - trunk-fmt-pre-commit + enabled: - trunk-upgrade-available diff --git a/.trunk/valestyles/Common/AllowedWords.txt b/.trunk/valestyles/Common/AllowedWords.txt new file mode 100644 index 0000000..2173772 --- /dev/null +++ b/.trunk/valestyles/Common/AllowedWords.txt @@ -0,0 +1,22 @@ +CLAUDE +gRPC +apt +cache-apt-pkgs +trunk +Trunk +awalsh128 +pkg +EOF + +# Add technical/project tokens +pprof +cpu.prof +mem.prof +block.prof +mutex +goroutine +GC +cpuprofile + +# Regex fragments +lication diff --git a/.trunk/valestyles/Microsoft/AMPM.yml b/.trunk/valestyles/Microsoft/AMPM.yml new file mode 100644 index 0000000..8b9fed1 --- /dev/null +++ b/.trunk/valestyles/Microsoft/AMPM.yml @@ -0,0 +1,9 @@ +extends: existence +message: Use 'AM' or 'PM' (preceded by a space). +link: https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/date-time-terms +level: error +nonword: true +tokens: + - '\d{1,2}[AP]M' + - '\d{1,2} ?[ap]m' + - '\d{1,2} ?[aApP]\.[mM]\.' diff --git a/.trunk/valestyles/Microsoft/Accessibility.yml b/.trunk/valestyles/Microsoft/Accessibility.yml new file mode 100644 index 0000000..f5f4829 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Accessibility.yml @@ -0,0 +1,30 @@ +extends: existence +message: "Don't use language (such as '%s') that defines people by their disability." +link: https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/accessibility-terms +level: suggestion +ignorecase: true +tokens: + - a victim of + - able-bodied + - an epileptic + - birth defect + - crippled + - differently abled + - disabled + - dumb + - handicapped + - handicaps + - healthy person + - hearing-impaired + - lame + - maimed + - mentally handicapped + - missing a limb + - mute + - non-verbal + - normal person + - sight-impaired + - slow learner + - stricken with + - suffers from + - vision-impaired diff --git a/.trunk/valestyles/Microsoft/Acronyms.yml b/.trunk/valestyles/Microsoft/Acronyms.yml new file mode 100644 index 0000000..308ff7c --- /dev/null +++ b/.trunk/valestyles/Microsoft/Acronyms.yml @@ -0,0 +1,64 @@ +extends: conditional +message: "'%s' has no definition." +link: https://docs.microsoft.com/en-us/style-guide/acronyms +level: suggestion +ignorecase: false +# Ensures that the existence of 'first' implies the existence of 'second'. +first: '\b([A-Z]{3,5})\b' +second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)' +# ... with the exception of these: +exceptions: + - API + - ASP + - CLI + - CPU + - CSS + - CSV + - DEBUG + - DOM + - DPI + - FAQ + - GCC + - GDB + - GET + - GPU + - GTK + - GUI + - HTML + - HTTP + - HTTPS + - IDE + - JAR + - JSON + - JSX + - LESS + - LLDB + - NET + - NOTE + - NVDA + - OSS + - PATH + - PDF + - PHP + - POST + - RAM + - REPL + - RSA + - SCM + - SCSS + - SDK + - SQL + - SSH + - SSL + - SVG + - TBD + - TCP + - TODO + - URI + - URL + - USB + - UTF + - XML + - XSS + - YAML + - ZIP diff --git a/.trunk/valestyles/Microsoft/Adverbs.yml b/.trunk/valestyles/Microsoft/Adverbs.yml new file mode 100644 index 0000000..5619f99 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Adverbs.yml @@ -0,0 +1,272 @@ +extends: existence +message: "Remove '%s' if it's not important to the meaning of the statement." +link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-simple-words-concise-sentences +ignorecase: true +level: warning +action: + name: remove +tokens: + - abnormally + - absentmindedly + - accidentally + - adventurously + - anxiously + - arrogantly + - awkwardly + - bashfully + - beautifully + - bitterly + - bleakly + - blindly + - blissfully + - boastfully + - boldly + - bravely + - briefly + - brightly + - briskly + - broadly + - busily + - calmly + - carefully + - carelessly + - cautiously + - cheerfully + - cleverly + - closely + - coaxingly + - colorfully + - continually + - coolly + - courageously + - crossly + - cruelly + - curiously + - daintily + - dearly + - deceivingly + - deeply + - defiantly + - deliberately + - delightfully + - diligently + - dimly + - doubtfully + - dreamily + - easily + - effectively + - elegantly + - energetically + - enormously + - enthusiastically + - excitedly + - extremely + - fairly + - faithfully + - famously + - ferociously + - fervently + - fiercely + - fondly + - foolishly + - fortunately + - frankly + - frantically + - freely + - frenetically + - frightfully + - furiously + - generally + - generously + - gently + - gladly + - gleefully + - gracefully + - gratefully + - greatly + - greedily + - happily + - hastily + - healthily + - heavily + - helplessly + - honestly + - hopelessly + - hungrily + - innocently + - inquisitively + - intensely + - intently + - interestingly + - inwardly + - irritably + - jaggedly + - jealously + - jovially + - joyfully + - joyously + - jubilantly + - judgmentally + - justly + - keenly + - kiddingly + - kindheartedly + - knavishly + - knowingly + - knowledgeably + - lazily + - lightly + - limply + - lively + - loftily + - longingly + - loosely + - loudly + - lovingly + - loyally + - madly + - majestically + - meaningfully + - mechanically + - merrily + - miserably + - mockingly + - mortally + - mysteriously + - naturally + - nearly + - neatly + - nervously + - nicely + - noisily + - obediently + - obnoxiously + - oddly + - offensively + - optimistically + - overconfidently + - painfully + - partially + - patiently + - perfectly + - playfully + - politely + - poorly + - positively + - potentially + - powerfully + - promptly + - properly + - punctually + - quaintly + - queasily + - queerly + - questionably + - quickly + - quietly + - quirkily + - quite + - quizzically + - randomly + - rapidly + - rarely + - readily + - really + - reassuringly + - recklessly + - regularly + - reluctantly + - repeatedly + - reproachfully + - restfully + - righteously + - rightfully + - rigidly + - roughly + - rudely + - safely + - scarcely + - scarily + - searchingly + - sedately + - seemingly + - selfishly + - separately + - seriously + - shakily + - sharply + - sheepishly + - shrilly + - shyly + - silently + - sleepily + - slowly + - smoothly + - softly + - solemnly + - solidly + - speedily + - stealthily + - sternly + - strictly + - suddenly + - supposedly + - surprisingly + - suspiciously + - sweetly + - swiftly + - sympathetically + - tenderly + - tensely + - terribly + - thankfully + - thoroughly + - thoughtfully + - tightly + - tremendously + - triumphantly + - truthfully + - ultimately + - unabashedly + - unaccountably + - unbearably + - unethically + - unexpectedly + - unfortunately + - unimpressively + - unnaturally + - unnecessarily + - urgently + - usefully + - uselessly + - utterly + - vacantly + - vaguely + - vainly + - valiantly + - vastly + - verbally + - very + - viciously + - victoriously + - violently + - vivaciously + - voluntarily + - warmly + - weakly + - wearily + - wetly + - wholly + - wildly + - willfully + - wisely + - woefully + - wonderfully + - worriedly + - yawningly + - yearningly + - yieldingly + - youthfully + - zealously + - zestfully + - zestily diff --git a/.trunk/valestyles/Microsoft/Auto.yml b/.trunk/valestyles/Microsoft/Auto.yml new file mode 100644 index 0000000..4da4393 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Auto.yml @@ -0,0 +1,11 @@ +extends: existence +message: "In general, don't hyphenate '%s'." +link: https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/auto +ignorecase: true +level: error +action: + name: convert + params: + - simple +tokens: + - 'auto-\w+' diff --git a/.trunk/valestyles/Microsoft/Avoid.yml b/.trunk/valestyles/Microsoft/Avoid.yml new file mode 100644 index 0000000..dab7822 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Avoid.yml @@ -0,0 +1,14 @@ +extends: existence +message: "Don't use '%s'. See the A-Z word list for details." +# See the A-Z word list +link: https://docs.microsoft.com/en-us/style-guide +ignorecase: true +level: error +tokens: + - abortion + - and so on + - app(?:lication)?s? (?:developer|program) + - app(?:lication)? file + - backbone + - backend + - contiguous selection diff --git a/.trunk/valestyles/Microsoft/Contractions.yml b/.trunk/valestyles/Microsoft/Contractions.yml new file mode 100644 index 0000000..8c81dcb --- /dev/null +++ b/.trunk/valestyles/Microsoft/Contractions.yml @@ -0,0 +1,50 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-contractions +level: error +ignorecase: true +action: + name: replace +swap: + are not: aren't + cannot: can't + could not: couldn't + did not: didn't + do not: don't + does not: doesn't + has not: hasn't + have not: haven't + how is: how's + is not: isn't + + 'it is(?!\.)': it's + 'it''s(?=\.)': it is + + should not: shouldn't + + "that is(?![.,])": that's + 'that''s(?=\.)': that is + + 'they are(?!\.)': they're + 'they''re(?=\.)': they are + + was not: wasn't + + 'we are(?!\.)': we're + 'we''re(?=\.)': we are + + 'we have(?!\.)': we've + 'we''ve(?=\.)': we have + + were not: weren't + + 'what is(?!\.)': what's + 'what''s(?=\.)': what is + + 'when is(?!\.)': when's + 'when''s(?=\.)': when is + + 'where is(?!\.)': where's + 'where''s(?=\.)': where is + + will not: won't diff --git a/.trunk/valestyles/Microsoft/Dashes.yml b/.trunk/valestyles/Microsoft/Dashes.yml new file mode 100644 index 0000000..72b05ba --- /dev/null +++ b/.trunk/valestyles/Microsoft/Dashes.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Remove the spaces around '%s'." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/dashes-hyphens/emes +ignorecase: true +nonword: true +level: error +action: + name: edit + params: + - trim + - " " +tokens: + - '\s[—–]\s|\s[—–]|[—–]\s' diff --git a/.trunk/valestyles/Microsoft/DateFormat.yml b/.trunk/valestyles/Microsoft/DateFormat.yml new file mode 100644 index 0000000..a472eea --- /dev/null +++ b/.trunk/valestyles/Microsoft/DateFormat.yml @@ -0,0 +1,10 @@ +extends: existence +message: Use 'July 31, 2016' format, not '%s'. +link: https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/date-time-terms +ignorecase: true +level: error +nonword: true +tokens: + - '\d{1,2} + (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)|May|Jun(?:e)|Jul(?:y)|Aug(?:ust)|Sep(?:tember)?|Oct(?:ober)|Nov(?:ember)?|Dec(?:ember)?) + \d{4}' diff --git a/.trunk/valestyles/Microsoft/DateNumbers.yml b/.trunk/valestyles/Microsoft/DateNumbers.yml new file mode 100644 index 0000000..14d4674 --- /dev/null +++ b/.trunk/valestyles/Microsoft/DateNumbers.yml @@ -0,0 +1,40 @@ +extends: existence +message: "Don't use ordinal numbers for dates." +link: https://docs.microsoft.com/en-us/style-guide/numbers#numbers-in-dates +level: error +nonword: true +ignorecase: true +raw: + - \b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)|May|Jun(?:e)|Jul(?:y)|Aug(?:ust)|Sep(?:tember)?|Oct(?:ober)|Nov(?:ember)?|Dec(?:ember)?)\b\s* +tokens: + - first + - second + - third + - fourth + - fifth + - sixth + - seventh + - eighth + - ninth + - tenth + - eleventh + - twelfth + - thirteenth + - fourteenth + - fifteenth + - sixteenth + - seventeenth + - eighteenth + - nineteenth + - twentieth + - twenty-first + - twenty-second + - twenty-third + - twenty-fourth + - twenty-fifth + - twenty-sixth + - twenty-seventh + - twenty-eighth + - twenty-ninth + - thirtieth + - thirty-first diff --git a/.trunk/valestyles/Microsoft/DateOrder.yml b/.trunk/valestyles/Microsoft/DateOrder.yml new file mode 100644 index 0000000..12d69ba --- /dev/null +++ b/.trunk/valestyles/Microsoft/DateOrder.yml @@ -0,0 +1,8 @@ +extends: existence +message: "Always spell out the name of the month." +link: https://docs.microsoft.com/en-us/style-guide/numbers#numbers-in-dates +ignorecase: true +level: error +nonword: true +tokens: + - '\b\d{1,2}/\d{1,2}/(?:\d{4}|\d{2})\b' diff --git a/.trunk/valestyles/Microsoft/Ellipses.yml b/.trunk/valestyles/Microsoft/Ellipses.yml new file mode 100644 index 0000000..320457a --- /dev/null +++ b/.trunk/valestyles/Microsoft/Ellipses.yml @@ -0,0 +1,9 @@ +extends: existence +message: "In general, don't use an ellipsis." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/ellipses +nonword: true +level: warning +action: + name: remove +tokens: + - '\.\.\.' diff --git a/.trunk/valestyles/Microsoft/FirstPerson.yml b/.trunk/valestyles/Microsoft/FirstPerson.yml new file mode 100644 index 0000000..f58dea3 --- /dev/null +++ b/.trunk/valestyles/Microsoft/FirstPerson.yml @@ -0,0 +1,16 @@ +extends: existence +message: "Use first person (such as '%s') sparingly." +link: https://docs.microsoft.com/en-us/style-guide/grammar/person +ignorecase: true +level: warning +nonword: true +tokens: + - (?:^|\s)I(?=\s) + - (?:^|\s)I(?=,\s) + - \bI'd\b + - \bI'll\b + - \bI'm\b + - \bI've\b + - \bme\b + - \bmy\b + - \bmine\b diff --git a/.trunk/valestyles/Microsoft/Foreign.yml b/.trunk/valestyles/Microsoft/Foreign.yml new file mode 100644 index 0000000..0d3d600 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Foreign.yml @@ -0,0 +1,13 @@ +extends: substitution +message: "Use '%s' instead of '%s'." +link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-us-spelling-avoid-non-english-words +ignorecase: true +level: error +nonword: true +action: + name: replace +swap: + '\b(?:eg|e\.g\.)[\s,]': for example + '\b(?:ie|i\.e\.)[\s,]': that is + '\b(?:viz\.)[\s,]': namely + '\b(?:ergo)[\s,]': therefore diff --git a/.trunk/valestyles/Microsoft/Gender.yml b/.trunk/valestyles/Microsoft/Gender.yml new file mode 100644 index 0000000..47c0802 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Gender.yml @@ -0,0 +1,8 @@ +extends: existence +message: "Don't use '%s'." +link: https://github.com/MicrosoftDocs/microsoft-style-guide/blob/master/styleguide/grammar/nouns-pronouns.md#pronouns-and-gender +level: error +ignorecase: true +tokens: + - he/she + - s/he diff --git a/.trunk/valestyles/Microsoft/GenderBias.yml b/.trunk/valestyles/Microsoft/GenderBias.yml new file mode 100644 index 0000000..fc987b9 --- /dev/null +++ b/.trunk/valestyles/Microsoft/GenderBias.yml @@ -0,0 +1,42 @@ +extends: substitution +message: "Consider using '%s' instead of '%s'." +ignorecase: true +level: error +action: + name: replace +swap: + (?:alumna|alumnus): graduate + (?:alumnae|alumni): graduates + air(?:m[ae]n|wom[ae]n): pilot(s) + anchor(?:m[ae]n|wom[ae]n): anchor(s) + authoress: author + camera(?:m[ae]n|wom[ae]n): camera operator(s) + door(?:m[ae]|wom[ae]n): concierge(s) + draft(?:m[ae]n|wom[ae]n): drafter(s) + fire(?:m[ae]n|wom[ae]n): firefighter(s) + fisher(?:m[ae]n|wom[ae]n): fisher(s) + fresh(?:m[ae]n|wom[ae]n): first-year student(s) + garbage(?:m[ae]n|wom[ae]n): waste collector(s) + lady lawyer: lawyer + ladylike: courteous + mail(?:m[ae]n|wom[ae]n): mail carriers + man and wife: husband and wife + man enough: strong enough + mankind: human kind + manmade: manufactured + manpower: personnel + middle(?:m[ae]n|wom[ae]n): intermediary + news(?:m[ae]n|wom[ae]n): journalist(s) + ombuds(?:man|woman): ombuds + oneupmanship: upstaging + poetess: poet + police(?:m[ae]n|wom[ae]n): police officer(s) + repair(?:m[ae]n|wom[ae]n): technician(s) + sales(?:m[ae]n|wom[ae]n): salesperson or sales people + service(?:m[ae]n|wom[ae]n): soldier(s) + steward(?:ess)?: flight attendant + tribes(?:m[ae]n|wom[ae]n): tribe member(s) + waitress: waiter + woman doctor: doctor + woman scientist[s]?: scientist(s) + work(?:m[ae]n|wom[ae]n): worker(s) diff --git a/.trunk/valestyles/Microsoft/GeneralURL.yml b/.trunk/valestyles/Microsoft/GeneralURL.yml new file mode 100644 index 0000000..dcef503 --- /dev/null +++ b/.trunk/valestyles/Microsoft/GeneralURL.yml @@ -0,0 +1,11 @@ +extends: existence +message: "For a general audience, use 'address' rather than 'URL'." +link: https://docs.microsoft.com/en-us/style-guide/urls-web-addresses +level: warning +action: + name: replace + params: + - URL + - address +tokens: + - URL diff --git a/.trunk/valestyles/Microsoft/HeadingAcronyms.yml.disabled b/.trunk/valestyles/Microsoft/HeadingAcronyms.yml.disabled new file mode 100644 index 0000000..9dc3b6c --- /dev/null +++ b/.trunk/valestyles/Microsoft/HeadingAcronyms.yml.disabled @@ -0,0 +1,7 @@ +extends: existence +message: "Avoid using acronyms in a title or heading." +link: https://docs.microsoft.com/en-us/style-guide/acronyms#be-careful-with-acronyms-in-titles-and-headings +level: warning +scope: heading +tokens: + - '[A-Z]{2,4}' diff --git a/.trunk/valestyles/Microsoft/HeadingColons.yml b/.trunk/valestyles/Microsoft/HeadingColons.yml new file mode 100644 index 0000000..7013c39 --- /dev/null +++ b/.trunk/valestyles/Microsoft/HeadingColons.yml @@ -0,0 +1,8 @@ +extends: existence +message: "Capitalize '%s'." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/colons +nonword: true +level: error +scope: heading +tokens: + - ':\s[a-z]' diff --git a/.trunk/valestyles/Microsoft/HeadingPunctuation.yml.disabled b/.trunk/valestyles/Microsoft/HeadingPunctuation.yml.disabled new file mode 100644 index 0000000..4954cb1 --- /dev/null +++ b/.trunk/valestyles/Microsoft/HeadingPunctuation.yml.disabled @@ -0,0 +1,13 @@ +extends: existence +message: "Don't use end punctuation in headings." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/periods +nonword: true +level: warning +scope: heading +action: + name: edit + params: + - trim_right + - ".?!" +tokens: + - "[a-z][.?!]$" diff --git a/.trunk/valestyles/Microsoft/Headings.yml.disabled b/.trunk/valestyles/Microsoft/Headings.yml.disabled new file mode 100644 index 0000000..63624ed --- /dev/null +++ b/.trunk/valestyles/Microsoft/Headings.yml.disabled @@ -0,0 +1,28 @@ +extends: capitalization +message: "'%s' should use sentence-style capitalization." +link: https://docs.microsoft.com/en-us/style-guide/capitalization +level: suggestion +scope: heading +match: $sentence +indicators: + - ':' +exceptions: + - Azure + - CLI + - Code + - Cosmos + - Docker + - Emmet + - I + - Kubernetes + - Linux + - macOS + - Marketplace + - MongoDB + - REPL + - Studio + - TypeScript + - URLs + - Visual + - VS + - Windows diff --git a/.trunk/valestyles/Microsoft/Hyphens.yml b/.trunk/valestyles/Microsoft/Hyphens.yml new file mode 100644 index 0000000..7e5731c --- /dev/null +++ b/.trunk/valestyles/Microsoft/Hyphens.yml @@ -0,0 +1,14 @@ +extends: existence +message: "'%s' doesn't need a hyphen." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/dashes-hyphens/hyphens +level: warning +ignorecase: false +nonword: true +action: + name: edit + params: + - regex + - "-" + - " " +tokens: + - '\b[^\s-]+ly-\w+\b' diff --git a/.trunk/valestyles/Microsoft/Microsoft.ini b/.trunk/valestyles/Microsoft/Microsoft.ini new file mode 100644 index 0000000..6a5e2d3 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Microsoft.ini @@ -0,0 +1,2 @@ +# Minimal placeholder for Microsoft-style rules +version = 1 diff --git a/.trunk/valestyles/Microsoft/Negative.yml b/.trunk/valestyles/Microsoft/Negative.yml new file mode 100644 index 0000000..d73221f --- /dev/null +++ b/.trunk/valestyles/Microsoft/Negative.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Form a negative number with an en dash, not a hyphen." +link: https://docs.microsoft.com/en-us/style-guide/numbers +nonword: true +level: error +action: + name: edit + params: + - regex + - "-" + - "–" +tokens: + - '(?<=\s)-\d+(?:\.\d+)?\b' diff --git a/.trunk/valestyles/Microsoft/Ordinal.yml b/.trunk/valestyles/Microsoft/Ordinal.yml new file mode 100644 index 0000000..e3483e3 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Ordinal.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Don't add -ly to an ordinal number." +link: https://docs.microsoft.com/en-us/style-guide/numbers +level: error +action: + name: edit + params: + - trim + - ly +tokens: + - firstly + - secondly + - thirdly diff --git a/.trunk/valestyles/Microsoft/OxfordComma.yml b/.trunk/valestyles/Microsoft/OxfordComma.yml new file mode 100644 index 0000000..493b55c --- /dev/null +++ b/.trunk/valestyles/Microsoft/OxfordComma.yml @@ -0,0 +1,8 @@ +extends: existence +message: "Use the Oxford comma in '%s'." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/commas +scope: sentence +level: suggestion +nonword: true +tokens: + - '(?:[^\s,]+,){1,} \w+ (?:and|or) \w+[.?!]' diff --git a/.trunk/valestyles/Microsoft/Passive.yml b/.trunk/valestyles/Microsoft/Passive.yml new file mode 100644 index 0000000..102d377 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Passive.yml @@ -0,0 +1,183 @@ +extends: existence +message: "'%s' looks like passive voice." +ignorecase: true +level: suggestion +raw: + - \b(am|are|were|being|is|been|was|be)\b\s* +tokens: + - '[\w]+ed' + - awoken + - beat + - become + - been + - begun + - bent + - beset + - bet + - bid + - bidden + - bitten + - bled + - blown + - born + - bought + - bound + - bred + - broadcast + - broken + - brought + - built + - burnt + - burst + - cast + - caught + - chosen + - clung + - come + - cost + - crept + - cut + - dealt + - dived + - done + - drawn + - dreamt + - driven + - drunk + - dug + - eaten + - fallen + - fed + - felt + - fit + - fled + - flown + - flung + - forbidden + - foregone + - forgiven + - forgotten + - forsaken + - fought + - found + - frozen + - given + - gone + - gotten + - ground + - grown + - heard + - held + - hidden + - hit + - hung + - hurt + - kept + - knelt + - knit + - known + - laid + - lain + - leapt + - learnt + - led + - left + - lent + - let + - lighted + - lost + - made + - meant + - met + - misspelt + - mistaken + - mown + - overcome + - overdone + - overtaken + - overthrown + - paid + - pled + - proven + - put + - quit + - read + - rid + - ridden + - risen + - run + - rung + - said + - sat + - sawn + - seen + - sent + - set + - sewn + - shaken + - shaven + - shed + - shod + - shone + - shorn + - shot + - shown + - shrunk + - shut + - slain + - slept + - slid + - slit + - slung + - smitten + - sold + - sought + - sown + - sped + - spent + - spilt + - spit + - split + - spoken + - spread + - sprung + - spun + - stolen + - stood + - stridden + - striven + - struck + - strung + - stuck + - stung + - stunk + - sung + - sunk + - swept + - swollen + - sworn + - swum + - swung + - taken + - taught + - thought + - thrived + - thrown + - thrust + - told + - torn + - trodden + - understood + - upheld + - upset + - wed + - wept + - withheld + - withstood + - woken + - won + - worn + - wound + - woven + - written + - wrung diff --git a/.trunk/valestyles/Microsoft/Percentages.yml b/.trunk/valestyles/Microsoft/Percentages.yml new file mode 100644 index 0000000..b68a736 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Percentages.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Use a numeral plus the units." +link: https://docs.microsoft.com/en-us/style-guide/numbers +nonword: true +level: error +tokens: + - '\b[a-zA-z]+\spercent\b' diff --git a/.trunk/valestyles/Microsoft/Plurals.yml b/.trunk/valestyles/Microsoft/Plurals.yml new file mode 100644 index 0000000..1bb6660 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Plurals.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Don't add '%s' to a singular noun. Use plural instead." +ignorecase: true +level: error +link: https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/s/s-es +raw: + - '\(s\)|\(es\)' diff --git a/.trunk/valestyles/Microsoft/Quotes.yml b/.trunk/valestyles/Microsoft/Quotes.yml new file mode 100644 index 0000000..37fb92a --- /dev/null +++ b/.trunk/valestyles/Microsoft/Quotes.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Punctuation should be inside the quotes." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/quotation-marks +level: error +nonword: true +tokens: + - '["“][^"”“]+["”][.,]' diff --git a/.trunk/valestyles/Microsoft/RangeTime.yml b/.trunk/valestyles/Microsoft/RangeTime.yml new file mode 100644 index 0000000..72d8bbf --- /dev/null +++ b/.trunk/valestyles/Microsoft/RangeTime.yml @@ -0,0 +1,13 @@ +extends: existence +message: "Use 'to' instead of a dash in '%s'." +link: https://docs.microsoft.com/en-us/style-guide/numbers +nonword: true +level: error +action: + name: edit + params: + - regex + - "[-–]" + - "to" +tokens: + - '\b(?:AM|PM)\s?[-–]\s?.+(?:AM|PM)\b' diff --git a/.trunk/valestyles/Microsoft/Semicolon.yml b/.trunk/valestyles/Microsoft/Semicolon.yml new file mode 100644 index 0000000..c6526ff --- /dev/null +++ b/.trunk/valestyles/Microsoft/Semicolon.yml @@ -0,0 +1,8 @@ +extends: existence +message: "Try to simplify this sentence." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/semicolons +nonword: true +scope: sentence +level: suggestion +tokens: + - ";" diff --git a/.trunk/valestyles/Microsoft/SentenceLength.yml b/.trunk/valestyles/Microsoft/SentenceLength.yml new file mode 100644 index 0000000..82e2656 --- /dev/null +++ b/.trunk/valestyles/Microsoft/SentenceLength.yml @@ -0,0 +1,6 @@ +extends: occurrence +message: "Try to keep sentences short (< 30 words)." +scope: sentence +level: suggestion +max: 30 +token: \b(\w+)\b diff --git a/.trunk/valestyles/Microsoft/Spacing.yml b/.trunk/valestyles/Microsoft/Spacing.yml new file mode 100644 index 0000000..c8b5236 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Spacing.yml @@ -0,0 +1,8 @@ +extends: existence +message: "'%s' should have one space." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/periods +level: error +nonword: true +tokens: + - "[a-z][.?!] {2,}[A-Z]" + - "[a-z][.?!][A-Z]" diff --git a/.trunk/valestyles/Microsoft/Suspended.yml b/.trunk/valestyles/Microsoft/Suspended.yml new file mode 100644 index 0000000..7282e9c --- /dev/null +++ b/.trunk/valestyles/Microsoft/Suspended.yml @@ -0,0 +1,7 @@ +extends: existence +message: "Don't use '%s' unless space is limited." +link: https://docs.microsoft.com/en-us/style-guide/punctuation/dashes-hyphens/hyphens +ignorecase: true +level: warning +tokens: + - '\w+- and \w+-' diff --git a/.trunk/valestyles/Microsoft/Terms.yml b/.trunk/valestyles/Microsoft/Terms.yml new file mode 100644 index 0000000..65fca10 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Terms.yml @@ -0,0 +1,42 @@ +extends: substitution +message: "Prefer '%s' over '%s'." +# term preference should be based on microsoft style guide, such as +link: https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/adapter +level: warning +ignorecase: true +action: + name: replace +swap: + "(?:agent|virtual assistant|intelligent personal assistant)": personal digital assistant + "(?:assembler|machine language)": assembly language + "(?:drive C:|drive C>|C: drive)": drive C + "(?:internet bot|web robot)s?": bot(s) + "(?:microsoft cloud|the cloud)": cloud + "(?:mobile|smart) ?phone": phone + "24/7": every day + "audio(?:-| )book": audiobook + "back(?:-| )light": backlight + "chat ?bots?": chatbot(s) + adaptor: adapter + administrate: administer + afterwards: afterward + alphabetic: alphabetical + alphanumerical: alphanumeric + an URL: a URL + anti-aliasing: antialiasing + anti-malware: antimalware + anti-spyware: antispyware + anti-virus: antivirus + appendixes: appendices + artificial intelligence: AI + caap: CaaP + conversation-as-a-platform: conversation as a platform + eb: EB + gb: GB + gbps: Gbps + kb: KB + keypress: keystroke + mb: MB + pb: PB + tb: TB + zb: ZB diff --git a/.trunk/valestyles/Microsoft/URLFormat.yml b/.trunk/valestyles/Microsoft/URLFormat.yml new file mode 100644 index 0000000..4e24aa5 --- /dev/null +++ b/.trunk/valestyles/Microsoft/URLFormat.yml @@ -0,0 +1,9 @@ +extends: substitution +message: Use 'of' (not 'for') to describe the relationship of the word URL to a resource. +ignorecase: true +link: https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/u/url +level: suggestion +action: + name: replace +swap: + URL for: URL of diff --git a/.trunk/valestyles/Microsoft/Units.yml b/.trunk/valestyles/Microsoft/Units.yml new file mode 100644 index 0000000..4e3d71e --- /dev/null +++ b/.trunk/valestyles/Microsoft/Units.yml @@ -0,0 +1,16 @@ +extends: existence +message: "Don't spell out the number in '%s'." +link: https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/units-of-measure-terms +level: error +raw: + - '[a-zA-Z]+\s' +tokens: + - "(?:centi|milli)?meters" + - "(?:kilo)?grams" + - "(?:kilo)?meters" + - "(?:mega)?pixels" + - cm + - inches + - lb + - miles + - pounds diff --git a/.trunk/valestyles/Microsoft/Vocab.yml b/.trunk/valestyles/Microsoft/Vocab.yml new file mode 100644 index 0000000..73b5cd1 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Vocab.yml @@ -0,0 +1,25 @@ +extends: existence +message: "Verify your use of '%s' with the A-Z word list." +link: "https://docs.microsoft.com/en-us/style-guide" +level: suggestion +ignorecase: true +tokens: + - above + - accessible + - actionable + - against + - alarm + - alert + - alias + - allows? + - and/or + - as well as + - assure + - author + - avg + - beta + - ensure + - he + - insure + - sample + - she diff --git a/.trunk/valestyles/Microsoft/We.yml b/.trunk/valestyles/Microsoft/We.yml new file mode 100644 index 0000000..97c901c --- /dev/null +++ b/.trunk/valestyles/Microsoft/We.yml @@ -0,0 +1,11 @@ +extends: existence +message: "Try to avoid using first-person plural like '%s'." +link: https://docs.microsoft.com/en-us/style-guide/grammar/person#avoid-first-person-plural +level: warning +ignorecase: true +tokens: + - we + - we'(?:ve|re) + - ours? + - us + - let's diff --git a/.trunk/valestyles/Microsoft/Wordiness.yml b/.trunk/valestyles/Microsoft/Wordiness.yml new file mode 100644 index 0000000..8a4fea7 --- /dev/null +++ b/.trunk/valestyles/Microsoft/Wordiness.yml @@ -0,0 +1,127 @@ +extends: substitution +message: "Consider using '%s' instead of '%s'." +link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-simple-words-concise-sentences +ignorecase: true +level: suggestion +action: + name: replace +swap: + "sufficient number(?: of)?": enough + (?:extract|take away|eliminate): remove + (?:in order to|as a means to): to + (?:inform|let me know): tell + (?:previous|prior) to: before + (?:utilize|make use of): use + a (?:large)? majority of: most + a (?:large)? number of: many + a myriad of: myriad + adversely impact: hurt + all across: across + all of a sudden: suddenly + all of these: these + all of(?! a sudden| these): all + all-time record: record + almost all: most + almost never: seldom + along the lines of: similar to + an adequate number of: enough + an appreciable number of: many + an estimated: about + any and all: all + are in agreement: agree + as a matter of fact: in fact + as a means of: to + as a result of: because of + as of yet: yet + as per: per + at a later date: later + at all times: always + at the present time: now + at this point in time: at this point + based in large part on: based on + based on the fact that: because + basic necessity: necessity + because of the fact that: because + came to a realization: realized + came to an abrupt end: ended abruptly + carry out an evaluation of: evaluate + close down: close + closed down: closed + complete stranger: stranger + completely separate: separate + concerning the matter of: regarding + conduct a review of: review + conduct an investigation: investigate + conduct experiments: experiment + continue on: continue + despite the fact that: although + disappear from sight: disappear + doomed to fail: doomed + drag and drop: drag + drag-and-drop: drag + due to the fact that: because + during the period of: during + during the time that: while + emergency situation: emergency + establish connectivity: connect + except when: unless + excessive number: too many + extend an invitation: invite + fall down: fall + fell down: fell + for the duration of: during + gather together: gather + has the ability to: can + has the capacity to: can + has the opportunity to: could + hold a meeting: meet + if this is not the case: if not + in a careful manner: carefully + in a thoughtful manner: thoughtfully + in a timely manner: timely + in addition: also + in an effort to: to + in between: between + in lieu of: instead of + in many cases: often + in most cases: usually + in order to: to + in some cases: sometimes + in spite of the fact that: although + in spite of: despite + in the (?:very)? near future: soon + in the event that: if + in the neighborhood of: roughly + in the vicinity of: close to + it would appear that: apparently + lift up: lift + made reference to: referred to + make reference to: refer to + mix together: mix + none at all: none + not in a position to: unable + not possible: impossible + of major importance: important + perform an assessment of: assess + pertaining to: about + place an order: order + plays a key role in: is essential to + present time: now + readily apparent: apparent + some of the: some + span across: span + subsequent to: after + successfully complete: complete + take action: act + take into account: consider + the question as to whether: whether + there is no doubt but that: doubtless + this day and age: this age + this is a subject that: this subject + time (?:frame|period): time + under the provisions of: under + until such time as: until + used for fuel purposes: used for fuel + whether or not: whether + with regard to: regarding + with the exception of: except for diff --git a/.trunk/valestyles/Microsoft/meta.json b/.trunk/valestyles/Microsoft/meta.json new file mode 100644 index 0000000..297719b --- /dev/null +++ b/.trunk/valestyles/Microsoft/meta.json @@ -0,0 +1,4 @@ +{ + "feed": "https://github.com/errata-ai/Microsoft/releases.atom", + "vale_version": ">=1.0.0" +} diff --git a/.trunk/valestyles/WriteGood/Cliches.yml b/.trunk/valestyles/WriteGood/Cliches.yml new file mode 100644 index 0000000..c953143 --- /dev/null +++ b/.trunk/valestyles/WriteGood/Cliches.yml @@ -0,0 +1,702 @@ +extends: existence +message: "Try to avoid using clichés like '%s'." +ignorecase: true +level: warning +tokens: + - a chip off the old block + - a clean slate + - a dark and stormy night + - a far cry + - a fine kettle of fish + - a loose cannon + - a penny saved is a penny earned + - a tough row to hoe + - a word to the wise + - ace in the hole + - acid test + - add insult to injury + - against all odds + - air your dirty laundry + - all fun and games + - all in a day's work + - all talk, no action + - all thumbs + - all your eggs in one basket + - all's fair in love and war + - all's well that ends well + - almighty dollar + - American as apple pie + - an axe to grind + - another day, another dollar + - armed to the teeth + - as luck would have it + - as old as time + - as the crow flies + - at loose ends + - at my wits end + - avoid like the plague + - babe in the woods + - back against the wall + - back in the saddle + - back to square one + - back to the drawing board + - bad to the bone + - badge of honor + - bald faced liar + - ballpark figure + - banging your head against a brick wall + - baptism by fire + - barking up the wrong tree + - bat out of hell + - be all and end all + - beat a dead horse + - beat around the bush + - been there, done that + - beggars can't be choosers + - behind the eight ball + - bend over backwards + - benefit of the doubt + - bent out of shape + - best thing since sliced bread + - bet your bottom dollar + - better half + - better late than never + - better mousetrap + - better safe than sorry + - between a rock and a hard place + - beyond the pale + - bide your time + - big as life + - big cheese + - big fish in a small pond + - big man on campus + - bigger they are the harder they fall + - bird in the hand + - bird's eye view + - birds and the bees + - birds of a feather flock together + - bit the hand that feeds you + - bite the bullet + - bite the dust + - bitten off more than he can chew + - black as coal + - black as pitch + - black as the ace of spades + - blast from the past + - bleeding heart + - blessing in disguise + - blind ambition + - blind as a bat + - blind leading the blind + - blood is thicker than water + - blood sweat and tears + - blow off steam + - blow your own horn + - blushing bride + - boils down to + - bolt from the blue + - bone to pick + - bored stiff + - bored to tears + - bottomless pit + - boys will be boys + - bright and early + - brings home the bacon + - broad across the beam + - broken record + - brought back to reality + - bull by the horns + - bull in a china shop + - burn the midnight oil + - burning question + - burning the candle at both ends + - burst your bubble + - bury the hatchet + - busy as a bee + - by hook or by crook + - call a spade a spade + - called onto the carpet + - calm before the storm + - can of worms + - can't cut the mustard + - can't hold a candle to + - case of mistaken identity + - cat got your tongue + - cat's meow + - caught in the crossfire + - caught red-handed + - checkered past + - chomping at the bit + - cleanliness is next to godliness + - clear as a bell + - clear as mud + - close to the vest + - cock and bull story + - cold shoulder + - come hell or high water + - cool as a cucumber + - cool, calm, and collected + - cost a king's ransom + - count your blessings + - crack of dawn + - crash course + - creature comforts + - cross that bridge when you come to it + - crushing blow + - cry like a baby + - cry me a river + - cry over spilt milk + - crystal clear + - curiosity killed the cat + - cut and dried + - cut through the red tape + - cut to the chase + - cute as a bugs ear + - cute as a button + - cute as a puppy + - cuts to the quick + - dark before the dawn + - day in, day out + - dead as a doornail + - devil is in the details + - dime a dozen + - divide and conquer + - dog and pony show + - dog days + - dog eat dog + - dog tired + - don't burn your bridges + - don't count your chickens + - don't look a gift horse in the mouth + - don't rock the boat + - don't step on anyone's toes + - don't take any wooden nickels + - down and out + - down at the heels + - down in the dumps + - down the hatch + - down to earth + - draw the line + - dressed to kill + - dressed to the nines + - drives me up the wall + - dull as dishwater + - dyed in the wool + - eagle eye + - ear to the ground + - early bird catches the worm + - easier said than done + - easy as pie + - eat your heart out + - eat your words + - eleventh hour + - even the playing field + - every dog has its day + - every fiber of my being + - everything but the kitchen sink + - eye for an eye + - face the music + - facts of life + - fair weather friend + - fall by the wayside + - fan the flames + - feast or famine + - feather your nest + - feathered friends + - few and far between + - fifteen minutes of fame + - filthy vermin + - fine kettle of fish + - fish out of water + - fishing for a compliment + - fit as a fiddle + - fit the bill + - fit to be tied + - flash in the pan + - flat as a pancake + - flip your lid + - flog a dead horse + - fly by night + - fly the coop + - follow your heart + - for all intents and purposes + - for the birds + - for what it's worth + - force of nature + - force to be reckoned with + - forgive and forget + - fox in the henhouse + - free and easy + - free as a bird + - fresh as a daisy + - full steam ahead + - fun in the sun + - garbage in, garbage out + - gentle as a lamb + - get a kick out of + - get a leg up + - get down and dirty + - get the lead out + - get to the bottom of + - get your feet wet + - gets my goat + - gilding the lily + - give and take + - go against the grain + - go at it tooth and nail + - go for broke + - go him one better + - go the extra mile + - go with the flow + - goes without saying + - good as gold + - good deed for the day + - good things come to those who wait + - good time was had by all + - good times were had by all + - greased lightning + - greek to me + - green thumb + - green-eyed monster + - grist for the mill + - growing like a weed + - hair of the dog + - hand to mouth + - happy as a clam + - happy as a lark + - hasn't a clue + - have a nice day + - have high hopes + - have the last laugh + - haven't got a row to hoe + - head honcho + - head over heels + - hear a pin drop + - heard it through the grapevine + - heart's content + - heavy as lead + - hem and haw + - high and dry + - high and mighty + - high as a kite + - hit paydirt + - hold your head up high + - hold your horses + - hold your own + - hold your tongue + - honest as the day is long + - horns of a dilemma + - horse of a different color + - hot under the collar + - hour of need + - I beg to differ + - icing on the cake + - if the shoe fits + - if the shoe were on the other foot + - in a jam + - in a jiffy + - in a nutshell + - in a pig's eye + - in a pinch + - in a word + - in hot water + - in the gutter + - in the nick of time + - in the thick of it + - in your dreams + - it ain't over till the fat lady sings + - it goes without saying + - it takes all kinds + - it takes one to know one + - it's a small world + - it's only a matter of time + - ivory tower + - Jack of all trades + - jockey for position + - jog your memory + - joined at the hip + - judge a book by its cover + - jump down your throat + - jump in with both feet + - jump on the bandwagon + - jump the gun + - jump to conclusions + - just a hop, skip, and a jump + - just the ticket + - justice is blind + - keep a stiff upper lip + - keep an eye on + - keep it simple, stupid + - keep the home fires burning + - keep up with the Joneses + - keep your chin up + - keep your fingers crossed + - kick the bucket + - kick up your heels + - kick your feet up + - kid in a candy store + - kill two birds with one stone + - kiss of death + - knock it out of the park + - knock on wood + - knock your socks off + - know him from Adam + - know the ropes + - know the score + - knuckle down + - knuckle sandwich + - knuckle under + - labor of love + - ladder of success + - land on your feet + - lap of luxury + - last but not least + - last hurrah + - last-ditch effort + - law of the jungle + - law of the land + - lay down the law + - leaps and bounds + - let sleeping dogs lie + - let the cat out of the bag + - let the good times roll + - let your hair down + - let's talk turkey + - letter perfect + - lick your wounds + - lies like a rug + - life's a bitch + - life's a grind + - light at the end of the tunnel + - lighter than a feather + - lighter than air + - like clockwork + - like father like son + - like taking candy from a baby + - like there's no tomorrow + - lion's share + - live and learn + - live and let live + - long and short of it + - long lost love + - look before you leap + - look down your nose + - look what the cat dragged in + - looking a gift horse in the mouth + - looks like death warmed over + - loose cannon + - lose your head + - lose your temper + - loud as a horn + - lounge lizard + - loved and lost + - low man on the totem pole + - luck of the draw + - luck of the Irish + - make hay while the sun shines + - make money hand over fist + - make my day + - make the best of a bad situation + - make the best of it + - make your blood boil + - man of few words + - man's best friend + - mark my words + - meaningful dialogue + - missed the boat on that one + - moment in the sun + - moment of glory + - moment of truth + - money to burn + - more power to you + - more than one way to skin a cat + - movers and shakers + - moving experience + - naked as a jaybird + - naked truth + - neat as a pin + - needle in a haystack + - needless to say + - neither here nor there + - never look back + - never say never + - nip and tuck + - nip it in the bud + - no guts, no glory + - no love lost + - no pain, no gain + - no skin off my back + - no stone unturned + - no time like the present + - no use crying over spilled milk + - nose to the grindstone + - not a hope in hell + - not a minute's peace + - not in my backyard + - not playing with a full deck + - not the end of the world + - not written in stone + - nothing to sneeze at + - nothing ventured nothing gained + - now we're cooking + - off the top of my head + - off the wagon + - off the wall + - old hat + - older and wiser + - older than dirt + - older than Methuselah + - on a roll + - on cloud nine + - on pins and needles + - on the bandwagon + - on the money + - on the nose + - on the rocks + - on the spot + - on the tip of my tongue + - on the wagon + - on thin ice + - once bitten, twice shy + - one bad apple doesn't spoil the bushel + - one born every minute + - one brick short + - one foot in the grave + - one in a million + - one red cent + - only game in town + - open a can of worms + - open and shut case + - open the flood gates + - opportunity doesn't knock twice + - out of pocket + - out of sight, out of mind + - out of the frying pan into the fire + - out of the woods + - out on a limb + - over a barrel + - over the hump + - pain and suffering + - pain in the + - panic button + - par for the course + - part and parcel + - party pooper + - pass the buck + - patience is a virtue + - pay through the nose + - penny pincher + - perfect storm + - pig in a poke + - pile it on + - pillar of the community + - pin your hopes on + - pitter patter of little feet + - plain as day + - plain as the nose on your face + - play by the rules + - play your cards right + - playing the field + - playing with fire + - pleased as punch + - plenty of fish in the sea + - point with pride + - poor as a church mouse + - pot calling the kettle black + - pretty as a picture + - pull a fast one + - pull your punches + - pulling your leg + - pure as the driven snow + - put it in a nutshell + - put one over on you + - put the cart before the horse + - put the pedal to the metal + - put your best foot forward + - put your foot down + - quick as a bunny + - quick as a lick + - quick as a wink + - quick as lightning + - quiet as a dormouse + - rags to riches + - raining buckets + - raining cats and dogs + - rank and file + - rat race + - reap what you sow + - red as a beet + - red herring + - reinvent the wheel + - rich and famous + - rings a bell + - ripe old age + - ripped me off + - rise and shine + - road to hell is paved with good intentions + - rob Peter to pay Paul + - roll over in the grave + - rub the wrong way + - ruled the roost + - running in circles + - sad but true + - sadder but wiser + - salt of the earth + - scared stiff + - scared to death + - sealed with a kiss + - second to none + - see eye to eye + - seen the light + - seize the day + - set the record straight + - set the world on fire + - set your teeth on edge + - sharp as a tack + - shoot for the moon + - shoot the breeze + - shot in the dark + - shoulder to the wheel + - sick as a dog + - sigh of relief + - signed, sealed, and delivered + - sink or swim + - six of one, half a dozen of another + - skating on thin ice + - slept like a log + - slinging mud + - slippery as an eel + - slow as molasses + - smart as a whip + - smooth as a baby's bottom + - sneaking suspicion + - snug as a bug in a rug + - sow wild oats + - spare the rod, spoil the child + - speak of the devil + - spilled the beans + - spinning your wheels + - spitting image of + - spoke with relish + - spread like wildfire + - spring to life + - squeaky wheel gets the grease + - stands out like a sore thumb + - start from scratch + - stick in the mud + - still waters run deep + - stitch in time + - stop and smell the roses + - straight as an arrow + - straw that broke the camel's back + - strong as an ox + - stubborn as a mule + - stuff that dreams are made of + - stuffed shirt + - sweating blood + - sweating bullets + - take a load off + - take one for the team + - take the bait + - take the bull by the horns + - take the plunge + - takes one to know one + - takes two to tango + - the more the merrier + - the real deal + - the real McCoy + - the red carpet treatment + - the same old story + - there is no accounting for taste + - thick as a brick + - thick as thieves + - thin as a rail + - think outside of the box + - third time's the charm + - this day and age + - this hurts me worse than it hurts you + - this point in time + - three sheets to the wind + - through thick and thin + - throw in the towel + - tie one on + - tighter than a drum + - time and time again + - time is of the essence + - tip of the iceberg + - tired but happy + - to coin a phrase + - to each his own + - to make a long story short + - to the best of my knowledge + - toe the line + - tongue in cheek + - too good to be true + - too hot to handle + - too numerous to mention + - touch with a ten foot pole + - tough as nails + - trial and error + - trials and tribulations + - tried and true + - trip down memory lane + - twist of fate + - two cents worth + - two peas in a pod + - ugly as sin + - under the counter + - under the gun + - under the same roof + - under the weather + - until the cows come home + - unvarnished truth + - up the creek + - uphill battle + - upper crust + - upset the applecart + - vain attempt + - vain effort + - vanquish the enemy + - vested interest + - waiting for the other shoe to drop + - wakeup call + - warm welcome + - watch your p's and q's + - watch your tongue + - watching the clock + - water under the bridge + - weather the storm + - weed them out + - week of Sundays + - went belly up + - wet behind the ears + - what goes around comes around + - what you see is what you get + - when it rains, it pours + - when push comes to shove + - when the cat's away + - when the going gets tough, the tough get going + - white as a sheet + - whole ball of wax + - whole hog + - whole nine yards + - wild goose chase + - will wonders never cease? + - wisdom of the ages + - wise as an owl + - wolf at the door + - words fail me + - work like a dog + - world weary + - worst nightmare + - worth its weight in gold + - wrong side of the bed + - yanking your chain + - yappy as a dog + - years young + - you are what you eat + - you can run but you can't hide + - you only live once + - you're the boss + - young and foolish + - young and vibrant diff --git a/.trunk/valestyles/WriteGood/E-Prime.yml b/.trunk/valestyles/WriteGood/E-Prime.yml new file mode 100644 index 0000000..074a102 --- /dev/null +++ b/.trunk/valestyles/WriteGood/E-Prime.yml @@ -0,0 +1,32 @@ +extends: existence +message: "Try to avoid using '%s'." +ignorecase: true +level: suggestion +tokens: + - am + - are + - aren't + - be + - been + - being + - he's + - here's + - here's + - how's + - i'm + - is + - isn't + - it's + - she's + - that's + - there's + - they're + - was + - wasn't + - we're + - were + - weren't + - what's + - where's + - who's + - you're diff --git a/.trunk/valestyles/WriteGood/Illusions.yml b/.trunk/valestyles/WriteGood/Illusions.yml new file mode 100644 index 0000000..b4f1321 --- /dev/null +++ b/.trunk/valestyles/WriteGood/Illusions.yml @@ -0,0 +1,11 @@ +extends: repetition +message: "'%s' is repeated!" +level: warning +alpha: true +action: + name: edit + params: + - truncate + - " " +tokens: + - '[^\s]+' diff --git a/.trunk/valestyles/WriteGood/Passive.yml b/.trunk/valestyles/WriteGood/Passive.yml new file mode 100644 index 0000000..f472cb9 --- /dev/null +++ b/.trunk/valestyles/WriteGood/Passive.yml @@ -0,0 +1,183 @@ +extends: existence +message: "'%s' may be passive voice. Use active voice if you can." +ignorecase: true +level: warning +raw: + - \b(am|are|were|being|is|been|was|be)\b\s* +tokens: + - '[\w]+ed' + - awoken + - beat + - become + - been + - begun + - bent + - beset + - bet + - bid + - bidden + - bitten + - bled + - blown + - born + - bought + - bound + - bred + - broadcast + - broken + - brought + - built + - burnt + - burst + - cast + - caught + - chosen + - clung + - come + - cost + - crept + - cut + - dealt + - dived + - done + - drawn + - dreamt + - driven + - drunk + - dug + - eaten + - fallen + - fed + - felt + - fit + - fled + - flown + - flung + - forbidden + - foregone + - forgiven + - forgotten + - forsaken + - fought + - found + - frozen + - given + - gone + - gotten + - ground + - grown + - heard + - held + - hidden + - hit + - hung + - hurt + - kept + - knelt + - knit + - known + - laid + - lain + - leapt + - learnt + - led + - left + - lent + - let + - lighted + - lost + - made + - meant + - met + - misspelt + - mistaken + - mown + - overcome + - overdone + - overtaken + - overthrown + - paid + - pled + - proven + - put + - quit + - read + - rid + - ridden + - risen + - run + - rung + - said + - sat + - sawn + - seen + - sent + - set + - sewn + - shaken + - shaven + - shed + - shod + - shone + - shorn + - shot + - shown + - shrunk + - shut + - slain + - slept + - slid + - slit + - slung + - smitten + - sold + - sought + - sown + - sped + - spent + - spilt + - spit + - split + - spoken + - spread + - sprung + - spun + - stolen + - stood + - stridden + - striven + - struck + - strung + - stuck + - stung + - stunk + - sung + - sunk + - swept + - swollen + - sworn + - swum + - swung + - taken + - taught + - thought + - thrived + - thrown + - thrust + - told + - torn + - trodden + - understood + - upheld + - upset + - wed + - wept + - withheld + - withstood + - woken + - won + - worn + - wound + - woven + - written + - wrung diff --git a/.trunk/valestyles/WriteGood/README.md b/.trunk/valestyles/WriteGood/README.md new file mode 100644 index 0000000..20a1cd7 --- /dev/null +++ b/.trunk/valestyles/WriteGood/README.md @@ -0,0 +1,28 @@ +Based on [write-good](https://github.com/btford/write-good). + +> Naive linter for English prose for developers who can't write good and wanna learn to do other +> stuff good too. + +``` +The MIT License (MIT) + +Copyright (c) 2014 Brian Ford + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/.trunk/valestyles/WriteGood/So.yml b/.trunk/valestyles/WriteGood/So.yml new file mode 100644 index 0000000..e57f099 --- /dev/null +++ b/.trunk/valestyles/WriteGood/So.yml @@ -0,0 +1,5 @@ +extends: existence +message: "Don't start a sentence with '%s'." +level: error +raw: + - '(?:[;-]\s)so[\s,]|\bSo[\s,]' diff --git a/.trunk/valestyles/WriteGood/ThereIs.yml b/.trunk/valestyles/WriteGood/ThereIs.yml new file mode 100644 index 0000000..8b82e8f --- /dev/null +++ b/.trunk/valestyles/WriteGood/ThereIs.yml @@ -0,0 +1,6 @@ +extends: existence +message: "Don't start a sentence with '%s'." +ignorecase: false +level: error +raw: + - '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b' diff --git a/.trunk/valestyles/WriteGood/TooWordy.yml b/.trunk/valestyles/WriteGood/TooWordy.yml new file mode 100644 index 0000000..275701b --- /dev/null +++ b/.trunk/valestyles/WriteGood/TooWordy.yml @@ -0,0 +1,221 @@ +extends: existence +message: "'%s' is too wordy." +ignorecase: true +level: warning +tokens: + - a number of + - abundance + - accede to + - accelerate + - accentuate + - accompany + - accomplish + - accorded + - accrue + - acquiesce + - acquire + - additional + - adjacent to + - adjustment + - admissible + - advantageous + - adversely impact + - advise + - aforementioned + - aggregate + - aircraft + - all of + - all things considered + - alleviate + - allocate + - along the lines of + - already existing + - alternatively + - amazing + - ameliorate + - anticipate + - apparent + - appreciable + - as a matter of fact + - as a means of + - as far as I'm concerned + - as of yet + - as to + - as yet + - ascertain + - assistance + - at the present time + - at this time + - attain + - attributable to + - authorize + - because of the fact that + - belated + - benefit from + - bestow + - by means of + - by virtue of + - by virtue of the fact that + - cease + - close proximity + - commence + - comply with + - concerning + - consequently + - consolidate + - constitutes + - demonstrate + - depart + - designate + - discontinue + - due to the fact that + - each and every + - economical + - eliminate + - elucidate + - employ + - endeavor + - enumerate + - equitable + - equivalent + - evaluate + - evidenced + - exclusively + - expedite + - expend + - expiration + - facilitate + - factual evidence + - feasible + - finalize + - first and foremost + - for all intents and purposes + - for the most part + - for the purpose of + - forfeit + - formulate + - have a tendency to + - honest truth + - however + - if and when + - impacted + - implement + - in a manner of speaking + - in a timely manner + - in a very real sense + - in accordance with + - in addition + - in all likelihood + - in an effort to + - in between + - in excess of + - in lieu of + - in light of the fact that + - in many cases + - in my opinion + - in order to + - in regard to + - in some instances + - in terms of + - in the case of + - in the event that + - in the final analysis + - in the nature of + - in the near future + - in the process of + - inception + - incumbent upon + - indicate + - indication + - initiate + - irregardless + - is applicable to + - is authorized to + - is responsible for + - it is + - it is essential + - it seems that + - it was + - magnitude + - maximum + - methodology + - minimize + - minimum + - modify + - monitor + - multiple + - necessitate + - nevertheless + - not certain + - not many + - not often + - not unless + - not unlike + - notwithstanding + - null and void + - numerous + - objective + - obligate + - obtain + - on the contrary + - on the other hand + - one particular + - optimum + - overall + - owing to the fact that + - participate + - particulars + - pass away + - pertaining to + - point in time + - portion + - possess + - preclude + - previously + - prior to + - prioritize + - procure + - proficiency + - provided that + - purchase + - put simply + - readily apparent + - refer back + - regarding + - relocate + - remainder + - remuneration + - requirement + - reside + - residence + - retain + - satisfy + - shall + - should you wish + - similar to + - solicit + - span across + - strategize + - subsequent + - substantial + - successfully complete + - sufficient + - terminate + - the month of + - the point I am trying to make + - therefore + - time period + - took advantage of + - transmit + - transpire + - type of + - until such time as + - utilization + - utilize + - validate + - various different + - what I mean to say is + - whether or not + - with respect to + - with the exception of + - witnessed diff --git a/.trunk/valestyles/WriteGood/Weasel.yml b/.trunk/valestyles/WriteGood/Weasel.yml new file mode 100644 index 0000000..d1d90a7 --- /dev/null +++ b/.trunk/valestyles/WriteGood/Weasel.yml @@ -0,0 +1,29 @@ +extends: existence +message: "'%s' is a weasel word!" +ignorecase: true +level: warning +tokens: + - clearly + - completely + - exceedingly + - excellent + - extremely + - fairly + - huge + - interestingly + - is a number + - largely + - mostly + - obviously + - quite + - relatively + - remarkably + - several + - significantly + - substantially + - surprisingly + - tiny + - usually + - various + - vast + - very diff --git a/.trunk/valestyles/WriteGood/WriteGood.ini b/.trunk/valestyles/WriteGood/WriteGood.ini new file mode 100644 index 0000000..3d7738e --- /dev/null +++ b/.trunk/valestyles/WriteGood/WriteGood.ini @@ -0,0 +1,3 @@ +# Minimal placeholder for WriteGood-style rules +# This folder acts as a vendored style. Add rules here if desired. +version = 1 diff --git a/.trunk/valestyles/WriteGood/meta.json b/.trunk/valestyles/WriteGood/meta.json new file mode 100644 index 0000000..a115d28 --- /dev/null +++ b/.trunk/valestyles/WriteGood/meta.json @@ -0,0 +1,4 @@ +{ + "feed": "https://github.com/errata-ai/write-good/releases.atom", + "vale_version": ">=1.0.0" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 337950a..f676b9f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,7 +3,6 @@ "golang.go", // Official Go extension "trunk.io", // trunk.io Linters "wayou.vscode-todo-highlight", // Highlight TODOs - "eamodio.gitlens", // Git integration - "github.vscode-github-actions" // GitHub Actions support + "eamodio.gitlens" // Git integration ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index ad46259..e6bb098 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,11 +2,13 @@ "version": "0.2.0", "configurations": [ { + // keep-sorted start "name": "Launch Package", "type": "go", "request": "launch", "mode": "auto", "program": "${fileDirname}" + // keep-sorted end } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d4832f..4eb4f61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,41 +1,22 @@ { - "editor.tabSize": 2, - "editor.insertSpaces": true, + "editor.defaultFormatter": "trunk.io", "editor.detectIndentation": false, - "editor.rulers": [100], - "editor.formatOnSave": true, "editor.formatOnPaste": true, + "editor.formatOnSave": true, "editor.formatOnType": true, + "editor.rulers": [100], + "editor.tabSize": 2, "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 100, "editor.wrappingIndent": "indent", - "[go]": { - "editor.defaultFormatter": "golang.go" - }, - "[shellscript]": { - "editor.defaultFormatter": "trunk.io" - }, - "[bash]": { - "editor.defaultFormatter": "trunk.io" - }, - "[json,jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" - }, - "[shfmt]": { - "editor.defaultFormatter": "trunk.io" - }, - "[yaml]": { - "editor.defaultFormatter": "trunk.io" - }, - "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 + "editor.insertSpaces": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "git.ignoreLimitWarning": true, + "git.enableSmartCommit": true, + "gitlens.advanced.fileHistoryFollowsRenames": true, + "gitlens.codeLens.enabled": false, + "terminal.integrated.scrollback": 100000, + "workbench.list.horizontalScrolling": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 49b0feb..222da91 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,14 +39,7 @@ "label": "go: test with coverage", "type": "shell", "command": "go", - "args": [ - "test", - "-v", - "-race", - "-coverprofile=coverage.txt", - "-covermode=atomic", - "./..." - ], + "args": ["test", "-v", "-race", "-coverprofile=coverage.txt", "-covermode=atomic", "./..."], "problemMatcher": ["$go"], "presentation": { "reveal": "always", diff --git a/CLAUDE.md b/CLAUDE.md index b0f4cf6..9589629 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Code Improvements by Claude +# Code improvements by claude @@ -59,32 +59,32 @@ -## General Code Organization Principles +## General code organization principles -### 1. Package Structure +### 1 . package structure - Keep packages focused and single-purpose - Use internal packages for code not meant to be imported - Organize by feature/domain rather than by type - Follow Go standard layout conventions -### 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 (for example, CamelCase for exported names) - Keep functions small and focused - Use meaningful variable names - Follow standard Go formatting guidelines - Use comments to explain "why" not "what" -### 3. Error Handling +### 3 . error handling - Return errors rather than using panics - Wrap errors with context when crossing package boundaries - Create custom error types only when needed for client handling - Use sentinel errors sparingly -### 4. API Design +### 4 . API design - Make zero values useful - Keep interfaces small and focused, observing the @@ -98,9 +98,9 @@ - Use option patterns for complex configurations - Make dependencies explicit -### 5. Documentation Practices +### 5 . documentation practices -#### Go Code Documentation Standards +#### Go code documentation standards Following the official [Go Documentation Guidelines](https://go.dev/blog/godoc): @@ -170,7 +170,7 @@ Following the official [Go Documentation Guidelines](https://go.dev/blog/godoc): - Keep examples up to date and passing - Update docs when changing behavior -#### Code Documentation +#### Code documentation - Write package documentation with examples - Document exported symbols comprehensively @@ -201,7 +201,7 @@ Example: package cache ``` -#### Project Documentation +#### Project documentation - Maintain a comprehensive README - Include getting started guide @@ -210,9 +210,9 @@ package cache - Keep changelog updated - Include contribution guidelines -### 6. Testing Strategy +### 6 . testing strategy -#### Types of Tests +#### Types of tests 1. **Unit Tests** - Test individual components @@ -229,7 +229,7 @@ package cache - Use real external services - Verify key user scenarios -#### Test Coverage Strategy +#### Test coverage strategy - Aim for high but meaningful coverage - Focus on critical paths @@ -237,9 +237,9 @@ package cache - Balance cost vs benefit of testing - Document untested scenarios -### 7. Security Best Practices +### 7 . security best practices -#### Input Validation +#### Input validation - Validate all external input - Use strong types over strings @@ -247,7 +247,7 @@ package cache - Assert array bounds - Validate file paths -#### Secure Coding +#### Secure coding - Use latest dependencies - Implement proper error handling @@ -255,7 +255,7 @@ package cache - Use secure random numbers - Follow principle of least privilege -#### Secrets Management +#### Secrets management - Never commit secrets - Use environment variables @@ -263,7 +263,7 @@ package cache - Rotate credentials regularly - Log access to sensitive operations -### 8. Performance Considerations +### 8 . performance considerations - Minimize allocations in hot paths - Use `sync.Pool` for frequently allocated objects @@ -271,9 +271,9 @@ package cache - Profile before optimizing - Document performance characteristics -### 9. Profiling and Benchmarking +### 9 . profiling and benchmarking -#### CPU Profiling +#### CPU profiling ```go import "runtime/pprof" @@ -295,7 +295,7 @@ View with: go tool pprof cpu.prof ``` -#### Memory Profiling +#### Memory profiling ```go import "runtime/pprof" @@ -333,7 +333,7 @@ Run with: go test -bench=. -benchmem ``` -#### Trace Profiling +#### Trace profiling ```go import "runtime/trace" @@ -354,7 +354,7 @@ View with: go tool trace trace.out ``` -#### Common Profiling Tasks +#### Common profiling tasks 1. **CPU Usage** @@ -388,7 +388,7 @@ go tool trace trace.out go tool pprof mutex.prof ``` -#### `pprof` Web Interface +#### `pprof` web interface For visual analysis: @@ -396,7 +396,7 @@ For visual analysis: go tool pprof -http=:8080 cpu.prof ``` -#### Key Metrics to Watch +#### Key metrics to watch 1. **CPU Profile** - Hot functions @@ -422,7 +422,7 @@ go tool pprof -http=:8080 cpu.prof - Goroutine scheduling - Network/syscall blocking -### 10. Concurrency Patterns +### 10 . concurrency patterns - Use channels for coordination, mutexes for state - Keep critical sections small @@ -430,7 +430,7 @@ go tool pprof -http=:8080 cpu.prof - Use context for cancellation - Consider rate limiting and load shedding -### 11. Configuration Management +### 11 . configuration management - Use environment variables for deployment-specific values - Validate configuration at startup @@ -438,7 +438,7 @@ go tool pprof -http=:8080 cpu.prof - Support multiple configuration sources - Document all configuration options -### 12. Logging and Observability +### 12 . logging and observability - Use structured logging - Include relevant context in logs @@ -446,11 +446,11 @@ go tool pprof -http=:8080 cpu.prof - Add tracing for complex operations - Include metrics for important operations -## Non-Go Files +## Non - go files -### GitHub Actions +### Github actions -#### Action File Formatting +#### Action file formatting - 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 @@ -458,38 +458,38 @@ go tool pprof -http=:8080 cpu.prof 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 (for example, v1.0.0) - Recommend users reference major version tags (v1) instead of the default branch for stability. - 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 -##### Testing and Automation +##### Testing and automation - Add workflows to test your action on feature branches and pull requests - Automate releases using workflows triggered by publishing or editing a release. -##### Community Engagement +##### Community engagement - Maintain a clear README with examples. - Add community health files like CODE_OF_CONDUCT and CONTRIBUTING. - Use badges to display workflow status. -##### Further Guidance +##### Further guidance For more details, visit: - - -### YAML Formatting +### YAML formatting -#### Quoting Guidelines +#### Quoting guidelines Follow these rules for consistent YAML formatting: @@ -515,7 +515,7 @@ value: "null" # String "null", not null value enable: "false" # String "false", not boolean false ``` -**DO NOT quote simple values:** +**do not quote simple values:** ```yaml # Booleans @@ -531,7 +531,7 @@ name: ubuntu-latest step: checkout action: setup-node -# GitHub Actions expressions (never quote these) +# Github actions expressions ( never quote these ) if: github.event_name == 'push' with: ${{ secrets.TOKEN }} ``` @@ -543,7 +543,7 @@ with: ${{ secrets.TOKEN }} uses: actions/checkout@v4 uses: ./path/to/local/action -# Boolean inputs - don't quote +# Boolean inputs - don ' t quote debug: false cache: true @@ -555,7 +555,7 @@ if: ${{ github.ref == 'refs/heads/main' }} run: echo "${{ github.actor }}" ``` -#### Formatting Standards +#### Formatting standards - Use 2 spaces for indentation - Use `-` for list items with proper indentation @@ -587,11 +587,11 @@ jobs: DEBUG: false ``` -### Bash Scripts +### Bash scripts Project scripts should follow these guidelines: -#### File and Directory Structure +#### File and directory structure All scripts must go into the project's script directory with specific guidance below @@ -626,11 +626,15 @@ scripts - Add new development script functionality to the `scripts/dev/menu.sh` script for easy access - Always use `template.sh` when creating a script -#### Style and Format Rules +#### Style and format rules -- **MANDATORY:** All Bash scripts must strictly follow the [Google Bash Style Guide](https://google.github.io/styleguide/shellguide) for naming, formatting, comments, and best practices. -- **MANDATORY:** All Bash scripts must pass [ShellCheck](https://github.com/koalaman/shellcheck/wiki) with no warnings or errors. -- **MANDATORY:** All script comments and header blocks must wrap at a maximum line length of 80 characters. +- **MANDATORY:** All Bash scripts must strictly follow the + [Google Bash Style Guide](https://google.github.io/styleguide/shellguide) for naming, formatting, + comments, and best practices. +- **MANDATORY:** All Bash scripts must pass + [ShellCheck](https://github.com/koalaman/shellcheck/wiki) with no warnings or errors. +- **MANDATORY:** All script comments and header blocks must wrap at a maximum line length of 80 + characters. - Use the `function` keyword before all function definitions: `function my_function() {` - Use imperative verb form for script names: - Good: `export_version.sh`, `build_package.sh`, `run_tests.sh` @@ -649,33 +653,33 @@ For functions: - Always follow the format described in [Google Bash Style Guide: Function Comments](https://google.github.io/styleguide/shellguide#function-comments) -#### Script Header Requirements (MANDATORY) +#### Script header requirements ( MANDATORY ) Every Bash script must begin with a standardized header block, formatted as follows: ```bash #!/bin/bash #============================================================================== -# .sh +# < script_name > . sh #============================================================================== # -# DESCRIPTION: -# +# DESCRIPTION : +# < detailed description of the script ' s purpose and functionality > # -# USAGE: -# .sh [args] +# USAGE : +# < script_name > . sh < command > [ args ] # -# COMMANDS: -# -# -# ... +# COMMANDS : +# < command_1 > < description of command_1 > +# < command_2 > < description of command_2 > +# . . . # -# OPTIONS: -# -h, --help Show this help message -# ... +# OPTIONS : +# - h , - - help show this help message +# . . . < other options and their descriptions > # -# DEPENDENCIES: -# +# DEPENDENCIES : +# < list required dependencies , e . g . external tools , environment variables > #============================================================================== ``` @@ -687,9 +691,10 @@ Checklist for script headers: - Command and option documentation - Required dependencies -All new and updated scripts must comply with this format. Non-compliant scripts will be flagged in code review and CI. +All new and updated scripts must comply with this format. Non-compliant scripts will be flagged in +code review and CI. -#### Script Testing +#### Script testing All scripts must have corresponding tests in the `tests` sub-directory using the common test library: @@ -717,7 +722,7 @@ library: - Test execution is part of the validate-scripts job - Test failures block PR merges -##### Test Framework Architecture Pattern +##### Test framework architecture pattern All tests must start with `scripts/template_test.sh` @@ -732,7 +737,7 @@ All tests must start with `scripts/template_test.sh` - **Framework Integration**: Call `start_tests "$@"` before running tests to handle argument parsing and setup -##### Centralized Configuration Management +##### Centralized configuration management The project implements centralized version management using the `.env` file as a single source of truth: @@ -740,7 +745,7 @@ truth: **Configuration Structure:** ```bash -# .env file contents +# . env file contents GO_VERSION=1.23.4 GO_TOOLCHAIN=go1.23.4 ``` @@ -748,7 +753,7 @@ GO_TOOLCHAIN=go1.23.4 **GitHub Actions Integration:** ```yaml -# .github/workflows/ci.yml pattern +# . github / workflows / ci . yml pattern jobs: setup: runs-on: ubuntu-latest @@ -776,9 +781,9 @@ jobs: - Ensures consistency between environment configuration and Go module requirements - Can be extended for other configuration synchronization needs -## Testing Principles +## Testing principles -### 1. Test Organization Strategy +### 1 . test organization strategy We established a balanced approach to test organization: @@ -787,9 +792,9 @@ We established a balanced approach to test organization: cannot be shared amongst other cases - Group related test cases that operate on the same API method / function -### 2. Code Structure +### 2 . code structure -#### Constants and Variables +#### Constants and variables ```go const ( @@ -809,7 +814,7 @@ var ( - Group related constants and variables together - Do not prefix constants or variables with `test` -#### Helper Functions +#### Helper functions Simple examples of factory and assert functions. @@ -892,9 +897,9 @@ func TestAddPrefixToDescription_WithValidInput_AddsPrefix(t *testing.T) { - Keep helpers focused and single-purpose - Helper functions that require logic should go into their own file and have tests -### 3. Test Case Patterns +### 3 . test case patterns -#### Table-Driven Tests (for simple cases) +#### Table - driven tests ( for simple cases ) ```go // Each test case is its own function - no loops or conditionals in test body @@ -948,7 +953,7 @@ func assertFormatError(t *testing.T, actual string, err error, expectedErrMsg st } ``` -#### Individual Tests (for complex cases) +#### Individual tests ( for complex cases ) ```go func TestProcessTransaction_WithConcurrentUpdates_PreservesConsistency(t *testing.T) { @@ -1001,7 +1006,7 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) { } ``` -### 4. Best Practices Applied +### 4 . best practices applied 1. **Clear Naming** - Name test data clearly and meaningfully @@ -1009,7 +1014,7 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) { - Use `expected` for expected values - Use `actual` for function results - 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__` for free functions, and @@ -1084,7 +1089,7 @@ func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) { - Group related assertions logically - Test one concept per assertion -### 5. Examples of Improvements +### 5 . examples of improvements #### Before @@ -1156,7 +1161,7 @@ func TestValidateConfig_WithEmptyPath_ReturnsError(t *testing.T) { } ``` -## Key Benefits +## Key benefits 1. **Maintainability** - Easier to update and modify tests diff --git a/COMMANDS.md b/COMMANDS.md index 734b4ee..519e0be 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -1,7 +1,6 @@ # Command Line Usage Guide -This document provides information about using the `cache-apt-pkgs` command line -tool. +This document provides information about using the `cache-apt-pkgs` command line tool. ## Basic Usage @@ -24,8 +23,7 @@ cache-apt-pkgs install [flags] [packages] #### Flags for Install - `--version`: Cache version identifier (optional) -- `--execute-scripts`: Execute package install scripts (optional, default: - false) +- `--execute-scripts`: Execute package install scripts (optional, default: false) #### Install Examples @@ -76,8 +74,7 @@ cache-apt-pkgs restore [flags] [packages] #### Flags for Restore - `--version`: Cache version to restore from (optional) -- `--execute-scripts`: Execute package install scripts (optional, default: - false) +- `--execute-scripts`: Execute package install scripts (optional, default: false) #### Restore Examples diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fadb6a..caa8e80 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,19 @@ # 🤝 Contributing to cache-apt-pkgs-action -Thank you for your interest in contributing to cache-apt-pkgs-action! This -document provides guidelines and instructions for contributing to the project. +Thank you for your interest in contributing to cache-apt-pkgs-action! This document provides +guidelines and instructions for contributing to the project. [![CI](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/ci.yml/badge.svg?branch=dev-v2.0)](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/ci.yml?query=branch%3Adev-v2.0) [![Go Report Card](https://goreportcard.com/badge/github.com/awalsh128/cache-apt-pkgs-action)](https://goreportcard.com/report/github.com/awalsh128/cache-apt-pkgs-action) -[![Go Reference](https://pkg.go.dev/badge/github.com/awalsh128/cache-apt-pkgs-action.svg)](https://pkg.go.dev/github.com/awalsh128/cache-apt-pkgs-action) [![License](https://img.shields.io/github/license/awalsh128/cache-apt-pkgs-action)](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/LICENSE) [![Release](https://img.shields.io/github/v/release/awalsh128/cache-apt-pkgs-action)](https://github.com/awalsh128/cache-apt-pkgs-action/releases) -⚠️ **IMPORTANT**: This is a very unstable branch and will be introduced as -version 2.0 once in beta. +⚠️ **IMPORTANT**: This is a very unstable branch and will be introduced as version 2.0 once in beta. ## 🔗 Useful Links -- 📖 - [GitHub Action Documentation](https://github.com/awalsh128/cache-apt-pkgs-action#readme) -- 📦 - [Go Package Documentation](https://pkg.go.dev/github.com/awalsh128/cache-apt-pkgs-action) -- 🔄 - [GitHub Actions Workflow Status](https://github.com/awalsh128/cache-apt-pkgs-action/actions) +- 📖 [GitHub Action Documentation](https://github.com/awalsh128/cache-apt-pkgs-action#readme) +- 🔄 [GitHub Actions Workflow Status](https://github.com/awalsh128/cache-apt-pkgs-action/actions) - 🐛 [Issues](https://github.com/awalsh128/cache-apt-pkgs-action/issues) - 🛠️ [Pull Requests](https://github.com/awalsh128/cache-apt-pkgs-action/pulls) @@ -99,14 +93,13 @@ There are two ways to test the GitHub Action workflows: 1. ☁️ **Using GitHub Actions**: - Push your changes to a branch - Create a PR to trigger the - [test workflow](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/.github/workflows/test-action.yml) + [test workflow](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/.github/workflows/action-tests.yml) - Or manually trigger the workflow from the [Actions tab](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/test-action.yml) 2. 🐳 **Running Tests Locally** (requires Docker): - Install Docker - - 🪟 WSL users install - [Docker Desktop](https://www.docker.com/products/docker-desktop/) + - 🪟 WSL users install [Docker Desktop](https://www.docker.com/products/docker-desktop/) - 🐧 Non-WSL users (native Linux) ```bash @@ -116,8 +109,7 @@ There are two ways to test the GitHub Action workflows: sudo systemctl start docker ``` - - 🎭 Install [`act`](https://github.com/nektos/act) for local GitHub Actions - testing: + - 🎭 Install [`act`](https://github.com/nektos/act) for local GitHub Actions testing: - ▶️ Run `act` on any action test in the following ways: @@ -159,14 +151,13 @@ There are two ways to test the GitHub Action workflows: 1. **Using GitHub Actions**: - Push your changes to a branch - Create a PR to trigger the - [test workflow](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/.github/workflows/test-action.yml) + [test workflow](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/.github/workflows/action-tests.yml) - Or manually trigger the workflow from the [Actions tab](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/test-action.yml) 2. **Running Tests Locally** (requires Docker): - Install Docker - - WSL users install - [Docker Desktop](https://www.docker.com/products/docker-desktop/) + - WSL users install [Docker Desktop](https://www.docker.com/products/docker-desktop/) - Non-WSL users (native Linux) ```bash @@ -176,8 +167,7 @@ There are two ways to test the GitHub Action workflows: sudo systemctl start docker ``` - - Install [`act`](https://github.com/nektos/act) for local GitHub Actions - testing: + - Install [`act`](https://github.com/nektos/act) for local GitHub Actions testing: - Run `act` on any action test in the following ways: @@ -197,14 +187,12 @@ There are two ways to test the GitHub Action workflows: ``` 2. ✏️ Make your changes, following these guidelines: - - 📚 Follow Go coding - [standards and conventions](https://go.dev/doc/effective_go) + - 📚 Follow Go coding [standards and conventions](https://go.dev/doc/effective_go) - ✅ Add tests for new features - 🎯 Test behaviors on the public interface not implementation - 🔍 Keep tests for each behavior separate - - 🏭 Use constants and factory functions to keep testing arrangement and - asserts clear. Not a lot of boilerplate not directly relevant to the - test. + - 🏭 Use constants and factory functions to keep testing arrangement and asserts clear. Not a + lot of boilerplate not directly relevant to the test. - 📖 Update documentation as needed - 🎯 Keep commits focused and atomic - 📝 Write clear commit messages @@ -230,11 +218,8 @@ There are two ways to test the GitHub Action workflows: ## 💻 Code Style Guidelines -- 📏 Follow - [standard Go formatting](https://golang.org/doc/effective_go#formatting) (use - `gofmt`) -- 📖 Follow - [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) +- 📏 Follow [standard Go formatting](https://golang.org/doc/effective_go#formatting) (use `gofmt`) +- 📖 Follow [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - 🔍 Write clear, self-documenting code - 📚 Add [GoDoc](https://blog.golang.org/godoc) comments for complex logic - 🏷️ Use meaningful variable and function names @@ -247,8 +232,7 @@ There are two ways to test the GitHub Action workflows: 📚 For more details on Go best practices, refer to: - 📖 [Effective Go](https://golang.org/doc/effective_go) -- 🔍 - [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) +- 🔍 [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) ## Documentation @@ -265,49 +249,15 @@ There are two ways to test the GitHub Action workflows: ## Questions or Problems? -- Open an [issue](https://github.com/awalsh128/cache-apt-pkgs-action/issues/new) - for bugs or feature requests -- Use - [discussions](https://github.com/awalsh128/cache-apt-pkgs-action/discussions) - for questions or ideas +- Open an [issue](https://github.com/awalsh128/cache-apt-pkgs-action/issues/new) for bugs or feature + requests - Reference the [GitHub Action documentation](https://github.com/awalsh128/cache-apt-pkgs-action#readme) -- Check existing - [issues](https://github.com/awalsh128/cache-apt-pkgs-action/issues) and +- Check existing [issues](https://github.com/awalsh128/cache-apt-pkgs-action/issues) and [pull requests](https://github.com/awalsh128/cache-apt-pkgs-action/pulls) - Tag maintainers for urgent issues ## License -By contributing to this project, you agree that your contributions will be -licensed under the same license as the project. - -## 📦 Publishing to pkg.go.dev - -NOTE: This is done by the maintainers - -To make the library available on [pkg.go.dev](https://pkg.go.dev): - -1. 🏷️ Ensure your code is tagged with a version: - - ```bash - git tag v2.0.0 # Use semantic versioning - git push origin v2.0.0 - ``` - -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) - - Or fetch via command line: - - ```bash - GOPROXY=https://proxy.golang.org GO111MODULE=on go get github.com/awalsh128/cache-apt-pkgs-action@v2.0.0 - ``` - -3. 📝 Best practices for publishing: - - Add comprehensive `godoc` comments - - Include examples in your documentation - - Use semantic versioning for tags - - Keep the module path consistent - - Update go.mod with the correct module path - - [Go Best Practices](https://golang.org/doc/effective_go#names) +By contributing to this project, you agree that your contributions will be licensed under the same +license as the project. diff --git a/README.md b/README.md index 1412193..57b22b5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![CI](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/ci.yml/badge.svg?branch=dev-v2.0)](https://github.com/awalsh128/cache-apt-pkgs-action/actions/workflows/ci.yml?query=branch%3Adev-v2.0) [![Go Report Card](https://goreportcard.com/badge/github.com/awalsh128/cache-apt-pkgs-action)](https://goreportcard.com/report/github.com/awalsh128/cache-apt-pkgs-action) -[![Go Reference](https://pkg.go.dev/badge/github.com/awalsh128/cache-apt-pkgs-action.svg)](https://pkg.go.dev/github.com/awalsh128/cache-apt-pkgs-action) [![License](https://img.shields.io/github/license/awalsh128/cache-apt-pkgs-action)](https://github.com/awalsh128/cache-apt-pkgs-action/blob/dev-v2.0/LICENSE) [![Release](https://img.shields.io/github/v/release/awalsh128/cache-apt-pkgs-action)](https://github.com/awalsh128/cache-apt-pkgs-action/releases) @@ -36,14 +35,12 @@ -Speed up your GitHub Actions workflows by caching APT package dependencies. This -action integrates with [actions/cache](https://github.com/actions/cache/) to -provide efficient package caching, significantly reducing workflow execution -time by avoiding repeated package installations. +Speed up your GitHub Actions workflows by caching APT package dependencies. This action integrates +with [actions/cache](https://github.com/actions/cache/) to provide efficient package caching, +significantly reducing workflow execution time by avoiding repeated package installations. -> **Important:** We're looking for co-maintainers to help review changes and -> investigate issues. If you're interested in contributing to this project, -> please reach out. +> **Important:** We're looking for co-maintainers to help review changes and investigate issues. If +> you're interested in contributing to this project, please reach out. ## 🚀 Quick Start @@ -92,10 +89,9 @@ steps: ### Version Selection -> ⚠️ The action enforces immutable references. Workflows must pin -> `awalsh128/cache-apt-pkgs-action` to a release tag or commit SHA. Referencing -> a branch (for example `@main`) will now fail during the `setup` step. For more -> information on blocking and SHA pinning actions, see the +> ⚠️ The action enforces immutable references. Workflows must pin `awalsh128/cache-apt-pkgs-action` +> to a release tag or commit SHA. Referencing a branch (for example `@main`) will now fail during +> the `setup` step. For more information on blocking and SHA pinning actions, see the > [announcement on the GitHub changelog](https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions). Recommended options: @@ -103,9 +99,8 @@ Recommended options: - `@v2` or any other published release tag. - A full commit SHA such as `@4f5c863ba5ce9f1784c8ad7d8f63a9cfd3f1ab2c`. -Avoid floating references such as `@latest`, `@master`, or `@dev`. The action -will refuse to run when a branch reference is detected to protect consumers from -involuntary updates. +Avoid floating references such as `@latest`, `@master`, or `@dev`. The action will refuse to run +when a branch reference is detected to protect consumers from involuntary updates. ### Example Workflows @@ -210,39 +205,34 @@ permissions: ## 🤝 Contributing -We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) -for details. +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. ## 📜 License -This project is licensed under the Apache License 2.0 - see the -[LICENSE](LICENSE) file for details. +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. ## Caveats ### Edge Cases -This action is able to speed up installs by skipping the number of steps that -`apt` uses. +This action is able to speed up installs by skipping the number of steps that `apt` uses. -- This means there will be certain cases that it may not be able to handle like - state management of other file configurations outside the package scope. -- In cases that can't be immediately addressed or run counter to the approach of - this action, the packages affected should go into their own action `step` and - using the normal `apt` utility. +- This means there will be certain cases that it may not be able to handle like state management of + other file configurations outside the package scope. +- In cases that can't be immediately addressed or run counter to the approach of this action, the + packages affected should go into their own action `step` and using the normal `apt` utility. ### Non-file Dependencies -This action is based on the principle that most packages can be cached as a set -of files. There are situations though where this is not enough. +This action is based on the principle that most packages can be cached as a set of files. There are +situations though where this is not enough. -- Pre and post installation scripts need to be run from +- Pre-installation and post-installation scripts need to be run from `/var/lib/dpkg/info/{package name}.[preinst, postinst]`. -- The Debian package database needs to be queried for scripts above (i.e. - `dpkg-query`). +- The Debian package database needs to be queried for scripts above (i.e. `dpkg-query`). -The `execute_install_scripts` argument can be used to attempt to execute the -install scripts but they are no guaranteed to resolve the issue. +The `execute_install_scripts` argument can be used to attempt to execute the install scripts but +they are no guaranteed to resolve the issue. ```yaml - uses: awalsh128/cache-apt-pkgs-action@latest @@ -252,19 +242,18 @@ install scripts but they are no guaranteed to resolve the issue. execute_install_scripts: true ``` -If this does not solve your issue, you will need to run `apt-get install` as a -separate step for that particular package unfortunately. +If this does not solve your issue, you will need to run `apt-get install` as a separate step for +that particular package unfortunately. ```yaml run: apt-get install mypackage shell: bash ``` -Please reach out if you have found a workaround for your scenario and it can be -generalized. There is only so much this action can do and can't get into the -area of reverse engineering Debian package manager. It would be beyond the scope -of this action and may result in a lot of extended support and brittleness. -Also, it would be better to contribute to Debian packager instead at that point. +Please reach out if you have found a workaround for your scenario and it can be generalized. There +is only so much this action can do and can't get into the area of reverse engineering Debian package +manager. It would be beyond the scope of this action and may result in a lot of extended support and +brittleness. Also, it would be better to contribute to Debian packager instead at that point. For more context and information see [issue #57](https://github.com/awalsh128/cache-apt-pkgs-action/issues/57#issuecomment-1321024283) @@ -272,14 +261,12 @@ which contains the investigation and conclusion. ### Cache Limits -A repository can have up to 5GB of caches. Once the 5GB limit is reached, older -caches will be evicted based on when the cache was last accessed. Caches that -are not accessed within the last week will also be evicted. To get more -information on how to access and manage your actions's caches, see +A repository can have up to 5GB of caches. Once the 5GB limit is reached, older caches will be +evicted based on when the cache was last accessed. Caches that are not accessed within the last week +will also be evicted. To get more information on how to access and manage your actions's caches, see [GitHub Actions / Using workflows / Cache dependencies](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#viewing-cache-entries). ## 🌟 Acknowledgements - [actions/cache](https://github.com/actions/cache/) team -- All our - [contributors](https://github.com/awalsh128/cache-apt-pkgs-action/graphs/contributors) +- All our [contributors](https://github.com/awalsh128/cache-apt-pkgs-action/graphs/contributors) diff --git a/action.yml b/action.yml index 3963ef9..81c057d 100644 --- a/action.yml +++ b/action.yml @@ -7,19 +7,22 @@ branding: inputs: packages: description: > - Space delimited list of packages to install. Version can be specified optionally using APT command syntax of = (e.g. xdot=1.2-2). + Space delimited list of packages to install. Version can be specified optionally using APT + command syntax of = (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. + 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. + Execute Debian package pre and post install script upon restore. See README.md caveats + for more information. required: false default: "false" @@ -35,14 +38,22 @@ outputs: 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. ::). + The main requested packages and versions that are installed. Represented as a comma + delimited list with equals delimit on the package version (i.e. + ::). - value: ${{ steps.install-pkgs.outputs.package-version-list || steps.restore-pkgs.outputs.package-version-list }} + value: + ${{ steps.install-pkgs.outputs.package-version-list || + steps.restore-pkgs.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. ::). + 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. ::). - value: ${{ steps.install-pkgs.outputs.all-package-version-list || steps.restore-pkgs.outputs.all-package-version-list }} + value: + ${{ steps.install-pkgs.outputs.all-package-version-list || + steps.restore-pkgs.outputs.all-package-version-list }} runs: using: composite steps: diff --git a/cmd/cache_apt_pkgs/cmdflags/cmd.go b/cmd/cache_apt_pkgs/cmdflags/cmd.go new file mode 100644 index 0000000..b8b08a9 --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/cmd.go @@ -0,0 +1,117 @@ +// Package cmdflags provides types and utilities for parsing command-line flags +// and managing subcommands in the cache-apt-pkgs CLI tool. +package cmdflags + +import ( + "flag" + "fmt" + "os" + "strings" + + "awalsh128.com/cache-apt-pkgs-action/internal/cio" + "awalsh128.com/cache-apt-pkgs-action/internal/logging" + "awalsh128.com/cache-apt-pkgs-action/internal/pkgs" +) + +// 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 { + // Name is the command identifier used in CLI arguments + Name string + // Description explains what the command does + Description string + // 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 + // GhioPrinter provides an interface for GitHub Actions output printing that supports testing + // locally and in Action workflows + GhioPrinter cio.GhPrinter + // Examples provides example usage strings for the command + Examples []string + // ExamplePackages provides example package arguments for documentation and testing + 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) + } + } + wrappedRunFunc := func(cmd *Cmd, pkgArgs pkgs.Packages) error { + // If any command args include help flag, print usage and don't execute command. + if helpFlagSet(cmd.Flags) { + cmd.Flags.Usage() + return nil + } + return runFunc(cmd, pkgArgs) + } + return &Cmd{ + Name: name, + Description: description, + Flags: flags, + Run: wrappedRunFunc, + GhioPrinter: cio.NewGhPrinter(), + Examples: examples, + ExamplePackages: ExamplePackages, + } +} + +// 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 { + return c.Flags.Lookup(name).Value.String() +} + +// 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, error) { + logging.Debug("Parsing flags for command %q with args: %v", c.Name, os.Args[2:]) + if len(os.Args) < 3 { + return nil, fmt.Errorf("command %q requires arguments", c.Name) + } + // Parse the command line flags + if err := c.Flags.Parse(os.Args[2:]); err != nil { + return nil, fmt.Errorf("unable to parse flags for command %q: %v", c.Name, err) + } + + // Check for missing required flags + missingFlagNames := []string{} + c.Flags.VisitAll(func(f *flag.Flag) { + // Skip all global flags since they are considered optional + if gf := globalFlags.Lookup(f.Name); gf != nil { + return + } + if f.DefValue == "" && f.Value.String() == "" { + logging.Info("Missing required flag: %s", f.Name) + missingFlagNames = append(missingFlagNames, f.Name) + } + }) + if len(missingFlagNames) > 0 { + return nil, fmt.Errorf("missing required flags for command %q: %s", c.Name, missingFlagNames) + } + logging.Debug("Parsed flags successfully") + + // Parse the remaining arguments as package arguments + pkgArgs, err := pkgs.ParsePackageArgs(c.Flags.Args()) + if err != nil { + return nil, fmt.Errorf("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, nil +} diff --git a/cmd/cache_apt_pkgs/cmdflags/cmd_test.go b/cmd/cache_apt_pkgs/cmdflags/cmd_test.go new file mode 100644 index 0000000..023fbe5 --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/cmd_test.go @@ -0,0 +1,105 @@ +package cmdflags + +import ( + "flag" + "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" + flagDescription = "This is a test flag" + cmdName = "test-command-name" + argExample = "test-package" + requiredFlagName = "required-flag" +) + +func TestCmd_StringFlag(t *testing.T) { + cmd := &Cmd{ + Name: cmdName, + Flags: flag.NewFlagSet(flagSetName, flag.ContinueOnError), + } + cmd.Flags.String(flagName, flagDefaultValue, flagDescription) + + // Parse some args to set the flag value + cmd.Flags.Set(flagName, flagValue) + + result := cmd.StringFlag(flagName) + if result != flagValue { + t.Errorf("Expected %q, got %q", flagValue, result) + } +} + +func TestNewCmd(t *testing.T) { + runCalled := false + runFunc := func(cmd *Cmd, pkgArgs pkgs.Packages) error { + runCalled = true + return nil + } + + cmd := NewCmd(cmdName, "test description", []string{argExample}, runFunc) + + if cmd == nil { + t.Fatal("NewCmd returned nil") + } + if cmd.Name != cmdName { + t.Errorf("Expected name %q, got %q", cmdName, cmd.Name) + } + if cmd.Description == "" { + t.Error("Expected non-empty description") + } + if cmd.Flags == nil { + t.Error("Expected flags to be initialized") + } + if cmd.Run == nil { + t.Error("Expected Run function to be set") + } + if len(cmd.Examples) != 1 { + t.Errorf("Expected 1 example, got %d", len(cmd.Examples)) + } + + // Test that Run function works + err := cmd.Run(cmd, pkgs.NewPackages()) + if err != nil { + t.Errorf("Unexpected error calling Run: %v", err) + } + if !runCalled { + t.Error("Expected run function to be called") + } +} + +func TestCmd_ParseFlagsLogic(t *testing.T) { + t.Run("missing required flags", func(t *testing.T) { + cmd := NewCmd(cmdName, "test description", []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("flag registration", func(t *testing.T) { + cmd := NewCmd(cmdName, "test description", []string{}, func(cmd *Cmd, pkgArgs pkgs.Packages) error { + return nil + }) + + // Check that global flags are inherited + if cmd.Flags.Lookup("verbose") == nil { + t.Error("Expected verbose flag to be inherited from global flags") + } + if cmd.Flags.Lookup("help") == nil { + t.Error("Expected help flag to be inherited from global flags") + } + }) +} diff --git a/cmd/cache_apt_pkgs/cmdflags/cmds.go b/cmd/cache_apt_pkgs/cmdflags/cmds.go new file mode 100644 index 0000000..374e908 --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/cmds.go @@ -0,0 +1,87 @@ +package cmdflags + +import ( + "flag" + "fmt" + "os" + + "awalsh128.com/cache-apt-pkgs-action/internal/pkgs" +) + +// Cmds is a collection of subcommands indexed by their names. +// It provides methods for managing and executing CLI subcommands. +type Cmds map[string]*Cmd + +// 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 { + if _, exists := (*c)[cmd.Name]; exists { + return fmt.Errorf("command %q already exists", cmd.Name) + } + (*c)[cmd.Name] = cmd + 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) { + cmd, ok := (*c)[name] + return cmd, ok +} + +// usage prints the overall usage help for all commands. +func (c *Cmds) usage() { + fmt.Fprintf(os.Stderr, "usage: %s [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 --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 command, package arguments, and any error encountered. +func (c *Cmds) Parse() (*Cmd, pkgs.Packages, error) { + if len(os.Args) < 2 { + c.usage() + return nil, nil, fmt.Errorf("no command specified") + } + + cmdName := os.Args[1] + if cmdName == "--"+helpFlagName || cmdName == "-"+helpShortFlagName { + c.usage() + return nil, nil, nil + } + + cmd, ok := c.Get(cmdName) + if !ok { + c.usage() + return nil, nil, fmt.Errorf("unknown command %q", cmdName) + } + + pkgArgs, err := cmd.parseFlags() + if err != nil { + return nil, nil, err + } + if pkgArgs == nil { + return nil, nil, fmt.Errorf("failed to parse package arguments for command %q", cmd.Name) + } + + return cmd, pkgArgs, nil +} + +// 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 { + commands := &Cmds{} + for _, c := range cmd { + commands.Add(c) + } + return commands +} diff --git a/cmd/cache_apt_pkgs/cmdflags/cmds_test.go b/cmd/cache_apt_pkgs/cmdflags/cmds_test.go new file mode 100644 index 0000000..0752ebd --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/cmds_test.go @@ -0,0 +1,141 @@ +package cmdflags + +import ( + "os" + "testing" + + "awalsh128.com/cache-apt-pkgs-action/internal/pkgs" +) + +const ( + cmdName1 = "test-command-1" + cmdName2 = "test-command-2" +) + +func TestCmds_Add(t *testing.T) { + cmds := make(Cmds) + cmd := NewCmd(cmdName, "test description", []string{argExample}, func(cmd *Cmd, pkgArgs pkgs.Packages) error { + return nil + }) + + err := cmds.Add(cmd) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if len(cmds) != 1 { + t.Errorf("Expected 1 command, got %d", len(cmds)) + } + if _, exists := cmds[cmdName]; !exists { + t.Errorf("Expected command %q to be added", cmdName) + } +} + +func TestCmds_Get(t *testing.T) { + cmds := make(Cmds) + expectedCmd := NewCmd(cmdName, "test description", []string{argExample}, func(cmd *Cmd, pkgArgs pkgs.Packages) error { + return nil + }) + cmds.Add(expectedCmd) + + t.Run("existing command", func(t *testing.T) { + cmd, ok := cmds.Get(cmdName) + if !ok { + t.Error("Expected to find command") + } + if cmd != expectedCmd { + t.Error("Expected to get the same command instance") + } + }) + + t.Run("non-existing command", func(t *testing.T) { + _, ok := cmds.Get("non-existent-command") + if ok { + t.Error("Expected not to find non-existent command") + } + }) + + // Test multiple commands + cmd1 := NewCmd(cmdName1, "description 1", []string{}, func(cmd *Cmd, pkgArgs pkgs.Packages) error { return nil }) + cmd2 := NewCmd(cmdName2, "description 2", []string{}, func(cmd *Cmd, pkgArgs pkgs.Packages) error { return nil }) + + cmds2 := make(Cmds) + cmds2.Add(cmd1) + cmds2.Add(cmd2) + + if _, ok := cmds2.Get(cmdName1); !ok { + t.Errorf("Expected to find %s", cmdName1) + } + if _, ok := cmds2.Get(cmdName2); !ok { + t.Errorf("Expected to find %s", cmdName2) + } +} + +func TestCreateCmds(t *testing.T) { + cmds := CreateCmds() + + expectedCommands := []string{"install", "restore", "validate", "setup", "cleanup", "createkey"} + + if len(*cmds) != len(expectedCommands) { + t.Errorf("Expected %d commands, got %d", len(expectedCommands), len(*cmds)) + } + + for _, cmdName := range expectedCommands { + cmd, ok := cmds.Get(cmdName) + if !ok { + t.Errorf("Expected command %q to exist", cmdName) + continue + } + if cmd.Name != cmdName { + t.Errorf("Expected command name %q, got %q", cmdName, cmd.Name) + } + if cmd.Description == "" { + t.Errorf("Expected non-empty description for command %q", cmdName) + } + if cmd.Flags == nil { + t.Errorf("Expected flags to be initialized for command %q", cmdName) + } + if cmd.Run == nil { + t.Errorf("Expected Run function to be set for command %q", cmdName) + } + } +} + +func TestCmds_Parse(t *testing.T) { + // Note: These tests don't call Parse() directly as it calls os.Exit + // Instead they test the Parse method's logic through integration with os.Args + + 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 + os.Args = []string{binaryName} + // Can't actually call Parse() here as it will exit the test process + // Just verify the setup + if len(os.Args) < 2 { + t.Log("Command would be missing, Parse() would show usage") + } + }) + + const pkgArg1 = "test-package=1.1-beta" + const pkgArg2 = "test-package=2.0" + + t.Run("valid command with packages", func(t *testing.T) { + os.Args = []string{binaryName, cmdName, pkgArg1, pkgArg2} + + if len(os.Args) >= 4 { + actualCmdName := os.Args[1] + actualPkgArgs := os.Args[2:] + + if actualCmdName != cmdName { + 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") + } + }) +} diff --git a/cmd/cache_apt_pkgs/cmdflags/doc.go b/cmd/cache_apt_pkgs/cmdflags/doc.go new file mode 100644 index 0000000..ba6397d --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/doc.go @@ -0,0 +1,6 @@ +// Package cmdflags provides command-line flag handling and command management for cache-apt-pkgs. +// +// The package is responsible for parsing and validating command-line arguments used +// to control the action's behavior. It is an extension of the standard flag package with support +// for subcommands and shared global flags. +package cmdflags diff --git a/cmd/cache_apt_pkgs/cmdflags/shared.go b/cmd/cache_apt_pkgs/cmdflags/shared.go new file mode 100644 index 0000000..15dbc38 --- /dev/null +++ b/cmd/cache_apt_pkgs/cmdflags/shared.go @@ -0,0 +1,48 @@ +// Package cmdflags provides types and utilities for parsing command-line flags +// and managing subcommands in the cache-apt-pkgs CLI tool. +package cmdflags + +import ( + "flag" + "os" + "path/filepath" + + "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( + pkgs.Package{Name: "rolldice"}, + pkgs.Package{Name: "xdot", Version: "1.1-2"}, + 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]) + +const ( + helpFlagName = "help" + helpShortFlagName = "h" +) + +// 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), helpFlagName, false, "Show help") + flags.BoolVar(new(bool), helpShortFlagName, false, "Show help (shorthand)") + return flags +}() + +// helpFlagSet checks if the help flag is set in the given flag set. +func helpFlagSet(flags *flag.FlagSet) bool { + for _, name := range []string{helpFlagName, helpShortFlagName} { + if f := flags.Lookup(name); f != nil && f.Value.String() == "true" { + return true + } + } + return false +} diff --git a/cmd/cache_apt_pkgs/doc.go b/cmd/cache_apt_pkgs/doc.go new file mode 100644 index 0000000..edeb2a5 --- /dev/null +++ b/cmd/cache_apt_pkgs/doc.go @@ -0,0 +1,4 @@ +// Package main implements the cache-apt-pkgs GitHub Action. +// This action caches APT packages to speed up CI/CD workflows by avoiding +// repeated package downloads and installations. +package main diff --git a/cmd/cache_apt_pkgs/main_integration_test.go b/cmd/cache_apt_pkgs/main_integration_test.go index d97f2bd..0c92424 100644 --- a/cmd/cache_apt_pkgs/main_integration_test.go +++ b/cmd/cache_apt_pkgs/main_integration_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "os/exec" "path/filepath" @@ -8,11 +9,43 @@ import ( "awalsh128.com/cache-apt-pkgs-action/internal/logging" atesting "awalsh128.com/cache-apt-pkgs-action/internal/testing" + "github.com/awalsh128/syspkg" + "github.com/awalsh128/syspkg/manager" ) +func installAptFastIfMissing(t *testing.T) error { + t.Helper() + registry, err := syspkg.New(syspkg.IncludeOptions{Apt: true}) + if err != nil { + return fmt.Errorf("failed to initialize syspkg: %v", err) + } + + // Get APT package manager (if available) + aptManager, err := registry.GetPackageManager("apt-fast") + if err != nil { + return fmt.Errorf("APT package manager not available: %v", err) + } + + _, err = aptManager.ListInstalledFiles("apt-fast") + if err != nil { + logging.Info("apt-fast not installed, attempting installation.") + _, err := aptManager.Install( + []string{"apt-fast"}, &manager.Options{AssumeYes: true, Verbose: true}, + ) + if err != nil { + return fmt.Errorf("failed to install apt-fast: %v", err) + } + } + return nil +} + // SetupTest performs per-test initialization and registers cleanup hooks. func SetupTest(t *testing.T) { logging.Init(true) + err := installAptFastIfMissing(t) + if err != nil { + t.Fatalf("Aborting testt: %v", err) + } t.Cleanup(func() { logging.InitDefault() }) diff --git a/internal/actions/action.go b/internal/actions/action.go new file mode 100644 index 0000000..df4c1cb --- /dev/null +++ b/internal/actions/action.go @@ -0,0 +1,357 @@ +package actions + +import ( + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +const parserIndentSize = 2 + +// Action represents a complete GitHub Action configuration with all its metadata, +// inputs, outputs, and execution details. +// +// Action corresponds to the structure of an action.yml file and contains all +// necessary information to define a GitHub Action including: +// - Metadata (name, description, author) +// - Branding (icon, color for GitHub marketplace) +// - Inputs (parameters that can be passed to the action) +// - Outputs (values the action produces) +// - Runs (execution configuration and steps) +// +// Example: +// +// action := &Action{ +// Name: "My Action", +// Description: "Does something useful", +// Author: "username", +// Inputs: Inputs{...}, +// Outputs: Outputs{...}, +// Runs: Runs{...}, +// } +// +// Actions can be loaded from YAML files: +// +// action := &Action{} +// err := action.LoadFromFile("action.yml") +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 for display in the +// GitHub marketplace. +// +// The branding information controls how the action appears visually: +// - Icon: The feather icon to use (see GitHub's supported icons) +// - Color: The background color (white, yellow, blue, green, orange, red, purple, gray-dark) +// +// Example: +// +// branding := Branding{ +// Icon: "archive", +// Color: "gray-dark", +// } +type Branding struct { + Icon string `yaml:"icon"` + Color string `yaml:"color"` +} + +// Inputs represents all input parameters for the action as a map of input names +// to their configurations. +// +// Each key in the map is the input name, and the value is the Input configuration +// that defines the parameter's properties. +// +// Example: +// +// inputs := Inputs{ +// "key": Input{ +// Description: "Cache key", +// Required: true, +// }, +// "path": Input{ +// Description: "Files to cache", +// Required: true, +// }, +// } +type Inputs map[string]Input + +// Input represents a single input parameter configuration for an action. +// +// An input defines a parameter that can be passed to the action when it's used. +// It includes: +// - Description: Human-readable description of the parameter +// - Required: Whether the parameter must be provided +// - Default: Default value if not provided (only valid if Required is false) +// - DeprecationMessage: Optional message if the input is deprecated +// +// Example: +// +// input := Input{ +// Description: "An explicit key for a cache entry", +// Required: true, +// } +// +// inputWithDefault := Input{ +// Description: "Enable debug mode", +// Required: false, +// Default: "false", +// } +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 as a map of output +// names to their configurations. +// +// Each key in the map is the output name, and the value is the Output configuration +// that defines what the output represents and how it's computed. +// +// Example: +// +// outputs := Outputs{ +// "cache-hit": Output{ +// Description: "Whether cache was found", +// Value: "${{ steps.cache.outputs.cache-hit }}", +// }, +// } +type Outputs map[string]Output + +// Output represents a single output parameter configuration from an action. +// +// An output defines a value that the action produces which can be used by +// subsequent steps in a workflow. +// - Description: Human-readable description of the output +// - Value: Expression that computes the output value (uses GitHub Actions expression syntax) +// +// Example: +// +// output := Output{ +// Description: "A boolean value to indicate an exact match was found", +// Value: "${{ steps.restore.outputs.cache-hit }}", +// } +type Output struct { + Description string `yaml:"description"` + Value string `yaml:"value"` +} + +// Runs represents the action's execution configuration. +// +// This defines how the action executes, including: +// - Using: The runtime environment (e.g., "composite", "node20", "docker") +// - Env: Environment variables available during execution +// - Steps: Sequence of steps to execute (for composite actions) +// +// For composite actions, Steps contains the commands to run. +// For other action types, this may reference a main entry point. +// +// Example: +// +// runs := Runs{ +// Using: "composite", +// Steps: []Step{ +// {ID: "install", Run: "npm install"}, +// {ID: "test", Run: "npm test"}, +// }, +// } +type Runs struct { + Using string `yaml:"using"` + Main string `yaml:"main,omitempty"` + Env map[string]string `yaml:"env"` + Steps []Step `yaml:"steps"` +} + +// Step represents a single step in the action's execution sequence. +// +// A step can either: +// - Run a shell command (via Run field) +// - Use another action (via Uses field) +// +// Fields: +// - ID: Unique identifier for the step +// - Uses: Reference to another action (e.g., "actions/checkout@v4") +// - With: Input parameters for the referenced action +// - Shell: Shell to use for execution (e.g., "bash", "pwsh") +// - Run: Shell command to execute +// - Env: Environment variables for this step +// +// Example (running a command): +// +// step := Step{ +// ID: "build", +// Shell: "bash", +// Run: "go build -v ./...", +// } +// +// Example (using another action): +// +// step := Step{ +// ID: "cache-restore", +// Uses: "actions/cache/restore@v4", +// With: map[string]string{ +// "key": "my-cache-key", +// "path": "/tmp/cache", +// }, +// } +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 + for k, v := range i { + b.WriteString(fmt.Sprintf("%s:\n", k)) + b.WriteString(indent(v.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 + for k, v := range o { + b.WriteString(fmt.Sprintf("%s:\n", k)) + b.WriteString(indent(v.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*parserIndentSize) + 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 +} diff --git a/internal/actions/cache_actions_test.go b/internal/actions/cache_actions_test.go new file mode 100644 index 0000000..0c52e55 --- /dev/null +++ b/internal/actions/cache_actions_test.go @@ -0,0 +1,172 @@ +package actions + +import ( + "testing" +) + +func TestNewCacheRestoreAction(t *testing.T) { + action := NewCacheRestoreAction() + + // Test basic properties + if action.Name != "Cache restore" { + t.Errorf("Expected name 'Cache restore', got '%s'", action.Name) + } + + if action.Description != "Restore cache without saving it" { + t.Errorf( + "Expected description 'Restore cache without saving it', got '%s'", + action.Description, + ) + } + + if action.Author != "GitHub" { + t.Errorf("Expected author 'GitHub', got '%s'", action.Author) + } + + // Test branding + if action.Branding.Icon != "archive" { + t.Errorf("Expected icon 'archive', got '%s'", action.Branding.Icon) + } + + if action.Branding.Color != "gray-dark" { + t.Errorf("Expected color 'gray-dark', got '%s'", action.Branding.Color) + } + + // Test required inputs + if !action.Inputs["key"].Required { + t.Error("Expected 'key' input to be required") + } + + if !action.Inputs["path"].Required { + t.Error("Expected 'path' input to be required") + } + + // Test optional inputs with defaults + if action.Inputs["fail-on-cache-miss"].Required { + t.Error("Expected 'fail-on-cache-miss' input to be optional") + } + + if action.Inputs["fail-on-cache-miss"].Default != "false" { + t.Errorf( + "Expected 'fail-on-cache-miss' default to be 'false', got '%s'", + action.Inputs["fail-on-cache-miss"].Default, + ) + } + + // Test outputs + expectedOutputs := []string{"cache-hit", "cache-primary-key", "cache-matched-key"} + for _, expectedOutput := range expectedOutputs { + if _, exists := action.Outputs[expectedOutput]; !exists { + t.Errorf("Expected output '%s' to exist", expectedOutput) + } + } + + // Test runs configuration + if action.Runs.Using != "node20" { + t.Errorf("Expected runs.using 'node20', got '%s'", action.Runs.Using) + } + + if action.Runs.Main != "dist/restore/index.js" { + t.Errorf("Expected runs.main 'dist/restore/index.js', got '%s'", action.Runs.Main) + } +} + +func TestNewCacheSaveAction(t *testing.T) { + action := NewCacheSaveAction() + + // Test basic properties + if action.Name != "Cache save" { + t.Errorf("Expected name 'Cache save', got '%s'", action.Name) + } + + if action.Description != "Save cache with key and path" { + t.Errorf( + "Expected description 'Save cache with key and path', got '%s'", + action.Description, + ) + } + + if action.Author != "GitHub" { + t.Errorf("Expected author 'GitHub', got '%s'", action.Author) + } + + // Test branding + if action.Branding.Icon != "archive" { + t.Errorf("Expected icon 'archive', got '%s'", action.Branding.Icon) + } + + if action.Branding.Color != "gray-dark" { + t.Errorf("Expected color 'gray-dark', got '%s'", action.Branding.Color) + } + + // Test required inputs + if !action.Inputs["key"].Required { + t.Error("Expected 'key' input to be required") + } + + if !action.Inputs["path"].Required { + t.Error("Expected 'path' input to be required") + } + + // Test optional inputs + if action.Inputs["upload-chunk-size"].Required { + t.Error("Expected 'upload-chunk-size' input to be optional") + } + + if action.Inputs["enableCrossOsArchive"].Required { + t.Error("Expected 'enableCrossOsArchive' input to be optional") + } + + if action.Inputs["enableCrossOsArchive"].Default != "false" { + t.Errorf( + "Expected 'enableCrossOsArchive' default to be 'false', got '%s'", + action.Inputs["enableCrossOsArchive"].Default, + ) + } + + // Test that save action has no outputs (as per documentation) + if len(action.Outputs) != 0 { + t.Errorf("Expected no outputs for save action, got %d", len(action.Outputs)) + } + + // Test runs configuration + if action.Runs.Using != "node20" { + t.Errorf("Expected runs.using 'node20', got '%s'", action.Runs.Using) + } + + if action.Runs.Main != "dist/save/index.js" { + t.Errorf("Expected runs.main 'dist/save/index.js', got '%s'", action.Runs.Main) + } +} + +func TestCacheRestoreActionString(t *testing.T) { + action := NewCacheRestoreAction() + + // Test that String() method works without panicking + result := action.String() + if result == "" { + t.Error("Expected non-empty string representation") + } + + // Test that ShortString() method works without panicking + shortResult := action.ShortString() + if shortResult == "" { + t.Error("Expected non-empty short string representation") + } +} + +func TestCacheSaveActionString(t *testing.T) { + action := NewCacheSaveAction() + + // Test that String() method works without panicking + result := action.String() + if result == "" { + t.Error("Expected non-empty string representation") + } + + // Test that ShortString() method works without panicking + shortResult := action.ShortString() + if shortResult == "" { + t.Error("Expected non-empty short string representation") + } +} diff --git a/internal/actions/cache_restore.go b/internal/actions/cache_restore.go new file mode 100644 index 0000000..ba14970 --- /dev/null +++ b/internal/actions/cache_restore.go @@ -0,0 +1,113 @@ +package actions + +// NewCacheRestoreAction creates a new cache restore action with default configuration +func NewCacheRestoreAction() *Action { + return &Action{ + Name: "Cache restore", + Description: "Restore cache without saving it", + Author: "GitHub", + Branding: Branding{ + Icon: "archive", + Color: "gray-dark", + }, + Inputs: Inputs{ + "key": Input{ + Description: "An explicit key for a cache entry", + Required: true, + }, + "path": Input{ + Description: "A list of files, directories, and wildcard patterns to restore", + Required: true, + }, + "restore-keys": Input{ + Description: "An ordered list of prefix-matched keys to use for restoring stale cache if no cache hit occurred for key", + Required: false, + }, + "fail-on-cache-miss": Input{ + Description: "Fail the workflow if cache entry is not found", + Required: false, + Default: "false", + }, + "lookup-only": Input{ + Description: "If true, only checks if cache entry exists and skips download", + Required: false, + Default: "false", + }, + "enableCrossOsArchive": Input{ + Description: "An optional boolean when enabled, allows Windows runners to restore caches from other platforms", + Required: false, + Default: "false", + }, + }, + Outputs: Outputs{ + "cache-hit": Output{ + Description: "A boolean value to indicate an exact match was found for the key", + }, + "cache-primary-key": Output{ + Description: "Cache primary key passed in the input to use in subsequent steps of the workflow", + }, + "cache-matched-key": Output{ + Description: "Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys", + }, + }, + Runs: Runs{ + // Actual values for GitHub action. + // Using: "node20", + // Main: "dist/restore/index.js", + Steps: []Step{ + { + ID: "Restore cache", + Uses: "actions/cache@v3", + With: map[string]string{ + "key": "${{ inputs.key }}", + "restore-keys": "${{ inputs.restore-keys }}", + "path": "${{ inputs.path }}", + }, + Shell: "bash", + Run: ` + if [ "${{ inputs.lookup-only }}" = "true" ]; then + echo "Lookup only mode enabled. Skipping cache restore." + exit 0 + fi + + CACHE_DIR="/tmp/cache-apt-pkgs-action-test" + mkdir -p "$CACHE_DIR" + + if [ -d "$CACHE_DIR/${{ inputs.key }}" ]; then + echo "Cache hit for key '${{ inputs.key }}'. Restoring cache..." + find "$CACHE_DIR/${{ inputs.key }}" -type f -exec cp --parents -r {} ./ \; + echo "Cache restored from $CACHE_DIR/${{ inputs.key }}" + echo "cache-hit=true" >> $GITHUB_OUTPUT + echo "cache-primary-key=${{ inputs.key }}" >> $GITHUB_OUTPUT + echo "cache-matched-key=${{ inputs.key }}" >> $GITHUB_OUTPUT + else + if [ -n "${{ inputs.restore-keys }}" ]; then + IFS=',' read -ra RESTORE_KEYS <<< "${{ inputs.restore-keys }}" + for KEY in "${RESTORE_KEYS[@]}"; do + if [ -d "$CACHE_DIR/$KEY" ]; then + echo "Partial cache hit for restore key '$KEY'. Restoring cache..." + find "$CACHE_DIR/$KEY" -type f -exec cp --parents -r {} ./ \; + echo "Cache restored from $CACHE_DIR/$KEY" + echo "cache-hit=false" >> $GITHUB_OUTPUT + echo "cache-primary-key=${{ inputs.key }}" >> $GITHUB_OUTPUT + echo "cache-matched-key=$KEY" >> $GITHUB_OUTPUT + exit 0 + fi + done + fi + echo "No cache found for key '${{ inputs.key }}' or restore keys. Continuing without cache." + echo "cache-hit=false" >> $GITHUB_OUTPUT + echo "cache-primary-key=${{ inputs.key }}" >> $GITHUB_OUTPUT + echo "cache-matched-key=" >> $GITHUB_OUTPUT + + if [ "${{ inputs.fail-on-cache-miss }}" = "true" ]; then + echo "Cache miss and 'fail-on-cache-miss' is set to true. Failing the workflow." + exit 1 + fi + fi + `, + }, + }, + }, + } +} diff --git a/internal/actions/cache_save.go b/internal/actions/cache_save.go new file mode 100644 index 0000000..30a4ffa --- /dev/null +++ b/internal/actions/cache_save.go @@ -0,0 +1,57 @@ +package actions + +// NewCacheSaveAction creates a new cache save action with default configuration +func NewCacheSaveAction() *Action { + return &Action{ + Name: "Cache save", + Description: "Save cache with key and path", + Author: "GitHub", + Branding: Branding{ + Icon: "archive", + Color: "gray-dark", + }, + Inputs: Inputs{ + "key": Input{ + Description: "An explicit key for a cache entry", + Required: true, + }, + "path": Input{ + Description: "A list of files, directories, and wildcard patterns to cache", + Required: true, + }, + "upload-chunk-size": Input{ + Description: "The chunk size used to split up large files during upload, in bytes", + Required: false, + }, + "enableCrossOsArchive": Input{ + Description: "An optional boolean when enabled, allows Windows runners to save caches that can be restored on other platforms", + Required: false, + Default: "false", + }, + }, + Outputs: Outputs{ + // Cache save action has no outputs according to the documentation + }, + Runs: Runs{ + // Actual values for GitHub action. + // Using: "node20", + // Main: "dist/save/index.js", + Steps: []Step{ + { + ID: "Save cache", + Uses: "actions/cache@v3", + With: map[string]string{ + "key": "${{ inputs.key }}", + "path": "${{ inputs.path }}", + }, + Shell: "bash", + Run: ` + mkdir -p /tmp/cache-apt-pkgs-action-test/${key} + find ${path} -type f -exec cp --parents -r {} /tmp/cache-apt-pkgs-action-test/${key} \; + echo "Cache saved to /tmp/cache-apt-pkgs-action-test/${key}" + `, + }, + }, + }, + } +} diff --git a/internal/actions/doc.go b/internal/actions/doc.go new file mode 100644 index 0000000..a390212 --- /dev/null +++ b/internal/actions/doc.go @@ -0,0 +1,70 @@ +// Package actions provides data structures for GitHub Actions configurations +// and factory functions for creating pre-configured cache actions. +// +// This package defines the structure of GitHub Actions (action.yml format) and provides some action +// templates for external action dependencies. At this point it is very minimal and just used by the +// cache-apt-pkgs-action. +// +// # YAML Parsing +// +// Actions can be loaded from YAML files: +// +// action := &Action{} +// err := action.LoadFromFile("action.yml") +// if err != nil { +// log.Fatal(err) +// } +// +// Or parsed from YAML data: +// +// yamlData := []byte(` +// name: My Action +// description: Does something useful +// inputs: +// key: +// description: Cache key +// required: true +// `) +// +// action := &Action{} +// err := yaml.Unmarshal(yamlData, action) +// +// # Usage Examples +// +// Accessing action metadata: +// +// action := NewCacheRestoreAction() +// fmt.Println(action.Name) // "Cache restore" +// fmt.Println(action.Description) // "Restore cache without saving it" +// fmt.Println(action.Author) // "GitHub" +// +// Working with inputs: +// +// keyInput := action.Inputs["key"] +// fmt.Println(keyInput.Required) // true +// fmt.Println(keyInput.Description) // "An explicit key for a cache entry" +// +// // Check if input has a default value +// lookupInput := action.Inputs["lookup-only"] +// if lookupInput.Default != "" { +// fmt.Println("Default:", lookupInput.Default) // "false" +// } +// +// Examining outputs: +// +// cacheHitOutput := action.Outputs["cache-hit"] +// fmt.Println(cacheHitOutput.Description) +// +// # Integration +// +// This package is designed to be used by the parent action2sh package for +// converting GitHub Actions to bash scripts. The action structures provide +// the metadata needed for script generation and validation. +// +// # Compatibility +// +// Action definitions are compatible with: +// - GitHub Actions specification (action.yml format) +// +// All YAML tags follow GitHub Actions conventions for proper serialization. +package actions diff --git a/internal/cio/ghio.go b/internal/cio/ghio.go new file mode 100644 index 0000000..6d1087b --- /dev/null +++ b/internal/cio/ghio.go @@ -0,0 +1,94 @@ +package cio + +import ( + "fmt" + "os" + "strings" + + "awalsh128.com/cache-apt-pkgs-action/internal/pkgs" + "github.com/sethvargo/go-githubactions" +) + +const localPrintPrefix = "ghio::" + +// formatPackages formats a Packages collection as a comma-delimited list with = delimiter +// Format: package1=version1,package2=version2,... +func formatPackages(packages pkgs.Packages) string { + if packages.Len() == 0 { + return "" + } + + var parts []string + for i := 0; i < packages.Len(); i++ { + pkg := packages.Get(i) + parts = append(parts, fmt.Sprintf("%s=%s", pkg.Name, pkg.Version)) + } + + return strings.Join(parts, ",") +} + +// GhPrinter defines an interface for printing outputs in GitHub Actions or locally +type GhPrinter interface { + // SetOutput sets an output variable name and value + SetOutput(name string, value any) +} + +// ghActionEnvPrinter implements GhPrinter for GitHub Actions environment +type ghActionEnvPrinter struct { +} + +// localPrinter implements GhPrinter for local (non-GitHub Actions) environment +// Used in local testing and debugging +type localPrinter struct { +} + +// isGitHubActions checks if the code is running in a GitHub Actions environment +func NewGhPrinter() GhPrinter { + if os.Getenv("GITHUB_ACTIONS") == "true" { + return &ghActionEnvPrinter{} + } + return &localPrinter{} +} + +func (p *ghActionEnvPrinter) SetOutput(name string, value any) { + switch v := value.(type) { + case string: + githubactions.SetOutput(name, v) + case bool: + githubactions.SetOutput(name, fmt.Sprintf("%t", v)) + case pkgs.Packages: + githubactions.SetOutput(name, formatPackages(v)) + default: + githubactions.SetOutput(name, fmt.Sprintf("%v", v)) + } +} + +func (p *localPrinter) SetOutput(name string, value any) { + switch v := value.(type) { + case string: + fmt.Printf("%s%s=%v\n", localPrintPrefix, name, v) + case bool: + fmt.Printf("%s%s=%v\n", localPrintPrefix, name, fmt.Sprintf("%t", v)) + case pkgs.Packages: + fmt.Printf("%s%s=%v\n", localPrintPrefix, name, formatPackages(v)) + default: + fmt.Printf("%s%s=%v\n", localPrintPrefix, name, fmt.Sprintf("%v", v)) + } +} + +// ReadLocalPrinterOutputs reads outputs printed by localPrinter from the given text. +// It returns a map of output names to their values. +// Lines not starting with the localPrintPrefix are ignored. +func ReadLocalPrinterOutputs(text string) map[string]string { + outputs := make(map[string]string) + lines := strings.Split(text, "\n") + for _, line := range lines { + if strings.HasPrefix(line, localPrintPrefix) { + parts := strings.SplitN(line[len(localPrintPrefix):], "=", 2) + if len(parts) == 2 { + outputs[parts[0]] = parts[1] + } + } + } + return outputs +} diff --git a/internal/cio/print_scanner.go b/internal/cio/print_scanner.go new file mode 100644 index 0000000..539f0eb --- /dev/null +++ b/internal/cio/print_scanner.go @@ -0,0 +1,148 @@ +package cio + +import ( + "bufio" + "context" + "io" + "os" + "strings" + "sync" +) + +// PrintScanner scans stdout and stderr for local print prefixes while allowing +// normal terminal output to continue. This is useful for detecting and handling local +// print statements in a non-blocking way. +type PrintScanner struct { + ctx context.Context + cancel context.CancelFunc + origStdout *os.File + origStderr *os.File + rOut, wOut *os.File + rErr, wErr *os.File + prefix string + matches []string + mu sync.Mutex + wg sync.WaitGroup +} + +// NewPrintScanner creates a new scanner that monitors stdout and stderr for the given prefix. +// It sets up pipe redirection while maintaining terminal output. +func NewPrintScanner(prefix string) (*PrintScanner, error) { + ctx, cancel := context.WithCancel(context.Background()) + + // Save original file descriptors + origStdout := os.Stdout + origStderr := os.Stderr + + // Create pipes for stdout and stderr + rOut, wOut, err := os.Pipe() + if err != nil { + cancel() + return nil, err + } + + rErr, wErr, err := os.Pipe() + if err != nil { + cancel() + rOut.Close() + wOut.Close() + return nil, err + } + + scanner := &PrintScanner{ + ctx: ctx, + cancel: cancel, + origStdout: origStdout, + origStderr: origStderr, + rOut: rOut, + wOut: wOut, + rErr: rErr, + wErr: wErr, + prefix: prefix, + } + + return scanner, nil +} + +// Start begins monitoring stdout and stderr in the background. +// It sets up the necessary pipes and starts goroutines for scanning. +func (s *PrintScanner) Start() error { + // Redirect stdout and stderr + os.Stdout = s.wOut + os.Stderr = s.wErr + + // Start monitoring stdout + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.monitorStream(s.rOut, s.origStdout) + }() + + // Start monitoring stderr + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.monitorStream(s.rErr, s.origStderr) + }() + + return nil +} + +// Stop terminates the monitoring and restores original stdout/stderr. +// It waits for all goroutines to complete before returning. +func (s *PrintScanner) Stop() error { + s.cancel() + + // Restore original stdout and stderr + os.Stdout = s.origStdout + os.Stderr = s.origStderr + + // Close write ends of pipes + s.wOut.Close() + s.wErr.Close() + + // Wait for monitoring goroutines to finish + s.wg.Wait() + + // Close read ends of pipes + s.rOut.Close() + s.rErr.Close() + + return nil +} + +// GetMatches returns all captured lines that matched the prefix. +// It's safe to call this method concurrently. +func (s *PrintScanner) GetMatches() []string { + s.mu.Lock() + defer s.mu.Unlock() + result := make([]string, len(s.matches)) + copy(result, s.matches) + return result +} + +// monitorStream reads from the pipe and writes to the original file descriptor +// while scanning for the prefix. This is run in a goroutine for each stream. +func (s *PrintScanner) monitorStream(r *os.File, orig *os.File) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + // Write to original file descriptor for normal output + io.WriteString(orig, line+"\n") + + // Check for prefix and store match + if strings.HasPrefix(line, s.prefix) { + s.mu.Lock() + s.matches = append(s.matches, line) + s.mu.Unlock() + } + + // Check if we should stop + select { + case <-s.ctx.Done(): + return + default: + } + } +} diff --git a/internal/cio/print_scanner_test.go b/internal/cio/print_scanner_test.go new file mode 100644 index 0000000..b889d3c --- /dev/null +++ b/internal/cio/print_scanner_test.go @@ -0,0 +1,95 @@ +package cio + +import ( + "testing" + "time" +) + +func TestPrintScanner(t *testing.T) { + const prefix = "::local::" + + tests := []struct { + name string + writes []string + expected []string + }{ + { + name: "No matching lines", + writes: []string{ + "regular output", + "another line", + }, + expected: []string{}, + }, + { + name: "Some matching lines", + writes: []string{ + "regular output", + "::local::matched line 1", + "another regular line", + "::local::matched line 2", + }, + expected: []string{ + "::local::matched line 1", + "::local::matched line 2", + }, + }, + { + name: "Mixed stdout and stderr", + writes: []string{ + "stdout regular", + "::local::stdout match", + "stderr regular", + "::local::stderr match", + }, + expected: []string{ + "::local::stdout match", + "::local::stderr match", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanner, err := NewPrintScanner(prefix) + if err != nil { + t.Fatalf("Failed to create scanner: %v", err) + } + + err = scanner.Start() + if err != nil { + t.Fatalf("Failed to start scanner: %v", err) + } + + // Write test lines + for _, line := range tt.writes { + println(line) + time.Sleep(10 * time.Millisecond) // Small delay to ensure processing + } + + // Give scanner time to process + time.Sleep(50 * time.Millisecond) + + err = scanner.Stop() + if err != nil { + t.Fatalf("Failed to stop scanner: %v", err) + } + + // Compare results + matches := scanner.GetMatches() + if len(matches) != len(tt.expected) { + t.Errorf("Got %d matches, want %d", len(matches), len(tt.expected)) + } + + for i, match := range matches { + if i >= len(tt.expected) { + t.Errorf("Extra match: %s", match) + continue + } + if match != tt.expected[i] { + t.Errorf("Match %d: got %s, want %s", i, match, tt.expected[i]) + } + } + }) + } +} diff --git a/scripts/dev/export_version.sh b/scripts/dev/export_version.sh new file mode 100755 index 0000000..3fda38b --- /dev/null +++ b/scripts/dev/export_version.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +#============================================================================== +# export_version.sh +#============================================================================== +# +# DESCRIPTION: +# Script to export Go library version information for package development. +# Extracts and exports version information from go.mod including Go version, +# toolchain version, and syspkg version. +# +# USAGE: +# export_version.sh [OPTIONS] +# +# 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 + +# Function to extract Go version from go.mod +function get_go_version() { + local go_version + go_version=$(grep "^go " "${PROJECT_ROOT}/go.mod" | awk '{print $2}') + log_debug "Extracted Go version: ${go_version}" + echo "${go_version}" +} + +# Function to extract toolchain version from go.mod +function get_toolchain_version() { + local toolchain_version + toolchain_version=$(grep "^toolchain " "${PROJECT_ROOT}/go.mod" | awk '{print $2}') + log_debug "Extracted toolchain version: ${toolchain_version}" + echo "${toolchain_version}" +} + +# Function to extract syspkg version from go.mod +function get_syspkg_version() { + local syspkg_version + syspkg_version=$(grep "github.com/awalsh128/syspkg" "${PROJECT_ROOT}/go.mod" | awk '{print $2}') + log_debug "Extracted syspkg version: ${syspkg_version}" + echo "${syspkg_version}" +} + +# Export versions as environment variables +log_info "Exporting version information..." +GO_VERSION=$(get_go_version) +export GO_VERSION +TOOLCHAIN_VERSION=$(get_toolchain_version) +export TOOLCHAIN_VERSION +SYSPKG_VERSION=$(get_syspkg_version) +export SYSPKG_VERSION + +# Create a version info file +VERSION_FILE="${PROJECT_ROOT}/.version-info" +log_debug "Creating version file: ${VERSION_FILE}" +cat >"${VERSION_FILE}" <"${VERSION_JSON}" </dev/null # prevent return from echo'ng + +print_status "Running trunk format and code check..." +if ! command_exists trunk; then + print_status "Installing trunk..." + # trunk-ignore(semgrep/bash.curl.security.curl-pipe-bash.curl-pipe-bash) + curl https://get.trunk.io -fsSL | bash +fi +trunk check --all --ci +trunk fmt --all --ci + +print_status "Checking for table of content updates in markdown files..." +"${REPO_DIR}"/scripts/dev/update_md_tocs.sh + +log_success "All fixes applied and checks complete." diff --git a/scripts/dev/lib.sh b/scripts/dev/lib.sh new file mode 100755 index 0000000..49d3932 --- /dev/null +++ b/scripts/dev/lib.sh @@ -0,0 +1,370 @@ +#!/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' + +function 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 +#============================================================================== + +function log_info() { + if ! ${QUIET}; then + echo -e "${BLUE}[INFO]${NC} $1" + fi +} + +function log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" >&2 +} + +function log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +function log_success() { + if ! ${QUIET}; then + echo -e "${GREEN}[SUCCESS]${NC} $1" + fi +} + +function log_debug() { + if ${VERBOSE}; then + echo -e "${DIM}[DEBUG]${NC} $1" >&2 + fi +} + +# Print formatted headers +function print_header() { + if ! ${QUIET}; then + echo -en "\n${BOLD}${BLUE}$1${NC}\n" + fi +} + +function print_section() { + if ! ${QUIET}; then + echo -en "\n${CYAN}${BOLD}$1${NC}\n\n" + fi +} + +function print_option() { + if ! ${QUIET}; then + echo -en "${YELLOW}$1)${CYAN} $2${NC}\n" + fi +} + +function print_status() { + if ! ${QUIET}; then + echo -en "${GREEN}==>${NC} $1\n" + fi +} + +function print_success() { + if ! ${QUIET}; then + echo -en "${GREEN}${BOLD}$1${NC}\n" + fi +} + +#============================================================================== +# Error Handling +#============================================================================== + +function 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 +function 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}" +} + +function setup_cleanup() { + trap 'cleanup_on_exit' EXIT +} + +#============================================================================== +# Command and Dependency Checking +#============================================================================== + +function command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +function 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}" +} + +function 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}" +} + +function npm_package_installed() { + npm list -g "$1" >/dev/null 2>&1 +} + +function go_tool_installed() { + go list -m "$1" >/dev/null 2>&1 || command_exists "$(basename "$1")" +} + +#============================================================================== +# File and Directory Operations +#============================================================================== + +function file_exists() { + [[ -f $1 ]] +} + +function dir_exists() { + [[ -d $1 ]] +} + +function ensure_dir() { + [[ ! -d $1 ]] && mkdir -p "$1" + log_debug "Ensured directory exists: $1" +} + +function create_temp_dir() { + TEMP_DIR=$(mktemp -d) + log_debug "Created temporary directory: ${TEMP_DIR}" + echo "${TEMP_DIR}" +} + +function safe_remove() { + local path="$1" + if [[ -e ${path} ]]; then + rm -rf "${path}" + log_debug "Removed: ${path}" + fi +} + +#============================================================================== +# Project Structure +#============================================================================== + +function 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 +#============================================================================== + +function install_trunk() { + if command_exists trunk; then + log_debug "trunk already installed" + return 0 + fi + + log_info "Installing trunk..." + # trunk-ignore(semgrep/bash.curl.security.curl-pipe-bash.curl-pipe-bash) + curl -fsSL https://get.trunk.io | bash + log_success "trunk installed successfully" +} + +function 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" +} + +function 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 +#============================================================================== + +function 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" +} + +function 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 Operations +#============================================================================== + +function 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 +} + +function update_go_modules() { + run_with_status "Updating Go modules" "go mod tidy && go mod verify" +} + +function run_tests() { + run_with_status "Running tests" "go test -v ./..." +} + +function run_build() { + run_with_status "Building project" "go build -v ./..." +} + +function run_lint() { + require_command trunk "Please install trunk first" + run_with_status "Running linting" "trunk check" +} + +#============================================================================== +# Initialization +#============================================================================== + +# Set up cleanup trap when library is sourced +setup_cleanup + +function 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. diff --git a/scripts/dev/menu.sh b/scripts/dev/menu.sh new file mode 100755 index 0000000..aca46c3 --- /dev/null +++ b/scripts/dev/menu.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +#============================================================================== +# menu.sh +#============================================================================== +# +# DESCRIPTION: +# Streamlined interactive menu for essential development tasks. Provides +# quick access to the most commonly used development operations. +# +# USAGE: +# menu.sh +# +# OPTIONS: +# -v, --verbose Enable verbose output +# -h, --help Show this help message +#============================================================================== + +source "$(git rev-parse --show-toplevel)/scripts/lib.sh" +SCRIPT_DIR="${PROJECT_ROOT}/scripts/dev" +CAP_CMD_DIR="${PROJECT_ROOT}/cmd/cache_apt_pkgs" + +parse_common_args "$@" >/dev/null # prevent return from echo'ng + +#============================================================================== +# Menu Operations +#============================================================================== + +function 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 show_project_status() { + print_header "Project Status" + + echo "Git Status:" + git status --short --branch + echo + + echo "Go Module Status:" + go mod verify && log_success "Go modules are valid" + echo + + if command_exists trunk; then + echo "Linting Status:" + trunk check --no-fix --quiet || log_warn "Linting issues detected" + echo + fi + + pause +} + +#============================================================================== +# Main Menu Loop +#============================================================================== + +function main_menu() { + while true; do + clear + print_header "Cache Apt Packages - Development Menu" + + print_section "Essential Tasks:" + print_option 1 "Setup Development Environment" + print_option 2 "Run All Checks (test + lint + build)" + print_option 3 "Test Only" + print_option 4 "Lint & Fix" + print_option 5 "Build Project" + + 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 + print_option q "Quit" + echo + + echo_color -n green "choice > " + read -n 1 -rp "" choice + printf "\n\n" + + case ${choice} in + 1) + run_task "Setting up development environment" \ + "${SCRIPT_DIR}/setup_dev.sh" + ;; + 2) + print_header "Running All Checks" + echo "" + run_task "Running linting" "trunk check --fix" + run_task "Building project" "go build -v ${CAP_CMD_DIR}" + run_task "Running tests" "go test -v ${CAP_CMD_DIR}" + ;; + 3) + run_task "Running tests" "go test -v ${CAP_CMD_DIR}" + ;; + 4) + run_task "Running lint with fixes" "trunk check --fix" + ;; + 5) + run_task "Building project" "go build -v ${CAP_CMD_DIR}" + ;; + 6) + run_task "Updating documentation TOCs" \ + "${SCRIPT_DIR}/update_md_tocs.sh" + ;; + 7) + run_task "Exporting version information" \ + "${SCRIPT_DIR}/export_version.sh" + ;; + 8) + show_project_status + ;; + 9) + print_header "Recent Changes" + git log --oneline --graph --decorate -n 10 + pause + ;; + q | Q | "") + echo -e "${GREEN}Goodbye!${NC}" + exit 0 + ;; + *) + echo "" + log_error "Invalid option: ${choice}" + pause + ;; + esac + 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 diff --git a/scripts/dev/run_local_fake_action.sh b/scripts/dev/run_local_fake_action.sh new file mode 100644 index 0000000..a9bf588 --- /dev/null +++ b/scripts/dev/run_local_fake_action.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/scripts/dev/setup_dev.sh b/scripts/dev/setup_dev.sh new file mode 100755 index 0000000..71d7980 --- /dev/null +++ b/scripts/dev/setup_dev.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +#============================================================================== +# setup_dev.sh +#============================================================================== +# +# 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. +# +# USAGE: +# setup_dev.sh [options] +# +# OPTIONS: +# -v, --verbose Enable verbose output +# -h, --help Show this help message +#============================================================================== + +source "$(git rev-parse --show-toplevel)/scripts/lib.sh" + +parse_common_args "$@" + +#============================================================================== +# Setup Functions +#============================================================================== + +function 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 setup_go_environment() { + validate_go_project + + print_status "Configuring Go environment" + go env -w GO111MODULE=auto + + update_go_modules +} + +function install_development_tools() { + print_status "Installing development tools" + + install_trunk + install_doctoc + install_go_tools + + log_success "All development tools installed" +} + +function setup_git_hooks() { + validate_git_repo + + print_status "Setting up Git hooks" + + # Initialize trunk if not already done + if [[ ! -f .trunk/trunk.yaml ]]; then + log_info "Initializing trunk configuration" + trunk init + fi + + # Configure git hooks + git config core.hooksPath .git/hooks/ + + log_success "Git hooks configured" +} + +function update_project_documentation() { + print_status "Updating project documentation" + + local update_script="${SCRIPT_DIR}/update_md_tocs.sh" + if [[ -x ${update_script} ]]; then + "${update_script}" + else + log_warn "Markdown TOC update script not found or not executable" + fi +} + +function run_initial_checks() { + print_status "Running initial project validation" + + # Run trunk check + 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" +} + +function 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/dev/update_md_tocs.sh" + echo " • Interactive menu: ./scripts/dev/menu.sh" + echo + log_success "Ready for development!" +} + +#============================================================================== +# Main Setup Process +#============================================================================== + +function main() { + # Parse command line arguments first + while [[ $# -gt 0 ]]; do + case $1 in + -v | --verbose) + export VERBOSE=true + ;; + -h | --help) + show_help + 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 "$@" diff --git a/scripts/dev/tests/export_version_test.sh b/scripts/dev/tests/export_version_test.sh new file mode 100755 index 0000000..204db54 --- /dev/null +++ b/scripts/dev/tests/export_version_test.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +#============================================================================== +# export_version_test.sh +#============================================================================== +# +# DESCRIPTION: +# Test suite for export_version.sh script. +# Validates version extraction, file generation, and error handling. +# +# USAGE: +# export_version_test.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/dev/tests/test_lib.sh" + +# Define test functions +function run_tests() { + test_section "Command Line Interface" + + test_case "basic execution" \ + "" \ + "Exporting version information" \ + true + + test_section "File Generation" + + test_case "version info file creation" \ + "" \ + "Version information has been exported" \ + true + + test_case "JSON file creation" \ + "" \ + "exported in JSON format" \ + true + + test_section "File Contents Validation" + + local project_root + project_root=$(get_project_root) + # Test that files exist and contain expected content + test_file_exists "version info file exists" "${project_root}/.version-info" + test_file_exists "JSON version file exists" "${project_root}/.version-info.json" + + test_file_contains "version file contains Go version" \ + "${project_root}/.version-info" \ + "GO_VERSION=" + + test_file_contains "JSON file contains Go version" \ + "${project_root}/.version-info.json" \ + '"goVersion":' +} + +# Start the test framework and run tests +start_tests "$@" +run_tests diff --git a/scripts/dev/tests/setup_dev_test.sh b/scripts/dev/tests/setup_dev_test.sh new file mode 100755 index 0000000..3b09fbc --- /dev/null +++ b/scripts/dev/tests/setup_dev_test.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +#============================================================================== +# setup_dev_test.sh +#============================================================================== +# +# DESCRIPTION: +# Test script for setup_dev.sh functionality. +# Validates development environment setup without modifying the actual system. +# +# USAGE: +# setup_dev_test.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/dev/tests/test_lib.sh" + +# Define test functions +function run_tests() { + test_section "Help and Usage" + + test_case "shows help message" \ + "--help" \ + "USAGE:" \ + true + + test_case "shows error for invalid option" \ + "--invalid-option" \ + "Unknown option" \ + false + + test_section "Argument Processing" + + test_case "accepts verbose flag" \ + "--verbose --help" \ + "USAGE:" \ + true +} + +# Start the test framework and run tests +start_tests "$@" +run_tests diff --git a/scripts/dev/update_md_tocs.sh b/scripts/dev/update_md_tocs.sh new file mode 100755 index 0000000..715d8a5 --- /dev/null +++ b/scripts/dev/update_md_tocs.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +#============================================================================== +# update_md_tocs.sh +#============================================================================== +# +# DESCRIPTION: +# Automatically updates table of contents in all markdown files that contain +# doctoc markers. The script handles installation of doctoc if not present +# and applies consistent formatting across all markdown files. +# +# USAGE: +# update_md_tocs.sh [OPTIONS] +# +# FEATURES: +# - Auto-detects markdown files with doctoc markers +# - Installs Node.js and npm automatically if not present (on Debian/Ubuntu +# systems) +# - Installs doctoc if not present (requires npm) +# - Applies consistent settings across all files: +# * Excludes document title +# * Includes headers up to level 4 +# * Uses GitHub-compatible links +# - Provides clear progress and error feedback +# +# TO ADD TOC TO A NEW FILE: +# Add these markers to your markdown: +# +# +# +# +# +# +# DEPENDENCIES: +# - Node.js and npm (will be installed automatically if missing on +# Debian/Ubuntu systems) +# - doctoc (will be installed automatically if missing) +# +# EXIT CODES: +# 0 - Success +# 1 - Missing dependencies or installation failure +# +# NOTES: +# - Only processes files containing doctoc markers +# - Preserves existing markdown content +# - Safe to run multiple times +#============================================================================== + +source "$(git rev-parse --show-toplevel)/scripts/dev/lib.sh" + +# Install doctoc if not present +if ! command doctoc; then + log_info "doctoc not found. Installing..." + if ! command npm; then + log_info "npm not found. Installing Node.js and npm..." + if command apt-get; then + if ! sudo apt-get update && sudo apt-get install -y nodejs npm; then + fail "Failed to install Node.js and npm via apt-get" + fi + else + fail "Unable to install npm: apt-get not available. Please install Node.js and npm manually." + fi + fi + + if ! npm_package_installed doctoc; then + if ! npm install -g doctoc; then + fail "Failed to install doctoc" + fi + fi +fi + +print_status "Updating table of contents in markdown files..." +# Find all markdown files that contain doctoc markers +find . -type f -name "*.md" -exec grep -l "START doctoc" {} \; | while read -r file; do + log_info "Processing: ${file}" + if ! doctoc --maxlevel 4 --no-title --notitle --github "${file}"; then + log_error "Failed to update TOC in ${file}" + fi +done + +print_success "Table of contents update complete!" diff --git a/scripts/dev/update_trunkio.sh b/scripts/dev/update_trunkio.sh new file mode 100755 index 0000000..c122953 --- /dev/null +++ b/scripts/dev/update_trunkio.sh @@ -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/dev/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 diff --git a/scripts/distribute.sh b/scripts/distribute.sh new file mode 100644 index 0000000..10f88a3 --- /dev/null +++ b/scripts/distribute.sh @@ -0,0 +1,157 @@ +#!/bin/bash + +#============================================================================== +# distribute.sh +#============================================================================== +# +# DESCRIPTION: +# Build, package, and verify cache-apt-pkgs-action release artifacts for all +# supported architectures. This includes version generation, apt-fast +# cloning, binary building, checksum generation, and artifact reorganization. +# +# USAGE: +# distribute.sh [args] +# +# COMMANDS: +# generate-version Output version and commit info +# create-distribute-directory Create output directory for architecture +# clone-apt-fast Clone apt-fast repository +# build-binary Build binary for GOOS/GOARCH/GOARM +# generate-checksums Generate checksums for binaries +# verify-build Verify build output for architecture +# reorganize-artifacts Reorganize build artifacts +# +# OPTIONS: +# -h, --help Show this help message +#============================================================================== + +set -eEuo pipefail + +source "$(git rev-parse --show-toplevel)/scripts/lib.sh" + +log_info "Using shared library functions for enhanced logging and utilities." + +# Generate version from commit SHA +function generate_version() { + local commit_sha="${GITHUB_SHA:0:8}" + local version="${VERSION_PREFIX}-${commit_sha}" + echo "version=${version}" + echo "commit_sha=${commit_sha}" + echo "full_sha=${GITHUB_SHA}" +} + +# Create distribute directory +function create_distribute_directory() { + local arch="$1" + mkdir -p "distribute/${arch}" +} + +# Clone apt-fast repository https://github.com/ilikenwf/apt-fast +function clone_apt_fast() { + mkdir -p distribute/apt-fast + git clone https://github.com/ilikenwf/apt-fast.git temp-apt-fast + pushd temp-apt-fast + git checkout 607f8ca5be31f5c45ebd5f6a47f724a07e49894b # v1.11.0 + local checksum + checksum=$(sha256sum apt-fast | awk '{print $1}') + log_info "Checksum: ${checksum}" + cp -r ./* ../distribute/apt-fast/ + popd + rm -rf temp-apt-fast +} + +# Build binary for specific GOOS and GOARCH +function build_binary() { + local goos="$1" + local goarch="$2" + local goarm="$3" + local arch="$4" + + export GOOS="${goos}" + export GOARCH="${goarch}" + export GOARM="${goarm}" + export CGO_ENABLED=0 + + local binary_name="cache_apt_pkgs" + if [[ ${goos} == "windows" ]]; then + binary_name="${binary_name}.exe" + fi + + local build_flags="-ldflags=-s -w -X main.version=${version} -X main.commit=${full_sha}" + + go build "${build_flags}" -o "distribute/${arch}/${binary_name}" ./cmd/cache_apt_pkgs + + # Make executable (no-op on Windows but harmless) + chmod +x "distribute/${arch}/${binary_name}" +} + +# Generate checksums for binaries +function generate_checksums() { + local arch="$1" + pushd "distribute/${arch}" + for file in *; do + if [[ -f ${file} ]] && [[ ${file} != *.sha256 ]]; then + sha256sum "${file}" | awk '{print $1}' >"${file}.sha256" + log_info "Generated checksum for ${file}" + fi + done + popd +} + +# Verify build output +function verify_build() { + local arch="$1" + ls -la "distribute/${arch}/" + if [[ -f "distribute/${arch}/cache_apt_pkgs" ]]; then + log_info "Binary built successfully" + file "distribute/${arch}/cache_apt_pkgs" + else + log_error "Binary not found!" + exit 1 + fi +} + +# Reorganize artifacts +function reorganize_artifacts() { + mkdir -p distribute + for arch_dir in distribute-artifacts/cache-apt-pkgs-*; do + if [[ -d ${arch_dir} ]]; then + local arch_name + arch_name=$(basename "${arch_dir}" | sed 's/cache-apt-pkgs-\(.*\)-[a-f0-9]*/\1/') + cp -r "${arch_dir}"/* "distribute/${arch_name}/" 2>/dev/null || mkdir -p "distribute/${arch_name}" + cp "${arch_dir}"/* "distribute/${arch_name}/" 2>/dev/null || true + fi + done + + log_info "Final distribute structure:" + find distribute -type f -exec ls -la {} \; +} + +# Main script logic +case "$1" in +generate-version) + generate_version + ;; +create-distribute-directory) + create_distribute_directory "$2" + ;; +clone-apt-fast) + clone_apt_fast + ;; +build-binary) + build_binary "$2" "$3" "$4" "$5" + ;; +generate-checksums) + generate_checksums "$2" + ;; +verify-build) + verify_build "$2" + ;; +reorganize-artifacts) + reorganize_artifacts + ;; +*) + log_error "Unknown command: $1" + exit 1 + ;; +esac diff --git a/scripts/lib.sh b/scripts/lib.sh index 01dfc52..89ff99c 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -86,23 +86,23 @@ export BLINK='\033[5m' # Blinking text # echo_color green "Operation successful!" # echo_color -n blue "Processing..." function echo_color() { - local echo_flags=() - # Collect valid echo flags that start with dash - while [[ $1 == -* ]]; do - if [[ $1 == "-e" || $1 == "-n" ]]; then - echo_flags+=("$1") - fi - shift - done + local echo_flags=() + # Collect valid echo flags that start with dash + while [[ $1 == -* ]]; do + if [[ $1 == "-e" || $1 == "-n" ]]; then + echo_flags+=("$1") + fi + shift + done - # Convert color name to uppercase variable name - local color="$1" - local color_var - color_var=$(echo "${color}" | tr '[:lower:]' '[:upper:]') - shift + # Convert color name to uppercase variable name + local color="$1" + local color_var + color_var=$(echo "${color}" | tr '[:lower:]' '[:upper:]') + shift - # Print message with color codes and any specified flags - echo -e "${echo_flags[@]}" "${!color_var}$*${NC}" + # Print message with color codes and any specified flags + echo -e "${echo_flags[@]}" "${!color_var}$*${NC}" } #============================================================================== @@ -117,9 +117,9 @@ function echo_color() { # message The information to log # Respects: QUIET=true will suppress output function log_info() { - if ! ${QUIET}; then - echo -e "${BLUE}[INFO]${NC} $1" - fi + if ! ${QUIET}; then + echo -e "${BLUE}[INFO]${NC} $1" + fi } # Log a warning message to stderr @@ -127,7 +127,7 @@ function log_info() { # message The warning to log # Notes: Not affected by QUIET mode function log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" >&2 + echo -e "${YELLOW}[WARN]${NC} $1" >&2 } # Log an error message to stderr @@ -135,7 +135,7 @@ function log_warn() { # message The error message to log # Notes: Not affected by QUIET mode function log_error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 + echo -e "${RED}[ERROR]${NC} $1" >&2 } # Log a success message to stdout @@ -143,9 +143,9 @@ function log_error() { # message The success message to log # Respects: QUIET=true will suppress output function log_success() { - if ! ${QUIET}; then - echo -e "${GREEN}[SUCCESS]${NC} $1" - fi + if ! ${QUIET}; then + echo -e "${GREEN}[SUCCESS]${NC} $1" + fi } # Log a debug message to stderr if verbose mode is enabled @@ -153,9 +153,9 @@ function log_success() { # message The debug information to log # Requires: VERBOSE=true to display output function log_debug() { - if ${VERBOSE}; then - echo -e "${DIM}[DEBUG]${NC} $1" >&2 - fi + if ${VERBOSE}; then + echo -e "${DIM}[DEBUG]${NC} $1" >&2 + fi } # Print a formatted section header with proper spacing @@ -163,9 +163,9 @@ function log_debug() { # text The header text to display # Respects: QUIET=true will suppress output function print_header() { - if ! ${QUIET}; then - echo -en "\n${BOLD}${BLUE}$1${NC}\n" - fi + if ! ${QUIET}; then + echo -en "\n${BOLD}${BLUE}$1${NC}\n" + fi } #============================================================================== @@ -185,29 +185,29 @@ function print_header() { # - Removes comment markers (#) and formats for clean display # - Returns early with message if script file cannot be found function show_help() { - # Extract header comment block from calling script - local script_file="${BASH_SOURCE[1]}" + # Extract header comment block from calling script + local script_file="${BASH_SOURCE[1]}" - if [[ ! -f ${script_file} ]]; then - echo "Help information not available" - return - fi + if [[ ! -f ${script_file} ]]; then + echo "Help information not available" + return + fi - # Process the header block and format output - 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}" + # Process the header block and format output + 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}" } # Process common command-line arguments used across all scripts @@ -221,35 +221,35 @@ function show_help() { # Prints any unhandled arguments to stdout for capture by caller # Returns 0 on success function 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 - ;; - *) - # Stop at first non-flag argument - break - ;; - esac - done + 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 + ;; + *) + # Stop at first non-flag argument + break + ;; + esac + done - # Return any unprocessed arguments to caller - if [[ $# -gt 0 ]]; then - echo "$@" - fi - return 0 + # Return any unprocessed arguments to caller + if [[ $# -gt 0 ]]; then + echo "$@" + fi + return 0 } #============================================================================== @@ -266,10 +266,10 @@ function parse_common_args() { # - Automatically skipped if QUIET=true # - Adds newlines before and after prompt for clean formatting function pause() { - [[ ${QUIET} == true ]] && return - echo - read -n 1 -s -r -p "Press any key to continue..." - echo + [[ ${QUIET} == true ]] && return + echo + read -n 1 -s -r -p "Press any key to continue..." + echo } # Prompt user for yes/no confirmation @@ -284,15 +284,15 @@ function pause() { # - Accepts y, yes, n, no (case insensitive) # - Repeats prompt until valid input received function confirm() { - local prompt="${1:-Are you sure?}" - local response + 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 + 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 } diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..1f9743d --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +#============================================================================== +# setup.sh +#============================================================================== +# +# DESCRIPTION: +# Validates binary existence, checksum, and action pinning for +# cache-apt-pkgs-action in CI/CD workflows. Provides functions for checksum +# reading, SHA256 calculation, and pinning validation. +# +# USAGE: +# setup.sh [OPTIONS] +# +# OPTIONS: +# -h, --help Show this help message +# -v, --verbose Enable verbose output +#============================================================================== + +set -euo pipefail + +source "$(git rev-parse --show-toplevel)/scripts/lib.sh" + +## +# Reads and trims a checksum file, errors if empty. +# Arguments: +# $1 - Path to checksum file +# Returns: +# Trimmed checksum string, or exits with error if file is missing/empty. +function read_checksum() { + local path="$1" + if [[ ! -f ${path} ]]; then + log_error "Checksum file ${path} does not exist" + return 1 + fi + local trimmed + trimmed="$(tr <"${path}" -d '\n' | xargs)" + if [[ -z ${trimmed} ]]; then + log_error "Checksum file ${path} is empty" + return 1 + fi + echo "${trimmed}" +} + +## +# Computes SHA256 checksum of a file. +# Arguments: +# $1 - Path to file +# Returns: +# SHA256 checksum string, or exits with error if file is missing. +function compute_checksum() { + local path="$1" + if [[ ! -f ${path} ]]; then + log_error "File ${path} does not exist" + return 1 + fi + sha256sum "${path}" | awk '{print $1}' +} + +## +# Checks if a string is a 40-character hex SHA. +# Arguments: +# $1 - String to check +# Returns: +# 0 if valid hex SHA, 1 otherwise. +function is_hex_sha() { + local value="$1" + [[ ${#value} -eq 40 ]] && [[ ${value} =~ ^[0-9a-fA-F]{40}$ ]] +} + +## +# Ensures the action is pinned to a tag or commit. +# Checks GITHUB_ACTION_REF and GITHUB_ACTION_REF_TYPE to ensure the action +# is not referencing a branch. Logs info or error and returns appropriate code. +function ensure_action_is_pinned() { + local ref="${GITHUB_ACTION_REF-}" # e.g. refs/tags/v1.2.3 or commit SHA + local ref_type="${GITHUB_ACTION_REF_TYPE-}" # branch, tag, commit + case "${ref_type,,}" in + branch) + log_error "GitHub workflow must pin awalsh128/cache-apt-pkgs-action to a specific tag or commit; current reference '${ref}' resolves to a branch" + return 1 + ;; + tag) + log_info "Action is pinned to release tag ${ref}" + return 0 + ;; + commit) + log_info "Action is pinned to commit ${ref}" + return 0 + ;; + *) + # Unknown ref type — fall through to additional detection below. + # Intentionally do not return here so later checks can inspect GITHUB_ACTION_REF + # and determine if the reference is a tag, commit SHA, or a branch. + ;; + esac + if [[ -z ${ref} ]]; then + log_info "Action is pinned to a commit SHA (GITHUB_ACTION_REF not provided)" + return 0 + fi + if is_hex_sha "${ref}"; then + log_info "Action is pinned to commit ${ref}" + return 0 + fi + if [[ ${ref} == refs/tags/* ]]; then + log_info "Action is pinned to release tag ${ref#refs/tags/}" + return 0 + fi + if [[ ${ref,,} == refs/heads/* ]]; then + log_error "GitHub workflow must pin awalsh128/cache-apt-pkgs-action to a specific tag or commit; current reference '${ref}' resolves to a branch" + return 1 + fi + log_error "GitHub workflow must pin awalsh128/cache-apt-pkgs-action to a specific tag or commit; unable to determine reference type for '${ref}'" + return 1 +} + +# Main script logic: expects 2 arguments +# $1 - binary path +# $2 - checksum file path +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 1 + fi + binary_path="$1" + checksum_file="$2" + + # Validate binary existence + if [[ ! -f ${binary_path} ]]; then + log_error "Binary not found: ${binary_path}" + exit 1 + fi + + # Validate checksum file existence and contents + expected_checksum="$(read_checksum "${checksum_file}")" + actual_checksum="$(compute_checksum "${binary_path}")" + if [[ ${expected_checksum} != "${actual_checksum}" ]]; then + log_error "Checksum mismatch for ${binary_path}" + log_error "Expected: ${expected_checksum}" + log_error "Actual: ${actual_checksum}" + exit 1 + fi + + # Ensure action is pinned + ensure_action_is_pinned +fi diff --git a/scripts/template.sh b/scripts/template.sh index 3a4ae0f..93b5d36 100755 --- a/scripts/template.sh +++ b/scripts/template.sh @@ -32,6 +32,19 @@ source "$(git rev-parse --show-toplevel)/scripts/lib.sh" # ... or for development specific functions # source "$(git rev-parse --show-toplevel)/scripts/dev/lib.sh" +## +# Process command line arguments and perform main script functionality. +# Arguments: +# $1 your variable to handle +# Returns: +# 0 on success, non-zero on failure +function handle_your_var() { + local your_var="$1" + print_status "Handling your_var: ${your_var}" + # your code here + return 0 +} + ## # Process command line arguments and perform main script functionality. # Arguments: @@ -39,30 +52,32 @@ source "$(git rev-parse --show-toplevel)/scripts/lib.sh" # Returns: # 0 on success, non-zero on failure function main() { - # Parse common args (verbose, help) first - parse_common_args "$@" - shift $((OPTIND - 1)) + # Parse common args (verbose, help) first + parse_common_args "$@" + shift $((OPTIND - 1)) - # Process remaining arguments - while [[ $# -gt 0 ]]; do - case $1 in - -yv | --your_var) - if [[ -z ${2-} ]]; then - log_error "Missing value for $1" - return 1 - fi - local your_var="$2" - shift 2 - ;; - *) - log_error "Unknown option: $1" - log_info "Use --help for usage information." - return 1 - ;; - esac - done + # Process remaining arguments + while [[ $# -gt 0 ]]; do + case $1 in + -yv | --your_var) + if [[ -z ${2-} ]]; then + log_error "Missing value for $1" + return 1 + fi + local your_var + your_var="$2" + handle_your_var "${your_var}" || return 1 + shift 2 + ;; + *) + log_error "Unknown option: $1" + log_info "Use --help for usage information." + return 1 + ;; + esac + done - # your code here + # your code here } main "$@" diff --git a/scripts/template_test.sh b/scripts/template_test.sh new file mode 100755 index 0000000..dd7f4bb --- /dev/null +++ b/scripts/template_test.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +#============================================================================== +#