From da84b3168da99c1e0206968bf7895f6ed2263871 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:05:49 +0000 Subject: [PATCH 1/3] feat: add gh-release template --- .../{ => bak}/src/devcontainer-feature.json | 0 templates/{ => bak}/src/install.sh | 0 templates/{ => bak}/src/library_scripts.sh | 0 templates/bak/test/scenarios.json | 16 ++ templates/bak/test/test.sh | 9 + templates/bak/test/test_debian.sh | 9 + templates/bak/test/test_specific_version.sh | 9 + templates/gh-release/boilerplate.yml | 19 ++ .../gh-release/devcontainer-feature.json | 18 ++ templates/gh-release/install.sh | 21 +++ templates/gh-release/library_scripts.sh | 173 ++++++++++++++++++ templates/test/boilerplate.yml | 13 ++ templates/test/scenarios.json | 8 +- templates/test/test.sh | 13 +- templates/test/test_debian.sh | 12 +- templates/test/test_specific_version.sh | 14 +- 16 files changed, 327 insertions(+), 7 deletions(-) rename templates/{ => bak}/src/devcontainer-feature.json (100%) rename templates/{ => bak}/src/install.sh (100%) rename templates/{ => bak}/src/library_scripts.sh (100%) create mode 100644 templates/bak/test/scenarios.json create mode 100755 templates/bak/test/test.sh create mode 100755 templates/bak/test/test_debian.sh create mode 100755 templates/bak/test/test_specific_version.sh create mode 100644 templates/gh-release/boilerplate.yml create mode 100644 templates/gh-release/devcontainer-feature.json create mode 100755 templates/gh-release/install.sh create mode 100644 templates/gh-release/library_scripts.sh create mode 100644 templates/test/boilerplate.yml diff --git a/templates/src/devcontainer-feature.json b/templates/bak/src/devcontainer-feature.json similarity index 100% rename from templates/src/devcontainer-feature.json rename to templates/bak/src/devcontainer-feature.json diff --git a/templates/src/install.sh b/templates/bak/src/install.sh similarity index 100% rename from templates/src/install.sh rename to templates/bak/src/install.sh diff --git a/templates/src/library_scripts.sh b/templates/bak/src/library_scripts.sh similarity index 100% rename from templates/src/library_scripts.sh rename to templates/bak/src/library_scripts.sh diff --git a/templates/bak/test/scenarios.json b/templates/bak/test/scenarios.json new file mode 100644 index 000000000..48ca39c54 --- /dev/null +++ b/templates/bak/test/scenarios.json @@ -0,0 +1,16 @@ +{ + "test_debian": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": {} + } + }, + "test_specific_version": { + "image": "mcr.microsoft.com/devcontainers/base:debian", + "features": { + "": { + "version": "x.y.z" + } + } + } +} \ No newline at end of file diff --git a/templates/bak/test/test.sh b/templates/bak/test/test.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_debian.sh b/templates/bak/test/test_debian.sh new file mode 100755 index 000000000..b2873a73c --- /dev/null +++ b/templates/bak/test/test_debian.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something is installed" something --version + +reportResults diff --git a/templates/bak/test/test_specific_version.sh b/templates/bak/test/test_specific_version.sh new file mode 100755 index 000000000..e4858fa21 --- /dev/null +++ b/templates/bak/test/test_specific_version.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +source dev-container-features-test-lib + +check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" + +reportResults diff --git a/templates/gh-release/boilerplate.yml b/templates/gh-release/boilerplate.yml new file mode 100644 index 000000000..16ec14aa3 --- /dev/null +++ b/templates/gh-release/boilerplate.yml @@ -0,0 +1,19 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: Description + description: A brief description of the feature. + + - name: Repository + description: The GitHub repository in the format "owner/repo" (e.g., akamai/cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). + + # - name: BinaryVersionCommands + # description: The commands to check the version of the installed binaries (comma-separated if multiple). + # default: "--version" + + # - name: BinaryNonLatestVersions + # description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. diff --git a/templates/gh-release/devcontainer-feature.json b/templates/gh-release/devcontainer-feature.json new file mode 100644 index 000000000..466db7e1e --- /dev/null +++ b/templates/gh-release/devcontainer-feature.json @@ -0,0 +1,18 @@ +{ + "id": "{{.ID}}", + "version": "1.0.0", + "name": "{{.ID}} (via Github Releases)", + "documentationURL": "http://github.com/devcontainers-extra/features/tree/main/src/{{.ID}}", + "description": "{{.Description}}", + "options": { + "version": { + "default": "latest", + "description": "Select the version to install.", + "proposals": [ + "latest" + ], + "type": "string" + } + }, + "installsAfter": [] +} diff --git a/templates/gh-release/install.sh b/templates/gh-release/install.sh new file mode 100755 index 000000000..ff1c69aeb --- /dev/null +++ b/templates/gh-release/install.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-extra/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + +# Example nanolayer installation via devcontainer-feature +$nanolayer_location \ + install \ + devcontainer-feature \ + "ghcr.io/devcontainers-extra/features/gh-release:1" \ + --option repo='{{.Repository}}' --option binaryNames='{{.BinaryNames}}' --option version="$VERSION" + +echo 'Done!' diff --git a/templates/gh-release/library_scripts.sh b/templates/gh-release/library_scripts.sh new file mode 100644 index 000000000..f6d0760d7 --- /dev/null +++ b/templates/gh-release/library_scripts.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ]; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ]; then + if [ -x "/usr/bin/apt-get" ]; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ]; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ]; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up() { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + if [ -x "/sbin/apk" ]; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-extra/nanolayer/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} diff --git a/templates/test/boilerplate.yml b/templates/test/boilerplate.yml new file mode 100644 index 000000000..fe68ad94c --- /dev/null +++ b/templates/test/boilerplate.yml @@ -0,0 +1,13 @@ +variables: + - name: ID + description: The unique identifier for the feature (e.g., akamai-cli). + + - name: BinaryNames + description: The names of the binaries to install from the release (comma-separated if multiple). + + - name: BinaryVersionCommands + description: The commands to check the version of the installed binaries (comma-separated if multiple). + default: "--version" + + - name: BinaryNonLatestVersions + description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here. diff --git a/templates/test/scenarios.json b/templates/test/scenarios.json index 48ca39c54..283d4b0ab 100644 --- a/templates/test/scenarios.json +++ b/templates/test/scenarios.json @@ -2,15 +2,15 @@ "test_debian": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": {} + "{{.ID}}": {} } }, "test_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:debian", "features": { - "": { - "version": "x.y.z" + "{{.ID}}": { + "version": "{{index (splitList "," .BinaryNonLatestVersions) 0}}" } } } -} \ No newline at end of file +} diff --git a/templates/test/test.sh b/templates/test/test.sh index b2873a73c..f5101fa3f 100755 --- a/templates/test/test.sh +++ b/templates/test/test.sh @@ -4,6 +4,17 @@ set -e source dev-container-features-test-lib -check "something is installed" something --version +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} + +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_debian.sh b/templates/test/test_debian.sh index b2873a73c..f7780a21d 100755 --- a/templates/test/test_debian.sh +++ b/templates/test/test_debian.sh @@ -3,7 +3,17 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} -check "something is installed" something --version +check "{{$binary}} is installed" {{$binary}} {{$versionCmd}} +{{- end}} reportResults diff --git a/templates/test/test_specific_version.sh b/templates/test/test_specific_version.sh index e4858fa21..0108dac96 100755 --- a/templates/test/test_specific_version.sh +++ b/templates/test/test_specific_version.sh @@ -3,7 +3,19 @@ set -e source dev-container-features-test-lib +{{- $binaryNames := splitList "," .BinaryNames}} +{{- $versionCommands := splitList "," .BinaryVersionCommands}} +{{- $nonLatestVersions := splitList "," .BinaryNonLatestVersions}} +{{- range $index, $binary := $binaryNames}} +{{- $versionCmd := "--version"}} +{{- if gt (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands $index}} +{{- else if eq (len $versionCommands) 1}} +{{- $versionCmd = index $versionCommands 0}} +{{- end}} +{{- $nonLatestVersion := index $nonLatestVersions $index}} -check "something version is equal to x.y.z" sh -c "something --version | grep 'x.y.z'" +check "{{$binary}} version is equal to {{$nonLatestVersion}}" sh -c "{{$binary}} {{$versionCmd}} | grep '{{$nonLatestVersion}}'" +{{- end}} reportResults From ca2ebda4eb84de4092cdbb79faacb8f4b838baf2 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:07:08 +0000 Subject: [PATCH 2/3] ci: generate gh-release based feature with template --- .github/workflows/generate-gh-feature.yml | 134 ++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/generate-gh-feature.yml diff --git a/.github/workflows/generate-gh-feature.yml b/.github/workflows/generate-gh-feature.yml new file mode 100644 index 000000000..1719d1db5 --- /dev/null +++ b/.github/workflows/generate-gh-feature.yml @@ -0,0 +1,134 @@ +name: Generate Feature from gh-release Template + +on: + workflow_dispatch: + inputs: + ID: + description: 'Feature identifier (e.g., akamai-cli)' + required: true + type: string + Description: + description: 'Feature description' + required: true + type: string + Repository: + description: 'GitHub repository in format "owner/repo" (e.g., akamai/cli)' + required: true + type: string + BinaryNames: + description: 'Binary names (comma-separated if multiple)' + required: true + type: string + BinaryVersionCommands: + description: 'Version check commands (comma-separated if multiple)' + required: false + type: string + default: '--version' + BinaryNonLatestVersions: + description: 'Non-latest versions for testing (comma-separated if multiple)' + required: true + type: string + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.ARCHIVE_TOKEN }} + + - name: Check if feature already exists + run: | + if [ -d "src/${{ inputs.ID }}" ] || [ -d "test/${{ inputs.ID }}" ]; then + echo "❌ Feature '${{ inputs.ID }}' already exists!" + echo "Please use a different feature ID or remove the existing feature first." + exit 1 + fi + echo "✅ Feature ID is available" + + - name: Validate GitHub repository exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh api repos/${{ inputs.Repository }} > /dev/null 2>&1; then + echo "✅ Repository '${{ inputs.Repository }}' exists and is accessible" + else + echo "⚠️ Warning: Could not verify repository '${{ inputs.Repository }}'" + echo "The repository might be private, not exist, or you don't have access to it." + echo "Continuing with feature generation..." + fi + + - name: Download boilerplate + run: | + wget -q https://github.com/gruntwork-io/boilerplate/releases/download/v0.10.1/boilerplate_linux_amd64 + chmod +x boilerplate_linux_amd64 + + - name: Create vars.yml + run: | + cat > vars.yml << 'EOF' + ID: ${{ inputs.ID }} + Description: ${{ inputs.Description }} + Repository: ${{ inputs.Repository }} + BinaryNames: ${{ inputs.BinaryNames }} + BinaryVersionCommands: ${{ inputs.BinaryVersionCommands }} + BinaryNonLatestVersions: ${{ inputs.BinaryNonLatestVersions }} + EOF + echo "📝 Created vars.yml:" + cat vars.yml + + - name: Create branch for new feature + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b feature/${{ inputs.ID }}-init + + - name: Generate feature files + run: | + echo "🔧 Generating feature files from templates/gh-release..." + ./boilerplate_linux_amd64 \ + --template-url templates/gh-release/ \ + --output-folder src/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Generate test files + run: | + echo "🧪 Generating test files from templates/test..." + ./boilerplate_linux_amd64 \ + --template-url templates/test/ \ + --output-folder test/${{ inputs.ID }} \ + --var-file vars.yml \ + --non-interactive + + - name: Commit changes + run: | + git add -A + git commit -m "feat(${{ inputs.ID }}): init + + Generated using boilerplate templates." + + - name: Push changes + run: git push origin feature/${{ inputs.ID }}-init + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.ARCHIVE_TOKEN }} + run: | + gh pr create \ + --title "feat(${{ inputs.ID }}): init" \ + --body "## New Feature: \`${{ inputs.ID }}\` + + **Description:** ${{ inputs.Description }} + + **Repository:** [${{ inputs.Repository }}](https://github.com/${{ inputs.Repository }}) + + **Binaries:** \`${{ inputs.BinaryNames }}\` + + **Version Commands:** \`${{ inputs.BinaryVersionCommands }}\` + + **Test Versions:** \`${{ inputs.BinaryNonLatestVersions }}\` + + --- + + This PR was automatically generated using the boilerplate templates. From f571228986cd5cf7eaa25b8c0fa0a997fc259e33 Mon Sep 17 00:00:00 2001 From: koralowiec <36413794+koralowiec@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:08:51 +0000 Subject: [PATCH 3/3] chore: remove comments from gh-release template --- templates/gh-release/boilerplate.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/templates/gh-release/boilerplate.yml b/templates/gh-release/boilerplate.yml index 16ec14aa3..6f1ba7a9e 100644 --- a/templates/gh-release/boilerplate.yml +++ b/templates/gh-release/boilerplate.yml @@ -10,10 +10,3 @@ variables: - name: BinaryNames description: The names of the binaries to install from the release (comma-separated if multiple). - - # - name: BinaryVersionCommands - # description: The commands to check the version of the installed binaries (comma-separated if multiple). - # default: "--version" - - # - name: BinaryNonLatestVersions - # description: Specific non-latest versions for test scenarios (comma-separated if multiple). For example, if 1.2.3 is the latest version, you should use 1.2.2 here.