Compare commits

..

25 commits

Author SHA1 Message Date
Dane Wilson acb598e5dd
Pin actions for immutable and secure dependency versions (#175)
Pin dependency actions per security best practices.

https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions
2025-10-03 23:35:39 -07:00
Copilot 23602f4229
Add support for third-party PPA repositories via add-repository parameter (#173)
* Initial plan

* Add add-repository parameter support for third-party PPAs

Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>

* Add validation and complete PPA repository support implementation

Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>

* Remove accidentally committed log file and update .gitignore

Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>
2025-09-30 01:06:22 -07:00
Copilot 2ae65d5bbf
Fix ls error when no tar files exist in cache restore (#170)
* Initial plan

* Fix ls error when no tar files exist in cache restore

Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: awalsh128 <2087466+awalsh128@users.noreply.github.com>
2025-09-30 01:06:02 -07:00
awalsh128 9f7a885e33 Support symlinks and their targets. 2025-08-29 21:40:34 -07:00
Takahiro Ueda a605dbde2a
Add empty_packages_behavior option to handle empty package list (#154)
Available options: 'error' (default), 'warn' and 'ignore'.
2025-08-16 19:34:34 -07:00
awalsh128 2c09a5e66d Binary for https://github.com/awalsh128/cache-apt-pkgs-action/pull/160 2025-08-10 17:15:27 -07:00
Sébastien Morais 9a146f43d1
fix: skip invalid lines (#160) 2025-08-10 17:11:12 -07:00
Mike Tesch cbdbab28e6
Automatically append the OS architecture to the cache key (#150) 2025-08-10 13:24:23 -07:00
Andrew Walsh 23ccb675d9
Update README.md 2025-07-07 20:37:29 -07:00
Andrew Walsh db548ecc55
Update README.md 2025-07-07 20:35:48 -07:00
Khyber Sen 4c82c3ccdc
Remove GitHub URL shorteners (git.io) as they no longer work. (#164)
Fixes #163.
2025-07-07 20:15:12 -07:00
awalsh128 7ca5f46d06 Fix exec error. 2025-03-16 14:39:03 -07:00
Andrew Walsh 2330cb6dfb
Add support for virtual packages and break out APT logic into common library. (#155)
* Add support for virtual packages and break out APT logic into common library.
* Update binaries from commit 128de25ccb
2025-03-16 13:05:35 -07:00
Andrew Walsh 128de25ccb
Add support for virtual packages and break out APT logic into common library. (#153) 2025-03-16 12:35:38 -07:00
Bradley Scott 13d2226e13
Avoid using xargs when creating package tarballs (#136) 2025-02-04 10:08:04 -08:00
Ülgen Sarıkavak d1a184e480
Update actions/checkout versions in README (#148) 2025-02-03 11:42:51 -08:00
Jacco Broeren dfe9c8af34
fix: remove timestamps from loglines (#146) 2025-01-23 20:00:46 -08:00
Max Schwenk 5902b33ae2
Upgrade to cache v4 (#140) 2025-01-09 11:16:31 -08:00
Andrew Walsh f2fc6d1af4 Explicitly don't fail on error in library calls (required for conditonals) 2024-03-03 14:39:27 -08:00
Oliver Kopp a6c3917cc9
Add arm64 binary (#127)
* Add support for arm64 APT in Go apt-query binary.
2024-03-01 12:59:14 -08:00
Andrew Walsh 2555a377df
Bump force reload to accommodate update on cache action save version. #122 2024-02-13 21:42:01 -08:00
Hadrien G 75ab37ec52
Use matching versions of actions/cache actions (#123)
Needed to finish migration to node20 + probably wiser overall to use matching versions of the caching actions.
2024-02-13 21:38:36 -08:00
Andrew Walsh a9d925863b
Bump global version of cache.
Force cache reload for new version of cache action.
2024-02-13 11:36:57 -08:00
Daniel Possenriede 5c74a020dc
Bump actions/cache/restore to v4 (#120) 2024-02-01 13:13:10 -08:00
Andrew Walsh 6460a33c29
First version of a Golang version for APT package querying. (#118) (#119)
* Pull dev upstream to staging. (#112)

* Use awk to enclose filename in single quotes tar #99

* Add null field separator so filenames don't get broken up.

* Move upload logs up in the action sequence so it captures data before it gets deleted.

* Fix awk (#109)

---------

Co-authored-by: sn-o-w <cristian.silaghi@mozilla.ro>

* Fix awk delimiter.

Pull in fix by @sn-o-w in d0ee83b497 mentioned in issue #99

* Swap out Bash based APT query logic for Golang version. (#117)

* First version of a Golang version of command handling in general. (#118)

---------

Co-authored-by: sn-o-w <cristian.silaghi@mozilla.ro>
2023-12-22 10:28:03 -08:00
31 changed files with 1028 additions and 102 deletions

29
.github/workflows/pull_request.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Pull Request
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
jobs:
integrate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
- name: Build and test
run: |
go build -v ./...
go test -v ./...
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.52.2

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
src/cmd/apt_query/apt_query*
*.log

12
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
}
]
}

View file

@ -2,7 +2,7 @@
[![License: Apache2](https://shields.io/badge/license-apache2-blue.svg)](https://github.com/awalsh128/fluentcpp/blob/master/LICENSE)
[![Master Test status](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/master_test.yml/badge.svg)](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/master_test.yml)
[![Staging Test status](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/staging_test.yml/badge.svg)](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/staging_test.yml)
[![Dev Test status](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/dev_test.yml/badge.svg)](https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/dev_test.yml)
This action allows caching of Advanced Package Tool (APT) package dependencies to improve workflow execution time instead of installing the packages on every run.
@ -23,24 +23,26 @@ Create a workflow `.yml` file in your repositories `.github/workflows` directory
There are three kinds of version labels you can use.
* `@latest` - This will give you the latest release.
* `@v#` - Major only will give you the latest release for that major version only (e.g. `v1`).
* Branch
* `@master` - Most recent manual and automated tested code. Possibly unstable since it is pre-release.
* `@staging` - Most recent automated tested code and can sometimes contain experimental features. Is pulled from dev stable code.
* `@dev` - Very unstable and contains experimental features. Automated testing may not show breaks since CI is also updated based on code in dev.
- `@latest` - This will give you the latest release.
- `@v#` - Major only will give you the latest release for that major version only (e.g. `v1`).
- Branch
- `@master` - Most recent manual and automated tested code. Possibly unstable since it is pre-release.
- `@staging` - Most recent automated tested code and can sometimes contain experimental features. Is pulled from dev stable code.
- `@dev` - Very unstable and contains experimental features. Automated testing may not show breaks since CI is also updated based on code in dev.
### Inputs
* `packages` - Space delimited list of packages to install.
* `version` - Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.
* `execute_install_scripts` - Execute Debian package pre and post install script upon restore. See [Caveats / Non-file Dependencies](#non-file-dependencies) for more information.
- `packages` - Space delimited list of packages to install.
- `version` - Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.
- `execute_install_scripts` - Execute Debian package pre and post install script upon restore. See [Caveats / Non-file Dependencies](#non-file-dependencies) for more information.
- `empty_packages_behavior` - Desired behavior when the given `packages` is empty. `'error'` (default), `'warn'` or `'ignore'`.
- `add-repository` - Space delimited list of repositories to add via `apt-add-repository` before installing packages. Supports PPA (e.g., `ppa:user/repo`) and other repository formats.
### Outputs
* `cache-hit` - A boolean value to indicate a cache was found for the packages requested.
* `package-version-list` - The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. \<package1>=<version1\>,\<package2>=\<version2>,...).
* `all-package-version-list` - 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. \<package1>=<version1\>,\<package2>=\<version2>,...).
- `cache-hit` - A boolean value to indicate a cache was found for the packages requested.
- `package-version-list` - The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. \<package1>=<version1\>,\<package2>=\<version2>,...).
- `all-package-version-list` - 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. \<package1>=<version1\>,\<package2>=\<version2>,...).
### Cache scopes
@ -54,18 +56,17 @@ This was a motivating use case for creating this action.
name: Create Documentation
on: push
jobs:
build_and_deploy_docs:
runs-on: ubuntu-latest
name: Build Doxygen documentation and deploy
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: dia doxygen doxygen-doc doxygen-gui doxygen-latex graphviz mscgen
version: 1.0
- name: Build
- name: Build
run: |
cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
@ -78,15 +79,46 @@ jobs:
```
```yaml
...
install_doxygen_deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: dia doxygen doxygen-doc doxygen-gui doxygen-latex graphviz mscgen
version: 1.0
---
install_doxygen_deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: dia doxygen doxygen-doc doxygen-gui doxygen-latex graphviz mscgen
version: 1.0
```
### Using with Third-party PPAs
This example shows how to install packages from a third-party PPA:
```yaml
install_from_ppa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: chromium-browser
add-repository: ppa:canonical-chromium-builds/stage
version: 1.0
```
You can also add multiple repositories:
```yaml
install_from_multiple_repos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: package1 package2
add-repository: ppa:user/repo1 ppa:user/repo2
version: 1.0
```
## Caveats
@ -95,8 +127,8 @@ jobs:
This action is based on the principle that most packages can be cached as a fileset. There are situations though where this is not enough.
* Pre and post installation scripts needs to be ran from `/var/lib/dpkg/info/{package name}.[preinst, postinst]`.
* The Debian package database needs to be queried for scripts above (i.e. `dpkg-query`).
- Pre and post installation scripts needs to be ran 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 `execute_install_scripts` argument can be used to attempt to execute the install scripts but they are no guaranteed to resolve the issue.
@ -121,4 +153,4 @@ For more context and information see [issue #57](https://github.com/awalsh128/ca
### 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.
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).

View file

@ -18,6 +18,16 @@ inputs:
description: 'Execute Debian package pre and post install script upon restore. See README.md caveats for more information.'
required: false
default: 'false'
empty_packages_behavior:
description: >
Desired behavior when the provided package list is empty.
Available Options:
error: Fail the action with an error message
warn: Output a warning without failing the action
ignore: Proceed silently without warnings or errors
required: false
default: 'error'
refresh:
description: 'OBSOLETE: Refresh is not used by the action, use version instead.'
deprecationMessage: 'Refresh is not used by the action, use version instead.'
@ -25,6 +35,10 @@ inputs:
description: 'Enable debugging when there are issues with action. Minor performance penalty.'
required: false
default: 'false'
add-repository:
description: 'Space delimited list of repositories to add via apt-add-repository before installing packages. Supports PPA (ppa:user/repo) and other repository formats.'
required: false
default: ''
outputs:
cache-hit:
@ -49,22 +63,31 @@ runs:
"$VERSION" \
"$EXEC_INSTALL_SCRIPTS" \
"$DEBUG" \
"$ADD_REPOSITORY" \
"$PACKAGES"
echo "CACHE_KEY=$(cat ~/cache-apt-pkgs/cache_key.md5)" >> $GITHUB_ENV
if [ -f ~/cache-apt-pkgs/cache_key.md5 ]; then
echo "CACHE_KEY=$(cat ~/cache-apt-pkgs/cache_key.md5)" >> $GITHUB_ENV
else
echo "CACHE_KEY=" >> $GITHUB_ENV
fi
shell: bash
env:
VERSION: "${{ inputs.version }}"
EXEC_INSTALL_SCRIPTS: "${{ inputs.execute_install_scripts }}"
EMPTY_PACKAGES_BEHAVIOR: "${{ inputs.empty_packages_behavior }}"
DEBUG: "${{ inputs.debug }}"
ADD_REPOSITORY: "${{ inputs.add-repository }}"
PACKAGES: "${{ inputs.packages }}"
- id: load-cache
uses: actions/cache/restore@v3
if: ${{ env.CACHE_KEY }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/cache-apt-pkgs
key: cache-apt-pkgs_${{ env.CACHE_KEY }}
- id: post-cache
if: ${{ env.CACHE_KEY }}
run: |
${GITHUB_ACTION_PATH}/post_cache_action.sh \
~/cache-apt-pkgs \
@ -72,6 +95,7 @@ runs:
"$CACHE_HIT" \
"$EXEC_INSTALL_SCRIPTS" \
"$DEBUG" \
"$ADD_REPOSITORY" \
"$PACKAGES"
function create_list { local list=$(cat ~/cache-apt-pkgs/manifest_${1}.log | tr '\n' ','); echo ${list:0:-1}; };
echo "package-version-list=$(create_list main)" >> $GITHUB_OUTPUT
@ -81,18 +105,19 @@ runs:
CACHE_HIT: "${{ steps.load-cache.outputs.cache-hit }}"
EXEC_INSTALL_SCRIPTS: "${{ inputs.execute_install_scripts }}"
DEBUG: "${{ inputs.debug }}"
ADD_REPOSITORY: "${{ inputs.add-repository }}"
PACKAGES: "${{ inputs.packages }}"
- id: upload-logs
if: ${{ inputs.debug == 'true' }}
uses: actions/upload-artifact@v3
if: ${{ env.CACHE_KEY && inputs.debug == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cache-apt-pkgs-logs_${{ env.CACHE_KEY }}
path: ~/cache-apt-pkgs/*.log
- id: save-cache
if: ${{ ! steps.load-cache.outputs.cache-hit }}
uses: actions/cache/save@v3
if: ${{ env.CACHE_KEY && ! steps.load-cache.outputs.cache-hit }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/cache-apt-pkgs
key: ${{ steps.load-cache.outputs.cache-primary-key }}

BIN
apt_query-arm64 Executable file

Binary file not shown.

BIN
apt_query-x86 Executable file

Binary file not shown.

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module awalsh128.com/cache-apt-pkgs-action
go 1.20

View file

@ -15,18 +15,32 @@ source "${script_dir}/lib.sh"
# Directory that holds the cached packages.
cache_dir="${1}"
# Repositories to add before installing packages.
add_repository="${3}"
# List of the packages to use.
input_packages="${@:3}"
input_packages="${@:4}"
if ! apt-fast --version > /dev/null 2>&1; then
log "Installing apt-fast for optimized installs..."
# Install apt-fast for optimized installs.
/bin/bash -c "$(curl -sL https://git.io/vokNn)"
/bin/bash -c "$(curl -sL https://raw.githubusercontent.com/ilikenwf/apt-fast/master/quick-install.sh)"
log "done"
log_empty_line
fi
# Add custom repositories if specified
if [ -n "${add_repository}" ]; then
log "Adding custom repositories..."
for repository in ${add_repository}; do
log "- Adding repository: ${repository}"
sudo apt-add-repository -y "${repository}"
done
log "done"
log_empty_line
fi
log "Updating APT package list..."
if [[ -z "$(find -H /var/lib/apt/lists -maxdepth 0 -mmin -5)" ]]; then
sudo apt-fast update > /dev/null
@ -87,14 +101,23 @@ for installed_package in ${installed_packages}; do
read package_name package_ver < <(get_package_name_ver "${installed_package}")
log " * Caching ${package_name} to ${cache_filepath}..."
# Pipe all package files (no folders) and installation control data to Tar.
{ dpkg -L "${package_name}" \
& get_install_script_filepath "" "${package_name}" "preinst" \
& get_install_script_filepath "" "${package_name}" "postinst"; } |
while IFS= read -r f; do test -f "${f}" -o -L "${f}" && get_tar_relpath "${f}"; done |
# Single quotes ensure literals like backslash get captured. Use \0 to avoid field separation.
awk -F"\0" '{print "\x27"$1"\x27"}' |
sudo xargs tar -cf "${cache_filepath}" -C /
# Pipe all package files (no folders), including symlinks, their targets, and installation control data to Tar.
tar -cf "${cache_filepath}" -C / --verbatim-files-from --files-from <(
{ dpkg -L "${package_name}" &&
get_install_script_filepath "" "${package_name}" "preinst" &&
get_install_script_filepath "" "${package_name}" "postinst" ; } |
while IFS= read -r f; do
if test -f "${f}" -o -L "${f}"; then
get_tar_relpath "${f}"
if [ -L "${f}" ]; then
target="$(readlink -f "${f}")"
if [ -f "${target}" ]; then
get_tar_relpath "${target}"
fi
fi
fi
done
)
log " done (compressed size $(du -h "${cache_filepath}" | cut -f1))."
fi

64
lib.sh
View file

@ -1,5 +1,11 @@
#!/bin/bash
# Don't fail on error. We use the exit status as a conditional.
#
# This is the default behavior but can be overridden by the caller in the
# SHELLOPTS env var.
set +e
###############################################################################
# Execute the Debian install script.
# Arguments:
@ -99,57 +105,17 @@ function get_normalized_package_list {
# Remove commas, and block scalar folded backslashes,
# extraneous spaces at the middle, beginning and end
# then sort.
packages=$(echo "${1}" \
local packages=$(echo "${1}" \
| sed 's/[,\]/ /g; s/\s\+/ /g; s/^\s\+//g; s/\s\+$//g' \
| sort -t' ')
local script_dir="$(dirname -- "$(realpath -- "${0}")")"
# Validate package names and get versions.
log_err "resolving package versions..."
data=$(apt-cache --quiet=0 --no-all-versions show ${packages} 2>&1 | \
grep -E '^(Package|Version|N):')
log_err "resolved"
local ORIG_IFS="${IFS}"
IFS=$'\n'
declare -A missing
local package_versions=''
local package='' separator=''
for key_value in ${data}; do
local key="${key_value%%: *}"
local value="${key_value##*: }"
case $key in
Package)
package=$value
;;
Version)
package_versions="${package_versions}${separator}"${package}=${value}""
separator=' '
;;
N)
# Warning messages.
case $value in
'Unable to locate package '*)
package="${value#'Unable to locate package '}"
# Avoid duplicate messages.
if [ -z "${missing[$package]}" ]; then
package="${value#'Unable to locate package '}"
log_err "Package '${package}' not found."
missing[$package]=1
fi
;;
esac
;;
esac
done
IFS="${ORIG_IFS}"
if [ ${#missing[@]} -gt 0 ]; then
echo "aborted"
exit 5
local architecture=$(dpkg --print-architecture)
if [ "${architecture}" == "arm64" ]; then
${script_dir}/apt_query-arm64 normalized-list ${packages}
else
${script_dir}/apt_query-x86 normalized-list ${packages}
fi
echo "${package_versions}"
}
###############################################################################
@ -169,8 +135,8 @@ function get_tar_relpath {
fi
}
function log { echo "$(date +%T.%3N)" "${@}"; }
function log_err { >&2 echo "$(date +%T.%3N)" "${@}"; }
function log { echo "${@}"; }
function log_err { >&2 echo "${@}"; }
function log_empty_line { echo ""; }

View file

@ -25,13 +25,16 @@ execute_install_scripts="${4}"
debug="${5}"
test "${debug}" = "true" && set -x
# Repositories to add before installing packages.
add_repository="${6}"
# List of the packages to use.
packages="${@:6}"
packages="${@:7}"
if test "${cache_hit}" = "true"; then
${script_dir}/restore_pkgs.sh "${cache_dir}" "${cache_restore_root}" "${execute_install_scripts}" "${debug}"
else
${script_dir}/install_and_cache_pkgs.sh "${cache_dir}" "${debug}" ${packages}
${script_dir}/install_and_cache_pkgs.sh "${cache_dir}" "${debug}" "${add_repository}" ${packages}
fi
log_empty_line

View file

@ -24,11 +24,16 @@ execute_install_scripts="${3}"
# Debug mode for diagnosing issues.
debug="${4}"
# Repositories to add before installing packages.
add_repository="${5}"
# List of the packages to use.
input_packages="${@:5}"
input_packages="${@:6}"
# Trim commas, excess spaces, and sort.
log "Normalizing package list..."
packages="$(get_normalized_package_list "${input_packages}")"
log "done"
# Create cache directory so artifacts can be saved.
mkdir -p ${cache_dir}
@ -42,13 +47,39 @@ fi
# Is length of string zero?
if test -z "${packages}"; then
log "aborted"
log "Packages argument cannot be empty." >&2
exit 3
case "$EMPTY_PACKAGES_BEHAVIOR" in
ignore)
exit 0
;;
warn)
echo "::warning::Packages argument is empty."
exit 0
;;
*)
log "aborted"
log "Packages argument is empty." >&2
exit 3
;;
esac
fi
validate_bool "${execute_install_scripts}" execute_install_scripts 4
# Basic validation for repository parameter
if [ -n "${add_repository}" ]; then
log "Validating repository parameter..."
for repository in ${add_repository}; do
# Check if repository format looks valid (basic check)
if [[ "${repository}" =~ [^a-zA-Z0-9:\/.-] ]]; then
log "aborted"
log "Repository '${repository}' contains invalid characters." >&2
log "Supported formats: 'ppa:user/repo', 'deb http://...', 'http://...', 'multiverse', etc." >&2
exit 6
fi
done
log "done"
fi
log "done"
log_empty_line
@ -59,10 +90,27 @@ set -e
log "Creating cache key..."
# Forces an update in cases where an accidental breaking change was introduced
# and a global cache reset is required.
force_update_inc="1"
# and a global cache reset is required, or change in cache action requiring reload.
force_update_inc="3"
# Force a different cache key for different architectures (currently x86_64 and aarch64 are available on GitHub)
cpu_arch="$(arch)"
log "- CPU architecture is '${cpu_arch}'."
value="${packages} @ ${version} ${force_update_inc}"
# Include repositories in cache key to ensure different repos get different caches
if [ -n "${add_repository}" ]; then
value="${value} ${add_repository}"
log "- Repositories '${add_repository}' added to value."
fi
# Don't invalidate existing caches for the standard Ubuntu runners
if [ "${cpu_arch}" != "x86_64" ]; then
value="${value} ${cpu_arch}"
log "- Architecture '${cpu_arch}' added to value."
fi
log "- Value to hash is '${value}'."
key="$(echo "${value}" | md5sum | cut -f1 -d' ')"

View file

@ -40,7 +40,7 @@ log "done"
log_empty_line
# Only search for archived results. Manifest and cache key also live here.
cached_filepaths=$(ls -1 "${cache_dir}"/*.tar | sort)
cached_filepaths=$(ls -1 "${cache_dir}"/*.tar 2>/dev/null | sort)
cached_filecount=$(echo ${cached_filepaths} | wc -w)
log "Restoring ${cached_filecount} packages from cache..."

53
src/cmd/apt_query/main.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"flag"
"fmt"
"os"
"awalsh128.com/cache-apt-pkgs-action/src/internal/common"
"awalsh128.com/cache-apt-pkgs-action/src/internal/exec"
"awalsh128.com/cache-apt-pkgs-action/src/internal/logging"
)
func getExecutor(replayFilename string) exec.Executor {
if len(replayFilename) == 0 {
return &exec.BinExecutor{}
}
return exec.NewReplayExecutor(replayFilename)
}
func main() {
debug := flag.Bool("debug", false, "Log diagnostic information to a file alongside the binary.")
replayFilename := flag.String("replayfile", "",
"Replay command output from a specified file rather than executing a binary."+
"The file should be in the same format as the log generated by the debug flag.")
flag.Parse()
unparsedFlags := flag.Args()
logging.Init(os.Args[0]+".log", *debug)
executor := getExecutor(*replayFilename)
if len(unparsedFlags) < 2 {
logging.Fatalf("Expected at least 2 non-flag arguments but found %d.", len(unparsedFlags))
return
}
command := unparsedFlags[0]
pkgNames := unparsedFlags[1:]
switch command {
case "normalized-list":
pkgs, err := common.GetAptPackages(executor, pkgNames)
if err != nil {
logging.Fatalf("Encountered error resolving some or all package names, see combined std[out,err] below.\n%s", err.Error())
}
fmt.Println(pkgs.Serialize())
default:
logging.Fatalf("Command '%s' not recognized.", command)
}
}

View file

@ -0,0 +1,70 @@
package main
import (
"flag"
"testing"
"awalsh128.com/cache-apt-pkgs-action/src/internal/cmdtesting"
)
var createReplayLogs bool = false
func init() {
flag.BoolVar(&createReplayLogs, "createreplaylogs", false, "Execute the test commands, save the command output for future replay and skip the tests themselves.")
}
func TestMain(m *testing.M) {
cmdtesting.TestMain(m)
}
func TestNormalizedList_MultiplePackagesExists_StdoutsAlphaSortedPackageNameVersionPairs(t *testing.T) {
result := cmdtesting.New(t, createReplayLogs).Run("normalized-list", "xdot", "rolldice")
result.ExpectSuccessfulOut("rolldice=1.16-1build1 xdot=1.1-2")
}
func TestNormalizedList_SamePackagesDifferentOrder_StdoutsMatch(t *testing.T) {
expected := "rolldice=1.16-1build1 xdot=1.1-2"
ct := cmdtesting.New(t, createReplayLogs)
result := ct.Run("normalized-list", "rolldice", "xdot")
result.ExpectSuccessfulOut(expected)
result = ct.Run("normalized-list", "xdot", "rolldice")
result.ExpectSuccessfulOut(expected)
}
func TestNormalizedList_MultiVersionWarning_StdoutSingleVersion(t *testing.T) {
var result = cmdtesting.New(t, createReplayLogs).Run("normalized-list", "libosmesa6-dev", "libgl1-mesa-dev")
result.ExpectSuccessfulOut("libgl1-mesa-dev=21.2.6-0ubuntu0.1~20.04.2 libosmesa6-dev=21.2.6-0ubuntu0.1~20.04.2")
}
func TestNormalizedList_SinglePackageExists_StdoutsSinglePackageNameVersionPair(t *testing.T) {
var result = cmdtesting.New(t, createReplayLogs).Run("normalized-list", "xdot")
result.ExpectSuccessfulOut("xdot=1.1-2")
}
func TestNormalizedList_VersionContainsColon_StdoutsEntireVersion(t *testing.T) {
var result = cmdtesting.New(t, createReplayLogs).Run("normalized-list", "default-jre")
result.ExpectSuccessfulOut("default-jre=2:1.11-72")
}
func TestNormalizedList_NonExistentPackageName_StderrsAptCacheErrors(t *testing.T) {
var result = cmdtesting.New(t, createReplayLogs).Run("normalized-list", "nonexistentpackagename")
result.ExpectError(
`Error encountered running apt-cache --quiet=0 --no-all-versions show nonexistentpackagename
Exited with status code 100; see combined std[out,err] below:
N: Unable to locate package nonexistentpackagename
N: Unable to locate package nonexistentpackagename
E: No packages found`)
}
func TestNormalizedList_NoPackagesGiven_StderrsArgMismatch(t *testing.T) {
var result = cmdtesting.New(t, createReplayLogs).Run("normalized-list")
result.ExpectError("Expected at least 2 non-flag arguments but found 1.")
}
func TestNormalizedList_VirtualPackagesExists_StdoutsConcretePackage(t *testing.T) {
result := cmdtesting.New(t, createReplayLogs).Run("normalized-list", "libvips")
result.ExpectSuccessfulOut("libvips42=8.9.1-2")
}

View file

@ -0,0 +1,10 @@
2025/03/15 22:29:08 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:09 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show xdot rolldice",
"Stdout": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\nPackage: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\n",
"Stderr": "",
"CombinedOut": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\nPackage: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,10 @@
2025/03/15 22:29:10 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:12 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show libosmesa6-dev libgl1-mesa-dev",
"Stdout": "Package: libosmesa6-dev\nArchitecture: amd64\nVersion: 21.2.6-0ubuntu0.1~20.04.2\nMulti-Arch: same\nPriority: extra\nSection: devel\nSource: mesa\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian X Strike Force \u003cdebian-x@lists.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 88\nProvides: libosmesa-dev\nDepends: libosmesa6 (= 21.2.6-0ubuntu0.1~20.04.2), mesa-common-dev (= 21.2.6-0ubuntu0.1~20.04.2) | libgl-dev\nConflicts: libosmesa-dev\nReplaces: libosmesa-dev\nFilename: pool/main/m/mesa/libosmesa6-dev_21.2.6-0ubuntu0.1~20.04.2_amd64.deb\nSize: 8844\nMD5sum: b6c380d1b916bc6955aaf108a3be468e\nSHA1: 4b772c8127e60a342dabec4ff0939969d99038b4\nSHA256: bf003b66573d611877664e01659046d281b26698f3665345cb784ddded662c6a\nSHA512: 761557925874473e4408504772a0af4d29f6dc1dcbd53e772315dffb6da87d47960edca4de39deda7cae33a8730d87a19b40a7d29739ba7cff5b60ee4900a13a\nHomepage: https://mesa3d.org/\nDescription-en: Mesa Off-screen rendering extension -- development files\n This package provides the required environment for developing programs\n that use the off-screen rendering extension of Mesa.\n .\n For more information on OSmesa see the libosmesa6 package.\nDescription-md5: 9b1d7a0b3e6a2ea021f4443f42dcff4f\n\nPackage: libgl1-mesa-dev\nArchitecture: amd64\nVersion: 21.2.6-0ubuntu0.1~20.04.2\nMulti-Arch: same\nPriority: extra\nSection: libdevel\nSource: mesa\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian X Strike Force \u003cdebian-x@lists.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 70\nDepends: libgl-dev, libglvnd-dev\nFilename: pool/main/m/mesa/libgl1-mesa-dev_21.2.6-0ubuntu0.1~20.04.2_amd64.deb\nSize: 6420\nMD5sum: 759a811dcb12adfcebfc1a6aa52e85b9\nSHA1: 3b9de17b1c67ee40603e1eebaefa978810a2f2d2\nSHA256: 76846d96ae0706a7edcd514d452a1393bb8b8a8ac06518253dd5869441807052\nSHA512: 581e4b3752b4c98399f3519fce2c5ab033cea3cac66bde3c204af769ff0377400087f9c4c6aaebe06c19d05f8715b3346d249a86c3ae80a098ca476e76af01c3\nHomepage: https://mesa3d.org/\nDescription-en: transitional dummy package\n This is a transitional dummy package, it can be safely removed.\nDescription-md5: 635a93bcd1440d16621693fe064c2aa9\n\n",
"Stderr": "N: There are 2 additional records. Please use the '-a' switch to see them.\n",
"CombinedOut": "Package: libosmesa6-dev\nArchitecture: amd64\nVersion: 21.2.6-0ubuntu0.1~20.04.2\nMulti-Arch: same\nPriority: extra\nSection: devel\nSource: mesa\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian X Strike Force \u003cdebian-x@lists.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 88\nProvides: libosmesa-dev\nDepends: libosmesa6 (= 21.2.6-0ubuntu0.1~20.04.2), mesa-common-dev (= 21.2.6-0ubuntu0.1~20.04.2) | libgl-dev\nConflicts: libosmesa-dev\nReplaces: libosmesa-dev\nFilename: pool/main/m/mesa/libosmesa6-dev_21.2.6-0ubuntu0.1~20.04.2_amd64.deb\nSize: 8844\nMD5sum: b6c380d1b916bc6955aaf108a3be468e\nSHA1: 4b772c8127e60a342dabec4ff0939969d99038b4\nSHA256: bf003b66573d611877664e01659046d281b26698f3665345cb784ddded662c6a\nSHA512: 761557925874473e4408504772a0af4d29f6dc1dcbd53e772315dffb6da87d47960edca4de39deda7cae33a8730d87a19b40a7d29739ba7cff5b60ee4900a13a\nHomepage: https://mesa3d.org/\nDescription-en: Mesa Off-screen rendering extension -- development files\n This package provides the required environment for developing programs\n that use the off-screen rendering extension of Mesa.\n .\n For more information on OSmesa see the libosmesa6 package.\nDescription-md5: 9b1d7a0b3e6a2ea021f4443f42dcff4f\n\nPackage: libgl1-mesa-dev\nArchitecture: amd64\nVersion: 21.2.6-0ubuntu0.1~20.04.2\nMulti-Arch: same\nPriority: extra\nSection: libdevel\nSource: mesa\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian X Strike Force \u003cdebian-x@lists.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 70\nDepends: libgl-dev, libglvnd-dev\nFilename: pool/main/m/mesa/libgl1-mesa-dev_21.2.6-0ubuntu0.1~20.04.2_amd64.deb\nSize: 6420\nMD5sum: 759a811dcb12adfcebfc1a6aa52e85b9\nSHA1: 3b9de17b1c67ee40603e1eebaefa978810a2f2d2\nSHA256: 76846d96ae0706a7edcd514d452a1393bb8b8a8ac06518253dd5869441807052\nSHA512: 581e4b3752b4c98399f3519fce2c5ab033cea3cac66bde3c204af769ff0377400087f9c4c6aaebe06c19d05f8715b3346d249a86c3ae80a098ca476e76af01c3\nHomepage: https://mesa3d.org/\nDescription-en: transitional dummy package\n This is a transitional dummy package, it can be safely removed.\nDescription-md5: 635a93bcd1440d16621693fe064c2aa9\n\nN: There are 2 additional records. Please use the '-a' switch to see them.\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,15 @@
2025/03/15 22:29:13 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:14 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show nonexistentpackagename",
"Stdout": "",
"Stderr": "N: Unable to locate package nonexistentpackagename\nN: Unable to locate package nonexistentpackagename\nE: No packages found\n",
"CombinedOut": "N: Unable to locate package nonexistentpackagename\nN: Unable to locate package nonexistentpackagename\nE: No packages found\n",
"ExitCode": 100
}
EXECUTION-OBJ-END
2025/03/15 22:29:14 Error encountered running apt-cache --quiet=0 --no-all-versions show nonexistentpackagename
Exited with status code 100; see combined std[out,err] below:
N: Unable to locate package nonexistentpackagename
N: Unable to locate package nonexistentpackagename
E: No packages found

View file

@ -0,0 +1,2 @@
2025/03/15 22:29:14 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:14 Expected at least 2 non-flag arguments but found 1.

View file

@ -0,0 +1,20 @@
2025/03/15 22:29:09 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:10 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show rolldice xdot",
"Stdout": "Package: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\nPackage: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\n",
"Stderr": "",
"CombinedOut": "Package: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\nPackage: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\n",
"ExitCode": 0
}
EXECUTION-OBJ-END
2025/03/15 22:29:10 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:10 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show xdot rolldice",
"Stdout": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\nPackage: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\n",
"Stderr": "",
"CombinedOut": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\nPackage: rolldice\nArchitecture: amd64\nVersion: 1.16-1build1\nPriority: optional\nSection: universe/games\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Thomas Ross \u003cthomasross@thomasross.io\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 31\nDepends: libc6 (\u003e= 2.7), libreadline8 (\u003e= 6.0)\nFilename: pool/universe/r/rolldice/rolldice_1.16-1build1_amd64.deb\nSize: 9628\nMD5sum: af6390bf2d5d5b4710d308ac06d4913a\nSHA1: 1d87ccac5b20f4e2a217a0e058408f46cfe5caff\nSHA256: 2e076006200057da0be52060e3cc2f4fc7c51212867173e727590bd7603a0337\nHomepage: https://github.com/sstrickl/rolldice\nDescription-en: virtual dice roller\n rolldice is a virtual dice roller that takes a string on the command\n line in the format of some fantasy role playing games like Advanced\n Dungeons \u0026 Dragons [1] and returns the result of the dice rolls.\n .\n [1] Advanced Dungeons \u0026 Dragons is a registered trademark of TSR, Inc.\nDescription-md5: fc24e9e12c794a8f92ab0ca6e1058501\n\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,10 @@
2025/03/15 22:29:12 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:12 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show xdot",
"Stdout": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\n",
"Stderr": "",
"CombinedOut": "Package: xdot\nArchitecture: all\nVersion: 1.1-2\nPriority: optional\nSection: universe/python\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Python Applications Packaging Team \u003cpython-apps-team@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 153\nDepends: gir1.2-gtk-3.0, graphviz, python3-gi, python3-gi-cairo, python3:any\nFilename: pool/universe/x/xdot/xdot_1.1-2_all.deb\nSize: 26708\nMD5sum: aab630b6e1f73a0e3ae85b208b8b6d00\nSHA1: 202155c123c7bd7628023b848e997f342d14359d\nSHA256: 4b7ecb2c4dc948a850024a9b7378d58195230659307bbde4018a1be17645e690\nHomepage: https://github.com/jrfonseca/xdot.py\nDescription-en: interactive viewer for Graphviz dot files\n xdot is an interactive viewer for graphs written in Graphviz's dot language.\n It uses internally the graphviz's xdot output format as an intermediate\n format, and PyGTK and Cairo for rendering. xdot can be used either as a\n standalone application from command line, or as a library embedded in your\n Python 3 application.\n .\n Features:\n * Since it doesn't use bitmaps it is fast and has a small memory footprint.\n * Arbitrary zoom.\n * Keyboard/mouse navigation.\n * Supports events on the nodes with URLs.\n * Animated jumping between nodes.\n * Highlights node/edge under mouse.\nDescription-md5: eb58f25a628b48a744f1b904af3b9282\n\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,10 @@
2025/03/15 22:29:12 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:13 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show default-jre",
"Stdout": "Package: default-jre\nArchitecture: amd64\nVersion: 2:1.11-72\nPriority: optional\nSection: interpreters\nSource: java-common (0.72)\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian Java Maintainers \u003cpkg-java-maintainers@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 6\nProvides: java-runtime, java10-runtime, java11-runtime, java2-runtime, java5-runtime, java6-runtime, java7-runtime, java8-runtime, java9-runtime\nDepends: default-jre-headless (= 2:1.11-72), openjdk-11-jre\nFilename: pool/main/j/java-common/default-jre_1.11-72_amd64.deb\nSize: 1084\nMD5sum: 4f441bb884801f3a07806934e4519652\nSHA1: 9922edaa7bd91921a2beee6c60343ebf551957a9\nSHA256: 063bb2ca3b51309f6625033c336beffb0eb8286aaabcf3bf917eef498de29ea5\nHomepage: https://wiki.debian.org/Java/\nDescription-en: Standard Java or Java compatible Runtime\n This dependency package points to the Java runtime, or Java compatible\n runtime recommended for this architecture, which is\n openjdk-11-jre for amd64.\nDescription-md5: 071b7a2f9baf048d89c849c14bcafb9e\nCnf-Extra-Commands: java,jexec\n\n",
"Stderr": "",
"CombinedOut": "Package: default-jre\nArchitecture: amd64\nVersion: 2:1.11-72\nPriority: optional\nSection: interpreters\nSource: java-common (0.72)\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers \u003cubuntu-devel-discuss@lists.ubuntu.com\u003e\nOriginal-Maintainer: Debian Java Maintainers \u003cpkg-java-maintainers@lists.alioth.debian.org\u003e\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 6\nProvides: java-runtime, java10-runtime, java11-runtime, java2-runtime, java5-runtime, java6-runtime, java7-runtime, java8-runtime, java9-runtime\nDepends: default-jre-headless (= 2:1.11-72), openjdk-11-jre\nFilename: pool/main/j/java-common/default-jre_1.11-72_amd64.deb\nSize: 1084\nMD5sum: 4f441bb884801f3a07806934e4519652\nSHA1: 9922edaa7bd91921a2beee6c60343ebf551957a9\nSHA256: 063bb2ca3b51309f6625033c336beffb0eb8286aaabcf3bf917eef498de29ea5\nHomepage: https://wiki.debian.org/Java/\nDescription-en: Standard Java or Java compatible Runtime\n This dependency package points to the Java runtime, or Java compatible\n runtime recommended for this architecture, which is\n openjdk-11-jre for amd64.\nDescription-md5: 071b7a2f9baf048d89c849c14bcafb9e\nCnf-Extra-Commands: java,jexec\n\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,19 @@
2025/03/15 22:29:14 Debug log created at /home/awalsh128/cache-apt-pkgs-action/src/cmd/apt_query/apt_query.log
2025/03/15 22:29:14 EXECUTION-OBJ-START
{
"Cmd": "apt-cache --quiet=0 --no-all-versions show libvips",
"Stdout": "",
"Stderr": "N: Can't select candidate version from package libvips as it has no candidate\nN: Can't select versions from package 'libvips' as it is purely virtual\nN: No packages found\n",
"CombinedOut": "N: Can't select candidate version from package libvips as it has no candidate\nN: Can't select versions from package 'libvips' as it is purely virtual\nN: No packages found\n",
"ExitCode": 0
}
EXECUTION-OBJ-END
2025/03/15 22:29:14 EXECUTION-OBJ-START
{
"Cmd": "bash -c apt-cache showpkg libvips | grep -A 1 \"Reverse Provides\" | tail -1",
"Stdout": "libvips42 8.9.1-2 (= )\n",
"Stderr": "",
"CombinedOut": "libvips42 8.9.1-2 (= )\n",
"ExitCode": 0
}
EXECUTION-OBJ-END

View file

@ -0,0 +1,91 @@
package cmdtesting
import (
"os"
"os/exec"
"strings"
"testing"
"awalsh128.com/cache-apt-pkgs-action/src/internal/common"
)
const binaryName = "apt_query"
type CmdTesting struct {
*testing.T
createReplayLogs bool
replayFilename string
}
func New(t *testing.T, createReplayLogs bool) *CmdTesting {
replayFilename := "testlogs/" + strings.ToLower(t.Name()) + ".log"
if createReplayLogs {
os.Remove(replayFilename)
os.Remove(binaryName + ".log")
}
return &CmdTesting{t, createReplayLogs, replayFilename}
}
type RunResult struct {
Testing *CmdTesting
CombinedOut string
Err error
}
func TestMain(m *testing.M) {
cmd := exec.Command("go", "build")
out, err := cmd.CombinedOutput()
if err != nil {
panic(string(out))
}
os.Exit(m.Run())
}
func (t *CmdTesting) Run(command string, pkgNames ...string) RunResult {
replayfile := "testlogs/" + strings.ToLower(t.Name()) + ".log"
flags := []string{"-debug=true"}
if !t.createReplayLogs {
flags = append(flags, "-replayfile="+replayfile)
}
cmd := exec.Command("./"+binaryName, append(append(flags, command), pkgNames...)...)
combinedOut, err := cmd.CombinedOutput()
if t.createReplayLogs {
err := common.AppendFile(binaryName+".log", t.replayFilename)
if err != nil {
t.T.Fatalf("Error encountered appending log file.\n%s", err.Error())
}
}
return RunResult{Testing: t, CombinedOut: string(combinedOut), Err: err}
}
func (r *RunResult) ExpectSuccessfulOut(expected string) {
if r.Testing.createReplayLogs {
r.Testing.Log("Skipping test while creating replay logs.")
return
}
if r.Err != nil {
r.Testing.Errorf("Error running command: %v\n%s", r.Err, r.CombinedOut)
return
}
fullExpected := expected + "\n" // Output will always have a end of output newline.
if r.CombinedOut != fullExpected {
r.Testing.Errorf("Unexpected combined std[err,out] found.\nExpected:\n'%s'\nActual:\n'%s'", fullExpected, r.CombinedOut)
}
}
func (r *RunResult) ExpectError(expectedCombinedOut string) {
if r.Testing.createReplayLogs {
r.Testing.Log("Skipping test while creating replay logs.")
return
}
fullExpectedCombinedOut := expectedCombinedOut + "\n" // Output will always have a end of output newline.
if r.CombinedOut != fullExpectedCombinedOut {
r.Testing.Errorf("Unexpected combined std[err,out] found.\nExpected:\n'%s'\nActual:\n'%s'", fullExpectedCombinedOut, r.CombinedOut)
}
}

127
src/internal/common/apt.go Normal file
View file

@ -0,0 +1,127 @@
package common
import (
"errors"
"fmt"
"sort"
"strings"
"awalsh128.com/cache-apt-pkgs-action/src/internal/exec"
"awalsh128.com/cache-apt-pkgs-action/src/internal/logging"
)
// An APT package name and version representation.
type AptPackage struct {
Name string
Version string
}
type AptPackages []AptPackage
// Serialize the APT packages into lines of <name>=<version>.
func (ps AptPackages) Serialize() string {
tokens := []string{}
for _, p := range ps {
tokens = append(tokens, p.Name+"="+p.Version)
}
return strings.Join(tokens, " ")
}
func isErrLine(line string) bool {
return strings.HasPrefix(line, "E: ") || strings.HasPrefix(line, "N: ")
}
// Resolves virtual packages names to their concrete one.
func getNonVirtualPackage(executor exec.Executor, name string) (pkg *AptPackage, err error) {
execution := executor.Exec("bash", "-c", fmt.Sprintf("apt-cache showpkg %s | grep -A 1 \"Reverse Provides\" | tail -1", name))
err = execution.Error()
if err != nil {
logging.Fatal(err)
return pkg, err
}
if isErrLine(execution.CombinedOut) {
return pkg, execution.Error()
}
splitLine := GetSplitLine(execution.CombinedOut, " ", 3)
if len(splitLine.Words) < 2 {
return pkg, fmt.Errorf("unable to parse space delimited line's package name and version from apt-cache showpkg output below:\n%s", execution.CombinedOut)
}
return &AptPackage{Name: splitLine.Words[0], Version: splitLine.Words[1]}, nil
}
func getPackage(executor exec.Executor, paragraph string) (pkg *AptPackage, err error) {
errMsgs := []string{}
for _, splitLine := range GetSplitLines(paragraph, ":", 2) {
if len(splitLine.Words) < 2 {
logging.Debug("Skipping invalid line: %+v\n", splitLine.Line)
continue
}
switch splitLine.Words[0] {
case "Package":
// Initialize since this will provide the first struct value if present.
pkg = &AptPackage{}
pkg.Name = splitLine.Words[1]
case "Version":
pkg.Version = splitLine.Words[1]
case "N":
// e.g. Can't select versions from package 'libvips' as it is purely virtual
if strings.Contains(splitLine.Words[1], "as it is purely virtual") {
return getNonVirtualPackage(executor, GetSplitLine(splitLine.Words[1], "'", 4).Words[2])
}
if strings.HasPrefix(splitLine.Words[1], "Unable to locate package") && !ArrContainsString(errMsgs, splitLine.Line) {
errMsgs = append(errMsgs, splitLine.Line)
}
case "E":
if !ArrContainsString(errMsgs, splitLine.Line) {
errMsgs = append(errMsgs, splitLine.Line)
}
}
}
if len(errMsgs) == 0 {
return pkg, nil
}
return pkg, errors.New(strings.Join(errMsgs, "\n"))
}
// Gets the APT based packages as a sorted by package name list (normalized).
func GetAptPackages(executor exec.Executor, names []string) (AptPackages, error) {
prefixArgs := []string{"--quiet=0", "--no-all-versions", "show"}
execution := executor.Exec("apt-cache", append(prefixArgs, names...)...)
pkgs := []AptPackage{}
err := execution.Error()
if err != nil {
logging.Fatal(err)
return pkgs, err
}
errMsgs := []string{}
for _, paragraph := range strings.Split(execution.CombinedOut, "\n\n") {
trimmed := strings.TrimSpace(paragraph)
if trimmed == "" {
continue
}
pkg, err := getPackage(executor, trimmed)
if err != nil {
errMsgs = append(errMsgs, err.Error())
} else if pkg != nil { // Ignore cases where no package parsed and no errors occurred.
pkgs = append(pkgs, *pkg)
}
}
if len(errMsgs) > 0 {
errMsgs = append(errMsgs, strings.Join(errMsgs, "\n"))
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].Name < pkgs[j].Name
})
if len(errMsgs) > 0 {
return pkgs, errors.New(strings.Join(errMsgs, "\n"))
}
return pkgs, nil
}

85
src/internal/common/io.go Normal file
View file

@ -0,0 +1,85 @@
package common
import (
"io"
"os"
"path/filepath"
)
func AppendFile(source string, destination string) error {
err := createDirectoryIfNotPresent(filepath.Dir(destination))
if err != nil {
return err
}
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(destination, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer out.Close()
data, err := io.ReadAll(in)
if err != nil {
return err
}
_, err = out.Write(data)
if err != nil {
return err
}
return nil
}
func CopyFile(source string, destination string) error {
err := createDirectoryIfNotPresent(filepath.Dir(destination))
if err != nil {
return err
}
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(destination)
if err != nil {
return err
}
defer out.Close()
data, err := io.ReadAll(in)
if err != nil {
return err
}
_, err = out.Write(data)
if err != nil {
return err
}
return nil
}
func MoveFile(source string, destination string) error {
err := createDirectoryIfNotPresent(filepath.Dir(destination))
if err != nil {
return err
}
return os.Rename(source, destination)
}
func createDirectoryIfNotPresent(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
err := os.MkdirAll(path, 0755)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,44 @@
package common
import (
"strings"
)
// Checks if an exact string is in an array of strings.
func ArrContainsString(arr []string, element string) bool {
for _, x := range arr {
if x == element {
return true
}
}
return false
}
// A line that has been split into words.
type SplitLine struct {
Line string // The original line.
Words []string // The split words in the line.
}
// Splits a line into words by the delimiter and max number of delimitation.
func GetSplitLine(line string, delimiter string, numWords int) SplitLine {
words := strings.SplitN(line, delimiter, numWords)
trimmedWords := make([]string, len(words))
for i, word := range words {
trimmedWords[i] = strings.TrimSpace(word)
}
return SplitLine{line, trimmedWords}
}
// Splits a paragraph into lines by newline and then splits each line into words specified by the delimiter and max number of delimitation.
func GetSplitLines(paragraph string, delimiter string, numWords int) []SplitLine {
lines := []SplitLine{}
for _, line := range strings.Split(paragraph, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
lines = append(lines, GetSplitLine(trimmed, delimiter, numWords))
}
return lines
}

View file

@ -0,0 +1,37 @@
package exec
import (
"fmt"
"os/exec"
"strings"
"awalsh128.com/cache-apt-pkgs-action/src/internal/logging"
)
// An executor that proxies command executions from the OS.
//
// NOTE: Extra abstraction layer needed for testing and replay.
type BinExecutor struct{}
func (c *BinExecutor) Exec(name string, arg ...string) *Execution {
cmd := exec.Command(name, arg...)
out, err := cmd.CombinedOutput()
if err != nil {
logging.Fatal(err)
}
execution := &Execution{
Cmd: name + " " + strings.Join(arg, " "),
CombinedOut: string(out),
ExitCode: cmd.ProcessState.ExitCode(),
}
logging.DebugLazy(func() string {
return fmt.Sprintf("EXECUTION-OBJ-START\n%s\nEXECUTION-OBJ-END", execution.Serialize())
})
if err != nil {
logging.Fatal(execution.Error())
}
return execution
}

View file

@ -0,0 +1,49 @@
package exec
import (
"encoding/json"
"fmt"
"awalsh128.com/cache-apt-pkgs-action/src/internal/logging"
)
type Executor interface {
// Executes a command and either returns the output or exits the programs and writes the output (including error) to STDERR.
Exec(name string, arg ...string) *Execution
}
type Execution struct {
Cmd string
CombinedOut string
ExitCode int
}
// Gets the error, if the command ran with a non-zero exit code.
func (e *Execution) Error() error {
if e.ExitCode == 0 {
return nil
}
return fmt.Errorf(
"Error encountered running %s\nExited with status code %d; see combined std[out,err] below:\n%s",
e.Cmd,
e.ExitCode,
e.CombinedOut,
)
}
func DeserializeExecution(payload string) *Execution {
var execution Execution
err := json.Unmarshal([]byte(payload), &execution)
if err != nil {
logging.Fatalf("Error encountered deserializing Execution object.\n%s", err)
}
return &execution
}
func (e *Execution) Serialize() string {
bytes, err := json.MarshalIndent(e, "", " ")
if err != nil {
logging.Fatalf("Error encountered serializing Execution object.\n%s", err)
}
return string(bytes)
}

View file

@ -0,0 +1,74 @@
package exec
import (
"bufio"
"os"
"strings"
"awalsh128.com/cache-apt-pkgs-action/src/internal/logging"
)
// An executor that replays execution results from a recorded result.
type ReplayExecutor struct {
logFilepath string
cmdExecs map[string]*Execution
}
func NewReplayExecutor(logFilepath string) *ReplayExecutor {
file, err := os.Open(logFilepath)
if err != nil {
logging.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
cmdExecs := make(map[string]*Execution)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "EXECUTION-OBJ-START") {
payload := ""
for scanner.Scan() {
line = scanner.Text()
if strings.Contains(line, "EXECUTION-OBJ-END") {
execution := DeserializeExecution(payload)
cmdExecs[execution.Cmd] = execution
break
} else {
payload += line + "\n"
}
}
}
}
if err := scanner.Err(); err != nil {
logging.Fatal(err)
}
return &ReplayExecutor{logFilepath, cmdExecs}
}
func (e *ReplayExecutor) getCmds() []string {
cmds := []string{}
for cmd := range e.cmdExecs {
cmds = append(cmds, cmd)
}
return cmds
}
func (e *ReplayExecutor) Exec(name string, arg ...string) *Execution {
cmd := name + " " + strings.Join(arg, " ")
value, ok := e.cmdExecs[cmd]
if !ok {
var available string
if len(e.getCmds()) > 0 {
available = "\n" + strings.Join(e.getCmds(), "\n")
} else {
available = " NONE"
}
logging.Fatalf(
"Unable to replay command '%s'.\n"+
"No command found in the debug log; available commands:%s", cmd, available)
}
return value
}

View file

@ -0,0 +1,57 @@
package logging
import (
"fmt"
"log"
"os"
"path/filepath"
)
type Logger struct {
wrapped *log.Logger
Filename string
Debug bool
}
var logger *Logger
var LogFilepath = os.Args[0] + ".log"
func Init(filename string, debug bool) *Logger {
os.Remove(LogFilepath)
file, err := os.OpenFile(LogFilepath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
os.Exit(2)
}
cwd, _ := os.Getwd()
logger = &Logger{
wrapped: log.New(file, "", log.LstdFlags),
Filename: filepath.Join(cwd, file.Name()),
Debug: debug,
}
Debug("Debug log created at %s", logger.Filename)
return logger
}
func DebugLazy(getLine func() string) {
if logger.Debug {
logger.wrapped.Println(getLine())
}
}
func Debug(format string, a ...any) {
if logger.Debug {
logger.wrapped.Printf(format, a...)
}
}
func Fatal(err error) {
fmt.Fprintf(os.Stderr, "%s", err.Error())
logger.wrapped.Fatal(err)
}
func Fatalf(format string, a ...any) {
fmt.Fprintf(os.Stderr, format+"\n", a...)
logger.wrapped.Fatalf(format, a...)
}