More stuff

This commit is contained in:
awalsh128 2026-01-05 16:36:23 -08:00
parent fc79483542
commit 3643802011
114 changed files with 6573 additions and 789 deletions

View file

@ -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

4
.env
View file

@ -1,3 +1,3 @@
GO111MODULE=auto
GO_TOOLCHAIN=go1.23.5
GO_VERSION=1.23.5
GO_TOOLCHAIN=go1.24
GO_VERSION=1.24

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

116
.github/workflows/build-distribute.yml vendored Normal file
View file

@ -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 }}

23
.github/workflows/pr.yml vendored Normal file
View file

@ -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

View file

@ -1 +0,0 @@
dist/**/*

View file

@ -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

View file

@ -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

View file

@ -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"]

View file

@ -1,7 +1,7 @@
enable=all
source-path=SCRIPTDIR
disable=SC1090
disable=SC1091
disable=SC2154
disable=SC2310
disable=SC2312
# If you're having issues with shellcheck following source, disable the errors via:
# disable=SC1090
# disable=SC1091

View file

@ -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/**

View file

@ -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

View file

@ -1,6 +0,0 @@
Goroutine
goroutines
Mutex
mutexes
heredoc
Profiler

View file

@ -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

View file

View file

@ -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

View file

@ -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

View file

@ -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]\.'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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+'

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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}'

View file

@ -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

View file

@ -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'

View file

@ -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:
- '\.\.\.'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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}'

View file

@ -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]'

View file

@ -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][.?!]$"

View file

@ -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

View file

@ -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'

View file

@ -0,0 +1,2 @@
# Minimal placeholder for Microsoft-style rules
version = 1

View file

@ -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'

View file

@ -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

View file

@ -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+[.?!]'

View file

@ -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

View file

@ -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'

View file

@ -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\)'

View file

@ -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:
- '["“][^"”“]+["”][.,]'

View file

@ -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'

View file

@ -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:
- ";"

View file

@ -0,0 +1,6 @@
extends: occurrence
message: "Try to keep sentences short (< 30 words)."
scope: sentence
level: suggestion
max: 30
token: \b(\w+)\b

View file

@ -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]"

View file

@ -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+-'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/Microsoft/releases.atom",
"vale_version": ">=1.0.0"
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,11 @@
extends: repetition
message: "'%s' is repeated!"
level: warning
alpha: true
action:
name: edit
params:
- truncate
- " "
tokens:
- '[^\s]+'

View file

@ -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

View file

@ -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.
```

View file

@ -0,0 +1,5 @@
extends: existence
message: "Don't start a sentence with '%s'."
level: error
raw:
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
# Minimal placeholder for WriteGood-style rules
# This folder acts as a vendored style. Add rules here if desired.
version = 1

View file

@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/write-good/releases.atom",
"vale_version": ">=1.0.0"
}

View file

@ -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
]
}

2
.vscode/launch.json vendored
View file

@ -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
}
]
}

47
.vscode/settings.json vendored
View file

@ -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
}

9
.vscode/tasks.json vendored
View file

@ -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",

171
CLAUDE.md
View file

@ -1,4 +1,4 @@
# Code Improvements by Claude
# Code improvements by claude
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@ -59,32 +59,32 @@
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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:
- <https://docs.github.com/en/actions/how-tos/create-and-publish-actions/manage-custom-actions>
- <https://docs.github.com/en/actions/how-tos/create-and-publish-actions/release-and-maintain-actions>
### 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
#==============================================================================
# <script_name>.sh
# < script_name > . sh
#==============================================================================
#
# DESCRIPTION:
# <Detailed description of the script's purpose and functionality>
# DESCRIPTION :
# < detailed description of the script ' s purpose and functionality >
#
# USAGE:
# <script_name>.sh <command> [args]
# USAGE :
# < script_name > . sh < command > [ args ]
#
# COMMANDS:
# <command_1> <Description of command_1>
# <command_2> <Description of command_2>
# ...
# COMMANDS :
# < command_1 > < description of command_1 >
# < command_2 > < description of command_2 >
# . . .
#
# OPTIONS:
# -h, --help Show this help message
# ... <Other options and their descriptions>
# OPTIONS :
# - h , - - help show this help message
# . . . < other options and their descriptions >
#
# DEPENDENCIES:
# <List required dependencies, e.g. external tools, environment variables>
# 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<function>_<arrangement>_<expectation>` 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

View file

@ -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

View file

@ -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.

View file

@ -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 @@
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
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)

View file

@ -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 <name>=<version> (e.g. xdot=1.2-2).
Space delimited list of packages to install. Version can be specified optionally using APT
command syntax of <name>=<version> (e.g. xdot=1.2-2).
required: true
default: ""
version:
description: >
Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.
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. <package>:<version,<package>:<version>).
The main requested packages and versions that are installed. Represented as a comma
delimited list with equals delimit on the package version (i.e.
<package>:<version,<package>:<version>).
value: ${{ steps.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. <package>:<version,<package>:<version>).
All the pulled in packages and versions, including dependencies, that are
installed. Represented as a comma delimited list with equals delimit on the package version
(i.e. <package>:<version,<package>:<version>).
value: ${{ steps.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:

View file

@ -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
}

View file

@ -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")
}
})
}

View file

@ -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 <command> [flags] [packages]\n\n", binaryName)
fmt.Fprintf(os.Stderr, "commands:\n")
for _, cmd := range *c {
fmt.Fprintf(os.Stderr, " %s: %s\n", cmd.Name, cmd.Description)
}
fmt.Fprintf(os.Stderr, "\nflags:\n")
// Print global flags (from any command, since they are the same)
globalFlags.VisitAll(func(f *flag.Flag) {
fmt.Fprintf(os.Stderr, " -%s: %s\n", f.Name, f.Usage)
})
fmt.Fprintf(os.Stderr, "\nUse \"%s <command> --help\" for more information about a command.\n", binaryName)
}
// 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
}

View file

@ -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")
}
})
}

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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()
})

357
internal/actions/action.go Normal file
View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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
`,
},
},
},
}
}

View file

@ -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}"
`,
},
},
},
}
}

70
internal/actions/doc.go Normal file
View file

@ -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

94
internal/cio/ghio.go Normal file
View file

@ -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
}

View file

@ -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:
}
}
}

View file

@ -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])
}
}
})
}
}

83
scripts/dev/export_version.sh Executable file
View file

@ -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}" <<EOF
# Version information for cache-apt-pkgs-action
GO_VERSION=${GO_VERSION}
TOOLCHAIN_VERSION=${TOOLCHAIN_VERSION}
SYSPKG_VERSION=${SYSPKG_VERSION}
EXPORT_DATE=$(date '+%Y-%m-%d %H:%M:%S')
EOF
echo "Version information has been exported to ${VERSION_FILE}"
echo "Go Version: ${GO_VERSION}"
echo "Toolchain Version: ${TOOLCHAIN_VERSION}"
echo "Syspkg Version: ${SYSPKG_VERSION}"
# Also create a JSON format for tools that prefer it
VERSION_JSON="${PROJECT_ROOT}/.version-info.json"
cat >"${VERSION_JSON}" <<EOF
{
"goVersion": "${GO_VERSION}",
"toolchainVersion": "${TOOLCHAIN_VERSION}",
"syspkgVersion": "${SYSPKG_VERSION}",
"exportDate": "$(date '+%Y-%m-%d %H:%M:%S')"
}
EOF
echo "Version information also exported in JSON format to ${VERSION_JSON}"

36
scripts/dev/fix_and_update.sh Executable file
View file

@ -0,0 +1,36 @@
#!/bin/bash
#==============================================================================
# fix_and_update.sh
#==============================================================================
#
# DESCRIPTION:
# Runs lint fixes and updates to code based on changes in the repository.
# Intended to help maintain code quality and formatting consistency.
#
# USAGE:
# fix_and_update.sh
#
# OPTIONS:
# -v, --verbose Enable verbose output
# -h, --help Show this help message
#==============================================================================
REPO_DIR="$(git rev-parse --show-toplevel)"
source "${REPO_DIR}/scripts/lib.sh"
parse_common_args "$@" >/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."

Some files were not shown because too many files have changed in this diff Show more