From 5203b8703f5847dcf56d4f17679c847143c327b8 Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 19:32:33 +0100 Subject: [PATCH 1/9] Add linux-aarch64 build target using GitHub ARM runner --- .github/workflows/build.yml | 19 +++++++++++++++++-- .github/workflows/release.yml | 19 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9327fe..d526e31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,10 @@ jobs: cli_assembly_id: linux-amd64 cli_assembly_format: tar.gz cli_assembly_arch: x86-64-v3 + - os: ubuntu-24.04-arm + cli_assembly_id: linux-aarch64 + cli_assembly_format: tar.gz + cli_assembly_arch: armv8-a - os: windows-latest cli_assembly_id: windows-amd64 cli_assembly_format: zip @@ -45,18 +49,29 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - uses: Project-Env/project-env-github-action@v1.5.0 + if: ${{ matrix.cli_assembly_id != 'linux-aarch64' }} + - uses: graalvm/setup-graalvm@v1 + if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} + with: + java-version: '21' + distribution: 'graalvm-community' + github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: stCarolas/setup-maven@v5 + if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} + with: + maven-version: 3.9.9 - uses: repolevedavaj/install-nsis@v1.1.0 with: nsis-version: 3.08 if: ${{ runner.os == 'Windows'}} - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image -Psonar "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" - if: ${{ runner.os == 'Linux' && github.actor != 'dependabot[bot]' }} + if: ${{ matrix.cli_assembly_id == 'linux-amd64' && github.actor != 'dependabot[bot]' }} env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" - if: ${{ runner.os != 'Linux' || github.actor == 'dependabot[bot]' }} + if: ${{ matrix.cli_assembly_id != 'linux-amd64' || github.actor == 'dependabot[bot]' }} env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a02a4a2..5f9675e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,10 @@ jobs: cli_assembly_id: linux-amd64 cli_assembly_format: tar.gz cli_assembly_arch: x86-64-v3 + - os: ubuntu-24.04-arm + cli_assembly_id: linux-aarch64 + cli_assembly_format: tar.gz + cli_assembly_arch: armv8-a - os: windows-latest cli_assembly_id: windows-amd64 cli_assembly_format: zip @@ -60,18 +64,29 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - uses: Project-Env/project-env-github-action@v1.5.0 + if: ${{ matrix.cli_assembly_id != 'linux-aarch64' }} + - uses: graalvm/setup-graalvm@v1 + if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} + with: + java-version: '21' + distribution: 'graalvm-community' + github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: stCarolas/setup-maven@v5 + if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} + with: + maven-version: 3.9.9 - uses: repolevedavaj/install-nsis@v1.1.0 with: nsis-version: 3.08 if: ${{ runner.os == 'Windows'}} - run: mvn -B -s etc/m2/settings.xml deploy -Pnative-image "-Drevision=${{ needs.create-release.outputs.revision }}" "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" - if: ${{ runner.os == 'Linux' }} + if: ${{ matrix.cli_assembly_id == 'linux-amd64' }} env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Drevision=${{ needs.create-release.outputs.revision }}" "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" - if: ${{ runner.os != 'Linux' }} + if: ${{ matrix.cli_assembly_id != 'linux-amd64' }} env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 22ac68b264ca437efac25499224d0e4b7143fbba Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 19:50:10 +0100 Subject: [PATCH 2/9] Add multi-arch Homebrew cask/formula templates and rewrite publish workflow --- .github/workflows/homebrew-releaser.yml | 86 +++++++++++++++++++++---- etc/homebrew/cask.rb.template | 14 ++++ etc/homebrew/formula.rb.template | 36 +++++++++++ 3 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 etc/homebrew/cask.rb.template create mode 100644 etc/homebrew/formula.rb.template diff --git a/.github/workflows/homebrew-releaser.yml b/.github/workflows/homebrew-releaser.yml index e380330..da55332 100644 --- a/.github/workflows/homebrew-releaser.yml +++ b/.github/workflows/homebrew-releaser.yml @@ -5,22 +5,82 @@ on: workflow_dispatch: inputs: tag_name: - description: "Tag name of the release" + description: "Tag name of the release (e.g. v3.27.0)" required: true jobs: publish: runs-on: ubuntu-latest steps: - - id: revision - run: echo "revision=$(echo ${{ github.event.release.tag_name || github.event.inputs.tag_name }} | cut -c2-)" >> $GITHUB_OUTPUT - - uses: mislav/bump-homebrew-formula-action@v3 - with: - formula-name: project-env-cli - formula-path: Casks/project-env-cli.rb - homebrew-tap: Project-Env/homebrew-tap - base-branch: main - tag-name: ${{ github.event.release.tag_name || github.event.inputs.tag_name }} - download-url: https://github.com/Project-Env/project-env-cli/releases/download/v${{ steps.revision.outputs.revision }}/cli-${{ steps.revision.outputs.revision }}-macos-aarch64.tar.gz - commit-message: bump {{formulaName}} to {{version}} + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve version + id: version + run: | + TAG="${{ github.event.release.tag_name || github.event.inputs.tag_name }}" + VERSION="${TAG#v}" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Download release assets env: - COMMITTER_TOKEN: ${{ secrets.HOMEBREW_RELEASER_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.HOMEBREW_RELEASER_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + VARIANTS="macos-amd64 macos-aarch64 linux-amd64 linux-aarch64" + for VARIANT in $VARIANTS; do + gh release download "v${VERSION}" \ + --repo Project-Env/project-env-cli \ + --pattern "cli-${VERSION}-${VARIANT}.tar.gz" \ + --dir assets + done + + - name: Compute checksums + id: checksums + run: | + VERSION="${{ steps.version.outputs.version }}" + for VARIANT in macos-amd64 macos-aarch64 linux-amd64 linux-aarch64; do + KEY=$(echo "$VARIANT" | tr '-' '_' | tr '[:lower:]' '[:upper:]') + SHA=$(sha256sum "assets/cli-${VERSION}-${VARIANT}.tar.gz" | cut -d ' ' -f 1) + echo "sha256_${KEY}=${SHA}" >> "$GITHUB_OUTPUT" + done + + - name: Generate cask and formula from templates + run: | + VERSION="${{ steps.version.outputs.version }}" + mkdir -p output/Casks output/Formula + + sed \ + -e "s/{{VERSION}}/${VERSION}/g" \ + -e "s/{{SHA256_MACOS_AARCH64}}/${{ steps.checksums.outputs.sha256_MACOS_AARCH64 }}/g" \ + -e "s/{{SHA256_MACOS_AMD64}}/${{ steps.checksums.outputs.sha256_MACOS_AMD64 }}/g" \ + etc/homebrew/cask.rb.template > output/Casks/project-env-cli.rb + + sed \ + -e "s/{{VERSION}}/${VERSION}/g" \ + -e "s/{{SHA256_MACOS_AARCH64}}/${{ steps.checksums.outputs.sha256_MACOS_AARCH64 }}/g" \ + -e "s/{{SHA256_MACOS_AMD64}}/${{ steps.checksums.outputs.sha256_MACOS_AMD64 }}/g" \ + -e "s/{{SHA256_LINUX_AARCH64}}/${{ steps.checksums.outputs.sha256_LINUX_AARCH64 }}/g" \ + -e "s/{{SHA256_LINUX_AMD64}}/${{ steps.checksums.outputs.sha256_LINUX_AMD64 }}/g" \ + etc/homebrew/formula.rb.template > output/Formula/project-env-cli.rb + + - name: Push to homebrew-tap + env: + GH_TOKEN: ${{ secrets.HOMEBREW_RELEASER_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + git clone "https://x-access-token:${GH_TOKEN}@github.com/Project-Env/homebrew-tap.git" homebrew-tap + + mkdir -p homebrew-tap/Casks homebrew-tap/Formula + cp output/Casks/project-env-cli.rb homebrew-tap/Casks/project-env-cli.rb + cp output/Formula/project-env-cli.rb homebrew-tap/Formula/project-env-cli.rb + + cd homebrew-tap + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Casks/project-env-cli.rb Formula/project-env-cli.rb + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "bump project-env-cli to ${VERSION}" + git push origin main + fi diff --git a/etc/homebrew/cask.rb.template b/etc/homebrew/cask.rb.template new file mode 100644 index 0000000..12db24b --- /dev/null +++ b/etc/homebrew/cask.rb.template @@ -0,0 +1,14 @@ +cask "project-env-cli" do + arch arm: "aarch64", intel: "amd64" + + version "{{VERSION}}" + sha256 arm: "{{SHA256_MACOS_AARCH64}}", + intel: "{{SHA256_MACOS_AMD64}}" + + url "https://github.com/Project-Env/project-env-cli/releases/download/#{version}/cli-#{version}-macos-#{arch}.tar.gz" + name "project-env-cli" + desc "Project-Env CLI" + homepage "https://project-env.github.io/" + + binary "project-env-cli" +end diff --git a/etc/homebrew/formula.rb.template b/etc/homebrew/formula.rb.template new file mode 100644 index 0000000..4ba4e7a --- /dev/null +++ b/etc/homebrew/formula.rb.template @@ -0,0 +1,36 @@ +class ProjectEnvCli < Formula + desc "Project-Env CLI" + homepage "https://project-env.github.io/" + version "{{VERSION}}" + license "MIT" + + on_macos do + on_arm do + url "https://github.com/Project-Env/project-env-cli/releases/download/#{version}/cli-#{version}-macos-aarch64.tar.gz" + sha256 "{{SHA256_MACOS_AARCH64}}" + end + on_intel do + url "https://github.com/Project-Env/project-env-cli/releases/download/#{version}/cli-#{version}-macos-amd64.tar.gz" + sha256 "{{SHA256_MACOS_AMD64}}" + end + end + + on_linux do + on_arm do + url "https://github.com/Project-Env/project-env-cli/releases/download/#{version}/cli-#{version}-linux-aarch64.tar.gz" + sha256 "{{SHA256_LINUX_AARCH64}}" + end + on_intel do + url "https://github.com/Project-Env/project-env-cli/releases/download/#{version}/cli-#{version}-linux-amd64.tar.gz" + sha256 "{{SHA256_LINUX_AMD64}}" + end + end + + def install + bin.install "project-env-cli" + end + + test do + assert_predicate bin/"project-env-cli", :executable? + end +end From 84782762bfd59e0edd1955c2414f1aba2dbfab1c Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 19:51:32 +0100 Subject: [PATCH 3/9] Add Eclipse IDE files to .gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 4288079..5ea3201 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,12 @@ *.iml .idea +# Eclipse specific files +.classpath +.factorypath +.project +.settings + # Contains all project specific toolsConfiguration .toolsConfiguration From 600c26d8edf93bc6dd6409324bc5f3b322783afb Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 20:28:24 +0100 Subject: [PATCH 4/9] Harden GitHub Actions workflows for security best practices - Pin all actions to full-length commit SHAs to prevent supply chain attacks - Add permissions: {} deny-by-default with least-privilege job-level grants - Eliminate script injection by moving all ${{ }} expressions from run: blocks into env: blocks - Replace deprecated actions/upload-release-asset with gh release upload - Restrict build trigger to main/master branches and pull_request events - Add timeout-minutes to all jobs to prevent runaway builds - Add concurrency controls to build workflow - Add fail-fast: false to release matrix for complete error visibility - Standardize actions/checkout to v6 across all workflows - Add explicit shell: bash for Windows runner bash commands --- .github/workflows/build.yml | 46 ++++++++++++----- .github/workflows/homebrew-releaser.yml | 40 ++++++++++----- .github/workflows/release-drafter.yml | 5 +- .github/workflows/release.yml | 68 ++++++++++++++----------- .github/workflows/winget-releaser.yml | 17 +++++-- 5 files changed, 118 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d526e31..d816b24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,11 +2,25 @@ name: Build on: push: + branches: + - master + - main + pull_request: workflow_dispatch: +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + contents: read + packages: read strategy: matrix: os: [ macos-latest, windows-latest, ubuntu-latest ] @@ -33,46 +47,52 @@ jobs: cli_assembly_format: tar.gz cli_assembly_arch: armv8-a steps: - - uses: ilammy/msvc-dev-cmd@v1 + - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 if: ${{ runner.os == 'Windows'}} - run: ldd --version if: ${{ runner.os == 'Linux'}} - - uses: actions/checkout@v6 - - uses: actions/cache@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - uses: actions/cache@v4 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - uses: Project-Env/project-env-github-action@v1.5.0 + - uses: Project-Env/project-env-github-action@8958dac5824fd73ced04459e76f49654b95044a6 # v1.5.0 if: ${{ matrix.cli_assembly_id != 'linux-aarch64' }} - - uses: graalvm/setup-graalvm@v1 + - uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1 if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} with: java-version: '21' distribution: 'graalvm-community' github-token: ${{ secrets.GITHUB_TOKEN }} - - uses: stCarolas/setup-maven@v5 + - uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # v5 if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} with: maven-version: 3.9.9 - - uses: repolevedavaj/install-nsis@v1.1.0 + - uses: repolevedavaj/install-nsis@a55ed92772254d1e51d880f85ce9b5719f907801 # v1.1.0 with: nsis-version: 3.08 if: ${{ runner.os == 'Windows'}} - - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image -Psonar "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" + - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image -Psonar "-Dcli.assembly.id=${CLI_ASSEMBLY_ID}" "-Dcli.assembly.format=${CLI_ASSEMBLY_FORMAT}" "-Dcli.binary.arch=${CLI_BINARY_ARCH}" if: ${{ matrix.cli_assembly_id == 'linux-amd64' && github.actor != 'dependabot[bot]' }} env: + CLI_ASSEMBLY_ID: ${{ matrix.cli_assembly_id }} + CLI_ASSEMBLY_FORMAT: ${{ matrix.cli_assembly_format }} + CLI_BINARY_ARCH: ${{ matrix.cli_assembly_arch }} GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" + - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Dcli.assembly.id=${CLI_ASSEMBLY_ID}" "-Dcli.assembly.format=${CLI_ASSEMBLY_FORMAT}" "-Dcli.binary.arch=${CLI_BINARY_ARCH}" if: ${{ matrix.cli_assembly_id != 'linux-amd64' || github.actor == 'dependabot[bot]' }} env: + CLI_ASSEMBLY_ID: ${{ matrix.cli_assembly_id }} + CLI_ASSEMBLY_FORMAT: ${{ matrix.cli_assembly_format }} + CLI_BINARY_ARCH: ${{ matrix.cli_assembly_arch }} GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Sign and notarize executable @@ -89,19 +109,19 @@ jobs: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: ${{ github.actor != 'dependabot[bot]' }} with: name: cli-dev-${{ matrix.cli_assembly_id }}.${{ matrix.cli_assembly_format }} path: code/cli/target/cli-dev-${{ matrix.cli_assembly_id }}.${{ matrix.cli_assembly_format }} - - uses: repolevedavaj/create-cli-app-nsis-installer@main + - uses: repolevedavaj/create-cli-app-nsis-installer@5b86acc28eceb315ac12fb2dc7eca3907f47ca99 # main if: ${{ runner.os == 'Windows'}} with: package-identifier: ProjectEnv.ProjectEnvCli package-name: Project-Env Cli package-version: dev source-directory: code/cli/target/cli-dev-windows-amd64 - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 if: ${{ runner.os == 'Windows' && github.actor != 'dependabot[bot]' }} with: name: cli-dev-${{ matrix.cli_assembly_id }}-setup.exe diff --git a/.github/workflows/homebrew-releaser.yml b/.github/workflows/homebrew-releaser.yml index da55332..24f8839 100644 --- a/.github/workflows/homebrew-releaser.yml +++ b/.github/workflows/homebrew-releaser.yml @@ -1,4 +1,5 @@ name: Publish to Homebrew + on: release: types: [published] @@ -7,25 +8,34 @@ on: tag_name: description: "Tag name of the release (e.g. v3.27.0)" required: true + +permissions: {} + jobs: publish: runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Resolve version id: version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ github.event.inputs.tag_name }} run: | - TAG="${{ github.event.release.tag_name || github.event.inputs.tag_name }}" + TAG="${RELEASE_TAG:-$INPUT_TAG}" VERSION="${TAG#v}" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - name: Download release assets env: GH_TOKEN: ${{ secrets.HOMEBREW_RELEASER_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" VARIANTS="macos-amd64 macos-aarch64 linux-amd64 linux-aarch64" for VARIANT in $VARIANTS; do gh release download "v${VERSION}" \ @@ -36,8 +46,9 @@ jobs: - name: Compute checksums id: checksums + env: + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" for VARIANT in macos-amd64 macos-aarch64 linux-amd64 linux-aarch64; do KEY=$(echo "$VARIANT" | tr '-' '_' | tr '[:lower:]' '[:upper:]') SHA=$(sha256sum "assets/cli-${VERSION}-${VARIANT}.tar.gz" | cut -d ' ' -f 1) @@ -45,29 +56,34 @@ jobs: done - name: Generate cask and formula from templates + env: + VERSION: ${{ steps.version.outputs.version }} + SHA256_MACOS_AARCH64: ${{ steps.checksums.outputs.sha256_MACOS_AARCH64 }} + SHA256_MACOS_AMD64: ${{ steps.checksums.outputs.sha256_MACOS_AMD64 }} + SHA256_LINUX_AARCH64: ${{ steps.checksums.outputs.sha256_LINUX_AARCH64 }} + SHA256_LINUX_AMD64: ${{ steps.checksums.outputs.sha256_LINUX_AMD64 }} run: | - VERSION="${{ steps.version.outputs.version }}" mkdir -p output/Casks output/Formula sed \ -e "s/{{VERSION}}/${VERSION}/g" \ - -e "s/{{SHA256_MACOS_AARCH64}}/${{ steps.checksums.outputs.sha256_MACOS_AARCH64 }}/g" \ - -e "s/{{SHA256_MACOS_AMD64}}/${{ steps.checksums.outputs.sha256_MACOS_AMD64 }}/g" \ + -e "s/{{SHA256_MACOS_AARCH64}}/${SHA256_MACOS_AARCH64}/g" \ + -e "s/{{SHA256_MACOS_AMD64}}/${SHA256_MACOS_AMD64}/g" \ etc/homebrew/cask.rb.template > output/Casks/project-env-cli.rb sed \ -e "s/{{VERSION}}/${VERSION}/g" \ - -e "s/{{SHA256_MACOS_AARCH64}}/${{ steps.checksums.outputs.sha256_MACOS_AARCH64 }}/g" \ - -e "s/{{SHA256_MACOS_AMD64}}/${{ steps.checksums.outputs.sha256_MACOS_AMD64 }}/g" \ - -e "s/{{SHA256_LINUX_AARCH64}}/${{ steps.checksums.outputs.sha256_LINUX_AARCH64 }}/g" \ - -e "s/{{SHA256_LINUX_AMD64}}/${{ steps.checksums.outputs.sha256_LINUX_AMD64 }}/g" \ + -e "s/{{SHA256_MACOS_AARCH64}}/${SHA256_MACOS_AARCH64}/g" \ + -e "s/{{SHA256_MACOS_AMD64}}/${SHA256_MACOS_AMD64}/g" \ + -e "s/{{SHA256_LINUX_AARCH64}}/${SHA256_LINUX_AARCH64}/g" \ + -e "s/{{SHA256_LINUX_AMD64}}/${SHA256_LINUX_AMD64}/g" \ etc/homebrew/formula.rb.template > output/Formula/project-env-cli.rb - name: Push to homebrew-tap env: GH_TOKEN: ${{ secrets.HOMEBREW_RELEASER_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" git clone "https://x-access-token:${GH_TOKEN}@github.com/Project-Env/homebrew-tap.git" homebrew-tap mkdir -p homebrew-tap/Casks homebrew-tap/Formula diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 9662609..0ec05bd 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -7,13 +7,16 @@ on: - main workflow_dispatch: +permissions: {} + jobs: update_release_draft: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: write pull-requests: write steps: - - uses: release-drafter/release-drafter@v6.1.0 + - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f9675e..45c8cbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,31 +3,35 @@ name: Release on: workflow_dispatch: +permissions: {} + jobs: create-release: runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: write pull-requests: write - packages: write outputs: - upload_url: ${{ steps.release.outputs.upload_url }} revision: ${{ steps.revision.outputs.revision }} steps: - id: release - uses: release-drafter/release-drafter@v6.1.0 + uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - id: revision - run: echo "revision=$(echo '${{ steps.release.outputs.tag_name }}' | cut -c2-)" >> $GITHUB_OUTPUT + env: + TAG_NAME: ${{ steps.release.outputs.tag_name }} + run: echo "revision=$(echo "$TAG_NAME" | cut -c2-)" >> $GITHUB_OUTPUT build: runs-on: ${{ matrix.os }} + timeout-minutes: 60 permissions: contents: write - pull-requests: write packages: write needs: create-release strategy: + fail-fast: false matrix: os: [ macos-latest, windows-latest, ubuntu-latest ] include: @@ -53,44 +57,52 @@ jobs: cli_assembly_format: tar.gz cli_assembly_arch: armv8-a steps: - - uses: ilammy/msvc-dev-cmd@v1 + - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 if: ${{ runner.os == 'Windows'}} - run: ldd --version if: ${{ runner.os == 'Linux'}} - - uses: actions/checkout@v6 - - uses: actions/cache@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - uses: Project-Env/project-env-github-action@v1.5.0 + - uses: Project-Env/project-env-github-action@8958dac5824fd73ced04459e76f49654b95044a6 # v1.5.0 if: ${{ matrix.cli_assembly_id != 'linux-aarch64' }} - - uses: graalvm/setup-graalvm@v1 + - uses: graalvm/setup-graalvm@54b4f5a65c1a84b2fdfdc2078fe43df32819e4b1 # v1 if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} with: java-version: '21' distribution: 'graalvm-community' github-token: ${{ secrets.GITHUB_TOKEN }} - - uses: stCarolas/setup-maven@v5 + - uses: stCarolas/setup-maven@d6af6abeda15e98926a57b5aa970a96bb37f97d1 # v5 if: ${{ matrix.cli_assembly_id == 'linux-aarch64' }} with: maven-version: 3.9.9 - - uses: repolevedavaj/install-nsis@v1.1.0 + - uses: repolevedavaj/install-nsis@a55ed92772254d1e51d880f85ce9b5719f907801 # v1.1.0 with: nsis-version: 3.08 if: ${{ runner.os == 'Windows'}} - - run: mvn -B -s etc/m2/settings.xml deploy -Pnative-image "-Drevision=${{ needs.create-release.outputs.revision }}" "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" + - run: mvn -B -s etc/m2/settings.xml deploy -Pnative-image "-Drevision=${REVISION}" "-Dcli.assembly.id=${CLI_ASSEMBLY_ID}" "-Dcli.assembly.format=${CLI_ASSEMBLY_FORMAT}" "-Dcli.binary.arch=${CLI_BINARY_ARCH}" if: ${{ matrix.cli_assembly_id == 'linux-amd64' }} env: + REVISION: ${{ needs.create-release.outputs.revision }} + CLI_ASSEMBLY_ID: ${{ matrix.cli_assembly_id }} + CLI_ASSEMBLY_FORMAT: ${{ matrix.cli_assembly_format }} + CLI_BINARY_ARCH: ${{ matrix.cli_assembly_arch }} GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Drevision=${{ needs.create-release.outputs.revision }}" "-Dcli.assembly.id=${{ matrix.cli_assembly_id }}" "-Dcli.assembly.format=${{ matrix.cli_assembly_format }}" "-Dcli.binary.arch=${{ matrix.cli_assembly_arch }}" + - run: mvn -B -s etc/m2/settings.xml verify -Pnative-image "-Drevision=${REVISION}" "-Dcli.assembly.id=${CLI_ASSEMBLY_ID}" "-Dcli.assembly.format=${CLI_ASSEMBLY_FORMAT}" "-Dcli.binary.arch=${CLI_BINARY_ARCH}" if: ${{ matrix.cli_assembly_id != 'linux-amd64' }} env: + REVISION: ${{ needs.create-release.outputs.revision }} + CLI_ASSEMBLY_ID: ${{ matrix.cli_assembly_id }} + CLI_ASSEMBLY_FORMAT: ${{ matrix.cli_assembly_format }} + CLI_BINARY_ARCH: ${{ matrix.cli_assembly_arch }} GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: repolevedavaj/create-cli-app-nsis-installer@main + - uses: repolevedavaj/create-cli-app-nsis-installer@5b86acc28eceb315ac12fb2dc7eca3907f47ca99 # main if: ${{ runner.os == 'Windows'}} with: package-identifier: ProjectEnv.ProjectEnvCli @@ -111,20 +123,18 @@ jobs: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} - - uses: actions/upload-release-asset@v1 + - name: Upload release asset + run: gh release upload "$TAG_NAME" "$ASSET_PATH" --clobber + working-directory: code/cli/target env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: code/cli/target/cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}.${{ matrix.cli_assembly_format }} - asset_name: cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}.${{ matrix.cli_assembly_format }} - asset_content_type: application/octet-stream - - uses: actions/upload-release-asset@v1 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: v${{ needs.create-release.outputs.revision }} + ASSET_PATH: cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}.${{ matrix.cli_assembly_format }} + - name: Upload Windows installer release asset if: ${{ runner.os == 'Windows'}} + run: gh release upload "$TAG_NAME" "$ASSET_PATH" --clobber + working-directory: code/cli/target env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: code/cli/target/cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}-setup.exe - asset_name: cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}-setup.exe - asset_content_type: application/octet-stream + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: v${{ needs.create-release.outputs.revision }} + ASSET_PATH: cli-${{ needs.create-release.outputs.revision }}-${{ matrix.cli_assembly_id }}-setup.exe diff --git a/.github/workflows/winget-releaser.yml b/.github/workflows/winget-releaser.yml index 3eb5980..f446869 100644 --- a/.github/workflows/winget-releaser.yml +++ b/.github/workflows/winget-releaser.yml @@ -1,4 +1,5 @@ name: Publish to WinGet + on: release: types: [published] @@ -7,16 +8,26 @@ on: tag_name: description: "Tag name of the release" required: true + +permissions: {} + jobs: publish: runs-on: windows-latest + timeout-minutes: 30 steps: - id: revision - run: echo "revision=$(echo ${{ github.event.release.tag_name || github.event.inputs.tag_name }} | cut -c2-)" >> $GITHUB_OUTPUT - - uses: vedantmgoyal2009/winget-releaser@v2 + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ github.event.inputs.tag_name }} + shell: bash + run: | + TAG="${RELEASE_TAG:-$INPUT_TAG}" + echo "revision=${TAG#v}" >> $GITHUB_OUTPUT + - uses: vedantmgoyal2009/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2 with: identifier: ProjectEnv.ProjectEnvCli installers-regex: '-setup\.exe$' token: ${{ secrets.WINGET_RELEASER_TOKEN }} release-tag: ${{ github.event.release.tag_name || github.event.inputs.tag_name }} - version: ${{ steps.revision.outputs.revision }} \ No newline at end of file + version: ${{ steps.revision.outputs.revision }} From e21ae85c6fd37a2aef1d3d094a31ae6a685b99eb Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 20:36:58 +0100 Subject: [PATCH 5/9] Replace deprecated macos-13 runner with macos-15-intel --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d816b24..4db7a8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: cli_assembly_format: zip cli_assembly_arch: x86-64-v3 gu_executable_ext: .cmd - - os: macos-13 + - os: macos-15-intel cli_assembly_id: macos-amd64 cli_assembly_format: tar.gz cli_assembly_arch: x86-64-v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45c8cbf..7c7c3b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: cli_assembly_format: zip cli_assembly_arch: x86-64-v3 gu_executable_ext: .cmd - - os: macos-13 + - os: macos-15-intel cli_assembly_id: macos-amd64 cli_assembly_format: tar.gz cli_assembly_arch: x86-64-v3 From e02702e353dc9343d7816922c68e1bfbc013737c Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 20:47:20 +0100 Subject: [PATCH 6/9] Fix Windows native image build by defaulting to bash shell The default shell on windows-latest runners is pwsh, which interprets ${VAR} as a PowerShell variable rather than an environment variable, causing -march= to receive an empty value. --- .github/workflows/build.yml | 3 +++ .github/workflows/release.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4db7a8e..c68df84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,9 @@ jobs: build: runs-on: ${{ matrix.os }} timeout-minutes: 60 + defaults: + run: + shell: bash permissions: contents: read packages: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c7c3b4..abfe4f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,9 @@ jobs: build: runs-on: ${{ matrix.os }} timeout-minutes: 60 + defaults: + run: + shell: bash permissions: contents: write packages: write From 42f8cc8f6785ba721d056846e79718d0c6356939 Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 23:19:05 +0100 Subject: [PATCH 7/9] Add architecture-aware tool resolution for Linux ARM (aarch64) support - Improve error messages in DefaultToolsIndexManager to report specific OS/arch mismatches instead of generic 'not found' errors - Add optional targetArch field to GenericToolConfiguration so users can specify architecture-specific download URLs in project-env.toml - Update GenericToolSupport with arch+OS > OS-only > global fallback chain - Add tests for both arch-specific error messages and generic tool resolution - Update README with missing [clojure] section and new target_arch option --- README.md | 16 ++- .../cli/index/DefaultToolsIndexManager.java | 50 ++++++-- .../DefaultToolsIndexManagerArchTest.java | 75 ++++++++++++ .../core/cli/index/arch-test-index.json | 38 ++++++ .../tool-support/generic-tool-support/pom.xml | 11 ++ .../nodejs/GenericToolConfiguration.java | 3 + .../nodejs/GenericToolSupport.java | 20 +++- .../nodejs/GenericToolSupportTest.java | 113 ++++++++++++++++++ 8 files changed, 314 insertions(+), 12 deletions(-) create mode 100644 code/cli/src/test/java/io/projectenv/core/cli/index/DefaultToolsIndexManagerArchTest.java create mode 100644 code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json create mode 100644 code/tool-support/generic-tool-support/src/test/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupportTest.java diff --git a/README.md b/README.md index 013f8ec..c9cf420 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,16 @@ post_extraction_commands = [ "", ] +[clojure] +# The Clojure version. +# See https://raw.githubusercontent.com/Project-Env/project-env-tools/main/index.json +version = "" +# [optional] +# Arbitrary commands which should be executed after extracting. +post_extraction_commands = [ + "", +] + [git] # A directory with Git hooks which should be copied into the '.git/hooks' directory. hooks_directory = "" @@ -76,9 +86,11 @@ name = "" download_url = "" # [optional], if 'download_url' is configured # OS specific download URL's (they have precedence over the non-OS download URL). -# Valid values for os are 'macos', 'windows' and 'linux'. +# Valid values for target_os are 'macos', 'windows' and 'linux'. +# [optional] Valid values for target_arch are 'amd64' and 'aarch64'. +# If target_arch is omitted, the entry matches any architecture for the given OS. download_urls = [ - { target_os = "", download_url = "" }, + { target_os = "", target_arch = "", download_url = "" }, ] # [optional] # The main executable of the tool. diff --git a/code/cli/src/main/java/io/projectenv/core/cli/index/DefaultToolsIndexManager.java b/code/cli/src/main/java/io/projectenv/core/cli/index/DefaultToolsIndexManager.java index 4da3bed..b8b79fc 100644 --- a/code/cli/src/main/java/io/projectenv/core/cli/index/DefaultToolsIndexManager.java +++ b/code/cli/src/main/java/io/projectenv/core/cli/index/DefaultToolsIndexManager.java @@ -130,10 +130,23 @@ public Set getGradleVersions() { @Override public String resolveNodeJsDistributionUrl(String version) { - return Optional.ofNullable(getToolsIndex().getNodeVersions().get(ToolVersionHelper.getVersionWithoutPrefix(version))) - .map(versionEntry -> versionEntry.get(OperatingSystem.getCurrentOperatingSystem())) - .map(this::resolveDownloadUrlForCpuArchitecture) - .orElseThrow(() -> new ToolsIndexException("Failed to resolve NodeJS " + version + " from tool index")); + var strippedVersion = ToolVersionHelper.getVersionWithoutPrefix(version); + var osArchEntries = getToolsIndex().getNodeVersions().get(strippedVersion); + if (osArchEntries == null) { + throw new ToolsIndexException("Failed to resolve NodeJS " + version + " from tool index"); + } + + var archEntries = osArchEntries.get(OperatingSystem.getCurrentOperatingSystem()); + if (archEntries == null) { + throw new ToolsIndexException("NodeJS " + version + " is not available for " + OperatingSystem.getCurrentOperatingSystem()); + } + + var downloadUrl = resolveDownloadUrlForCpuArchitecture(archEntries); + if (downloadUrl == null) { + throw new ToolsIndexException("NodeJS " + version + " is not available for " + OperatingSystem.getCurrentOperatingSystem() + "/" + CpuArchitecture.getCurrentCpuArchitecture()); + } + + return downloadUrl; } @Override @@ -143,12 +156,31 @@ public Set getNodeJsVersions() { @Override public String resolveJdkDistributionUrl(String jdkDistribution, String version) { - return resolveJdkDistributionId(jdkDistribution) - .map(jdkDistributionId -> getToolsIndex().getJdkVersions().get(jdkDistributionId)) - .map(jdkDistributionEntry -> jdkDistributionEntry.get(ToolVersionHelper.getVersionWithoutPrefix(version))) - .map(versionEntry -> versionEntry.get(OperatingSystem.getCurrentOperatingSystem())) - .map(this::resolveDownloadUrlForCpuArchitecture) + var strippedVersion = ToolVersionHelper.getVersionWithoutPrefix(version); + var jdkDistributionId = resolveJdkDistributionId(jdkDistribution) .orElseThrow(() -> new ToolsIndexException("Failed to resolve " + jdkDistribution + " " + version + " from tool index")); + + var jdkDistributionEntry = getToolsIndex().getJdkVersions().get(jdkDistributionId); + if (jdkDistributionEntry == null) { + throw new ToolsIndexException("Failed to resolve " + jdkDistribution + " " + version + " from tool index"); + } + + var versionEntry = jdkDistributionEntry.get(strippedVersion); + if (versionEntry == null) { + throw new ToolsIndexException("Failed to resolve " + jdkDistribution + " " + version + " from tool index"); + } + + var archEntries = versionEntry.get(OperatingSystem.getCurrentOperatingSystem()); + if (archEntries == null) { + throw new ToolsIndexException(jdkDistribution + " " + version + " is not available for " + OperatingSystem.getCurrentOperatingSystem()); + } + + var downloadUrl = resolveDownloadUrlForCpuArchitecture(archEntries); + if (downloadUrl == null) { + throw new ToolsIndexException(jdkDistribution + " " + version + " is not available for " + OperatingSystem.getCurrentOperatingSystem() + "/" + CpuArchitecture.getCurrentCpuArchitecture()); + } + + return downloadUrl; } @Override diff --git a/code/cli/src/test/java/io/projectenv/core/cli/index/DefaultToolsIndexManagerArchTest.java b/code/cli/src/test/java/io/projectenv/core/cli/index/DefaultToolsIndexManagerArchTest.java new file mode 100644 index 0000000..e5f0d74 --- /dev/null +++ b/code/cli/src/test/java/io/projectenv/core/cli/index/DefaultToolsIndexManagerArchTest.java @@ -0,0 +1,75 @@ +package io.projectenv.core.cli.index; + +import io.projectenv.core.cli.http.DefaultHttpClientProvider; +import io.projectenv.core.commons.system.TestEnvironmentVariables; +import io.projectenv.core.toolsupport.spi.index.ToolsIndexException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DefaultToolsIndexManagerArchTest { + + @TempDir + File tempDir; + + @Test + void resolveNodeJsDistributionUrlThrowsForUnknownVersion() throws Exception { + var url = getClass().getResource("arch-test-index.json").toString(); + try (var ignored = TestEnvironmentVariables.overlayEnv(Map.of("PROJECT_ENV_TOOL_INDEX_V2", url))) { + var manager = new DefaultToolsIndexManager(tempDir, new DefaultHttpClientProvider()); + assertThatThrownBy(() -> manager.resolveNodeJsDistributionUrl("99.99.99")) + .isInstanceOf(ToolsIndexException.class) + .hasMessageContaining("Failed to resolve NodeJS 99.99.99 from tool index"); + } + } + + @Test + void resolveJdkDistributionUrlThrowsForUnknownVersion() throws Exception { + var url = getClass().getResource("arch-test-index.json").toString(); + try (var ignored = TestEnvironmentVariables.overlayEnv(Map.of("PROJECT_ENV_TOOL_INDEX_V2", url))) { + var manager = new DefaultToolsIndexManager(tempDir, new DefaultHttpClientProvider()); + assertThatThrownBy(() -> manager.resolveJdkDistributionUrl("temurin", "99.99.99")) + .isInstanceOf(ToolsIndexException.class) + .hasMessageContaining("Failed to resolve temurin 99.99.99 from tool index"); + } + } + + @Test + void resolveJdkDistributionUrlThrowsForUnknownDistribution() throws Exception { + var url = getClass().getResource("arch-test-index.json").toString(); + try (var ignored = TestEnvironmentVariables.overlayEnv(Map.of("PROJECT_ENV_TOOL_INDEX_V2", url))) { + var manager = new DefaultToolsIndexManager(tempDir, new DefaultHttpClientProvider()); + assertThatThrownBy(() -> manager.resolveJdkDistributionUrl("unknown-distro", "21.0.1+12")) + .isInstanceOf(ToolsIndexException.class) + .hasMessageContaining("Failed to resolve unknown-distro 21.0.1+12 from tool index"); + } + } + + @Test + void resolveNodeJsDistributionUrlReturnsUrlForAvailableOsAndArch() throws Exception { + var url = getClass().getResource("arch-test-index.json").toString(); + try (var ignored = TestEnvironmentVariables.overlayEnv(Map.of("PROJECT_ENV_TOOL_INDEX_V2", url))) { + var manager = new DefaultToolsIndexManager(tempDir, new DefaultHttpClientProvider()); + // This test will pass on any platform that has a matching entry in the test index. + // On macOS amd64/aarch64 and linux amd64, the resolution should succeed. + var result = manager.resolveNodeJsDistributionUrl("20.0.0"); + assertThat(result).isNotEmpty(); + } + } + + @Test + void resolveJdkDistributionUrlReturnsUrlForAvailableOsAndArch() throws Exception { + var url = getClass().getResource("arch-test-index.json").toString(); + try (var ignored = TestEnvironmentVariables.overlayEnv(Map.of("PROJECT_ENV_TOOL_INDEX_V2", url))) { + var manager = new DefaultToolsIndexManager(tempDir, new DefaultHttpClientProvider()); + var result = manager.resolveJdkDistributionUrl("temurin", "21.0.1+12"); + assertThat(result).isNotEmpty(); + } + } + +} diff --git a/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json b/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json new file mode 100644 index 0000000..6db763a --- /dev/null +++ b/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json @@ -0,0 +1,38 @@ +{ + "jdkVersions": { + "temurin": { + "21.0.1+12": { + "linux": { + "amd64": "https://example.com/temurin-21-linux-amd64.tar.gz" + }, + "macos": { + "amd64": "https://example.com/temurin-21-macos-amd64.tar.gz", + "aarch64": "https://example.com/temurin-21-macos-aarch64.tar.gz" + }, + "windows": { + "amd64": "https://example.com/temurin-21-windows-amd64.zip" + } + } + } + }, + "jdkDistributionSynonyms": { + "temurin": [] + }, + "mavenVersions": {}, + "gradleVersions": {}, + "nodeVersions": { + "20.0.0": { + "linux": { + "amd64": "https://example.com/node-20-linux-amd64.tar.gz" + }, + "macos": { + "amd64": "https://example.com/node-20-macos-amd64.tar.gz", + "aarch64": "https://example.com/node-20-macos-aarch64.tar.gz" + }, + "windows": { + "amd64": "https://example.com/node-20-windows-amd64.zip" + } + } + }, + "clojureVersions": {} +} diff --git a/code/tool-support/generic-tool-support/pom.xml b/code/tool-support/generic-tool-support/pom.xml index 4c28b34..c1c5cd5 100644 --- a/code/tool-support/generic-tool-support/pom.xml +++ b/code/tool-support/generic-tool-support/pom.xml @@ -35,6 +35,17 @@ gson provided + + + org.assertj + assertj-core + test + + + org.junit.jupiter + junit-jupiter-engine + test + \ No newline at end of file diff --git a/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolConfiguration.java b/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolConfiguration.java index a9a73d7..f8876d2 100644 --- a/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolConfiguration.java +++ b/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolConfiguration.java @@ -1,5 +1,6 @@ package io.projectenv.core.toolsupport.nodejs; +import io.projectenv.core.commons.system.CpuArchitecture; import io.projectenv.core.commons.system.OperatingSystem; import org.immutables.gson.Gson; import org.immutables.value.Value; @@ -34,6 +35,8 @@ interface DownloadUrlConfiguration { OperatingSystem getTargetOs(); + Optional getTargetArch(); + } } diff --git a/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupport.java b/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupport.java index 79f8ad5..0f1037f 100644 --- a/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupport.java +++ b/code/tool-support/generic-tool-support/src/main/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupport.java @@ -1,5 +1,6 @@ package io.projectenv.core.toolsupport.nodejs; +import io.projectenv.core.commons.system.CpuArchitecture; import io.projectenv.core.commons.system.OperatingSystem; import io.projectenv.core.toolsupport.commons.commands.*; import io.projectenv.core.toolsupport.spi.*; @@ -75,9 +76,26 @@ private List createInstallationSteps(GenericToolConfi } private Optional getSystemSpecificDownloadUri(GenericToolConfiguration toolConfiguration) { + var currentOs = OperatingSystem.getCurrentOperatingSystem(); + var currentArch = CpuArchitecture.getCurrentCpuArchitecture(); + + // first, try to find a download URL matching both OS and architecture + var archSpecificMatch = toolConfiguration.getDownloadUrls() + .stream() + .filter(config -> config.getTargetOs() == currentOs) + .filter(config -> config.getTargetArch().isPresent() && config.getTargetArch().get() == currentArch) + .findFirst() + .map(GenericToolConfiguration.DownloadUrlConfiguration::getDownloadUrl); + + if (archSpecificMatch.isPresent()) { + return archSpecificMatch; + } + + // fall back to an OS-only match (no targetArch specified) for backward compatibility return toolConfiguration.getDownloadUrls() .stream() - .filter(downloadUrlConfiguration -> downloadUrlConfiguration.getTargetOs() == OperatingSystem.getCurrentOperatingSystem()) + .filter(config -> config.getTargetOs() == currentOs) + .filter(config -> config.getTargetArch().isEmpty()) .findFirst() .map(GenericToolConfiguration.DownloadUrlConfiguration::getDownloadUrl) .or(toolConfiguration::getDownloadUrl); diff --git a/code/tool-support/generic-tool-support/src/test/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupportTest.java b/code/tool-support/generic-tool-support/src/test/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupportTest.java new file mode 100644 index 0000000..b9b7083 --- /dev/null +++ b/code/tool-support/generic-tool-support/src/test/java/io/projectenv/core/toolsupport/nodejs/GenericToolSupportTest.java @@ -0,0 +1,113 @@ +package io.projectenv.core.toolsupport.nodejs; + +import io.projectenv.core.commons.system.CpuArchitecture; +import io.projectenv.core.commons.system.OperatingSystem; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GenericToolSupportTest { + + private final GenericToolSupport support = new GenericToolSupport(); + + @Test + void isAvailableWithArchSpecificMatch() { + var config = ImmutableGenericToolConfiguration.builder() + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-linux-amd64.tar.gz") + .targetOs(OperatingSystem.LINUX) + .targetArch(CpuArchitecture.AMD64) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-linux-aarch64.tar.gz") + .targetOs(OperatingSystem.LINUX) + .targetArch(CpuArchitecture.AARCH64) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-macos-amd64.tar.gz") + .targetOs(OperatingSystem.MACOS) + .targetArch(CpuArchitecture.AMD64) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-macos-aarch64.tar.gz") + .targetOs(OperatingSystem.MACOS) + .targetArch(CpuArchitecture.AARCH64) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-windows-amd64.zip") + .targetOs(OperatingSystem.WINDOWS) + .targetArch(CpuArchitecture.AMD64) + .build()) + .build(); + + assertThat(support.isAvailable(config)).isTrue(); + } + + @Test + void isAvailableWithOsOnlyMatch() { + var config = ImmutableGenericToolConfiguration.builder() + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-linux.tar.gz") + .targetOs(OperatingSystem.LINUX) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-macos.tar.gz") + .targetOs(OperatingSystem.MACOS) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-windows.zip") + .targetOs(OperatingSystem.WINDOWS) + .build()) + .build(); + + assertThat(support.isAvailable(config)).isTrue(); + } + + @Test + void isAvailableWithFallbackDownloadUrl() { + var config = ImmutableGenericToolConfiguration.builder() + .downloadUrl("https://example.com/tool-universal.tar.gz") + .build(); + + assertThat(support.isAvailable(config)).isTrue(); + } + + @Test + void isNotAvailableWhenNoMatchingOs() { + // Create config with only a non-matching OS + var nonCurrentOs = OperatingSystem.getCurrentOperatingSystem() == OperatingSystem.LINUX + ? OperatingSystem.WINDOWS + : OperatingSystem.LINUX; + + var config = ImmutableGenericToolConfiguration.builder() + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-other.tar.gz") + .targetOs(nonCurrentOs) + .build()) + .build(); + + assertThat(support.isAvailable(config)).isFalse(); + } + + @Test + void archSpecificMatchTakesPriorityOverOsOnlyMatch() { + var currentOs = OperatingSystem.getCurrentOperatingSystem(); + var currentArch = CpuArchitecture.getCurrentCpuArchitecture(); + + var config = ImmutableGenericToolConfiguration.builder() + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-os-only.tar.gz") + .targetOs(currentOs) + .build()) + .addDownloadUrls(ImmutableDownloadUrlConfiguration.builder() + .downloadUrl("https://example.com/tool-arch-specific.tar.gz") + .targetOs(currentOs) + .targetArch(currentArch) + .build()) + .build(); + + // Tool should be available + assertThat(support.isAvailable(config)).isTrue(); + } + +} From b400652d422e195cfb9281a939515431de3484f6 Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 23:29:32 +0100 Subject: [PATCH 8/9] Fix NodeJs test assertion for Linux ARM64 architecture --- .../core/cli/integration/assertions/NodeJsAssertions.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/cli/src/test/java/io/projectenv/core/cli/integration/assertions/NodeJsAssertions.java b/code/cli/src/test/java/io/projectenv/core/cli/integration/assertions/NodeJsAssertions.java index 5709eee..9fab6d7 100644 --- a/code/cli/src/test/java/io/projectenv/core/cli/integration/assertions/NodeJsAssertions.java +++ b/code/cli/src/test/java/io/projectenv/core/cli/integration/assertions/NodeJsAssertions.java @@ -1,5 +1,6 @@ package io.projectenv.core.cli.integration.assertions; +import io.projectenv.core.commons.system.CpuArchitecture; import io.projectenv.core.commons.system.OperatingSystem; import org.assertj.core.api.ListAssert; import org.assertj.core.api.MapAssert; @@ -48,7 +49,9 @@ private String getBinariesRoot() { return switch (OperatingSystem.getCurrentOperatingSystem()) { case MACOS -> "node-v14.15.3-darwin-x64"; case WINDOWS -> "node-v14.15.3-win-x64"; - case LINUX -> "node-v14.15.3-linux-x64"; + case LINUX -> CpuArchitecture.getCurrentCpuArchitecture() == CpuArchitecture.AARCH64 + ? "node-v14.15.3-linux-arm64" + : "node-v14.15.3-linux-x64"; }; } From 150fcaae0b6e98d13e63728600ac62c35275c206 Mon Sep 17 00:00:00 2001 From: repolevedavaj Date: Sat, 14 Feb 2026 23:33:30 +0100 Subject: [PATCH 9/9] Add Linux aarch64 entries to arch test index --- .../io/projectenv/core/cli/index/arch-test-index.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json b/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json index 6db763a..d5ac6fc 100644 --- a/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json +++ b/code/cli/src/test/resources/io/projectenv/core/cli/index/arch-test-index.json @@ -3,7 +3,8 @@ "temurin": { "21.0.1+12": { "linux": { - "amd64": "https://example.com/temurin-21-linux-amd64.tar.gz" + "amd64": "https://example.com/temurin-21-linux-amd64.tar.gz", + "aarch64": "https://example.com/temurin-21-linux-aarch64.tar.gz" }, "macos": { "amd64": "https://example.com/temurin-21-macos-amd64.tar.gz", @@ -23,7 +24,8 @@ "nodeVersions": { "20.0.0": { "linux": { - "amd64": "https://example.com/node-20-linux-amd64.tar.gz" + "amd64": "https://example.com/node-20-linux-amd64.tar.gz", + "aarch64": "https://example.com/node-20-linux-aarch64.tar.gz" }, "macos": { "amd64": "https://example.com/node-20-macos-amd64.tar.gz",