From 5d4e33b503f1cec7f05c74f786208c09817e1e8c Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 6 Apr 2025 16:20:53 +0200 Subject: [PATCH 01/63] Enhance Lefthook configuration --- lefthook.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index 92445f255..57d5c7246 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,5 +1,4 @@ post-checkout: - jobs: - - name: Update action references + scripts: + update-action-references.elv: runner: elvish - script: update-action-references.elv From 4dd9208ce87e1cf90b1eb76cbc635826a146d6e5 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 11:46:22 +0200 Subject: [PATCH 02/63] Enhance the "Update action references" Git hook --- .../update-action-references.elv | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/.lefthook/post-checkout/update-action-references.elv b/.lefthook/post-checkout/update-action-references.elv index 864d9081f..9e2c2cd5c 100755 --- a/.lefthook/post-checkout/update-action-references.elv +++ b/.lefthook/post-checkout/update-action-references.elv @@ -1,5 +1,14 @@ use str +var _ _ flag = (all $args) + +var is-branch-change = (== $flag 1) + +if (not $is-branch-change) { + echo 💡 Skipping action reference update whe not changing branch! + exit 0 +} + var branch-version = "" try { @@ -8,32 +17,32 @@ try { #Just do nothing } -if (eq $branch-version main) { - echo "💡Skipping action reference update when on main!" +echo 🌲 Branch version: "'"$branch-version"'" + +if (==s $branch-version main) { + echo 💡 Skipping action reference update when on main! exit 0 } -if (eq $branch-version "") { - echo "💡Skipping action reference update when not in a checkout/switch!" +if (==s $branch-version "") { + echo 💡 Skipping action reference update when not in a checkout/switch! exit 0 } -echo 🌲Branch version: "'"$branch-version"'" - var major-version = (str:split . $branch-version | take 1) -echo 💎Major version: "'"$major-version"'" +echo 💎 Major version: "'"$major-version"'" -put actions/**.{yml md} | each { |file-path| +put actions/**[nomatch-ok].{yml md} | peach { |file-path| sed -i -E 's/(giancosta86\/aurora-github\/actions\/[^@]+)@v[0-9]+\.[0-9]+\.[0-9]+/\1@'$branch-version'/' $file-path sed -i -E 's/(giancosta86\/aurora-github\/actions\/[^@]+)@v[0-9]+/\1@'$major-version'/' $file-path } -if (not ?(git diff --quiet)) { - echo 🧭Creating a commit to track the updated action references... - git add . - git commit -m "Update action references" - echo ✅Commit created! -} else { - echo ✅The action references are already set up! -} \ No newline at end of file +if ?(git diff --quiet) { + echo ✅ The action references are already set up! +} + +echo 🧭 Creating a commit to track the updated action references... +git add . +git commit -m "Update action references" +echo ✅ Commit created! \ No newline at end of file From 59da6dd620eb4282ef0e9abc42636746bd73f818 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 18:03:12 +0200 Subject: [PATCH 03/63] Add a pre-commit hook to check the Elvish syntax --- .lefthook/pre-commit/check-elvish-syntax.elv | 3 +++ lefthook.yml | 5 +++++ 2 files changed, 8 insertions(+) create mode 100755 .lefthook/pre-commit/check-elvish-syntax.elv diff --git a/.lefthook/pre-commit/check-elvish-syntax.elv b/.lefthook/pre-commit/check-elvish-syntax.elv new file mode 100755 index 000000000..668eca9ed --- /dev/null +++ b/.lefthook/pre-commit/check-elvish-syntax.elv @@ -0,0 +1,3 @@ +put core/**.elv | peach { + |script| elvish -compileonly $script +} \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml index 57d5c7246..4db295685 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,3 +1,8 @@ +pre-commit: + scripts: + check-elvish-syntax.elv: + runner: elvish + post-checkout: scripts: update-action-references.elv: From b0bbd87cda305459c0665aced2fbb94b51b1eedc Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 6 Apr 2025 16:21:10 +0200 Subject: [PATCH 04/63] Update action references --- actions/generate-wasm-target/action.yml | 2 +- actions/publish-github-pages/action.yml | 4 ++-- actions/publish-jvm-project/action.yml | 10 +++++----- actions/publish-npm-package/action.yml | 6 +++--- actions/publish-python-package/action.yml | 4 ++-- actions/publish-rust-crate/action.yml | 4 ++-- actions/publish-rust-wasm/action.yml | 8 ++++---- actions/run-custom-tests/action.yml | 4 ++-- actions/tag-and-release/action.yml | 2 +- actions/verify-jvm-project/action.yml | 12 ++++++------ actions/verify-npm-package/action.yml | 12 ++++++------ actions/verify-python-package/action.yml | 6 +++--- actions/verify-rust-crate/action.yml | 10 +++++----- actions/verify-rust-wasm/action.yml | 8 ++++---- 14 files changed, 46 insertions(+), 46 deletions(-) diff --git a/actions/generate-wasm-target/action.yml b/actions/generate-wasm-target/action.yml index 9dcf68e9c..2495e5e64 100644 --- a/actions/generate-wasm-target/action.yml +++ b/actions/generate-wasm-target/action.yml @@ -52,7 +52,7 @@ runs: - name: Parse npm scope id: parse-npm-scope if: inputs.npm-scope != '' - uses: giancosta86/aurora-github/actions/parse-npm-scope@v10.2.0 + uses: giancosta86/aurora-github/actions/parse-npm-scope@v10.3.0 with: scope: ${{ inputs.npm-scope }} diff --git a/actions/publish-github-pages/action.yml b/actions/publish-github-pages/action.yml index 4fb0b1ade..16f9f2f99 100644 --- a/actions/publish-github-pages/action.yml +++ b/actions/publish-github-pages/action.yml @@ -109,14 +109,14 @@ runs: - name: Enforce branch version if: env.strategy == 'nodejs' || env.strategy == 'maven' - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.source-directory }} - name: Setup NodeJS context if: env.strategy == 'nodejs' - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.2.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 with: project-directory: ${{ inputs.source-directory }} diff --git a/actions/publish-jvm-project/action.yml b/actions/publish-jvm-project/action.yml index bcc24a743..8efff426c 100644 --- a/actions/publish-jvm-project/action.yml +++ b/actions/publish-jvm-project/action.yml @@ -60,7 +60,7 @@ runs: fi - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -84,21 +84,21 @@ runs: - name: Install Java if: inputs.java-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: java version: ${{ inputs.java-version }} - name: Install Maven if: env.buildTool == 'mvn' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: maven version: ${{ inputs.tool-version }} - name: Install Gradle if: env.buildTool == 'gradle' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: gradle version: ${{ inputs.tool-version }} @@ -170,7 +170,7 @@ runs: echo "🐘🐘🐘" - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-npm-package/action.yml b/actions/publish-npm-package/action.yml index c36bfcad0..688413661 100644 --- a/actions/publish-npm-package/action.yml +++ b/actions/publish-npm-package/action.yml @@ -59,13 +59,13 @@ runs: fi - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup NodeJS context - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.2.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 with: project-directory: ${{ inputs.project-directory }} @@ -78,7 +78,7 @@ runs: tryToRunNpmBuildScript - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-python-package/action.yml b/actions/publish-python-package/action.yml index d610e3498..5290bc849 100644 --- a/actions/publish-python-package/action.yml +++ b/actions/publish-python-package/action.yml @@ -56,7 +56,7 @@ runs: fi - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -70,7 +70,7 @@ runs: ensurePdm - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-rust-crate/action.yml b/actions/publish-rust-crate/action.yml index 72931cca9..0f1c3dbcd 100644 --- a/actions/publish-rust-crate/action.yml +++ b/actions/publish-rust-crate/action.yml @@ -63,7 +63,7 @@ runs: fi - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -97,7 +97,7 @@ runs: prepareDescriptor - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-rust-wasm/action.yml b/actions/publish-rust-wasm/action.yml index 63bf8f713..4f835e5be 100644 --- a/actions/publish-rust-wasm/action.yml +++ b/actions/publish-rust-wasm/action.yml @@ -93,12 +93,12 @@ runs: fi - name: Install wasm-pack - uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.2.0 + uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 with: wasm-pack-version: ${{ inputs.wasm-pack-version }} - name: Generate the NodeJS package source files - uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.2.0 + uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.3.0 with: target: ${{ inputs.wasm-target }} npm-scope: ${{ inputs.npm-scope }} @@ -124,7 +124,7 @@ runs: fi - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} @@ -132,7 +132,7 @@ runs: enforce-branch-version: ${{ inputs.enforce-branch-version }} - name: Publish NodeJS package - uses: giancosta86/aurora-github/actions/publish-npm-package@v10.2.0 + uses: giancosta86/aurora-github/actions/publish-npm-package@v10.3.0 with: dry-run: ${{ inputs.dry-run }} npm-token: ${{ inputs.npm-token }} diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 0f7a6ea4c..98c1eaef0 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -103,7 +103,7 @@ runs: - name: Setup NodeJS context if: env.strategy == 'nodejs' - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.2.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 with: project-directory: ${{ inputs.root-directory }} @@ -129,7 +129,7 @@ runs: - name: Check Rust versions if: env.strategy == 'rust' && env.checkRustVersions == 'true' - uses: giancosta86/aurora-github/actions/check-rust-versions@v10.2.0 + uses: giancosta86/aurora-github/actions/check-rust-versions@v10.3.0 with: project-directory: ${{ inputs.root-directory }} diff --git a/actions/tag-and-release/action.yml b/actions/tag-and-release/action.yml index 426621e08..e5d8082cb 100644 --- a/actions/tag-and-release/action.yml +++ b/actions/tag-and-release/action.yml @@ -74,7 +74,7 @@ runs: - name: Detect branch and version id: detect-branch-version - uses: giancosta86/aurora-github/actions/detect-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/detect-branch-version@v10.3.0 - name: Verify versions shell: bash diff --git a/actions/verify-jvm-project/action.yml b/actions/verify-jvm-project/action.yml index 1d9c41470..ddd073ac2 100644 --- a/actions/verify-jvm-project/action.yml +++ b/actions/verify-jvm-project/action.yml @@ -60,7 +60,7 @@ runs: fi - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.2.0 + uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 - name: Setup Python context shell: bash @@ -80,28 +80,28 @@ runs: get_jvm_build_tool(inputs).write_to_github_env() - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Install Java if: inputs.java-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: java version: ${{ inputs.java-version }} - name: Install Maven if: env.buildTool == 'mvn' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: maven version: ${{ inputs.tool-version }} - name: Install Gradle if: env.buildTool == 'gradle' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.2.0 + uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 with: candidate: gradle version: ${{ inputs.tool-version }} @@ -143,7 +143,7 @@ runs: echo "✅Verification successful!" - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.2.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-npm-package/action.yml b/actions/verify-npm-package/action.yml index 5fb56afdd..0a0eb3e28 100644 --- a/actions/verify-npm-package/action.yml +++ b/actions/verify-npm-package/action.yml @@ -60,16 +60,16 @@ runs: fi - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.2.0 + uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup NodeJS context - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.2.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 with: project-directory: ${{ inputs.project-directory }} @@ -91,18 +91,18 @@ runs: - name: Check subpath exports if: inputs.check-subpath-exports == 'true' - uses: giancosta86/aurora-github/actions/check-subpath-exports@v10.2.0 + uses: giancosta86/aurora-github/actions/check-subpath-exports@v10.3.0 with: project-directory: ${{ inputs.project-directory }} - name: Run optional custom tests from the 'tests' directory - uses: giancosta86/aurora-github/actions/run-custom-tests@v10.2.0 + uses: giancosta86/aurora-github/actions/run-custom-tests@v10.3.0 with: optional: true root-directory: ${{ inputs.project-directory }}/tests - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.2.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-python-package/action.yml b/actions/verify-python-package/action.yml index 1180b624b..89d284cc6 100644 --- a/actions/verify-python-package/action.yml +++ b/actions/verify-python-package/action.yml @@ -47,10 +47,10 @@ runs: fi - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.2.0 + uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -80,7 +80,7 @@ runs: echo "✅Project built successfully!" - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.2.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-rust-crate/action.yml b/actions/verify-rust-crate/action.yml index ca59323ed..5f2e2a7ff 100644 --- a/actions/verify-rust-crate/action.yml +++ b/actions/verify-rust-crate/action.yml @@ -64,16 +64,16 @@ runs: fi - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.2.0 + uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.2.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Check Rust versions - uses: giancosta86/aurora-github/actions/check-rust-versions@v10.2.0 + uses: giancosta86/aurora-github/actions/check-rust-versions@v10.3.0 with: project-directory: ${{ inputs.project-directory }} @@ -105,7 +105,7 @@ runs: checkStyle - name: Extract code snippets as tests from README.md - uses: giancosta86/aurora-github/actions/extract-rust-snippets@v10.2.0 + uses: giancosta86/aurora-github/actions/extract-rust-snippets@v10.3.0 with: optional: true project-directory: ${{ inputs.project-directory }} @@ -146,7 +146,7 @@ runs: runTests - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.2.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-rust-wasm/action.yml b/actions/verify-rust-wasm/action.yml index b335e3cb0..b85c3aff9 100644 --- a/actions/verify-rust-wasm/action.yml +++ b/actions/verify-rust-wasm/action.yml @@ -102,12 +102,12 @@ runs: fi - name: Install wasm-pack - uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.2.0 + uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 with: wasm-pack-version: ${{ inputs.wasm-pack-version }} - name: Verify Rust source files - uses: giancosta86/aurora-github/actions/verify-rust-crate@v10.2.0 + uses: giancosta86/aurora-github/actions/verify-rust-crate@v10.3.0 with: run-clippy-checks: ${{ inputs.run-clippy-checks }} check-rustdoc: ${{ inputs.check-rustdoc }} @@ -125,7 +125,7 @@ runs: echo "✅Headless browser tests OK!" - name: Generate the NodeJS package source files - uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.2.0 + uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.3.0 with: target: ${{ inputs.wasm-target }} npm-scope: ${{ inputs.npm-scope }} @@ -134,7 +134,7 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Run client tests - uses: giancosta86/aurora-github/actions/run-custom-tests@v10.2.0 + uses: giancosta86/aurora-github/actions/run-custom-tests@v10.3.0 with: optional: true root-directory: ${{ inputs.project-directory }}/${{ inputs.client-tests-directory }} From a6eb0df810df7c5ec3d90604b2649eeefdecc8d6 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 21 May 2025 03:28:42 +0200 Subject: [PATCH 05/63] Refactor and enhance `setup-elvish-context`: * remove the `packages` input - use `setup-elvish-package` instead * optimize library loading * introduce the `quiet` input * only accept comma-separated values for the `packages` input setup-elvish-context --- .../test-setup-elvish-context/action.yml | 45 ++---- actions/setup-elvish-context/README.md | 44 +++-- actions/setup-elvish-context/action.yml | 151 ++++++------------ actions/setup-elvish-context/bootstrap.sh | 71 ++++++++ 4 files changed, 147 insertions(+), 164 deletions(-) create mode 100644 actions/setup-elvish-context/bootstrap.sh diff --git a/.github/test-actions/test-setup-elvish-context/action.yml b/.github/test-actions/test-setup-elvish-context/action.yml index 8ff79c4f3..ecf392b65 100644 --- a/.github/test-actions/test-setup-elvish-context/action.yml +++ b/.github/test-actions/test-setup-elvish-context/action.yml @@ -1,52 +1,25 @@ name: Test setup-elvish-context +inputs: + skip-if-existing: + description: Forwarded to the action. + runs: using: composite steps: - shell: bash - run: echo "🎭Setup just the shell for the first time..." - - - uses: ./actions/setup-elvish-context - with: - skip-if-existing: false - - - shell: bash - run: echo "🎭Setup just the shell a second time..." - - - uses: ./actions/setup-elvish-context - with: - skip-if-existing: false - - - shell: bash - run: echo "🎭Setup the shell with startup packages..." + run: echo "🎭 Setup the Elvish shell..." - uses: ./actions/setup-elvish-context with: - packages: github.com/elves/sample-pkg, github.com/xiaq/edit.elv - skip-if-existing: false + skip-if-existing: ${{ inputs.skip-if-existing }} + quiet: false - shell: bash - run: echo "🎭Setup the shell with packages a second time..." - - - uses: ./actions/setup-elvish-context - with: - packages: github.com/elves/sample-pkg, github.com/xiaq/edit.elv - skip-if-existing: false - - - shell: bash - run: echo "🎭Try enabling the skip-if-existing flag..." - - - uses: ./actions/setup-elvish-context - with: - packages: github.com/elves/sample-pkg, github.com/xiaq/edit.elv - skip-if-existing: true - - - shell: bash - run: echo "🎭Run Elvish code to confirm..." + run: echo "🎭 Run Elvish code to confirm..." - shell: elvish {0} run: | - var message = '✅Elvish confirms: the shell is ready! 🚀' + var message = '✅ Elvish confirms: the shell is ready! 🚀' echo $message - diff --git a/actions/setup-elvish-context/README.md b/actions/setup-elvish-context/README.md index e77a60da3..e6dfdbdda 100644 --- a/actions/setup-elvish-context/README.md +++ b/actions/setup-elvish-context/README.md @@ -1,8 +1,8 @@ # setup-elvish-context -Installs an **Elvish** version and optional startup packages, caching everything between multiple jobs of the same workflow execution. +Installs the **Elvish** shell, caching it between multiple jobs of the same workflow execution. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,7 +11,7 @@ steps: - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10 ``` -## 💡How it works +## 💡 How it works 1. If the `elvish` command is already available in the system and the `skip-if-existing` input is set to **true** (the default), the action will do just nothing @@ -19,39 +19,31 @@ steps: - Otherwise, install the **Elvish** shell, making the `elvish` command available along the system **PATH** -1. If at least one package was specified in the `packages` input (which **MUST** be either empty or a comma/space-separated list): +## 💬 Remarks - - If the exact set of packages was already requested by a previous execution of this action during the current workflow, use the cached version +- The cache spans over the lifetime of a specific **workflow execution** - so every new workflow run will not see the previously cached entries. - - Otherwise, install them via `epm:install`, then cache the entire `$epm:managed-dir` directory +- The following packages will also be available: -### Notes + - `aurora-github` - contained in the [core](../../core/) directory - however, such library is to be considered **unstable** even between patch versions, so it should be used in custom script steps _only_ when your workflow references this action from a _specific release_ of aurora-github. -1. The cache spans over the lifetime of a specific **workflow execution** - so every new workflow run will not see the previously cached entries. + - [aurora-elvish](https://github.com/giancosta86/aurora-elvish) - at branch **v1** -1. The `aurora-github` package - contained in the [lib](../../lib/) directory - will also be available to any Elvish shell retrieved via this action; however, such library is to be considered **unstable** even between patch versions, so it should be used in custom script steps _only_ when your workflow references this action from a _specific release_ of aurora-github. +- You need this action only to run your custom Elvish scripts - because it is automatically called by almost every action in aurora-github. -## ☑️Requirements +## ☑️ Requirements -The requested Elvish version **must** include an `epm` module having a `$managed-dir` variable. +The requested Elvish version **must** include an `epm` module having a `$managed-dir` variable - for example, Elvish v0.21. -Also, if the `packages` input is non-empty, `epm` must provide the following functions: +## 📥 Inputs -- `install` +| Name | Type | Description | Default value | +| :----------------: | :---------: | :---------------------------------------------------: | :-----------: | +| `version` | **string** | The Elvish version to download and cache | **0.21.0** | +| `skip-if-existing` | **boolean** | If the `elvish` command is available, just do nothing | **true** | +| `quiet` | **boolean** | Only print warnings and errors | **true** | -- `installed` - -All the above features must follow the protocol described in the documentation for Elvish v0.21. - -## 📥Inputs - -| Name | Type | Description | Default value | -| :----------------: | :-----------------------------------------: | :---------------------------------------------------: | :-----------: | -| `version` | **string** | The Elvish version to download and cache | **0.21.0** | -| `packages` | **string** - empty or space/comma-separated | Packages to install and cache with Elvish | | -| `skip-if-existing` | **boolean** | If the `elvish` command is available, just do nothing | **true** | - -## 🌐Further references +## 🌐 Further references - [Elvish](https://elv.sh/) diff --git a/actions/setup-elvish-context/action.yml b/actions/setup-elvish-context/action.yml index 3ba00b6fa..21b987d0d 100644 --- a/actions/setup-elvish-context/action.yml +++ b/actions/setup-elvish-context/action.yml @@ -1,162 +1,109 @@ name: Setup an Elvish context -description: Installs an Elvish version and optional startup packages, caching everything between multiple jobs of the same workflow execution. +description: Installs the Elvish shell, caching it between multiple jobs of the same workflow execution. inputs: version: description: The requested Elvish version. default: 0.21.0 - packages: - description: Startup packages - empty or space/comma-separated - to install via epm. - default: "" - skip-if-existing: - description: If the 'elvish' command is available, just do nothing. + description: If the `elvish` command is available, just do nothing. + default: true + + quiet: + description: Only print warnings and errors. default: true runs: using: composite steps: - - name: Validate inputs + - name: Initialize the action shell: bash run: | - if [[ -z "${{ inputs.version }}" ]] - then - echo "❌Missing action input: 'version'!" >&2 - exit 1 - fi + source "${{ github.action_path }}/bootstrap.sh" - if [[ -z "${{ inputs.skip-if-existing }}" ]] - then - echo "❌Missing action input: 'skip-if-existing'!" >&2 - exit 1 - fi + requireInput version '${{ inputs.version }}' + requireInput skip-if-existing '${{ inputs.skip-if-existing }}' + requireInput quiet '${{ inputs.quiet }}' - - name: Detect if the action should be skipped - shell: bash - run: | - skip=false + tracingEnabled="$(not '${{ inputs.quiet }}')" + setTracing $tracingEnabled - if type elvish + if shouldInstallCommand elvish '${{ inputs.skip-if-existing }}' then - echo "💬Elvish is already installed on the system!" - - if [[ "${{ inputs.skip-if-existing }}" == "true" ]] - then - echo "🌟Skipping the setup, as requested!" - skip=true - else - echo "📥Still proceeding with the setup, as requested!" - fi + elvishCacheKey="${{ github.workflow }}-${{ github.run_number }}-elvish-shell-v${{ inputs.version }}" + trace "🔎Elvish cache key: '$elvishCacheKey'" + + writeEnv elvish-cache-key "$elvishCacheKey" + writeEnv elvish-command-path /usr/local/bin/elvish + writeEnv skip-binary false else - echo "💭Elvish shell not found on the system..." + writeEnv skip-binary true fi - echo "skip=$skip" >> $GITHUB_ENV - - - name: Setup environment variables - if: env.skip != 'true' - shell: bash - run: | - elvishCacheKey="${{ github.workflow }}-${{ github.run_number }}-elvish-shell-v${{ inputs.version }}" - echo "🔎Elvish cache key: '$elvishCacheKey'" - - echo "elvish-cache-key=$elvishCacheKey" >> $GITHUB_ENV - - name: Restore cached Elvish binary id: restore-cached-elvish - if: env.skip != 'true' + if: env.skip-binary != 'true' uses: actions/cache/restore@v4 with: - path: /usr/local/bin/elvish key: ${{ env.elvish-cache-key }} + path: ${{ env.elvish-command-path }} - - name: Confirm cache hit for Elvish - if: env.skip != 'true' && steps.restore-cached-elvish.outputs.cache-hit == 'true' + - name: Confirm cache hit for Elvish binary + if: env.skip-binary != 'true' && inputs.quiet != 'true' && steps.restore-cached-elvish.outputs.cache-hit == 'true' shell: elvish {0} - run: echo 🚀Elvish ${{ inputs.version }} loaded from cache! + run: echo 🚀 Elvish ${{ inputs.version }} loaded from cache! > &2 - name: Install Elvish - if: env.skip != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' + if: env.skip-binary != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' uses: elves/setup-elvish@v1 with: elvish-version: ${{ inputs.version }} - name: Confirm installation for Elvish - if: env.skip != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' + if: env.skip-binary != 'true' && inputs.quiet != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' shell: elvish {0} - run: echo 🚀Elvish ${{ inputs.version }} installed! + run: echo 🚀 Elvish ${{ inputs.version }} installed! > &2 - name: Cache Elvish binary - if: env.skip != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' + if: env.skip-binary != 'true' && steps.restore-cached-elvish.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: - path: /usr/local/bin/elvish + path: ${{ env.elvish-command-path }} key: ${{ env.elvish-cache-key }} - - name: Copy the aurora-github package for Elvish - if: env.skip != 'true' + - name: Link the aurora-github core as an Elvish package shell: elvish {0} run: | use epm use os + use path - echo "📥Now installing the 🔮aurora-github package for Elvish..." os:mkdir-all $epm:managed-dir - var aurora-github-package-directory = ${{ github.action_path }}/../../lib + var aurora-github-core-directory = (path:join '${{ github.action_path }}' .. .. core) - cp -r $aurora-github-package-directory $epm:managed-dir/aurora-github + var link-path = (path:join $epm:managed-dir aurora-github) - echo "✅aurora-github package installed!" + if (not (os:exists $link-path)) { + os:symlink $aurora-github-core-directory $link-path + } - - name: Compute package-related environment variables - if: env.skip != 'true' && inputs.packages != '' + - name: Print core confirmation message + if: inputs.quiet != 'true' shell: elvish {0} run: | - use aurora-github/startup-libs - - startup-libs:setup-env-vars [ - &workflow=${{ github.workflow }} - &run-number=${{ github.run_number }} - &packages='${{ inputs.packages }}' - ] + echo 🔮 Now booting aurora-github for Elvish! - - name: Restore cached startup packages - if: env.skip != 'true' && inputs.packages != '' - id: restore-cached-packages - uses: actions/cache/restore@v4 + - name: Setup aurora-elvish + uses: giancosta86/aurora-github/actions/setup-elvish-package@v11.0.0 with: - path: ${{ env.epm-dir }} - key: ${{ env.epm-cache-key }} + package: github.com/giancosta86/aurora-elvish + git-ref: v1 + quiet: ${{ inputs.quiet }} - - name: Confirm cache hit for Elvish - if: env.skip != 'true' && inputs.packages != '' && steps.restore-cached-packages.outputs.cache-hit == 'true' - shell: elvish {0} - run: echo 🚀Startup packages for Elvish loaded from cache! - - - name: Install startup packages - if: env.skip != 'true' && inputs.packages != '' && steps.restore-cached-packages.outputs.cache-hit != 'true' + - name: Print final confirmation message + if: inputs.quiet != 'true' shell: elvish {0} run: | - use aurora-github/startup-libs - startup-libs:install $E:comma-separated-packages - - - name: Cache Elvish packages - if: env.skip != 'true' && inputs.packages != '' && steps.restore-cached-packages.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 - with: - path: ${{ env.epm-dir }} - key: ${{ env.epm-cache-key }} - - - name: Print startup packages - if: env.skip != 'true' && inputs.packages != '' - shell: elvish {0} - run: | - use aurora-github/startup-libs - startup-libs:list - - - name: Print confirmation message - if: env.skip != 'true' - shell: elvish {0} - run: echo ✅Elvish context ready! + echo ✅ Elvish context ready! diff --git a/actions/setup-elvish-context/bootstrap.sh b/actions/setup-elvish-context/bootstrap.sh new file mode 100644 index 000000000..43fcca45e --- /dev/null +++ b/actions/setup-elvish-context/bootstrap.sh @@ -0,0 +1,71 @@ +set -e +set -u +set -o pipefail + +tracingVarName='AURORA_GITHUB_TRACING_ENABLED' + +requireInput() { + local name="$1" + local value="$2" + + if [[ -z "$value" ]] + then + echo "❌ Missing action input: '$name'!" >&2 + exit 1 + fi +} + +writeEnv() { + local name="$1" + local value="$2" + + echo "$name=$value" >> $GITHUB_ENV +} + +not() { + local value="$1" + + if [[ "$value" == 'true' ]] + then + echo false + else + echo true + fi +} + +setTracing() { + local enabled="$1" + + export "$tracingVarName=$enabled" + + writeEnv $tracingVarName "$enabled" +} + +trace() { + if [[ "${!tracingVarName:-}" == "true" ]] + then + echo "$@" + fi +} + +shouldInstallCommand() { + local command="$1" + local skipIfExisting="$2" + + if type "$command" > /dev/null 2>&1 + then + trace "🌟The '$command' command is already installed on the system!" + + if [[ "$skipIfExisting" == "true" ]] + then + trace "💫Skipping the setup, as requested!" + return 1 + else + trace "📥Still proceeding with the setup, as requested!" + return 0 + fi + fi + + trace "💭The '$command' command is not available - now installing it..." + return 0 +} From 29a0333036265938f25e275629562beffd496e2f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Mon, 28 Apr 2025 02:42:01 +0200 Subject: [PATCH 06/63] Introduce an Elvish core --- core/action-references.elv | 24 ++++ core/bash/npmPackage.sh | 9 -- core/bash/pdm.sh | 30 ----- core/bash/pipx.sh | 25 ---- core/branch-version/detection.elv | 39 ++++++ core/branch-version/enforcement.elv | 72 ++++++++++ core/ci-cd/action-references.elv | 9 ++ core/ci-cd/boot.elv | 111 ++++++++++++++++ core/ci-cd/env.elv | 9 ++ core/ci-cd/git-refs.elv | 9 ++ core/ci-cd/input.elv | 85 ++++++++++++ core/ci-cd/io.elv | 32 +++++ core/ci-cd/output.elv | 9 ++ core/ci-cd/pull-request.elv | 33 +++++ core/ci-cd/release-assets.elv | 19 +++ core/ci-cd/repository.elv | 7 + core/ci-cd/required-jobs.elv | 42 ++++++ core/critical-todos.elv | 47 +++++++ core/custom-tests.elv | 106 +++++++++++++++ core/git.elv | 53 ++++++++ core/jvm/context.elv | 36 +++++ core/jvm/gradle.elv | 24 ++++ core/jvm/gradle/descriptor.elv | 17 +++ core/jvm/gradle/settings.elv | 23 ++++ core/jvm/maven.elv | 30 +++++ core/jvm/maven/settings.elv | 57 ++++++++ .../jvm/maven}/settings.xml | 0 core/jvm/project.elv | 18 +++ core/jvm/project/publication.elv | 20 +++ core/jvm/project/verification.elv | 17 +++ core/jvm/sdkman.elv | 40 ++++++ core/jvm/settings.elv | 17 +++ core/license-file.elv | 24 ++++ core/nodejs/context.elv | 123 ++++++++++++++++++ core/nodejs/pnpm.elv | 47 +++++++ core/nodejs/project.elv | 16 +++ core/nodejs/subpath-exports.elv | 6 + core/nodejs/subpath-exports/check.elv | 62 +++++++++ core/nodejs/subpath-exports/inject.elv | 92 +++++++++++++ core/project.elv | 5 + core/project/base.elv | 38 ++++++ core/project/descriptors/gradle.elv | 14 ++ core/project/descriptors/json.elv | 9 ++ core/project/descriptors/plain-text.elv | 7 + core/project/descriptors/toml.elv | 18 +++ core/project/descriptors/xml-version.py | 19 +++ core/project/descriptors/xml.elv | 15 +++ core/project/gradle.elv | 16 +++ core/project/loader.elv | 79 +++++++++++ core/project/maven.elv | 16 +++ core/project/nodejs.elv | 16 +++ core/project/python.elv | 16 +++ core/project/rust.elv | 16 +++ core/project/unknown.elv | 16 +++ core/python/context.elv | 19 +++ core/python/core/__init__.py | 0 core/python/core/check_action_references.py | 44 ------- core/python/core/detect_branch_version.py | 44 ------- core/python/core/directories.py | 14 -- core/python/core/enforce_branch_version.py | 91 ------------- core/python/core/exceptions.py | 3 - core/python/core/extract_rust_snippets.py | 82 ------------ core/python/core/inputs.py | 45 ------- core/python/core/jvm.py | 28 ---- core/python/core/outputs.py | 50 ------- core/python/core/parse_npm_scope.py | 25 ---- core/python/core/processes.py | 22 ---- core/python/core/projects/__init__.py | 3 - core/python/core/projects/base.py | 62 --------- core/python/core/projects/detect.py | 69 ---------- core/python/core/projects/gradle.py | 39 ------ core/python/core/projects/maven.py | 34 ----- core/python/core/projects/nodejs.py | 34 ----- core/python/core/projects/python.py | 22 ---- core/python/core/projects/rust.py | 22 ---- core/python/core/projects/toml.py | 18 --- core/python/core/projects/unknown.py | 31 ----- core/python/pdm.elv | 27 ++++ core/python/pipx.elv | 19 +++ core/python/project.elv | 23 ++++ core/rust/context.elv | 72 ++++++++++ core/rust/crate.elv | 7 + core/rust/project.elv | 76 +++++++++++ core/rust/snippets.elv | 82 ++++++++++++ core/rust/wasm-pack.elv | 98 ++++++++++++++ core/rust/wasm.elv | 12 ++ core/system-packages.elv | 66 ++++++++++ core/tag-and-release.elv | 87 +++++++++++++ core/tag-and-release/git-log.elv | 22 ++++ core/tag-and-release/major-tag.elv | 18 +++ core/tag-and-release/preconditions.elv | 19 +++ core/tag-and-release/release-notes.elv | 80 ++++++++++++ core/tag-and-release/release.elv | 74 +++++++++++ core/website.elv | 84 ++++++++++++ lib/core.elv | 29 ----- lib/startup-libs.elv | 36 ----- 96 files changed, 2560 insertions(+), 911 deletions(-) create mode 100644 core/action-references.elv delete mode 100644 core/bash/npmPackage.sh delete mode 100644 core/bash/pdm.sh delete mode 100644 core/bash/pipx.sh create mode 100644 core/branch-version/detection.elv create mode 100644 core/branch-version/enforcement.elv create mode 100644 core/ci-cd/action-references.elv create mode 100644 core/ci-cd/boot.elv create mode 100644 core/ci-cd/env.elv create mode 100644 core/ci-cd/git-refs.elv create mode 100644 core/ci-cd/input.elv create mode 100644 core/ci-cd/io.elv create mode 100644 core/ci-cd/output.elv create mode 100644 core/ci-cd/pull-request.elv create mode 100644 core/ci-cd/release-assets.elv create mode 100644 core/ci-cd/repository.elv create mode 100644 core/ci-cd/required-jobs.elv create mode 100644 core/critical-todos.elv create mode 100644 core/custom-tests.elv create mode 100644 core/git.elv create mode 100644 core/jvm/context.elv create mode 100644 core/jvm/gradle.elv create mode 100644 core/jvm/gradle/descriptor.elv create mode 100644 core/jvm/gradle/settings.elv create mode 100644 core/jvm/maven.elv create mode 100644 core/jvm/maven/settings.elv rename {actions/publish-jvm-project => core/jvm/maven}/settings.xml (100%) create mode 100644 core/jvm/project.elv create mode 100644 core/jvm/project/publication.elv create mode 100644 core/jvm/project/verification.elv create mode 100644 core/jvm/sdkman.elv create mode 100644 core/jvm/settings.elv create mode 100644 core/license-file.elv create mode 100644 core/nodejs/context.elv create mode 100644 core/nodejs/pnpm.elv create mode 100644 core/nodejs/project.elv create mode 100644 core/nodejs/subpath-exports.elv create mode 100644 core/nodejs/subpath-exports/check.elv create mode 100644 core/nodejs/subpath-exports/inject.elv create mode 100644 core/project.elv create mode 100644 core/project/base.elv create mode 100644 core/project/descriptors/gradle.elv create mode 100644 core/project/descriptors/json.elv create mode 100644 core/project/descriptors/plain-text.elv create mode 100644 core/project/descriptors/toml.elv create mode 100644 core/project/descriptors/xml-version.py create mode 100644 core/project/descriptors/xml.elv create mode 100644 core/project/gradle.elv create mode 100644 core/project/loader.elv create mode 100644 core/project/maven.elv create mode 100644 core/project/nodejs.elv create mode 100644 core/project/python.elv create mode 100644 core/project/rust.elv create mode 100644 core/project/unknown.elv create mode 100644 core/python/context.elv delete mode 100644 core/python/core/__init__.py delete mode 100644 core/python/core/check_action_references.py delete mode 100644 core/python/core/detect_branch_version.py delete mode 100644 core/python/core/directories.py delete mode 100644 core/python/core/enforce_branch_version.py delete mode 100644 core/python/core/exceptions.py delete mode 100644 core/python/core/extract_rust_snippets.py delete mode 100644 core/python/core/inputs.py delete mode 100644 core/python/core/jvm.py delete mode 100644 core/python/core/outputs.py delete mode 100644 core/python/core/parse_npm_scope.py delete mode 100644 core/python/core/processes.py delete mode 100644 core/python/core/projects/__init__.py delete mode 100644 core/python/core/projects/base.py delete mode 100644 core/python/core/projects/detect.py delete mode 100644 core/python/core/projects/gradle.py delete mode 100644 core/python/core/projects/maven.py delete mode 100644 core/python/core/projects/nodejs.py delete mode 100644 core/python/core/projects/python.py delete mode 100644 core/python/core/projects/rust.py delete mode 100644 core/python/core/projects/toml.py delete mode 100644 core/python/core/projects/unknown.py create mode 100644 core/python/pdm.elv create mode 100644 core/python/pipx.elv create mode 100644 core/python/project.elv create mode 100644 core/rust/context.elv create mode 100644 core/rust/crate.elv create mode 100644 core/rust/project.elv create mode 100644 core/rust/snippets.elv create mode 100644 core/rust/wasm-pack.elv create mode 100644 core/rust/wasm.elv create mode 100644 core/system-packages.elv create mode 100644 core/tag-and-release.elv create mode 100644 core/tag-and-release/git-log.elv create mode 100644 core/tag-and-release/major-tag.elv create mode 100644 core/tag-and-release/preconditions.elv create mode 100644 core/tag-and-release/release-notes.elv create mode 100644 core/tag-and-release/release.elv create mode 100644 core/website.elv delete mode 100644 lib/core.elv delete mode 100644 lib/startup-libs.elv diff --git a/core/action-references.elv b/core/action-references.elv new file mode 100644 index 000000000..fe8b8cab5 --- /dev/null +++ b/core/action-references.elv @@ -0,0 +1,24 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/map +use ./branch-version/detection +use ./ci-cd/action-references + +fn check { + var branch = (detection:detect)[branch] + + var regex = (action-references:get-regex-for-references-to-other-branches $branch) + + var grep-result = ?( + grep --color=always --perl-regexp $regex **/*.yml > &2 + ) + + if $grep-result { + fail "There are references to actions within '"$pwd"' residing in other branches!" + } else { + if (==s (map:drill-down &default='' $grep-result reason exit-status) 1) { + console:echo ✅ No cross-branch action references detected! + } else { + fail $grep-result + } + } +} \ No newline at end of file diff --git a/core/bash/npmPackage.sh b/core/bash/npmPackage.sh deleted file mode 100644 index 4b2d4a310..000000000 --- a/core/bash/npmPackage.sh +++ /dev/null @@ -1,9 +0,0 @@ -tryToRunNpmBuildScript() { - if jq -e '.scripts.build? != null' package.json > /dev/null - then - echo "✅'build' script found in package.json - now running it!" - pnpm build - else - echo "💭No 'build' script found in package.json - skipping the build step." - fi -} \ No newline at end of file diff --git a/core/bash/pdm.sh b/core/bash/pdm.sh deleted file mode 100644 index 347770fb5..000000000 --- a/core/bash/pdm.sh +++ /dev/null @@ -1,30 +0,0 @@ -ensurePdm() { - local requestedVersion="$1" - - installPdm() { - source "$GITHUB_ACTION_PATH/../../core/bash/pipx.sh" - - installViaPipx pdm "$requestedVersion" - } - - if which pdm - then - echo "🌟pdm is already installed!" - - if [[ -n "$requestedVersion" ]] - then - local installedVersion="$(pdm --version)" - - echo "🔎Installed pdm version: '$installedVersion'" - - if echo "$installedVersion" | grep -Pq "\b$requestedVersion\b" - then - echo "✅The requested pdm version is already installed!" - else - installPdm - fi - fi - else - installPdm - fi -} \ No newline at end of file diff --git a/core/bash/pipx.sh b/core/bash/pipx.sh deleted file mode 100644 index aa449f45c..000000000 --- a/core/bash/pipx.sh +++ /dev/null @@ -1,25 +0,0 @@ -installViaPipx() { - local package="$1" - local requestedVersion="$2" - - echo "📥Installing $package via pipx..." - - if [[ -n "$requestedVersion" ]] - then - echo "🔎Requested version: $requestedVersion" - else - echo "🔎Latest version requested" - fi - - if [[ -n "$requestedVersion" ]] - then - versionArg="==$requestedVersion" - else - versionArg="" - fi - - pipx install ${package}${versionArg} - - echo "✅$package installed!" -} - diff --git a/core/branch-version/detection.elv b/core/branch-version/detection.elv new file mode 100644 index 000000000..968bd420c --- /dev/null +++ b/core/branch-version/detection.elv @@ -0,0 +1,39 @@ +use path +use re +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/semver +use github.com/giancosta86/aurora-elvish/seq +use ../ci-cd/git-refs + +fn detect { + var actual-ref = (git-refs:get-actual) + + if (seq:is-empty $actual-ref) { + fail 'Cannot detect the actual Git ref!' + } + + console:inspect &emoji=🌳 'Actual Git ref' $actual-ref + + var branch = (path:base $actual-ref) + console:inspect &emoji=🌲 'Current Git branch' $branch + + var semantic-version = (semver:parse $branch) + + var version = (semver:to-string $semantic-version) + console:inspect &emoji=🦋 'Detected version' $version + + var escaped-version = (re:quote $version) + console:inspect &emoji=🧵 'Escaped version' $escaped-version + + var major = $semantic-version[major] + console:inspect &emoji=🪩 'Major version' $major + + put [ + &full-branch=$actual-ref + &branch=$branch + &version=$version + &escaped-version=$escaped-version + &major=$major + &semantic-version=$semantic-version + ] +} diff --git a/core/branch-version/enforcement.elv b/core/branch-version/enforcement.elv new file mode 100644 index 000000000..2ef782be6 --- /dev/null +++ b/core/branch-version/enforcement.elv @@ -0,0 +1,72 @@ +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/edit +use github.com/giancosta86/aurora-elvish/map +use ../project +use ./detection + +fn -inject { |project| + console:echo 🧬 Injecting branch version into project: ($project[to-string]) + + var branch-version = (detection:detect)[version] + + edit:file $project[descriptor-path] { |text| + str:replace '0.0.0' $branch-version $text + } + + console:echo ✅ Version injected! + + $project[print-descriptor] +} + +fn -check { |project| + console:echo 🔎 Checking branch version for project: ($project[to-string]) + + $project[print-descriptor] + + var branch-version = (detection:detect)[version] + console:inspect &emoji=🌲 'Branch version' $branch-version + + var project-version = ($project[read-version]) + + if $project-version { + console:inspect &emoji=🏷 'Project version' $project-version + + if (==s $project-version $branch-version) { + console:echo ✅ The project version matches the branch version! + } else { + fail 'The project version and the branch version do not match!' + } + } else { + console:echo 💭 The project version cannot be detected... + console:echo 🔎 Ensuring the branch version is mentioned in the descriptor... + + var descriptor-content = (slurp < $project[descriptor-path]) + + if (str:contains $descriptor-content $branch-version) { + console:echo ✅ Branch version found in the descriptor! + } else { + fail 'The branch version cannot be found in the artifact descriptor!' + } + } +} + +var -strategies = [ + &inject=$-inject~ + + &check=$-check~ + + &skip={ |_| + console:echo 💭 Skipping branch version enforcement, as requested... + } +] + +fn enforce { |&descriptor-name=$nil mode| + var strategy = (map:get-value $-strategies $mode &default={ + fail "Invalid mode: '"$mode"'" + }) + + var project = (project:detect &descriptor-name=$descriptor-name) + + $strategy $project +} diff --git a/core/ci-cd/action-references.elv b/core/ci-cd/action-references.elv new file mode 100644 index 000000000..21bceb944 --- /dev/null +++ b/core/ci-cd/action-references.elv @@ -0,0 +1,9 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/seq +use ./repository + +fn get-regex-for-references-to-other-branches { |current-branch| + var full-repository-name = (repository:get-full-name) + + put 'uses:\s*'$full-repository-name'[^@]+@(?!'$current-branch')' +} \ No newline at end of file diff --git a/core/ci-cd/boot.elv b/core/ci-cd/boot.elv new file mode 100644 index 000000000..b7d3dc972 --- /dev/null +++ b/core/ci-cd/boot.elv @@ -0,0 +1,111 @@ +use epm +use str + +var -required-packages = [ + github.com/giancosta86/aurora-elvish +] + +var -aurora-elvish-version = 'v1' + +fn -tracer-action { |block| + if (has-env AURORA_GITHUB_TRACING_ENABLED) { + $block > &2 + } +} + +fn trace-echo { |@values| + -tracer-action { + echo $@values + } +} + +fn trace-inspect { |&emoji=🔎 description value| + -tracer-action { + print $emoji $description': ' + pprint $value + } +} + +fn -get-sha { |value| + to-string $value | + sha256sum | + str:split ' ' (all) | + take 1 +} + +fn -to-csv { |items| + str:join , $items +} + +fn -split-csv { |source| + str:split , $source | + each $str:trim-space~ | + keep-if { |value| !=s $value '' } +} + +fn confirm-aurora-github { + trace-echo 🔮 Now booting aurora-github for Elvish! +} + +fn set-epm-vars { |inputs| + trace-inspect &emoji=📥 Inputs $inputs + + var workflow = $inputs[workflow] + var run-number = $inputs[run-number] + var packages = [(-split-csv $inputs[packages])] + + var actual-packages = [$@-required-packages $@packages] + + var csv-packages = (-to-csv $actual-packages) + trace-inspect 'Comma-separated packages' $csv-packages + + var packages-sha = (-get-sha $csv-packages) + trace-inspect 'Packages SHA' $packages-sha + + var epm-cache-key = $workflow'-'$run-number'-'$packages-sha + trace-inspect 'EPM cache key' $epm-cache-key + + trace-inspect 'EPM managed directory' $epm:managed-dir + + { + echo csv-packages=$csv-packages + echo epm-cache-key=$epm-cache-key + echo epm-managed-dir=$epm:managed-dir + } >> (get-env GITHUB_ENV) +} + +fn -checkout-aurora-elvish-version { + tmp pwd = $epm:managed-dir/github.com/giancosta86/aurora-elvish + + git checkout -q $-aurora-elvish-version + + use github.com/giancosta86/aurora-elvish/console + + -tracer-action { + console:echo 🔮 aurora-elvish ready! + } +} + +fn install-packages { |csv-packages| + var packages = [(-split-csv $csv-packages)] + + trace-echo 📚 Packages to install: $packages + + for package $packages { + epm:install $package + } + + -checkout-aurora-elvish-version + + trace-echo 🚀 Startup packages for Elvish installed! +} + +fn list-packages { + trace-echo 📚 Elvish startup packages + + epm:installed | each { |pkg| + trace-echo '*' $pkg + } + + trace-echo 📚📚📚 +} \ No newline at end of file diff --git a/core/ci-cd/env.elv b/core/ci-cd/env.elv new file mode 100644 index 000000000..a0111d4e6 --- /dev/null +++ b/core/ci-cd/env.elv @@ -0,0 +1,9 @@ +use ./io + +fn write { |key value| + io:write (get-env GITHUB_ENV) $key $value +} + +fn map { |source-map| + io:map (get-env GITHUB_ENV) $source-map +} \ No newline at end of file diff --git a/core/ci-cd/git-refs.elv b/core/ci-cd/git-refs.elv new file mode 100644 index 000000000..4a315faf8 --- /dev/null +++ b/core/ci-cd/git-refs.elv @@ -0,0 +1,9 @@ +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/seq + +fn get-actual { + var head-ref = (get-env GITHUB_HEAD_REF) + var ref = (get-env GITHUB_REF) + + lang:ternary (seq:is-non-empty $head-ref) $head-ref $ref +} diff --git a/core/ci-cd/input.elv b/core/ci-cd/input.elv new file mode 100644 index 000000000..3b61ebb96 --- /dev/null +++ b/core/ci-cd/input.elv @@ -0,0 +1,85 @@ +use os +use path +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/seq + +fn string { |&optional=$false name value| + var trimmed-value = (str:trim-space $value) + + if (seq:is-empty $trimmed-value) { + if $optional { + put $nil + return + } else { + fail 'Missing input: '''$name'''!' + } + } + + put $trimmed-value +} + +fn -parse-string { |&optional=$false name value parser| + var source-string = (string &optional=$optional $name $value) + if (not $source-string) { + put $nil + return + } + + $parser $source-string +} + +fn bool { |&optional=$false name value| + -parse-string &optional=$optional $name $value { |source-string| + if (==s $source-string true) { + put $true + } elif (==s $source-string false) { + put $false + } else { + fail 'Invalid boolean value for the '''$name''' input: '''$source-string'''!' + } + } +} + +fn enum { |&optional=$false name value admissible-list| + -parse-string &optional=$optional $name $value { |source-string| + if (not (has-value $admissible-list $source-string)) { + fail 'Invalid enum value for the '''$name''' input: '''$source-string'''!' + } + + put $source-string + } +} + +fn -file-system-input { |&optional=$false &can-be-missing=$false type-description name value path-checker| + -parse-string &optional=$optional $name $value { |source-string| + var clean-path = (path:clean $source-string) + + if ($path-checker $clean-path) { + put $clean-path + } else { + if $can-be-missing { + console:inspect &emoji=💭 'Missing '$type-description' for input '''$name''' at path' $clean-path + put $nil + } else { + fail 'Inexistent '$type-description' for input '''$name''' at path: '''$clean-path'''!' + } + } + } +} + +fn directory { |&optional=$false &can-be-missing=$false name value| + -file-system-input &optional=$optional &can-be-missing=$can-be-missing directory $name $value $os:is-dir~ +} + +fn file { |&optional=$false &can-be-missing=$false name value| + -file-system-input &optional=$optional &can-be-missing=$can-be-missing file $name $value $os:is-regular~ +} + +fn list { |source| + put [( + str:split , $source | + each $str:trim-space~ | + keep-if $seq:is-non-empty~ + )] +} \ No newline at end of file diff --git a/core/ci-cd/io.elv b/core/ci-cd/io.elv new file mode 100644 index 000000000..cc2504e4f --- /dev/null +++ b/core/ci-cd/io.elv @@ -0,0 +1,32 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/map +use github.com/giancosta86/aurora-elvish/seq + +var -constant-mappings = [ + &$nil='' + &$true='true' + &$false='false' +] + +var -supported-value-kinds = [string number] + +fn -format-value { |value| + var mapped-value = (map:get-value $-constant-mappings $value) + + coalesce $mapped-value $value +} + +fn write { |target-channel key value| + echo $key'='(-format-value $value) >> $target-channel +} + +fn map { |target-channel source-map| + map:entries $source-map | + seq:each-spread { |key value| + var value-kind = (kind-of $value) + + if (has-value $-supported-value-kinds $value-kind) { + write $target-channel $key $value + } + } +} \ No newline at end of file diff --git a/core/ci-cd/output.elv b/core/ci-cd/output.elv new file mode 100644 index 000000000..fedcb1b85 --- /dev/null +++ b/core/ci-cd/output.elv @@ -0,0 +1,9 @@ +use ./io + +fn write { |key value| + io:write (get-env GITHUB_OUTPUT) $key $value +} + +fn map { |source-map| + io:map (get-env GITHUB_OUTPUT) $source-map +} \ No newline at end of file diff --git a/core/ci-cd/pull-request.elv b/core/ci-cd/pull-request.elv new file mode 100644 index 000000000..ff5ff1e5b --- /dev/null +++ b/core/ci-cd/pull-request.elv @@ -0,0 +1,33 @@ +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn triggers-current-workflow { + var github-ref = (get-env GITHUB_REF) + console:inspect &emoji=🧭 'GITHUB_REF' $github-ref + + lang:ternary (str:has-prefix $github-ref 'refs/pull/') $true $false +} + +fn fetch-info { |branch| + console:inspect &emoji=😺 'Retrieving Git pull request info for branch' $branch + + var raw-data = (gh pr view $branch --json title,number,baseRefOid,headRefOid | from-json) + + put [ + &branch=$branch + &title=$raw-data[title] + &number=$raw-data[number] + &base-sha=$raw-data[baseRefOid] + &head-sha=$raw-data[headRefOid] + ] +} + +fn merge { |branch git-strategy| + console:inspect &emoji=🔀 'Now merging the PR for branch' $branch + console:inspect &emoji=💡 'Git strategy' $git-strategy + + var git-stategy-arg = '--'$git-strategy + + gh pr merge $branch $git-stategy-arg --delete-branch +} \ No newline at end of file diff --git a/core/ci-cd/release-assets.elv b/core/ci-cd/release-assets.elv new file mode 100644 index 000000000..2473f1fc9 --- /dev/null +++ b/core/ci-cd/release-assets.elv @@ -0,0 +1,19 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/seq + +fn publish { |inputs| + console:inspect-inputs $inputs + + var release-tag = $inputs[release-tag] + var files = $inputs[files] + var overwrite = $inputs[overwrite] + + if (seq:is-empty $files) { + fail 'No files declared!' + } + + var clobber-arg = (lang:ternary $overwrite [--clobber] []) + + gh release upload $@clobber-arg $release-tag $@files +} diff --git a/core/ci-cd/repository.elv b/core/ci-cd/repository.elv new file mode 100644 index 000000000..bf56656b1 --- /dev/null +++ b/core/ci-cd/repository.elv @@ -0,0 +1,7 @@ +fn get-full-name { + get-env GITHUB_REPOSITORY +} + +fn get-changelog { |base-reference tag| + put 'https://github.com/'(get-full-name)'/compare/'$base-reference'..'$tag +} \ No newline at end of file diff --git a/core/ci-cd/required-jobs.elv b/core/ci-cd/required-jobs.elv new file mode 100644 index 000000000..7eec0cf4e --- /dev/null +++ b/core/ci-cd/required-jobs.elv @@ -0,0 +1,42 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/map + +var -result-descriptions = [ + &success='✅ (OK)' + &failure='❌ (FAILURE)' + &skipped='😴 (SKIPPED)' +] + +var -unknown-result-description = '❓ (UNKNOWN)' + +fn check { |needs-as-json| + var failure = $false + + var required-jobs = (echo $needs-as-json | from-json) + + console:section &emoji=🤹 'JOB SUMMARY' { + keys $required-jobs | + order | + each { |job-id| + var raw-job = $required-jobs[$job-id] + + var job-result = $raw-job[result] + + if (!=s $job-result success) { + set failure = $true + } + + var result-description = ( + map:get-value $-result-descriptions $job-result &default=$-unknown-result-description + ) + + console:echo '* '$job-id': '$result-description + } + } + + if $failure { + fail 'All the required jobs must be successful!' + } + + console:echo ✅ All the required jobs are OK! +} diff --git a/core/critical-todos.elv b/core/critical-todos.elv new file mode 100644 index 000000000..7cd6c347b --- /dev/null +++ b/core/critical-todos.elv @@ -0,0 +1,47 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +var -todo-text = (printf '%s%s' TODO !) + +fn find { |inputs| + console:inspect-inputs $inputs + + var verbose = $inputs[verbose] + var display-lines = $inputs[display-lines] + var source-file-regex = $inputs[source-file-regex] + var crash-on-found = $inputs[crash-on-found] + + console:inspect &emoji=📁 'Current directory' $pwd + + var quiet-grep-arg = (lang:ternary $display-lines '' '-q') + + var found = $false + + put **[type:regular] | + keep-if { |path| + eq $ok ?(echo $path | grep --perl-regexp --quiet $source-file-regex) + } | + each { |path| + if $verbose { + console:echo 🔎 Looking for critical TODOs in path: $path... + } + + var found-in-file = ?(grep --color=always --with-filename --line-number $@quiet-grep-arg $-todo-text $path > &2) + + if $found-in-file { + set found = $true + } + } + + if $found { + if $crash-on-found { + fail 'Critical TODOs found!' + } else { + console:echo 🤯 Critical TODOs found! + put $true + } + } else { + console:echo ✅ No critical TODOs found! + put $false + } +} diff --git a/core/custom-tests.elv b/core/custom-tests.elv new file mode 100644 index 000000000..4307f964a --- /dev/null +++ b/core/custom-tests.elv @@ -0,0 +1,106 @@ +use os +use ./ci-cd/env +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/script +use github.com/giancosta86/aurora-elvish/testing + +fn -set-strategy { |strategy| + console:inspect &emoji=💡 'Current test strategy' $strategy + env:write strategy $strategy +} + +fn -set-script-strategy { |script-path| + -set-strategy script + + console:inspect &emoji=📃 'Test script to run' $script-path + + env:write scriptPath $script-path +} + +fn -enforce-exit-strategy { + -set-strategy exit +} + +fn detect-strategy { |inputs| + console:inspect-inputs $inputs + + var optional = $inputs[optional] + var script-file = $inputs[script-file] + var root-directory = $inputs[root-directory] + + console:echo 🔬 Looking for a custom test strategy... + + if (not $root-directory) { + if $optional { + console:echo 💭 Skipping optional tests in missing root directory... + -enforce-exit-strategy + return + } else { + fail "Cannot run custom tests in missing root directory: '"$root-directory"'!" + } + } + + console:echo 🔁📁 Test root directory "'"$root-directory"'" found! + tmp pwd = $root-directory + + var target-script-file = (coalesce $script-file verify) + + var actual-script-path = (script:get-actual-path $target-script-file) + + if $actual-script-path { + console:echo 🐚 Shell script strategy requested! + -set-script-strategy $actual-script-path + return + } else { + if $script-file { + var error-message = "Cannot find the '"$script-file"' script file" + + if $optional { + console:echo 💭 $error-message... + -enforce-exit-strategy + return + } else { + fail $error-message + } + } + } + + if (testing:has-tests) { + console:echo 📋 aurora-elvish .test.elv files found! + -set-strategy test-runner + return + } + + if (os:is-regular package.json) { + console:echo 📦 package.json file found! + -set-strategy nodejs + return + } + + if (os:is-regular Cargo.toml) { + console:echo 🦀 Cargo.toml file found! + -set-strategy rust + return + } + + if $optional { + console:echo 💭 No supported strategy detected for the optional custom tests... + -enforce-exit-strategy + } else { + fail 'Cannot run mandatory custom tests: no supported test strategy could be detected!' + } +} + +fn execute-test-runner { + var testing-result = (testing:run) + + if $testing-result[is-ok] { + console:echo ✅ All the $testing-result[total-tests] tests are OK! + } else { + console:block &emoji=❌ $testing-result[total-failed]' tests failed' { + pprint $testing-result + } + + exit 1 + } +} \ No newline at end of file diff --git a/core/git.elv b/core/git.elv new file mode 100644 index 000000000..6d1d3deda --- /dev/null +++ b/core/git.elv @@ -0,0 +1,53 @@ +use math +use github.com/giancosta86/aurora-elvish/command +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn hard-reset { + console:echo ⏱️ Discarding local changes to the Git repository... + + command:silence-until-error { + git reset --hard HEAD + } + + console:echo ✅ Git repository successfully reset +} + +fn fetch-branched-sha { |&depth-delta=25 &delta-factor=1.5 branch required-sha| + console:inspect &emoji=🧭 'Iteratively trying to fetch Git SHA' $required-sha + + while (not ?(command:silence { git cat-file commit $required-sha })) { + console:echo 📥 Fetching $depth-delta more commits... + + command:silence-until-error { + git fetch --deepen=$depth-delta origin $branch + } + + set depth-delta = (* $depth-delta $delta-factor | math:ceil (all) | exact-num (all)) + } + + console:inspect &emoji=✅ 'Git SHA ready' $required-sha +} + +fn fetch-tags { + console:echo 📥 Retrieving Git tags... + + command:silence-until-error { + git fetch --tags + } + + console:echo ✅ Git tags retrieved! +} + +fn create-and-push-tag { |&force=$false tag| + console:inspect &emoji=📌 'Creating and pushing Git tag' $tag + + var force-arg = (lang:ternary $force [--force] []) + + command:silence-until-error { + git tag $@force-arg $tag + git push origin $tag $@force-arg + } + + console:echo 📌 Tag created and pushed! +} \ No newline at end of file diff --git a/core/jvm/context.elv b/core/jvm/context.elv new file mode 100644 index 000000000..ff0f91e1f --- /dev/null +++ b/core/jvm/context.elv @@ -0,0 +1,36 @@ +use github.com/giancosta86/aurora-elvish/console +use ./project +use ./sdkman + +var -build-tool-sdks = [ + &mvn=maven + &gradle=gradle +] + +fn setup { |&java-version=$nil &tool-version=$nil| + console:inspect-inputs [ + &java-version=$java-version + &tool-version=$tool-version + ] + + console:echo ☕💻 Setting up JVM context in "'"$pwd"'"... + + var project = (project:detect-for-jvm) + var build-tool = $project[build-tool] + + if $java-version { + sdkman:install-sdk java $java-version + } + + if $tool-version { + var build-tool-sdk = $-build-tool-sdks[build-tool] + + sdkman:install $build-tool-sdk $tool-version + } + + put [ + &buildTool=$build-tool + ] + + console:echo ✅☕ JVM context in "'"$pwd"'" ready! +} \ No newline at end of file diff --git a/core/jvm/gradle.elv b/core/jvm/gradle.elv new file mode 100644 index 000000000..b522fa926 --- /dev/null +++ b/core/jvm/gradle.elv @@ -0,0 +1,24 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn run { |&quiet=$true @rest| + var quiet-arg = (lang:ternary $quiet [-q] []) + + gradle --no-daemon --no-scan $@quiet-arg $@rest +} + +fn verify-project { |&quiet=$true| + console:echo 🐘 Running Gradle to verify the project... + + run &quiet=$quiet build + + console:echo ✅ Gradle verification OK! +} + +fn publish-project { |&quiet=$true &dry-run=$true| + console:echo 🐘 Running Gradle to publish the project... + + var dry-run-arg = (lang:ternary $dry-run [--dry-run] []) + + run &quiet=$quiet $@dry-run-arg publish +} \ No newline at end of file diff --git a/core/jvm/gradle/descriptor.elv b/core/jvm/gradle/descriptor.elv new file mode 100644 index 000000000..12ca83217 --- /dev/null +++ b/core/jvm/gradle/descriptor.elv @@ -0,0 +1,17 @@ +use os + +var -supported-descriptors = [ + build.gradle.kts + build.gradle +] + +fn get-name { + for descriptor $-supported-descriptors { + if (os:is-regular $descriptor) { + put $descriptor + return + } + } + + fail 'No supported Gradle descriptor found in '''$pwd'''!' +} \ No newline at end of file diff --git a/core/jvm/gradle/settings.elv b/core/jvm/gradle/settings.elv new file mode 100644 index 000000000..ba7dc27cd --- /dev/null +++ b/core/jvm/gradle/settings.elv @@ -0,0 +1,23 @@ +use str +use github.com/giancosta86/aurora-elvish/console +use ./descriptor + +fn prepare-for-publication { + console:echo 🐘 Preparing Gradle settings for publication... + + var descriptor-name = (descriptor:get-name) + + var descriptor-content = (slurp < $descriptor-name) + + console:section &emoji=🐘 'Checking the optional use of environment variables in the descriptor' { + for env-var [JVM_AUTH_USER JVM_AUTH_TOKEN] { + if (str:contains $descriptor-content $env-var) { + console:inspect &emoji=✅ 'Referenced' $env-var + } else { + console:inspect &emoji=💭 'Not mentioned' $env-var + } + } + } + + console:echo ✅ Gradle settings now ready! +} \ No newline at end of file diff --git a/core/jvm/maven.elv b/core/jvm/maven.elv new file mode 100644 index 000000000..90a79bcb9 --- /dev/null +++ b/core/jvm/maven.elv @@ -0,0 +1,30 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn run { |&quiet=$true @rest| + var quiet-arg = (lang:ternary $quiet [-q] []) + + mvn -B $@quiet-arg $@rest +} + +fn verify-project { |&quiet=$true| + console:echo 🪶 Running Maven to verify the project... + run &quiet=$quiet verify + console:echo ✅ Maven verification OK! +} + +fn publish-project { |&quiet=$true &dry-run=$true| + var dry-run-arg + + if $dry-run { + var dry-run-directory = target/dry-run + + console:inspect &emoji=📁 'dry-run mode enabled - publishing to local directory' $dry-run-directory + + set dry-run-arg = [-DaltDeploymentRepository=target-server::default::file:$dry-run-directory] + } else { + set dry-run-arg = [] + } + + run &quiet=$quiet $@dry-run-arg deploy +} \ No newline at end of file diff --git a/core/jvm/maven/settings.elv b/core/jvm/maven/settings.elv new file mode 100644 index 000000000..6e619539e --- /dev/null +++ b/core/jvm/maven/settings.elv @@ -0,0 +1,57 @@ +use os +use path +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/highlighting +use github.com/giancosta86/aurora-elvish/resources + +var -resources = (resources:for-script (src)) + +var -user-settings-path = ~/.m2/settings.xml + +var -user-settings-filename = (path:base $-user-settings-path) + +fn -copy-default-settings { + console:echo 🌟 Providing a default settings file for 🪶 Maven... + + var default-settings-path = ($-resources[get-path] $-user-settings-filename) + + cp $default-settings-path $-user-settings-path + + console:section &emoji=🪶 'Content of the per-user Maven settings file' { + cat $-user-settings-path | highlighting:highlight xml + } +} + +fn -check-server-in-pom { + var server-name = target-server + + if (slurp < pom.xml | str:contains (all) $server-name) { + console:echo ✅ Server "'"$server-name"'" found in pom.xml! + } else { + console:echo 💭 Server "'"$server-name"'" not mentioned in pom.xml... + } +} + +fn prepare-for-publication { + console:echo 🪶 Preparing Maven settings for publication... + + if (os:is-regular $-user-settings-path) { + console:inspect &emoji=🌟 'Maven settings file found at' $-user-settings-path + return + } + + os:mkdir-all (path:dir $-user-settings-path) + + if (os:is-regular $-user-settings-filename) { + console:echo 📃 Maven settings file found in the current directory! Now copying it... + + cp $-user-settings-filename $-user-settings-path + } else { + -copy-default-settings + + -check-server-in-pom + } + + console:echo ✅ Maven settings now ready! +} diff --git a/actions/publish-jvm-project/settings.xml b/core/jvm/maven/settings.xml similarity index 100% rename from actions/publish-jvm-project/settings.xml rename to core/jvm/maven/settings.xml diff --git a/core/jvm/project.elv b/core/jvm/project.elv new file mode 100644 index 000000000..49e86957b --- /dev/null +++ b/core/jvm/project.elv @@ -0,0 +1,18 @@ +use github.com/giancosta86/aurora-elvish/console +use ../project + +var -supported-build-tools = [mvn gradle] + +fn detect-for-jvm { + var project = (project:detect) + + var build-tool = $project[build-tool] + + console:inspect &emoji=⚒ 'Project build tool' $build-tool + + if (not (has-value $-supported-build-tools $build-tool)) { + fail 'Unsupported build tool for a JVM project: '$build-tool + } + + put $project +} \ No newline at end of file diff --git a/core/jvm/project/publication.elv b/core/jvm/project/publication.elv new file mode 100644 index 000000000..1c823e3c6 --- /dev/null +++ b/core/jvm/project/publication.elv @@ -0,0 +1,20 @@ +var -project-publishers = [ + &mvn={ |inputs| + use ../maven + maven:publish-project &quiet=$inputs[quiet-tool] &dry-run=$inputs[dry-run] + } + + &gradle={ |inputs| + use ../gradle + gradle:publish-project &quiet=$inputs[quiet-tool] &dry-run=$inputs[dry-run] + } +] + +fn publish { |&quiet-tool=$true &dry-run=$true build-tool| + var publisher = $-project-publishers[$build-tool] + + $publisher [ + &quiet-tool=$quiet-tool + &dry-run=$dry-run + ] +} \ No newline at end of file diff --git a/core/jvm/project/verification.elv b/core/jvm/project/verification.elv new file mode 100644 index 000000000..338f02d97 --- /dev/null +++ b/core/jvm/project/verification.elv @@ -0,0 +1,17 @@ +var -project-verifiers = [ + &mvn={ |quiet-tool| + use ../maven + maven:verify-project &quiet=$quiet-tool + } + + &gradle={ |quiet-tool| + use ../gradle + gradle:verify-project &quiet=$quiet-tool + } +] + +fn verify { |&quiet-tool=$true build-tool| + var verifier = $-project-verifiers[$build-tool] + + $verifier $quiet-tool +} \ No newline at end of file diff --git a/core/jvm/sdkman.elv b/core/jvm/sdkman.elv new file mode 100644 index 000000000..828e77aee --- /dev/null +++ b/core/jvm/sdkman.elv @@ -0,0 +1,40 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use ../ci-cd/env + +var -sdkman-home = ~/.sdkman +var -sdkman-script = $-sdkman-home/bin/sdkman-init.sh + +fn -ensure-installed { + if (os:is-dir $-sdkman-home) { + console:echo 🎉 It seems that SDKMAN was previously installed! + return + } + + console:echo 📥 Installing SDKMAN... + + curl -s 'https://get.sdkman.io' | bash + + console:echo ✅ SDKMAN installed! +} + +fn install-sdk { |candidate version| + -ensure-installed + + console:echo 📥 Installing $candidate'('$version')...' + + bash -c "source '"$-sdkman-script"'; sdk install '"$candidate"' '"$version"'" + + var sdk-bin = $-sdkman-home/candidates/$candidate/$version/bin + + if (not (os:is-dir $sdk-bin)) { + fail 'Inexistent sdk ''bin'' directory: '$sdk-bin + } + + var updated-path = $sdk-bin':'(get-env PATH) + + set-env PATH $updated-path + env:write PATH $updated-path + + console:echo ✅ $candidate'('$version')' installed! +} diff --git a/core/jvm/settings.elv b/core/jvm/settings.elv new file mode 100644 index 000000000..5f54ae749 --- /dev/null +++ b/core/jvm/settings.elv @@ -0,0 +1,17 @@ +var -publication-preparers = [ + &mvn={ + use ./maven/settings + settings:prepare-for-publication + } + + &gradle={ + use ./gradle/settings + settings:prepare-for-publication + } +] + +fn prepare-for-publication { |build-tool| + var preparer = $-publication-preparers[$build-tool] + + $preparer +} \ No newline at end of file diff --git a/core/license-file.elv b/core/license-file.elv new file mode 100644 index 000000000..e7cd9f1c3 --- /dev/null +++ b/core/license-file.elv @@ -0,0 +1,24 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/seq + +fn -check-includes-current-year { |license-file| + var current-year = (date +%Y) + + if (seq:is-empty $current-year) { + fail 'Cannot detect the current year!' + } + + console:inspect &emoji=🗓 'Current year' $current-year + + console:echo 🔎🗓 Searching the license file for the current year... + + if ?(grep --color=always $current-year $license-file > &2) { + console:echo ✅ Current year found in the license file! + } else { + fail 'Cannot find the current year in the license file!' + } +} + +fn check { |license-file| + -check-includes-current-year $license-file +} diff --git a/core/nodejs/context.elv b/core/nodejs/context.elv new file mode 100644 index 000000000..2d55f308a --- /dev/null +++ b/core/nodejs/context.elv @@ -0,0 +1,123 @@ +use os +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/seq +use ../ci-cd/env + +fn check-preconditions { + console:echo 📦💻 Setting up NodeJS context in "'"$pwd"'"... + + if (not (os:is-regular package.json)) { + fail 'The package.json descriptor is missing!' + } +} + +fn -detect-node-version-from-package-json { + var requested-node-version = (jq -r '.engines.node // ""' package.json) + + if (seq:is-non-empty $requested-node-version) { + console:inspect 'NodeJS version requested in package.json' $requested-node-version + put $requested-node-version + } else { + console:echo 💭 No 'engines/node' field in package.json... + put $nil + } +} + +fn -detect-node-version-from-nvmrc { + if (os:is-regular .nvmrc) { + var requested-node-version = (slurp < .nvmrc | str:trim-space (all)) + + console:inspect 'Requested version in the .nvmrc file' $requested-node-version + + lang:ternary (seq:is-non-empty $requested-node-version) $requested-node-version $nil + } else { + console:echo 💭 No .nvmrc file... + put $nil + } +} + +fn detect-nodejs-constraints { + var requested-node-version = ( + coalesce ( + -detect-node-version-from-nvmrc + ) ( + -detect-node-version-from-package-json + ) + ) + + var install-toolchain + + if $requested-node-version { + console:inspect 'Requested NodeJS version' $requested-node-version + set install-toolchain = true + } else { + console:echo 💭 No requested NodeJS version... + set install-toolchain = false + } + + console:inspect 'Install NodeJS toolchain' $install-toolchain + + put [ + &install-toolchain=$install-toolchain + &requested-node-version=$requested-node-version + ] +} + +fn detect-pnpm-constraints { + var package-manager-reference = (jq -r '.packageManager // ""' package.json) + + console:inspect &emoji=📦 'Package manager reference' $package-manager-reference + + var requested-pnpm-version + + if (seq:is-non-empty $package-manager-reference) { + var requested-package-manager requested-package-manager-version = ( + echo $package-manager-reference | str:split @ (all) + ) + + if (!=s $requested-package-manager pnpm) { + fail 'The package manager must be pnpm!' + } + + set requested-pnpm-version = $requested-package-manager-version + + console:inspect 'Requested pnpm version' $requested-pnpm-version + } else { + console:echo 🌟 Defaulting to the latest pnpm version! + set requested-pnpm-version = latest + } + + console:inspect &emoji=🔬 'pnpm version to install' $requested-pnpm-version + + put [ + &requested-pnpm-version=$requested-pnpm-version + ] +} + +fn set-pnpm-colors { |enabled| + var key = FORCE_COLOR + var value = (lang:ternary $enabled 1 0) + + set-env $key $value + env:write $key $value +} + +fn install-dependencies { + var lockfile = pnpm-lock.yaml + + var lockfile-arg + + if (os:is-regular $lockfile) { + console:echo 🧊 Installing dependencies with frozen lockfile, as "'"$lockfile"'" is present... + set lockfile-arg = --frozen-lockfile + } else { + console:echo 🌞 Installing dependencies without frozen lockfile, as "'"$lockfile"'" is missing... + set lockfile-arg = --no-frozen-lockfile + } + + pnpm install $lockfile-arg + + console:echo ✅ Dependencies installed! +} diff --git a/core/nodejs/pnpm.elv b/core/nodejs/pnpm.elv new file mode 100644 index 000000000..119312f93 --- /dev/null +++ b/core/nodejs/pnpm.elv @@ -0,0 +1,47 @@ +use os +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/highlighting +use github.com/giancosta86/aurora-elvish/map + +fn parse-scope { |declared-scope| + if (==s $declared-scope '') { + console:echo 🫚 Root npm scope detected! + put $nil + } else { + var actual-scope = (str:trim-prefix $declared-scope @) + console:inspect &emoji=☂ 'Custom npm scope detected' $actual-scope + put $actual-scope + } +} + +fn run-optional-script { |script| + var package-json = (from-json < package.json) + + var scripts = (coalesce (map:get-value $package-json scripts) [&]) + + if (has-key $scripts $script) { + console:echo 🛠 Optional "'"$script"'" script found in package.json - now running it! + + pnpm $script + } else { + console:echo 💭 Optional "'"$script"'" script not found in package.json... + } +} + +fn ensure-config { + var config-path = .npmrc + + if (os:is-regular $config-path) { + console:echo 🌟You already have a custom $config-path file! + return + } + + console:echo 🧞 It seems you do not have a $config-path file - generating a default one... + + echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > $config-path + + console:section &emoji=🎀 'Your auto-generated '$config-path' configuration file' { + cat $config-path | highlighting:highlight ini + } +} \ No newline at end of file diff --git a/core/nodejs/project.elv b/core/nodejs/project.elv new file mode 100644 index 000000000..6bce49797 --- /dev/null +++ b/core/nodejs/project.elv @@ -0,0 +1,16 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn verify { + console:echo 📦 Now running the 'verify' script from package.json... + + pnpm verify + + console:echo ✅Verification script OK! +} + +fn publish { |dry-run| + var dry-run-arg = (lang:ternary $dry-run [--dry-run] []) + + pnpm publish --no-git-checks --access public $@dry-run-arg +} diff --git a/core/nodejs/subpath-exports.elv b/core/nodejs/subpath-exports.elv new file mode 100644 index 000000000..1fc0abef7 --- /dev/null +++ b/core/nodejs/subpath-exports.elv @@ -0,0 +1,6 @@ +use ./subpath-exports/check +use ./subpath-exports/inject + +var check~ = $check:check~ + +var inject~ = $inject:inject~ diff --git a/core/nodejs/subpath-exports/check.elv b/core/nodejs/subpath-exports/check.elv new file mode 100644 index 000000000..cb906e6e4 --- /dev/null +++ b/core/nodejs/subpath-exports/check.elv @@ -0,0 +1,62 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/map + +var -recursive-check-json-value + +fn -check-json-object { |path-in-json json-object| + keys $json-object | order | each { |key| + $-recursive-check-json-value $path-in-json'->'$key $json-object[$key] + } +} + +fn -check-file-pattern { |path-in-json file-pattern| + console:print 🔎 $path-in-json'->'$file-pattern...' ' + + var matches = (find . -wholename $file-pattern | wc -l) + + if (== $matches 0) { + console:echo ❌ + fail 'No file matching subpath pattern: '$file-pattern + } else { + console:echo ✅ + } +} + +fn -check-json-value { |path-in-json json-value| + var checker = ( + lang:ternary (==s (kind-of $json-value) map) $-check-json-object~ $-check-file-pattern~ + ) + + $checker $path-in-json $json-value +} + +set -recursive-check-json-value = $-check-json-value~ + +fn check { + if (not (os:is-regular package.json)) { + fail 'The package.json descriptor file does not exist!' + } + + var exports = ( + from-json < package.json | + map:get-value (all) exports + ) + + if (not $exports) { + console:echo 💭 No exports declared in package.json... + return + } + + if (== (count $exports) 0) { + console:echo 💭 Exports are an empty object... + return + } + + console:echo 🔎 Now inspecting subpath exports... + + -check-json-value exports $exports + + console:echo ✅ Export subpaths are OK! +} \ No newline at end of file diff --git a/core/nodejs/subpath-exports/inject.elv b/core/nodejs/subpath-exports/inject.elv new file mode 100644 index 000000000..f06053ee5 --- /dev/null +++ b/core/nodejs/subpath-exports/inject.elv @@ -0,0 +1,92 @@ +use os +use path +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/edit + +var -index-names = [index.ts index.js] + +fn -print-exports { |context-description| + console:section &emoji=📦 'package.json exports '$context-description { + jq -C .exports package.json + } +} + +fn -write-export { |relative-index-directory| + if (not ( + or (==s $relative-index-directory '') (str:has-prefix $relative-index-directory '/') + )) { + fail 'The relative index directory must be empty or have a leading /' + } + + var export-key = '.'$relative-index-directory + + var dist-index-directory = './dist'$relative-index-directory + + console:echo "🔑 Export '"$export-key"' provided by dist directory: '"$dist-index-directory"'" + + edit:json package.json ' + .exports += { + "'$export-key'": { + types: "'$dist-index-directory'/index.d.ts", + import: "'$dist-index-directory'/index.js" + } + }' + + console:echo ✅ Export $export-key injected! +} + +fn -inject-root-index { |source-directory| + put $source-directory/{$@-index-names} | each { |potential-root-index| + console:inspect 'Looking for potential root index' $potential-root-index + + if (os:is-regular $potential-root-index) { + console:inspect &emoji=✅ 'Root index file found' $potential-root-index + + -write-export '' + + put $true + return + } + } + + console:echo 💭 No root index file found... + put $false +} + +fn -inject-subdirectory-indexes { |source-directory| + console:echo 🔽 Looking for potential subdirectory indexes... + + put $source-directory/*[type:regular][nomatch-ok]/{$@-index-names} | + each { |index-path| + console:inspect &emoji=💡 'Index file found in subdirectory, at path' $index-path + + var export-name = (str:trim-prefix (path:dir $index-path) $source-directory/) + + -write-export '/'$export-name + } +} + +fn -do-inject { |source-directory mode| + var root-index-injected = (-inject-root-index $source-directory) + + if (and $root-index-injected (==s $mode prefer-index)) { + console:echo 🐹 In this mode, since the root index has been found, no more subpath exports will be injected + return + } + + -inject-subdirectory-indexes $source-directory +} + +fn inject { | inputs | + console:inspect-inputs $inputs + + var mode = $inputs[mode] + var source-directory = $inputs[source-directory] + + -print-exports 'before the injection' + + -do-inject $source-directory $mode + + -print-exports 'after the injection' +} \ No newline at end of file diff --git a/core/project.elv b/core/project.elv new file mode 100644 index 000000000..d979c09cd --- /dev/null +++ b/core/project.elv @@ -0,0 +1,5 @@ +use ./project/loader + +fn detect { |&descriptor-name=$nil| + loader:load &descriptor-name=$descriptor-name +} diff --git a/core/project/base.elv b/core/project/base.elv new file mode 100644 index 000000000..7f8d2070b --- /dev/null +++ b/core/project/base.elv @@ -0,0 +1,38 @@ +use path +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/map +use ./descriptors/plain-text + +fn load-project { |inputs| + var descriptor-namespace = $inputs[descriptor-namespace] + var descriptor-name = $inputs[descriptor-name] + var technology = $inputs[technology] + var build-tool = $inputs[build-tool] + var emoji = $inputs[emoji] + + var descriptor-path = (path:join $pwd $descriptor-name) + + put [ + &directory=$pwd + + &descriptor-path=$descriptor-path + + &descriptor-name=$descriptor-name + + &technology=$technology + + &build-tool=$build-tool + + &emoji=$emoji + + &read-version={ $descriptor-namespace[read-version~] $descriptor-path } + + &print-descriptor={ + console:section &emoji=$emoji 'Project descriptor ('$descriptor-name')' { + $descriptor-namespace[print-content~] $descriptor-path + } + } + + &to-string={ put $emoji' '$technology' ('$descriptor-name')' } + ] +} diff --git a/core/project/descriptors/gradle.elv b/core/project/descriptors/gradle.elv new file mode 100644 index 000000000..e1997708f --- /dev/null +++ b/core/project/descriptors/gradle.elv @@ -0,0 +1,14 @@ +use path +use github.com/giancosta86/aurora-elvish/highlighting +use github.com/giancosta86/aurora-elvish/lang +use ./toml + +var read-version~ = $toml:read-version~ + +fn print-content { |descriptor-path| + var extension = (path:ext $descriptor-path) + + var highlighting-format = (lang:ternary (==s $extension .kts) kotlin groovy) + + cat $descriptor-path | highlighting:highlight $highlighting-format +} \ No newline at end of file diff --git a/core/project/descriptors/json.elv b/core/project/descriptors/json.elv new file mode 100644 index 000000000..2d4bf34e6 --- /dev/null +++ b/core/project/descriptors/json.elv @@ -0,0 +1,9 @@ +use github.com/giancosta86/aurora-elvish/highlighting + +fn read-version { |descriptor-path| + put (from-json < $descriptor-path)[version] +} + +fn print-content { |descriptor-path| + cat $descriptor-path | highlighting:highlight json +} \ No newline at end of file diff --git a/core/project/descriptors/plain-text.elv b/core/project/descriptors/plain-text.elv new file mode 100644 index 000000000..46a6ede25 --- /dev/null +++ b/core/project/descriptors/plain-text.elv @@ -0,0 +1,7 @@ +fn read-version { |descriptor-path| + put $nil +} + +fn print-content { |descriptor-path| + cat $descriptor-path +} \ No newline at end of file diff --git a/core/project/descriptors/toml.elv b/core/project/descriptors/toml.elv new file mode 100644 index 000000000..7a7b78323 --- /dev/null +++ b/core/project/descriptors/toml.elv @@ -0,0 +1,18 @@ +use re +use github.com/giancosta86/aurora-elvish/highlighting + +fn read-version { |descriptor-path| + cat $descriptor-path | from-lines | each { |line| + re:find '^\s*version\s*=\s*["''](.*)["'']\s*$' $line | + each { |match| + put $match[groups][1][text] + return + } + } + + put $nil +} + +fn print-content { |descriptor-path| + cat $descriptor-path | highlighting:highlight toml +} \ No newline at end of file diff --git a/core/project/descriptors/xml-version.py b/core/project/descriptors/xml-version.py new file mode 100644 index 000000000..9837ec877 --- /dev/null +++ b/core/project/descriptors/xml-version.py @@ -0,0 +1,19 @@ +import sys +import xml.etree.ElementTree as ET + +descriptor_path = sys.argv[1] + +tree = ET.parse(descriptor_path) + +root = tree.getroot() + +namespaces = {"mvn": root.tag.split("}")[0].strip("{}")} + +version_tag = root.find(".//mvn:version", namespaces) + +if version_tag is None: + raise Exception("Cannot find the tag!") + +version = version_tag.text + +print(version) \ No newline at end of file diff --git a/core/project/descriptors/xml.elv b/core/project/descriptors/xml.elv new file mode 100644 index 000000000..0d5266624 --- /dev/null +++ b/core/project/descriptors/xml.elv @@ -0,0 +1,15 @@ +use str +use github.com/giancosta86/aurora-elvish/highlighting +use github.com/giancosta86/aurora-elvish/resources + +var -resources = (resources:for-script (src)) + +fn read-version { |descriptor-path| + var python-script-path = ($-resources[get-path] xml-version.py) + + python3 $python-script-path $descriptor-path +} + +fn print-content { |descriptor-path| + cat $descriptor-path | highlighting:highlight xml +} \ No newline at end of file diff --git a/core/project/gradle.elv b/core/project/gradle.elv new file mode 100644 index 000000000..5b1197534 --- /dev/null +++ b/core/project/gradle.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/gradle + +fn load-project { |descriptor-name| + base:load-project [ + &descriptor-namespace=$gradle: + + &descriptor-name=$descriptor-name + + &technology=Gradle + + &build-tool=gradle + + &emoji=🐘 + ] +} diff --git a/core/project/loader.elv b/core/project/loader.elv new file mode 100644 index 000000000..fbe9070f1 --- /dev/null +++ b/core/project/loader.elv @@ -0,0 +1,79 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/map + +var -loader-retrievers = [ + &package.json={ + use ./nodejs + put $nodejs:load-project~ + } + + &Cargo.toml={ + use ./rust + put $rust:load-project~ + } + + &pom.xml={ + use ./maven + put $maven:load-project~ + } + + &build.gradle={ + use ./gradle + put { gradle:load-project build.gradle } + } + + &build.gradle.kts={ + use ./gradle + put { gradle:load-project build.gradle.kts } + } + + &pyproject.toml={ + use ./python + put $python:load-project~ + } +] + +fn -from-descriptor-name { |descriptor-name| + console:inspect 'Requested descriptor' $descriptor-name + + var requested-retriever = (map:get-value $-loader-retrievers $descriptor-name) + + if $requested-retriever { + console:inspect &emoji=📤 'Fetching the requested loader for' $descriptor-name + + var loader = ($requested-retriever) + put $loader + } else { + console:echo 🎁 Unknown technology found! + + use ./unknown + put { unknown:load-project $descriptor-name } + } +} + +fn -infer-from-files { + console:echo 🔎 Now looking for a supported descriptor... + + keys $-loader-retrievers | each { |current-descriptor-name| + if (os:is-regular $current-descriptor-name) { + console:inspect &emoji=🌟 'Descriptor found' $current-descriptor-name + + var loader = ($-loader-retrievers[$current-descriptor-name]) + put $loader + + return + } + } + + fail 'Unknown technology found - and no descriptor name was specified!' +} + +fn load { |&descriptor-name=$nil| + var loader-retriever = (lang:ternary $descriptor-name { -from-descriptor-name $descriptor-name } { -infer-from-files }) + + var loader = ($loader-retriever) + + $loader +} \ No newline at end of file diff --git a/core/project/maven.elv b/core/project/maven.elv new file mode 100644 index 000000000..1dbf58797 --- /dev/null +++ b/core/project/maven.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/xml + +fn load-project { + base:load-project [ + &descriptor-namespace=$xml: + + &descriptor-name=pom.xml + + &technology=Maven + + &build-tool=mvn + + &emoji=🪶 + ] +} diff --git a/core/project/nodejs.elv b/core/project/nodejs.elv new file mode 100644 index 000000000..fa09f264e --- /dev/null +++ b/core/project/nodejs.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/json + +fn load-project { + base:load-project [ + &descriptor-namespace=$json: + + &descriptor-name=package.json + + &technology=NodeJS + + &build-tool=pnpm + + &emoji=📦 + ] +} diff --git a/core/project/python.elv b/core/project/python.elv new file mode 100644 index 000000000..6da4436db --- /dev/null +++ b/core/project/python.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/toml + +fn load-project { + base:load-project [ + &descriptor-namespace=$toml: + + &descriptor-name=pyproject.toml + + &technology=Python + + &build-tool=pdm + + &emoji=🐍 + ] +} diff --git a/core/project/rust.elv b/core/project/rust.elv new file mode 100644 index 000000000..d1cfa3139 --- /dev/null +++ b/core/project/rust.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/toml + +fn load-project { + base:load-project [ + &descriptor-namespace=$toml: + + &descriptor-name=Cargo.toml + + &technology=Rust + + &build-tool=cargo + + &emoji=🦀 + ] +} diff --git a/core/project/unknown.elv b/core/project/unknown.elv new file mode 100644 index 000000000..2c56c5ea0 --- /dev/null +++ b/core/project/unknown.elv @@ -0,0 +1,16 @@ +use ./base +use ./descriptors/plain-text + +fn load-project { |descriptor-name| + base:load-project [ + &descriptor-namespace=$plain-text: + + &descriptor-name=$descriptor-name + + &technology=Unknown + + &build-tool=$nil + + &emoji=🎁 + ] +} diff --git a/core/python/context.elv b/core/python/context.elv new file mode 100644 index 000000000..dc52d280c --- /dev/null +++ b/core/python/context.elv @@ -0,0 +1,19 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use ./pdm + +fn -check-preconditions { + if (not (os:is-regular pyproject.toml)) { + fail 'The pyproject.toml descriptor is missing!' + } +} + +fn setup { |&pdm-version=$nil| + console:echo 🐍💻 Setting up Python context in "'"$pwd"'"... + + -check-preconditions + + pdm:ensure &version=$pdm-version + + console:echo ✅🐍 NodeJS context in "'"$pwd"'" ready! +} \ No newline at end of file diff --git a/core/python/core/__init__.py b/core/python/core/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/core/python/core/check_action_references.py b/core/python/core/check_action_references.py deleted file mode 100644 index d88409246..000000000 --- a/core/python/core/check_action_references.py +++ /dev/null @@ -1,44 +0,0 @@ -from os import getenv - -from .detect_branch_version import detect_branch_version_info -from .exceptions import AuroraException -from .inputs import InputDto -from .processes import shell_run - - -class Inputs(InputDto): - actions_directory: str - - -def check_action_references(inputs: Inputs) -> None: - branch = detect_branch_version_info().branch - if not branch: - raise AuroraException("Cannot detect the branch!") - - print(f"🌲Current branch: '{branch}'") - - full_repo = getenv("GITHUB_REPOSITORY") - if not full_repo: - raise AuroraException("Cannot detect the full repository name!") - - print(f"🧭Full repository name: '{full_repo}'") - - reference_to_another_branch_regex = rf"uses:\s*{full_repo}[^@]+@(?!{branch})" - - grep_result = shell_run( - f"grep --color=always -P '{reference_to_another_branch_regex}' **/*.yml", - check=False, - cwd=inputs.actions_directory, - ) - - match grep_result.returncode: - case 0: - raise AuroraException( - f"There are references to actions within '{ inputs.actions_directory }' residing in other branches!" - ) - - case 1: - print("✅No cross-branch action references detected!") - - case _: - raise AuroraException("Error while invoking grep!") diff --git a/core/python/core/detect_branch_version.py b/core/python/core/detect_branch_version.py deleted file mode 100644 index 82d070d7f..000000000 --- a/core/python/core/detect_branch_version.py +++ /dev/null @@ -1,44 +0,0 @@ -import re -from dataclasses import dataclass -from os import getenv -from pathlib import Path - -from .exceptions import AuroraException -from .outputs import OutputDto - -leading_v = re.compile("^v") - - -@dataclass(frozen=True) -class BranchVersionInfo(OutputDto): - branch: str - version: str - escaped_version: str - major: str - - -def detect_branch_version_info() -> BranchVersionInfo: - head_ref = getenv("GITHUB_HEAD_REF") - ref = getenv("GITHUB_REF") - - retrieved_branch = head_ref if head_ref else ref - if not retrieved_branch: - raise AuroraException("Cannot detect the branch!") - - print(f"🌳Retrieved Git branch name: '{retrieved_branch}'") - - branch = retrieved_branch.replace("refs/heads/", "") - - print(f"🌲Current Git branch: '{branch}'") - - version = leading_v.sub("", Path(branch).name) - print(f"🦋Detected version: '{version}'") - - escaped_version = re.escape(version) - print(f"🧵Escaped version: '{escaped_version}'") - - major = version.split(".")[0].split("-")[0].split("+")[0] - - return BranchVersionInfo( - branch=branch, version=version, escaped_version=escaped_version, major=major - ) diff --git a/core/python/core/directories.py b/core/python/core/directories.py deleted file mode 100644 index 83d8ccd52..000000000 --- a/core/python/core/directories.py +++ /dev/null @@ -1,14 +0,0 @@ -from contextlib import contextmanager -from os import chdir, getcwd - - -@contextmanager -def switch_to(target_path): - previous_path = getcwd() - - chdir(target_path) - - try: - yield - finally: - chdir(previous_path) diff --git a/core/python/core/enforce_branch_version.py b/core/python/core/enforce_branch_version.py deleted file mode 100644 index 401d2c328..000000000 --- a/core/python/core/enforce_branch_version.py +++ /dev/null @@ -1,91 +0,0 @@ -from pathlib import Path -from typing import Optional - -from .detect_branch_version import detect_branch_version_info -from .exceptions import AuroraException -from .inputs import InputDto -from .projects import Project, UnknownProject, detect_project - - -class Inputs(InputDto): - mode: str - artifact_descriptor: Optional[str] - project_directory: str - - -def _detect_project(inputs: Inputs) -> Project: - project = detect_project(Path(inputs.project_directory), inputs.artifact_descriptor) - print(f"🔮Detected project: {project}") - - return project - - -def enforce_branch_version(inputs: Inputs) -> None: - match inputs.mode: - case "inject": - project = _detect_project(inputs) - strategy = lambda: _inject(project) - - case "check": - project = _detect_project(inputs) - strategy = lambda: _check(project) - - case "skip": - strategy = _skip - - case _: - raise AuroraException(f"Invalid value for 'mode' input: '{inputs.mode}'!") - - strategy() - - -def _inject(project: Project) -> None: - version = detect_branch_version_info().version - - print(f"🧬Injecting the branch version into {project}...") - - descriptor_file_content = project.descriptor_path.read_text() - - updated_content = descriptor_file_content.replace("0.0.0", version) - - project.descriptor_path.write_text(updated_content) - - print("✅Version injected!") - - project.print_descriptor() - - -def _check(project: Project) -> None: - project.print_descriptor() - - branch_version = detect_branch_version_info().version - - if isinstance(project, UnknownProject): - print(f"🌲Branch version: '{branch_version}'") - - print(f"🔎Ensuring the branch version exists in {project}...") - - version_found = branch_version in project.descriptor_path.read_text() - - if version_found: - print("✅Version found in the descriptor!") - else: - raise AuroraException( - "The branch version cannot be found in the artifact descriptor!" - ) - else: - artifact_version = project.read_version() - - print(f"🌲Branch version: '{branch_version}'") - print(f"🔎Artifact version: '{artifact_version}'") - - if branch_version == artifact_version: - print("✅The descriptor version matches the branch version!") - else: - raise AuroraException( - "The descriptor version and the branch version do not match!" - ) - - -def _skip() -> None: - print("💭Skipping branch version enforcement, as requested...") diff --git a/core/python/core/exceptions.py b/core/python/core/exceptions.py deleted file mode 100644 index 1b4297980..000000000 --- a/core/python/core/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -class AuroraException(Exception): - def __init__(self, message): - super().__init__(f"❌{message}") diff --git a/core/python/core/extract_rust_snippets.py b/core/python/core/extract_rust_snippets.py deleted file mode 100644 index 63192e824..000000000 --- a/core/python/core/extract_rust_snippets.py +++ /dev/null @@ -1,82 +0,0 @@ -import re -from pathlib import Path -from sys import exit - -from .directories import switch_to -from .exceptions import AuroraException -from .inputs import InputDto - -_snippet_pattern = re.compile(r"``rust\s*(.*?)\s*```", re.DOTALL) - -_test_bootstrap_code = """ - -#[test] -fn run_code_snippet() { - main().unwrap(); -} -""" - - -class Inputs(InputDto): - markdown_file: str - optional: str - test_filename_prefix: str - project_directory: str - - -def extract_rust_snippets(inputs: Inputs) -> None: - with switch_to(inputs.project_directory): - markdown_path = Path(inputs.markdown_file) - - _check_markdown_file( - markdown_path=markdown_path, optional=inputs.optional == "true" - ) - - _extract_snippets_to_files( - markdown_path=markdown_path, - test_filename_prefix=inputs.test_filename_prefix, - ) - - -def _check_markdown_file(markdown_path: Path, optional: bool) -> None: - if markdown_path.is_file(): - print(f"🗒️{markdown_path} found!") - return - - if optional: - print(f"💭{markdown_path} not found - cannot extract snippets.") - exit(0) - - raise AuroraException(f"Missing source Markdown file: '{markdown_path}'!") - - -def _extract_snippets_to_files(markdown_path: Path, test_filename_prefix: str) -> None: - test_directory_path = Path(test_filename_prefix).parent - print(f"📁Ensuring test directory: '{test_directory_path}'") - test_directory_path.mkdir(parents=True) - print("✅Test directory ready!") - - generated_test_paths = [] - - print("🔁Trying to extract tests from Rust snippets in Markdown...") - for index, matcher in enumerate( - re.finditer(_snippet_pattern, markdown_path.read_text()) - ): - snippet = matcher.group(1) - updated_snippet = snippet + _test_bootstrap_code - - snippet_path = Path(f"{test_filename_prefix}{index + 1}.rs") - - snippet_path.write_text(updated_snippet) - - generated_test_paths.append(snippet_path) - - if generated_test_paths: - print("🎩Process completed! Generated test files:") - - for test_path in generated_test_paths: - print(test_path) - - print("🎩🎩🎩") - else: - print("💭No snippets found in the source Markdown file...") diff --git a/core/python/core/inputs.py b/core/python/core/inputs.py deleted file mode 100644 index faf7f9b58..000000000 --- a/core/python/core/inputs.py +++ /dev/null @@ -1,45 +0,0 @@ -from os import getenv -from typing import Union, get_args, get_origin, get_type_hints - -from .exceptions import AuroraException - - -class Input: - @staticmethod - def get(input_name: str) -> str: - source_env_var = f"INPUT_{input_name.upper().replace('-', '_')}" - - input_value = getenv(source_env_var, "") - - print(f"📥{input_name}: '{input_value}'") - - return input_value - - @staticmethod - def require(input_name: str) -> str: - value = Input.get(input_name) - - if not value: - raise AuroraException(f"Missing action input: '{input_name}'!") - - return value - - -class InputDto: - @classmethod - def from_env(cls): - instance = cls() - instance.set_fields_from_env() - return instance - - def set_fields_from_env(self) -> None: - for field_name, field_type in get_type_hints(type(self)).items(): - optional = get_origin(field_type) == Union and type(None) in get_args( - field_type - ) - - value_retriever = Input.get if optional else Input.require - - input_value = value_retriever(field_name) - - setattr(self, field_name, input_value) diff --git a/core/python/core/jvm.py b/core/python/core/jvm.py deleted file mode 100644 index 87dbba9bb..000000000 --- a/core/python/core/jvm.py +++ /dev/null @@ -1,28 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - -from .exceptions import AuroraException -from .inputs import InputDto -from .outputs import OutputDto -from .projects import Project, detect_project - - -class Inputs(InputDto): - project_directory: str - - -@dataclass(frozen=True) -class Outputs(OutputDto): - build_tool: str - descriptor: str - - -def get_jvm_build_tool(inputs: Inputs) -> Outputs: - project = detect_project(Path(inputs.project_directory)) - - build_tool = project.build_tool - - if build_tool not in ["mvn", "gradle"]: - raise AuroraException("No available JVM build tool for this project!") - - return Outputs(build_tool=build_tool, descriptor=project.descriptor_name) diff --git a/core/python/core/outputs.py b/core/python/core/outputs.py deleted file mode 100644 index bf5d05056..000000000 --- a/core/python/core/outputs.py +++ /dev/null @@ -1,50 +0,0 @@ -import re -from os import getenv -from typing import Callable, Optional, get_type_hints - -from .exceptions import AuroraException - -UNDERSCORE_TO_UPPERCASE_REGEX = re.compile(r"_([^_])") - - -class OutputDto: - def write_to_github_output(self) -> None: - self._write_to_cicd_context( - "GITHUB_OUTPUT", lambda source_field: source_field.replace("_", "-") - ) - - def write_to_github_env(self) -> None: - self._write_to_cicd_context( - "GITHUB_ENV", - lambda source_field: re.sub( - UNDERSCORE_TO_UPPERCASE_REGEX, - lambda m: m.group(1).upper(), - source_field, - ), - ) - - def _write_to_cicd_context( - self, - context_name: str, - to_context_field_name_converter: Callable[[str], str], - ) -> None: - source_fields = get_type_hints(type(self)).keys() - - env_file = getenv(context_name) - if not env_file: - raise AuroraException( - f"Cannot find a file associated with the '{context_name}' context" - ) - - print(f"✒️Writing fields in the '{context_name}' context...") - with open(env_file, "a") as github_context: - for source_field in source_fields: - context_field_name = to_context_field_name_converter(source_field) - context_field_value = getattr(self, source_field) - - print( - f"📤Setting context name '{context_field_name}' = '{context_field_value}'" - ) - - github_context.write(f"{context_field_name}={context_field_value}\n") - print(f"✒️✒️✒️") diff --git a/core/python/core/parse_npm_scope.py b/core/python/core/parse_npm_scope.py deleted file mode 100644 index ef9491e53..000000000 --- a/core/python/core/parse_npm_scope.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass - -from .inputs import InputDto -from .outputs import OutputDto - - -class Inputs(InputDto): - scope: str - - -@dataclass(frozen=True) -class ScopeResult(OutputDto): - actual_scope: str - - -def parse_npm_scope(inputs: Inputs) -> ScopeResult: - if inputs.scope == "": - print("🫚Root npm scope detected!") - return ScopeResult(actual_scope="") - - actual_scope = inputs.scope.lstrip("@") - - print(f"🖌️Custom npm scope detected: '{actual_scope}'") - - return ScopeResult(actual_scope=actual_scope) diff --git a/core/python/core/processes.py b/core/python/core/processes.py deleted file mode 100644 index dd5e0fbf9..000000000 --- a/core/python/core/processes.py +++ /dev/null @@ -1,22 +0,0 @@ -import subprocess -import sys -from typing import Union - - -def shell_run(command: Union[str, list[str]], **kwargs) -> subprocess.CompletedProcess: - print(f"🚀Running shell command: {command}") - - kwargs["shell"] = True - - if not kwargs.get("capture_output"): - sys.stdout.flush() - sys.stderr.flush() - - return subprocess.run(command, **kwargs) - - -def text_run(command: Union[str, list[str]], **kwargs) -> subprocess.CompletedProcess: - kwargs["text"] = True - kwargs["capture_output"] = True - - return shell_run(command, **kwargs) diff --git a/core/python/core/projects/__init__.py b/core/python/core/projects/__init__.py deleted file mode 100644 index 4ec8c4d64..000000000 --- a/core/python/core/projects/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .base import Project -from .detect import detect_project -from .unknown import UnknownProject diff --git a/core/python/core/projects/base.py b/core/python/core/projects/base.py deleted file mode 100644 index 4fa56a172..000000000 --- a/core/python/core/projects/base.py +++ /dev/null @@ -1,62 +0,0 @@ -from abc import ABCMeta, abstractmethod -from pathlib import Path -from typing import Optional - -from ..exceptions import AuroraException - - -class Project(metaclass=ABCMeta): - def __init__(self, directory: Path): - self._directory = directory - - @property - def directory(self) -> Path: - return self._directory - - @property - @abstractmethod - def technology(self) -> str: - pass - - @property - @abstractmethod - def emoji(self) -> str: - pass - - @property - @abstractmethod - def descriptor_name(self) -> str: - pass - - @property - def descriptor_path(self) -> Path: - return self.directory / self.descriptor_name - - def print_descriptor(self) -> None: - print(f"{self.emoji}{self.descriptor_name} content:") - self._print_descriptor_content() - print(self.emoji * 3) - - def _print_descriptor_content(self) -> None: - descriptor_content = self.descriptor_path.read_text() - print(descriptor_content) - - def read_version(self) -> str: - version = self._read_version() - - if not version: - raise AuroraException(f"Cannot detect version for {self}") - - return version - - @abstractmethod - def _read_version(self) -> Optional[str]: - pass - - @property - @abstractmethod - def build_tool(self) -> str: - pass - - def __str__(self): - return f"{self.emoji}{self.technology}({self.descriptor_name})" diff --git a/core/python/core/projects/detect.py b/core/python/core/projects/detect.py deleted file mode 100644 index 80f5b453f..000000000 --- a/core/python/core/projects/detect.py +++ /dev/null @@ -1,69 +0,0 @@ -from pathlib import Path -from typing import Optional - -from ..exceptions import AuroraException -from .base import Project - -NODEJS_DESCRIPTOR_NAME = "package.json" - -RUST_DESCRIPTOR_NAME = "Cargo.toml" - -MAVEN_DESCRIPTOR_NAME = "pom.xml" - -GRADLE_DESCRIPTOR_NAMES = ["build.gradle", "build.gradle.kts"] - -PYTHON_DESCRIPTOR_NAME = "pyproject.toml" - - -def detect_project( - project_directory: Path, descriptor_name: Optional[str] = None -) -> Project: - if ( - descriptor_name == NODEJS_DESCRIPTOR_NAME - or (project_directory / NODEJS_DESCRIPTOR_NAME).is_file() - ): - from .nodejs import NodeJsProject - - return NodeJsProject(project_directory) - - if ( - descriptor_name == RUST_DESCRIPTOR_NAME - or (project_directory / RUST_DESCRIPTOR_NAME).is_file() - ): - from .rust import RustProject - - return RustProject(project_directory) - - if ( - descriptor_name == MAVEN_DESCRIPTOR_NAME - or (project_directory / MAVEN_DESCRIPTOR_NAME).is_file() - ): - from .maven import MavenProject - - return MavenProject(project_directory) - - for gradle_descriptor_name in GRADLE_DESCRIPTOR_NAMES: - if ( - descriptor_name == gradle_descriptor_name - or (project_directory / gradle_descriptor_name).is_file() - ): - from .gradle import GradleProject - - return GradleProject(project_directory, gradle_descriptor_name) - - if ( - descriptor_name == PYTHON_DESCRIPTOR_NAME - or (project_directory / PYTHON_DESCRIPTOR_NAME).is_file() - ): - from .python import PythonProject - - return PythonProject(project_directory) - - from .unknown import UnknownProject - - if not descriptor_name: - raise AuroraException( - "Descriptor file name must be specified for unknown technology!" - ) - - return UnknownProject(project_directory, descriptor_name) diff --git a/core/python/core/projects/gradle.py b/core/python/core/projects/gradle.py deleted file mode 100644 index 250b2c793..000000000 --- a/core/python/core/projects/gradle.py +++ /dev/null @@ -1,39 +0,0 @@ -import re -from pathlib import Path -from typing import Optional - -from .base import Project - - -class GradleProject(Project): - _version_regex = re.compile(r"""^version\s*=\s*["'](.*)["']""") - - def __init__(self, directory: Path, descriptor_name: str): - super().__init__(directory) - self._descriptor_name = descriptor_name - - @property - def technology(self) -> str: - return "Gradle" - - @property - def emoji(self) -> str: - return "🐘" - - @property - def descriptor_name(self) -> str: - return self._descriptor_name - - @property - def build_tool(self) -> str: - return "gradle" - - def _read_version(self) -> Optional[str]: - with self.descriptor_path.open() as descriptor: - for line in descriptor: - matcher = self._version_regex.match(line) - - if matcher: - return matcher.group(1) - - return None diff --git a/core/python/core/projects/maven.py b/core/python/core/projects/maven.py deleted file mode 100644 index f79456f3c..000000000 --- a/core/python/core/projects/maven.py +++ /dev/null @@ -1,34 +0,0 @@ -import xml.etree.ElementTree as ET -from typing import Optional - -from ..exceptions import AuroraException -from .base import Project - - -class MavenProject(Project): - @property - def technology(self) -> str: - return "Maven" - - @property - def emoji(self) -> str: - return "🪶" - - @property - def descriptor_name(self) -> str: - return "pom.xml" - - @property - def build_tool(self) -> str: - return "mvn" - - def _read_version(self) -> Optional[str]: - tree = ET.parse(self.descriptor_path) - root = tree.getroot() - namespaces = {"mvn": root.tag.split("}")[0].strip("{}")} - - version_tag = root.find(".//mvn:version", namespaces) - if version_tag is None: - raise AuroraException("Cannot find the tag!") - - return version_tag.text diff --git a/core/python/core/projects/nodejs.py b/core/python/core/projects/nodejs.py deleted file mode 100644 index 2c0604828..000000000 --- a/core/python/core/projects/nodejs.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import sys -from subprocess import run -from typing import Optional - -from .base import Project - - -class NodeJsProject(Project): - @property - def technology(self) -> str: - return "NodeJS" - - @property - def emoji(self) -> str: - return "📦" - - @property - def descriptor_name(self) -> str: - return "package.json" - - def _read_version(self) -> Optional[str]: - descriptor_json = json.loads(self.descriptor_path.read_text()) - - return descriptor_json["version"] - - def _print_descriptor_content(self): - sys.stdout.flush() - sys.stderr.flush() - run(["jq", "-C", ".", self.descriptor_path]) - - @property - def build_tool(self) -> str: - return "pnpm" diff --git a/core/python/core/projects/python.py b/core/python/core/projects/python.py deleted file mode 100644 index d0a3e7425..000000000 --- a/core/python/core/projects/python.py +++ /dev/null @@ -1,22 +0,0 @@ -import re -from typing import Optional - -from .toml import TomlProject - - -class PythonProject(TomlProject): - @property - def technology(self) -> str: - return "Python" - - @property - def emoji(self) -> str: - return "🐍" - - @property - def descriptor_name(self) -> str: - return "pyproject.toml" - - @property - def build_tool(self) -> str: - return "pdm" diff --git a/core/python/core/projects/rust.py b/core/python/core/projects/rust.py deleted file mode 100644 index 31eac4c3f..000000000 --- a/core/python/core/projects/rust.py +++ /dev/null @@ -1,22 +0,0 @@ -import re -from typing import Optional - -from .toml import TomlProject - - -class RustProject(TomlProject): - @property - def technology(self) -> str: - return "Rust" - - @property - def emoji(self) -> str: - return "🦀" - - @property - def descriptor_name(self) -> str: - return "Cargo.toml" - - @property - def build_tool(self) -> str: - return "cargo" diff --git a/core/python/core/projects/toml.py b/core/python/core/projects/toml.py deleted file mode 100644 index fad3dd87a..000000000 --- a/core/python/core/projects/toml.py +++ /dev/null @@ -1,18 +0,0 @@ -import re -from typing import Optional - -from .base import Project - - -class TomlProject(Project): - _version_regex = re.compile(r"""^version\s*=\s*["'](.*)["']""") - - def _read_version(self) -> Optional[str]: - with self.descriptor_path.open() as descriptor: - for line in descriptor: - matcher = self._version_regex.match(line) - - if matcher: - return matcher.group(1) - - return None diff --git a/core/python/core/projects/unknown.py b/core/python/core/projects/unknown.py deleted file mode 100644 index 58d169651..000000000 --- a/core/python/core/projects/unknown.py +++ /dev/null @@ -1,31 +0,0 @@ -from pathlib import Path -from typing import Optional - -from ..exceptions import AuroraException -from .base import Project - - -class UnknownProject(Project): - def __init__(self, directory: Path, descriptor_name: str): - super().__init__(directory) - - self._descriptor_name = descriptor_name - - @property - def technology(self) -> str: - return "Unknown" - - @property - def emoji(self) -> str: - return "🎁" - - @property - def descriptor_name(self) -> str: - return self._descriptor_name - - def _read_version(self) -> Optional[str]: - return None - - @property - def build_tool(self) -> str: - raise AuroraException("No build tool for unknown project") diff --git a/core/python/pdm.elv b/core/python/pdm.elv new file mode 100644 index 000000000..740a8ffe9 --- /dev/null +++ b/core/python/pdm.elv @@ -0,0 +1,27 @@ +use re +use github.com/giancosta86/aurora-elvish/console +use ./pipx + +fn -install { |version| + pipx:install-package &version=$version pdm +} + +fn ensure { |&version=$nil| + if (not (has-external pdm)) { + console:echo 💬 pdm is not available: now installing it! + -install $version + return + } + + console:echo 🌟 pdm is already installed! + + var installed-version = (pdm --version) + console:inspect 'Installed pdm version' $installed-version + + if (and $version (re:match '\b'$version'\b' installed-version )) { + console:echo ✅ The requested pdm version is already installed! + return + } + + -install $version +} \ No newline at end of file diff --git a/core/python/pipx.elv b/core/python/pipx.elv new file mode 100644 index 000000000..847f7c7a1 --- /dev/null +++ b/core/python/pipx.elv @@ -0,0 +1,19 @@ +use github.com/giancosta86/aurora-elvish/console + +fn install-package { |package &version=$nil| + console:echo 📥 Installing $package via pipx... + + var version-suffix + + if $version { + console:inspect &emoji=🏷 'Requested version' $version + set version-suffix = '=='$version + } else { + console:echo 🌟 Installing the latest version + set version-suffix = '' + } + + pipx install $package''$version-suffix + + console:echo ✅ $package installed! +} \ No newline at end of file diff --git a/core/python/project.elv b/core/python/project.elv new file mode 100644 index 000000000..cd9a1e0aa --- /dev/null +++ b/core/python/project.elv @@ -0,0 +1,23 @@ +use github.com/giancosta86/aurora-elvish/console + +fn verify { + console:echo 🔬 Verifying the project... + pdm run verify + console:echo ✅ Project verified! +} + +fn build { + console:echo 📦 Building the project... + pdm build + console:echo ✅ Project built successfully! +} + +fn publish { |dry-run| + if $dry-run { + console:echo 💭 dry-run is enabled: just building the 🐍 Python project... + pdm build + } else { + console:echo 📤 Publishing the 🐍 Python package... + pdm publish + } +} \ No newline at end of file diff --git a/core/rust/context.elv b/core/rust/context.elv new file mode 100644 index 000000000..2680a529d --- /dev/null +++ b/core/rust/context.elv @@ -0,0 +1,72 @@ +use os +use ../ci-cd/env +use github.com/giancosta86/aurora-elvish/command +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang + +fn -check-preconditions { + if (not (os:is-regular Cargo.toml)) { + fail 'The Cargo.toml descriptor is missing!' + } + + if (not (has-external cargo)) { + fail 'Some version of Cargo must be installed!' + } + + if (not (has-external rustup)) { + fail 'Some version of rustup must be installed!' + } +} + +fn -set-cargo-colors { |enabled| + var key = CARGO_TERM_COLOR + var value = (lang:ternary $enabled always never) + + set-env $key $value + env:write $key $value +} + +fn -check-toolchain-file { + var toolchain-file = rust-toolchain.toml + + if (os:is-regular $toolchain-file) { + console:inspect &emoji=✅ 'Toolchain file found' $toolchain-file + } else { + fail "Missing toolchain file: '"$toolchain-file"'" + } +} + +fn -ensure-required-components { + command:silence { + cargo --version + + rustup component add rustfmt clippy + } +} + +fn -print-component-versions { + console:section &emoji=🦀 'Rust component versions' { + cargo --version + rustc --version + cargo fmt --version + cargo clippy --version + } +} + +fn setup { |&cargo-colors=$true &check-toolchain-file=$true| + console:echo 🦀💻 Setting up Rust context in "'"$pwd"'"... + + -check-preconditions + + -set-cargo-colors $cargo-colors + + if $check-toolchain-file { + -check-toolchain-file + } + + -ensure-required-components + + -print-component-versions + + console:echo ✅🦀 Rust context in "'"$pwd"'" ready! +} diff --git a/core/rust/crate.elv b/core/rust/crate.elv new file mode 100644 index 000000000..a2a4d8887 --- /dev/null +++ b/core/rust/crate.elv @@ -0,0 +1,7 @@ +use github.com/giancosta86/aurora-elvish/lang + +fn publish { |dry-run| + var dry-run-arg = (lang:ternary $dry-run [--dry-run] []) + + cargo publish --all-features --allow-dirty $@dry-run-arg +} \ No newline at end of file diff --git a/core/rust/project.elv b/core/rust/project.elv new file mode 100644 index 000000000..32add2e4d --- /dev/null +++ b/core/rust/project.elv @@ -0,0 +1,76 @@ +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/edit + +fn -check-format { + console:echo 🎨 Checking source code format... + cargo fmt --check + console:echo ✅ Source code format OK! +} + +fn -run-clippy-checks { + console:echo 📎 Running clippy checks... + cargo clippy --all-targets --all-features -- -D warnings + console:echo ✅ Clippy checks OK! +} + +fn -check-rustdoc { + console:echo 📚 Building rustdoc documentation with all the features enabled... + + tmp E:RUSTDOCFLAGS = '-D warnings' + + cargo doc --all-features + + console:echo ✅ Documentation built successfully! +} + +fn check-style { |&run-clippy-checks=$true &check-rustdoc=$true| + -check-format + + if $run-clippy-checks { + -run-clippy-checks + } + + if $check-rustdoc { + -check-rustdoc + } +} + +fn -run-vanilla-tests { + console:echo 🔬 Running tests with no features enabled... + cargo test + console:echo ✅ Tests with no features OK! +} + +fn -run-tests-with-all-features { + console:echo 🔬 Running tests with all the features enabled... + cargo test --all-features + console:echo ✅ Tests with all the features OK! +} + +fn run-tests { + -run-vanilla-tests + + -run-tests-with-all-features +} + +fn document-all-features { + edit:file Cargo.toml { |content| + var docs-header = '[package.metadata.docs.rs]' + + if (str:contains $content $docs-header) { + console:echo 💬 Skipping documentation addendum because $docs-header already appears in Cargo.toml... + + put $nil + } else { + console:echo 📚 Now adding the "'"all-features = true"'" documentation addendum to the project descriptor! + + var descriptor-addendum = (str:join "\n" [ + $docs-header + 'all-features = true' + ]) + + put (str:trim-space $content)"\n\n"$descriptor-addendum + } + } +} \ No newline at end of file diff --git a/core/rust/snippets.elv b/core/rust/snippets.elv new file mode 100644 index 000000000..18d8720da --- /dev/null +++ b/core/rust/snippets.elv @@ -0,0 +1,82 @@ +use os +use path +use re +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/seq + +var -snippet-pattern = '(?s)```rust\s+(.*?)```' + +fn -get-test-bootstrap-code { |ordinal| + str:join "\n" [ + '#[test]' + 'fn run_code_snippet_'$ordinal'() {' + ' main().unwrap();' + '}' + ] +} + +fn -ensure-test-directory { |snippet-path| + var test-directory = (path:dir $snippet-path) + + if (not (os:is-dir $test-directory)) { + console:inspect &emoji=📁 'Creating test directory' $test-directory + os:mkdir-all $test-directory + console:echo ✅ Test directory ready! + } +} + +fn -extract-snippets-to-files { |markdown-path test-filename-prefix| + var test-directory = (path:dir $test-filename-prefix) + var generated-test-paths = [] + + console:echo 🎩 Trying to extract tests from Rust snippets in Markdown... + + slurp < $markdown-path | re:find $-snippet-pattern (all) | seq:enumerate { |index match| + var snippet = (str:trim-space $match[groups][1][text]) + + var ordinal = (+ $index 1) + + var test-bootstrap-code = (-get-test-bootstrap-code $ordinal) + + var updated-snippet = $snippet"\n\n"$test-bootstrap-code + + var snippet-path = $test-filename-prefix''$ordinal'.rs' + + -ensure-test-directory $snippet-path + + echo $updated-snippet > $snippet-path + + set generated-test-paths = [$@generated-test-paths $snippet-path] + } + + if (seq:is-non-empty $generated-test-paths) { + console:section &emoji=🎩 'Process completed! Generated test files' { + for test-path $generated-test-paths { + console:echo 📄 $test-path + } + } + } else { + console:echo 💭 No snippets found in the source Markdown file... + } +} + +fn extract { |inputs| + console:inspect-inputs $inputs + + var markdown-path = $inputs[markdown-path] + var optional = $inputs[optional] + var test-filename-prefix = $inputs[test-filename-prefix] + + if $markdown-path { + console:inspect &emoji=🗒️ 'Source markdown found' $markdown-path + + -extract-snippets-to-files $markdown-path $test-filename-prefix + } else { + if $optional { + console:echo 💭 Source markdown path not found - cannot extract snippets + } else { + fail 'Missing source Markdown file!' + } + } +} \ No newline at end of file diff --git a/core/rust/wasm-pack.elv b/core/rust/wasm-pack.elv new file mode 100644 index 000000000..6a2207da8 --- /dev/null +++ b/core/rust/wasm-pack.elv @@ -0,0 +1,98 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/edit +use github.com/giancosta86/aurora-elvish/lang +use ../nodejs/pnpm +use ../project/descriptors/json + +fn install { |version| + console:echo 🌐 Installing wasm-pack $version... + + npm install -g 'wasm-pack@'$version + + console:echo ✅ wasm-pack installed! +} + +fn -run { |inputs| + console:echo 📦 Generating the WebAssembly project files... + + var target = $inputs[target] + var npm-scope = $inputs[npm-scope] + var development = $inputs[development] + var target-directory = $inputs[target-directory] + + var mode-arg = ( + lang:ternary $development '--dev' '--release' + ) + + var actual-npm-scope = (pnpm:parse-scope $npm-scope) + + var npm-scope-args = ( + lang:ternary $actual-npm-scope ['--scope' $actual-npm-scope] [] + ) + + wasm-pack build --target $target $mode-arg $@npm-scope-args --out-dir $target-directory +} + +fn -inject-nodejs-version { |nodejs-version| + if (not (os:is-regular package.json)) { + fail 'package.json was not generated for this target - cannot inject the requested NodeJS version!' + } + + console:inspect &emoji=🧬 'Injecting the requested NodeJS version' $nodejs-version + + edit:json package.json '.engines.node = "'$nodejs-version'"' +} + +fn -inject-pnpm-version { |pnpm-version| + if (not (os:is-regular package.json)) { + fail 'package.json was not generated for this target - cannot inject the requested pnpm version!' + } + + console:inspect &emoji=🧬 'Injecting the requested pnpm version' $pnpm-version + + edit:json package.json '.packageManager = "pnpm@'$pnpm-version'"' +} + +fn -try-to-display-package-json { |target| + if (os:is-regular package.json) { + console:section &emoji=📦 'Generated package.json descriptor for the '''$target''' target' { + json:print-content package.json + } + } else { + console:echo 💭 No package.json descriptor generated for the "'"$target"'" target... + } +} + +fn run-browser-tests { + console:echo 🌐 Running headless browser tests... + + wasm-pack test --chrome --headless --release + + console:echo ✅ Headless browser tests OK! +} + +fn generate-target { |inputs| + console:inspect-inputs $inputs + + -run $inputs + + var target = $inputs[target] + var nodejs-version = $inputs[nodejs-version] + var pnpm-version = $inputs[pnpm-version] + var target-directory = $inputs[target-directory] + + tmp pwd = $target-directory + + if $nodejs-version { + -inject-nodejs-version $nodejs-version + } + + if $pnpm-version { + -inject-pnpm-version $pnpm-version + } + + -try-to-display-package-json $target + + console:inspect &emoji=✅ 'WebAssembly target ready in' $target-directory +} \ No newline at end of file diff --git a/core/rust/wasm.elv b/core/rust/wasm.elv new file mode 100644 index 000000000..1b11fcc0d --- /dev/null +++ b/core/rust/wasm.elv @@ -0,0 +1,12 @@ +use os +use github.com/giancosta86/aurora-elvish/console + +fn copy-npmrc-from-project-directory { + if (os:is-regular .npmrc) { + console:echo 🎉 Root .npmrc configuration file found! Copying it to the package directory... + cp .npmrc pkg/ + console:echo ✅ .npmrc file copied! + } else { + console:echo 💭 No .npmrc configuration file found in the project directory... + } +} diff --git a/core/system-packages.elv b/core/system-packages.elv new file mode 100644 index 000000000..3ff91bc6c --- /dev/null +++ b/core/system-packages.elv @@ -0,0 +1,66 @@ +use os +use github.com/giancosta86/aurora-elvish/command +use github.com/giancosta86/aurora-elvish/console + +fn -should-run-installer { |required-command| + if $required-command { + console:inspect &emoji=📥 'Required command' $required-command + + if (command:exists-in-bash $required-command) { + console:echo ✅ Required command available - no need to install it! + put $false + return + } else { + console:echo 💬 Required command not available - now installing its packages... + } + } else { + console:echo 💫 No required command passed - the requested packages will be installed unconditionally... + } + + put $true +} + +fn -run-initial-update { + var flag-file = ~/.install-system-packages-updated + + if (os:is-regular $flag-file) { + console:echo 💡 The package list has already been updated! + return + } + + console:echo 📥 Updating the package list... + + command:silence-until-error { + sudo apt-get update + } + + touch $flag-file +} + +fn -run-installer { |requested-packages| + console:echo 📦 Installing packages... + + command:silence-until-error { + sudo apt-get install -y $@requested-packages + } + + console:echo ✅ Packages installed! +} + +fn install { |inputs| + console:inspect-inputs $inputs + + var packages = $inputs[packages] + var required-command = $inputs[required-command] + var initial-update = $inputs[initial-update] + + if (not (-should-run-installer $required-command)) { + return + } + + if $initial-update { + -run-initial-update + } + + -run-installer $packages +} diff --git a/core/tag-and-release.elv b/core/tag-and-release.elv new file mode 100644 index 000000000..b6582bd4b --- /dev/null +++ b/core/tag-and-release.elv @@ -0,0 +1,87 @@ +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/semver +use ./branch-version/detection +use ./ci-cd/pull-request +use ./git +use ./tag-and-release/git-log +use ./tag-and-release/major-tag +use ./tag-and-release/preconditions +use ./tag-and-release/release + +fn -get-actual-draft-release { |draft-release semantic-version| + if (==s $draft-release true) { + put $true + return + } + + if (==s $draft-release false) { + put $false + return + } + + semver:is-new-major $semantic-version +} + +fn run-action { |inputs| + console:inspect-inputs $inputs + + var draft-release = $inputs[draft-release] + var notes-file-processor = $inputs[notes-file-processor] + var set-major-tag = $inputs[set-major-tag] + var dry-run = $inputs[dry-run] + var git-strategy = $inputs[git-strategy] + + preconditions:check $dry-run + + var version-info = (detection:detect) + var version = $version-info[version] + var branch = $version-info[branch] + var major = $version-info[major] + var semantic-version = $version-info[semantic-version] + + var pull-request = (pull-request:fetch-info $branch) + console:inspect &emoji=🔁 'Pull request' $pull-request + + git:hard-reset + + git-log:fetch $pull-request + + git:fetch-tags + + if (not $dry-run) { + pull-request:merge $branch $git-strategy + } else { + console:echo 💭 Just simulating pull request merging, in dry-run mode... + } + + var tag = v$version + console:inspect &emoji=📌 'Tag to create' $tag + + if (not $dry-run) { + git:create-and-push-tag $tag + } else { + console:echo 💭 Just simulating Git tag creation, in dry-run mode... + } + + release:create [ + &tag=$tag + &version=$version + &dry-run=$dry-run + &draft-release=(-get-actual-draft-release $draft-release $semantic-version) + ¬es-file-processor=$notes-file-processor + &pull-request=$pull-request + ] + + var major-tag + if $set-major-tag { + set major-tag = (major-tag:create $major $dry-run) + } else { + console:echo 💬 Skipping major tag, as requested + set major-tag = $nil + } + + put [ + &tag=$tag + &major-tag=$major-tag + ] +} \ No newline at end of file diff --git a/core/tag-and-release/git-log.elv b/core/tag-and-release/git-log.elv new file mode 100644 index 000000000..087f0302f --- /dev/null +++ b/core/tag-and-release/git-log.elv @@ -0,0 +1,22 @@ +use github.com/giancosta86/aurora-elvish/console +use ../ci-cd/pull-request +use ../git + +fn fetch { |pull-request| + var branch = $pull-request[branch] + var base-sha = $pull-request[base-sha] + var head-sha = $pull-request[head-sha] + + if (pull-request:triggers-current-workflow) { + console:echo 📥 Fetching Git log within a pull request workflow... + + git fetch origin $base-sha $head-sha + } else { + console:echo 📥 Fetching Git log not within a pull request workflow... + + git:fetch-branched-sha $branch $head-sha + git:fetch-branched-sha $branch $base-sha + } + + console:echo ✅ Git log ready! +} \ No newline at end of file diff --git a/core/tag-and-release/major-tag.elv b/core/tag-and-release/major-tag.elv new file mode 100644 index 000000000..d5b982d89 --- /dev/null +++ b/core/tag-and-release/major-tag.elv @@ -0,0 +1,18 @@ +use github.com/giancosta86/aurora-elvish/console +use ../git + +fn create { |major dry-run| + var major-tag = v$major + + console:inspect &emoji=🪩 'Setting major version tag' $major-tag + + if (not $dry-run) { + git:create-and-push-tag &force $major-tag + + console:echo 🪩 Major version tag set! + } else { + console:echo 💭 Just simulating major version tag creation, in dry-run mode... + } + + put $major-tag +} \ No newline at end of file diff --git a/core/tag-and-release/preconditions.elv b/core/tag-and-release/preconditions.elv new file mode 100644 index 000000000..5b69cceaf --- /dev/null +++ b/core/tag-and-release/preconditions.elv @@ -0,0 +1,19 @@ +use github.com/giancosta86/aurora-elvish/console +use ../ci-cd/pull-request + +fn -check-trigger { |dry-run| + var triggered-by-pull-request = (pull-request:triggers-current-workflow) + console:inspect 'Triggered by pull request' $triggered-by-pull-request + + if $triggered-by-pull-request { + if $dry-run { + console:echo ⛵ Since dry-run is enabled, the action can be run in a pull-request-triggered workflow + } else { + fail 'This action can be run from a workflow triggered by a pull-request only when dry-run is enabled' + } + } +} + +fn check { |dry-run| + -check-trigger $dry-run +} diff --git a/core/tag-and-release/release-notes.elv b/core/tag-and-release/release-notes.elv new file mode 100644 index 000000000..8073c5cf5 --- /dev/null +++ b/core/tag-and-release/release-notes.elv @@ -0,0 +1,80 @@ +use file +use re +use str +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/seq +use ../ci-cd/repository + +fn -write-commit-list { |pull-request| + var base-sha = $pull-request[base-sha] + var head-sha = $pull-request[head-sha] + + var marker = '§+-+§' + + var commit-list = (git log --no-merges --reverse --pretty=format:$marker'* %B' $base-sha'..'$head-sha | + slurp | + re:replace '(?m)^' ' ' (all) | + str:replace ' '$marker '' (all) | + str:trim-space (all) + ) + + echo $commit-list +} + +fn -write-pull-request-data { |pull-request| + var title = $pull-request[title] + var number = $pull-request[number] + + echo '**Pull request**: '$title' (#'$number')' +} + +fn -write-changelog-footer { |pull-request tag| + var base-sha = $pull-request[base-sha] + + var most-specific-base-tag = ( + git tag --points-at $base-sha | + order &key={ |tag| count $tag } &reverse | + take 1 | + lang:ensure-put + ) + + var base-reference + + if $most-specific-base-tag { + console:echo 📌 Tag "'"$most-specific-base-tag"'" found for the "'"$base-sha"'" base SHA + set base-reference = $most-specific-base-tag + } else { + console:echo 💭 No tags associated with the "'"$base-sha"'" base SHA - using it directly + set base-reference = $base-sha + } + + console:inspect &emoji=🧭 'Base reference' $base-reference + console:inspect &emoji=📌 'Release tag' $tag + + echo '**Full changelog**: '(repository:get-changelog $base-reference $tag) +} + +fn generate { |inputs| + console:inspect-inputs $inputs + + var output-file = $inputs[output-file] + var tag = $inputs[tag] + var pull-request = $inputs[pull-request] + + { + -write-commit-list $pull-request + + echo '---' + + -write-pull-request-data $pull-request + + -write-changelog-footer $pull-request $tag + } >> $output-file + + file:close $output-file + + console:section &emoji=📝 'Generated release notes' { + cat $output-file[name] + } +} \ No newline at end of file diff --git a/core/tag-and-release/release.elv b/core/tag-and-release/release.elv new file mode 100644 index 000000000..c2e23ddd7 --- /dev/null +++ b/core/tag-and-release/release.elv @@ -0,0 +1,74 @@ +use os +use path +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/script +use ../ci-cd/repository +use ./release-notes + +fn -get-release-title { |version| + var repository-full-name = (repository:get-full-name) + + var repository-basename = (path:base $repository-full-name) + console:inspect &emoji=🧭 'Repository basename' $repository-basename + + put $repository-basename' '$version +} + +fn create { |inputs| + console:inspect-inputs $inputs + + var tag = $inputs[tag] + var version = $inputs[version] + var dry-run = $inputs[dry-run] + var draft-release = $inputs[draft-release] + var notes-file-processor = $inputs[notes-file-processor] + var pull-request = $inputs[pull-request] + + var release-title = (-get-release-title $version) + console:inspect &emoji=🔎 'Release title' $release-title + + var release-notes-file = (os:temp-file) + defer { + os:remove $release-notes-file[name] + } + + release-notes:generate [ + &output-file=$release-notes-file + &tag=$tag + &pull-request=$pull-request + ] + + if $notes-file-processor { + console:inspect &emoji=🖋 'Release notes file processor' $notes-file-processor + + script:run $notes-file-processor $release-notes-file[name] + + console:section &emoji=🎀 'Processed release notes' { + cat $release-notes-file[name] + } + } else { + console:echo 💭 No release notes file processor... + } + + if $draft-release { + console:inspect &emoji=📝 'Drafting release' $release-title + + if (not $dry-run) { + gh release create $tag --title $release-title --latest --notes-file $release-notes-file[name] --draft + + console:echo 📝 Release drafted! + } else { + console:echo 💭 Just simulating draft release creation, in dry-run mode... + } + } else { + console:inspect &emoji=🌟 'Publishing release' $release-title + + if (not $dry-run) { + gh release create $tag --title $release-title --latest --notes-file $release-notes-file[name] + + console:echo 🌟 Release published! + } else { + console:echo 💭 Just simulating release publication, in dry-run mode... + } + } +} \ No newline at end of file diff --git a/core/website.elv b/core/website.elv new file mode 100644 index 000000000..000694d4c --- /dev/null +++ b/core/website.elv @@ -0,0 +1,84 @@ +use os +use path +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/seq +use ./ci-cd/env + +fn -set-strategy { |strategy| + console:inspect &emoji=💡 'Current website publication strategy' $strategy + env:write strategy $strategy +} + +fn -enforce-exit-strategy { + -set-strategy exit +} + +fn detect-strategy-from-sources { |source-directory| + if (not $source-directory) { + -enforce-exit-strategy + return + } + + console:inspect &emoji=🌐📁 'Website source directory' $source-directory + + tmp pwd = $source-directory + + if (os:is-regular package.json) { + -set-strategy nodejs + } elif (os:is-regular pom.xml) { + -set-strategy maven + } else { + -set-strategy static-files + } +} + +fn set-artifact-directory { |@path-components| + env:write artifactDirectory (path:join $@path-components) +} + +fn build-via-nodejs { |source-directory| + tmp pwd = $source-directory + + console:echo 📦 Now building the website via NodeJS... + pnpm build + console:echo ✅ Website built! + + set-artifact-directory $source-directory dist +} + +fn build-via-maven { |source-directory| + use ./jvm/context + use ./jvm/maven + + tmp pwd = $source-directory + + context:setup + + console:echo 🪶 Now building the website via Maven... + maven:run site + console:echo ✅ Website built! + + set-artifact-directory $source-directory target site +} + +fn check-artifact-directory { |artifact-directory optional| + console:inspect &emoji=📁 'Website artifact directory' $artifact-directory + + if (os:is-dir $artifact-directory) { + console:echo ✅ The artifact directory for the 🌐 website exists! + } else { + if $optional { + console:echo 💬 Missing website artifact directory - cannot publish website + -enforce-exit-strategy + } else { + fail 'Missing website artifact directory!' + } + } +} + +fn skip-publication-on-dry-run { |dry-run| + if $dry-run { + console:echo 💭 Skipping publication, as requested by dry-run. + -enforce-exit-strategy + } +} diff --git a/lib/core.elv b/lib/core.elv deleted file mode 100644 index 446a23c07..000000000 --- a/lib/core.elv +++ /dev/null @@ -1,29 +0,0 @@ -use str -use re - -fn write-env { |key value| - echo $key'='$value >> $E:GITHUB_ENV -} - -fn write-output { |key value| - echo $key'='$value >> $E:GITHUB_OUTPUT -} - -fn parse-string-list { |string-list| - re:split ',| ' $string-list | each { |entry| - if (not-eq $entry "") { - put $entry - } - } -} - -fn string-list-to-csv { |csv-list| - parse-string-list $csv-list | str:join , -} - -fn to-sha { |source| - echo $source | - sha256sum | - str:split ' ' (all) | - take 1 -} diff --git a/lib/startup-libs.elv b/lib/startup-libs.elv deleted file mode 100644 index af2b8afa3..000000000 --- a/lib/startup-libs.elv +++ /dev/null @@ -1,36 +0,0 @@ -use epm -use str -use ./core - -fn setup-env-vars { |inputs| - echo 🔎EPM managed directory: "'"$epm:managed-dir"'" - - var comma-separated-packages = (core:string-list-to-csv $inputs[packages]) - echo 🔎Comma-separated packages: "'"$comma-separated-packages"'" - - var packages-sha = (core:to-sha $comma-separated-packages) - echo 🔎Packages SHA: $packages-sha - - var epm-cache-key = $inputs[workflow]-$inputs[run-number]-$packages-sha - echo 🔎Epm cache key: "'"$epm-cache-key"'" - - core:write-env epm-dir $epm:managed-dir - core:write-env comma-separated-packages $comma-separated-packages - core:write-env epm-cache-key $epm-cache-key -} - -fn install { |comma-separated-packages| - str:split , $comma-separated-packages | each { |pkg| - epm:install $pkg - } - - echo 🚀Startup packages for Elvish installed! -} - -fn list { - echo 📚Elvish startup packages: - epm:installed | each { |pkg| - echo '*' $pkg - } - echo 📚📚📚 -} \ No newline at end of file From 20d5909ea5c555310626da93e8ba90c78479097a Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 20:04:23 +0200 Subject: [PATCH 07/63] Refactor `detect-branch-version` --- .../test-detect-branch-version/action.yml | 39 +++++++------------ actions/detect-branch-version/README.md | 12 +++--- actions/detect-branch-version/action.yml | 22 +++++------ 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/.github/test-actions/test-detect-branch-version/action.yml b/.github/test-actions/test-detect-branch-version/action.yml index dbb098b95..65b9a47f7 100644 --- a/.github/test-actions/test-detect-branch-version/action.yml +++ b/.github/test-actions/test-detect-branch-version/action.yml @@ -13,25 +13,9 @@ runs: if [[ -n "$branch" ]] then - echo "🌲Branch retrieved from action: '$branch'" + echo "✅ Branch retrieved from action: '$branch'" else - echo "❌The branch could not be detected!" >&2 - exit 1 - fi - - - name: The detected branch should be the one starting the pull request for this test - shell: bash - run: | - headRef="${{ github.head_ref }}" - branch="${{ steps.detector.outputs.branch }}" - - echo "🌲Head ref in pull request: '$headRef'" - - if [[ "$branch" == "$headRef" ]] - then - echo "✅The detected branch coincides with the head ref!" - else - echo "❌The branch ('$branch') should match the head_ref ('$headRef')!" >&2 + echo "❌ The branch could not be detected!" >&2 exit 1 fi @@ -42,9 +26,9 @@ runs: if [[ -n "$version" ]] then - echo "🔎Version retrieved from action: '$version'" + echo "✅ Version retrieved from action: '$version'" else - echo "❌The version could not be detected!" >&2 + echo "❌ The version could not be detected!" >&2 exit 1 fi @@ -53,13 +37,13 @@ runs: run: | escapedVersion="${{ steps.detector.outputs.escaped-version }}" - echo "🔎Escaped version retrieved from action: '$escapedVersion'" + echo "🔎 Escaped version retrieved from action: '$escapedVersion'" if echo "$escapedVersion" | grep -Pq "\\\." then - echo "✅The escaped version contains '\.', as expected!" + echo "✅ The escaped version contains '\.', as expected!" else - echo "❌The escaped version should include '\.'!" >&2 + echo "❌ The escaped version should include '\.'!" >&2 exit 1 fi @@ -70,8 +54,13 @@ runs: if [[ -n "$major" ]] then - echo "🪩Major version component: '$major'" + echo "✅ Major version component: '$major'" else - echo "❌The major component could not be detected!" >&2 + echo "❌ The major component could not be detected!" >&2 exit 1 fi + + - name: Display completion message + shell: bash + run: | + echo '✅ All tests successful!' diff --git a/actions/detect-branch-version/README.md b/actions/detect-branch-version/README.md index e9ecdb24c..e6d099486 100644 --- a/actions/detect-branch-version/README.md +++ b/actions/detect-branch-version/README.md @@ -1,8 +1,8 @@ # detect-branch-version -Extracts the version from the name of the current **Git** branch, returning both. +Extracts the version from the name of the current **Git** branch, returning a variety of info. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -19,17 +19,17 @@ steps: echo "🔎Major component: '${major}'" ``` -## ☑️Requirements +## ☑️ Requirements - **Essential**: the branch name must have a [semantic version](https://semver.org/) format, optionally preceded by `v`. For example: `v1.0.2`. - the branch name is read from `github.head_ref` if such variable is available - because the action is especially designed for pull-request workflows - and from `github.ref` otherwise. -## 📥Inputs +## 📥 Inputs _No inputs required._ -## 📤Outputs +## 📤 Outputs | Name | Type | Description | Example | | :---------------: | :--------: | :----------------------------------------------------------: | :---------: | @@ -38,7 +38,7 @@ _No inputs required._ | `escaped-version` | **string** | The escaped version - for regular expressions | **2\.4\.8** | | `major` | **string** | The `major` component of the version | **2** | -## 🌐Further references +## 🌐 Further references - [semantic version](https://semver.org/) diff --git a/actions/detect-branch-version/action.yml b/actions/detect-branch-version/action.yml index b2c59b7d6..d61e4c861 100644 --- a/actions/detect-branch-version/action.yml +++ b/actions/detect-branch-version/action.yml @@ -1,5 +1,5 @@ name: Detect branch version -description: Extracts the version from the name of the current Git branch, returning both. +description: Extracts the version from the name of the current Git branch, returning a variety of info. outputs: branch: @@ -11,26 +11,22 @@ outputs: value: ${{ steps.detect-version.outputs.version }} escaped-version: - description: The escaped version - for example, with '\.' in lieu of '.' - suitable for regular expressions. + description: The escaped version - for example, with `\.` in lieu of `.` - suitable for regular expressions. value: ${{ steps.detect-version.outputs.escaped-version }} major: - description: The 'major' component of the version. + description: The `major` component of the version. value: ${{ steps.detect-version.outputs.major }} runs: using: composite steps: - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Call the Python implementation - id: detect-version - shell: python + - id: detect-version + shell: elvish {0} run: | - from core.detect_branch_version import detect_branch_version_info + use aurora-github/branch-version/detection + use aurora-github/ci-cd/output - detect_branch_version_info().write_to_github_output() + output:map (detection:detect) From 8f472c7661126c52c54cb1d20c738ff57773f4cf Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 20:05:35 +0200 Subject: [PATCH 08/63] Refactor `check-action-references` --- actions/check-action-references/README.md | 12 ++++++------ actions/check-action-references/action.yml | 21 +++++++-------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/actions/check-action-references/README.md b/actions/check-action-references/README.md index d916fb565..6e645873f 100644 --- a/actions/check-action-references/README.md +++ b/actions/check-action-references/README.md @@ -1,29 +1,29 @@ # check-action-references -Prevents cross-branch `uses:` directives to **GitHub** actions residing below the same root directory. +Prevents cross-branch `uses:` directives between **GitHub** actions residing below the same root directory. -## 🃏Example +## 🃏 Example ```yaml steps: - uses: giancosta86/aurora-github/actions/check-action-references@v10 ``` -## 💡How it works +## 💡 How it works When creating actions in a repository acting as a library for GitHub Actions, you could need to reference one action from another: this action ensures that all the above local `@` tags only reference actions on the current branch. -## ☑️Requirements +## ☑️ Requirements - The check is only performed on [composite GitHub actions](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action), written in files having `.yml` extension and residing below the given `actions-directory`. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------: | :--------: | :---------------------------------------------------: | :-----------: | | `actions-directory` | **string** | The root of the directory tree containing the actions | **actions** | -## 🌐Further references +## 🌐 Further references - [Composite GitHub actions](https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-composite-action) diff --git a/actions/check-action-references/action.yml b/actions/check-action-references/action.yml index 2f6419990..ca08516b1 100644 --- a/actions/check-action-references/action.yml +++ b/actions/check-action-references/action.yml @@ -1,5 +1,5 @@ name: Check GitHub Actions references -description: Prevents cross-branch 'uses:' directives to GitHub actions residing below the same root directory. +description: Prevents cross-branch `uses:` directives between GitHub actions residing below the same root directory. inputs: actions-directory: @@ -9,19 +9,12 @@ inputs: runs: using: composite steps: - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_ACTIONS_DIRECTORY=${{ inputs.actions-directory }}" >> $GITHUB_ENV + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Call the Python implementation - shell: python + - shell: elvish {0} + working-directory: ${{ inputs.actions-directory }} run: | - from core.check_action_references import check_action_references, Inputs - - inputs = Inputs.from_env() + use aurora-github/action-references + use aurora-github/ci-cd/input - check_action_references(inputs) + action-references:check From 33d0aeaf0d8925042ac2f26da0522fb93e375c46 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 20:06:35 +0200 Subject: [PATCH 09/63] Refactor `parse-npm-scope` --- .../test-parse-npm-scope/action.yml | 24 ++++++++--------- actions/parse-npm-scope/README.md | 10 +++---- actions/parse-npm-scope/action.yml | 27 +++++++++---------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.github/test-actions/test-parse-npm-scope/action.yml b/.github/test-actions/test-parse-npm-scope/action.yml index dfaffa4a5..8e26cac2a 100644 --- a/.github/test-actions/test-parse-npm-scope/action.yml +++ b/.github/test-actions/test-parse-npm-scope/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Parsing root scope..." + run: echo "🎭 Parsing root scope..." - name: Parse root scope uses: ./actions/parse-npm-scope @@ -17,17 +17,17 @@ runs: run: | rootScope="${{ steps.root-detector.outputs.actual-scope }}" - echo "🔎Root scope parsed by action: '$rootScope'" + echo "🔎 Root scope parsed by action: '$rootScope'" if [[ -z "$rootScope" ]] then - echo "✅Root scope detected!" + echo "✅ Root scope detected!" else - echo "❌The parsed root scope must be empty!" >&2 + echo "❌ The parsed root scope must be empty!" >&2 exit 1 fi - shell: bash - run: echo "🎭Parsing scope without leading @..." + run: echo "🎭 Parsing scope without leading @..." - name: Parse scope without leading '@' uses: ./actions/parse-npm-scope @@ -40,17 +40,17 @@ runs: run: | actualScope="${{ steps.without-leading-detector.outputs.actual-scope }}" - echo "🔎Scope parsed by action: '$actualScope'" + echo "🔎 Scope parsed by action: '$actualScope'" if [[ "$actualScope" == 'giancosta86' ]] then - echo "✅Scope without '@' detected!" + echo "✅ Scope without '@' detected!" else - echo "❌Incorrect scope detected!" >&2 + echo "❌ Incorrect scope detected!" >&2 exit 1 fi - shell: bash - run: echo "🎭Parsing scope with leading @..." + run: echo "🎭 Parsing scope with leading @..." - name: Parse scope with leading '@' uses: ./actions/parse-npm-scope @@ -63,11 +63,11 @@ runs: run: | actualScope="${{ steps.with-leading-detector.outputs.actual-scope }}" - echo "🔎Scope parsed by action: '$actualScope'" + echo "🔎 Scope parsed by action: '$actualScope'" if [[ "$actualScope" == 'giancosta86' ]] then - echo "✅Scope with '@' detected!" + echo "✅ Scope with '@' detected!" else - echo "❌Incorrect scope detected!" >&2 + echo "❌ Incorrect scope detected!" >&2 exit 1 fi diff --git a/actions/parse-npm-scope/README.md b/actions/parse-npm-scope/README.md index ad2476c6c..b8cd87cbb 100644 --- a/actions/parse-npm-scope/README.md +++ b/actions/parse-npm-scope/README.md @@ -2,7 +2,7 @@ Parses a mandatory [npm scope](https://docs.npmjs.com/cli/v10/using-npm/scope) declaration. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -12,7 +12,7 @@ steps: scope: giancosta86 ``` -## ☑️Requirements +## ☑️ Requirements - The `scope` input _must_ be declared. It can be: @@ -20,19 +20,19 @@ steps: - the _package scope_ as a string, with an optional leading `@` -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----: | :--------: | :--------------------: | :-----------: | | `scope` | **string** | The npm scope to parse | | -## 📤Outputs +## 📤 Outputs | Name | Type | Description | Example | | :------------: | :--------: | :-------------------------------------------------------: | :-------------: | | `actual-scope` | **string** | The scope without '@', or the empty string for root scope | **giancosta86** | -## 🌐Further references +## 🌐 Further references - [npm scope](https://docs.npmjs.com/cli/v10/using-npm/scope) diff --git a/actions/parse-npm-scope/action.yml b/actions/parse-npm-scope/action.yml index 6c47d705e..c2365c028 100644 --- a/actions/parse-npm-scope/action.yml +++ b/actions/parse-npm-scope/action.yml @@ -3,30 +3,27 @@ description: Parses a mandatory npm scope declaration. inputs: scope: - description: The npm scope to parse. Can be either '' or a string with an optional leading "@". + description: The npm scope to parse. Can be either `` or a string with an optional leading `@`. outputs: actual-scope: - description: The scope without '@', or the empty string for root scope. + description: The scope without `@`, or the empty string for root scope. value: ${{ steps.parse-actual-scope.outputs.actual-scope }} runs: using: composite steps: - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_SCOPE=${{ inputs.scope }}" >> $GITHUB_ENV + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Call the Python implementation - id: parse-actual-scope - shell: python + - id: parse-actual-scope + shell: elvish {0} run: | - from core.parse_npm_scope import parse_npm_scope, Inputs + use aurora-github/ci-cd/input + use aurora-github/ci-cd/output + use aurora-github/nodejs/pnpm + + var scope = (input:string scope '${{ inputs.scope }}') - inputs = Inputs.from_env() + var actual-scope = (pnpm:parse-scope $scope) - parse_npm_scope(inputs).write_to_github_output() + output:write actual-scope $actual-scope From 2da9a26b46ee31537e2422b8fd351f631ef26772 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 20:07:27 +0200 Subject: [PATCH 10/63] Refactor `check-project-license` --- .../test-check-project-license/action.yml | 4 +- actions/check-project-license/README.md | 14 ++++-- actions/check-project-license/action.yml | 49 +++---------------- 3 files changed, 19 insertions(+), 48 deletions(-) diff --git a/.github/test-actions/test-check-project-license/action.yml b/.github/test-actions/test-check-project-license/action.yml index f744bd247..83cd498f6 100644 --- a/.github/test-actions/test-check-project-license/action.yml +++ b/.github/test-actions/test-check-project-license/action.yml @@ -1,10 +1,10 @@ -name: Test check-rust-versions +name: Test check-project-license runs: using: composite steps: - shell: bash - run: echo "🎭Checking project license..." + run: echo "🎭 Checking project license..." - name: Check project license uses: ./actions/check-project-license diff --git a/actions/check-project-license/README.md b/actions/check-project-license/README.md index 539c2ffc4..a4795edef 100644 --- a/actions/check-project-license/README.md +++ b/actions/check-project-license/README.md @@ -1,8 +1,8 @@ # check-project-license -Ensures the validity of the project license file. +Checks the validity of the project license file. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -21,19 +21,23 @@ steps: - [verify-python-package](../verify-python-package/README.md) -## 💡How it works +## ☑️ Requirements + +- The POSIX `date` command must be available on the system. + +## 💡 How it works 1. Check that the `license-file` path actually points to an existing file. 1. Verify that the current year is mentioned within the license file. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :------------: | :--------: | :--------------------------: | :-----------: | | `license-file` | **string** | The path to the license file | **LICENSE** | -## 🌐Further references +## 🌐 Further references - [verify-npm-package](../verify-npm-package/README.md) diff --git a/actions/check-project-license/action.yml b/actions/check-project-license/action.yml index 10806701a..569eb9f68 100644 --- a/actions/check-project-license/action.yml +++ b/actions/check-project-license/action.yml @@ -1,5 +1,5 @@ name: Check project license -description: Ensures the validity of the project license file. +description: Checks the validity of the project license file. inputs: license-file: @@ -9,46 +9,13 @@ inputs: runs: using: composite steps: - - shell: bash - run: | - licenseFile="${{ inputs.license-file }}" - - main() { - licenseFileMustExist - - licenseMustIncludeCurrentYear - } - - licenseFileMustExist() { - if [[ -f "$licenseFile" ]] - then - echo "✅License file found: $licenseFile" - else - echo "❌Missing license file: '$licenseFile'" >&2 - exit 1 - fi - } + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - licenseMustIncludeCurrentYear() { - currentYear="$(date +"%Y")" - - if [[ -z "$currentYear" ]] - then - echo "❌Cannot detect the current year!" >&2 - exit 1 - fi - - echo "🔎Current year: $currentYear" - - echo "🔎Searching the license file for the current year..." + - shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/license-file - if grep --color=always "$currentYear" "$licenseFile" - then - echo "✅Current year found in the license file!" - else - echo "❌Cannot find the current year in the license file!" >&2 - exit 1 - fi - } + var license-file = (input:file license-file '${{ inputs.license-file }}') - main + license-file:check $license-file From 5ee7ed73f15fbf6d5fd4ffd563a2f255d6ef8c11 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 13 Apr 2025 07:50:38 +0200 Subject: [PATCH 11/63] Refactor `install-wasm-pack` --- .../test-install-wasm-pack/action.yml | 26 +++++++---- actions/install-wasm-pack/README.md | 14 +++--- actions/install-wasm-pack/action.yml | 43 +++++-------------- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/.github/test-actions/test-install-wasm-pack/action.yml b/.github/test-actions/test-install-wasm-pack/action.yml index c40a77928..cf6a590ef 100644 --- a/.github/test-actions/test-install-wasm-pack/action.yml +++ b/.github/test-actions/test-install-wasm-pack/action.yml @@ -7,21 +7,31 @@ runs: shell: bash run: echo "expectedVersion=0.13.1" >> $GITHUB_ENV + - name: The expected version should not be already installed + shell: bash + run: | + if type wasm-pack >/dev/null 2>&1 + then + if wasm-pack --version | grep --color=always "$expectedVersion" + then + echo "❌ The expected wasm-pack version ('$expectedVersion') is already installed - this test would be pointless!" >&2 + exit 1 + fi + else + echo "💭 The requested wasm-pack version is not running in this environment..." + fi + - uses: ./actions/install-wasm-pack with: wasm-pack-version: ${{ env.expectedVersion }} - - name: The expected wasm-pack version should be available + - name: The expected wasm-pack version should now be available shell: bash run: | - wasmPackVersion="$(wasm-pack --version | cut -d' ' -f2)" - - echo "🔎Detected wasm-pack version: '$wasmPackVersion'" - - if [[ "$wasmPackVersion" == "$expectedVersion" ]] + if wasm-pack --version | grep --color=always "$expectedVersion" then - echo "✅The requested wasm-pack version is in use!" + echo "✅ The requested wasm-pack version is in use!" else - echo "❌The expected wasm-pack version ('$expectedVersion') is not installed!" >&2 + echo "❌ The expected wasm-pack version ('$expectedVersion') is not installed!" >&2 exit 1 fi diff --git a/actions/install-wasm-pack/README.md b/actions/install-wasm-pack/README.md index 6a5eb0124..759ea0fb5 100644 --- a/actions/install-wasm-pack/README.md +++ b/actions/install-wasm-pack/README.md @@ -2,7 +2,7 @@ Installs [wasm-pack](https://rustwasm.github.io/wasm-pack/), for creating **Rust**-based web assemblies. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,17 +11,17 @@ steps: **Please, note**: this action is automatically run by [verify-rust-wasm](../verify-rust-wasm/README.md) and [publish-rust-wasm](../publish-rust-wasm/README.md). -## ☑️Requirements +## ☑️ Requirements The **npm** command must be available on the system. -## 📥Inputs +## 📥 Inputs -| Name | Type | Description | Default value | -| :-----------------: | :--------: | :------------------------------: | :-----------: | -| `wasm-pack-version` | **string** | The wasm-pack version to install | | +| Name | Type | Description | Default value | +| :-----------------: | :--------: | :---------------------------------: | :-----------: | +| `wasm-pack-version` | **string** | The version of wasm-pack to install | | -## 🌐Further references +## 🌐 Further references - [verify-rust-wasm](../verify-rust-wasm/README.md) diff --git a/actions/install-wasm-pack/action.yml b/actions/install-wasm-pack/action.yml index bf6024314..9b2e242e1 100644 --- a/actions/install-wasm-pack/action.yml +++ b/actions/install-wasm-pack/action.yml @@ -3,43 +3,20 @@ description: Installs wasm-pack, for creating Rust-based web assemblies. inputs: wasm-pack-version: - description: The wasm-pack version to install. + description: The version of wasm-pack to install. runs: using: composite steps: - - shell: bash - run: | - main() { - validateInputs - installWasmPack - printWasmPackVersion - } - - validateInputs() { - if [[ -z "${{ inputs.wasm-pack-version }}" ]] - then - echo "❌Missing action input: 'wasm-pack-version'!" >&2 - exit 1 - fi - } - - installWasmPack() { - local requestedVersion="${{ inputs.wasm-pack-version }}" - - echo "🌐Installing wasm-pack $requestedVersion..." + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - npm install -g "wasm-pack@$requestedVersion" - - echo "✅wasm-pack installed!" - } - - printWasmPackVersion() { - echo "🔎Now ensuring wasm-pack is available..." - - wasm-pack --version + - shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/rust/wasm-pack - echo "✅wasm-pack ready!" - } + var version = ( + input:string wasm-pack-version '${{ inputs.wasm-pack-version }}' + ) - main \ No newline at end of file + wasm-pack:install $version From cf981f3785dcc4c2edcaa9cdfe325334febc6fd1 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 20:09:59 +0200 Subject: [PATCH 12/63] Replace `check-rust-versions` with `setup-rust-context`: * add support for Cargo coloring - enabled by default * improve output of the toolchain versions --- .../test-check-rust-versions/action.yml | 20 ------- .../test-setup-rust-context/action.yml | 20 +++++++ README.md | 2 + actions/check-rust-versions/README.md | 35 ------------ actions/check-rust-versions/action.yml | 41 -------------- actions/setup-rust-context/README.md | 55 +++++++++++++++++++ actions/setup-rust-context/action.yml | 31 +++++++++++ 7 files changed, 108 insertions(+), 96 deletions(-) delete mode 100644 .github/test-actions/test-check-rust-versions/action.yml create mode 100644 .github/test-actions/test-setup-rust-context/action.yml delete mode 100644 actions/check-rust-versions/README.md delete mode 100644 actions/check-rust-versions/action.yml create mode 100644 actions/setup-rust-context/README.md create mode 100644 actions/setup-rust-context/action.yml diff --git a/.github/test-actions/test-check-rust-versions/action.yml b/.github/test-actions/test-check-rust-versions/action.yml deleted file mode 100644 index bd5057b9c..000000000 --- a/.github/test-actions/test-check-rust-versions/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Test check-rust-versions - -runs: - using: composite - steps: - - shell: bash - run: echo "🎭Testing on Rust crate..." - - - name: Checking Rust versions for Rust crate should work - uses: ./actions/check-rust-versions - with: - project-directory: tests/rust-crate - - - shell: bash - run: echo "🎭Testing on Rust Wasm..." - - - name: Checking Rust versions for Rust web assembly should work - uses: ./actions/check-rust-versions - with: - project-directory: tests/rust-wasm diff --git a/.github/test-actions/test-setup-rust-context/action.yml b/.github/test-actions/test-setup-rust-context/action.yml new file mode 100644 index 000000000..fafdf217a --- /dev/null +++ b/.github/test-actions/test-setup-rust-context/action.yml @@ -0,0 +1,20 @@ +name: Test setup-rust-context + +runs: + using: composite + steps: + - shell: bash + run: echo "🎭 Testing on Rust crate..." + + - name: Run for Rust crate project + uses: ./actions/setup-rust-context + with: + project-directory: tests/rust-crate + + - shell: bash + run: echo "🎭 Testing on Rust Wasm..." + + - name: Run for Rust web assembly project + uses: ./actions/setup-rust-context + with: + project-directory: tests/rust-wasm diff --git a/README.md b/README.md index 422f033ea..5e84da5b8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ The actions can be grouped by technology: ## 🦀Rust +- [setup-rust-context](actions/setup-rust-context/README.md) + - [verify-rust-crate](actions/verify-rust-crate/README.md) - [publish-rust-crate](actions/publish-rust-crate/README.md) diff --git a/actions/check-rust-versions/README.md b/actions/check-rust-versions/README.md deleted file mode 100644 index 37d50ca95..000000000 --- a/actions/check-rust-versions/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# check-rust-versions - -Displays the current version of the main **Rust** components, after ensuring that `rust-toolchain.toml` is in the project directory. - -## 🃏Example - -```yaml -steps: - - uses: giancosta86/aurora-github/actions/check-rust-versions@v10 -``` - -**Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md). - -## ☑️Requirements - -- the [toolchain file](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file must exist within `project-directory`. - - It should include at least the required toolchain version, for example: - - ```toml - [toolchain] - channel = "1.80.0" - ``` - -## 📥Inputs - -| Name | Type | Description | Default value | -| :-----------------: | :--------: | :-----------------------------------------: | :-----------: | -| `project-directory` | **string** | The directory containing the toolchain file | **.** | - -## 🌐Further references - -- [verify-rust-crate](../verify-rust-crate/README.md) - -- [aurora-github](../../README.md) diff --git a/actions/check-rust-versions/action.yml b/actions/check-rust-versions/action.yml deleted file mode 100644 index 40297fc20..000000000 --- a/actions/check-rust-versions/action.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Check Rust version info -description: Displays the versions of the main Rust components, after ensuring that 'rust-toolchain.toml' is in the project directory. - -inputs: - project-directory: - description: The directory containing the toolchain file. - default: "." - -runs: - using: composite - steps: - - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - main() { - checkToolchainFile - printToolVersions - } - - checkToolchainFile() { - local toolchainFile="rust-toolchain.toml" - - if [[ -f "$toolchainFile" ]] - then - echo "✅Toolchain file found: $toolchainFile" - else - echo "❌Missing toolchain file: '$toolchainFile'" >&2 - exit 1 - fi - } - - printToolVersions() { - echo "🦀Rust toolchain versions:" - cargo --version - rustc --version - cargo fmt --version - cargo clippy --version - echo "🦀🦀🦀" - } - - main \ No newline at end of file diff --git a/actions/setup-rust-context/README.md b/actions/setup-rust-context/README.md new file mode 100644 index 000000000..05f2af013 --- /dev/null +++ b/actions/setup-rust-context/README.md @@ -0,0 +1,55 @@ +# setup-rust-context + +Installs and configures a **Rust** toolchain. + +## 🃏 Example + +```yaml +steps: + - uses: giancosta86/aurora-github/actions/setup-rust-context@v10 +``` + +**Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md), [publish-rust-crate](../publish-rust-crate/README.md) and [run-custom-tests](../run-custom-tests/README.md). + +## 💡 How it works + +1. Set the `CARGO_TERM_COLOR` environment variable according to the value of the `cargo-colors` input. + +1. If the `check-toolchain-file` input is **true**, verify that the toolchain file (discussed above) exists. + +1. Ensure the required components (**rustfmt**, **clippy**) are installed. + +1. Display the versions of the executables in the current Rust toolchain. + +## ☑️ Requirements + +- The **Cargo.toml** descriptor must exist in `project-directory`. + +- Some version of the `cargo` and `rustup` executables must already be available in the path. + +- if the `check-toolchain-file` input is **true**, the [toolchain file](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file must exist within `project-directory`. + + It should include at least the required toolchain version, for example: + + ```toml + [toolchain] + channel = "1.80.0" + ``` + +## 📥 Inputs + +| Name | Type | Description | Default value | +| :--------------------: | :---------: | :-----------------------------------------: | :-----------: | +| `cargo-colors` | **boolean** | Enable colors for Cargo | **true** | +| `check-toolchain-file` | **boolean** | Verify the existence of the toolchain file | **true** | +| `project-directory` | **string** | The directory containing the toolchain file | **.** | + +## 🌐 Further references + +- [verify-rust-crate](../verify-rust-crate/README.md) + +- [publish-rust-crate](../publish-rust-crate/README.md) + +- [run-custom-tests](../run-custom-tests/README.md) + +- [aurora-github](../../README.md) diff --git a/actions/setup-rust-context/action.yml b/actions/setup-rust-context/action.yml new file mode 100644 index 000000000..474e91b92 --- /dev/null +++ b/actions/setup-rust-context/action.yml @@ -0,0 +1,31 @@ +name: Setup Rust context +description: Installs and configures a Rust toolchain. + +inputs: + cargo-colors: + description: Enable colors for `cargo` + default: true + + check-toolchain-file: + description: Verify the existence of the toolchain file. + default: true + + project-directory: + description: The directory containing the toolchain file. + default: . + +runs: + using: composite + steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - shell: elvish {0} + working-directory: ${{ inputs.project-directory }} + run: | + use aurora-github/ci-cd/input + use aurora-github/rust/context + + var cargo-colors = (input:bool cargo-colors '${{ inputs.cargo-colors }}') + var check-toolchain-file = (input:bool check-toolchain-file '${{ inputs.check-toolchain-file }}') + + context:setup &cargo-colors=$cargo-colors &check-toolchain-file=$check-toolchain-file From 32fd0f82f5e595fd6e6121b07cf279e4bf61065c Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Apr 2025 02:05:32 +0200 Subject: [PATCH 13/63] Refactor and enhance `enforce-branch-version`: * Add syntax highlighting for project descriptors --- .../test-enforce-branch-version/action.yml | 144 +++++------------- .../scenario/action.yml | 95 ++++++++++++ actions/enforce-branch-version/README.md | 10 +- actions/enforce-branch-version/action.yml | 25 ++- 4 files changed, 145 insertions(+), 129 deletions(-) create mode 100644 .github/test-actions/test-enforce-branch-version/scenario/action.yml diff --git a/.github/test-actions/test-enforce-branch-version/action.yml b/.github/test-actions/test-enforce-branch-version/action.yml index 5a232c9e7..3826f965a 100644 --- a/.github/test-actions/test-enforce-branch-version/action.yml +++ b/.github/test-actions/test-enforce-branch-version/action.yml @@ -3,143 +3,69 @@ name: Test enforce-branch-version runs: using: composite steps: + - uses: ./actions/detect-branch-version + id: version-detector + - shell: bash - run: echo "🎭Testing 'skip' mode..." + run: echo "🎭 Testing 'skip' mode..." - uses: ./actions/enforce-branch-version with: mode: skip project-directory: tests/rust-crate - - shell: bash - run: echo "🎭Testing 'inject' mode for 📦NodeJS package..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 📦 NodeJS package project-directory: tests/npm-package + artifact-descriptor: package.json + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🦀Rust crate..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 🦀 Rust crate project-directory: tests/rust-crate + artifact-descriptor: Cargo.toml + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🦀Rust 🌐WebAssembly..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 🦀🌐 Rust WebAssembly project-directory: tests/rust-wasm + artifact-descriptor: Cargo.toml + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🪶Maven project..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 🪶 Maven project project-directory: tests/maven-project + artifact-descriptor: pom.xml + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🐘Gradle project..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 🐘 Gradle project project-directory: tests/gradle-project + artifact-descriptor: build.gradle.kts + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🐍Python library..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject + project-type: 🐍 Python library project-directory: tests/python-lib + artifact-descriptor: pyproject.toml + expected-version: ${{ steps.version-detector.outputs.version }} - - shell: bash - run: echo "🎭Testing 'inject' mode for 🎁unknown tech stack..." - - - uses: ./actions/enforce-branch-version + - uses: ./.github/test-actions/test-enforce-branch-version/scenario with: - mode: inject - artifact-descriptor: some-descriptor.txt + project-type: 🎁 unknown tech stack project-directory: tests/unknown-tech - - - shell: bash - run: echo "🎭Testing 'check' mode for 📦NodeJS package..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/npm-package - - - shell: bash - run: echo "🎭Testing 'check' mode for 🦀Rust crate..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/rust-crate - - - shell: bash - run: echo "🎭Testing 'check' mode for 🦀Rust 🌐WebAssembly..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/rust-wasm - - - shell: bash - run: echo "🎭Testing 'check' mode for 🪶Maven project..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/maven-project - - - shell: bash - run: echo "🎭Testing 'check' mode for 🐘Gradle project..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/gradle-project - - - shell: bash - run: echo "🎭Testing 'check' mode for 🐍Python library..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - project-directory: tests/python-lib - - - shell: bash - run: echo "🎭Testing 'check' mode for 🎁unknown tech stack..." - - - name: Check version for unknown tech - uses: ./actions/enforce-branch-version - with: - mode: check artifact-descriptor: some-descriptor.txt - project-directory: tests/unknown-tech - - - shell: bash - run: echo "🎭Now testing the NodeJS package scenario, passing an explicit descriptor..." - - - uses: ./actions/enforce-branch-version - with: - mode: check - artifact-descriptor: package.json - project-directory: tests/npm-package - - - uses: ./actions/detect-branch-version - id: version-detector + expected-version: ${{ steps.version-detector.outputs.version }} + can-infer-descriptor: false - shell: bash - run: echo "🎭Verifying that the version has been injected multiple times..." + run: echo "🎭 Verifying that the version has been injected multiple times..." - shell: bash working-directory: tests/unknown-tech @@ -148,8 +74,8 @@ runs: if grep "A: v$version" some-descriptor.txt && grep "B: '$version'" some-descriptor.txt then - echo "✅The version was injected multiple times into the 🎁unknown tech descriptor!" + echo "✅ The version was injected multiple times into the 🎁 unknown tech descriptor!" else - echo "❌The version was not injected multiple times into the 🎁unknown tech descriptor!" >&2 + echo "❌ The version was not injected multiple times into the 🎁 unknown tech descriptor!" >&2 exit 1 fi diff --git a/.github/test-actions/test-enforce-branch-version/scenario/action.yml b/.github/test-actions/test-enforce-branch-version/scenario/action.yml new file mode 100644 index 000000000..d5cf44300 --- /dev/null +++ b/.github/test-actions/test-enforce-branch-version/scenario/action.yml @@ -0,0 +1,95 @@ +name: Run a project-specific scenario for enforce-branch-version + +inputs: + project-type: + description: A brief description of the project. + + project-directory: + description: The project directory. + + artifact-descriptor: + description: The descriptor file name - relative to the project directory. + + expected-version: + description: The expected version after injection. + + can-infer-descriptor: + description: Whether the descriptor can be inferred from the directory. + default: true + +runs: + using: composite + steps: + - shell: bash + run: | + header() { + local text="$1" + + local line + line="$(printf '=%.0s' $(seq 1 "${#text}"))" + + echo -e "$line\n$text\n$line" >&2 + } + + header "${{ inputs.project-type }}" + + - name: Backup the descriptor in preparation for multiple tests + if: inputs.can-infer-descriptor == 'true' + shell: bash + working-directory: ${{ inputs.project-directory }} + run: cp "${{ inputs.artifact-descriptor }}" "${{ inputs.artifact-descriptor }}.orig" + + - shell: bash + run: echo "🎭 Testing 'inject' mode (with declared descriptor)..." + + - uses: ./actions/enforce-branch-version + with: + mode: inject + project-directory: ${{ inputs.project-directory }} + artifact-descriptor: ${{ inputs.artifact-descriptor }} + + - shell: bash + run: | + grep --color=always -F -Hn '${{ inputs.expected-version }}' ${{ inputs.project-directory }}/${{ inputs.artifact-descriptor }} + echo '✅ Grep found the expected version!' + + - name: Restore the original descriptor + if: inputs.can-infer-descriptor == 'true' + shell: bash + working-directory: ${{ inputs.project-directory }} + run: cp "${{ inputs.artifact-descriptor }}.orig" "${{ inputs.artifact-descriptor }}" + + - shell: bash + if: inputs.can-infer-descriptor == 'true' + run: echo "🎭 Testing 'inject' mode (with inferred descriptor)..." + + - uses: ./actions/enforce-branch-version + if: inputs.can-infer-descriptor == 'true' + with: + mode: inject + project-directory: ${{ inputs.project-directory }} + + - shell: bash + if: inputs.can-infer-descriptor == 'true' + run: | + grep --color=always -F -Hn '${{ inputs.expected-version }}' ${{ inputs.project-directory }}/${{ inputs.artifact-descriptor }} + echo '✅ Grep found the expected version!' + + - shell: bash + run: echo "🎭 Testing 'check' mode (with declared descriptor)..." + + - uses: ./actions/enforce-branch-version + with: + mode: check + project-directory: ${{ inputs.project-directory }} + artifact-descriptor: ${{ inputs.artifact-descriptor }} + + - shell: bash + if: inputs.can-infer-descriptor == 'true' + run: echo "🎭 Testing 'check' mode (with inferred descriptor)..." + + - uses: ./actions/enforce-branch-version + if: inputs.can-infer-descriptor == 'true' + with: + mode: check + project-directory: ${{ inputs.project-directory }} diff --git a/actions/enforce-branch-version/README.md b/actions/enforce-branch-version/README.md index 8eef11ff7..19ef2d531 100644 --- a/actions/enforce-branch-version/README.md +++ b/actions/enforce-branch-version/README.md @@ -2,7 +2,7 @@ Ensures that the version in the artifact descriptor matches the **Git** branch version - by injecting or merely by checking. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -37,7 +37,7 @@ and indirectly by: - [publish-rust-wasm](../publish-rust-wasm/README.md) -## 💡How it works +## 💡 How it works 1. If `mode` is **skip**, just exit the action flow. @@ -79,11 +79,11 @@ and indirectly by: - for any other technology, verify that the branch version exists at least once in the descriptor -## ☑️Requirements +## ☑️ Requirements - The `artifact-descriptor` - no matter whether declared or detected - must exist in the file system. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-------------------: | :---------------------: | :---------------------------------------: | :-----------: | @@ -91,7 +91,7 @@ and indirectly by: | `artifact-descriptor` | **string** | Relative path to the artifact descriptor | | | `project-directory` | **string** | The directory containing the project | **.** | -## 🌐Further references +## 🌐 Further references - [verify-npm-package](../verify-npm-package/README.md) diff --git a/actions/enforce-branch-version/action.yml b/actions/enforce-branch-version/action.yml index 836227754..9cd7250f5 100644 --- a/actions/enforce-branch-version/action.yml +++ b/actions/enforce-branch-version/action.yml @@ -3,7 +3,7 @@ description: Ensures that the version in the artifact descriptor matches the Git inputs: mode: - description: How to enforce the branch version. Can be "inject", "check" or "skip". + description: How to enforce the branch version. Can be `inject`, `check` or `skip`. artifact-descriptor: description: Relative path to the artifact descriptor. @@ -15,21 +15,16 @@ inputs: runs: using: composite steps: - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_MODE=${{ inputs.mode }}" >> $GITHUB_ENV - echo "INPUT_ARTIFACT_DESCRIPTOR=${{ inputs.artifact-descriptor }}" >> $GITHUB_ENV - echo "INPUT_PROJECT_DIRECTORY=${{ inputs.project-directory }}" >> $GITHUB_ENV + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Call the Python implementation - shell: python + - shell: elvish {0} + working-directory: ${{ inputs.project-directory }} run: | - from core.enforce_branch_version import enforce_branch_version, Inputs + use aurora-github/ci-cd/input + use aurora-github/branch-version/enforcement + + var mode = (input:enum mode '${{ inputs.mode }}' [inject check skip]) - inputs = Inputs.from_env() + var artifact-descriptor = (input:file &optional artifact-descriptor '${{ inputs.artifact-descriptor }}') - enforce_branch_version(inputs) + enforcement:enforce &descriptor-name=$artifact-descriptor $mode From 6ff6c22d38ef5f4198ff96676b1bd490b2403a60 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 9 Apr 2025 21:14:56 +0200 Subject: [PATCH 14/63] Refactor `generate-wasm-target` --- .../test-generate-wasm-target/action.yml | 32 ++-- actions/generate-wasm-target/README.md | 10 +- actions/generate-wasm-target/action.yml | 151 +++--------------- 3 files changed, 40 insertions(+), 153 deletions(-) diff --git a/.github/test-actions/test-generate-wasm-target/action.yml b/.github/test-actions/test-generate-wasm-target/action.yml index 19d2bb0f8..be3f52af1 100644 --- a/.github/test-actions/test-generate-wasm-target/action.yml +++ b/.github/test-actions/test-generate-wasm-target/action.yml @@ -16,7 +16,7 @@ runs: project-directory: tests/rust-wasm - shell: bash - run: echo "🎭Now generating a dev project with 'web' target..." + run: echo "🎭 Now generating a dev project with 'web' target..." - name: Set up the expected versions shell: bash @@ -40,13 +40,13 @@ runs: working-directory: tests/rust-wasm/pkg-web run: | packageName="$(jq -r '.name' package.json)" - echo "🔎Name field in the generated package.json: '$packageName'" + echo "🔎 Name field in the generated package.json: '$packageName'" if [[ "$packageName" == "@giancosta86/test-wasm" ]] then - echo "✅The generated package.json has the expected name!" + echo "✅ The generated package.json has the expected name!" else - echo "❌Unexpected value for the 'name' field in package.json!" + echo "❌ Unexpected value for the 'name' field in package.json!" exit 1 fi @@ -55,13 +55,13 @@ runs: working-directory: tests/rust-wasm/pkg-web run: | packageVersion="$(jq -r '.version' package.json)" - echo "🔎Version field in the generated package.json: '$packageVersion'" + echo "🔎 Version field in the generated package.json: '$packageVersion'" if [[ "$packageVersion" == "${{ steps.version-detector.outputs.version }}" ]] then - echo "✅The generated package.json has the expected version!" + echo "✅ The generated package.json has the expected version!" else - echo "❌Unexpected value for the 'version' field in package.json!" + echo "❌ Unexpected value for the 'version' field in package.json!" exit 1 fi @@ -70,13 +70,13 @@ runs: working-directory: tests/rust-wasm/pkg-web run: | nodeJsVersion="$(jq -r '.engines.node' package.json)" - echo "🔎NodeJS version field in the generated package.json: '$nodeJsVersion'" + echo "🔎 NodeJS version field in the generated package.json: '$nodeJsVersion'" if [[ "$nodeJsVersion" == "${{ env.expectedNodeJsVersion }}" ]] then - echo "✅The generated package.json requires the expected NodeJS version!" + echo "✅ The generated package.json requires the expected NodeJS version!" else - echo "❌Unexpected value for the 'engines / node' field in package.json!" + echo "❌ Unexpected value for the 'engines / node' field in package.json!" exit 1 fi @@ -85,18 +85,18 @@ runs: working-directory: tests/rust-wasm/pkg-web run: | pnpmReference="$(jq -r '.packageManager' package.json)" - echo "🔎pnpm reference field in the generated package.json: '$pnpmReference'" + echo "🔎 pnpm reference field in the generated package.json: '$pnpmReference'" if [[ "$pnpmReference" == "pnpm@${{ env.expectedPnpmVersion }}" ]] then - echo "✅The generated package.json references the expected pnpm version!" + echo "✅ The generated package.json references the expected pnpm version!" else - echo "❌Unexpected value for the 'packageManager' field in package.json!" + echo "❌ Unexpected value for the 'packageManager' field in package.json!" exit 1 fi - shell: bash - run: echo "🎭Now generating a release project with 'deno' target..." + run: echo "🎭 Now generating a release project with 'deno' target..." - uses: ./actions/generate-wasm-target with: @@ -111,8 +111,8 @@ runs: run: | if [[ ! -f "package.json" ]] then - echo "✅No package.json was generated for Deno, as expected!" + echo "✅ No package.json was generated for Deno, as expected!" else - echo "❌Why was package.json generated for Deno?" + echo "❌ Why was package.json generated for Deno?" exit 1 fi diff --git a/actions/generate-wasm-target/README.md b/actions/generate-wasm-target/README.md index 970f2feb1..07b5706d6 100644 --- a/actions/generate-wasm-target/README.md +++ b/actions/generate-wasm-target/README.md @@ -2,7 +2,7 @@ Generates the source files for a **WebAssembly** target from a **Rust** project. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -15,7 +15,7 @@ steps: project-directory: . ``` -## 💡How it works +## 💡 How it works 1. Invoke [wasm-pack](https://rustwasm.github.io/wasm-pack/) - with arguments depending on the inputs - to generate a WebAssembly project from the given Rust sources. @@ -25,11 +25,11 @@ steps: 1. If **package.json** was actually generated, display it -## ☑️Requirements +## ☑️ Requirements - The `wasm-pack` command must be available in the system - for example, via [install-wasm-pack](../install-wasm-pack/README.md). -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------: | :---------: | :------------------------------------------------------------------: | :-----------: | @@ -41,7 +41,7 @@ steps: | `target-directory` | **string** | Directory (relative to `project-directory`) for the generated target | | | `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [wasm-pack](https://rustwasm.github.io/wasm-pack/) diff --git a/actions/generate-wasm-target/action.yml b/actions/generate-wasm-target/action.yml index 2495e5e64..cfb7eae1a 100644 --- a/actions/generate-wasm-target/action.yml +++ b/actions/generate-wasm-target/action.yml @@ -1,164 +1,51 @@ -name: generate-wasm-target +name: Generate Rust web assembly target description: Generates the source files for a WebAssembly target from a Rust project. inputs: target: - description: The target of the 'wasm-pack build' command. + description: The target of the `wasm-pack build` command. npm-scope: - description: The package scope or "", for npm targets. + description: The package scope or ``, for npm targets. nodejs-version: - description: The "engines / node" version within package.json. + description: The `engines -> node` version within package.json. pnpm-version: - description: The "packageManager" reference to pnpm within package.json. + description: The `packageManager` reference to pnpm within package.json. development: description: Enable debugging info. target-directory: - description: Directory (relative to 'project-directory') for the generated target. + description: Directory (relative to `project-directory`) for the generated target. project-directory: - description: The directory containing Cargo.toml. + description: The directory containing `Cargo.toml`. default: "." runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.target }}" ]] - then - echo "❌Missing action input: 'target'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.development }}" ]] - then - echo "❌Missing action input: 'development'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.target-directory }}" ]] - then - echo "❌Missing action input: 'target-directory'!" >&2 - exit 1 - fi - - - name: Parse npm scope - id: parse-npm-scope - if: inputs.npm-scope != '' - uses: giancosta86/aurora-github/actions/parse-npm-scope@v10.3.0 - with: - scope: ${{ inputs.npm-scope }} + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Generate the WebAssembly target - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - target="${{ inputs.target }}" - echo "📥Target: '$target'" - - echo "📥Requested npm-scope: '${{ inputs.npm-scope }}'" - scope="${{ steps.parse-npm-scope.outputs.actual-scope }}" - echo "🔎Actual npm scope: '$scope'" - - development="${{ inputs.development }}" - echo "📥Development? $development" - - nodejsVersion="${{ inputs.nodejs-version }}" - echo "📥NodeJS version: '$nodejsVersion'" - - pnpmVersion="${{ inputs.pnpm-version }}" - echo "📥pnpm version: '$pnpmVersion'" - - echo "📥Project directory: '${{ inputs.project-directory }}'" - - targetDirectory="${{ inputs.target-directory }}" - echo "📥Target directory: '$targetDirectory'" - - main() { - generateWebAssemblyTarget - - pushd "$targetDirectory" - - if [[ -n "$nodejsVersion" ]] - then - injectNodeJsVersion - fi - - if [[ -n "$pnpmVersion" ]] - then - injectPnpmVersion - fi - - tryToDisplayPackageJson - - popd - - echo "✅WebAssembly target ready in '$targetDirectory'!" - } - - generateWebAssemblyTarget() { - echo "📦Generating the WebAssembly project files..." - - if [[ "$development" == "true" ]] - then - local modeArg="--dev" - else - local modeArg="--release" - fi - - if [[ -n "$scope" ]] - then - local scopeArg="--scope $scope" - else - local scopeArg="" - fi - - wasm-pack build --target "$target" $modeArg $scopeArg --out-dir "$targetDirectory" - } - - injectNodeJsVersion() { - if [[ ! -f "package.json" ]] - then - echo "❌package.json was not generated for this target - cannot inject the requested NodeJS version!" - exit 1 - fi - - echo "🧬Injecting the requested NodeJS version ('$nodejsVersion')..." + use aurora-github/ci-cd/input + use aurora-github/rust/wasm-pack - local tempDescriptor="$(mktemp)" - jq '.engines.node = "'"$nodejsVersion"'"' package.json > "$tempDescriptor" - mv "$tempDescriptor" package.json - } + wasm-pack:generate-target [ + &target=(input:string target '${{ inputs.target }}') - injectPnpmVersion() { - if [[ ! -f "package.json" ]] - then - echo "❌package.json was not generated for this target - cannot inject the requested pnpm version!" - exit 1 - fi + &npm-scope=(input:string npm-scope '${{ inputs.npm-scope }}') - echo "🧬Injecting the requested pnpm version ('$pnpmVersion')..." + &nodejs-version=(input:string &optional nodejs-version '${{ inputs.nodejs-version }}') - local tempDescriptor="$(mktemp)" - jq '.packageManager = "pnpm@'"$pnpmVersion"'"' package.json > "$tempDescriptor" - mv "$tempDescriptor" package.json - } + &pnpm-version=(input:string &optional pnpm-version '${{ inputs.pnpm-version }}') - tryToDisplayPackageJson() { - if [[ -f "package.json" ]] - then - echo "📦Generated package.json descriptor for the '$target' target:" - jq -C '.' package.json - else - echo "💭No package.json descriptor generated for the '$target' target..." - fi - } + &development=(input:bool development '${{ inputs.development }}') - main + &target-directory=(input:string target-directory '${{ inputs.target-directory }}') + ] From c762b821ec12c023b347700f536c1cdc981ce316 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 13 Apr 2025 08:55:50 +0200 Subject: [PATCH 15/63] Refactor and redesign `find-critical-todos`: * each path to check does **not** include anymore a leading `./` --- .../test-find-critical-todos/action.yml | 14 +-- actions/find-critical-todos/README.md | 42 +++++--- actions/find-critical-todos/action.yml | 99 +++---------------- 3 files changed, 51 insertions(+), 104 deletions(-) diff --git a/.github/test-actions/test-find-critical-todos/action.yml b/.github/test-actions/test-find-critical-todos/action.yml index 80df62a73..311df45e4 100644 --- a/.github/test-actions/test-find-critical-todos/action.yml +++ b/.github/test-actions/test-find-critical-todos/action.yml @@ -3,7 +3,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Find existing critical TODOs without crashing..." + run: echo "🎭 Find existing critical TODOs without crashing..." - uses: ./actions/find-critical-todos id: find-existing-critical-todos @@ -20,20 +20,20 @@ runs: if [[ "$found" == "true" ]] then - echo "✅Critical TODOs found, as expected" + echo "✅ Critical TODOs found, as expected" else - echo "❌The expected critical TODOs have not been found!" >&2 + echo "❌ The expected critical TODOs have not been found!" >&2 exit 1 fi - shell: bash - run: echo "🎭Look for inexisting critical TODOs..." + run: echo "🎭 Look for inexistent critical TODOs..." - uses: ./actions/find-critical-todos id: look-for-missing-critical-todos with: root-directory: tests/custom - source-file-regex: .* + source-file-regex: \.(rs|elv)$ crash-on-found: true verbose: true @@ -44,8 +44,8 @@ runs: if [[ "$found" != "true" ]] then - echo "✅Critical TODOs not found, as expected" + echo "✅ Critical TODOs not found, as expected" else - echo "❌Critical TODOs have been found where they should be missing!" >&2 + echo "❌ Critical TODOs have been found where they should be missing!" >&2 exit 1 fi diff --git a/actions/find-critical-todos/README.md b/actions/find-critical-todos/README.md index dc40349d6..e235e2c72 100644 --- a/actions/find-critical-todos/README.md +++ b/actions/find-critical-todos/README.md @@ -2,36 +2,46 @@ Looks for _critical TODOs_ - that is, instances of the `TODO!` string - in source files. -## 🃏Example +## 🃏 Example ```yaml steps: - uses: giancosta86/aurora-github/actions/find-critical-todos@v10 with: - source-file-regex: ^\.\/(src|tests)\/.+\.(c|m)?(j|t)sx?$ + source-file-regex: ^(src|tests)\/.+\.(c|m)?(j|t)sx?$ ``` -**Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md), [verify-npm-package](../verify-npm-package/README.md) and [verify-rust-wasm](../verify-rust-wasm/README.md). +**Please, note**: this action is automatically run by: -## 📥Inputs +- [verify-rust-crate](../verify-rust-crate/README.md) + +- [verify-npm-package](../verify-npm-package/README.md) + +- [verify-rust-wasm](../verify-rust-wasm/README.md) -| Name | Type | Description | Default value | -| :-----------------: | :---------: | :---------------------------------------------------------------: | :-----------: | -| `source-file-regex` | **string** | The **PCRE** pattern of source file names, for the `find` command | | -| `crash-on-found` | **boolean** | If **true**, exits with error when critical TODOs are found | **true** | -| `display-lines` | **boolean** | Display the lines with critical TODOs | **true** | -| `verbose` | **boolean** | Show details such as the filterable paths | **false** | -| `root-directory` | **string** | The root of the directory tree | **.** | +- [verify-jvm-project](../verify-jvm-project/README.md) -**Please, note**: `source-file-regex` should be designed keeping in mind that it will be applied to a path _always_ relative to `root-directory` and _always_ starting with `./`. +- [verify-python-package](../verify-python-package/README.md) -## 📤Outputs +## 📥 Inputs + +| Name | Type | Description | Default value | +| :-----------------: | :---------: | :---------------------------------------------------------: | :-----------: | +| `source-file-regex` | **string** | The **Perl regex** to select the source file names | | +| `crash-on-found` | **boolean** | If **true**, exits with error when critical TODOs are found | **true** | +| `display-lines` | **boolean** | Display the lines with critical TODOs | **true** | +| `verbose` | **boolean** | Show details such as the filterable paths | **false** | +| `root-directory` | **string** | The root of the directory tree | **.** | + +**Please, note**: `source-file-regex` should be designed keeping in mind that it will be applied to a path _always_ relative to `root-directory` and _never_ starting with `./`. + +## 📤 Outputs | Name | Type | Description | Example | | :-----: | :---------: | :--------------------------------------------------------------------------: | :-------: | | `found` | **boolean** | **true** if at least one `TODO!` was found in some file, **false** otherwise | **false** | -## 🌐Further references +## 🌐 Further references - [verify-rust-crate](../verify-rust-crate/README.md) @@ -39,4 +49,8 @@ steps: - [verify-rust-wasm](../verify-rust-wasm/README.md) +- [verify-jvm-project](../verify-jvm-project/README.md) + +- [verify-python-package](../verify-python-package/README.md) + - [aurora-github](../../README.md) diff --git a/actions/find-critical-todos/action.yml b/actions/find-critical-todos/action.yml index 874cdecfc..d10a9c65b 100644 --- a/actions/find-critical-todos/action.yml +++ b/actions/find-critical-todos/action.yml @@ -1,9 +1,9 @@ name: Find critical TODOs -description: Looks for "critical TODOs" - that is, instances of the "TODO!" string - in source files. +description: Looks for "critical TODOs" - that is, instances of the `TODO!` string - in source files. inputs: source-file-regex: - description: The PCRE pattern of source file names, for the "find" command. + description: The Perl regex to select the source file names. crash-on-found: description: If true, exits with error when critical TODOs are found. @@ -23,97 +23,30 @@ inputs: outputs: found: - description: true if at least one TODO! was found in some file, false otherwise. + description: true if at least one `TODO!` was found in some file, false otherwise. value: ${{ steps.main.outputs.found }} runs: using: composite steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - id: main - shell: bash + shell: elvish {0} working-directory: ${{ inputs.root-directory }} run: | - todoText="TODO!" - - main() { - validateInputs - scanDirectoryTree - } - - validateInputs() { - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.crash-on-found }}" ]] - then - echo "❌Missing action input: 'crash-on-found'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.display-lines }}" ]] - then - echo "❌Missing action input: 'display-lines'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.verbose }}" ]] - then - echo "❌Missing action input: 'verbose'!" >&2 - exit 1 - fi - } - - scanDirectoryTree() { - local sourceFileRegex="${{ inputs.source-file-regex }}" - echo "📥Source file regex: '$sourceFileRegex'" - - local crashOnFound="${{ inputs.crash-on-found }}" - echo "📥Crash on found: $crashOnFound" - - local displayLines="${{ inputs.display-lines }}" - echo "📥Display lines: $displayLines" - - local verbose="${{ inputs.verbose }}" - echo "📥Verbose: $verbose" - - if [[ "$verbose" == "true" ]] - then - echo "🔎File list to filter when looking for TODOs:" - find -type f -print - echo "🔎🔎🔎" - fi + use aurora-github/critical-todos + use aurora-github/ci-cd/input + use aurora-github/ci-cd/output - if [[ "$displayLines" == "true" ]] - then - local quietArg="" - else - local quietArg="-q" - fi + var found = (critical-todos:find [ + &source-file-regex=(input:string source-file-regex '${{ inputs.source-file-regex }}') - if find -type f -print0 | grep -zP "$sourceFileRegex" | xargs -0 grep --color=always -Hn $quietArg "$todoText" - then - local found=true - else - local found=false - fi + &crash-on-found=(input:bool crash-on-found '${{ inputs.crash-on-found }}') - if [[ "$found" == "true" ]] - then - if [[ "$crashOnFound" == "true" ]] - then - echo "❌Critical TODOs found!" >&2 - exit 1 - else - echo "⚠️Critical TODOs found!" - fi - else - echo "✅No critical TODOs found!" - fi + &display-lines=(input:bool display-lines '${{ inputs.display-lines }}') - echo "found=$found" >> $GITHUB_OUTPUT - } + &verbose=(input:bool verbose '${{ inputs.verbose }}') + ]) - main + output:write found $found From b7f27a3958678224f6356d39083db92ad4385e57 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 13 Apr 2025 15:31:08 +0200 Subject: [PATCH 16/63] Refactor and enhance `install-via-sdkman`: * run curl without progress notifications --- .../test-install-via-sdkman/action.yml | 36 +++------ actions/install-via-sdkman/README.md | 10 +-- actions/install-via-sdkman/action.yml | 74 +++---------------- 3 files changed, 28 insertions(+), 92 deletions(-) diff --git a/.github/test-actions/test-install-via-sdkman/action.yml b/.github/test-actions/test-install-via-sdkman/action.yml index d76be9c8c..ed17b70d0 100644 --- a/.github/test-actions/test-install-via-sdkman/action.yml +++ b/.github/test-actions/test-install-via-sdkman/action.yml @@ -10,36 +10,28 @@ runs: echo "expectedGradleVersion=8.2.1" >> $GITHUB_ENV - shell: bash - run: echo "🎭Checking the test environment..." + run: echo "🎭 Checking the test environment..." - name: Ensure the expected Maven version is not installed shell: bash run: | - mavenVersion="$(mvn --version | head -n 1 | cut -d ' ' -f 3)" - - echo "🔎Initial Maven version: '$mavenVersion'" - - if [[ "$mavenVersion" == "$expectedMavenVersion" ]] + if mvn --version | grep --color=always "$expectedMavenVersion" then - echo "❌The installed Maven version coincides with the expected one, making the test meaningless" + echo "❌ The installed Maven version coincides with the expected one, making the test meaningless" exit 1 fi - name: Ensure the expected Gradle version is not installed shell: bash run: | - gradleVersion="$(gradle --version | grep "Gradle" | head -n 1 | cut -d ' ' -f 2)" - - echo "🔎Initial Gradle version: '$gradleVersion'" - - if [[ "$gradleVersion" == "$expectedGradleVersion" ]] + if gradle --version | grep --color=always "$expectedGradleVersion" then - echo "❌The installed Gradle version coincides with the expected one, making the test meaningless" + echo "❌ The installed Gradle version coincides with the expected one, making the test meaningless" exit 1 fi - shell: bash - run: echo "🎭Installing 🪶Maven..." + run: echo "🎭 Installing 🪶 Maven..." - uses: ./actions/install-via-sdkman with: @@ -49,20 +41,16 @@ runs: - name: The expected Maven version should be available shell: bash run: | - mavenVersion="$(mvn --version | head -n 1 | cut -d ' ' -f 3)" - - echo "🔎Detected Maven version: '$mavenVersion'" - - if [[ "$mavenVersion" == "$expectedMavenVersion" ]] + if mvn --version | grep --color=always "$expectedMavenVersion" then - echo "✅The requested Maven version is ready!" + echo "✅ The requested Maven version is ready!" else - echo "❌The expected Maven version ('$expectedMavenVersion') is not installed!" >&2 + echo "❌ The expected Maven version ('$expectedMavenVersion') is not installed!" >&2 exit 1 fi - shell: bash - run: echo "🎭Installing 🐘Gradle..." + run: echo "🎭 Installing 🐘 Gradle..." - uses: ./actions/install-via-sdkman with: @@ -74,8 +62,8 @@ runs: run: | if gradle --version | grep --color=always "$expectedGradleVersion" then - echo "✅The requested Gradle version is ready!" + echo "✅ The requested Gradle version is ready!" else - echo "❌The expected Gradle version ('$expectedGradleVersion') is not installed!" >&2 + echo "❌ The expected Gradle version ('$expectedGradleVersion') is not installed!" >&2 exit 1 fi diff --git a/actions/install-via-sdkman/README.md b/actions/install-via-sdkman/README.md index 1a3da7f32..e1cffcc7d 100644 --- a/actions/install-via-sdkman/README.md +++ b/actions/install-via-sdkman/README.md @@ -2,7 +2,7 @@ Installs the requested SDK using [SDKMAN!](https://sdkman.io/) -## 🃏Example +## 🃏 Example ```yaml steps: @@ -12,7 +12,7 @@ steps: version: 23-open ``` -## 💡How it works +## 💡 How it works 1. If SDKMAN is not installed, download and install it. @@ -20,7 +20,7 @@ steps: **Please, note**: all the SDKS are installed to `$HOME/.sdkman/candidates` -## ☑️Requirements +## ☑️ Requirements - `candidate` must be an identifier belonging to the [list of SDKs](https://sdkman.io/sdks) supported by SDKMAN. @@ -32,14 +32,14 @@ steps: where `candidate` is the identifier of the required SDK. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :---------: | :--------: | :------------------------------------------: | :-----------: | | `candidate` | **string** | Identifier (in SDKMAN) of the SDK to install | | | `version` | **string** | Identifier (in SDKMAN) of the version | | -## 🌐Further references +## 🌐 Further references - [SDKMAN!](https://sdkman.io/) diff --git a/actions/install-via-sdkman/action.yml b/actions/install-via-sdkman/action.yml index 63ff17ba9..ff36f7921 100644 --- a/actions/install-via-sdkman/action.yml +++ b/actions/install-via-sdkman/action.yml @@ -1,4 +1,4 @@ -name: Install SDK via SDKMAN. +name: Install SDK via SDKMAN description: Installs the requested SDK using SDKMAN. inputs: @@ -11,70 +11,18 @@ inputs: runs: using: composite steps: - - shell: bash - run: | - sdkmanHome="$HOME/.sdkman" - - main() { - validateInputs - - ensureSdkmanInstalled - - ensureSdkmanReady - - installCandidateSdk - } - - validateInputs() { - if [[ -z "${{ inputs.candidate }}" ]] - then - echo "❌Missing action input: 'candidate'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.version }}" ]] - then - echo "❌Missing action input: 'version'!" >&2 - exit 1 - fi - } - - ensureSdkmanInstalled() { - if [[ -d "$sdkmanHome" ]] - then - echo "☑It seems that SDKMAN was previously installed" - return 0 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - echo "📥Installing SDKMAN..." - - curl -s "https://get.sdkman.io" | bash - - echo "✅SDKMAN installed!" - } - - ensureSdkmanReady() { - if ! type -t sdk - then - echo '🚀Setting up SDKMAN...' - - source "$sdkmanHome/bin/sdkman-init.sh" - fi - - echo "✅SDKMAN ready!" - } - - installCandidateSdk() { - local candidate="${{ inputs.candidate }}" - local version="${{ inputs.version }}" - - echo "📥Installing $candidate($version)..." + - shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/curl + use aurora-github/ci-cd/input + use aurora-github/jvm/sdkman - sdk install "$candidate" "$version" + var candidate = (input:string candidate '${{ inputs.candidate }}') - echo "PATH=$sdkmanHome/candidates/candidate/$version:$PATH" >> $GITHUB_ENV + var version = (input:string version '${{ inputs.version }}') - echo "✅$candidate($version) installed!" - } + curl:disable-non-error-output - main + sdkman:install-sdk $candidate $version From fcfd67d808eeaf2245ffb1122f6980d2db62e0c7 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 13 Apr 2025 18:21:34 +0200 Subject: [PATCH 17/63] Refactor `extract-rust-snippets` --- .../test-extract-rust-snippets/action.yml | 47 ++++++++---------- actions/extract-rust-snippets/README.md | 48 +++++++++---------- actions/extract-rust-snippets/action.yml | 30 +++++------- 3 files changed, 57 insertions(+), 68 deletions(-) diff --git a/.github/test-actions/test-extract-rust-snippets/action.yml b/.github/test-actions/test-extract-rust-snippets/action.yml index 90cd15e48..52de15e86 100644 --- a/.github/test-actions/test-extract-rust-snippets/action.yml +++ b/.github/test-actions/test-extract-rust-snippets/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Trying to generate test files from Markdown file without snippets..." + run: echo "🎭 Trying to generate test files from Markdown file without snippets..." - uses: ./actions/extract-rust-snippets with: @@ -12,29 +12,22 @@ runs: markdown-file: NO-SNIPPETS.md test-filename-prefix: fake-tests/fake- - - name: The Rust test files should not have been created + - name: The test directory should not have been created shell: bash - working-directory: tests/unknown-tech/fake-tests + working-directory: tests/unknown-tech run: | - echo "🔎Inspecting the 'fake-tests' subdirectory..." - ls -1 2>/dev/null - echo "🔎🔎🔎" - - files="$(ls -1 2>/dev/null)" - - if [[ -z "$files" ]] + if [[ ! -d fake-tests ]] then - echo "✅No generated test files, as expected" + echo "✅ The test directory was not even created, as expected!" else - echo "❌There should be no generated test files!" >&2 + echo "❌ The test directory should not have been created!" exit 1 fi - shell: bash - run: echo "🎭Generating test files from Markdown file with snippets..." + run: echo "🎭 Generating test files from Markdown file with snippets..." - - name: - uses: ./actions/extract-rust-snippets + - uses: ./actions/extract-rust-snippets with: project-directory: tests/unknown-tech @@ -42,23 +35,23 @@ runs: shell: bash working-directory: tests/unknown-tech/tests run: | - echo "🔎Inspecting the 'tests' subdirectory..." + echo "🔎 Inspecting the 'tests' subdirectory..." ls -1 2>/dev/null echo "🔎🔎🔎" - echo "🔎Now ensuring all the 3 test files exist..." + echo "🔎 Now ensuring all the 3 test files exist..." for i in {1..3} do testFile="readme_test_${i}.rs" if [[ ! -f "$testFile" ]] then - echo "❌Missing Rust test file: '$testFile'" + echo "❌ Missing Rust test file: '$testFile'" exit 1 fi done - echo "✅All the test files exist!" + echo "✅ All the test files exist!" - name: The first test must contain the expected Rust source code shell: bash @@ -66,14 +59,14 @@ runs: run: | testFile="readme_test_1.rs" - echo "🔎Inspecting the content of the first test file..." + echo "🔎 Inspecting the content of the first test file..." cat $testFile echo "🔎🔎🔎" grep -q 'Alpha' "$testFile" grep -q 'Beta' "$testFile" grep -q 'main().unwrap();' "$testFile" - echo "✅First test file OK!" + echo "✅ First test file OK!" - name: The second test must contain the expected Rust source code shell: bash @@ -81,7 +74,7 @@ runs: run: | testFile="readme_test_2.rs" - echo "🔎Inspecting the content of the second test file..." + echo "🔎 Inspecting the content of the second test file..." cat $testFile echo "🔎🔎🔎" @@ -89,7 +82,7 @@ runs: grep -q 'Delta' "$testFile" grep -q 'Epsilon' "$testFile" grep -q 'main().unwrap();' "$testFile" - echo "✅Second test file OK!" + echo "✅ Second test file OK!" - name: The third test must contain the expected Rust source code shell: bash @@ -97,21 +90,21 @@ runs: run: | testFile="readme_test_3.rs" - echo "🔎Inspecting the content of the third test file..." + echo "🔎 Inspecting the content of the third test file..." cat $testFile echo "🔎🔎🔎" grep -q 'Sigma' "$testFile" grep -q 'Tau' "$testFile" grep -q 'main().unwrap();' "$testFile" - echo "✅Third test file OK!" + echo "✅ Third test file OK!" - shell: bash - run: echo "🎭Trying to generate test files from inexisting Markdown file..." + run: echo "🎭 Trying to generate test files from inexistent Markdown file..." - uses: ./actions/extract-rust-snippets with: project-directory: tests/unknown-tech - markdown-file: JUST_INEXISTING_MARKDOWN.md + markdown-file: JUST_INEXISTENT_MARKDOWN.md optional: true test-filename-prefix: fake-tests/fake- diff --git a/actions/extract-rust-snippets/README.md b/actions/extract-rust-snippets/README.md index 49a1da7eb..2de0dbb19 100644 --- a/actions/extract-rust-snippets/README.md +++ b/actions/extract-rust-snippets/README.md @@ -1,8 +1,8 @@ # extract-rust-snippets -Extracts **Rust** code snippets from a **Markdown** file to standalone test files. +Extracts **Rust** test snippets from a **Markdown** file to standalone test files. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,26 +11,9 @@ steps: **Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md). -## ☑️Requirements +## 💡 How it works -- each code snippet must reside in a ` ```rust` code block and must define a `main()`function returning a type supporting`.unwrap()`, such as `Result<(), Box`: - - ```rust - //Required imports here - use std::error::Error; - - fn main() -> Result<(), Box> { - //Code with assertions here - - Ok(()) - } - ``` - -- the Markdown file must exist unless `optional` is set to **true**, but it is never required to contain code snippets: in this case, no test file will be generated. - -## 💡How it works - -The action extracts each Rust snippet from the given Markdown file, creating a test file containing: +The action extracts Rust snippets from the given Markdown file, creating for each a test file containing: - the snippet itself @@ -49,16 +32,33 @@ Each test file has this relative path: where `N` is the position of the snippet within the Markdown content, starting from **1**. -## 📥Inputs +## ☑️ Requirements + +- each code snippet must reside in a `rust` triple-back-quoted code block and must define a `main()`function returning a type supporting`.unwrap()`, such as `Result<(), Box`: + + ```rust + //Required imports here + use std::error::Error; + + fn main() -> Result<(), Box> { + //Code with assertions here + + Ok(()) + } + ``` + +- the Markdown file must exist unless `optional` is set to **true**, but it is never required to contain code snippets: in this case, no test file will be generated. + +## 📥 Inputs | Name | Type | Description | Default value | | :--------------------: | :---------: | :--------------------------------------------------------: | :---------------------: | | `markdown-file` | **string** | Relative path of the Markdown file containing the snippets | **README.md** | -| `optional` | **boolean** | Just do nothing if `mardown-file` does not exist | **false** | +| `optional` | **boolean** | Just do nothing if `markdown-file` does not exist | **false** | | `test-filename-prefix` | **string** | Relative path prefix for each generated test source file | **tests/readme_test\_** | | `project-directory` | **string** | The directory containing Cargo.toml | **.** | -## 🌐Further references +## 🌐 Further references - [verify-rust-crate](../verify-rust-crate/README.md) diff --git a/actions/extract-rust-snippets/action.yml b/actions/extract-rust-snippets/action.yml index d2adc6aff..22cc53089 100644 --- a/actions/extract-rust-snippets/action.yml +++ b/actions/extract-rust-snippets/action.yml @@ -1,4 +1,4 @@ -name: Extract Rust code snippets +name: Extract Rust test snippets description: Extracts Rust code snippets from a Markdown file to standalone test files. inputs: @@ -7,7 +7,7 @@ inputs: default: README.md optional: - description: Just do nothing if `mardown-file` does not exist. + description: Just do nothing if `markdown-file` does not exist. default: false test-filename-prefix: @@ -21,22 +21,18 @@ inputs: runs: using: composite steps: - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_MARKDOWN_FILE=${{ inputs.markdown-file }}" >> $GITHUB_ENV - echo "INPUT_OPTIONAL=${{ inputs.optional }}" >> $GITHUB_ENV - echo "INPUT_TEST_FILENAME_PREFIX=${{ inputs.test-filename-prefix }}" >> $GITHUB_ENV - echo "INPUT_PROJECT_DIRECTORY=${{ inputs.project-directory }}" >> $GITHUB_ENV + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Call the Python implementation - shell: python + - shell: elvish {0} + working-directory: ${{ inputs.project-directory }} run: | - from core.extract_rust_snippets import extract_rust_snippets, Inputs + use aurora-github/ci-cd/input + use aurora-github/rust/snippets + + snippets:extract [ + &markdown-path=(input:file &can-be-missing markdown-file '${{ inputs.markdown-file }}') - inputs = Inputs.from_env() + &optional=(input:bool optional '${{ inputs.optional }}') - extract_rust_snippets(inputs) + &test-filename-prefix=(input:string test-filename-prefix '${{ inputs.test-filename-prefix }}') + ] From 1d344d40d5c42e6fceae21e12c28c5bfb47c78d5 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 13 Apr 2025 19:04:57 +0200 Subject: [PATCH 18/63] Refactor and enhance `upload-release-assets`: * the `files` input now must be explicitly comma-separated --- .../test-upload-release-assets/action.yml | 36 +++++++------ actions/upload-release-assets/README.md | 24 ++++----- actions/upload-release-assets/action.yml | 53 +++++-------------- 3 files changed, 46 insertions(+), 67 deletions(-) diff --git a/.github/test-actions/test-upload-release-assets/action.yml b/.github/test-actions/test-upload-release-assets/action.yml index 123c1d73d..f33d166db 100644 --- a/.github/test-actions/test-upload-release-assets/action.yml +++ b/.github/test-actions/test-upload-release-assets/action.yml @@ -13,30 +13,34 @@ runs: - shell: bash run: | - echo "🧹Preventively deleting the '$tag' release and its tag, if existing..." + echo "🧹 Preventively deleting the '$tag' release and its tag, if existing..." if gh release delete --cleanup-tag --yes "$tag" > /dev/null 2>&1 then - echo "✅Pre-existing test release and tag deleted!" + echo "✅ Pre-existing test release does not exist now!" fi + + git tag --delete "$tag" > /dev/null 2>&1 || true + git push origin --delete "$tag" > /dev/null 2>&1 || true + echo "✅ Pre-existing tag does not exist now!" env: GH_TOKEN: ${{ github.token }} - shell: bash run: | - echo "📝Now creating a '$tag' tag just for the tests..." + echo "📝 Now creating a '$tag' tag just for the tests..." git tag -f "$tag" git push origin "$tag" - echo "✅Test tag created!" + echo "✅ Test tag created!" - echo "📝Now creating a '$tag' draft release just for the tests..." + echo "📝 Now creating a '$tag' draft release just for the tests..." gh release create "$tag" --draft --title "Test release" --notes "This volatile release is only used by the tests!" - echo "✅Test release created!" + echo "✅ Test release created!" env: GH_TOKEN: ${{ github.token }} - shell: bash - run: echo "🎭Uploading a single asset..." + run: echo "🎭 Uploading a single asset..." - uses: ./actions/upload-release-assets with: @@ -45,16 +49,16 @@ runs: files: index.html - shell: bash - run: echo "🎭Uploading multiple assets - also using quotes..." + run: echo "🎭 Uploading multiple assets..." - uses: ./actions/upload-release-assets with: release-tag: ${{ env.tag }} source-directory: website/nextjs/public - files: next.svg 'favicon.png' + files: next.svg, favicon.png - shell: bash - run: echo "🎭Re-uploading one of the previous assets..." + run: echo "🎭 Re-uploading one of the previous assets..." - uses: ./actions/upload-release-assets with: @@ -64,7 +68,7 @@ runs: overwrite: true - shell: bash - run: echo "🎭Downloading the uploaded assets..." + run: echo "🎭 Downloading the uploaded assets..." - shell: bash run: | @@ -76,7 +80,7 @@ runs: GH_TOKEN: ${{ github.token }} - shell: bash - run: echo "🎭Checking the integrity of the downloaded assets..." + run: echo "🎭 Checking the integrity of the downloaded assets..." - shell: bash run: | @@ -89,9 +93,9 @@ runs: if cmp -s "$downloadedFile" "$originalFile" then - echo "✅Asset '$fileName' is OK!" + echo "✅ Asset '$fileName' from '$sourceDirectory' is OK!" else - echo "❌Asset '$fileName' is corrupted!" + echo "❌ Asset '$fileName' from '$sourceDirectory' is corrupted!" exit 1 fi } @@ -102,8 +106,8 @@ runs: - shell: bash run: | - echo "🚮Now deleting the '$tag' release and its tag..." + echo "🚮 Now deleting the '$tag' release and its tag..." gh release delete --cleanup-tag --yes "$tag" - echo "✅Test release and tag deleted!" + echo "✅ Test release and tag deleted!" env: GH_TOKEN: ${{ github.token }} diff --git a/actions/upload-release-assets/README.md b/actions/upload-release-assets/README.md index 25f926748..f7bc81dfe 100644 --- a/actions/upload-release-assets/README.md +++ b/actions/upload-release-assets/README.md @@ -2,7 +2,7 @@ Uploads one or more asset files to a **GitHub** release. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -13,26 +13,26 @@ steps: source-directory: dist ``` -## 💡How it works +## 💡 How it works This action calls the [gh release upload](https://cli.github.com/manual/gh_release_upload) command to upload the given files. -## ☑️Requirements +## ☑️ Requirements - The `release-tag` input must be the tag of an existing release - which could be, for example, the `release-tag` output of the [tag-and-release](../tag-and-release/README.md) action. -- The `files` input must be passed just like a string of relative file paths on the Bash command line. +- The `files` input must be comma-separated -## 📥Inputs +## 📥 Inputs -| Name | Type | Description | Default value | -| :----------------: | :---------: | :---------------------------------------------: | :-----------: | -| `release-tag` | **string** | The tag of the target release | | -| `files` | **string** | The relative paths of the asset files to upload | | -| `overwrite` | **boolean** | Overwrite existing assets in the release | **true** | -| `source-directory` | **string** | Directory containing the files | **.** | +| Name | Type | Description | Default value | +| :----------------: | :-------------------------: | :-----------------------------------------: | :-----------: | +| `release-tag` | **string** | The tag of the target release | | +| `files` | **string**, comma-separated | Relative paths of the asset files to upload | | +| `overwrite` | **boolean** | Overwrite existing assets in the release | **true** | +| `source-directory` | **string** | Directory containing the files | **.** | -## 🌐Further references +## 🌐 Further references - [tag-and-release](../tag-and-release/README.md) diff --git a/actions/upload-release-assets/action.yml b/actions/upload-release-assets/action.yml index 836201d4e..ce9f41e16 100644 --- a/actions/upload-release-assets/action.yml +++ b/actions/upload-release-assets/action.yml @@ -6,7 +6,7 @@ inputs: description: The tag of the target release. files: - description: The relative paths of the asset files to upload. + description: Comma-separated relative paths of the asset files to upload. overwrite: description: Overwrite existing assets in the release. @@ -19,45 +19,20 @@ inputs: runs: using: composite steps: - - shell: bash + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - shell: elvish {0} working-directory: ${{ inputs.source-directory }} run: | - main() { - validateInputs - publishReleaseAssets - } - - validateInputs() { - if [[ -z "${{ inputs.release-tag }}" ]] - then - echo "❌Missing action input: 'release-tag'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.files }}" ]] - then - echo "❌Missing action input: 'files'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.overwrite }}" ]] - then - echo "❌Missing action input: 'overwrite'!" >&2 - exit 1 - fi - } - - publishReleaseAssets() { - if [[ "${{ inputs.overwrite }}" == "true" ]] - then - local clobberArg="--clobber" - else - local clobberArg="" - fi - - gh release upload $clobberArg "${{ inputs.release-tag }}" ${{ inputs.files }} - } - - main + use aurora-github/ci-cd/input + use aurora-github/ci-cd/release-assets + + release-assets:publish [ + &release-tag=(input:string release-tag '${{ inputs.release-tag }}') + + &files=(input:list '${{ inputs.files }}') + + &overwrite=(input:bool overwrite '${{ inputs.overwrite }}') + ] env: GH_TOKEN: ${{ github.token }} From c342726bb2b6c2bdc11630e182fe103b8783f992 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Mon, 14 Apr 2025 05:52:45 +0200 Subject: [PATCH 19/63] Refactor `check-subpath-exports` --- .../test-check-subpath-exports/action.yml | 34 +++---- .../scenario/action.yml | 10 +- actions/check-subpath-exports/README.md | 8 +- actions/check-subpath-exports/action.yml | 92 ++----------------- .../src/alpha/{index.ts => index.d.ts} | 0 .../src/{omega/index.ts => alpha/index.js} | 0 tests/npm-package/src/beta/index.d.ts | 0 tests/npm-package/src/omega/index.js | 0 8 files changed, 36 insertions(+), 108 deletions(-) rename tests/npm-package/src/alpha/{index.ts => index.d.ts} (100%) rename tests/npm-package/src/{omega/index.ts => alpha/index.js} (100%) create mode 100644 tests/npm-package/src/beta/index.d.ts create mode 100644 tests/npm-package/src/omega/index.js diff --git a/.github/test-actions/test-check-subpath-exports/action.yml b/.github/test-actions/test-check-subpath-exports/action.yml index 793dc4ad6..cad6cf9d7 100644 --- a/.github/test-actions/test-check-subpath-exports/action.yml +++ b/.github/test-actions/test-check-subpath-exports/action.yml @@ -3,54 +3,54 @@ name: Test check-subpath-exports runs: using: composite steps: - - name: When the "exports" field is missing - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" field is missing... jq-operation: del(.exports) - - name: When the "exports" field is present but empty - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" field is present but empty... jq-operation: .exports={} - - name: When the "exports" field is a string - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" field is a string... jq-operation: .exports="./src/beta/index.js" - - name: When the "exports" field contains a string field - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" object contains a string field... jq-operation: | .exports={ "beta": "./src/beta/index.js" } - - name: When the "exports" field contains an object field - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" object contains an object field... jq-operation: | .exports={ "beta": { - "import": "./src/beta/index.js", + "import": "./src/beta/index.d.ts", "default": "./src/beta/index.js" } } - - name: When the "exports" field contains multiple fields - uses: ./.github/test-actions/test-check-subpath-exports/scenario + - uses: ./.github/test-actions/test-check-subpath-exports/scenario with: + header: Test when the "exports" object contains multiple fields... jq-operation: | .exports={ "alpha": { - "import": "./src/alpha/index.ts", - "default": "./src/alpha/index.ts" + "types": "./src/alpha/index.d.ts", + "default": "./src/alpha/index.js" }, "beta": { - "import": "./src/beta/index.js", + "types": "./src/beta/index.d.ts", "default": "./src/beta/index.js" }, - "omega": "./src/omega/index.ts" + "omega": "./src/omega/index.js" } diff --git a/.github/test-actions/test-check-subpath-exports/scenario/action.yml b/.github/test-actions/test-check-subpath-exports/scenario/action.yml index f192551a4..ce1100b77 100644 --- a/.github/test-actions/test-check-subpath-exports/scenario/action.yml +++ b/.github/test-actions/test-check-subpath-exports/scenario/action.yml @@ -1,12 +1,18 @@ name: Update and check subpath exports in the npm test project inputs: + header: + description: Header for this scenario. + jq-operation: - description: The operation altering the package.json descriptor + description: The jq operation altering the package.json descriptor. runs: using: composite steps: + - shell: bash + run: echo "🎭 ${{inputs.header}}" + - name: Run the requested jq operation on package.json shell: bash working-directory: tests/npm-package @@ -14,7 +20,7 @@ runs: jq '${{ inputs.jq-operation }}' package.json > package.json.temp mv package.json.temp package.json - echo "🔎The 'exports' field is:" + echo "🔎 The 'exports' field is:" jq -C '.exports' package.json - uses: ./actions/check-subpath-exports diff --git a/actions/check-subpath-exports/README.md b/actions/check-subpath-exports/README.md index 4886d94ae..4cf2cc2a0 100644 --- a/actions/check-subpath-exports/README.md +++ b/actions/check-subpath-exports/README.md @@ -2,7 +2,7 @@ Verifies that all the [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) in **package.json** actually match existing files. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,17 +11,17 @@ steps: **Please, note**: this action is automatically run by [verify-npm-package](../verify-npm-package/README.md). -## ☑️Requirements +## ☑️ Requirements - The `jq` command (especially version **1.7**) must be available in the operating system. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------: | :--------: | :-------------------------------------: | :-----------: | | `project-directory` | **string** | The directory containing `package.json` | **.** | -## 🌐Further references +## 🌐 Further references - [package.json - subpath exports](https://nodejs.org/api/packages.html#subpath-exports) diff --git a/actions/check-subpath-exports/action.yml b/actions/check-subpath-exports/action.yml index 9030edba2..2f6b980b4 100644 --- a/actions/check-subpath-exports/action.yml +++ b/actions/check-subpath-exports/action.yml @@ -3,95 +3,17 @@ description: Verifies that all the subpath exports in package.json actually matc inputs: project-directory: - description: The directory containing package.json. - default: "." + description: The directory containing `package.json`. + default: . runs: using: composite steps: - - shell: bash + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - main() { - validateInputs - - inspectRecursively - - echo "✅Export subpaths are OK!" - } - - validateInputs() { - if [[ ! -f "package.json" ]] - then - echo "❌The package.json descriptor file does not exist!" >&2 - exit 1 - fi - } - - inspectRecursively() { - local exports="$(jq -r '.exports' package.json)" - - if [[ "$exports" == "null" ]] - then - echo "💭No exports declared in package.json..." - exit 0 - fi - - echo "🔎 Now inspecting subpath exports..." - - checkJsonValue "exports" "$exports" - } - - checkJsonValue() { - local positionInJson="$1" - local jsonValue="$2" - - if isJsonObject "$jsonValue" - then - checkJsonObject "$positionInJson" "$jsonValue" - else - checkFilePattern "$positionInJson" "$jsonValue" - fi - } - - isJsonObject() { - local jsonValue="$1" - - if [[ "$jsonValue" =~ ^\{ ]] - then - return 0 - else - return 1 - fi - } - - checkJsonObject() { - local positionInJson="$1" - local jsonObject="$2" - - echo "$jsonObject" | jq -r '. | keys[]' | while IFS= read -r key - do - local jsonValue="$(echo "$jsonObject" | jq -r '.["'$key'"]')" - checkJsonValue "$positionInJson -> $key" "$jsonValue" - done - } - - checkFilePattern() { - local positionInJson="$1" - local filePattern="$2" - - echo -n "🔎 $positionInJson -> $filePattern... " - - local matches="$(find . -wholename "$filePattern" | wc -l)" - - if [[ "$matches" == 0 ]] - then - echo "❌" - echo "❌No file matching subpath pattern: '$filePattern'" - exit 1 - else - echo "✅" - fi - } + use aurora-github/nodejs/subpath-exports - main + subpath-exports:check diff --git a/tests/npm-package/src/alpha/index.ts b/tests/npm-package/src/alpha/index.d.ts similarity index 100% rename from tests/npm-package/src/alpha/index.ts rename to tests/npm-package/src/alpha/index.d.ts diff --git a/tests/npm-package/src/omega/index.ts b/tests/npm-package/src/alpha/index.js similarity index 100% rename from tests/npm-package/src/omega/index.ts rename to tests/npm-package/src/alpha/index.js diff --git a/tests/npm-package/src/beta/index.d.ts b/tests/npm-package/src/beta/index.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/npm-package/src/omega/index.js b/tests/npm-package/src/omega/index.js new file mode 100644 index 000000000..e69de29bb From f4440ce078cc7206baf09fe307c20539e3097b73 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Mon, 21 Apr 2025 20:09:25 +0200 Subject: [PATCH 20/63] Refactor `inject-subpath-exports` --- .../test-inject-subpath-exports/action.yml | 70 +++------ .../scenario/action.yml | 98 +++++++++++++ actions/inject-subpath-exports/README.md | 10 +- actions/inject-subpath-exports/action.yml | 135 ++---------------- 4 files changed, 136 insertions(+), 177 deletions(-) create mode 100644 .github/test-actions/test-inject-subpath-exports/scenario/action.yml diff --git a/.github/test-actions/test-inject-subpath-exports/action.yml b/.github/test-actions/test-inject-subpath-exports/action.yml index cd6c48f54..6d06c0324 100644 --- a/.github/test-actions/test-inject-subpath-exports/action.yml +++ b/.github/test-actions/test-inject-subpath-exports/action.yml @@ -21,29 +21,19 @@ runs: run: rm -f "$rootIndexFile" - shell: bash - run: echo "🎭Run with prefer-index mode - when the root index file is missing..." + run: echo "🎭 Run with prefer-index mode - when the root index file is missing..." - uses: ./actions/inject-subpath-exports with: project-directory: tests/npm-package mode: prefer-index - - name: A subpath export should be generated for each subdirectory - shell: bash - working-directory: tests/npm-package - run: | - echo "🔎Asserting injected subpath exports from subdirectories only..." - grep -q '"./alpha"' package.json - grep -q '"./beta"' package.json - grep -q '"./omega"' package.json - - [[ "$(jq -r '.exports."./alpha".types' package.json)" == "./dist/alpha/index.d.ts" ]] || exit 1 - - [[ "$(jq -r '.exports."./alpha".import' package.json)" == "./dist/alpha/index.js" ]] || exit 1 - - ! grep -q '"."' package.json - ! grep -q '"./sigma"' package.json - echo "✅OK!" + - uses: ./.github/test-actions/test-inject-subpath-exports/scenario + with: + should-have-root: false + should-have-subdirectories: true + should-have-initial: false + working-directory: tests/npm-package - name: Restore package.json and add a root index file shell: bash @@ -53,31 +43,21 @@ runs: touch "$rootIndexFile" - shell: bash - run: echo "🎭Run again with prefer-index mode - now that the root index exists..." + run: echo "🎭 Run again with prefer-index mode - now that the root index exists..." - uses: ./actions/inject-subpath-exports with: project-directory: tests/npm-package mode: prefer-index - - name: A subpath export should be generated only for the root index - shell: bash - working-directory: tests/npm-package - run: | - echo "🔎Asserting injected subpath export for root index only..." - ! grep -q '"./alpha"' package.json - ! grep -q '"./beta"' package.json - ! grep -q '"./omega"' package.json - grep -q '"."' package.json - - [[ "$(jq -r '.exports.".".types' package.json)" == "./dist/index.d.ts" ]] || exit 1 - - [[ "$(jq -r '.exports.".".import' package.json)" == "./dist/index.js" ]] || exit 1 - - ! grep -q '"./sigma"' package.json - echo "✅OK!" + - uses: ./.github/test-actions/test-inject-subpath-exports/scenario + with: + should-have-root: true + should-have-subdirectories: false + should-have-initial: false + working-directory: tests/npm-package - - name: Restore package descriptor with custom export + - name: Restore package descriptor and add a custom export shell: bash working-directory: tests/npm-package run: | @@ -88,22 +68,16 @@ runs: mv package.json.temp package.json - shell: bash - run: echo "🎭Run action with 'all' mode..." + run: echo "🎭 Run action with 'all' mode..." - uses: ./actions/inject-subpath-exports with: project-directory: tests/npm-package mode: all - - name: Subpath exports should be generated for both root index and subdirectories - shell: bash - working-directory: tests/npm-package - run: | - echo "🔎Asserting that all exports exist, including the manual ones..." - grep -q '"./my-export"' package.json - grep -q '"./alpha"' package.json - grep -q '"./beta"' package.json - grep -q '"./omega"' package.json - grep -q '"."' package.json - ! grep -q '"./sigma"' package.json - echo "✅OK!" + - uses: ./.github/test-actions/test-inject-subpath-exports/scenario + with: + should-have-root: true + should-have-subdirectories: true + should-have-initial: true + working-directory: tests/npm-package diff --git a/.github/test-actions/test-inject-subpath-exports/scenario/action.yml b/.github/test-actions/test-inject-subpath-exports/scenario/action.yml new file mode 100644 index 000000000..e79aa3eb4 --- /dev/null +++ b/.github/test-actions/test-inject-subpath-exports/scenario/action.yml @@ -0,0 +1,98 @@ +name: Verify injected subpath exports + +inputs: + should-have-root: + description: Whether the root index file should have been injected. + + should-have-subdirectories: + description: Whether the index files in subdirectories should have been injected. + + should-have-initial: + description: Whether the initial exports should be found. + + working-directory: + description: The directory containing package.json. + +runs: + using: composite + steps: + - shell: bash + if: inputs.should-have-root == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting exports for root index file..." + + grep -q '"."' package.json || exit 1 + [[ "$(jq -r '.exports.".".types' package.json)" == "./dist/index.d.ts" ]] || exit 1 + [[ "$(jq -r '.exports.".".import' package.json)" == "./dist/index.js" ]] || exit 1 + + echo "✅ OK!" + + - shell: bash + if: inputs.should-have-root != 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting no exports for root index file..." + + ! grep -q '"."' package.json || exit 1 + + echo "✅ OK!" + + - shell: bash + if: inputs.should-have-subdirectories == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting exports for subdirectories..." + + [[ "$(jq -r '.exports."./alpha".types' package.json)" == "./dist/alpha/index.d.ts" ]] || exit 1 + [[ "$(jq -r '.exports."./alpha".import' package.json)" == "./dist/alpha/index.js" ]] || exit 1 + + [[ "$(jq -r '.exports."./beta".types' package.json)" == "./dist/beta/index.d.ts" ]] || exit 1 + [[ "$(jq -r '.exports."./beta".import' package.json)" == "./dist/beta/index.js" ]] || exit 1 + + [[ "$(jq -r '.exports."./omega".types' package.json)" == "./dist/omega/index.d.ts" ]] || exit 1 + [[ "$(jq -r '.exports."./omega".import' package.json)" == "./dist/omega/index.js" ]] || exit 1 + + echo "✅ OK!" + + - shell: bash + if: inputs.should-have-subdirectories != 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting no exports for subdirectories..." + + ! grep -q '"./alpha"' package.json || exit 1 + ! grep -q '"./beta"' package.json || exit 1 + ! grep -q '"./omega"' package.json || exit 1 + + echo "✅ OK!" + + - shell: bash + if: inputs.should-have-initial == 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting the initial exports are untouched..." + + [[ "$(jq -r '.exports."./my-export".types' package.json)" == "./dist/my-export/index.d.ts" ]] || exit 1 + [[ "$(jq -r '.exports."./my-export".import' package.json)" == "./dist/my-export/index.js" ]] || exit 1 + + echo "✅ OK!" + + - shell: bash + if: inputs.should-have-initial != 'true' + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting no initial exports have been declared..." + + ! grep -q '"./my-export"' package.json || exit 1 + + echo "✅ OK!" + + - shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + echo "🔎 Asserting no exports for second-level subdirectories..." + + ! grep -q '"./sigma"' package.json || exit 1 + + echo "✅ OK!" diff --git a/actions/inject-subpath-exports/README.md b/actions/inject-subpath-exports/README.md index 920811fc0..d8d837b01 100644 --- a/actions/inject-subpath-exports/README.md +++ b/actions/inject-subpath-exports/README.md @@ -2,14 +2,14 @@ Appends [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) to **package.json** according to the directory tree. -## 🃏Example +## 🃏 Example ```yaml steps: - uses: giancosta86/aurora-github/actions/inject-subpath-exports@v10 ``` -## 💡How it works +## 💡 How it works Appends keys to the `exports` field of a `package.json` descriptor - according to the index files in the source directory tree. @@ -45,13 +45,13 @@ The `mode` input can have the following values: - The keys reference directories under `./dist`, **not** under `./`. -## ☑️Requirements +## ☑️ Requirements - The `jq` command (especially version **1.7**) must be available in the operating system. - The `source-directory` and the root `package.json` descriptor must exist. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------: | :-------------------: | :-------------------------------------: | :--------------: | @@ -59,7 +59,7 @@ The `mode` input can have the following values: | `source-directory` | **string** | Relative path to the source directory | **src** | | `project-directory` | **string** | The directory containing `package.json` | **.** | -## 🌐Further references +## 🌐 Further references - [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) diff --git a/actions/inject-subpath-exports/action.yml b/actions/inject-subpath-exports/action.yml index 6c5dd6221..a52260135 100644 --- a/actions/inject-subpath-exports/action.yml +++ b/actions/inject-subpath-exports/action.yml @@ -3,7 +3,7 @@ description: Appends subpath exports to package.json according to the directory inputs: mode: - description: Subpath exports generation mode; can be "prefer-index" or "all". + description: Subpath exports generation mode; can be `prefer-index` or `all`. default: prefer-index source-directory: @@ -11,135 +11,22 @@ inputs: default: src project-directory: - description: The directory containing package.json. + description: The directory containing `package.json`. default: . runs: using: composite steps: - - shell: bash + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - main() { - validateInputs - displayInitialExports - performInjection - displayFinalExports - } - - validateInputs() { - case "${{ inputs.mode }}" in - "prefer-index" | all) - ;; - *) - echo "❌Invalid value for 'mode' input: '${{ inputs.mode }}'!" >&2 - exit 1 - esac - - if [[ -z "${{ inputs.source-directory }}" ]] - then - echo "❌Missing action input: 'source-directory'!" >&2 - exit 1 - fi - - if [[ ! -f "package.json" ]] - then - echo "❌The package.json descriptor file does not exist!!" >&2 - exit 1 - fi - - if [[ ! -d "${{ inputs.source-directory }}" ]] - then - echo "❌Source directory '${{ inputs.source-directory }}' does not exist!" >&2 - exit 1 - fi - } - - displayInitialExports() { - echo "📦package.json exports at the beginning of inject-subpath-exports:" - jq -C .exports package.json - } - - performInjection() { - local mode="${{ inputs.mode }}" - local sourceDirectory="${{ inputs.source-directory }}" - - local rootIndexNames=("index.ts" "index.js") - - local rootIndexName - for rootIndexName in ${rootIndexNames[@]} - do - local potentialRootIndex="$sourceDirectory/$rootIndexName" - - echo "🔎Looking for potential root index: '${potentialRootIndex}'" - - if [[ -f "$potentialRootIndex" ]] - then - local rootIndex="$potentialRootIndex" - break - fi - done - - if [[ -n "$rootIndex" ]] - then - echo "✅Root index file found: '$rootIndex'!" - - updatePackageJson "$(cat < "$tempPackage" - mv "$tempPackage" package.json - } + use aurora-github/ci-cd/input + use aurora-github/nodejs/subpath-exports - displayFinalExports() { - echo "📦package.json exports at the end of inject-subpath-exports:" - jq -C .exports package.json - } + subpath-exports:inject [ + &source-directory=(input:directory source-directory '${{ inputs.source-directory }}') - main + &mode=(input:enum mode '${{ inputs.mode }}' [prefer-index all]) + ] From 9e847c3286d945d389cb8051fe0cc4f41942afc5 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Tue, 22 Apr 2025 03:44:43 +0200 Subject: [PATCH 21/63] Refactor `install-system-packages`: * the `packages` input now only supports a **comma-separated** list --- .../test-install-system-packages/action.yml | 12 +- actions/install-system-packages/README.md | 18 +-- actions/install-system-packages/action.yml | 107 ++---------------- 3 files changed, 26 insertions(+), 111 deletions(-) diff --git a/.github/test-actions/test-install-system-packages/action.yml b/.github/test-actions/test-install-system-packages/action.yml index 26c5485e8..f33a86cd1 100644 --- a/.github/test-actions/test-install-system-packages/action.yml +++ b/.github/test-actions/test-install-system-packages/action.yml @@ -12,12 +12,12 @@ runs: run: | if which "$expectedCommand" then - echo "❌This test must be revised, as the '$expectedCommand' command is already installed." + echo "❌ This test must be revised, as the '$expectedCommand' command is already installed." exit 1 fi - shell: bash - run: echo "🎭Running unconditional installation..." + run: echo "🎭 Running unconditional installation..." - uses: ./actions/install-system-packages with: @@ -26,18 +26,18 @@ runs: - name: The expected command should now be available shell: bash run: | - echo "🔎Now looking for the '$expectedCommand' command..." + echo "🔎 Now looking for the '$expectedCommand' command..." if which "$expectedCommand" then - echo "✅The expected command is now available!" + echo "✅ The expected command is now available!" else - echo "❌The expected command was not installed!" >&2 + echo "❌ The expected command was not installed!" >&2 exit 1 fi - shell: bash - run: echo "🎭Running conditional installation for existing command..." + run: echo "🎭 Running conditional installation for existing command - which should be skipped..." - uses: ./actions/install-system-packages with: diff --git a/actions/install-system-packages/README.md b/actions/install-system-packages/README.md index 4fc437dbe..ca0ef462a 100644 --- a/actions/install-system-packages/README.md +++ b/actions/install-system-packages/README.md @@ -2,7 +2,7 @@ Installs software using the platform's package manager. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,11 +11,7 @@ steps: packages: moreutils ``` -## ☑️Requirements - -This action currently supports only the `apt-get` package manager. - -## 💡How it works +## 💡 How it works 1. If `required-command` is specified and can be found by the `type` command, just exit the action. @@ -23,14 +19,18 @@ This action currently supports only the `apt-get` package manager. 1. Install the requested packages. -## 📥Inputs +## ☑️ Requirements + +This action currently supports only the `apt-get` package manager. + +## 📥 Inputs | Name | Type | Description | Default value | | :----------------: | :---------: | :----------------------------------------------------------------------: | :-----------: | | `required-command` | **string** | When declared, the packages are installed only if the command is missing | | -| `packages` | **string** | The packages to install, separated by any spaces or commas | | +| `packages` | **string** | The packages to install, separated by commas | | | `initial-update` | **boolean** | Update the package list before the first installation | **true** | -## 🌐Further references +## 🌐 Further references - [aurora-github](../../README.md) diff --git a/actions/install-system-packages/action.yml b/actions/install-system-packages/action.yml index d7b32d61c..be303de6d 100644 --- a/actions/install-system-packages/action.yml +++ b/actions/install-system-packages/action.yml @@ -6,7 +6,7 @@ inputs: description: When declared, the packages are installed only if the command is missing. packages: - description: The packages to install, separated by any spaces or commas. + description: The packages to install, separated by commas. initial-update: description: Update the package list before the first installation. @@ -15,102 +15,17 @@ inputs: runs: using: composite steps: - - shell: bash - run: | - main() { - validateInputs - - if ! shouldRunInstaller - then - exit 0 - fi - - if [[ "${{ inputs.initial-update }}" == 'true' ]] - then - tryToUpdatePackageList - fi - - installPackages - } - - validateInputs() { - if [[ -z "${{ inputs.packages }}" ]] - then - echo "❌Missing action input: 'packages'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.initial-update }}" ]] - then - echo "❌Missing action input: 'initial-update'!" >&2 - exit 1 - fi - } - - shouldRunInstaller() { - local requiredCommand="${{inputs.required-command}}" - - if [[ -n "$requiredCommand" ]] - then - echo "📥Required command: '$requiredCommand'" - - if type "$requiredCommand" > /dev/null 2>&1 - then - echo "✅Required command '$requiredCommand' available - no need to install it!" - return 1 - else - echo "💫Required command '$requiredCommand' not available - now installing its packages..." - fi - else - echo "💫No required command passed - the requested packages will be installed unconditionally" - fi - - return 0 - } - - tryToUpdatePackageList() { - local flagFile="$HOME/.install-system-packages-updated" - - if [[ -f "$flagFile" ]] - then - echo "💡The package list has already been updated!" - return 0 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - echo "📥Updating the package list..." - - runQuietly "sudo apt-get update" - - touch "$flagFile" - } - - runQuietly() { - local commandLine="$1" - - local outputFile="$(mktemp)" - - if ! $commandLine &> "$outputFile" - then - echo "❌Error while running the command! Log:" - cat "$outputFile" - echo "❌❌❌" - - exit 1 - fi - } - - installPackages() { - local requestedPackages="${{ inputs.packages }}" - - echo "📥Requested packages: $requestedPackages" - - local actualPackages="$(echo "$requestedPackages" | perl -pe 's/[\s,]+/ /g')" - - echo "📦Installing packages..." + - shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/system-packages - runQuietly "sudo apt-get install -y $actualPackages" + system-packages:install [ + &packages=(input:list '${{ inputs.packages }}') - echo "✅Packages installed!" - } + &required-command=(input:string &optional required-command '${{ inputs.required-command }}') - main + &initial-update=(input:bool initial-update '${{ inputs.initial-update}}') + ] From 3470ab90a8d8f55754a27a60954d250cf51a3944 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Tue, 22 Apr 2025 05:04:32 +0200 Subject: [PATCH 22/63] Refactor and enhance `setup-nodejs-context`: * now support reading the NodeJS version from: 1. the .nvmrc file for nvm 1. package.json's `engines.node` field --- .../test-setup-nodejs-context/action.yml | 74 +++------- .../scenario/action.yml | 93 ++++++++++++ actions/setup-nodejs-context/README.md | 45 +++--- actions/setup-nodejs-context/action.yml | 134 +++++++----------- tests/npm-package/package.json | 2 +- 5 files changed, 197 insertions(+), 151 deletions(-) create mode 100644 .github/test-actions/test-setup-nodejs-context/scenario/action.yml diff --git a/.github/test-actions/test-setup-nodejs-context/action.yml b/.github/test-actions/test-setup-nodejs-context/action.yml index cebd90429..61f93a945 100644 --- a/.github/test-actions/test-setup-nodejs-context/action.yml +++ b/.github/test-actions/test-setup-nodejs-context/action.yml @@ -3,54 +3,36 @@ name: Test setup-nodejs-context runs: using: composite steps: - - name: Setup environment variables + - name: Backup package.json and initialize environment variables shell: bash working-directory: tests/npm-package run: | - expectedNodeVersion="$(jq -r '.engines.node' package.json)" - echo "🔎Expected NodeJS version: '$expectedNodeVersion'" + cp package.json package.json.bak expectedPnpmVersion="$(jq -r '.packageManager' package.json | sed s/^pnpm@//)" - echo "🔎Expected pnpm version: '$expectedPnpmVersion'" + echo "🔎 Expected pnpm version: '$expectedPnpmVersion'" - echo "expectedNodeVersion=$expectedNodeVersion" >> $GITHUB_ENV echo "expectedPnpmVersion=$expectedPnpmVersion" >> $GITHUB_ENV echo "dependenciesDirectory=node_modules" >> $GITHUB_ENV - - name: The expected NodeJS version should not be pre-installed - shell: bash - working-directory: tests/npm-package - run: | - if type node &> /dev/null - then - installedNodeVersion="$(node --version | sed s/^v//)" - echo "🔎Pre-installed NodeJS version: '$installedNodeVersion'" - - if [[ "$installedNodeVersion" == "$expectedNodeVersion" ]] - then - echo "❌The expected NodeJS version is pre-installed!" >&2 - exit 1 - fi - else - echo "💭NodeJS not preinstalled..." - fi - - - name: The expected pnpm version should not be pre-installed + - name: The expected pnpm version should not be running shell: bash working-directory: tests/npm-package run: | if type pnpm &> /dev/null then installedPnpmVersion="$(pnpm --version)" - echo "🔎Pre-installed pnpm version: '$installedPnpmVersion'" + echo "🔎 Pre-installed pnpm version: '$installedPnpmVersion'" - if [[ "$installedPnpmVersion" == "$expectedPnpmVersion" ]] + if [[ "$installedPnpmVersion" != "$expectedPnpmVersion" ]] then - echo "❌The expected pnpm version is pre-installed!" >&2 + echo "✅ The expected pnpm version is not running!" + else + echo "❌ The expected pnpm version is running! This test would be pointless!" >&2 exit 1 fi else - echo "💭pnpm not preinstalled..." + echo "✅ pnpm not installed..." fi - name: Ensure the dependencies are not installed @@ -58,38 +40,28 @@ runs: working-directory: tests/npm-package run: rm -rf "$dependenciesDirectory" - - name: Setup the NodeJS context - uses: ./actions/setup-nodejs-context + - name: Setup NodeJS from .nvmrc + uses: ./.github/test-actions/test-setup-nodejs-context/scenario with: - project-directory: tests/npm-package - - - name: The expected NodeJS version should be used - shell: bash - working-directory: tests/npm-package - run: | - installedNodeVersion="$(node --version | sed s/^v//)" - echo "🔎Installed NodeJS version: '$installedNodeVersion'" + version-source: nvmrc - if [[ "$installedNodeVersion" == "$expectedNodeVersion" ]] - then - echo "✅The requested NodeJS version is being used!" - else - echo "❌The requested NodeJS version is not running!" >&2 - exit 1 - fi + - name: Setup NodeJS from package.json + uses: ./.github/test-actions/test-setup-nodejs-context/scenario + with: + version-source: package-json - - name: The expected pnpm version should be used + - name: The expected pnpm version should have been installed shell: bash working-directory: tests/npm-package run: | installedPnpmVersion="$(pnpm --version)" - echo "🔎Installed pnpm version: '$installedPnpmVersion'" + echo "🔎 Installed pnpm version: '$installedPnpmVersion'" if [[ "$installedPnpmVersion" == "$expectedPnpmVersion" ]] then - echo "✅The expected pnpm version is being used!" + echo "✅ The expected pnpm version is being used!" else - echo "❌The expected pnpm version is not running!" >&2 + echo "❌ The expected pnpm version is not running!" >&2 exit 1 fi @@ -99,8 +71,8 @@ runs: run: | if [[ -d "$dependenciesDirectory" ]] then - echo "✅The dependencies have been installed!" + echo "✅ The dependencies have been installed!" else - echo "❌The dependencies have not been installed!" >&2 + echo "❌ The dependencies have not been installed!" >&2 exit 1 fi diff --git a/.github/test-actions/test-setup-nodejs-context/scenario/action.yml b/.github/test-actions/test-setup-nodejs-context/scenario/action.yml new file mode 100644 index 000000000..45363df71 --- /dev/null +++ b/.github/test-actions/test-setup-nodejs-context/scenario/action.yml @@ -0,0 +1,93 @@ +name: Install the expected NodeJS context + +inputs: + version-source: + description: The source (`nvmrc` or `package-json`) containing the requested NodeJS version. + +runs: + using: composite + steps: + - name: Validate inputs + shell: bash + run: | + case '${{ inputs.version-source }}' in + nvmrc | package-json) + ;; + + *) + echo "❌ Invalid value for the 'version-source' input: '${{ inputs.version-source }}'" + exit 1 + ;; + esac + + - shell: bash + run: | + echo "🎭 Setting up NodeJS from source: '${{inputs.version-source}}'" + + - name: Initialize expected NodeJS version from .nvmrc + if: inputs.version-source == 'nvmrc' + shell: bash + working-directory: tests/npm-package + run: | + jq '(.engines | del(.node)) as $e | .engines = $e' package.json > temp-package + mv temp-package package.json + + echo v20.18.2 > .nvmrc + + expectedNodeVersion="$(cat .nvmrc | sed 's/v//')" + echo "🔎 Expected NodeJS version from .nvmrc: '$expectedNodeVersion'" + + echo "expectedNodeVersion=$expectedNodeVersion" >> $GITHUB_ENV + + - name: Initialize expected NodeJS version from package.json + if: inputs.version-source == 'package-json' + shell: bash + working-directory: tests/npm-package + run: | + rm .nvmrc + mv package.json.bak package.json + + expectedNodeVersion="$(jq -r '.engines.node' package.json)" + echo "🔎 Expected NodeJS version from package.json: '$expectedNodeVersion'" + + echo "expectedNodeVersion=$expectedNodeVersion" >> $GITHUB_ENV + + - name: The expected NodeJS version should not be running + shell: bash + working-directory: tests/npm-package + run: | + if type node &> /dev/null + then + installedNodeVersion="$(node --version | sed s/^v//)" + echo "🔎 Pre-installed NodeJS version: '$installedNodeVersion'" + + if [[ "$installedNodeVersion" != "$expectedNodeVersion" ]] + then + echo "✅ The expected NodeJS version is not running!" + else + echo "❌ The expected NodeJS version is running! This test would be pointless!" >&2 + exit 1 + fi + else + echo "✅ NodeJS not installed..." + fi + + - name: Setup NodeJS context + uses: ./actions/setup-nodejs-context + with: + project-directory: tests/npm-package + + - name: The expected NodeJS version should be in use + shell: bash + working-directory: tests/npm-package + run: | + installedNodeVersion="$(node --version | sed s/^v//)" + echo "🔎 Installed NodeJS version: '$installedNodeVersion'" + + if [[ "$installedNodeVersion" == "$expectedNodeVersion" ]] + then + echo "✅ The requested NodeJS version is being used!" + else + echo "❌ The requested NodeJS version is not running!" >&2 + exit 1 + fi diff --git a/actions/setup-nodejs-context/README.md b/actions/setup-nodejs-context/README.md index 1f3ad5e74..9b844ff77 100644 --- a/actions/setup-nodejs-context/README.md +++ b/actions/setup-nodejs-context/README.md @@ -2,7 +2,7 @@ Conditionally installs **NodeJS** along with **pnpm**, as well as the dependencies listed in **package.json**. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -13,23 +13,27 @@ steps: **Please, note**: this action is automatically run by [verify-npm-package](../verify-npm-package/README.md) and [publish-npm-package](../publish-npm-package/README.md). -## 💡How it works +## 💡 How it works -1. If **package.json** - which must exist - declares the following field: +1. Determine whether a specific NodeJS toolchain must be installed; this happens in one of the following cases - considered in this order: - ```json - { - "engines": { - "node": "..." - } - } - ``` + 1. If the **.nvmrc** file exists in the project directory - containing the requested NodeJS version, as expected by `nvm` - an entire NodeJS toolchain will be set up; in particular: + 1. If **package.json** declares the following field: - 1. The requested **NodeJS** version - or a compatible one, if a range is passed - will be installed via [actions/setup-node](https://github.com/actions/setup-node) + ```json + { + "engines": { + "node": "..." + } + } + ``` + + **PLEASE, NOTE**: if you are using Bash as your development shell, you may want to install the [nvmcd](https://github.com/giancosta86/aurora-bash/tree/main/scripts/nvmcd) command from the [aurora-bash](https://github.com/giancosta86/aurora-bash) project. - **PLEASE, NOTE**: if you are using Bash as your development shell, you may want to install the [nvmcd](https://github.com/giancosta86/aurora-bash/tree/main/scripts/nvmcd) command from the [aurora-bash](https://github.com/giancosta86/aurora-bash) project. +1. If the NodeJS toolchain is to be set up: + + 1. The requested **NodeJS** version - or a compatible one, if a range is passed - will be installed via [actions/setup-node](https://github.com/actions/setup-node) 1. **pnpm** will be downloaded via [pnpm/action-setup](https://github.com/pnpm/action-setup). @@ -47,13 +51,15 @@ steps: - otherwise, the **latest** version will be installed +1. Set the `FORCE_COLOR` environment variable according to the value of the `pnpm-colors` input. + 1. No matter whether the toolchain was installed, retrieve the dependencies - as follows: - 🧊 if **pnpm-lock.yaml** exists, it is considered _frozen_ via the `--frozen-lockfile` flag - 🌞 otherwise, `--no-frozen-lockfile` is passed explicitly -## ☑️Requirements +## ☑️ Requirements - The **package.json** descriptor must exist in `project-directory`. @@ -61,13 +67,14 @@ steps: - If the **pnpm-lock.yaml** file exists, it must be up-to-date - because it's considered _frozen_. -## 📥Inputs +## 📥 Inputs -| Name | Type | Description | Default value | -| :-----------------: | :--------: | :-------------------------------------: | :-----------: | -| `project-directory` | **string** | The directory containing `package.json` | **.** | +| Name | Type | Description | Default value | +| :-----------------: | :---------: | :-------------------------------------: | :-----------: | +| `pnpm-colors` | **boolean** | Enable colors for pnpm | **true** | +| `project-directory` | **string** | The directory containing `package.json` | **.** | -## 🌐Further references +## 🌐 Further references - [nvmcd](https://github.com/giancosta86/aurora-bash/tree/main/scripts/nvmcd) from [aurora-bash](https://github.com/giancosta86/aurora-bash) diff --git a/actions/setup-nodejs-context/action.yml b/actions/setup-nodejs-context/action.yml index 315db488d..57f046de7 100644 --- a/actions/setup-nodejs-context/action.yml +++ b/actions/setup-nodejs-context/action.yml @@ -1,126 +1,100 @@ name: Setup a NodeJS context -description: Conditionally installs NodeJS along with pnpm, as well as the dependencies listed in package.json. +description: Conditionally installs NodeJS along with `pnpm`, as well as the dependencies listed in package.json. inputs: + pnpm-colors: + description: Enable colors for `pnpm` + default: true + project-directory: - description: The directory containing package.json. + description: The directory containing `package.json`. default: . runs: using: composite steps: - - name: Verify package.json - shell: bash + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - name: Check preconditions + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "💻Setting up NodeJS context in '${{ inputs.project-directory }}'..." + use aurora-github/nodejs/context - if [[ ! -f "package.json" ]] - then - echo "❌The package.json descriptor is missing!" >&2 - exit 1 - fi + context:check-preconditions - name: Detect toolchain constraints - id: detect-toolchain-constraints - shell: bash + id: detect-nodejs-constraints + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - requestedNodeVersion="$(jq -r '.engines.node // ""' package.json)" - - if [[ -n "$requestedNodeVersion" ]] - then - echo "🔎NodeJS version requested in package.json: '$requestedNodeVersion'" - installToolchain=true - else - echo "💭No requested NodeJS version in package.json..." - installToolchain=false - fi + use aurora-github/ci-cd/output + use aurora-github/nodejs/context - echo "echo 🔎Install NodeJS toolchain? $installToolchain" - - echo "install-toolchain=$installToolchain" >> $GITHUB_OUTPUT - - echo "requested-node-version=$requestedNodeVersion" >> $GITHUB_OUTPUT + output:map (context:detect-nodejs-constraints) - name: Install NodeJS - if: steps.detect-toolchain-constraints.outputs.install-toolchain == 'true' + if: steps.detect-nodejs-constraints.outputs.install-toolchain == 'true' uses: actions/setup-node@v4 with: - node-version: ${{ steps.detect-toolchain-constraints.outputs.requested-node-version }} + node-version: ${{ steps.detect-nodejs-constraints.outputs.requested-node-version }} - name: Print NodeJS version - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "🎡NodeJS version: '$(node --version)'" + use github.com/giancosta86/aurora-elvish/console + + console:inspect &emoji=🎡 'NodeJS version' (node --version) - name: Detect the requested pnpm version - if: steps.detect-toolchain-constraints.outputs.install-toolchain == 'true' - id: detect-pnpm-version - shell: bash + if: steps.detect-nodejs-constraints.outputs.install-toolchain == 'true' + id: detect-pnpm-constraints + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - packageManagerReference="$(jq -r '.packageManager // ""' package.json)" - - echo "📦Package manager reference: '$packageManagerReference'" - - if [[ -n "$packageManagerReference" ]] - then - requestedPackageManager="$(echo "$packageManagerReference" | cut -d '@' -f 1)" - - if [[ "$requestedPackageManager" != "pnpm" ]] - then - echo "❌The package manager must be pnpm!" >&2 - exit 1 - fi - - requestedPnpmVersion="$(echo "$packageManagerReference" | cut -d '@' -f 2)" - - echo "🔎Requested pnpm version: '$requestedPnpmVersion'" - - pnpmVersion="$requestedPnpmVersion" - else - echo "🌟Defaulting to the latest pnpm version!" - pnpmVersion="latest" - fi + use aurora-github/ci-cd/output + use aurora-github/nodejs/context - echo "🔬pnpm version to install: '$pnpmVersion'" - - echo "requested-pnpm-version=$pnpmVersion" >> $GITHUB_OUTPUT + output:map (context:detect-pnpm-constraints) - name: Install pnpm - if: steps.detect-toolchain-constraints.outputs.install-toolchain == 'true' + if: steps.detect-nodejs-constraints.outputs.install-toolchain == 'true' uses: pnpm/action-setup@v4 with: - version: ${{ steps.detect-pnpm-version.outputs.requested-pnpm-version }} + version: ${{ steps.detect-pnpm-constraints.outputs.requested-pnpm-version }} - - name: Print pnpm version - shell: bash + - name: Setup colors for pnpm + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "📦pnpm version: $(pnpm --version)" + use aurora-github/ci-cd/input + use aurora-github/nodejs/context - - name: Install the dependencies - shell: bash + var pnpm-colors = (input:bool pnpm-colors '${{ inputs.pnpm-colors }}') + + context:set-pnpm-colors $pnpm-colors + + - name: Print pnpm version + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - lockFile="pnpm-lock.yaml" + use github.com/giancosta86/aurora-elvish/console - if [[ -f "$lockFile" ]] - then - echo "🧊Installing dependencies with frozen lockfile, as '$lockFile' is present..." - lockFileArg="--frozen-lockfile" - else - echo "🌞Installing dependencies without frozen lockfile, as '$lockFile' is missing..." - lockFileArg="--no-frozen-lockfile" - fi + console:inspect &emoji=📦 'pnpm version' (pnpm --version) - pnpm install $lockFileArg + - name: Install the dependencies + shell: elvish {0} + working-directory: ${{ inputs.project-directory }} + run: | + use aurora-github/nodejs/context - echo "✅Dependencies installed!" + context:install-dependencies - name: Print confirmation message - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} - run: echo "✅NodeJS context in '${{ inputs.project-directory }}' ready!" + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅📦 NodeJS context in "'"${{ inputs.project-directory }}"'" ready! diff --git a/tests/npm-package/package.json b/tests/npm-package/package.json index 4b6f825a5..750d41908 100644 --- a/tests/npm-package/package.json +++ b/tests/npm-package/package.json @@ -13,7 +13,7 @@ }, "private": true, "engines": { - "node": "20.15.1" + "node": "20.18.3" }, "packageManager": "pnpm@9.7.1" } From 9ddd4dd3b225cdb1f5d30225eaae48c5baa1a43f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Tue, 22 Apr 2025 16:57:16 +0200 Subject: [PATCH 23/63] Refactor `publish-github-pages` --- .../test-publish-github-pages/action.yml | 26 ++- actions/publish-github-pages/README.md | 12 +- actions/publish-github-pages/action.yml | 185 +++++------------- website/nextjs/global.d.ts | 1 + website/nextjs/package.json | 3 +- website/nextjs/src/app/page.tsx | 2 + 6 files changed, 83 insertions(+), 146 deletions(-) create mode 100644 website/nextjs/global.d.ts diff --git a/.github/test-actions/test-publish-github-pages/action.yml b/.github/test-actions/test-publish-github-pages/action.yml index 9b0962a5d..4f89186f2 100644 --- a/.github/test-actions/test-publish-github-pages/action.yml +++ b/.github/test-actions/test-publish-github-pages/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Now publishing static files with dry-run enabled..." + run: echo "🎭 Now publishing (in dry-run) static files..." - name: A static website should be publishable uses: ./actions/publish-github-pages @@ -13,7 +13,7 @@ runs: dry-run: true - shell: bash - run: echo "🎭Now publishing a Maven-generated site with dry-run enabled..." + run: echo "🎭 Now publishing (in dry-run) a Maven-generated site..." - name: A Maven-generated website should be publishable uses: ./actions/publish-github-pages @@ -22,7 +22,7 @@ runs: dry-run: true - shell: bash - run: echo "🎭Now publishing a NextJS site..." + run: echo "🎭 Now publishing a NextJS site..." - name: A NextJS website should be publishable id: publish-nextjs-pages @@ -37,8 +37,24 @@ runs: if [[ -n "$websiteUrl" ]] then - echo "✅Website published and available at: '$websiteUrl'" + echo "✅ Website published and available at: '$websiteUrl'" else - echo "❌Website not available!" >&2 + echo "❌ Website not available!" >&2 exit 1 fi + + - shell: bash + run: echo "🎭 Now trying to publish an inexistent source directory..." + + - uses: ./actions/publish-github-pages + with: + source-directory: INEXISTENT_DIRECTORY + optional: true + + - shell: bash + run: echo "🎭 Now trying to omit the source directory..." + + - uses: ./actions/publish-github-pages + with: + source-directory: "" + optional: true diff --git a/actions/publish-github-pages/README.md b/actions/publish-github-pages/README.md index 70dd3947e..41af1cebc 100644 --- a/actions/publish-github-pages/README.md +++ b/actions/publish-github-pages/README.md @@ -2,7 +2,7 @@ Publishes a directory as the [GitHub Pages](https://pages.github.com/) website for the current repository. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -23,7 +23,7 @@ steps: - [publish-rust-wasm](../publish-rust-wasm/README.md). -## 💡How it works +## 💡 How it works 1. If `source-directory` is set to an empty string (the default) or refers to a missing directory, if `optional` is set to **true** the action will simply exit, otherwise the workflow will fail. @@ -51,7 +51,7 @@ steps: 1. Publish the files to GitHub Pages. -## ☑️Requirements +## ☑️ Requirements - The following [permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token) must be set for the action to work: @@ -67,7 +67,7 @@ steps: - Please, refer to [setup-nodejs-context](../setup-nodejs-context/README.md) for details about setting up a NodeJS environment. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :-----------------------------------------------------------------: | :-----------: | @@ -76,13 +76,13 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `dry-run` | **boolean** | Stop the publication just before uploading | **false** | -## 📤Outputs +## 📤 Outputs | Name | Type | Description | Example | | :---: | :--------: | :------------------------------: | :---------: | | `url` | **string** | The URL of the published website | _HTTPS url_ | -## 🌐Further references +## 🌐 Further references - [setup-nodejs-context](../setup-nodejs-context/README.md) diff --git a/actions/publish-github-pages/action.yml b/actions/publish-github-pages/action.yml index 16f9f2f99..96786bb6a 100644 --- a/actions/publish-github-pages/action.yml +++ b/actions/publish-github-pages/action.yml @@ -7,7 +7,7 @@ inputs: default: . optional: - description: Whether "source-directory" can be empty string or missing directory. + description: Whether `source-directory` can be empty string or missing directory. default: false enforce-branch-version: @@ -26,86 +26,19 @@ outputs: runs: using: composite steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - name: Set up strategy - shell: bash + shell: elvish {0} run: | - strategy="exit" - - main() { - validateInputs - - if setupEnvironment - then - detectSourceStrategy - fi - - echo "🔎Website build strategy: '$strategy'" - echo "strategy=$strategy" >> $GITHUB_ENV - } - - validateInputs() { - if [[ -z "${{ inputs.optional }}" ]] - then - echo "❌Missing action input: 'optional'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - } - - setupEnvironment() { - local sourceDirectory="${{ inputs.source-directory }}" - local optional="${{ inputs.optional }}" - - if [[ -z "$sourceDirectory" ]] - then - if [[ "$optional" == "true" ]] - then - echo "💬Missing action input: 'source-directory' - can't proceed" - return 1 - else - echo "❌Missing action input: 'source-directory'!" >&2 - exit 1 - fi - elif [[ ! -d "$sourceDirectory" ]] - then - if [[ "$optional" == "true" ]] - then - echo "💬Missing website directory '$sourceDirectory' - can't proceed" - return 1 - else - echo "❌Missing website directory '$sourceDirectory'!" >&2 - exit 1 - fi - else - echo "🌐Website source directory '$sourceDirectory' found!" - cd "$sourceDirectory" - fi - } - - detectSourceStrategy() { - if [[ -f "package.json" ]] - then - strategy=nodejs - elif [[ -f "pom.xml" ]] - then - strategy=maven - else - strategy="static-files" - fi - } - - main + use aurora-github/ci-cd/input + use aurora-github/website + + var optional = (input:bool optional '${{ inputs.optional }}') + + var source-directory = (input:directory &optional=$optional &can-be-missing=$optional source-directory '${{ inputs.source-directory }}') + + website:detect-strategy-from-sources $source-directory - name: Enforce branch version if: env.strategy == 'nodejs' || env.strategy == 'maven' @@ -122,70 +55,52 @@ runs: - name: Create the website via NodeJS if: env.strategy == 'nodejs' - shell: bash - working-directory: ${{ inputs.source-directory }} + shell: elvish {0} run: | - echo "📦Now building the website via NodeJS..." - pnpm build - echo "✅Website ready!" - - artifactDirectory=${{ inputs.source-directory }}/dist + use aurora-github/ci-cd/input + use aurora-github/website - echo "artifactDirectory=$artifactDirectory" >> $GITHUB_ENV + website:build-via-nodejs (input:string source-directory '${{ inputs.source-directory }}') - name: Create the website via Maven if: env.strategy == 'maven' - shell: bash - working-directory: ${{ inputs.source-directory }} + shell: elvish {0} run: | - echo "🪶Now building the website via Maven..." - mvn -q site - echo "✅Website ready!" + use aurora-github/ci-cd/input + use aurora-github/website - artifactDirectory=${{ inputs.source-directory }}/target/site - - echo "artifactDirectory=$artifactDirectory" >> $GITHUB_ENV + website:build-via-maven (input:string source-directory '${{ inputs.source-directory }}') - name: Setup static artifacts if: env.strategy == 'static-files' - shell: bash - run: echo "artifactDirectory=${{ inputs.source-directory }}" >> $GITHUB_ENV + shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/website - - if: env.strategy != 'exit' - shell: bash + website:set-artifact-directory (input:string source-directory '${{ inputs.source-directory }}') + + - name: Check artifacts + if: env.strategy != 'exit' + shell: elvish {0} run: | - main() { - checkArtifactDirectory - skipUploadingOnDryRun - } - - checkArtifactDirectory() { - echo "🔎Website artifact directory: '$artifactDirectory'" - - if [[ -d "$artifactDirectory" ]] - then - echo "✅The artifact directory for the 🌐website exists!" - else - if [[ "${{ inputs.optional }}" == "true" ]] - then - echo "💬Missing website artifact directory - can't proceed" - echo "strategy=exit" >> $GITHUB_ENV - else - echo "❌Missing website artifact directory!" >&2 - exit 1 - fi - fi - } - - skipUploadingOnDryRun() { - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - echo "💭Skipping publication, as requested by dry-run." - echo "strategy=exit" >> $GITHUB_ENV - fi - } - - main + use aurora-github/ci-cd/input + use aurora-github/website + + var optional = (input:bool optional '${{ inputs.optional }}') + + website:check-artifact-directory (get-env artifactDirectory) $optional + + - name: Skip publication on dry-run + if: env.strategy != 'exit' + shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/website + + var dry-run = (input:bool dry-run '${{ inputs.dry-run }}') + + website:skip-publication-on-dry-run $dry-run - name: Upload website artifacts if: env.strategy != 'exit' @@ -200,7 +115,9 @@ runs: - name: Print confirmation message if: env.strategy != 'exit' - shell: bash + shell: elvish {0} run: | - echo "✅Publication to GitHub Pages successful!" - echo "🌐Website: ${{ steps.publish-pages.outputs.page_url }}" + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅ Website publication successful! + console:echo 🌐 Project website: ${{ steps.publish-pages.outputs.page_url }} diff --git a/website/nextjs/global.d.ts b/website/nextjs/global.d.ts new file mode 100644 index 000000000..dd4ab7e91 --- /dev/null +++ b/website/nextjs/global.d.ts @@ -0,0 +1 @@ +declare module "*.svg"; diff --git a/website/nextjs/package.json b/website/nextjs/package.json index acf47000e..e84ca9782 100644 --- a/website/nextjs/package.json +++ b/website/nextjs/package.json @@ -4,8 +4,9 @@ "private": true, "scripts": { "dev": "next dev", + "start": "pnpm dev", "build": "next build", - "start": "next start", + "start:prod": "next start", "lint": "next lint" }, "dependencies": { diff --git a/website/nextjs/src/app/page.tsx b/website/nextjs/src/app/page.tsx index af635d022..05eb24ec6 100644 --- a/website/nextjs/src/app/page.tsx +++ b/website/nextjs/src/app/page.tsx @@ -22,6 +22,8 @@ export default function Home() { {" "} can actually publish websites via GitHub Pages! 🤗🦋

+ +

🔮Now using the Elvish shell! 🥳

); From dc90ddc46ff0aa0e65c5958c476be3f1f8c0903a Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 21 May 2025 16:44:51 +0200 Subject: [PATCH 24/63] Introduce new action: `run-shell-script` --- .../test-run-shell-script/action.yml | 96 +++++++++++++++++++ .../test-run-shell-script/bash-test.sh | 3 + .../test-run-shell-script/elvish-hidden.txt | 3 + .../test-run-shell-script/elvish-test.elv | 3 + .../expect-result/action.yml | 27 ++++++ .../test-run-shell-script/python-test.py | 6 ++ .../test-run-shell-script/scenario/action.yml | 78 +++++++++++++++ actions/run-shell-script/README.md | 52 ++++++++++ actions/run-shell-script/action.yml | 43 +++++++++ 9 files changed, 311 insertions(+) create mode 100644 .github/test-actions/test-run-shell-script/action.yml create mode 100644 .github/test-actions/test-run-shell-script/bash-test.sh create mode 100644 .github/test-actions/test-run-shell-script/elvish-hidden.txt create mode 100644 .github/test-actions/test-run-shell-script/elvish-test.elv create mode 100644 .github/test-actions/test-run-shell-script/expect-result/action.yml create mode 100644 .github/test-actions/test-run-shell-script/python-test.py create mode 100644 .github/test-actions/test-run-shell-script/scenario/action.yml create mode 100644 actions/run-shell-script/README.md create mode 100644 actions/run-shell-script/action.yml diff --git a/.github/test-actions/test-run-shell-script/action.yml b/.github/test-actions/test-run-shell-script/action.yml new file mode 100644 index 000000000..174275adf --- /dev/null +++ b/.github/test-actions/test-run-shell-script/action.yml @@ -0,0 +1,96 @@ +name: Test run-custom-tests + +runs: + using: composite + steps: + - uses: ./.github/test-actions/test-run-shell-script/scenario + with: + script-base: bash-test + script-extension: .sh + shell: bash + multiplier: 10 + + - uses: ./.github/test-actions/test-run-shell-script/scenario + with: + script-base: elvish-test + script-extension: .elv + shell: elvish + multiplier: 50 + + - shell: bash + run: echo "🎭 Testing script having an unknown extension..." + + - uses: ./actions/run-shell-script + with: + script-file: elvish-hidden.txt + shell: elvish + args: alpha, beta, gamma + working-directory: ${{ github.action_path }} + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 3 + multiplier: 400 + + - shell: bash + run: echo "🎭 Testing inexistent optional script..." + + - shell: bash + working-directory: ${{ github.action_path }} + run: rm -f result.txt + + - uses: ./actions/run-shell-script + with: + script-file: INEXISTENT-SCRIPT + optional: true + working-directory: ${{ github.action_path }} + + - shell: bash + working-directory: ${{ github.action_path }} + run: | + if [[ ! -f result.txt ]] + then + echo "✅ Result file missing, as expected!" + else + echo "❌ The result file was somehow created!" + exit 1 + fi + + - shell: bash + run: echo "🎭 Testing optional script with unsupported, undeclared shell..." + + - shell: bash + working-directory: ${{ github.action_path }} + run: rm -f result.txt + + - uses: ./actions/run-shell-script + with: + script-file: python-test.py + optional: true + working-directory: ${{ github.action_path }} + + - shell: bash + working-directory: ${{ github.action_path }} + run: | + if [[ ! -f result.txt ]] + then + echo "✅ Result file missing, as expected!" + else + echo "❌ The result file was somehow created!" + exit 1 + fi + + - shell: bash + run: echo "🎭 Testing script with unsupported but declared shell..." + + - uses: ./actions/run-shell-script + with: + script-file: python-test.py + shell: python3 + args: alpha, beta, gamma + working-directory: ${{ github.action_path }} + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 3 + multiplier: 2000 diff --git a/.github/test-actions/test-run-shell-script/bash-test.sh b/.github/test-actions/test-run-shell-script/bash-test.sh new file mode 100644 index 000000000..2efd01f53 --- /dev/null +++ b/.github/test-actions/test-run-shell-script/bash-test.sh @@ -0,0 +1,3 @@ +argCount="$#" + +echo $(($argCount * 10)) > result.txt \ No newline at end of file diff --git a/.github/test-actions/test-run-shell-script/elvish-hidden.txt b/.github/test-actions/test-run-shell-script/elvish-hidden.txt new file mode 100644 index 000000000..740ff3181 --- /dev/null +++ b/.github/test-actions/test-run-shell-script/elvish-hidden.txt @@ -0,0 +1,3 @@ +var arg-count = (count $args) + +echo (* $arg-count 400) > result.txt \ No newline at end of file diff --git a/.github/test-actions/test-run-shell-script/elvish-test.elv b/.github/test-actions/test-run-shell-script/elvish-test.elv new file mode 100644 index 000000000..04b598591 --- /dev/null +++ b/.github/test-actions/test-run-shell-script/elvish-test.elv @@ -0,0 +1,3 @@ +var arg-count = (count $args) + +echo (* $arg-count 50) > result.txt \ No newline at end of file diff --git a/.github/test-actions/test-run-shell-script/expect-result/action.yml b/.github/test-actions/test-run-shell-script/expect-result/action.yml new file mode 100644 index 000000000..29d8ca14f --- /dev/null +++ b/.github/test-actions/test-run-shell-script/expect-result/action.yml @@ -0,0 +1,27 @@ +name: Ensure that result.txt contains the expected value + +inputs: + multiplier: + description: The factor by which the tested script multiplies the number of arguments. + + arg-count: + description: The number of arguments that were passed to the tested script. + +runs: + using: composite + steps: + - name: Ensure the expected result was printed out to the result file + shell: bash + working-directory: ${{ github.action_path }}/.. + run: | + expected=$((${{ inputs.arg-count }} * ${{ inputs.multiplier }})) + + scriptResult="$(cat result.txt)" + + if [[ "$scriptResult" == "$expected" ]] + then + echo "✅ The script result was $expected, just as expected!" + else + echo "❌ The script result is $scriptResult, but $expected was expected!" >&2 + exit 1 + fi diff --git a/.github/test-actions/test-run-shell-script/python-test.py b/.github/test-actions/test-run-shell-script/python-test.py new file mode 100644 index 000000000..66c793b59 --- /dev/null +++ b/.github/test-actions/test-run-shell-script/python-test.py @@ -0,0 +1,6 @@ +import sys + +arg_count = len(sys.argv[1:]) + +with open("result.txt", "w") as f: + f.write(f"{2000 * arg_count}") \ No newline at end of file diff --git a/.github/test-actions/test-run-shell-script/scenario/action.yml b/.github/test-actions/test-run-shell-script/scenario/action.yml new file mode 100644 index 000000000..58609213a --- /dev/null +++ b/.github/test-actions/test-run-shell-script/scenario/action.yml @@ -0,0 +1,78 @@ +name: Runs a project-specific scenario for run-shell-script + +inputs: + script-base: + description: The extension-less name of the script file. + + script-extension: + description: | + The explicit extension of the script file (starting with .). + + The script must count its arguments, multiply them by a given factor + and finally write the resulting number to 'result.txt' in this directory. + + shell: + description: The explicit shell used to run the script. + + multiplier: + description: The factor by which the script multiplies the number of arguments. + +runs: + using: composite + steps: + - shell: bash + run: echo "🎭 Testing script '${{ inputs.script-base }}' with no extension and no shell..." + + - uses: ./actions/run-shell-script + with: + script-file: ${{ inputs.script-base }} + working-directory: ${{ github.action_path }}/.. + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 0 + multiplier: ${{ inputs.multiplier }} + + - shell: bash + run: echo "🎭 Testing script '${{ inputs.script-base }}' with no extension and explicit shell ('${{ inputs.shell }}')..." + + - uses: ./actions/run-shell-script + with: + script-file: ${{ inputs.script-base }} + shell: ${{ inputs.shell }} + args: alpha + working-directory: ${{ github.action_path }}/.. + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 1 + multiplier: ${{ inputs.multiplier }} + + - shell: bash + run: echo "🎭 Testing script '${{ inputs.script-base }}' with explicit extension ('${{ inputs.script-extension }}') and no shell..." + + - uses: ./actions/run-shell-script + with: + script-file: ${{ inputs.script-base }}${{ inputs.script-extension }} + args: alpha, beta + working-directory: ${{ github.action_path }}/.. + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 2 + multiplier: ${{ inputs.multiplier }} + + - shell: bash + run: echo "🎭 Testing script '${{ inputs.script-base }}' with explicit extension ('${{ inputs.script-extension }}') and explicit shell ('${{ inputs.shell }}')..." + + - uses: ./actions/run-shell-script + with: + script-file: ${{ inputs.script-base }}${{ inputs.script-extension }} + shell: ${{ inputs.shell }} + args: alpha, beta, gamma + working-directory: ${{ github.action_path }}/.. + + - uses: ./.github/test-actions/test-run-shell-script/expect-result + with: + arg-count: 3 + multiplier: ${{ inputs.multiplier }} diff --git a/actions/run-shell-script/README.md b/actions/run-shell-script/README.md new file mode 100644 index 000000000..f9c835837 --- /dev/null +++ b/actions/run-shell-script/README.md @@ -0,0 +1,52 @@ +# run-shell-script + +Runs a shell script, supporting different shells. + +## 🃏 Example + +```yaml +steps: + - uses: giancosta86/aurora-github/actions/run-shell-script@v11 + with: + script-file: process-data #Can detect both the extension and the shell automatically + working-directory: src +``` + +## 💡 How it works + +1. Look for the given `script-file` within `working-directory`; if it does not exist, try adding the following extensions, in this order: + + 1. **.elv** + + 1. **.sh** + + If no script file can still be found, exit the process - with an error, unless `optional` is set to **true**. + +1. If `shell` is specified, it will be invoked to run the script; otherwise, detect it from the extension of the actual script file: + + | Extension | Default shell | + | :-------: | :-----------: | + | **.elv** | `elvish` | + | **.sh** | `bash` | + + If no shell can still be detected, exit the process - with an error, unless `optional` is set to **true**. + +1. Within `working-directory`, run: + + ``` + + ``` + +## 📥 Inputs + +| Name | Type | Description | Default value | +| :-----------------: | :----------------------: | :---------------------------------------------------------: | :-----------: | +| `optional` | **boolean** | Exit successfully if no script can be run | **false** | +| `script-file` | **string** | Relative path - in `working-directory` - to the script file | | +| `shell` | **string** | The shell used to run the script | | +| `args` | **comma-separated list** | Arguments to be passed to the script | | +| `working-directory` | **string** | The working directory | **.** | + +## 🌐 Further references + +- [aurora-github](../../README.md) diff --git a/actions/run-shell-script/action.yml b/actions/run-shell-script/action.yml new file mode 100644 index 000000000..a321e963f --- /dev/null +++ b/actions/run-shell-script/action.yml @@ -0,0 +1,43 @@ +name: Run shell script +description: Runs a shell script, supporting different shells. + +inputs: + optional: + description: Exit successfully if the script cannot be run. + default: false + + script-file: + description: Relative path - in the working directory - to the script file. + + shell: + description: The shell used to run the script. + + args: + description: Comma-separated list of arguments passed to the script. + + working-directory: + description: The working directory. + default: . + +runs: + using: composite + steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 + + - name: Run the script + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/script + use aurora-github/ci-cd/input + + var optional = (input:bool optional '${{ inputs.optional }}') + + var script-file = (input:string script-file '${{ inputs.script-file }}') + + var shell = (input:string &optional shell '${{ inputs.shell }}') + + var args = (input:list '${{ inputs.args }}') + + var working-directory = (input:directory &optional &can-be-missing working-directory '${{ inputs.working-directory }}') + + script:run &working-directory=$working-directory &optional=$optional &shell=$shell $script-file $@args From 1f92c4997fcfecc176a2d08c48bc8df90cc6905b Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Tue, 22 Apr 2025 19:44:27 +0200 Subject: [PATCH 25/63] Refactor and extend `run-custom-tests`: * internally invoke `run-shell-script` to support a wider range of test scripts --- .../test-run-custom-tests/action.yml | 81 ++++------ .../test-run-custom-tests/scenario/action.yml | 34 ++++ actions/run-custom-tests/README.md | 74 +++++---- actions/run-custom-tests/action.yml | 145 ++++++------------ tests/custom/.gitignore | 1 + tests/custom/bash/verify.sh | 1 + tests/custom/elvish-custom/super-test.elv | 1 + tests/custom/elvish/verify.elv | 1 + tests/custom/nodejs/.gitignore | 1 - tests/custom/nodejs/package.json | 2 +- tests/custom/rust/.gitignore | 3 +- tests/custom/rust/src/lib.rs | 2 +- tests/custom/script/.gitignore | 1 - tests/custom/script/verify.sh | 1 - tests/custom/test-runner/numbers.test.elv | 15 ++ tests/custom/test-runner/protocol.test.elv | 5 + tests/custom/test-runner/string.test.elv | 9 ++ 17 files changed, 189 insertions(+), 188 deletions(-) create mode 100644 .github/test-actions/test-run-custom-tests/scenario/action.yml create mode 100644 tests/custom/.gitignore create mode 100644 tests/custom/bash/verify.sh create mode 100644 tests/custom/elvish-custom/super-test.elv create mode 100644 tests/custom/elvish/verify.elv delete mode 100644 tests/custom/nodejs/.gitignore delete mode 100644 tests/custom/script/.gitignore delete mode 100644 tests/custom/script/verify.sh create mode 100644 tests/custom/test-runner/numbers.test.elv create mode 100644 tests/custom/test-runner/protocol.test.elv create mode 100644 tests/custom/test-runner/string.test.elv diff --git a/.github/test-actions/test-run-custom-tests/action.yml b/.github/test-actions/test-run-custom-tests/action.yml index 666997595..63fe16f5d 100644 --- a/.github/test-actions/test-run-custom-tests/action.yml +++ b/.github/test-actions/test-run-custom-tests/action.yml @@ -3,79 +3,56 @@ name: Test run-custom-tests runs: using: composite steps: - - shell: bash - run: echo "🎭Testing the 'script' strategy..." - - - uses: ./actions/run-custom-tests + - uses: ./.github/test-actions/test-run-custom-tests/scenario with: - root-directory: tests/custom/script + strategy: bash + root-directory: tests/custom/bash - - name: The Bash script should have run correctly - shell: bash - working-directory: tests/custom/script - run: | - generatedContent="$(cat generated-file.txt)" + - uses: ./.github/test-actions/test-run-custom-tests/scenario + with: + strategy: elvish + root-directory: tests/custom/elvish - if [[ "$generatedContent" == "Bash-generated content" ]] - then - echo "✅The Bash test script completed perfectly!" - else - echo "❌Issues with the Bash test script!" >&2 - exit 1 - fi + - uses: ./.github/test-actions/test-run-custom-tests/scenario + with: + strategy: elvish + script-file: super-test.elv + root-directory: tests/custom/elvish-custom - - shell: bash - run: echo "🎭Testing the 'nodejs' strategy..." + - uses: ./.github/test-actions/test-run-custom-tests/scenario + with: + strategy: test-runner + root-directory: tests/custom/test-runner - - uses: ./actions/run-custom-tests + - uses: ./.github/test-actions/test-run-custom-tests/scenario with: + strategy: nodejs root-directory: tests/custom/nodejs - - name: The NodeJS script should have run correctly - shell: bash - working-directory: tests/custom/nodejs/ - run: | - generatedContent="$(cat generated-file.txt)" - - if [[ "$generatedContent" == "NodeJS-generated content" ]] - then - echo "✅The NodeJS test script completed perfectly!" - else - echo "❌Issues with the NodeJS test script!" >&2 - exit 1 - fi + - uses: ./.github/test-actions/test-run-custom-tests/scenario + with: + strategy: rust + root-directory: tests/custom/rust - shell: bash - run: echo "🎭Testing the 'rust' strategy..." + run: echo "🎭 Trying to run tests from an inexistent directory..." - uses: ./actions/run-custom-tests with: - root-directory: tests/custom/rust - - - name: The Rust tests should have run correctly - shell: bash - working-directory: tests/custom/rust/ - run: | - generatedContent="$(cat generated-file.txt)" - - if [[ "$generatedContent" == "Rust-generated content" ]] - then - echo "✅The Rust test script completed perfectly!" - else - echo "❌Issues with the Rust test script!" >&2 - exit 1 - fi + root-directory: tests/custom/TOTALLY_INEXISTENT + optional: true - shell: bash - run: echo "🎭Trying to run tests from inexisting directory..." + run: echo "🎭 Trying to run tests via an inexistent script..." - uses: ./actions/run-custom-tests with: - root-directory: tests/custom/TOTALLY_INEXISTING + root-directory: tests/custom optional: true + script-file: INEXISTENT - shell: bash - run: echo "🎭Trying to run tests from a directory containing no tests..." + run: echo "🎭 Trying to run tests from a directory containing no tests..." - uses: ./actions/run-custom-tests with: diff --git a/.github/test-actions/test-run-custom-tests/scenario/action.yml b/.github/test-actions/test-run-custom-tests/scenario/action.yml new file mode 100644 index 000000000..88dab2cb0 --- /dev/null +++ b/.github/test-actions/test-run-custom-tests/scenario/action.yml @@ -0,0 +1,34 @@ +name: Run a project-specific scenario for run-custom-tests + +inputs: + strategy: + description: The test strategy. + + script-file: + description: The script-file parameter forwarded to the action. + + root-directory: + description: The root directory for the tests. + +runs: + using: composite + steps: + - shell: bash + run: echo "🎭 Testing the '${{ inputs.strategy }}' strategy in directory '${{ inputs.root-directory }}'..." + + - uses: ./actions/run-custom-tests + with: + script-file: ${{ inputs.script-file }} + root-directory: ${{ inputs.root-directory }} + + - name: Ensure the expected output file was actually created by the tests + shell: bash + working-directory: ${{ inputs.root-directory }} + run: | + if [[ "$(cat out.txt)" == "TEST OK" ]] + then + echo "✅ The tests for the '${{ inputs.strategy }}' strategy completed perfectly!" + else + echo "❌ Issues with the '${{ inputs.strategy }}' test strategy!" >&2 + exit 1 + fi diff --git a/actions/run-custom-tests/README.md b/actions/run-custom-tests/README.md index 56d567ac5..49e6075b3 100644 --- a/actions/run-custom-tests/README.md +++ b/actions/run-custom-tests/README.md @@ -1,8 +1,8 @@ # run-custom-tests -Executes arbitrary tests within a given directory; it runs a **shell** script by default, but can also run _pnpm_ (for **NodeJS**) or _cargo_ (for **Rust**). +Executes arbitrary tests within a given directory; it runs a shell script by default, but can also run _pnpm_ (for **NodeJS**) or _cargo_ (for **Rust**). -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,60 +11,70 @@ steps: root-directory: client-tests ``` -### Remarks +## 💡 How it works -- You should **not** call this action for unit tests when using [verify-rust-crate](../verify-rust-crate/README.md) or [verify-npm-package](../verify-npm-package/README.md) - they are automatically run by the workflow itself. +1. If `root-directory` does not exist: -- This action is already called by [verify-rust-wasm](../verify-rust-wasm/README.md) to optionally run the tests in the **client-tests** directory. + 1. If `optional` is **true**, exit the action successfully. -- This action is already called by [verify-npm-package](../verify-npm-package/README.md) to optionally run the tests in the **tests** directory. + 1. Otherwise, crash the workflow. -## 💡How it works +1. Use `root-directory` as the current directory. -1. If `root-directory` does not exist: +1. Select the first feasible course of action: + + 1. If `script-file` is specified **and** can be run by forwarding it to [run-shell-script](../run-shell-script/README.md), invoke the action accordingly, passing the (optional) `script-shell`. + + 1. If **verify** (**.elv**, **.sh**, ...) exists in `working-directory` **and** can be run via [run-shell-script](../run-shell-script/README.md), invoke the action accordingly, passing the (optional) `script-shell`. - - if `optional` is **true**, exit the action with no error + 1. If one or more files having `.test.elv` extension exist in the root directory, run the tests within them, assuming the test format introduced by [aurora-elvish](https://github.com/giancosta86/aurora-elvish) - - otherwise, crash the workflow + 1. If a file named **package.json** exists in the root directory: -1. Detect the test type and act accordingly: + 1. Invoke [setup-nodejs-context](../setup-nodejs-context/README.md). - - If a file named like `script-file` exists in the root directory, run it using `script-shell`; consequently, there is no need to mark the file as _executable_ + 1. Run the **verify** script in the **scripts** section of **package.json**. - - Otherwise, if a file named **package.json** exists in the root directory: + 1. If a file named **Cargo.toml** exists in the root directory: - 1. run [setup-nodejs-context](../setup-nodejs-context/README.md) + 1. Invoke [setup-rust-context](../setup-rust-context/README.md), without enforcing the existence of the toolchain file. - 1. run the **verify** script in the **scripts** section of **package.json** + 1. Run `cargo test`: - - Otherwise, if a file named **Cargo.toml** exists in the root directory: + 1. with no features enabled - 1. if the **rust-toolchain.toml** file exists, run [check-rust-versions](../check-rust-versions/README.md) to enforce a specific Rust toolkit + 1. with all the features enabled - 1. run `cargo test` with the `--all-features` flag + 1. Otherwise: - - Otherwise: + 1. If `optional` is **true**, exit the action successfully. - - if `optional` is **true**, exit the action with no error + 1. Otherwise, crash the workflow. - - otherwise, crash the workflow +## 💬 Remarks -## ☑️Requirements +- You should **not** call this action for unit tests when using [verify-rust-crate](../verify-rust-crate/README.md) or [verify-npm-package](../verify-npm-package/README.md) - they are automatically run by the workflow itself. + +- This action is already called by [verify-rust-wasm](../verify-rust-wasm/README.md) to optionally run the tests in the **client-tests** directory. -## 📥Inputs +- This action is already called by [verify-npm-package](../verify-npm-package/README.md) to optionally run the tests in the **tests** directory. -| Name | Type | Description | Default value | -| :--------------: | :---------: | :-------------------------------------------: | :-----------: | -| `optional` | **boolean** | Exit with no error if the tests cannot be run | **false** | -| `script-file` | **string** | Relative path to the script file | **verify.sh** | -| `script-shell` | **string** | The shell used to run `script-file` | **bash** | -| `root-directory` | **string** | The directory containing the tests | | +## 📥 Inputs -## 🌐Further references +| Name | Type | Description | Default value | +| :--------------: | :---------: | :------------------------------------------: | :-----------: | +| `optional` | **boolean** | Exit successfully if the tests cannot be run | **false** | +| `script-file` | **string** | Relative path to the script file | | +| `script-shell` | **string** | The shell used to run `script-file` | | +| `root-directory` | **string** | The directory containing the tests | | + +## 🌐 Further references + +- [run-shell-script](../run-shell-script/README.md) - [setup-nodejs-context](../setup-nodejs-context/README.md) -- [check-rust-versions](../check-rust-versions/README.md) +- [setup-rust-context](../setup-rust-context/README.md) - [verify-rust-crate](../verify-rust-crate/README.md) @@ -72,4 +82,6 @@ steps: - [verify-rust-wasm](../verify-rust-wasm/README.md) +- [aurora-elvish](https://github.com/giancosta86/aurora-elvish) + - [aurora-github](../../README.md) diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 98c1eaef0..6fd602b8c 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -1,18 +1,16 @@ name: Run custom tests -description: Executes arbitrary tests within a given directory; it runs a shell script by default, but can also run pnpm (for NodeJS) or cargo (for Rust). +description: Executes arbitrary tests within a given directory; it runs a shell script by default, but can also run `pnpm` (for NodeJS) or `cargo` (for Rust). inputs: optional: - description: Exit with no error if the tests cannot be run. + description: Exit successfully if the tests cannot be run. default: false script-file: description: Relative path to the script file. - default: verify.sh script-shell: description: The shell used to run script-file. - default: bash root-directory: description: The directory containing the tests. @@ -20,86 +18,43 @@ inputs: runs: using: composite steps: - - name: Set up strategy - shell: bash + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + + - name: Detect the test strategy + shell: elvish {0} run: | - main() { - validateInputs - detectTestStrategy - } + use aurora-github/ci-cd/input + use aurora-github/custom-tests - validateInputs() { - if [[ -z "${{ inputs.optional }}" ]] - then - echo "❌Missing action input: 'optional'!" >&2 - exit 1 - fi - - if [[ -d "${{ inputs.root-directory }}" ]] - then - cd "${{ inputs.root-directory }}" - else - if [[ "${{ inputs.optional }}" == "true" ]] - then - echo "💭Skipping optional tests in missing directory: '${{ inputs.root-directory }}'" - echo "strategy=exit" >> $GITHUB_ENV - exit 0 - else - echo "❌Cannot run custom tests in missing directory: '${{ inputs.root-directory }}'" >&2 - exit 1 - fi - fi - } + custom-tests:detect-strategy [ + &optional=(input:bool optional '${{ inputs.optional }}') - detectTestStrategy() { - echo "🔬Detecting custom test strategy in directory '${{ inputs.root-directory }}'..." - - local scriptFile="${{ inputs.script-file }}" - - if [[ -n "$scriptFile" && -f "$scriptFile" ]] - then - echo "🐚Custom test script '$scriptFile' found!" - local strategy="script" - - if [[ -z "${{ inputs.script-shell }}" ]] - then - echo "❌Missing action input: 'script-shell'!" >&2 - exit 1 - fi - elif [[ -f "package.json" ]] - then - echo "📦package.json file found!" - local strategy="nodejs" - elif [[ -f "Cargo.toml" ]] - then - echo "🦀Cargo.toml file found!" - local strategy="rust" - else - if [[ "${{ inputs.optional }}" == "true" ]] - then - echo "💭No supported strategy detected for the optional custom tests" - local strategy="exit" - else - echo "❌Cannot run custom tests: no supported test strategy could be detected!" >&2 - exit 1 - fi - fi - - echo "🔎Test strategy: '$strategy'" - - echo "strategy=$strategy" >> $GITHUB_ENV - } + &script-file=(input:string &optional script-file '${{ inputs.script-file }}') - main + &root-directory=(input:directory &optional &can-be-missing root-directory '${{ inputs.root-directory }}') + ] - - name: Run script + - name: Run test script if: env.strategy == 'script' - shell: bash + uses: giancosta86/aurora-github/actions/run-shell-script@v11.0.0 + with: + optional: false + script-file: ${{ env.scriptPath }} + shell: ${{ inputs.script-shell }} + working-directory: ${{ inputs.root-directory }} + + - name: Run test files + if: env.strategy == 'test-runner' + shell: elvish {0} working-directory: ${{ inputs.root-directory }} run: | - echo "🐚Now running the custom script '${{ inputs.script-file }}' using '${{ inputs.script-shell }}'..." + use github.com/giancosta86/aurora-elvish/testing + + var failed-tests = (testing:test &output-failures &clear=$false) - "${{ inputs.script-shell }}" "${{ inputs.script-file }}" + if (> $failed-tests 0) { + exit 1 + } - name: Setup NodeJS context if: env.strategy == 'nodejs' @@ -109,39 +64,33 @@ runs: - name: Run the 'verify' script from package.json if: env.strategy == 'nodejs' - shell: bash + shell: elvish {0} working-directory: ${{ inputs.root-directory }} run: | - echo "📦Now running the 'verify' script from package.json..." - pnpm verify + use aurora-github/nodejs/project - - name: Check for the Rust toolchain file + project:verify + + - name: Setup Rust context if: env.strategy == 'rust' - shell: bash - working-directory: ${{ inputs.root-directory }} - run: | - if [[ -f "rust-toolchain.toml" ]] - then - echo "checkRustVersions=true" >> $GITHUB_ENV - else - echo "checkRustVersions=false" >> $GITHUB_ENV - fi - - - name: Check Rust versions - if: env.strategy == 'rust' && env.checkRustVersions == 'true' - uses: giancosta86/aurora-github/actions/check-rust-versions@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 with: + check-toolchain-file: false project-directory: ${{ inputs.root-directory }} - - name: Run tests via Cargo + - name: Run the tests via Cargo if: env.strategy == 'rust' - shell: bash + shell: elvish {0} working-directory: ${{ inputs.root-directory }} run: | - echo "🦀Running Rust tests with all the features enabled..." - cargo test --all-features + use aurora-github/rust/project + + project:run-tests - name: Print confirmation message if: env.strategy != 'exit' - shell: bash - run: echo "✅Custom tests in directory '${{ inputs.root-directory }}' run successfully!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅ Custom tests in directory "'"${{ inputs.root-directory }}"'" run successfully! diff --git a/tests/custom/.gitignore b/tests/custom/.gitignore new file mode 100644 index 000000000..b01f4cd3c --- /dev/null +++ b/tests/custom/.gitignore @@ -0,0 +1 @@ +**/out.txt \ No newline at end of file diff --git a/tests/custom/bash/verify.sh b/tests/custom/bash/verify.sh new file mode 100644 index 000000000..63af88390 --- /dev/null +++ b/tests/custom/bash/verify.sh @@ -0,0 +1 @@ +echo 'TEST OK' > out.txt \ No newline at end of file diff --git a/tests/custom/elvish-custom/super-test.elv b/tests/custom/elvish-custom/super-test.elv new file mode 100644 index 000000000..63af88390 --- /dev/null +++ b/tests/custom/elvish-custom/super-test.elv @@ -0,0 +1 @@ +echo 'TEST OK' > out.txt \ No newline at end of file diff --git a/tests/custom/elvish/verify.elv b/tests/custom/elvish/verify.elv new file mode 100644 index 000000000..63af88390 --- /dev/null +++ b/tests/custom/elvish/verify.elv @@ -0,0 +1 @@ +echo 'TEST OK' > out.txt \ No newline at end of file diff --git a/tests/custom/nodejs/.gitignore b/tests/custom/nodejs/.gitignore deleted file mode 100644 index 457c5c7d0..000000000 --- a/tests/custom/nodejs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/generated-file.txt \ No newline at end of file diff --git a/tests/custom/nodejs/package.json b/tests/custom/nodejs/package.json index 90c4fc490..72e40e80c 100644 --- a/tests/custom/nodejs/package.json +++ b/tests/custom/nodejs/package.json @@ -1,7 +1,7 @@ { "name": "nodejs-test", "scripts": { - "verify": "echo 'NodeJS-generated content' > generated-file.txt" + "verify": "echo 'TEST OK' > out.txt" }, "private": true, "engines": { diff --git a/tests/custom/rust/.gitignore b/tests/custom/rust/.gitignore index 44902230a..c41cc9e35 100644 --- a/tests/custom/rust/.gitignore +++ b/tests/custom/rust/.gitignore @@ -1,2 +1 @@ -/target -/generated-file.txt \ No newline at end of file +/target \ No newline at end of file diff --git a/tests/custom/rust/src/lib.rs b/tests/custom/rust/src/lib.rs index 4c9ea346f..3807640ab 100644 --- a/tests/custom/rust/src/lib.rs +++ b/tests/custom/rust/src/lib.rs @@ -4,6 +4,6 @@ mod tests { #[test] fn generate_file() { - fs::write("generated-file.txt", "Rust-generated content").unwrap(); + fs::write("out.txt", "TEST OK").unwrap(); } } diff --git a/tests/custom/script/.gitignore b/tests/custom/script/.gitignore deleted file mode 100644 index 457c5c7d0..000000000 --- a/tests/custom/script/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/generated-file.txt \ No newline at end of file diff --git a/tests/custom/script/verify.sh b/tests/custom/script/verify.sh deleted file mode 100644 index 0350f4496..000000000 --- a/tests/custom/script/verify.sh +++ /dev/null @@ -1 +0,0 @@ -echo "Bash-generated content" > generated-file.txt \ No newline at end of file diff --git a/tests/custom/test-runner/numbers.test.elv b/tests/custom/test-runner/numbers.test.elv new file mode 100644 index 000000000..1830d60e4 --- /dev/null +++ b/tests/custom/test-runner/numbers.test.elv @@ -0,0 +1,15 @@ +describe 'Among numeric operations' { + describe 'addition' { + it 'should work' { + + 90 2 | + should-be 92 + } + } + + describe 'division' { + it 'should work' { + / 90 3 | + should-be 30 + } + } +} \ No newline at end of file diff --git a/tests/custom/test-runner/protocol.test.elv b/tests/custom/test-runner/protocol.test.elv new file mode 100644 index 000000000..de10d1485 --- /dev/null +++ b/tests/custom/test-runner/protocol.test.elv @@ -0,0 +1,5 @@ +describe 'The testing protocol required by test-run-custom-tests' { + it 'should be satisfied' { + echo 'TEST OK' > out.txt + } +} \ No newline at end of file diff --git a/tests/custom/test-runner/string.test.elv b/tests/custom/test-runner/string.test.elv new file mode 100644 index 000000000..7fa178e6b --- /dev/null +++ b/tests/custom/test-runner/string.test.elv @@ -0,0 +1,9 @@ +describe 'Concatenating two strings' { + it 'should work' { + var a = 'A' + var b = 'B' + + put $a''$b | + should-be 'AB' + } +} \ No newline at end of file From da34e7904000c739ea1632e360bc33c37bb726d6 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 03:46:04 +0200 Subject: [PATCH 26/63] Refactor `verify-npm-package` --- actions/verify-npm-package/README.md | 14 +++--- actions/verify-npm-package/action.yml | 64 ++++++++------------------- tests/npm-package/package.json | 5 +-- 3 files changed, 27 insertions(+), 56 deletions(-) diff --git a/actions/verify-npm-package/README.md b/actions/verify-npm-package/README.md index a86c7992e..62ec495a1 100644 --- a/actions/verify-npm-package/README.md +++ b/actions/verify-npm-package/README.md @@ -4,7 +4,7 @@ Verifies the source files of a **NodeJS** package - by running its `verify` scri It is worth noting this action can support any technology - as long as you comply with the requirements described below. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -23,7 +23,7 @@ steps: } ``` -## 💡How it works +## 💡 How it works 1. Run [check-project-license](../check-project-license/README.md) to verify the **LICENSE** file. @@ -39,19 +39,19 @@ steps: 1. If a **tests** directory exists within `project-directory`, execute [run-custom-tests](../run-custom-tests/README.md) on it, with the `optional` flag enabled. - 💡The rationale for this step is a parallelism with Rust's **tests** directory - dedicated to verify the crate under test from a _client_ perspective; however, in `verify-npm-package` you have even more fine-grained control over the test process: for example, you can automatically launch _a Bash script_ to test the system, while still relying on the **tests** directory to host utility modules imported by different tests in the **src** directory tree. + 💡The rationale for this step is a parallelism with Rust's **tests** directory - dedicated to verify the crate under test from a _client_ perspective; however, in `verify-npm-package` you have even more fine-grained control over the test process: for example, you can automatically launch _a shell script_ to test the system, while still relying on the **tests** directory to host utility modules imported by different tests in the **src** directory tree. - **Please, note**: should you need to execute a shell script for testing, a `verify.sh` script, run by Bash, is required; for further details, please refer to [run-custom-tests](../run-custom-tests/README.md). + **Please, note**: should you need to execute a shell script for testing, a `verify.elv` / `verify.sh` script, run by Elvish or Bash, is required; for further details, please refer to [run-custom-tests](../run-custom-tests/README.md). 1. Find [critical TODOs](../find-critical-todos/README.md) in the source code - which crash the workflow by default. -## ☑️Requirements +## ☑️ Requirements - The entire verification process for the package must be triggered by the `verify` script in `package.json` (see the example). - The requirements for [setup-nodejs-context](../setup-nodejs-context/README.md). -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------------: | :---------------------: | :-----------------------------------------------------: | :-----------------------: | @@ -61,7 +61,7 @@ steps: | `check-subpath-exports` | **boolean** | Run `check-subpath-exports` after the **verify** script | **true** | | `project-directory` | **string** | The directory containing `package.json` | **.** | -## 🌐Further references +## 🌐 Further references - [check-project-license](../check-project-license/README.md) diff --git a/actions/verify-npm-package/action.yml b/actions/verify-npm-package/action.yml index 0a0eb3e28..6bea3b0f6 100644 --- a/actions/verify-npm-package/action.yml +++ b/actions/verify-npm-package/action.yml @@ -1,5 +1,5 @@ name: Verify npm package -description: Verifies the source files of a NodeJS package - by running its 'verify' script within the 'scripts' section of 'package.json'. +description: Verifies the source files of a NodeJS package - by running its `verify` script within the `scripts` section of `package.json`. inputs: crash-on-critical-todos: @@ -8,56 +8,24 @@ inputs: source-file-regex: description: PCRE pattern describing the source files. - default: '^\.\/(src|(tests(?!\/node_modules\/)))\/.+\.(c|m)?(j|t)sx?$' + default: '^(src|(tests(?!\/node_modules\/)))\/.+\.(c|m)?(j|t)sx?$' enforce-branch-version: description: How the branch version should be enforced. default: inject check-subpath-exports: - description: Run check-subpath-exports after the "verify" package script. + description: Run check-subpath-exports after the `verify` package script. default: true project-directory: - description: The directory containing package.json. + description: The directory containing `package.json`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.crash-on-critical-todos }}" ]] - then - echo "❌Missing action input: 'crash-on-critical-todos'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.check-subpath-exports }}" ]] - then - echo "❌Missing action input: 'check-subpath-exports'!" >&2 - exit 1 - fi - - if [[ ! -f "package.json" ]] - then - echo "❌The package.json descriptor file does not exist!!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Check the project license uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 @@ -74,20 +42,21 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Run the 'verify' script from package.json - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "🔎Now running the 'verify' package.json script..." - pnpm verify - echo "✅'verify' script successful!" + use github.com/giancosta86/aurora-elvish/console + use aurora-github/nodejs/project + + project:verify - name: Try to build the package artifacts - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - source "${{ github.action_path }}/../../core/bash/npmPackage.sh" + use aurora-github/nodejs/pnpm - tryToRunNpmBuildScript + pnpm:run-optional-script build - name: Check subpath exports if: inputs.check-subpath-exports == 'true' @@ -110,5 +79,8 @@ runs: root-directory: ${{ inputs.project-directory }} - name: Print confirmation message - shell: bash - run: echo "✅npm package project verified!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅☕ npm package project verified! diff --git a/tests/npm-package/package.json b/tests/npm-package/package.json index 750d41908..851f21168 100644 --- a/tests/npm-package/package.json +++ b/tests/npm-package/package.json @@ -4,9 +4,8 @@ "type": "module", "description": "Toy npm package", "scripts": { - "test": "echo '✅NodeJS tests simulated!'", - "build": "echo '✅NodeJS build simulated!'", - "verify": "pnpm test && pnpm build" + "verify": "echo '📦📃 package.json `verify` script!'", + "build": "echo '📦📃 package.json `build` script!'" }, "devDependencies": { "@giancosta86/typed-env": "^2.0.3" From 7e9bf972249ebf0c82cd6c9572885bd0396cc902 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 16:09:23 +0200 Subject: [PATCH 27/63] Refactor `publish-npm-package` --- actions/publish-npm-package/README.md | 26 +++---- actions/publish-npm-package/action.yml | 95 ++++++++------------------ 2 files changed, 41 insertions(+), 80 deletions(-) diff --git a/actions/publish-npm-package/README.md b/actions/publish-npm-package/README.md index 3f5f7d816..57478085c 100644 --- a/actions/publish-npm-package/README.md +++ b/actions/publish-npm-package/README.md @@ -2,7 +2,7 @@ Publishes a **NodeJS** package to an [npm](https://www.npmjs.com/) registry. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -13,15 +13,7 @@ steps: npm-token: ${{ secrets.NPM_TOKEN }} ``` -### Remarks - -- This action is designed for _publication_ only - not for _verification_: you should call [verify-npm-package](../verify-npm-package/README.md) for that instead. - -- Before the first publication, running with `dry-run` set to **true** during the _verification_ phase is recommended. - -- This action is automatically run by [publish-rust-wasm](../publish-rust-wasm/README.md). - -## 💡How it works +## 💡 How it works 1. Run [enforce-branch-version](../enforce-branch-version/README.md), forwarding the `enforce-branch-version` input to its `mode` input. @@ -35,7 +27,15 @@ steps: 1. Run `pnpm publish`, with the value of `npm-token` injected into the **NPM_TOKEN** environment variable - accessible, for example, from the `.npmrc` configuration file. -## ☑️Requirements +## 💬 Remarks + +- This action is designed for _publication_ only - not for _verification_: you should call [verify-npm-package](../verify-npm-package/README.md) for that instead. + +- Before the first publication, running with `dry-run` set to **true** during the _verification_ phase is recommended. + +- This action is automatically run by [publish-rust-wasm](../publish-rust-wasm/README.md). + +## ☑️ Requirements - The requirements for [setup-nodejs-context](../setup-nodejs-context/README.md). @@ -45,7 +45,7 @@ steps: - Before the first publication, running with `dry-run` set to **true** is recommended. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :-----------------------------------------------: | :-----------: | @@ -55,7 +55,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `package.json` | **.** | -## 🌐Further references +## 🌐 Further references - [setup-nodejs-context](../setup-nodejs-context/README.md) diff --git a/actions/publish-npm-package/action.yml b/actions/publish-npm-package/action.yml index 688413661..2e0e9307e 100644 --- a/actions/publish-npm-package/action.yml +++ b/actions/publish-npm-package/action.yml @@ -3,7 +3,7 @@ description: Publishes a NodeJS package to an npm registry. inputs: dry-run: - description: Run a simulated publication via --dry-run. + description: Run a simulated publication via `--dry-run`. default: false npm-token: @@ -18,45 +18,13 @@ inputs: default: inject project-directory: - description: The directory containing package.json. + description: The directory containing `package.json`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - if [[ ${{ inputs.dry-run }} != 'true' && -z "${{ inputs.npm-token }}" ]] - then - echo "❌Missing action input: 'npm-token'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.website-directory }}" ]] - then - echo "❌Missing action input: 'website-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi - - if [[ ! -f "package.json" ]] - then - echo "❌The package.json descriptor file does not exist!!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Enforce branch version uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 @@ -70,12 +38,12 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Try to build the package artifacts - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - source "${{ github.action_path }}/../../core/bash/npmPackage.sh" + use aurora-github/nodejs/pnpm - tryToRunNpmBuildScript + pnpm:run-optional-script build - name: Publish the GitHub Pages website uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 @@ -86,47 +54,40 @@ runs: enforce-branch-version: ${{ inputs.enforce-branch-version }} - name: Display the artifact descriptor - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "The content of your 📦package.json just before publication is:" - jq -C . package.json - echo "📦📦📦" + use github.com/giancosta86/aurora-elvish/console + use aurora-github/project/descriptors/json + + console:section &emoji=📦 'package.json just before publication' { + json:print-content package.json + } - name: Ensure the package manager configuration file exists - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - npmConfigPath=".npmrc" - - if [[ ! -f "$npmConfigPath" ]] - then - echo "🌟It seems you don't have a $npmConfigPath file - generating a default one..." - - echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > "$npmConfigPath" + use aurora-github/nodejs/pnpm - echo "🎀Your auto-generated $npmConfigPath configuration file is:" - cat "$npmConfigPath" - echo "🎀🎀🎀" - else - echo "🌟You already have a custom $npmConfigPath file - ready to publish!" - fi + pnpm:ensure-config - name: Publish to the registry - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - dryRunArg="--dry-run" - else - dryRunArg="" - fi - - pnpm publish --no-git-checks --access public $dryRunArg + use aurora-github/ci-cd/input + use aurora-github/nodejs/project + + var dry-run = (input:bool dry-run '${{ inputs.dry-run }}') + + project:publish $dry-run env: NPM_TOKEN: ${{ inputs.npm-token }} - name: Print confirmation message - shell: bash - run: echo "✅Npm package published successfully!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅📦 Npm package publication action successful! From 1b0815df28d7a06447ed1d3feab65bd14f961f3a Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 17:16:11 +0200 Subject: [PATCH 28/63] Refactor `verify-rust-crate` --- actions/verify-rust-crate/README.md | 14 ++-- actions/verify-rust-crate/action.yml | 108 +++++---------------------- tests/rust-crate/src/lib.rs | 9 +++ 3 files changed, 35 insertions(+), 96 deletions(-) diff --git a/actions/verify-rust-crate/README.md b/actions/verify-rust-crate/README.md index ad18ac140..246b9a302 100644 --- a/actions/verify-rust-crate/README.md +++ b/actions/verify-rust-crate/README.md @@ -2,7 +2,7 @@ Verifies the source files of a **Rust** crate. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -13,7 +13,7 @@ steps: **Please, note**: this action is automatically run by [verify-rust-wasm](../verify-rust-wasm/README.md). -## 💡How it works +## 💡 How it works 1. Run [check-project-license](../check-project-license/README.md) to verify the **LICENSE** file. @@ -35,11 +35,11 @@ steps: 1. Find [critical TODOs](../find-critical-todos/README.md) in the source code - which crash the workflow by default. -## ☑️Requirements +## ☑️ Requirements -- `rust-toolchain.toml` must be present in `project-directory` - as described in [check-rust-versions](../check-rust-versions/README.md) +- `rust-toolchain.toml` must be present in `project-directory` - as described in [setup-rust-context](../setup-rust-context/README.md) -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------------: | :---------------------: | :-----------------------------------------------: | :-----------------------: | @@ -50,7 +50,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [check-project-license](../check-project-license/README.md) @@ -62,6 +62,6 @@ steps: - [enforce-branch-version](../enforce-branch-version/README.md) -- [check-rust-versions](../check-rust-versions/README.md) +- [setup-rust-context](../setup-rust-context/README.md) - [aurora-github](../../README.md) diff --git a/actions/verify-rust-crate/action.yml b/actions/verify-rust-crate/action.yml index 5f2e2a7ff..e663e8cd7 100644 --- a/actions/verify-rust-crate/action.yml +++ b/actions/verify-rust-crate/action.yml @@ -16,52 +16,20 @@ inputs: source-file-regex: description: PCRE pattern describing the source files. - default: '^\.\/(src|tests)\/.+\.rs$' + default: '^(src|tests)\/.+\.rs$' enforce-branch-version: description: How the branch version should be enforced. default: inject project-directory: - description: The directory containing Cargo.toml. + description: The directory containing `Cargo.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.run-clippy-checks }}" ]] - then - echo "❌Missing action input: 'run-clippy-checks'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.check-rustdoc }}" ]] - then - echo "❌Missing action input: 'check-rustdoc'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.crash-on-critical-todos }}" ]] - then - echo "❌Missing action input: 'crash-on-critical-todos'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Check the project license uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 @@ -72,37 +40,23 @@ runs: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - name: Check Rust versions - uses: giancosta86/aurora-github/actions/check-rust-versions@v10.3.0 + - name: Setup Rust context + uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 with: project-directory: ${{ inputs.project-directory }} - name: Check style - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - checkStyle() { - checkFormat - - if [[ "${{ inputs.run-clippy-checks }}" == "true" ]] - then - runClippyChecks - fi - } + use aurora-github/ci-cd/input + use aurora-github/rust/project - checkFormat() { - echo "🎨Checking source code format..." - cargo fmt --check - echo "✅Source code format OK!" - } + var run-clippy-checks = (input:bool run-clippy-checks '${{ inputs.run-clippy-checks }}') - runClippyChecks() { - echo "📎Running clippy checks..." - cargo clippy --all-targets --all-features -- -D warnings - echo "✅Clippy checks OK!" - } + var check-rustdoc = (input:bool check-rustdoc '${{ inputs.check-rustdoc }}') - checkStyle + project:check-style &run-clippy-checks=$run-clippy-checks &check-rustdoc=$check-rustdoc - name: Extract code snippets as tests from README.md uses: giancosta86/aurora-github/actions/extract-rust-snippets@v10.3.0 @@ -111,39 +65,12 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Run tests - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - runTests() { - runVanillaTests - - runAllFeaturesTests - - if [[ "${{ inputs.check-rustdoc }}" == "true" ]] - then - runDocTests - fi - } + use aurora-github/rust/project - runVanillaTests() { - echo "🔬Running tests with no features enabled..." - cargo test - echo "✅Tests with no features OK!" - } - - runAllFeaturesTests() { - echo "🔬Running tests with all the features enabled..." - cargo test --all-features - echo "✅Tests with all the features OK!" - } - - runDocTests() { - echo "📚Running doctests with all the features enabled..." - RUSTDOCFLAGS="-D warnings" cargo doc --all-features - echo "✅Doctests with all the features OK!" - } - - runTests + project:run-tests - name: Check for critical TODOs uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 @@ -154,5 +81,8 @@ runs: root-directory: ${{ inputs.project-directory }} - name: Print confirmation message - shell: bash - run: echo "✅Rust crate project verified!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🦀 Rust crate project verified! diff --git a/tests/rust-crate/src/lib.rs b/tests/rust-crate/src/lib.rs index b93cf3ffd..855cce2ee 100644 --- a/tests/rust-crate/src/lib.rs +++ b/tests/rust-crate/src/lib.rs @@ -1,3 +1,12 @@ +/// Sums two numbers. +/// +/// ``` +/// use rust_crate::*; +/// +/// let result = add(90, 2); +/// +/// assert_eq!(result, 92); +/// ``` pub fn add(left: u64, right: u64) -> u64 { left + right } From 1c835064b226ddc7ce3fe6dd3029516d4c4aef3f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 18:52:33 +0200 Subject: [PATCH 29/63] Refactor `verify-rust-wasm` --- actions/verify-rust-wasm/README.md | 14 ++--- actions/verify-rust-wasm/action.yml | 83 ++++++----------------------- 2 files changed, 22 insertions(+), 75 deletions(-) diff --git a/actions/verify-rust-wasm/README.md b/actions/verify-rust-wasm/README.md index 7b5796e0d..8f5687d94 100644 --- a/actions/verify-rust-wasm/README.md +++ b/actions/verify-rust-wasm/README.md @@ -2,7 +2,7 @@ Verifies the source files of a **Rust** web assembly. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -14,7 +14,7 @@ steps: npm-scope: your-npm-scope ``` -## 💡How it works +## 💡 How it works 1. Invoke the [install-wasm-pack](../install-wasm-pack/README.md) action, passing all the matching inputs, to install the `wasm-pack` command. @@ -26,13 +26,13 @@ steps: 1. If the directory referenced by the `client-tests-directory` input exists, execute the [run-custom-tests](../run-custom-tests/README.md) action on it, with the `optional` flag enabled. -## ☑️Requirements +## ☑️ Requirements -- `rust-toolchain.toml` must be present in `project-directory` - as described in [check-rust-versions](../check-rust-versions/README.md) +- `rust-toolchain.toml` must be present in `project-directory` - as described in [setup-rust-context](../setup-rust-context/README.md) - Please, refer to the documentation of [run-custom-tests](../run-custom-tests/README.md) for details about setting up a suitable structure for `client-tests-directory`. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------------: | :---------------------: | :-----------------------------------------------: | :-----------------------: | @@ -47,7 +47,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [check-project-license](../check-project-license/README.md) @@ -57,7 +57,7 @@ steps: - [parse-npm-scope](../parse-npm-scope/README.md) -- [check-rust-versions](../check-rust-versions/README.md) +- [setup-rust-context](../setup-rust-context/README.md) - [install-wasm-pack](../install-wasm-pack/README.md) diff --git a/actions/verify-rust-wasm/action.yml b/actions/verify-rust-wasm/action.yml index b85c3aff9..1aef8bb19 100644 --- a/actions/verify-rust-wasm/action.yml +++ b/actions/verify-rust-wasm/action.yml @@ -6,14 +6,14 @@ inputs: description: The wasm-pack version to install. npm-scope: - description: The npm package scope or "". + description: The npm package scope or ``. client-tests-directory: description: Relative directory containing the client tests. default: client-tests wasm-target: - description: The target of the 'wasm-pack build' command. + description: The target of the `wasm-pack build` command. default: web run-clippy-checks: @@ -30,76 +30,20 @@ inputs: source-file-regex: description: PCRE pattern describing the source files. - default: '^\.\/((src|tests)\/.+\.rs)|(client-tests\/.+\.(c|m)?(j|t)sx?)$' + default: '^((src|tests)\/.+\.rs)|(client-tests\/.+\.(c|m)?(j|t)sx?)$' enforce-branch-version: description: How the branch version should be enforced. default: inject project-directory: - description: The directory containing Cargo.toml. + description: The directory containing `Cargo.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.wasm-pack-version }}" ]] - then - echo "❌Missing action input: 'wasm-pack-version'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.npm-scope }}" ]] - then - echo "❌Missing action input: 'npm-scope'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.client-tests-directory }}" ]] - then - echo "❌Missing action input: 'client-tests-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.wasm-target }}" ]] - then - echo "❌Missing action input: 'wasm-target'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.run-clippy-checks }}" ]] - then - echo "❌Missing action input: 'run-clippy-checks'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.check-rustdoc }}" ]] - then - echo "❌Missing action input: 'check-rustdoc'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.crash-on-critical-todos }}" ]] - then - echo "❌Missing action input: 'crash-on-critical-todos'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Install wasm-pack uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 @@ -117,14 +61,14 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Run headless browser tests - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "🌐Running headless browser tests..." - wasm-pack test --chrome --headless --release - echo "✅Headless browser tests OK!" + use aurora-github/rust/wasm-pack + + wasm-pack:run-browser-tests - - name: Generate the NodeJS package source files + - name: Generate the wasm target uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.3.0 with: target: ${{ inputs.wasm-target }} @@ -140,5 +84,8 @@ runs: root-directory: ${{ inputs.project-directory }}/${{ inputs.client-tests-directory }} - name: Print confirmation message - shell: bash - run: echo "✅Rust web assembly project verified!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🦀🌐 Rust web assembly project verified! From d0608a14d8d55206464ffadd863e01cf302550cd Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Thu, 24 Apr 2025 16:14:07 +0200 Subject: [PATCH 30/63] Refactor and enhance `publish-rust-crate`: * now referencing `setup-rust-context` to ensure the expected toolchain is used * skip altering the `[package.metadata.docs.rs]` if it's already in Cargo.toml --- .../test-publish-rust-crate/action.yml | 27 ++++- .../assert-doc-addendum/action.yml | 22 ++++ .../scenario/action.yml | 54 +++++++++ actions/publish-rust-crate/README.md | 24 ++-- actions/publish-rust-crate/action.yml | 106 ++++++------------ 5 files changed, 152 insertions(+), 81 deletions(-) create mode 100644 .github/test-actions/test-publish-rust-crate/assert-doc-addendum/action.yml create mode 100644 .github/test-actions/test-publish-rust-crate/scenario/action.yml diff --git a/.github/test-actions/test-publish-rust-crate/action.yml b/.github/test-actions/test-publish-rust-crate/action.yml index 7534cf086..c9563b188 100644 --- a/.github/test-actions/test-publish-rust-crate/action.yml +++ b/.github/test-actions/test-publish-rust-crate/action.yml @@ -3,7 +3,28 @@ name: Test publish-rust-crate runs: using: composite steps: - - uses: ./actions/publish-rust-crate + - name: Backup Cargo.toml + shell: bash + working-directory: tests/rust-crate + run: cp Cargo.toml Cargo.toml.bak + + - uses: ./.github/test-actions/test-publish-rust-crate/scenario + with: + header: Testing publication with no initial addendum - and injection disabled + pre-existing-addendum: false + document-all-features: false + should-contain-addendum: false + + - uses: ./.github/test-actions/test-publish-rust-crate/scenario + with: + header: Testing publication with no initial addendum - and injection enabled + pre-existing-addendum: false + document-all-features: true + should-contain-addendum: true + + - uses: ./.github/test-actions/test-publish-rust-crate/scenario with: - dry-run: true - project-directory: tests/rust-crate + header: Testing publication with initial addendum - and injection enabled + pre-existing-addendum: true + document-all-features: true + should-contain-addendum: true diff --git a/.github/test-actions/test-publish-rust-crate/assert-doc-addendum/action.yml b/.github/test-actions/test-publish-rust-crate/assert-doc-addendum/action.yml new file mode 100644 index 000000000..d9bbcd531 --- /dev/null +++ b/.github/test-actions/test-publish-rust-crate/assert-doc-addendum/action.yml @@ -0,0 +1,22 @@ +name: Assert that the documentation addendum appears a given number of times in Cargo.toml + +inputs: + expected-count: + description: How many times the documentation addendum should appear in Cargo.toml. + +runs: + using: composite + steps: + - shell: bash + working-directory: tests/rust-crate + run: | + expectedCount="${{ inputs.expected-count }}" + actualCount="$(grep -c '\[package.metadata.docs.rs\]' Cargo.toml || true)" + + if [[ "$actualCount" == $expectedCount ]] + then + echo "✅ Instances of the documentation addendum: $actualCount - as expected!" + else + echo "❌ Instances of the documentation addendum: $actualCount. Expected: $expectedCount" + exit 1 + fi diff --git a/.github/test-actions/test-publish-rust-crate/scenario/action.yml b/.github/test-actions/test-publish-rust-crate/scenario/action.yml new file mode 100644 index 000000000..81b1a2699 --- /dev/null +++ b/.github/test-actions/test-publish-rust-crate/scenario/action.yml @@ -0,0 +1,54 @@ +name: Runs a scenario for publish-rust-crate + +inputs: + header: + description: The header for this test. + + pre-existing-addendum: + description: Whether the documentation addendum should exist before running the action. + + document-all-features: + description: Forwarded to the action. + + should-contain-addendum: + description: Whether Cargo.toml should contain the documentation addendum. + +runs: + using: composite + steps: + - name: Display the header + shell: bash + run: echo "🎭 ${{ inputs.header }}" + + - name: Restore Cargo.toml + shell: bash + working-directory: tests/rust-crate + run: cp Cargo.toml.bak Cargo.toml + + - name: Manually add the documentation addendum + if: inputs.pre-existing-addendum == 'true' + shell: bash + working-directory: tests/rust-crate + run: | + echo >> Cargo.toml + echo '# Manually added' >> Cargo.toml + echo '[package.metadata.docs.rs]' >> Cargo.toml + echo 'all-features = true' >> Cargo.toml + + - uses: ./actions/publish-rust-crate + with: + dry-run: true + document-all-features: ${{ inputs.document-all-features }} + project-directory: tests/rust-crate + + - name: Verify that Cargo.toml actually includes the documentation addendum + uses: ./.github/test-actions/test-publish-rust-crate/assert-doc-addendum + if: inputs.should-contain-addendum == 'true' + with: + expected-count: 1 + + - name: Verify that Cargo.toml does not include the documentation addendum + uses: ./.github/test-actions/test-publish-rust-crate/assert-doc-addendum + if: inputs.should-contain-addendum != 'true' + with: + expected-count: 0 diff --git a/actions/publish-rust-crate/README.md b/actions/publish-rust-crate/README.md index d3fa07a3b..51c423780 100644 --- a/actions/publish-rust-crate/README.md +++ b/actions/publish-rust-crate/README.md @@ -2,7 +2,7 @@ Publishes a **Rust** crate - by default, to [crates.io](https://crates.io/) - with all of its features enabled. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -15,25 +15,31 @@ steps: **Please, note**: this action is designed for _publication_ only - not for _verification_: you may want to use [verify-rust-crate](../verify-rust-crate/README.md) for that. -## 💡How it works +## 💡 How it works 1. Run [enforce-branch-version](../enforce-branch-version/README.md), forwarding the `enforce-branch-version` input to its `mode` input. -1. Prepare the descriptor - for example, enabling documentation for all the features +1. Run [setup-rust-context](../setup-rust-context/README.md) to setup a Rust toolchain. -1. Run [publish-github-pages](../publish-github-pages/README.md) with the `optional` flag enabled +1. If the `document-all-features` input is **true**, enable documentation for all the features - but only if the `[package.metadata.docs.rs]` header is not already in the descriptor. For details, please consult [this link](https://docs.rs/about/metadata). -1. Run `cargo publish`, with the `--all-features` flag +1. Run [publish-github-pages](../publish-github-pages/README.md) with the `optional` flag enabled. -## ☑️Requirements +1. Display **Cargo.toml** just before publication. + +1. Run `cargo publish`, with the `--all-features` flag. + +## ☑️ Requirements - `cargo-token` is _not_ mandatory when `dry-run` is enabled. +- `rust-toolchain.toml` must be present in `project-directory` - as described in [setup-rust-context](../setup-rust-context/README.md). + - The requirements for [publish-github-pages](../publish-github-pages/README.md) if `website-directory` references an existing directory. - Before the first publication, running with `dry-run` set to **true** is recommended. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :---------------------------------------------------------------: | :-----------: | @@ -44,12 +50,14 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [publish-github-pages](../publish-github-pages/README.md) - [enforce-branch-version](../enforce-branch-version/README.md) +- [setup-rust-context](../setup-rust-context/README.md) + - [verify-rust-crate](../verify-rust-crate/README.md) - [aurora-github](../../README.md) diff --git a/actions/publish-rust-crate/action.yml b/actions/publish-rust-crate/action.yml index 0f1c3dbcd..f4b65d63d 100644 --- a/actions/publish-rust-crate/action.yml +++ b/actions/publish-rust-crate/action.yml @@ -3,7 +3,7 @@ description: Publishes a Rust crate - by default, to crates.io - with all of its inputs: dry-run: - description: Run a simulated publication via --dry-run. + description: Run a simulated publication via `--dry-run`. default: false cargo-token: @@ -22,45 +22,13 @@ inputs: default: inject project-directory: - description: The directory containing Cargo.toml. + description: The directory containing `Cargo.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - if [[ ${{ inputs.dry-run }} != 'true' && -z "${{ inputs.cargo-token }}" ]] - then - echo "❌Missing action input: 'cargo-token'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.document-all-features }}" ]] - then - echo "❌Missing action input: 'document-all-features'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.website-directory }}" ]] - then - echo "❌Missing action input: 'website-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Enforce branch version uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 @@ -68,33 +36,19 @@ runs: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - shell: bash + - name: Setup Rust context + uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 + with: + project-directory: ${{ inputs.project-directory }} + + - name: Document all features + if: inputs.document-all-features == 'true' + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - prepareDescriptor() { - if [[ "${{ inputs.document-all-features }}" == "true" ]] - then - enableRustdocForAllFeatures - fi + use aurora-github/rust/project - displayDescriptor - } - - enableRustdocForAllFeatures() { - cat << EOF >> Cargo.toml - - [package.metadata.docs.rs] - all-features = true - EOF - } - - displayDescriptor() { - echo "The content of your 🦀Cargo.toml just before publication is:" - cat Cargo.toml - echo "🦀🦀🦀" - } - - prepareDescriptor + project:document-all-features - name: Publish the GitHub Pages website uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 @@ -104,21 +58,33 @@ runs: dry-run: ${{ inputs.dry-run }} enforce-branch-version: ${{ inputs.enforce-branch-version }} + - name: Display project descriptor + shell: elvish {0} + working-directory: ${{ inputs.project-directory }} + run: | + use github.com/giancosta86/aurora-elvish/console + use aurora-github/project/descriptors/toml + + console:section &emoji=🦀 'Cargo.toml just before publication' { + toml:print-content Cargo.toml + } + - name: Publish the crate - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - dryRunArg="--dry-run" - else - dryRunArg="" - fi - - cargo publish --all-features --allow-dirty $dryRunArg + use aurora-github/ci-cd/input + use aurora-github/rust/crate + + var dry-run = (input:bool dry-run '${{ inputs.dry-run }}') + + crate:publish $dry-run env: CARGO_REGISTRY_TOKEN: ${{ inputs.cargo-token }} - name: Print confirmation message - shell: bash - run: echo "✅The 🦀Rust crate was published successfully!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🦀 Rust crate publication action successful! From 218203d85966fe252a5bcf71051880db35879d95 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Thu, 24 Apr 2025 19:13:52 +0200 Subject: [PATCH 31/63] Refactor `publish-rust-wasm` --- .../test-publish-rust-wasm/action.yml | 1 + actions/publish-rust-wasm/README.md | 10 +-- actions/publish-rust-wasm/action.yml | 84 ++++--------------- 3 files changed, 23 insertions(+), 72 deletions(-) diff --git a/.github/test-actions/test-publish-rust-wasm/action.yml b/.github/test-actions/test-publish-rust-wasm/action.yml index cedf33cb3..96a4912be 100644 --- a/.github/test-actions/test-publish-rust-wasm/action.yml +++ b/.github/test-actions/test-publish-rust-wasm/action.yml @@ -9,4 +9,5 @@ runs: dry-run: true wasm-pack-version: 0.13.1 nodejs-version: 20.15.1 + pnpm-version: 10.6.1 npm-scope: giancosta86 diff --git a/actions/publish-rust-wasm/README.md b/actions/publish-rust-wasm/README.md index 8a1ecc5f0..0fa80cbc8 100644 --- a/actions/publish-rust-wasm/README.md +++ b/actions/publish-rust-wasm/README.md @@ -2,7 +2,7 @@ Publishes a **Rust** web assembly to an [npm](https://www.npmjs.com/) registry. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -17,7 +17,7 @@ steps: **Please, note**: this action is designed for _publication_ only - not for verification: you might want to use [verify-rust-wasm](../verify-rust-wasm/README.md) for that. -## 💡How it works +## 💡 How it works 1. Invoke the [install-wasm-pack](../install-wasm-pack/README.md) action, passing all the matching inputs, to install the `wasm-pack` command. @@ -27,7 +27,7 @@ steps: 1. Call [publish-npm-package](../publish-npm-package/README.md) on the **pkg** directory - passing all the matching inputs - to publish the npm package. -## ☑️Requirements +## ☑️ Requirements - the `nodejs-version` input is required for the build process; optionally, you can set the `pnpm-version` input as well, in order to request a specific pnpm version. @@ -39,7 +39,7 @@ steps: - Before the first publication, running with `dry-run` set to **true** is recommended. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :--------------------------------------------------------: | :-----------: | @@ -54,7 +54,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [generate-wasm-target](../generate-wasm-target/README.md) diff --git a/actions/publish-rust-wasm/action.yml b/actions/publish-rust-wasm/action.yml index 4f835e5be..c2f4f863e 100644 --- a/actions/publish-rust-wasm/action.yml +++ b/actions/publish-rust-wasm/action.yml @@ -3,7 +3,7 @@ description: Publishes a Rust web assembly to an npm registry. inputs: dry-run: - description: Run a simulated publication via --dry-run. + description: Run a simulated publication via `--dry-run`. default: false npm-token: @@ -13,16 +13,16 @@ inputs: description: The wasm-pack version to install. npm-scope: - description: The npm package scope or "". + description: The npm package scope or ``. nodejs-version: - description: The "engines / node" version within package.json. + description: The `engines -> node` version within `package.json`. pnpm-version: - description: The "packageManager" reference to pnpm within package.json. + description: The `packageManager` reference to pnpm within `package.json`. wasm-target: - description: The target of the 'wasm-pack build' command. + description: The target of the `wasm-pack build` command. default: web website-directory: @@ -34,63 +34,13 @@ inputs: default: inject project-directory: - description: The directory containing Cargo.toml. + description: The directory containing `Cargo.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - if [[ ${{ inputs.dry-run }} != 'true' && -z "${{ inputs.npm-token }}" ]] - then - echo "❌Missing action input: 'npm-token'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.wasm-pack-version }}" ]] - then - echo "❌Missing action input: 'wasm-pack-version'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.npm-scope }}" ]] - then - echo "❌Missing action input: 'npm-scope'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.nodejs-version }}" ]] - then - echo "❌Missing action input: 'nodejs-version'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.wasm-target }}" ]] - then - echo "❌Missing action input: 'wasm-target'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.website-directory }}" ]] - then - echo "❌Missing action input: 'website-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Install wasm-pack uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 @@ -109,19 +59,12 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Try to copy .npmrc from the project directory - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ -f ".npmrc" ]] - then - echo "🎉.npmrc configuration file found! Copying it to the package directory..." - - cp .npmrc pkg/ + use aurora-github/rust/wasm - echo "✅.npmrc file copied!" - else - echo "💭No .npmrc configuration file found in the project directory..." - fi + wasm:copy-npmrc-from-project-directory - name: Publish the GitHub Pages website uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 @@ -138,3 +81,10 @@ runs: npm-token: ${{ inputs.npm-token }} enforce-branch-version: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }}/pkg + + - name: Print confirmation message + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🦀🌐 Rust web assembly publication action successful! From 8b384f4971e5b6267875bddfdaf42c7d6f87b665 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 25 Apr 2025 19:10:10 +0200 Subject: [PATCH 32/63] Refactor `verify-python-package` --- .../test-verify-python-package/action.yml | 2 +- actions/verify-python-package/README.md | 10 ++-- actions/verify-python-package/action.yml | 60 +++++++------------ tests/python-lib/pyproject.toml | 2 +- 4 files changed, 30 insertions(+), 44 deletions(-) diff --git a/.github/test-actions/test-verify-python-package/action.yml b/.github/test-actions/test-verify-python-package/action.yml index b2f310cc6..dc049baa3 100644 --- a/.github/test-actions/test-verify-python-package/action.yml +++ b/.github/test-actions/test-verify-python-package/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Verifying 🐍Python library..." + run: echo "🎭 Verifying 🐍 Python library..." - uses: ./actions/verify-python-package with: diff --git a/actions/verify-python-package/README.md b/actions/verify-python-package/README.md index 4f19a97df..d469d9173 100644 --- a/actions/verify-python-package/README.md +++ b/actions/verify-python-package/README.md @@ -2,7 +2,7 @@ Verifies the source files of a **Python** package using [PDM](https://pdm-project.org). -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,7 +11,7 @@ steps: - uses: giancosta86/aurora-github/actions/verify-python-package@v10 ``` -## 💡How it works +## 💡 How it works 1. Run [check-project-license](../check-project-license/README.md) to verify the **LICENSE** file. @@ -25,13 +25,13 @@ steps: 1. Find [critical TODOs](../find-critical-todos/README.md) in the source code - which crash the workflow by default. -## ☑️Requirements +## ☑️ Requirements - `pipx` is mandatory when PDM has to be installed. - the **verify** script must be declared within **pyproject.toml**. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------------: | :---------------------: | :--------------------------------------------: | :-----------------------: | @@ -41,7 +41,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing **pyproject.toml** | **.** | -## 🌐Further references +## 🌐 Further references - [check-project-license](../check-project-license/README.md) diff --git a/actions/verify-python-package/action.yml b/actions/verify-python-package/action.yml index 89d284cc6..f2af595a3 100644 --- a/actions/verify-python-package/action.yml +++ b/actions/verify-python-package/action.yml @@ -11,40 +11,20 @@ inputs: source-file-regex: description: PCRE pattern describing the source files. - default: '^\.\/(src|tests)\/.+\.pyw?$' + default: '^(src|tests)\/.+\.pyw?$' enforce-branch-version: description: How the branch version should be enforced. default: inject project-directory: - description: The directory containing pyproject.toml. + description: The directory containing `pyproject.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.crash-on-critical-todos }}" ]] - then - echo "❌Missing action input: 'crash-on-critical-todos'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Check the project license uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 @@ -55,29 +35,32 @@ runs: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - name: Ensure pdm - shell: bash + - name: Setup Python context + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - source "${{ github.action_path }}/../../core/bash/pdm.sh" + use aurora-github/ci-cd/input + use aurora-github/python/context - ensurePdm + var pdm-version = (input:string &optional pdm-version '${{ inputs.pdm-version }}') + + context:setup &pdm-version=$pdm-version - name: Verify the project - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "🔬Verifying the project..." - pdm run verify - echo "✅Project verified!" + use aurora-github/python/project + + project:verify - name: Build the project - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - echo "📦Building the project..." - pdm build - echo "✅Project built successfully!" + use aurora-github/python/project + + project:build - name: Check for critical TODOs uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 @@ -88,5 +71,8 @@ runs: root-directory: ${{ inputs.project-directory }} - name: Print confirmation message - shell: bash - run: echo "✅Python project verified!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🐍 Python project verified! diff --git a/tests/python-lib/pyproject.toml b/tests/python-lib/pyproject.toml index 9607677c4..cd0c065ea 100644 --- a/tests/python-lib/pyproject.toml +++ b/tests/python-lib/pyproject.toml @@ -15,4 +15,4 @@ build-backend = "hatchling.build" distribution = true [tool.pdm.scripts] -verify = "echo '✅Simulating passing tests!'" +verify = "echo '🐍📃 pyproject.toml `verify` script!'" From 317a264d870e90639047e10d160ac0482c591e2f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 25 Apr 2025 20:02:13 +0200 Subject: [PATCH 33/63] Refactor `publish-python-package` --- .../test-publish-python-package/action.yml | 2 +- actions/publish-python-package/README.md | 10 ++-- actions/publish-python-package/action.yml | 58 +++++++------------ 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/.github/test-actions/test-publish-python-package/action.yml b/.github/test-actions/test-publish-python-package/action.yml index 04c4590d7..7fb4eb37c 100644 --- a/.github/test-actions/test-publish-python-package/action.yml +++ b/.github/test-actions/test-publish-python-package/action.yml @@ -2,7 +2,7 @@ name: Test publish-python-package inputs: index-secret: - description: The password/token for publishing to the index + description: The password/token for publishing to the index. runs: using: composite diff --git a/actions/publish-python-package/README.md b/actions/publish-python-package/README.md index 96220c4f1..0c438e7d0 100644 --- a/actions/publish-python-package/README.md +++ b/actions/publish-python-package/README.md @@ -2,7 +2,7 @@ Publishes a **Python** package using [PDM](https://pdm-project.org). -## 🃏Example +## 🃏 Example ```yaml steps: @@ -16,7 +16,7 @@ steps: **Please, note**: this action is designed for _publication_ only - not for _verification_: you may want to use [verify-python-package](../verify-python-package/README.md) for that. -## 💡How it works +## 💡 How it works 1. Run [enforce-branch-version](../enforce-branch-version/README.md), forwarding the `enforce-branch-version` input to its `mode` input. @@ -26,7 +26,7 @@ steps: 1. Run `pdm publish`, passing the `index-` inputs as environment variables; if `dry-run` is enabled, just perform a `pdm build`, skipping actual deployment. -## ☑️Requirements +## ☑️ Requirements - `pipx` is mandatory when PDM has to be installed. @@ -34,7 +34,7 @@ steps: - Before the first publication, running with `dry-run` set to **true** is recommended. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :-----------------------------------------------: | :-----------: | @@ -47,7 +47,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing `pyproject.toml` | **.** | -## 🌐Further references +## 🌐 Further references - [publish-github-pages](../publish-github-pages/README.md) diff --git a/actions/publish-python-package/action.yml b/actions/publish-python-package/action.yml index 5290bc849..d9363f88a 100644 --- a/actions/publish-python-package/action.yml +++ b/actions/publish-python-package/action.yml @@ -27,33 +27,13 @@ inputs: default: inject project-directory: - description: The directory containing pyproject.toml. + description: The directory containing `pyproject.toml`. default: . runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.website-directory }}" ]] - then - echo "❌Missing action input: 'website-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Enforce branch version uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 @@ -61,13 +41,16 @@ runs: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - name: Ensure pdm - shell: bash + - name: Setup Python context + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - source "${{ github.action_path }}/../../core/bash/pdm.sh" + use aurora-github/ci-cd/input + use aurora-github/python/context - ensurePdm + var pdm-version = (input:string &optional pdm-version '${{ inputs.pdm-version }}') + + context:setup &pdm-version=$pdm-version - name: Publish the GitHub Pages website uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 @@ -78,22 +61,23 @@ runs: enforce-branch-version: ${{ inputs.enforce-branch-version }} - name: Publish the package - shell: bash + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - echo "💭dry-run is enabled: just building the 🐍Python project..." - pdm build - else - echo "📤Publishing the 🐍Python project..." - pdm publish $dryRunArg - fi + use aurora-github/ci-cd/input + use aurora-github/python/project + + var dry-run = (input:bool dry-run '${{ inputs.dry-run }}') + + project:publish $dry-run env: PDM_PUBLISH_REPO: ${{ inputs.index-url }} PDM_PUBLISH_USERNAME: ${{ inputs.index-user }} PDM_PUBLISH_PASSWORD: ${{ inputs.index-secret }} - name: Print confirmation message - shell: bash - run: echo "✅The 🐍Python project was published successfully!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🐍 Python publication action successful! From f49eae1d52bf64c4ed9aac9d74af1d3c3178dc7f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 25 Apr 2025 20:52:43 +0200 Subject: [PATCH 34/63] Refactor `verify-jvm-project` --- .../test-verify-jvm-project/action.yml | 27 ++-- actions/verify-jvm-project/README.md | 10 +- actions/verify-jvm-project/action.yml | 117 ++++-------------- 3 files changed, 41 insertions(+), 113 deletions(-) diff --git a/.github/test-actions/test-verify-jvm-project/action.yml b/.github/test-actions/test-verify-jvm-project/action.yml index b7bddfebf..f6a9ce3c3 100644 --- a/.github/test-actions/test-verify-jvm-project/action.yml +++ b/.github/test-actions/test-verify-jvm-project/action.yml @@ -4,23 +4,24 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Displaying JVM versions..." + run: echo "🎭 Displaying JVM versions..." - shell: bash run: | - echo "☕Java version: $(java --version | head -n 1)" + echo "☕ Java version: $(java --version | head -n 1)" - echo "🪶Maven version: $(mvn --version | head -n 1)" + echo "🪶 Maven version: $(mvn --version | head -n 1)" - echo "🐘Gradle version: $(gradle --version | grep -P '^Gradle \d+\.\d+(\.\d+)?$' | head -n 1 | cut -d ' ' -f 2)" + echo "🐘 Gradle version: $(gradle --version | grep -P '^Gradle \d+\.\d+(\.\d+)?$' | head -n 1 | cut -d ' ' -f 2)" - uses: ./actions/detect-branch-version id: version-detector - shell: bash - run: echo "🎭Build and verify the Maven project..." + run: echo "🎭 Build and verify the Maven project..." - - uses: ./actions/verify-jvm-project + - name: Build and verify the Maven project + uses: ./actions/verify-jvm-project with: project-directory: tests/maven-project @@ -32,18 +33,18 @@ runs: programOutput="$(java -jar target/maven-project-$version.jar)" - echo "🔎Maven program output: '$programOutput'" + echo "🔎 Maven program output: '$programOutput'" if [[ "$programOutput" == "Hello, world - from Kotlin and Maven! 🥳" ]] then - echo "🪶Maven project verified and run successfully!" + echo "🪶 Maven project verified and run successfully!" else - echo "❌Unexpected program output!" >&2 + echo "❌ Unexpected program output!" >&2 exit 1 fi - shell: bash - run: echo "🎭Build and verify the Gradle project..." + run: echo "🎭 Build and verify the Gradle project..." - name: Build and verify the Gradle project uses: ./actions/verify-jvm-project @@ -58,12 +59,12 @@ runs: programOutput="$(java -jar build/libs/gradle-project-$version.jar)" - echo "🔎Gradle program output: '$programOutput'" + echo "🔎 Gradle program output: '$programOutput'" if [[ "$programOutput" == "Hello, world - from Kotlin and Gradle! 🥳" ]] then - echo "🐘Gradle project verified and run successfully!" + echo "🐘 Gradle project verified and run successfully!" else - echo "❌Unexpected program output!" >&2 + echo "❌ Unexpected program output!" >&2 exit 1 fi diff --git a/actions/verify-jvm-project/README.md b/actions/verify-jvm-project/README.md index 99d73d3d4..f4ae8f846 100644 --- a/actions/verify-jvm-project/README.md +++ b/actions/verify-jvm-project/README.md @@ -2,7 +2,7 @@ Verifies the source files of a project for the **Java Virtual Machine** - using **Maven** or **Gradle**. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -11,7 +11,7 @@ steps: - uses: giancosta86/aurora-github/actions/verify-jvm-project@v10 ``` -## 💡How it works +## 💡 How it works 1. Run [check-project-license](../check-project-license/README.md) to verify the **LICENSE** file. @@ -37,11 +37,11 @@ steps: 1. Find [critical TODOs](../find-critical-todos/README.md) in the source code - which crash the workflow by default. -## ☑️Requirements +## ☑️ Requirements - The `mvn` or `gradle` command must be available, depending on the descriptor within the project directory - which also implies that a suitable **Java** environment is installed; by passing `java-version` and `tool-version`, you can enforce specific required versions instead of the default ones provided by the selected GitHub Actions runner. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :-----------------------: | :---------------------: | :---------------------------------------------: | :-----------------------: | @@ -53,7 +53,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing the project descriptor | **.** | -## 🌐Further references +## 🌐 Further references - [check-project-license](../check-project-license/README.md) diff --git a/actions/verify-jvm-project/action.yml b/actions/verify-jvm-project/action.yml index ddd073ac2..e29f4f270 100644 --- a/actions/verify-jvm-project/action.yml +++ b/actions/verify-jvm-project/action.yml @@ -18,7 +18,7 @@ inputs: source-file-regex: description: PCRE pattern describing the source files. - default: '^\.\/src\/.+\.(java|kt|scala|groovy)$' + default: '^src\/.+\.(java|kt|scala|groovy)$' enforce-branch-version: description: How the branch version should be enforced. @@ -31,116 +31,40 @@ inputs: runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.quiet-tool }}" ]] - then - echo "❌Missing action input: 'quiet-tool'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.crash-on-critical-todos }}" ]] - then - echo "❌Missing action input: 'crash-on-critical-todos'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.source-file-regex }}" ]] - then - echo "❌Missing action input: 'source-file-regex'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Check the project license uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_PROJECT_DIRECTORY=${{ inputs.project-directory }}" >> $GITHUB_ENV - - - name: Detect the build tool - shell: python - run: | - from core.jvm import get_jvm_build_tool, Inputs - - inputs = Inputs.from_env() - - get_jvm_build_tool(inputs).write_to_github_env() - - name: Enforce branch version uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - name: Install Java - if: inputs.java-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: java - version: ${{ inputs.java-version }} - - - name: Install Maven - if: env.buildTool == 'mvn' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: maven - version: ${{ inputs.tool-version }} - - - name: Install Gradle - if: env.buildTool == 'gradle' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: gradle - version: ${{ inputs.tool-version }} - - - name: Run Maven - if: env.buildTool == 'mvn' - shell: bash + - name: Setup JVM context + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.quiet-tool }}" == "true" ]] - then - quietToolArg="-q" - else - quietToolArg="" - fi + use aurora-github/ci-cd/input + use aurora-github/ci-cd/env + use aurora-github/jvm/context - echo "🪶Running Maven to verify the project..." + env:map (context:setup ^ + &java-version=(input:string &optional java-version '${{ inputs.java-version }}') ^ + &tool-version=(input:string &optional tool-version '${{ inputs.tool-version }}') ^ + ) - mvn -B $quietToolArg verify - - echo "✅Verification successful!" - - - name: Run Gradle - if: env.buildTool == 'gradle' - shell: bash + - name: Verify project + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.quiet-tool }}" == "true" ]] - then - quietToolArg="-q" - else - quietToolArg="" - fi - - echo "🐘Running Gradle to verify the project..." + use aurora-github/ci-cd/input + use aurora-github/jvm/project/verification - gradle --no-daemon --no-scan $quietToolArg build + var quiet-tool = (input:bool quiet-tool '${{ inputs.quiet-tool }}') - echo "✅Verification successful!" + verification:verify &quiet-tool=$quiet-tool (get-env buildTool) - name: Check for critical TODOs uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 @@ -151,5 +75,8 @@ runs: root-directory: ${{ inputs.project-directory }} - name: Print confirmation message - shell: bash - run: echo "✅JVM project verified!" + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅☕ JVM project verified! From 5f59351097f4647317f6ba5b33be07c6698bb400 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 26 Apr 2025 15:17:42 +0200 Subject: [PATCH 35/63] Refactor `publish-jvm-project` --- .../test-publish-jvm-project/action.yml | 6 +- actions/publish-jvm-project/README.md | 10 +- actions/publish-jvm-project/action.yml | 191 +++--------------- 3 files changed, 34 insertions(+), 173 deletions(-) diff --git a/.github/test-actions/test-publish-jvm-project/action.yml b/.github/test-actions/test-publish-jvm-project/action.yml index bdc2a5280..e34bc198f 100644 --- a/.github/test-actions/test-publish-jvm-project/action.yml +++ b/.github/test-actions/test-publish-jvm-project/action.yml @@ -2,13 +2,13 @@ name: Test publish-jvm-project inputs: jvm-token: - description: The token used when publishing to a Maven repo + description: The token used when publishing to a Maven repo. runs: using: composite steps: - shell: bash - run: echo "🎭Testing publication via Maven..." + run: echo "🎭 Testing publication via Maven..." - uses: ./actions/publish-jvm-project with: @@ -18,7 +18,7 @@ runs: auth-token: ${{ inputs.jvm-token }} - shell: bash - run: echo "🎭Testing publication via Gradle..." + run: echo "🎭 Testing publication via Gradle..." - uses: ./actions/publish-jvm-project with: diff --git a/actions/publish-jvm-project/README.md b/actions/publish-jvm-project/README.md index 0f09ad720..5b366828c 100644 --- a/actions/publish-jvm-project/README.md +++ b/actions/publish-jvm-project/README.md @@ -2,7 +2,7 @@ Publishes a project for the **Java Virtual Machine** - using **Maven** or **Gradle**. -## 🃏Example +## 🃏 Example ```yaml steps: @@ -14,7 +14,7 @@ steps: auth-token: ${{ secrets.SERVER_TOKEN }} ``` -## 💡How it works +## 💡 How it works 1. Run [enforce-branch-version](../enforce-branch-version/README.md), forwarding the `enforce-branch-version` input to its `mode` input. @@ -52,7 +52,7 @@ steps: - **JVM_AUTH_TOKEN** - with the value of `auth-token` -## ☑️Requirements +## ☑️ Requirements - The `mvn` or `gradle` command must be available, depending on the descriptor within the project directory - which also implies that a suitable **Java** environment is installed; by passing `java-version` and `tool-version`, you can enforce specific required versions instead of the default ones provided by the selected GitHub Actions runner. @@ -99,7 +99,7 @@ steps: - Before the first publication, running with `dry-run` set to **true** is recommended. -## 📥Inputs +## 📥 Inputs | Name | Type | Description | Default value | | :----------------------: | :---------------------: | :-----------------------------------------------: | :-----------: | @@ -113,7 +113,7 @@ steps: | `enforce-branch-version` | `inject`,`check`,`skip` | How the branch version should be enforced | **inject** | | `project-directory` | **string** | The directory containing the project descriptor | **.** | -## 🌐Further references +## 🌐 Further references - [publish-github-pages](../publish-github-pages/README.md) diff --git a/actions/publish-jvm-project/action.yml b/actions/publish-jvm-project/action.yml index 8efff426c..54be78aa3 100644 --- a/actions/publish-jvm-project/action.yml +++ b/actions/publish-jvm-project/action.yml @@ -37,27 +37,7 @@ inputs: runs: using: composite steps: - - name: Validate inputs - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.website-directory }}" ]] - then - echo "❌Missing action input: 'website-directory'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.enforce-branch-version }}" ]] - then - echo "❌Missing action input: 'enforce-branch-version'!" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - name: Enforce branch version uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 @@ -65,109 +45,26 @@ runs: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - - name: Setup Python context - shell: bash - run: | - pythonPath="${{ github.action_path }}/../../core/python" - echo "PYTHONPATH=$pythonPath" >> $GITHUB_ENV - - echo "INPUT_PROJECT_DIRECTORY=${{ inputs.project-directory }}" >> $GITHUB_ENV - - - name: Detect the build tool - shell: python + - name: Setup JVM context + shell: elvish {0} + working-directory: ${{ inputs.project-directory }} run: | - from core.jvm import get_jvm_build_tool, Inputs - - inputs = Inputs.from_env() + use aurora-github/ci-cd/input + use aurora-github/ci-cd/env + use aurora-github/jvm/context - get_jvm_build_tool(inputs).write_to_github_env() - - - name: Install Java - if: inputs.java-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: java - version: ${{ inputs.java-version }} - - - name: Install Maven - if: env.buildTool == 'mvn' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: maven - version: ${{ inputs.tool-version }} - - - name: Install Gradle - if: env.buildTool == 'gradle' && inputs.tool-version != '' - uses: giancosta86/aurora-github/actions/install-via-sdkman@v10.3.0 - with: - candidate: gradle - version: ${{ inputs.tool-version }} + env:map (context:setup ^ + &java-version=(input:string &optional java-version '${{ inputs.java-version }}') ^ + &tool-version=(input:string &optional tool-version '${{ inputs.tool-version }}') ^ + ) - - name: Prepare Maven settings - if: env.buildTool == 'mvn' - shell: bash + - name: Prepare settings for publication + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - settingsDirectory="$HOME/.m2" - settingsBasename="settings.xml" - - main() { - echo "🪶Setting up Maven settings..." - - mkdir -p "$settingsDirectory" - - if [[ -f "$settingsBasename" ]] - then - echo "✅$settingsBasename found in the project directory! Now copying it to '$settingsDirectory'..." - cp "$settingsBasename" "$settingsDirectory" - else - echo "🌟Providing a default '$settingsBasename' file for 🪶Maven..." - copyDefaultSettings - checkServerInPom - fi - - echo "✅Maven settings file ready!" - } - - copyDefaultSettings() { - cp "${{ github.action_path }}/$settingsBasename" "$settingsDirectory" - - echo "The content of the generated 🪶$settingsBasename is:" - cat "$settingsDirectory/$settingsBasename" - echo "🪶🪶🪶" - } - - checkServerInPom() { - local serverName="target-server" - - if grep -q "$serverName" $descriptor - then - echo "✅Server '$serverName' found in $descriptor!" - else - echo "💭Server '$serverName' not used in $descriptor..." - fi - } - - main - - - name: Prepare Gradle settings - if: env.buildTool == 'gradle' - shell: bash - working-directory: ${{ inputs.project-directory }} - run: | - echo "🐘Checking the (optional) use of environment variables in $descriptor" - - for envVar in "JVM_AUTH_USER" "JVM_AUTH_TOKEN" - do - if grep -q "$envVar" $descriptor - then - echo "✅'$envVar' referenced by the project descriptor!" - else - echo "💭'$envVar' environment variable not referenced in $descriptor..." - fi - done + use aurora-github/jvm/settings - echo "🐘🐘🐘" + settings:prepare-for-publication (get-env buildTool) - name: Publish the GitHub Pages website uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 @@ -177,61 +74,25 @@ runs: dry-run: ${{ inputs.dry-run }} enforce-branch-version: ${{ inputs.enforce-branch-version }} - - name: Publish via Maven - if: env.buildTool == 'mvn' - shell: bash + - name: Publish project + shell: elvish {0} working-directory: ${{ inputs.project-directory }} run: | - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - dryRunDirectory="target/dry-run" - echo "📁dry-run mode enabled - publishing to '$dryRunDirectory' local directory instead..." + use aurora-github/ci-cd/input + use aurora-github/jvm/project/publication - dryRunArg="-DaltDeploymentRepository=target-server::default::file:$dryRunDirectory" - else - dryRunArg="" - fi + var quiet-tool = (input:bool quiet-tool '${{ inputs.quiet-tool }}') - if [[ "${{ inputs.quiet-tool }}" == "true" ]] - then - quietToolArg="-q" - else - quietToolArg="" - fi + var dry-run = (input:bool dry-run '${{ inputs.dry-run }}') - echo "🪶Running Maven to publish the project..." - - mvn -B $dryRunArg $quietToolArg deploy - - echo "✅Publication via Maven successful!" + publication:publish &quiet-tool=$quiet-tool &dry-run=$dry-run (get-env buildTool) env: JVM_AUTH_USER: ${{ inputs.auth-user }} JVM_AUTH_TOKEN: ${{ inputs.auth-token }} - - name: Publish via Gradle - if: env.buildTool == 'gradle' - shell: bash - working-directory: ${{ inputs.project-directory }} + - name: Print confirmation message + shell: elvish {0} run: | - if [[ "${{ inputs.dry-run }}" == "true" ]] - then - dryRunArg="--dry-run" - else - dryRunArg="" - fi + use github.com/giancosta86/aurora-elvish/console - if [[ "${{ inputs.quiet-tool }}" == "true" ]] - then - quietToolArg="-q" - else - quietToolArg="" - fi - - echo "🐘Running Gradle to publish the project..." - - gradle --no-daemon --no-scan $dryRunArg $quietToolArg publish - - echo "✅Publication via Gradle successful!" - env: - JVM_AUTH_USER: ${{ inputs.auth-user }} - JVM_AUTH_TOKEN: ${{ inputs.auth-token }} + console:echo ✅☕ JVM publication action successful! From 5dd3d0381199fc1d18c44a080810f469a423e23e Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 3 May 2025 04:38:10 +0200 Subject: [PATCH 36/63] Refactor `tag-and-release`: * add a new value to the `draft-release` input - `on-major` - which is now the default (closes #29) * the `git-strategy` input now defaults to `squash` * internally use `run-custom-script` to support a wider range of release note preprocessing scripts --- .github/process-release-notes.elv | 22 + .github/process-release-notes.sh | 8 - .../test-tag-and-release/action.yml | 30 +- actions/tag-and-release/README.md | 60 ++- actions/tag-and-release/action.yml | 402 +----------------- 5 files changed, 87 insertions(+), 435 deletions(-) create mode 100644 .github/process-release-notes.elv delete mode 100644 .github/process-release-notes.sh diff --git a/.github/process-release-notes.elv b/.github/process-release-notes.elv new file mode 100644 index 000000000..0473e6e95 --- /dev/null +++ b/.github/process-release-notes.elv @@ -0,0 +1,22 @@ +use str +use github.com/giancosta86/aurora-elvish/edit + +var notes-file = $args[0] + +{ + tmp pwd = actions + + edit:file $notes-file { |content| + var actual-content = $content + + put * | each { |action-name| + var tick-quoted-name = '`'$action-name'`' + + var markdown-link = '['$action-name'](actions/'$action-name'/README.md)' + + set actual-content = (str:replace $tick-quoted-name $markdown-link $actual-content) + } + + put $actual-content + } +} \ No newline at end of file diff --git a/.github/process-release-notes.sh b/.github/process-release-notes.sh deleted file mode 100644 index b4fef5f99..000000000 --- a/.github/process-release-notes.sh +++ /dev/null @@ -1,8 +0,0 @@ -notesFile="$1" - -for actionFolder in actions/* -do - action="$(basename "$actionFolder")" - - sed -i "s/\`$action\`/[$action](actions\/$action\/README.md)/" "$notesFile" -done \ No newline at end of file diff --git a/.github/test-actions/test-tag-and-release/action.yml b/.github/test-actions/test-tag-and-release/action.yml index 688afc640..a1756cec2 100644 --- a/.github/test-actions/test-tag-and-release/action.yml +++ b/.github/test-actions/test-tag-and-release/action.yml @@ -4,7 +4,7 @@ runs: using: composite steps: - shell: bash - run: echo "🎭Testing the creation of a 'draft' release in dry-run, without notes processor..." + run: echo "🎭 Testing the creation of a 'draft' release in dry-run, without notes processor..." - name: Draft a release id: draft-release-without-major @@ -19,18 +19,18 @@ runs: shell: bash run: | majorTag="${{ steps.draft-release-without-major.outputs.major-tag }}" - echo "🔎Major tag: '$majorTag'" + echo "🔎 Major tag: '$majorTag'" if [[ -z "$majorTag" ]] then - echo "✅The major tag is empty, as expected, because it was not requested!" + echo "✅ The major tag is empty, as expected, because it was not requested!" else - echo "❌The major tag should be empty when not requested!" >&2 + echo "❌ The major tag should be empty when not requested!" >&2 exit 1 fi - shell: bash - run: echo "🎭Testing the creation of a release in dry-run, with notes processor..." + run: echo "🎭 Testing the creation of a release in dry-run, with notes processor..." - name: Publish a release id: publish-release @@ -39,7 +39,7 @@ runs: dry-run: true draft-release: false set-major-tag: true - notes-file-processor: .github/process-release-notes.sh + notes-file-processor: .github/process-release-notes git-strategy: rebase - uses: ./actions/detect-branch-version @@ -49,32 +49,32 @@ runs: shell: bash run: | branchVersion="${{ steps.version-detector.outputs.version }}" - echo "🔎Branch version: '$branchVersion'" + echo "🔎 Branch version: '$branchVersion'" releaseTag="${{ steps.publish-release.outputs.release-tag }}" - echo "🔎Release tag: '$releaseTag'" + echo "🔎 Release tag: '$releaseTag'" if [[ "$releaseTag" == "v$branchVersion" ]] then - echo "✅The release tag derives from the current branch version!" + echo "✅ The release tag derives from the current branch version!" else - echo "❌The release tag does not derive from the current branch version!" >&2 + echo "❌ The release tag does not derive from the current branch version!" >&2 exit 1 fi - - name: Verify the major tag is correct when it was requested + - name: Verify the major tag is correct, as it was requested shell: bash run: | majorVersion="${{ steps.version-detector.outputs.major }}" - echo "🔎Major version: '$majorVersion'" + echo "🔎 Major version: '$majorVersion'" majorTag="${{ steps.publish-release.outputs.major-tag }}" - echo "🔎Major tag: '$majorTag'" + echo "🔎 Major tag: '$majorTag'" if [[ "$majorTag" == "v$majorVersion" ]] then - echo "✅The major tag derives from the current major version!" + echo "✅ The major tag derives from the current major version!" else - echo "❌The major tag does not derive from the current major version!" >&2 + echo "❌ The major tag does not derive from the current major version!" >&2 exit 1 fi diff --git a/actions/tag-and-release/README.md b/actions/tag-and-release/README.md index c1f35787d..39dfb4d16 100644 --- a/actions/tag-and-release/README.md +++ b/actions/tag-and-release/README.md @@ -2,7 +2,7 @@ Merges a pull request, creates a **Git** tag and publishes a **GitHub** release, from a Git branch named according to [semantic versioning](https://semver.org/). -## 🃏Example +## 🃏 Example ```yaml steps: @@ -24,7 +24,21 @@ The only exception is when `dry-run` is set to **true**. More generally, this action should be _the very last step_ in a manually-triggered workflow performing _artifact publication_ - so that the pull request gets closed and the related resources released once all the other steps are successful. -## ☑️Requirements +## 💡 How it works + +1. If the pull request associated with the current branch is open (as it should), merge it - using the selected `git-strategy` for merging/rebasing the code and deleting the branch + +1. Create a new tag - for example, `vX.Y.Z` - containing the semantic version inferred from the branch name. + +1. Generate the release notes - based on the Git commit list + +1. If `notes-file-processor` is not an empty string, it will be interpreted as the `script-file` input of [run-shell-script](../run-shell-script/README.md) + +1. Create or draft a GitHub release. + +1. Optionally, _create or move_ the tag of the major version related to the current version - for example, `vX`. + +## ☑️ Requirements - Unless the `dry-run` input is set to **true**, this action can only be called: @@ -48,44 +62,28 @@ More generally, this action should be _the very last step_ in a manually-trigger - The requirements discussed for [detect-branch-version](../detect-branch-version/README.md) also apply. -## 💡How it works +## 📥 Inputs -1. If the pull request associated with the current branch is open (as it should), merge it - using the selected `git-strategy` for merging/rebasing the code and deleting the branch +| Name | Type | Description | Default value | +| :--------------------: | :-------------------------: | :-------------------------------------------------------: | :-----------: | +| `git-strategy` | `merge`,`rebase`,`squash` | How to apply the pull request to the Git repository | **squash** | +| `draft-release` | `true`, `false`, `on-major` | Draft the release - do not publish it | **on-major** | +| `notes-file-processor` | **string** | Shell script editing the generated release notes | | +| `set-major-tag` | **boolean** | Create/move the `vX` tag to this commit (X=major version) | **false** | +| `dry-run` | **boolean** | Run the action without performing commands | **false** | -1. Create a new tag - for example, `vX.Y.Z` - containing the semantic version inferred from the branch name. +- The `draft-release` input, when set to **on-major**, will become **true** only if the semantic version detected from the current branch has the form `X.0.0` - otherwise, it will fallback to **false**. -1. Generate the release notes - based on the Git commit list - -1. If `notes-file-processor` is not an empty string, it will be interpreted as the filename - relative to `project-directory` - of a **Bash** script that: - - - must take in input (via `$1`) the path of the temporary file containing the generated release notes - - - can arbitrarily alter the content of such file - and even call other interpreters - - - does not need the `#!` prelude, nor the _executable flag_ - -1. Create or draft a GitHub release. - -1. Optionally, _create or move_ the tag of the major version related to the current version - for example, `vX`. - -## 📥Inputs - -| Name | Type | Description | Default value | -| :--------------------: | :-----------------------: | :-------------------------------------------------------: | :-----------: | -| `git-strategy` | `merge`,`rebase`,`squash` | How to apply the pull request to the Git repository | **rebase** | -| `draft-release` | **boolean** | Draft the release - do not publish it | **false** | -| `notes-file-processor` | **string** | Bash script editing the generated release notes | | -| `set-major-tag` | **boolean** | Create/move the `vX` tag to this commit (X=major version) | **false** | -| `dry-run` | **boolean** | Run the action without performing commands | **false** | - -## 📤Outputs +## 📤 Outputs | Name | Type | Description | Example | | :-----------: | :--------: | :--------------------------------------: | :--------: | | `release-tag` | **string** | The Git tag associated with the release | **v7.4.9** | | `major-tag` | **string** | The Git tag of the major version, if set | **v7** | -## 🌐Further references +## 🌐 Further references + +- [run-shell-script](../run-shell-script/README.md) - [semver](https://semver.org/) diff --git a/actions/tag-and-release/action.yml b/actions/tag-and-release/action.yml index e5d8082cb..cfe2c9266 100644 --- a/actions/tag-and-release/action.yml +++ b/actions/tag-and-release/action.yml @@ -3,14 +3,14 @@ description: Merges a pull request, creates a Git tag and publishes a GitHub rel inputs: draft-release: - description: Draft the release - do not publish it. - default: false + description: Draft the release - do not publish it. Can be `true`, `false` or `on-major`. + default: on-major notes-file-processor: - description: Bash script editing the generated release notes. + description: Shell script editing the generated release notes. set-major-tag: - description: Create/move the 'vX' tag to this commit (X=major version). + description: Create/move the `vX` tag to this commit (X=major version). default: false dry-run: @@ -19,401 +19,41 @@ inputs: git-strategy: description: How to apply the pull request to the Git repository. Can be `merge`, `rebase` or `squash`. - default: rebase + default: squash outputs: release-tag: description: The Git tag associated with the release. - value: ${{ steps.create-release.outputs.release-tag }} + value: ${{ steps.action-body.outputs.tag }} major-tag: description: The Git tag of the major version, if set. - value: ${{ steps.set-major-tag.outputs.major-tag }} + value: ${{ steps.action-body.outputs.major-tag }} runs: using: composite steps: - - name: Validate inputs - shell: bash - run: | - if [[ -z "${{ inputs.draft-release }}" ]] - then - echo "❌Missing action input: 'draft-release'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.set-major-tag }}" ]] - then - echo "❌Missing action input: 'set-major-tag'!" >&2 - exit 1 - fi - - if [[ -z "${{ inputs.dry-run }}" ]] - then - echo "❌Missing action input: 'dry-run'!" >&2 - exit 1 - fi - - case "${{ inputs.git-strategy }}" in - merge | rebase | squash) - ;; - - *) - echo "❌Invalid value for input: 'git-strategy': '${{ inputs.git-strategy }}'!" >&2 - exit 1 - esac - - - name: Display inputs - shell: bash - run: | - echo "📥dry-run: ${{ inputs.dry-run }}" - echo "📥draft-release: ${{ inputs.draft-release }}" - echo "📥set-major-tag: ${{ inputs.set-major-tag }}" - echo "📥notes-file-processor: ${{ inputs.notes-file-processor }}" - echo "📥git-strategy: ${{ inputs.git-strategy }}" - - - name: Detect branch and version - id: detect-branch-version - uses: giancosta86/aurora-github/actions/detect-branch-version@v10.3.0 - - - name: Verify versions - shell: bash - run: | - version="${{ steps.detect-branch-version.outputs.version }}" - - if [[ -n "$version" ]] - then - echo "🔎Detected version: '$version'" - else - echo "❌The current version could not be detected from the Git branch ('$branch')!" >&2 - exit 1 - fi - - setMajorTag="${{ inputs.set-major-tag }}" - - if [[ "$setMajorTag" == "true" ]] - then - major="${{ steps.detect-branch-version.outputs.major }}" - - if [[ -n "$major" ]] - then - echo "🔎Detected major version: '$major'" - else - echo "❌The major version could not be detected!" >&2 - exit 1 - fi - fi - - - name: Detect if a pull request was the trigger - shell: bash - run: | - echo "🧭GITHUB_REF IS: '$GITHUB_REF'" - - if [[ "$GITHUB_REF" =~ ^refs/pull/ ]] - then - inPullRequestWorkflow=true - - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - echo "❌This action can be run from a workflow triggered by a pull-request only when dry-run is enabled" - exit 1 - fi - else - inPullRequestWorkflow=false - fi - - echo "inPullRequestWorkflow=$inPullRequestWorkflow" >> $GITHUB_ENV - - - name: Retrieve pull request information - shell: bash - run: | - branch="${{ steps.detect-branch-version.outputs.branch }}" - - echo "😺Retrieving Git pull request info for branch: '$branch'..." - - pullRequestJson="$(gh pr view $branch --json title,number,baseRefOid,headRefOid --jq '{title: .title, number: .number, baseSha: .baseRefOid, headSha: .headRefOid}')" - - echo "✅Pull request JSON data retrieved!" - - title=$(echo "$pullRequestJson" | jq -r .title) - number=$(echo "$pullRequestJson" | jq -r .number) - baseSha=$(echo "$pullRequestJson" | jq -r .baseSha) - headSha=$(echo "$pullRequestJson" | jq -r .headSha) - - echo "pullRequestTitle=$title" >> $GITHUB_ENV - echo "pullRequestNumber=$number" >> $GITHUB_ENV - echo "pullRequestBaseSha=$baseSha" >> $GITHUB_ENV - echo "pullRequestHeadSha=$headSha" >> $GITHUB_ENV - env: - GH_TOKEN: ${{ github.token }} - - - name: Print pull request information - shell: bash - run: | - echo "🧭In pull request workflow? $inPullRequestWorkflow" - echo "🔁Pull request title: '$pullRequestTitle'" - echo "🔁Pull request number: '$pullRequestNumber'" - echo "🔁Pull request base SHA: '$pullRequestBaseSha'" - echo "🔁Pull request head SHA: '$pullRequestHeadSha'" - - - name: Reset any Git local change - shell: bash - run: | - echo "⏱️Discarding local changes to the Git repository..." - git reset --hard HEAD - echo "✅Git repository successfully reset" - - - name: Fetch Git log - shell: bash - run: | - main() { - if [[ "$inPullRequestWorkflow" == "true" ]] - then - echo "📥Fetching Git log within a pull request workflow..." - fetchLogWithinPullRequestWorkflow - else - echo "📥Fetching Git log not within a pull request workflow..." - fetchLogNotWithinPullRequestWorkflow - fi - - echo "✅Git log ready!" - } - - function fetchLogWithinPullRequestWorkflow() { - git fetch origin "$pullRequestBaseSha" "$pullRequestHeadSha" - } - - function fetchLogNotWithinPullRequestWorkflow() { - fetchGitSha "$pullRequestHeadSha" - - fetchGitSha "$pullRequestBaseSha" - } - - function fetchGitSha() { - local requiredSha="$1" - - local branch="${{ steps.detect-branch-version.outputs.branch }}" - local depthDelta=25 - - echo "🧭Ensuring Git SHA '$requiredSha' is available..." - - while ! git cat-file commit "$requiredSha" > /dev/null 2>&1 - do - echo "📥Fetching $depthDelta more commits..." - git fetch --deepen="$depthDelta" origin "$branch" - depthDelta=$((depthDelta * 2)) - done - - echo "✅Git SHA '$requiredSha' ready!" - } - - main - - - name: Fetch Git tags - shell: bash - run: | - echo "📥Retrieving Git tags..." - gitErrorLog="$(mktemp)" - if git fetch --tags > /dev/null 2> "$gitErrorLog" - then - echo "✅Git tags retrieved!" - else - echo "❌Cannot retrieve the Git tags, because of these errors:" >&2 - cat "$gitErrorLog" >&2 - echo "❌❌❌" >&2 - exit 1 - fi + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 - - name: Merge the pull request - shell: bash + - id: action-body + shell: elvish {0} run: | - branch="${{ steps.detect-branch-version.outputs.branch }}" + use aurora-github/ci-cd/input + use aurora-github/ci-cd/output + use aurora-github/tag-and-release - echo "🔀Now merging the PR for branch '$branch', via the '${{ inputs.git-strategy }}' Git strategy..." + var inputs = [ + &draft-release=(input:enum draft-release '${{ inputs.draft-release }}' [true false on-major]) - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - gitStrategyArg="--${{ inputs.git-strategy }}" - gh pr merge "$branch" $gitStrategyArg --delete-branch - else - echo "💭Just simulating pull request merging, in dry-run mode..." - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Create the Git tag - id: create-git-tag - shell: bash - run: | - version="${{ steps.detect-branch-version.outputs.version }}" - - tag="v${version}" - echo "📌Creating and pushing Git tag '$tag'..." - - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - git tag "$tag" - git push origin "$tag" - echo "📌Tag created and pushed!" - else - echo "💭Just simulating Git tag creation, in dry-run mode..." - fi - - echo "tag=$tag" >> $GITHUB_OUTPUT - - - name: Publish or draft release - id: create-release - shell: bash - run: | - tag="${{ steps.create-git-tag.outputs.tag }}" - echo "📌Release tag: '$tag'..." - - baseSha="$pullRequestBaseSha" - echo "🏰Base SHA: '$baseSha'" - - headSha="$pullRequestHeadSha" - echo "👤Head SHA: '$headSha'" - - function createRelease() { - local repoBasename="$(basename "$GITHUB_REPOSITORY")" - echo "🧭Repository basename: '$repoBasename'" - - local version="${{ steps.detect-branch-version.outputs.version }}" - echo "🔎Version: '$version'" - - local releaseTitle="$repoBasename $version" - echo "🔎Release title: '$releaseTitle'" - - local draftOnly="${{ inputs.draft-release }}" - echo "🔎Draft only? $draftOnly" - - local releaseNotesFile="$(mktemp)" - generateReleaseNotes "$releaseNotesFile" - - local notesFileProcessor=${{ inputs.notes-file-processor }} - if [[ -n "$notesFileProcessor" ]] - then - echo "🖋Release notes file processor found: '$notesFileProcessor'" - - bash "$notesFileProcessor" "$releaseNotesFile" - - echo "🎀Processed release notes:" - cat "$releaseNotesFile" - echo "🎀🎀🎀" - else - echo "💭No release notes file processor..." - fi - - if [[ "$draftOnly" == "true" ]] - then - echo "📝Drafting release '$releaseTitle'..." - - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - gh release create "$tag" --title "$releaseTitle" --latest --notes-file "$releaseNotesFile" --draft - - echo "📝Release drafted!" - else - echo "💭Just simulating draft release creation, in dry-run mode..." - fi - else - echo "🌟Publishing release '$releaseTitle'..." - - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - gh release create "$tag" --title "$releaseTitle" --latest --notes-file "$releaseNotesFile" - - echo "🌟Release published!" - else - echo "💭Just simulating release creation, in dry-run mode..." - fi - fi - } + ¬es-file-processor=(input:string &optional notes-file-processor '${{ inputs.notes-file-processor }}') - function generateReleaseNotes() { - local outputFile="$1" + &set-major-tag=(input:bool set-major-tag '${{ inputs.set-major-tag }}') - writeCommitList "$outputFile" + &dry-run=(input:bool dry-run '${{ inputs.dry-run }}') - echo "---" >> "$outputFile" + &git-strategy=(input:enum git-strategy '${{ inputs.git-strategy }}' [merge rebase squash]) + ] - writePullRequestData "$outputFile" - - echo >> "$outputFile" - - writeChangeLog "$outputFile" - - echo "📝Release notes generated!" - cat "$outputFile" - echo "📝📝📝" - } - - function writeCommitList() { - local outputFile="$1" - - printGitLogsAsMarkdown "§+-+§" >> "$outputFile" - } - - function printGitLogsAsMarkdown() { - local marker="$1" - - git log --no-merges --reverse --pretty=format:"$marker* %B" "$baseSha".."$headSha" | sed " - s/^/ / - s/^ $marker// - " - } - - function writePullRequestData() { - local outputFile="$1" - - echo -e "**Pull request**: $pullRequestTitle (#$pullRequestNumber):" >> "$outputFile" - } - - function writeChangeLog() { - local outputFile="$1" - - local mostSpecificBaseTag="$(git tag --points-at "$baseSha" | awk '{ print length, $0 }' | sort -nr | cut -d' ' -f2- | head -n 1)" - - if [[ -n "$mostSpecificBaseTag" ]] - then - echo "📌Tag '$mostSpecificBaseTag' found for the '$baseSha' base SHA" - local baseReference="$mostSpecificBaseTag" - else - echo "💭No tags associated with the '$baseSha' base SHA - using it directly" - local baseReference="$baseSha" - fi - - echo "🧭Base reference: '$baseReference'" - echo "📌Release tag: '$tag'" - - echo "**Full changelog**: https://github.com/$GITHUB_REPOSITORY/compare/$baseReference..$tag" >> "$outputFile" - } - - createRelease - - echo "release-tag=$tag" >> $GITHUB_OUTPUT + output:map (tag-and-release:run-action $inputs | only-values) env: GH_TOKEN: ${{ github.token }} - - - name: Create or move major version tag - if: inputs.set-major-tag == 'true' - id: set-major-tag - shell: bash - run: | - major="${{ steps.detect-branch-version.outputs.major }}" - - majorTag="v${major}" - - echo "🪩Setting major version tag '$majorTag'..." - - if [[ "${{ inputs.dry-run }}" != "true" ]] - then - git tag -f "$majorTag" - git push origin "$majorTag" --force - - echo "🪩Major version tag set!" - else - echo "💭Just simulating major version tag creation, in dry-run mode..." - fi - - echo "major-tag=$majorTag" >> $GITHUB_OUTPUT From 344dc7eefe9155ad8751fcc286c9d4b5f83d2a97 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Mon, 26 May 2025 19:45:05 +0200 Subject: [PATCH 37/63] Introduce new action: `check-required-jobs` --- actions/check-required-jobs/README.md | 60 ++++++++++++++++++++++++++ actions/check-required-jobs/action.yml | 21 +++++++++ 2 files changed, 81 insertions(+) create mode 100644 actions/check-required-jobs/README.md create mode 100644 actions/check-required-jobs/action.yml diff --git a/actions/check-required-jobs/README.md b/actions/check-required-jobs/README.md new file mode 100644 index 000000000..b2c1e1084 --- /dev/null +++ b/actions/check-required-jobs/README.md @@ -0,0 +1,60 @@ +# check-required-jobs + +Verifies that all the jobs in the `needs:` directive of the current job have completed successfully. + +## 🃏 Example + +```yaml +steps: + - uses: giancosta86/aurora-github/actions/check-required-jobs@v11 + with: + needs-as-json: ${{ toJSON(needs) }} +``` + +## 💡 How it works + +1. Extract the list of required jobs from the `needs:` directive of the **job** containing this action. + +1. Wait for all the required jobs to provide an outcome (**success**, **failure**, **skipped**). + +1. Display, within a summary, the **outcome** of each required job. + +1. Ensure that every required job: + + 1. has actually run - **skipped** jobs trigger an error just like **failed** ones. + + 1. has completed successfully. + +## 💬 Remarks + +This action is especially effective in a **job** that: + +- is declared **at the end** of its workflow. + +- references **all** the previous jobs via the `needs:` directive. + +- is flagged (the containing job, _not_ the action) with `if: ${{ always() }}`, ensuring it will always run - even if the required jobs have a **failed** or **skipped** outcome. + +### Recommended strategy + +You might want to flag **all the other jobs** in the workflow with the `if: true` flag, which has no effect; however should you need to run only a selected number of jobs (for example, while developing a feature), you can: + +1. Replace `if: true` with `if: false` to skip inessential jobs. + +1. Replace `if: true` with `if: ${{ always() }}` for the jobs you want to run - so that _they will run even though their required jobs have been skipped_. + +1. Anyway, this action acts as a barrier preventing the CI/CD branch checks to pass - because **skipped jobs result in an overall failure**. + +## ☑️ Requirements + +The job containing this action should have a `need:` list mentioning previous jobs in the workflow. + +## 📥 Inputs + +| Name | Type | Description | Default value | +| :-------------: | :--: | :--------------------------------: | :-----------: | +| `needs-as-json` | | Always pass `${{ toJSON(needs) }}` | | + +## 🌐 Further references + +- [aurora-github](../../README.md) diff --git a/actions/check-required-jobs/action.yml b/actions/check-required-jobs/action.yml new file mode 100644 index 000000000..3feb7e102 --- /dev/null +++ b/actions/check-required-jobs/action.yml @@ -0,0 +1,21 @@ +name: Check required jobs +description: Verifies that all the jobs in the `needs:` directive of the current job have completed successfully. + +inputs: + needs-as-json: + description: Always pass the evaluation of `toJSON(needs)`. + +runs: + using: composite + steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 + + - name: Check the required jobs + shell: elvish {0} + run: | + use aurora-github/ci-cd/input + use aurora-github/ci-cd/required-jobs + + var needs-as-json = (input:string needs-as-json '${{ inputs.needs-as-json }}') + + required-jobs:check $needs-as-json From 7e98f45259af163540c37cf4664de41a4c5e8436 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 25 Jun 2025 11:19:38 +0200 Subject: [PATCH 38/63] Introduce new action `verify-elvish-package` --- .../test-verify-elvish-package/action.yml | 8 +++ actions/verify-elvish-package/README.md | 42 ++++++++++++++++ actions/verify-elvish-package/action.yml | 50 +++++++++++++++++++ tests/elvish-library/metadata.json | 9 ++++ tests/elvish-library/my-ops.elv | 3 ++ tests/elvish-library/my-ops.test.elv | 17 +++++++ 6 files changed, 129 insertions(+) create mode 100644 .github/test-actions/test-verify-elvish-package/action.yml create mode 100644 actions/verify-elvish-package/README.md create mode 100644 actions/verify-elvish-package/action.yml create mode 100644 tests/elvish-library/metadata.json create mode 100644 tests/elvish-library/my-ops.elv create mode 100644 tests/elvish-library/my-ops.test.elv diff --git a/.github/test-actions/test-verify-elvish-package/action.yml b/.github/test-actions/test-verify-elvish-package/action.yml new file mode 100644 index 000000000..38ca07f76 --- /dev/null +++ b/.github/test-actions/test-verify-elvish-package/action.yml @@ -0,0 +1,8 @@ +name: Test verify-elvish-package + +runs: + using: composite + steps: + - uses: ./actions/verify-elvish-package + with: + project-directory: tests/elvish-library diff --git a/actions/verify-elvish-package/README.md b/actions/verify-elvish-package/README.md new file mode 100644 index 000000000..ef9733217 --- /dev/null +++ b/actions/verify-elvish-package/README.md @@ -0,0 +1,42 @@ +# verify-elvish-package + +Verifies the source files of an **Elvish** library. + +## 🃏 Example + +```yaml +steps: + - uses: giancosta86/aurora-github/actions/verify-elvish-package@v11 +``` + +## 💡 How it works + +1. Run [check-project-license](../check-project-license/README.md) to verify the **LICENSE** file. + +1. Ensure that the library _metadata_ are declared. + +1. Execute [run-custom-tests](../run-custom-tests/README.md). + +1. Find [critical TODOs](../find-critical-todos/README.md) in the source code - which crash the workflow by default. + +## ☑️ Requirements + +- `metadata.xml` must be present in `project-directory` - as described in [epm:metadata](https://elv.sh/ref/epm.html#epm:metadata) + +## 📥 Inputs + +| Name | Type | Description | Default value | +| :-----------------------: | :---------: | :--------------------------------------------: | :-----------------------: | +| `crash-on-critical-todos` | **boolean** | Crash the workflow if critical TODOs are found | **true** | +| `source-file-regex` | **string** | PCRE pattern describing the source files | view [source](action.yml) | +| `project-directory` | **string** | The directory containing `Cargo.toml` | **.** | + +## 🌐 Further references + +- [check-project-license](../check-project-license/README.md) + +- [run-custom-tests](../run-custom-tests/README.md) + +- [find-critical-todos](../find-critical-todos/README.md) + +- [aurora-github](../../README.md) diff --git a/actions/verify-elvish-package/action.yml b/actions/verify-elvish-package/action.yml new file mode 100644 index 000000000..4ac0b1282 --- /dev/null +++ b/actions/verify-elvish-package/action.yml @@ -0,0 +1,50 @@ +name: Verify Elvish library +description: Verifies the source files of an Elvish library. + +inputs: + crash-on-critical-todos: + description: Crash the workflow if critical TODOs are found. + default: true + + source-file-regex: + description: PCRE pattern describing the source files. + default: '\.elv$' + + project-directory: + description: The directory containing `metadata.json`. + default: . + +runs: + using: composite + steps: + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 + + - name: Check the project license + uses: giancosta86/aurora-github/actions/check-project-license@v11.0.0 + + - name: Check the metadata + shell: elvish {0} + working-directory: ${{ inputs.project-directory }} + run: | + use aurora-github/elvish-library + + elvish-library:check-metadata + + - uses: giancosta86/aurora-github/actions/run-custom-tests@v11.0.0 + with: + root-directory: ${{ inputs.project-directory }} + + - name: Check for critical TODOs + uses: giancosta86/aurora-github/actions/find-critical-todos@v11.0.0 + with: + source-file-regex: ${{ inputs.source-file-regex }} + crash-on-found: ${{ inputs.crash-on-critical-todos }} + display-lines: true + root-directory: ${{ inputs.project-directory }} + + - name: Print confirmation message + shell: elvish {0} + run: | + use github.com/giancosta86/aurora-elvish/console + + console:echo ✅🔮 Elvish library verified! diff --git a/tests/elvish-library/metadata.json b/tests/elvish-library/metadata.json new file mode 100644 index 000000000..c0f319694 --- /dev/null +++ b/tests/elvish-library/metadata.json @@ -0,0 +1,9 @@ +{ + "description": "My description", + + "maintainers": ["Gianluca Costa "], + + "homepage": "https://github.com/giancosta86/aurora-elvish", + + "dependencies": [] +} diff --git a/tests/elvish-library/my-ops.elv b/tests/elvish-library/my-ops.elv new file mode 100644 index 000000000..bf11b7f67 --- /dev/null +++ b/tests/elvish-library/my-ops.elv @@ -0,0 +1,3 @@ +fn test-sum { |a b c| + + $a $b $c +} \ No newline at end of file diff --git a/tests/elvish-library/my-ops.test.elv b/tests/elvish-library/my-ops.test.elv new file mode 100644 index 000000000..1b7cb2856 --- /dev/null +++ b/tests/elvish-library/my-ops.test.elv @@ -0,0 +1,17 @@ +use ./my-ops + +describe 'Summing three numbers' { + describe 'when they are all 0' { + it 'should return 0' { + my-ops:test-sum 0 0 0 | + should-be 0 + } + } + + describe 'when they are not 0' { + it 'should return the expected sum' { + my-ops:test-sum 90 5 3 | + should-be 98 + } + } +} \ No newline at end of file From 2d7df46d8be62c223c81619875ff1a8b771d1e7f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 08:07:58 +0200 Subject: [PATCH 39/63] Introduce new action: `setup-elvish-package` --- .../test-setup-elvish-package/action.yml | 32 +++++ actions/setup-elvish-package/action.yml | 113 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 .github/test-actions/test-setup-elvish-package/action.yml create mode 100644 actions/setup-elvish-package/action.yml diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml new file mode 100644 index 000000000..610bc2887 --- /dev/null +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -0,0 +1,32 @@ +name: Test setup-elvish-package + +runs: + using: composite + steps: + - shell: bash + run: echo "🎭 Test by installing Velvet from the main branch ..." + + - uses: ./actions/setup-elvish-package + with: + package: github.com/giancosta86/velvet + quiet: false + + - shell: elvish {0} + run: | + use epm + + var package-path = (epm:metadata 'github.com/giancosta86/velvet')[dst] + + echo elvish-package-path=$package-path >> (get-env GITHUB_ENV) + + - shell: bash + working-directory: ${{ env.elvish-package-path }} + run: | + gitRef="$(git describe --tags --always --dirty --broken)" + + if [[ "$gitRef" == 'main' ]] + then + echo "✅The package is on the main branch, as expected!" + else + echo "Current Git reference instead of main: '$gitRef'" + fi diff --git a/actions/setup-elvish-package/action.yml b/actions/setup-elvish-package/action.yml new file mode 100644 index 000000000..41b4094db --- /dev/null +++ b/actions/setup-elvish-package/action.yml @@ -0,0 +1,113 @@ +name: Setup Elvish package +description: Ensures a package for the Elvish shell is installed, possibly at a specific Git reference. + +inputs: + package: + description: The name of the library, passed to epm:install. + + git-ref: + description: The optional Git reference (branch, tag, ...) to checkout. + + quiet: + description: Displays more detailed messages. + default: true + +runs: + using: composite + steps: + - name: Validate inputs + shell: elvish {0} + run: | + if (== 0 (count '${{ inputs.package }}')) { + fail 'Missing input: package' + } + + if (== 0 (count '${{ inputs.quiet }}')) { + fail 'Missing input: quiet' + } + + if (not (has-value [true false] '${{ inputs.quiet }}')) { + fail 'Invalid boolean input for ''quiet'': '''${{ inputs.quiet }}"'" + } + + - name: Set environment variables + shell: elvish {0} + run: | + use epm + use os + + var package-cache-key = '${{ github.workflow }}-${{ github.run_number }}-elvish-package-${{ inputs.package }}' + + var package-path = (epm:metadata '${{ inputs.package }}')[dst] + + var package-installed = ( + if (os:is-dir $package-path) { + put 'true' + } else { + put 'false' + } + ) + + echo elvish-package-cache-key=$package-cache-key >> (get-env GITHUB_ENV) + + echo elvish-package-path=$package-path >> (get-env GITHUB_ENV) + + echo elvish-package-installed=$package-installed >> (get-env GITHUB_ENV) + + - name: Declare the package is already installed + if: env.elvish-package-installed == 'true' && inputs.quiet != 'true' + shell: elvish {0} + run: echo 🌟📚Elvish package '${{ inputs.package }}' is already installed! + + - name: Restore cached package + if: env.elvish-package-installed != 'true' + id: restore-cached-package + uses: actions/cache/restore@v4 + with: + key: ${{ env.elvish-package-cache-key }} + path: ${{ env.elvish-package-path }} + + - name: Confirm cache hit for Elvish package + if: env.elvish-package-installed != 'true' && steps.restore-cached-package.outputs.cache-hit == 'true' && inputs.quiet != 'true' + shell: elvish {0} + run: echo 📤📚 Elvish package '${{ inputs.package }}' restored from cache! > &2 + + - name: Install package via epm + if: env.elvish-package-installed != 'true' && steps.restore-cached-package.outputs.cache-hit != 'true' + shell: elvish {0} + run: | + use epm + + epm:install '${{ inputs.package }}' + + - name: Cache Elvish package + if: env.elvish-package-installed != 'true' && steps.restore-cached-package.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ env.elvish-package-path }} + key: ${{ env.elvish-package-cache-key }} + + - name: Ensure the requested Git reference + if: inputs.git-ref != '' + shell: elvish {0} + run: | + var quiet = (==s '${{ inputs.quiet }}' true) + + if (not $quiet) { + echo 🧭Switching Elvish package '${{ inputs.package }}' to Git reference: '${{ inputs.git-ref }}' + } + + cd (get-env elvish-package-path) + + git clean -df + + git checkout '${{ inputs.git-ref }}' + + if (not $quiet) { + echo ✅ Switched to Git reference '${{ inputs.git-ref }}' + } + + - name: Display final confirmation message + if: inputs.quiet != 'true' + shell: elvish {0} + run: echo 🚀📚 Elvish package '${{ inputs.package }}' ready! > &2 From 577e96dd24f750033d93ec65c99a8b22d447a474 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 21 May 2025 03:29:31 +0200 Subject: [PATCH 40/63] Revise the test network --- .github/workflows/verify.yml | 321 ++++++++++++++++++++++------------- 1 file changed, 204 insertions(+), 117 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 0c96eedfa..d36d6c8d9 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,4 +1,5 @@ name: Verify +description: Runs the entire test network for the project. on: pull_request: @@ -8,25 +9,51 @@ on: jobs: test-setup-elvish-context: + if: true runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 + - uses: ./.github/test-actions/test-setup-elvish-context + with: + skip-if-existing: false test-setup-elvish-context-in-subsequent-job: + if: true runs-on: ubuntu-24.04 needs: test-setup-elvish-context steps: - uses: actions/checkout@v4 + + - uses: ./.github/test-actions/test-setup-elvish-context + with: + skip-if-existing: true + + - uses: ./.github/test-actions/test-setup-elvish-context + with: + skip-if-existing: true + + test-setup-elvish-package: + if: true + runs-on: ubuntu-24.04 + needs: test-setup-elvish-context-in-subsequent-job + steps: + - uses: actions/checkout@v4 + - uses: ./actions/setup-elvish-context + - uses: ./.github/test-actions/test-setup-elvish-package + test-detect-branch-version: + if: true runs-on: ubuntu-24.04 + needs: test-setup-elvish-package steps: - uses: actions/checkout@v4 - uses: ./.github/test-actions/test-detect-branch-version test-check-action-references: + if: true runs-on: ubuntu-24.04 needs: test-detect-branch-version steps: @@ -34,6 +61,7 @@ jobs: - uses: ./actions/check-action-references test-check-project-license: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -41,6 +69,7 @@ jobs: - uses: ./.github/test-actions/test-check-project-license test-enforce-branch-version: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -48,262 +77,320 @@ jobs: - uses: ./.github/test-actions/test-enforce-branch-version test-find-critical-todos: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - uses: ./.github/test-actions/test-find-critical-todos - test-tag-and-release: + test-install-wasm-pack: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-tag-and-release + - uses: ./.github/test-actions/test-install-wasm-pack - test-install-wasm-pack: + test-setup-rust-context: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-install-wasm-pack + - uses: ./.github/test-actions/test-setup-rust-context - test-check-rust-versions: + test-extract-rust-snippets: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-check-rust-versions + - uses: ./.github/test-actions/test-extract-rust-snippets - test-extract-rust-snippets: + test-parse-npm-scope: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-extract-rust-snippets + - uses: ./.github/test-actions/test-parse-npm-scope - test-verify-npm-package: + test-install-system-packages: + if: true runs-on: ubuntu-24.04 - needs: - - test-check-project-license - - test-enforce-branch-version - - test-find-critical-todos - - test-check-subpath-exports - - test-setup-nodejs-context - - test-run-custom-tests + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-verify-npm-package + - uses: ./.github/test-actions/test-install-system-packages - test-publish-npm-package: + test-inject-subpath-exports: + if: true runs-on: ubuntu-24.04 - needs: - - test-enforce-branch-version - - test-setup-nodejs-context - - test-publish-github-pages + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-npm-package + - uses: ./.github/test-actions/test-inject-subpath-exports - test-verify-rust-crate: + test-check-subpath-exports: + if: true runs-on: ubuntu-24.04 - needs: - - test-check-project-license - - test-check-rust-versions - - test-enforce-branch-version - - test-find-critical-todos - - test-extract-rust-snippets + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-verify-rust-crate + - uses: ./.github/test-actions/test-check-subpath-exports - test-publish-rust-crate: + test-setup-nodejs-context: + if: true runs-on: ubuntu-24.04 - needs: - - test-publish-github-pages - - test-enforce-branch-version + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-rust-crate + - uses: ./.github/test-actions/test-setup-nodejs-context - test-parse-npm-scope: + test-run-shell-script: + if: true runs-on: ubuntu-24.04 needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-parse-npm-scope + - uses: ./.github/test-actions/test-run-shell-script - test-generate-wasm-target: + test-upload-release-assets: + if: true runs-on: ubuntu-24.04 - needs: test-parse-npm-scope + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-generate-wasm-target + - uses: ./.github/test-actions/test-upload-release-assets - test-verify-rust-wasm: + test-install-via-sdkman: + if: true runs-on: ubuntu-24.04 - needs: - - test-check-project-license - - test-verify-rust-crate - - test-install-wasm-pack - - test-generate-wasm-target - - test-run-custom-tests + needs: test-check-action-references steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-verify-rust-wasm + - uses: ./.github/test-actions/test-install-via-sdkman - test-publish-rust-wasm: + test-verify-python-package: + if: true runs-on: ubuntu-24.04 needs: - - test-install-wasm-pack - - test-generate-wasm-target - - test-publish-npm-package + - test-check-project-license + - test-enforce-branch-version + - test-find-critical-todos steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-rust-wasm + - uses: ./.github/test-actions/test-verify-python-package - test-verify-jvm-project: + test-verify-rust-crate: + if: true runs-on: ubuntu-24.04 needs: - test-check-project-license + - test-setup-rust-context - test-enforce-branch-version - test-find-critical-todos - - test-install-via-sdkman + - test-extract-rust-snippets steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-verify-jvm-project + - uses: ./.github/test-actions/test-verify-rust-crate - test-publish-jvm-project: + test-generate-wasm-target: + if: true runs-on: ubuntu-24.04 needs: + - test-parse-npm-scope - test-enforce-branch-version - - test-install-via-sdkman steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-jvm-project - with: - jvm-token: ${{ secrets.JVM_TOKEN }} + - uses: ./.github/test-actions/test-generate-wasm-target - test-verify-python-package: + test-publish-github-pages: + if: true runs-on: ubuntu-24.04 needs: - - test-check-project-license + - test-setup-nodejs-context - test-enforce-branch-version - - test-find-critical-todos + permissions: + pages: write + id-token: write steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-verify-python-package + - uses: ./.github/test-actions/test-publish-github-pages - test-publish-python-package: + test-tag-and-release: + if: true + runs-on: ubuntu-24.04 + needs: test-run-shell-script + steps: + - uses: actions/checkout@v4 + - uses: ./.github/test-actions/test-tag-and-release + + test-run-custom-tests: + if: true runs-on: ubuntu-24.04 needs: + - test-run-shell-script + - test-setup-nodejs-context + - test-setup-rust-context + steps: + - uses: actions/checkout@v4 + - uses: ./.github/test-actions/test-run-custom-tests + + test-verify-jvm-project: + if: true + runs-on: ubuntu-24.04 + needs: + - test-check-project-license - test-enforce-branch-version - - test-publish-github-pages + - test-find-critical-todos + - test-install-via-sdkman steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-python-package - with: - index-secret: ${{ secrets.PY_TOKEN }} + - uses: ./.github/test-actions/test-verify-jvm-project - test-install-system-packages: + test-publish-rust-crate: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-setup-rust-context + - test-publish-github-pages + - test-enforce-branch-version + - test-verify-rust-crate steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-install-system-packages + - uses: ./.github/test-actions/test-publish-rust-crate - test-inject-subpath-exports: + test-publish-python-package: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-enforce-branch-version + - test-publish-github-pages + - test-verify-python-package steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-inject-subpath-exports + - uses: ./.github/test-actions/test-publish-python-package + with: + index-secret: FAKE-SECRET - test-check-subpath-exports: + test-verify-npm-package: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-check-project-license + - test-enforce-branch-version + - test-find-critical-todos + - test-check-subpath-exports + - test-setup-nodejs-context + - test-run-custom-tests steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-check-subpath-exports + - uses: ./.github/test-actions/test-verify-npm-package - test-setup-nodejs-context: + test-verify-rust-wasm: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-check-project-license + - test-verify-rust-crate + - test-install-wasm-pack + - test-generate-wasm-target + - test-run-custom-tests steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-setup-nodejs-context + - uses: ./.github/test-actions/test-verify-rust-wasm - test-publish-github-pages: + test-publish-jvm-project: + if: true runs-on: ubuntu-24.04 needs: - - test-setup-nodejs-context - test-enforce-branch-version - permissions: - pages: write - id-token: write + - test-install-via-sdkman + - test-verify-jvm-project + - test-publish-github-pages steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-publish-github-pages + - uses: ./.github/test-actions/test-publish-jvm-project + with: + jvm-token: FAKE_TOKEN - test-run-custom-tests: + test-publish-npm-package: + if: true runs-on: ubuntu-24.04 needs: + - test-enforce-branch-version - test-setup-nodejs-context - - test-check-rust-versions + - test-publish-github-pages + - test-verify-npm-package steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-run-custom-tests + - uses: ./.github/test-actions/test-publish-npm-package - test-upload-release-assets: + test-publish-rust-wasm: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-install-wasm-pack + - test-generate-wasm-target + - test-publish-npm-package + - test-verify-rust-wasm steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-upload-release-assets + - uses: ./.github/test-actions/test-publish-rust-wasm - test-install-via-sdkman: + test-verify-elvish-package: + if: true runs-on: ubuntu-24.04 - needs: test-check-action-references + needs: + - test-check-project-license + - test-run-custom-tests + - test-find-critical-todos steps: - uses: actions/checkout@v4 - - uses: ./.github/test-actions/test-install-via-sdkman + - uses: ./.github/test-actions/test-verify-elvish-package verify: + if: ${{ always() }} runs-on: ubuntu-24.04 needs: - test-setup-elvish-context - test-setup-elvish-context-in-subsequent-job + - test-setup-elvish-package - test-detect-branch-version - test-check-action-references - test-check-project-license - test-enforce-branch-version - test-find-critical-todos - - test-tag-and-release - test-install-wasm-pack - - test-check-rust-versions + - test-setup-rust-context - test-extract-rust-snippets - - test-verify-npm-package - - test-publish-npm-package - - test-verify-rust-crate - - test-publish-rust-crate - test-parse-npm-scope - - test-generate-wasm-target - - test-verify-rust-wasm - - test-publish-rust-wasm - - test-verify-jvm-project - - test-publish-jvm-project - - test-verify-python-package - - test-publish-python-package - test-install-system-packages - test-inject-subpath-exports - test-check-subpath-exports - test-setup-nodejs-context - - test-publish-github-pages - - test-run-custom-tests + - test-run-shell-script - test-upload-release-assets - test-install-via-sdkman - + - test-verify-python-package + - test-verify-rust-crate + - test-generate-wasm-target + - test-publish-github-pages + - test-tag-and-release + - test-run-custom-tests + - test-verify-jvm-project + - test-publish-rust-crate + - test-publish-python-package + - test-verify-npm-package + - test-verify-rust-wasm + - test-publish-jvm-project + - test-publish-npm-package + - test-publish-rust-wasm + - test-verify-elvish-package steps: - - name: Show completion message - shell: bash - run: echo "✅Verification complete!" + - uses: actions/checkout@v4 + - uses: ./actions/check-required-jobs + with: + needs-as-json: ${{ toJSON(needs) }} From 0e6975ff0c64899ebbccc2d104167f974f6644af Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 7 Jun 2025 17:42:58 +0200 Subject: [PATCH 41/63] Update action references for v11 --- actions/check-action-references/README.md | 2 +- actions/check-action-references/action.yml | 2 +- actions/check-project-license/README.md | 2 +- actions/check-project-license/action.yml | 2 +- actions/check-subpath-exports/README.md | 2 +- actions/check-subpath-exports/action.yml | 2 +- actions/detect-branch-version/README.md | 2 +- actions/detect-branch-version/action.yml | 2 +- actions/enforce-branch-version/README.md | 2 +- actions/enforce-branch-version/action.yml | 2 +- actions/extract-rust-snippets/README.md | 2 +- actions/extract-rust-snippets/action.yml | 2 +- actions/find-critical-todos/README.md | 2 +- actions/find-critical-todos/action.yml | 2 +- actions/generate-wasm-target/README.md | 2 +- actions/generate-wasm-target/action.yml | 2 +- actions/inject-subpath-exports/README.md | 2 +- actions/inject-subpath-exports/action.yml | 2 +- actions/install-system-packages/README.md | 2 +- actions/install-system-packages/action.yml | 2 +- actions/install-via-sdkman/README.md | 2 +- actions/install-via-sdkman/action.yml | 2 +- actions/install-wasm-pack/README.md | 2 +- actions/install-wasm-pack/action.yml | 2 +- actions/parse-npm-scope/README.md | 2 +- actions/parse-npm-scope/action.yml | 2 +- actions/publish-github-pages/README.md | 2 +- actions/publish-github-pages/action.yml | 6 +++--- actions/publish-jvm-project/README.md | 2 +- actions/publish-jvm-project/action.yml | 6 +++--- actions/publish-npm-package/README.md | 2 +- actions/publish-npm-package/action.yml | 8 ++++---- actions/publish-python-package/README.md | 2 +- actions/publish-python-package/action.yml | 6 +++--- actions/publish-rust-crate/README.md | 2 +- actions/publish-rust-crate/action.yml | 8 ++++---- actions/publish-rust-wasm/README.md | 2 +- actions/publish-rust-wasm/action.yml | 10 +++++----- actions/run-custom-tests/README.md | 2 +- actions/run-custom-tests/action.yml | 6 +++--- actions/setup-elvish-context/README.md | 2 +- actions/setup-nodejs-context/README.md | 2 +- actions/setup-nodejs-context/action.yml | 2 +- actions/setup-rust-context/README.md | 2 +- actions/setup-rust-context/action.yml | 2 +- actions/tag-and-release/README.md | 2 +- actions/tag-and-release/action.yml | 2 +- actions/upload-release-assets/README.md | 2 +- actions/upload-release-assets/action.yml | 2 +- actions/verify-jvm-project/README.md | 2 +- actions/verify-jvm-project/action.yml | 8 ++++---- actions/verify-npm-package/README.md | 2 +- actions/verify-npm-package/action.yml | 14 +++++++------- actions/verify-python-package/README.md | 2 +- actions/verify-python-package/action.yml | 8 ++++---- actions/verify-rust-crate/README.md | 2 +- actions/verify-rust-crate/action.yml | 12 ++++++------ actions/verify-rust-wasm/README.md | 2 +- actions/verify-rust-wasm/action.yml | 10 +++++----- 59 files changed, 98 insertions(+), 98 deletions(-) diff --git a/actions/check-action-references/README.md b/actions/check-action-references/README.md index 6e645873f..888b9e273 100644 --- a/actions/check-action-references/README.md +++ b/actions/check-action-references/README.md @@ -6,7 +6,7 @@ Prevents cross-branch `uses:` directives between **GitHub** actions residing bel ```yaml steps: - - uses: giancosta86/aurora-github/actions/check-action-references@v10 + - uses: giancosta86/aurora-github/actions/check-action-references@v11 ``` ## 💡 How it works diff --git a/actions/check-action-references/action.yml b/actions/check-action-references/action.yml index ca08516b1..272c3f005 100644 --- a/actions/check-action-references/action.yml +++ b/actions/check-action-references/action.yml @@ -9,7 +9,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.actions-directory }} diff --git a/actions/check-project-license/README.md b/actions/check-project-license/README.md index a4795edef..4bc6e79d5 100644 --- a/actions/check-project-license/README.md +++ b/actions/check-project-license/README.md @@ -6,7 +6,7 @@ Checks the validity of the project license file. ```yaml steps: - - uses: giancosta86/aurora-github/actions/check-project-license@v10 + - uses: giancosta86/aurora-github/actions/check-project-license@v11 ``` **Please, note**: this action is automatically run by: diff --git a/actions/check-project-license/action.yml b/actions/check-project-license/action.yml index 569eb9f68..11041cca3 100644 --- a/actions/check-project-license/action.yml +++ b/actions/check-project-license/action.yml @@ -9,7 +9,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} run: | diff --git a/actions/check-subpath-exports/README.md b/actions/check-subpath-exports/README.md index 4cf2cc2a0..fc7008ea1 100644 --- a/actions/check-subpath-exports/README.md +++ b/actions/check-subpath-exports/README.md @@ -6,7 +6,7 @@ Verifies that all the [subpath exports](https://nodejs.org/api/packages.html#sub ```yaml steps: - - uses: giancosta86/aurora-github/actions/check-subpath-exports@v10 + - uses: giancosta86/aurora-github/actions/check-subpath-exports@v11 ``` **Please, note**: this action is automatically run by [verify-npm-package](../verify-npm-package/README.md). diff --git a/actions/check-subpath-exports/action.yml b/actions/check-subpath-exports/action.yml index 2f6b980b4..06e67c516 100644 --- a/actions/check-subpath-exports/action.yml +++ b/actions/check-subpath-exports/action.yml @@ -9,7 +9,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.project-directory }} diff --git a/actions/detect-branch-version/README.md b/actions/detect-branch-version/README.md index e6d099486..9ff55d0c4 100644 --- a/actions/detect-branch-version/README.md +++ b/actions/detect-branch-version/README.md @@ -7,7 +7,7 @@ Extracts the version from the name of the current **Git** branch, returning a va ```yaml steps: - id: detector - uses: giancosta86/aurora-github/actions/detect-branch-version@v10 + uses: giancosta86/aurora-github/actions/detect-branch-version@v11 - run: | branch="${{ steps.detector.outputs.branch }}" diff --git a/actions/detect-branch-version/action.yml b/actions/detect-branch-version/action.yml index d61e4c861..7eb1d384e 100644 --- a/actions/detect-branch-version/action.yml +++ b/actions/detect-branch-version/action.yml @@ -21,7 +21,7 @@ outputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - id: detect-version shell: elvish {0} diff --git a/actions/enforce-branch-version/README.md b/actions/enforce-branch-version/README.md index 19ef2d531..9f5bd7d06 100644 --- a/actions/enforce-branch-version/README.md +++ b/actions/enforce-branch-version/README.md @@ -6,7 +6,7 @@ Ensures that the version in the artifact descriptor matches the **Git** branch v ```yaml steps: - - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10 + - uses: giancosta86/aurora-github/actions/enforce-branch-version@v11 with: mode: inject ``` diff --git a/actions/enforce-branch-version/action.yml b/actions/enforce-branch-version/action.yml index 9cd7250f5..93cea9444 100644 --- a/actions/enforce-branch-version/action.yml +++ b/actions/enforce-branch-version/action.yml @@ -15,7 +15,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.project-directory }} diff --git a/actions/extract-rust-snippets/README.md b/actions/extract-rust-snippets/README.md index 2de0dbb19..51a6d45f2 100644 --- a/actions/extract-rust-snippets/README.md +++ b/actions/extract-rust-snippets/README.md @@ -6,7 +6,7 @@ Extracts **Rust** test snippets from a **Markdown** file to standalone test file ```yaml steps: - - uses: giancosta86/aurora-github/actions/extract-rust-snippets@v10 + - uses: giancosta86/aurora-github/actions/extract-rust-snippets@v11 ``` **Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md). diff --git a/actions/extract-rust-snippets/action.yml b/actions/extract-rust-snippets/action.yml index 22cc53089..b809053e3 100644 --- a/actions/extract-rust-snippets/action.yml +++ b/actions/extract-rust-snippets/action.yml @@ -21,7 +21,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.project-directory }} diff --git a/actions/find-critical-todos/README.md b/actions/find-critical-todos/README.md index e235e2c72..9154a2dd6 100644 --- a/actions/find-critical-todos/README.md +++ b/actions/find-critical-todos/README.md @@ -6,7 +6,7 @@ Looks for _critical TODOs_ - that is, instances of the `TODO!` string - in sourc ```yaml steps: - - uses: giancosta86/aurora-github/actions/find-critical-todos@v10 + - uses: giancosta86/aurora-github/actions/find-critical-todos@v11 with: source-file-regex: ^(src|tests)\/.+\.(c|m)?(j|t)sx?$ ``` diff --git a/actions/find-critical-todos/action.yml b/actions/find-critical-todos/action.yml index d10a9c65b..54da0f0c2 100644 --- a/actions/find-critical-todos/action.yml +++ b/actions/find-critical-todos/action.yml @@ -29,7 +29,7 @@ outputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - id: main shell: elvish {0} diff --git a/actions/generate-wasm-target/README.md b/actions/generate-wasm-target/README.md index 07b5706d6..67c166eab 100644 --- a/actions/generate-wasm-target/README.md +++ b/actions/generate-wasm-target/README.md @@ -6,7 +6,7 @@ Generates the source files for a **WebAssembly** target from a **Rust** project. ```yaml steps: - - uses: giancosta86/aurora-github/actions/generate-wasm-target@v10 + - uses: giancosta86/aurora-github/actions/generate-wasm-target@v11 with: target: web npm-scope: npmuser diff --git a/actions/generate-wasm-target/action.yml b/actions/generate-wasm-target/action.yml index cfb7eae1a..53597e4e1 100644 --- a/actions/generate-wasm-target/action.yml +++ b/actions/generate-wasm-target/action.yml @@ -27,7 +27,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Generate the WebAssembly target shell: elvish {0} diff --git a/actions/inject-subpath-exports/README.md b/actions/inject-subpath-exports/README.md index d8d837b01..cadfbd957 100644 --- a/actions/inject-subpath-exports/README.md +++ b/actions/inject-subpath-exports/README.md @@ -6,7 +6,7 @@ Appends [subpath exports](https://nodejs.org/api/packages.html#subpath-exports) ```yaml steps: - - uses: giancosta86/aurora-github/actions/inject-subpath-exports@v10 + - uses: giancosta86/aurora-github/actions/inject-subpath-exports@v11 ``` ## 💡 How it works diff --git a/actions/inject-subpath-exports/action.yml b/actions/inject-subpath-exports/action.yml index a52260135..f4b71f106 100644 --- a/actions/inject-subpath-exports/action.yml +++ b/actions/inject-subpath-exports/action.yml @@ -17,7 +17,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.project-directory }} diff --git a/actions/install-system-packages/README.md b/actions/install-system-packages/README.md index ca0ef462a..625ce92d2 100644 --- a/actions/install-system-packages/README.md +++ b/actions/install-system-packages/README.md @@ -6,7 +6,7 @@ Installs software using the platform's package manager. ```yaml steps: - - uses: giancosta86/aurora-github/actions/install-system-packages@v10 + - uses: giancosta86/aurora-github/actions/install-system-packages@v11 with: packages: moreutils ``` diff --git a/actions/install-system-packages/action.yml b/actions/install-system-packages/action.yml index be303de6d..ea2a36064 100644 --- a/actions/install-system-packages/action.yml +++ b/actions/install-system-packages/action.yml @@ -15,7 +15,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} run: | diff --git a/actions/install-via-sdkman/README.md b/actions/install-via-sdkman/README.md index e1cffcc7d..65cade0a0 100644 --- a/actions/install-via-sdkman/README.md +++ b/actions/install-via-sdkman/README.md @@ -6,7 +6,7 @@ Installs the requested SDK using [SDKMAN!](https://sdkman.io/) ```yaml steps: - - uses: giancosta86/aurora-github/actions/install-sdkman-candidate@v10 + - uses: giancosta86/aurora-github/actions/install-sdkman-candidate@v11 with: candidate: java version: 23-open diff --git a/actions/install-via-sdkman/action.yml b/actions/install-via-sdkman/action.yml index ff36f7921..eed5d498d 100644 --- a/actions/install-via-sdkman/action.yml +++ b/actions/install-via-sdkman/action.yml @@ -11,7 +11,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} run: | diff --git a/actions/install-wasm-pack/README.md b/actions/install-wasm-pack/README.md index 759ea0fb5..3b64f4e2c 100644 --- a/actions/install-wasm-pack/README.md +++ b/actions/install-wasm-pack/README.md @@ -6,7 +6,7 @@ Installs [wasm-pack](https://rustwasm.github.io/wasm-pack/), for creating **Rust ```yaml steps: - - uses: giancosta86/aurora-github/actions/install-wasm-pack@v10 + - uses: giancosta86/aurora-github/actions/install-wasm-pack@v11 ``` **Please, note**: this action is automatically run by [verify-rust-wasm](../verify-rust-wasm/README.md) and [publish-rust-wasm](../publish-rust-wasm/README.md). diff --git a/actions/install-wasm-pack/action.yml b/actions/install-wasm-pack/action.yml index 9b2e242e1..41b487b50 100644 --- a/actions/install-wasm-pack/action.yml +++ b/actions/install-wasm-pack/action.yml @@ -8,7 +8,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} run: | diff --git a/actions/parse-npm-scope/README.md b/actions/parse-npm-scope/README.md index b8cd87cbb..c9a48cd53 100644 --- a/actions/parse-npm-scope/README.md +++ b/actions/parse-npm-scope/README.md @@ -7,7 +7,7 @@ Parses a mandatory [npm scope](https://docs.npmjs.com/cli/v10/using-npm/scope) d ```yaml steps: - id: scope-parser - uses: giancosta86/aurora-github/actions/parse-npm-scope@v10 + uses: giancosta86/aurora-github/actions/parse-npm-scope@v11 with: scope: giancosta86 ``` diff --git a/actions/parse-npm-scope/action.yml b/actions/parse-npm-scope/action.yml index c2365c028..9ee2c06e5 100644 --- a/actions/parse-npm-scope/action.yml +++ b/actions/parse-npm-scope/action.yml @@ -13,7 +13,7 @@ outputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - id: parse-actual-scope shell: elvish {0} diff --git a/actions/publish-github-pages/README.md b/actions/publish-github-pages/README.md index 41af1cebc..1ceb58ab7 100644 --- a/actions/publish-github-pages/README.md +++ b/actions/publish-github-pages/README.md @@ -8,7 +8,7 @@ Publishes a directory as the [GitHub Pages](https://pages.github.com/) website f steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-github-pages@v10 + - uses: giancosta86/aurora-github/actions/publish-github-pages@v11 ``` **Please, note**: this action is automatically run by: diff --git a/actions/publish-github-pages/action.yml b/actions/publish-github-pages/action.yml index 96786bb6a..87748dbc3 100644 --- a/actions/publish-github-pages/action.yml +++ b/actions/publish-github-pages/action.yml @@ -26,7 +26,7 @@ outputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Set up strategy shell: elvish {0} @@ -42,14 +42,14 @@ runs: - name: Enforce branch version if: env.strategy == 'nodejs' || env.strategy == 'maven' - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.source-directory }} - name: Setup NodeJS context if: env.strategy == 'nodejs' - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11.0.0 with: project-directory: ${{ inputs.source-directory }} diff --git a/actions/publish-jvm-project/README.md b/actions/publish-jvm-project/README.md index 5b366828c..b84cd1259 100644 --- a/actions/publish-jvm-project/README.md +++ b/actions/publish-jvm-project/README.md @@ -8,7 +8,7 @@ Publishes a project for the **Java Virtual Machine** - using **Maven** or **Grad steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-jvm-project@v10 + - uses: giancosta86/aurora-github/actions/publish-jvm-project@v11 with: auth-user: userOnTheServer auth-token: ${{ secrets.SERVER_TOKEN }} diff --git a/actions/publish-jvm-project/action.yml b/actions/publish-jvm-project/action.yml index 54be78aa3..0bf4e9505 100644 --- a/actions/publish-jvm-project/action.yml +++ b/actions/publish-jvm-project/action.yml @@ -37,10 +37,10 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -67,7 +67,7 @@ runs: settings:prepare-for-publication (get-env buildTool) - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v11.0.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-npm-package/README.md b/actions/publish-npm-package/README.md index 57478085c..3c7e0d1d6 100644 --- a/actions/publish-npm-package/README.md +++ b/actions/publish-npm-package/README.md @@ -8,7 +8,7 @@ Publishes a **NodeJS** package to an [npm](https://www.npmjs.com/) registry. steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-npm-package@v10 + - uses: giancosta86/aurora-github/actions/publish-npm-package@v11 with: npm-token: ${{ secrets.NPM_TOKEN }} ``` diff --git a/actions/publish-npm-package/action.yml b/actions/publish-npm-package/action.yml index 2e0e9307e..5e6a2fc1f 100644 --- a/actions/publish-npm-package/action.yml +++ b/actions/publish-npm-package/action.yml @@ -24,16 +24,16 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup NodeJS context - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11.0.0 with: project-directory: ${{ inputs.project-directory }} @@ -46,7 +46,7 @@ runs: pnpm:run-optional-script build - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v11.0.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-python-package/README.md b/actions/publish-python-package/README.md index 0c438e7d0..e71c294ee 100644 --- a/actions/publish-python-package/README.md +++ b/actions/publish-python-package/README.md @@ -8,7 +8,7 @@ Publishes a **Python** package using [PDM](https://pdm-project.org). steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-python-package@v10 + - uses: giancosta86/aurora-github/actions/publish-python-package@v11 with: index-user: __token__ index-secret: ${{ secrets.PYPI_TOKEN }} diff --git a/actions/publish-python-package/action.yml b/actions/publish-python-package/action.yml index d9363f88a..92c4557ad 100644 --- a/actions/publish-python-package/action.yml +++ b/actions/publish-python-package/action.yml @@ -33,10 +33,10 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -53,7 +53,7 @@ runs: context:setup &pdm-version=$pdm-version - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v11.0.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-rust-crate/README.md b/actions/publish-rust-crate/README.md index 51c423780..d01d33f45 100644 --- a/actions/publish-rust-crate/README.md +++ b/actions/publish-rust-crate/README.md @@ -8,7 +8,7 @@ Publishes a **Rust** crate - by default, to [crates.io](https://crates.io/) - wi steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-rust-crate@v10 + - uses: giancosta86/aurora-github/actions/publish-rust-crate@v11 with: cargo-token: ${{ secrets.CARGO_TOKEN }} ``` diff --git a/actions/publish-rust-crate/action.yml b/actions/publish-rust-crate/action.yml index f4b65d63d..f92754131 100644 --- a/actions/publish-rust-crate/action.yml +++ b/actions/publish-rust-crate/action.yml @@ -28,16 +28,16 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup Rust context - uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-rust-context@v11.0.0 with: project-directory: ${{ inputs.project-directory }} @@ -51,7 +51,7 @@ runs: project:document-all-features - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v11.0.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} diff --git a/actions/publish-rust-wasm/README.md b/actions/publish-rust-wasm/README.md index 0fa80cbc8..e3733fe89 100644 --- a/actions/publish-rust-wasm/README.md +++ b/actions/publish-rust-wasm/README.md @@ -8,7 +8,7 @@ Publishes a **Rust** web assembly to an [npm](https://www.npmjs.com/) registry. steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-rust-wasm@v10 + - uses: giancosta86/aurora-github/actions/publish-rust-wasm@v11 with: npm-token: ${{ secrets.NPM_TOKEN }} wasm-pack-version: 0.13.1 diff --git a/actions/publish-rust-wasm/action.yml b/actions/publish-rust-wasm/action.yml index c2f4f863e..ebe2a5108 100644 --- a/actions/publish-rust-wasm/action.yml +++ b/actions/publish-rust-wasm/action.yml @@ -40,15 +40,15 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Install wasm-pack - uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 + uses: giancosta86/aurora-github/actions/install-wasm-pack@v11.0.0 with: wasm-pack-version: ${{ inputs.wasm-pack-version }} - name: Generate the NodeJS package source files - uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.3.0 + uses: giancosta86/aurora-github/actions/generate-wasm-target@v11.0.0 with: target: ${{ inputs.wasm-target }} npm-scope: ${{ inputs.npm-scope }} @@ -67,7 +67,7 @@ runs: wasm:copy-npmrc-from-project-directory - name: Publish the GitHub Pages website - uses: giancosta86/aurora-github/actions/publish-github-pages@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-github-pages@v11.0.0 with: optional: true source-directory: ${{ inputs.project-directory }}/${{ inputs.website-directory }} @@ -75,7 +75,7 @@ runs: enforce-branch-version: ${{ inputs.enforce-branch-version }} - name: Publish NodeJS package - uses: giancosta86/aurora-github/actions/publish-npm-package@v10.3.0 + uses: giancosta86/aurora-github/actions/publish-npm-package@v11.0.0 with: dry-run: ${{ inputs.dry-run }} npm-token: ${{ inputs.npm-token }} diff --git a/actions/run-custom-tests/README.md b/actions/run-custom-tests/README.md index 49e6075b3..76befa89a 100644 --- a/actions/run-custom-tests/README.md +++ b/actions/run-custom-tests/README.md @@ -6,7 +6,7 @@ Executes arbitrary tests within a given directory; it runs a shell script by def ```yaml steps: - - uses: giancosta86/aurora-github/actions/run-custom-tests@v10 + - uses: giancosta86/aurora-github/actions/run-custom-tests@v11 with: root-directory: client-tests ``` diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 6fd602b8c..1cd5050da 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -18,7 +18,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Detect the test strategy shell: elvish {0} @@ -58,7 +58,7 @@ runs: - name: Setup NodeJS context if: env.strategy == 'nodejs' - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11.0.0 with: project-directory: ${{ inputs.root-directory }} @@ -73,7 +73,7 @@ runs: - name: Setup Rust context if: env.strategy == 'rust' - uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-rust-context@v11.0.0 with: check-toolchain-file: false project-directory: ${{ inputs.root-directory }} diff --git a/actions/setup-elvish-context/README.md b/actions/setup-elvish-context/README.md index e6dfdbdda..d21540bab 100644 --- a/actions/setup-elvish-context/README.md +++ b/actions/setup-elvish-context/README.md @@ -8,7 +8,7 @@ Installs the **Elvish** shell, caching it between multiple jobs of the same work steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11 ``` ## 💡 How it works diff --git a/actions/setup-nodejs-context/README.md b/actions/setup-nodejs-context/README.md index 9b844ff77..0f4bd9267 100644 --- a/actions/setup-nodejs-context/README.md +++ b/actions/setup-nodejs-context/README.md @@ -8,7 +8,7 @@ Conditionally installs **NodeJS** along with **pnpm**, as well as the dependenci steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10 + - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11 ``` **Please, note**: this action is automatically run by [verify-npm-package](../verify-npm-package/README.md) and [publish-npm-package](../publish-npm-package/README.md). diff --git a/actions/setup-nodejs-context/action.yml b/actions/setup-nodejs-context/action.yml index 57f046de7..561d697c4 100644 --- a/actions/setup-nodejs-context/action.yml +++ b/actions/setup-nodejs-context/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Check preconditions shell: elvish {0} diff --git a/actions/setup-rust-context/README.md b/actions/setup-rust-context/README.md index 05f2af013..f1a01e3f3 100644 --- a/actions/setup-rust-context/README.md +++ b/actions/setup-rust-context/README.md @@ -6,7 +6,7 @@ Installs and configures a **Rust** toolchain. ```yaml steps: - - uses: giancosta86/aurora-github/actions/setup-rust-context@v10 + - uses: giancosta86/aurora-github/actions/setup-rust-context@v11 ``` **Please, note**: this action is automatically run by [verify-rust-crate](../verify-rust-crate/README.md), [publish-rust-crate](../publish-rust-crate/README.md) and [run-custom-tests](../run-custom-tests/README.md). diff --git a/actions/setup-rust-context/action.yml b/actions/setup-rust-context/action.yml index 474e91b92..0ad06cfe1 100644 --- a/actions/setup-rust-context/action.yml +++ b/actions/setup-rust-context/action.yml @@ -17,7 +17,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.project-directory }} diff --git a/actions/tag-and-release/README.md b/actions/tag-and-release/README.md index 39dfb4d16..4a6bc8939 100644 --- a/actions/tag-and-release/README.md +++ b/actions/tag-and-release/README.md @@ -6,7 +6,7 @@ Merges a pull request, creates a **Git** tag and publishes a **GitHub** release, ```yaml steps: - - uses: giancosta86/aurora-github/actions/tag-and-release@v10 + - uses: giancosta86/aurora-github/actions/tag-and-release@v11 ``` It is essential to remember that this action must be called: diff --git a/actions/tag-and-release/action.yml b/actions/tag-and-release/action.yml index cfe2c9266..a6f60daf7 100644 --- a/actions/tag-and-release/action.yml +++ b/actions/tag-and-release/action.yml @@ -33,7 +33,7 @@ outputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - id: action-body shell: elvish {0} diff --git a/actions/upload-release-assets/README.md b/actions/upload-release-assets/README.md index f7bc81dfe..ccf90f844 100644 --- a/actions/upload-release-assets/README.md +++ b/actions/upload-release-assets/README.md @@ -6,7 +6,7 @@ Uploads one or more asset files to a **GitHub** release. ```yaml steps: - - uses: giancosta86/aurora-github/actions/upload-release-assets@v10 + - uses: giancosta86/aurora-github/actions/upload-release-assets@v11 with: release-tag: v3.0.2 files: logo.png data.zip diff --git a/actions/upload-release-assets/action.yml b/actions/upload-release-assets/action.yml index ce9f41e16..c78f26508 100644 --- a/actions/upload-release-assets/action.yml +++ b/actions/upload-release-assets/action.yml @@ -19,7 +19,7 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - shell: elvish {0} working-directory: ${{ inputs.source-directory }} diff --git a/actions/verify-jvm-project/README.md b/actions/verify-jvm-project/README.md index f4ae8f846..617a3ea5d 100644 --- a/actions/verify-jvm-project/README.md +++ b/actions/verify-jvm-project/README.md @@ -8,7 +8,7 @@ Verifies the source files of a project for the **Java Virtual Machine** - using steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-jvm-project@v10 + - uses: giancosta86/aurora-github/actions/verify-jvm-project@v11 ``` ## 💡 How it works diff --git a/actions/verify-jvm-project/action.yml b/actions/verify-jvm-project/action.yml index e29f4f270..9d343fab6 100644 --- a/actions/verify-jvm-project/action.yml +++ b/actions/verify-jvm-project/action.yml @@ -31,13 +31,13 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 + uses: giancosta86/aurora-github/actions/check-project-license@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -67,7 +67,7 @@ runs: verification:verify &quiet-tool=$quiet-tool (get-env buildTool) - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v11.0.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-npm-package/README.md b/actions/verify-npm-package/README.md index 62ec495a1..52cdd463e 100644 --- a/actions/verify-npm-package/README.md +++ b/actions/verify-npm-package/README.md @@ -10,7 +10,7 @@ It is worth noting this action can support any technology - as long as you compl steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-npm-package@v10 + - uses: giancosta86/aurora-github/actions/verify-npm-package@v11 ``` **IMPORTANT**: please, remember to declare your verification process in the `verify` script within `package.json`! For example: diff --git a/actions/verify-npm-package/action.yml b/actions/verify-npm-package/action.yml index 6bea3b0f6..c58563450 100644 --- a/actions/verify-npm-package/action.yml +++ b/actions/verify-npm-package/action.yml @@ -25,19 +25,19 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 + uses: giancosta86/aurora-github/actions/check-project-license@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup NodeJS context - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11.0.0 with: project-directory: ${{ inputs.project-directory }} @@ -60,18 +60,18 @@ runs: - name: Check subpath exports if: inputs.check-subpath-exports == 'true' - uses: giancosta86/aurora-github/actions/check-subpath-exports@v10.3.0 + uses: giancosta86/aurora-github/actions/check-subpath-exports@v11.0.0 with: project-directory: ${{ inputs.project-directory }} - name: Run optional custom tests from the 'tests' directory - uses: giancosta86/aurora-github/actions/run-custom-tests@v10.3.0 + uses: giancosta86/aurora-github/actions/run-custom-tests@v11.0.0 with: optional: true root-directory: ${{ inputs.project-directory }}/tests - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v11.0.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-python-package/README.md b/actions/verify-python-package/README.md index d469d9173..3a8488182 100644 --- a/actions/verify-python-package/README.md +++ b/actions/verify-python-package/README.md @@ -8,7 +8,7 @@ Verifies the source files of a **Python** package using [PDM](https://pdm-projec steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-python-package@v10 + - uses: giancosta86/aurora-github/actions/verify-python-package@v11 ``` ## 💡 How it works diff --git a/actions/verify-python-package/action.yml b/actions/verify-python-package/action.yml index f2af595a3..fb979167c 100644 --- a/actions/verify-python-package/action.yml +++ b/actions/verify-python-package/action.yml @@ -24,13 +24,13 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 + uses: giancosta86/aurora-github/actions/check-project-license@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} @@ -63,7 +63,7 @@ runs: project:build - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v11.0.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-rust-crate/README.md b/actions/verify-rust-crate/README.md index 246b9a302..74a41aba3 100644 --- a/actions/verify-rust-crate/README.md +++ b/actions/verify-rust-crate/README.md @@ -8,7 +8,7 @@ Verifies the source files of a **Rust** crate. steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-rust-crate@v10 + - uses: giancosta86/aurora-github/actions/verify-rust-crate@v11 ``` **Please, note**: this action is automatically run by [verify-rust-wasm](../verify-rust-wasm/README.md). diff --git a/actions/verify-rust-crate/action.yml b/actions/verify-rust-crate/action.yml index e663e8cd7..96a5b691d 100644 --- a/actions/verify-rust-crate/action.yml +++ b/actions/verify-rust-crate/action.yml @@ -29,19 +29,19 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Check the project license - uses: giancosta86/aurora-github/actions/check-project-license@v10.3.0 + uses: giancosta86/aurora-github/actions/check-project-license@v11.0.0 - name: Enforce branch version - uses: giancosta86/aurora-github/actions/enforce-branch-version@v10.3.0 + uses: giancosta86/aurora-github/actions/enforce-branch-version@v11.0.0 with: mode: ${{ inputs.enforce-branch-version }} project-directory: ${{ inputs.project-directory }} - name: Setup Rust context - uses: giancosta86/aurora-github/actions/setup-rust-context@v10.3.0 + uses: giancosta86/aurora-github/actions/setup-rust-context@v11.0.0 with: project-directory: ${{ inputs.project-directory }} @@ -59,7 +59,7 @@ runs: project:check-style &run-clippy-checks=$run-clippy-checks &check-rustdoc=$check-rustdoc - name: Extract code snippets as tests from README.md - uses: giancosta86/aurora-github/actions/extract-rust-snippets@v10.3.0 + uses: giancosta86/aurora-github/actions/extract-rust-snippets@v11.0.0 with: optional: true project-directory: ${{ inputs.project-directory }} @@ -73,7 +73,7 @@ runs: project:run-tests - name: Check for critical TODOs - uses: giancosta86/aurora-github/actions/find-critical-todos@v10.3.0 + uses: giancosta86/aurora-github/actions/find-critical-todos@v11.0.0 with: source-file-regex: ${{ inputs.source-file-regex }} crash-on-found: ${{ inputs.crash-on-critical-todos }} diff --git a/actions/verify-rust-wasm/README.md b/actions/verify-rust-wasm/README.md index 8f5687d94..0660bfd70 100644 --- a/actions/verify-rust-wasm/README.md +++ b/actions/verify-rust-wasm/README.md @@ -8,7 +8,7 @@ Verifies the source files of a **Rust** web assembly. steps: - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-rust-wasm@v10 + - uses: giancosta86/aurora-github/actions/verify-rust-wasm@v11 with: wasm-pack-version: 0.13.1 npm-scope: your-npm-scope diff --git a/actions/verify-rust-wasm/action.yml b/actions/verify-rust-wasm/action.yml index 1aef8bb19..ced6ba5a6 100644 --- a/actions/verify-rust-wasm/action.yml +++ b/actions/verify-rust-wasm/action.yml @@ -43,15 +43,15 @@ inputs: runs: using: composite steps: - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v10.3.0 + - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11.0.0 - name: Install wasm-pack - uses: giancosta86/aurora-github/actions/install-wasm-pack@v10.3.0 + uses: giancosta86/aurora-github/actions/install-wasm-pack@v11.0.0 with: wasm-pack-version: ${{ inputs.wasm-pack-version }} - name: Verify Rust source files - uses: giancosta86/aurora-github/actions/verify-rust-crate@v10.3.0 + uses: giancosta86/aurora-github/actions/verify-rust-crate@v11.0.0 with: run-clippy-checks: ${{ inputs.run-clippy-checks }} check-rustdoc: ${{ inputs.check-rustdoc }} @@ -69,7 +69,7 @@ runs: wasm-pack:run-browser-tests - name: Generate the wasm target - uses: giancosta86/aurora-github/actions/generate-wasm-target@v10.3.0 + uses: giancosta86/aurora-github/actions/generate-wasm-target@v11.0.0 with: target: ${{ inputs.wasm-target }} npm-scope: ${{ inputs.npm-scope }} @@ -78,7 +78,7 @@ runs: project-directory: ${{ inputs.project-directory }} - name: Run client tests - uses: giancosta86/aurora-github/actions/run-custom-tests@v10.3.0 + uses: giancosta86/aurora-github/actions/run-custom-tests@v11.0.0 with: optional: true root-directory: ${{ inputs.project-directory }}/${{ inputs.client-tests-directory }} From 4c274a1a839f83799f040bfc54943cac0fc298bf Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Tue, 20 May 2025 20:46:40 +0200 Subject: [PATCH 42/63] Simplify example snippets for various actions --- actions/detect-branch-version/README.md | 9 --------- actions/publish-github-pages/README.md | 2 -- actions/publish-jvm-project/README.md | 2 -- actions/publish-npm-package/README.md | 2 -- actions/publish-python-package/README.md | 2 -- actions/publish-rust-crate/README.md | 2 -- actions/publish-rust-wasm/README.md | 2 -- actions/setup-elvish-context/README.md | 2 -- actions/setup-nodejs-context/README.md | 2 -- actions/verify-jvm-project/README.md | 2 -- actions/verify-npm-package/README.md | 2 -- actions/verify-python-package/README.md | 2 -- actions/verify-rust-crate/README.md | 2 -- actions/verify-rust-wasm/README.md | 2 -- 14 files changed, 35 deletions(-) diff --git a/actions/detect-branch-version/README.md b/actions/detect-branch-version/README.md index 9ff55d0c4..adc98e817 100644 --- a/actions/detect-branch-version/README.md +++ b/actions/detect-branch-version/README.md @@ -8,15 +8,6 @@ Extracts the version from the name of the current **Git** branch, returning a va steps: - id: detector uses: giancosta86/aurora-github/actions/detect-branch-version@v11 - - - run: | - branch="${{ steps.detector.outputs.branch }}" - version="${{ steps.detector.outputs.version }}" - escapedVersion="${{ steps.detector.outputs.escaped-version }}" - major="${{ steps.detector.outputs.major }}" - - echo "🔎Detected version '$version' (escaped: '${escapedVersion}') from branch '$branch'" - echo "🔎Major component: '${major}'" ``` ## ☑️ Requirements diff --git a/actions/publish-github-pages/README.md b/actions/publish-github-pages/README.md index 1ceb58ab7..b98040ee6 100644 --- a/actions/publish-github-pages/README.md +++ b/actions/publish-github-pages/README.md @@ -6,8 +6,6 @@ Publishes a directory as the [GitHub Pages](https://pages.github.com/) website f ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-github-pages@v11 ``` diff --git a/actions/publish-jvm-project/README.md b/actions/publish-jvm-project/README.md index b84cd1259..6ce0629ed 100644 --- a/actions/publish-jvm-project/README.md +++ b/actions/publish-jvm-project/README.md @@ -6,8 +6,6 @@ Publishes a project for the **Java Virtual Machine** - using **Maven** or **Grad ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-jvm-project@v11 with: auth-user: userOnTheServer diff --git a/actions/publish-npm-package/README.md b/actions/publish-npm-package/README.md index 3c7e0d1d6..1d4dbed89 100644 --- a/actions/publish-npm-package/README.md +++ b/actions/publish-npm-package/README.md @@ -6,8 +6,6 @@ Publishes a **NodeJS** package to an [npm](https://www.npmjs.com/) registry. ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-npm-package@v11 with: npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/actions/publish-python-package/README.md b/actions/publish-python-package/README.md index e71c294ee..999f92685 100644 --- a/actions/publish-python-package/README.md +++ b/actions/publish-python-package/README.md @@ -6,8 +6,6 @@ Publishes a **Python** package using [PDM](https://pdm-project.org). ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-python-package@v11 with: index-user: __token__ diff --git a/actions/publish-rust-crate/README.md b/actions/publish-rust-crate/README.md index d01d33f45..cd1478008 100644 --- a/actions/publish-rust-crate/README.md +++ b/actions/publish-rust-crate/README.md @@ -6,8 +6,6 @@ Publishes a **Rust** crate - by default, to [crates.io](https://crates.io/) - wi ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-rust-crate@v11 with: cargo-token: ${{ secrets.CARGO_TOKEN }} diff --git a/actions/publish-rust-wasm/README.md b/actions/publish-rust-wasm/README.md index e3733fe89..1998a2e2b 100644 --- a/actions/publish-rust-wasm/README.md +++ b/actions/publish-rust-wasm/README.md @@ -6,8 +6,6 @@ Publishes a **Rust** web assembly to an [npm](https://www.npmjs.com/) registry. ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/publish-rust-wasm@v11 with: npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/actions/setup-elvish-context/README.md b/actions/setup-elvish-context/README.md index d21540bab..cb0c47862 100644 --- a/actions/setup-elvish-context/README.md +++ b/actions/setup-elvish-context/README.md @@ -6,8 +6,6 @@ Installs the **Elvish** shell, caching it between multiple jobs of the same work ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/setup-elvish-context@v11 ``` diff --git a/actions/setup-nodejs-context/README.md b/actions/setup-nodejs-context/README.md index 0f4bd9267..5165e9fd1 100644 --- a/actions/setup-nodejs-context/README.md +++ b/actions/setup-nodejs-context/README.md @@ -6,8 +6,6 @@ Conditionally installs **NodeJS** along with **pnpm**, as well as the dependenci ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/setup-nodejs-context@v11 ``` diff --git a/actions/verify-jvm-project/README.md b/actions/verify-jvm-project/README.md index 617a3ea5d..eaefc3db5 100644 --- a/actions/verify-jvm-project/README.md +++ b/actions/verify-jvm-project/README.md @@ -6,8 +6,6 @@ Verifies the source files of a project for the **Java Virtual Machine** - using ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-jvm-project@v11 ``` diff --git a/actions/verify-npm-package/README.md b/actions/verify-npm-package/README.md index 52cdd463e..f7c1d6946 100644 --- a/actions/verify-npm-package/README.md +++ b/actions/verify-npm-package/README.md @@ -8,8 +8,6 @@ It is worth noting this action can support any technology - as long as you compl ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-npm-package@v11 ``` diff --git a/actions/verify-python-package/README.md b/actions/verify-python-package/README.md index 3a8488182..ae09b2cb6 100644 --- a/actions/verify-python-package/README.md +++ b/actions/verify-python-package/README.md @@ -6,8 +6,6 @@ Verifies the source files of a **Python** package using [PDM](https://pdm-projec ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-python-package@v11 ``` diff --git a/actions/verify-rust-crate/README.md b/actions/verify-rust-crate/README.md index 74a41aba3..81644d5cd 100644 --- a/actions/verify-rust-crate/README.md +++ b/actions/verify-rust-crate/README.md @@ -6,8 +6,6 @@ Verifies the source files of a **Rust** crate. ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-rust-crate@v11 ``` diff --git a/actions/verify-rust-wasm/README.md b/actions/verify-rust-wasm/README.md index 0660bfd70..9aa208ada 100644 --- a/actions/verify-rust-wasm/README.md +++ b/actions/verify-rust-wasm/README.md @@ -6,8 +6,6 @@ Verifies the source files of a **Rust** web assembly. ```yaml steps: - - uses: actions/checkout@v4 - - uses: giancosta86/aurora-github/actions/verify-rust-wasm@v11 with: wasm-pack-version: 0.13.1 From 02e1972ca82899df4fc5a8afc581076c3de2486f Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Wed, 23 Apr 2025 19:23:03 +0200 Subject: [PATCH 43/63] Introduce a logo in lieu of the action diagram --- docs/schema.drawio | 100 --------------------------------------------- docs/schema.png | Bin 193360 -> 0 bytes logo.jpg | Bin 0 -> 106443 bytes 3 files changed, 100 deletions(-) delete mode 100644 docs/schema.drawio delete mode 100644 docs/schema.png create mode 100644 logo.jpg diff --git a/docs/schema.drawio b/docs/schema.drawio deleted file mode 100644 index 143be6292..000000000 --- a/docs/schema.drawio +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/schema.png b/docs/schema.png deleted file mode 100644 index af54a144d08664eae41ca587c22db82fb71ff2cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 193360 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 z Date: Wed, 23 Apr 2025 19:07:57 +0200 Subject: [PATCH 44/63] Update README --- README.md | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 5e84da5b8..604702117 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ # aurora-github -_Elegant workflows for GitHub Actions_ +_Elegant CI/CD for GitHub Actions_ -![Schema](docs/schema.png) +![Logo](logo.jpg) -**aurora-github** is a gallery of GitHub actions designed to create elegant and minimalist _pipelines_ for a variety of technologies - focusing on best practices such as _default branch protection_, _pull requests_ and _convention-over-configuration_. +**aurora-github** is a gallery of **GitHub actions** - based on the superlative [Elvish](https://elv.sh/) shell - designed to create _elegant_ and _minimalist_ **workflows** for a variety of technologies, while focusing on best practices such as _default branch protection_, _pull requests_ and _convention-over-configuration_. -In particular, for most actions **it is essential to name branches according to semantic versioning** - like `v4.2.7`: this ensures a smooth workflow while remaning largely compatible with other flows - for example, multiple _feature branches_ can stem from a given _version branch_. +For most of the actions, **it is essential to name branches according to semantic versioning** - like `v4.2.7`: this ensures a smooth workflow while remaining largely compatible with other flow architectures - for example, multiple _feature branches_ can stem from a given _version branch_. The actions can be grouped by technology: -## 🔮Elvish shell +## 🔮 Elvish shell - [setup-elvish-context](actions/setup-elvish-context/README.md) -## 🦀Rust +- [install-elvish-package](actions/install-elvish-package/README.md) + +- [verify-elvish-package](actions/verify-elvish-package/README.md) + +## 🦀 Rust - [setup-rust-context](actions/setup-rust-context/README.md) @@ -22,11 +26,11 @@ The actions can be grouped by technology: - [publish-rust-crate](actions/publish-rust-crate/README.md) -- [check-rust-versions](actions/check-rust-versions/README.md) - - [extract-rust-snippets](actions/extract-rust-snippets/README.md) -## 📦NodeJS package +## 📦 NodeJS + +- [setup-nodejs-context](actions/setup-nodejs-context/README.md) - [verify-npm-package](actions/verify-npm-package/README.md) @@ -36,11 +40,9 @@ The actions can be grouped by technology: - [check-subpath-exports](actions/check-subpath-exports/README.md) -- [setup-nodejs-context](actions/setup-nodejs-context/README.md) - - [parse-npm-scope](actions/parse-npm-scope/README.md) -## 🦀Rust 🌐WebAssembly +## 🦀🌐 Rust wasm-pack - [verify-rust-wasm](actions/verify-rust-wasm/README.md) @@ -50,7 +52,7 @@ The actions can be grouped by technology: - [generate-wasm-target](actions/generate-wasm-target/README.md) -## ☕Java ecosystem +## ☕ Java Virtual Machine ecosystem - [verify-jvm-project](actions/verify-jvm-project/README.md) @@ -58,13 +60,13 @@ The actions can be grouped by technology: - [install-via-sdkman](actions/install-via-sdkman/README.md) -## 🐍Python +## 🐍 Python - [verify-python-package](actions/verify-python-package/README.md) - [publish-python-package](actions/publish-python-package/README.md) -## 😺GitHub +## 😺 GitHub - [check-action-references](actions/check-action-references/README.md) @@ -74,7 +76,9 @@ The actions can be grouped by technology: - [check-project-license](actions/check-project-license/README.md) -## 🏷️Semantic versioning +- [check-required-jobs](actions/check-required-jobs/README.md) + +## 🏷️ Semantic versioning - [detect-branch-version](actions/detect-branch-version/README.md) @@ -84,12 +88,20 @@ The actions can be grouped by technology: - [upload-release-assets](actions/upload-release-assets/README.md) -## 🧰General-purpose utilities +## 🖥 Operating-system utilities + +- [run-shell-script](actions/run-shell-script/README.md) - [find-critical-todos](actions/find-critical-todos/README.md) - [install-system-packages](actions/install-system-packages/README.md) -## 🌐Further references +## 🌐 Further references - [GitHub actions](https://docs.github.com/en/actions) + +- [Elvish](https://elv.sh/) - Powerful modern shell scripting + +- [Google Gemini](https://gemini.google.com) - used to generate the logo + +- [GIMP](https://www.gimp.org/) - used to manually retouch the logo From 32b90b197c45ab8d99808dea87f552bf0d6639c7 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 14 Jun 2025 18:55:07 +0200 Subject: [PATCH 45/63] Update GitHub workflows --- .github/workflows/publish.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b01c1e16f..5d239abef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,5 +13,3 @@ jobs: with: set-major-tag: true notes-file-processor: .github/process-release-notes.sh - git-strategy: rebase - draft-release: true From 84067f2cbe276c264f7b43d802a540ee2343491b Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 21 Jun 2025 13:15:56 +0200 Subject: [PATCH 46/63] CORE --- core/action-references.elv | 2 +- core/ci-cd/boot.elv | 111 ------------------------- core/custom-tests.elv | 19 +---- core/elvish-library.elv | 28 +++++++ core/highlighting.elv | 19 +++++ core/jvm/maven/settings.elv | 7 +- core/nodejs/context.elv | 34 +------- core/nodejs/pnpm.elv | 2 +- core/nodejs/subpath-exports/check.elv | 8 +- core/project/descriptors/gradle.elv | 2 +- core/project/descriptors/json.elv | 2 +- core/project/descriptors/toml.elv | 2 +- core/project/descriptors/xml.elv | 2 +- core/rust/wasm.elv | 3 +- core/script.elv | 78 +++++++++++++++++ core/tag-and-release/release-notes.elv | 9 +- core/tag-and-release/release.elv | 18 ++-- 17 files changed, 157 insertions(+), 189 deletions(-) delete mode 100644 core/ci-cd/boot.elv create mode 100644 core/elvish-library.elv create mode 100644 core/highlighting.elv create mode 100644 core/script.elv diff --git a/core/action-references.elv b/core/action-references.elv index fe8b8cab5..9ffc3ed13 100644 --- a/core/action-references.elv +++ b/core/action-references.elv @@ -9,7 +9,7 @@ fn check { var regex = (action-references:get-regex-for-references-to-other-branches $branch) var grep-result = ?( - grep --color=always --perl-regexp $regex **/*.yml > &2 + grep --color=always --perl-regexp $regex **.yml > &2 ) if $grep-result { diff --git a/core/ci-cd/boot.elv b/core/ci-cd/boot.elv deleted file mode 100644 index b7d3dc972..000000000 --- a/core/ci-cd/boot.elv +++ /dev/null @@ -1,111 +0,0 @@ -use epm -use str - -var -required-packages = [ - github.com/giancosta86/aurora-elvish -] - -var -aurora-elvish-version = 'v1' - -fn -tracer-action { |block| - if (has-env AURORA_GITHUB_TRACING_ENABLED) { - $block > &2 - } -} - -fn trace-echo { |@values| - -tracer-action { - echo $@values - } -} - -fn trace-inspect { |&emoji=🔎 description value| - -tracer-action { - print $emoji $description': ' - pprint $value - } -} - -fn -get-sha { |value| - to-string $value | - sha256sum | - str:split ' ' (all) | - take 1 -} - -fn -to-csv { |items| - str:join , $items -} - -fn -split-csv { |source| - str:split , $source | - each $str:trim-space~ | - keep-if { |value| !=s $value '' } -} - -fn confirm-aurora-github { - trace-echo 🔮 Now booting aurora-github for Elvish! -} - -fn set-epm-vars { |inputs| - trace-inspect &emoji=📥 Inputs $inputs - - var workflow = $inputs[workflow] - var run-number = $inputs[run-number] - var packages = [(-split-csv $inputs[packages])] - - var actual-packages = [$@-required-packages $@packages] - - var csv-packages = (-to-csv $actual-packages) - trace-inspect 'Comma-separated packages' $csv-packages - - var packages-sha = (-get-sha $csv-packages) - trace-inspect 'Packages SHA' $packages-sha - - var epm-cache-key = $workflow'-'$run-number'-'$packages-sha - trace-inspect 'EPM cache key' $epm-cache-key - - trace-inspect 'EPM managed directory' $epm:managed-dir - - { - echo csv-packages=$csv-packages - echo epm-cache-key=$epm-cache-key - echo epm-managed-dir=$epm:managed-dir - } >> (get-env GITHUB_ENV) -} - -fn -checkout-aurora-elvish-version { - tmp pwd = $epm:managed-dir/github.com/giancosta86/aurora-elvish - - git checkout -q $-aurora-elvish-version - - use github.com/giancosta86/aurora-elvish/console - - -tracer-action { - console:echo 🔮 aurora-elvish ready! - } -} - -fn install-packages { |csv-packages| - var packages = [(-split-csv $csv-packages)] - - trace-echo 📚 Packages to install: $packages - - for package $packages { - epm:install $package - } - - -checkout-aurora-elvish-version - - trace-echo 🚀 Startup packages for Elvish installed! -} - -fn list-packages { - trace-echo 📚 Elvish startup packages - - epm:installed | each { |pkg| - trace-echo '*' $pkg - } - - trace-echo 📚📚📚 -} \ No newline at end of file diff --git a/core/custom-tests.elv b/core/custom-tests.elv index 4307f964a..eff11a976 100644 --- a/core/custom-tests.elv +++ b/core/custom-tests.elv @@ -1,8 +1,9 @@ use os -use ./ci-cd/env use github.com/giancosta86/aurora-elvish/console -use github.com/giancosta86/aurora-elvish/script use github.com/giancosta86/aurora-elvish/testing +use ./ci-cd/env +use ./script + fn -set-strategy { |strategy| console:inspect &emoji=💡 'Current test strategy' $strategy @@ -90,17 +91,3 @@ fn detect-strategy { |inputs| fail 'Cannot run mandatory custom tests: no supported test strategy could be detected!' } } - -fn execute-test-runner { - var testing-result = (testing:run) - - if $testing-result[is-ok] { - console:echo ✅ All the $testing-result[total-tests] tests are OK! - } else { - console:block &emoji=❌ $testing-result[total-failed]' tests failed' { - pprint $testing-result - } - - exit 1 - } -} \ No newline at end of file diff --git a/core/elvish-library.elv b/core/elvish-library.elv new file mode 100644 index 000000000..3da35abd4 --- /dev/null +++ b/core/elvish-library.elv @@ -0,0 +1,28 @@ +use os +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/string + +var -metadata-file = metadata.json + +var -required-fields = [ + description + maintainers + homepage + dependencies +] + +fn check-metadata { + if (not (os:is-regular $-metadata-file)) { + fail 'Missing library metadata file: '$-metadata-file + } + + var metadata = (from-json < $-metadata-file) + + all $-required-fields | each { |field| + if (not (has-key $metadata $field)) { + fail 'Missing required field in '$-metadata-file': '$field + } + } + + console:echo ✅ Library metadata OK! +} \ No newline at end of file diff --git a/core/highlighting.elv b/core/highlighting.elv new file mode 100644 index 000000000..3c52e260e --- /dev/null +++ b/core/highlighting.elv @@ -0,0 +1,19 @@ +fn -run-or-cat { |@command-line| + var command = $command-line[0] + + if (has-external $command) { + var args = $command-line[1..] + + (external $command) $@args + } else { + cat + } +} + +fn highlight { |format| + if (==s $format json) { + -run-or-cat jq -C + } else { + -run-or-cat pygmentize -l $format + } +} \ No newline at end of file diff --git a/core/jvm/maven/settings.elv b/core/jvm/maven/settings.elv index 6e619539e..5e992ab05 100644 --- a/core/jvm/maven/settings.elv +++ b/core/jvm/maven/settings.elv @@ -2,8 +2,9 @@ use os use path use str use github.com/giancosta86/aurora-elvish/console -use github.com/giancosta86/aurora-elvish/highlighting +use github.com/giancosta86/aurora-elvish/fs use github.com/giancosta86/aurora-elvish/resources +use ../../highlighting var -resources = (resources:for-script (src)) @@ -16,7 +17,7 @@ fn -copy-default-settings { var default-settings-path = ($-resources[get-path] $-user-settings-filename) - cp $default-settings-path $-user-settings-path + fs:copy $default-settings-path $-user-settings-path console:section &emoji=🪶 'Content of the per-user Maven settings file' { cat $-user-settings-path | highlighting:highlight xml @@ -46,7 +47,7 @@ fn prepare-for-publication { if (os:is-regular $-user-settings-filename) { console:echo 📃 Maven settings file found in the current directory! Now copying it... - cp $-user-settings-filename $-user-settings-path + fs:copy $-user-settings-filename $-user-settings-path } else { -copy-default-settings diff --git a/core/nodejs/context.elv b/core/nodejs/context.elv index 2d55f308a..465b5cee9 100644 --- a/core/nodejs/context.elv +++ b/core/nodejs/context.elv @@ -2,6 +2,7 @@ use os use str use github.com/giancosta86/aurora-elvish/console use github.com/giancosta86/aurora-elvish/lang +use github.com/giancosta86/aurora-elvish/nvm/node-version use github.com/giancosta86/aurora-elvish/seq use ../ci-cd/env @@ -13,39 +14,8 @@ fn check-preconditions { } } -fn -detect-node-version-from-package-json { - var requested-node-version = (jq -r '.engines.node // ""' package.json) - - if (seq:is-non-empty $requested-node-version) { - console:inspect 'NodeJS version requested in package.json' $requested-node-version - put $requested-node-version - } else { - console:echo 💭 No 'engines/node' field in package.json... - put $nil - } -} - -fn -detect-node-version-from-nvmrc { - if (os:is-regular .nvmrc) { - var requested-node-version = (slurp < .nvmrc | str:trim-space (all)) - - console:inspect 'Requested version in the .nvmrc file' $requested-node-version - - lang:ternary (seq:is-non-empty $requested-node-version) $requested-node-version $nil - } else { - console:echo 💭 No .nvmrc file... - put $nil - } -} - fn detect-nodejs-constraints { - var requested-node-version = ( - coalesce ( - -detect-node-version-from-nvmrc - ) ( - -detect-node-version-from-package-json - ) - ) + var requested-node-version = (node-version:detect-in-pwd) var install-toolchain diff --git a/core/nodejs/pnpm.elv b/core/nodejs/pnpm.elv index 119312f93..bf5b7883b 100644 --- a/core/nodejs/pnpm.elv +++ b/core/nodejs/pnpm.elv @@ -1,8 +1,8 @@ use os use str use github.com/giancosta86/aurora-elvish/console -use github.com/giancosta86/aurora-elvish/highlighting use github.com/giancosta86/aurora-elvish/map +use ../highlighting fn parse-scope { |declared-scope| if (==s $declared-scope '') { diff --git a/core/nodejs/subpath-exports/check.elv b/core/nodejs/subpath-exports/check.elv index cb906e6e4..5753a12cc 100644 --- a/core/nodejs/subpath-exports/check.elv +++ b/core/nodejs/subpath-exports/check.elv @@ -3,11 +3,11 @@ use github.com/giancosta86/aurora-elvish/console use github.com/giancosta86/aurora-elvish/lang use github.com/giancosta86/aurora-elvish/map -var -recursive-check-json-value +var -check-json-value~ fn -check-json-object { |path-in-json json-object| keys $json-object | order | each { |key| - $-recursive-check-json-value $path-in-json'->'$key $json-object[$key] + -check-json-value $path-in-json'->'$key $json-object[$key] } } @@ -24,7 +24,7 @@ fn -check-file-pattern { |path-in-json file-pattern| } } -fn -check-json-value { |path-in-json json-value| +set -check-json-value~ = { |path-in-json json-value| var checker = ( lang:ternary (==s (kind-of $json-value) map) $-check-json-object~ $-check-file-pattern~ ) @@ -32,8 +32,6 @@ fn -check-json-value { |path-in-json json-value| $checker $path-in-json $json-value } -set -recursive-check-json-value = $-check-json-value~ - fn check { if (not (os:is-regular package.json)) { fail 'The package.json descriptor file does not exist!' diff --git a/core/project/descriptors/gradle.elv b/core/project/descriptors/gradle.elv index e1997708f..4c58e3db8 100644 --- a/core/project/descriptors/gradle.elv +++ b/core/project/descriptors/gradle.elv @@ -1,6 +1,6 @@ use path -use github.com/giancosta86/aurora-elvish/highlighting use github.com/giancosta86/aurora-elvish/lang +use ../../highlighting use ./toml var read-version~ = $toml:read-version~ diff --git a/core/project/descriptors/json.elv b/core/project/descriptors/json.elv index 2d4bf34e6..7802a5385 100644 --- a/core/project/descriptors/json.elv +++ b/core/project/descriptors/json.elv @@ -1,4 +1,4 @@ -use github.com/giancosta86/aurora-elvish/highlighting +use ../../highlighting fn read-version { |descriptor-path| put (from-json < $descriptor-path)[version] diff --git a/core/project/descriptors/toml.elv b/core/project/descriptors/toml.elv index 7a7b78323..b61995672 100644 --- a/core/project/descriptors/toml.elv +++ b/core/project/descriptors/toml.elv @@ -1,5 +1,5 @@ use re -use github.com/giancosta86/aurora-elvish/highlighting +use ../../highlighting fn read-version { |descriptor-path| cat $descriptor-path | from-lines | each { |line| diff --git a/core/project/descriptors/xml.elv b/core/project/descriptors/xml.elv index 0d5266624..df6ce0fd1 100644 --- a/core/project/descriptors/xml.elv +++ b/core/project/descriptors/xml.elv @@ -1,6 +1,6 @@ use str -use github.com/giancosta86/aurora-elvish/highlighting use github.com/giancosta86/aurora-elvish/resources +use ../../highlighting var -resources = (resources:for-script (src)) diff --git a/core/rust/wasm.elv b/core/rust/wasm.elv index 1b11fcc0d..faaa090d6 100644 --- a/core/rust/wasm.elv +++ b/core/rust/wasm.elv @@ -1,10 +1,11 @@ use os use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/fs fn copy-npmrc-from-project-directory { if (os:is-regular .npmrc) { console:echo 🎉 Root .npmrc configuration file found! Copying it to the package directory... - cp .npmrc pkg/ + fs:copy .npmrc pkg/ console:echo ✅ .npmrc file copied! } else { console:echo 💭 No .npmrc configuration file found in the project directory... diff --git a/core/script.elv b/core/script.elv new file mode 100644 index 000000000..1d70177d2 --- /dev/null +++ b/core/script.elv @@ -0,0 +1,78 @@ +use os +use path +use github.com/giancosta86/aurora-elvish/console +use github.com/giancosta86/aurora-elvish/map + +var -shells-by-extension = [ + &.elv='elvish' + &.sh='bash' +] + +var -prioritized-script-extensions = [.elv .sh] + +fn get-actual-path { |base-path| + if (os:is-regular $base-path) { + console:inspect &emoji=📃 'Script found at base path' $base-path + put $base-path + return + } + + for extension $-prioritized-script-extensions { + var extended-path = $base-path''$extension + console:inspect 'Looking for script at extended path' $extended-path + + if (os:is-regular $extended-path) { + console:inspect &emoji=📃 'Script found at extended path' $extended-path + put $extended-path + return + } + } + + console:echo 💭 No script found, even after adding extensions... + put $nil +} + +fn -detect-shell { |script-path| + var script-extension = (path:ext $script-path) + + map:get-value $-shells-by-extension $script-extension +} + +fn run { |&working-directory=$nil &optional=$false &shell=$nil script-path @script-args| + tmp pwd = (coalesce $working-directory $pwd) + + console:inspect &emoji=📁 'Current directory for scripting' $pwd + + var actual-script-path = (get-actual-path $script-path) + + if (not $actual-script-path) { + var error-message = "Cannot find a script associated with '"$script-path"'" + + if $optional { + console:echo 💭 $error-message... + return + } else { + fail $error-message + } + } + + console:inspect &emoji=🐚 'Requested shell' $shell + + var actual-shell = (coalesce $shell (-detect-shell $actual-script-path)) + console:inspect &emoji=🐚 'Actual shell' $actual-shell + + if (not $actual-shell) { + var error-message = "Cannot detect a shell for script path: '"$actual-script-path"'" + + if $optional { + console:echo 💭 $error-message... + return + } else { + fail $error-message + } + } + + console:inspect &emoji=📎 'Script args' $script-args + + (external $actual-shell) $actual-script-path $@script-args +} \ No newline at end of file diff --git a/core/tag-and-release/release-notes.elv b/core/tag-and-release/release-notes.elv index 8073c5cf5..69c5711bf 100644 --- a/core/tag-and-release/release-notes.elv +++ b/core/tag-and-release/release-notes.elv @@ -1,4 +1,3 @@ -use file use re use str use github.com/giancosta86/aurora-elvish/console @@ -58,7 +57,7 @@ fn -write-changelog-footer { |pull-request tag| fn generate { |inputs| console:inspect-inputs $inputs - var output-file = $inputs[output-file] + var output-path = $inputs[output-path] var tag = $inputs[tag] var pull-request = $inputs[pull-request] @@ -70,11 +69,9 @@ fn generate { |inputs| -write-pull-request-data $pull-request -write-changelog-footer $pull-request $tag - } >> $output-file - - file:close $output-file + } >> $output-path console:section &emoji=📝 'Generated release notes' { - cat $output-file[name] + cat $output-path } } \ No newline at end of file diff --git a/core/tag-and-release/release.elv b/core/tag-and-release/release.elv index c2e23ddd7..f8929f323 100644 --- a/core/tag-and-release/release.elv +++ b/core/tag-and-release/release.elv @@ -1,8 +1,8 @@ -use os use path use github.com/giancosta86/aurora-elvish/console -use github.com/giancosta86/aurora-elvish/script +use github.com/giancosta86/aurora-elvish/fs use ../ci-cd/repository +use ../script use ./release-notes fn -get-release-title { |version| @@ -27,13 +27,13 @@ fn create { |inputs| var release-title = (-get-release-title $version) console:inspect &emoji=🔎 'Release title' $release-title - var release-notes-file = (os:temp-file) + var release-notes-path = (fs:temp-file-path) defer { - os:remove $release-notes-file[name] + fs:rimraf release-notes-path } release-notes:generate [ - &output-file=$release-notes-file + &output-path=$release-notes-path &tag=$tag &pull-request=$pull-request ] @@ -41,10 +41,10 @@ fn create { |inputs| if $notes-file-processor { console:inspect &emoji=🖋 'Release notes file processor' $notes-file-processor - script:run $notes-file-processor $release-notes-file[name] + script:run $notes-file-processor $release-notes-path console:section &emoji=🎀 'Processed release notes' { - cat $release-notes-file[name] + cat $release-notes-path } } else { console:echo 💭 No release notes file processor... @@ -54,7 +54,7 @@ fn create { |inputs| console:inspect &emoji=📝 'Drafting release' $release-title if (not $dry-run) { - gh release create $tag --title $release-title --latest --notes-file $release-notes-file[name] --draft + gh release create $tag --title $release-title --latest --notes-file $release-notes-path --draft console:echo 📝 Release drafted! } else { @@ -64,7 +64,7 @@ fn create { |inputs| console:inspect &emoji=🌟 'Publishing release' $release-title if (not $dry-run) { - gh release create $tag --title $release-title --latest --notes-file $release-notes-file[name] + gh release create $tag --title $release-title --latest --notes-file $release-notes-path console:echo 🌟 Release published! } else { From 649f0bc1173da9849fbae17d691c35e04c06d46d Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 28 Jun 2025 02:00:39 +0200 Subject: [PATCH 47/63] run-shell-script --- actions/run-shell-script/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/run-shell-script/action.yml b/actions/run-shell-script/action.yml index a321e963f..406d0a2d9 100644 --- a/actions/run-shell-script/action.yml +++ b/actions/run-shell-script/action.yml @@ -27,8 +27,8 @@ runs: - name: Run the script shell: elvish {0} run: | - use github.com/giancosta86/aurora-elvish/script use aurora-github/ci-cd/input + use aurora-github/script var optional = (input:bool optional '${{ inputs.optional }}') From 347de50f773abcdb17be99fc91d946015ddea5c4 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 4 Jul 2025 19:00:32 +0200 Subject: [PATCH 48/63] run-custom-tests --- actions/run-custom-tests/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 1cd5050da..1ca86c25e 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -50,7 +50,7 @@ runs: run: | use github.com/giancosta86/aurora-elvish/testing - var failed-tests = (testing:test &output-failures &clear=$false) + var failed-tests = (testing:test)[stats][total-failed] if (> $failed-tests 0) { exit 1 From 5a51a8101c9c2042f9d2e324a4f42de7be908651 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 19:26:14 +0200 Subject: [PATCH 49/63] DEL THIS --- .github/workflows/verify.yml | 68 ++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index d36d6c8d9..3159c9513 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -9,7 +9,7 @@ on: jobs: test-setup-elvish-context: - if: true + if: false runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -19,7 +19,7 @@ jobs: skip-if-existing: false test-setup-elvish-context-in-subsequent-job: - if: true + if: false runs-on: ubuntu-24.04 needs: test-setup-elvish-context steps: @@ -34,7 +34,7 @@ jobs: skip-if-existing: true test-setup-elvish-package: - if: true + if: ${{ always() }} runs-on: ubuntu-24.04 needs: test-setup-elvish-context-in-subsequent-job steps: @@ -45,7 +45,7 @@ jobs: - uses: ./.github/test-actions/test-setup-elvish-package test-detect-branch-version: - if: true + if: false runs-on: ubuntu-24.04 needs: test-setup-elvish-package steps: @@ -53,7 +53,7 @@ jobs: - uses: ./.github/test-actions/test-detect-branch-version test-check-action-references: - if: true + if: false runs-on: ubuntu-24.04 needs: test-detect-branch-version steps: @@ -61,7 +61,7 @@ jobs: - uses: ./actions/check-action-references test-check-project-license: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -69,7 +69,7 @@ jobs: - uses: ./.github/test-actions/test-check-project-license test-enforce-branch-version: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -77,7 +77,7 @@ jobs: - uses: ./.github/test-actions/test-enforce-branch-version test-find-critical-todos: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -85,7 +85,7 @@ jobs: - uses: ./.github/test-actions/test-find-critical-todos test-install-wasm-pack: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -93,7 +93,7 @@ jobs: - uses: ./.github/test-actions/test-install-wasm-pack test-setup-rust-context: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -101,7 +101,7 @@ jobs: - uses: ./.github/test-actions/test-setup-rust-context test-extract-rust-snippets: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -109,7 +109,7 @@ jobs: - uses: ./.github/test-actions/test-extract-rust-snippets test-parse-npm-scope: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -117,7 +117,7 @@ jobs: - uses: ./.github/test-actions/test-parse-npm-scope test-install-system-packages: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -125,7 +125,7 @@ jobs: - uses: ./.github/test-actions/test-install-system-packages test-inject-subpath-exports: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -133,7 +133,7 @@ jobs: - uses: ./.github/test-actions/test-inject-subpath-exports test-check-subpath-exports: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -141,7 +141,7 @@ jobs: - uses: ./.github/test-actions/test-check-subpath-exports test-setup-nodejs-context: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -149,7 +149,7 @@ jobs: - uses: ./.github/test-actions/test-setup-nodejs-context test-run-shell-script: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -157,7 +157,7 @@ jobs: - uses: ./.github/test-actions/test-run-shell-script test-upload-release-assets: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -165,7 +165,7 @@ jobs: - uses: ./.github/test-actions/test-upload-release-assets test-install-via-sdkman: - if: true + if: false runs-on: ubuntu-24.04 needs: test-check-action-references steps: @@ -173,7 +173,7 @@ jobs: - uses: ./.github/test-actions/test-install-via-sdkman test-verify-python-package: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license @@ -184,7 +184,7 @@ jobs: - uses: ./.github/test-actions/test-verify-python-package test-verify-rust-crate: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license @@ -197,7 +197,7 @@ jobs: - uses: ./.github/test-actions/test-verify-rust-crate test-generate-wasm-target: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-parse-npm-scope @@ -207,7 +207,7 @@ jobs: - uses: ./.github/test-actions/test-generate-wasm-target test-publish-github-pages: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-setup-nodejs-context @@ -220,7 +220,7 @@ jobs: - uses: ./.github/test-actions/test-publish-github-pages test-tag-and-release: - if: true + if: false runs-on: ubuntu-24.04 needs: test-run-shell-script steps: @@ -228,7 +228,7 @@ jobs: - uses: ./.github/test-actions/test-tag-and-release test-run-custom-tests: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-run-shell-script @@ -239,7 +239,7 @@ jobs: - uses: ./.github/test-actions/test-run-custom-tests test-verify-jvm-project: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license @@ -251,7 +251,7 @@ jobs: - uses: ./.github/test-actions/test-verify-jvm-project test-publish-rust-crate: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-setup-rust-context @@ -263,7 +263,7 @@ jobs: - uses: ./.github/test-actions/test-publish-rust-crate test-publish-python-package: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-enforce-branch-version @@ -276,7 +276,7 @@ jobs: index-secret: FAKE-SECRET test-verify-npm-package: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license @@ -290,7 +290,7 @@ jobs: - uses: ./.github/test-actions/test-verify-npm-package test-verify-rust-wasm: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license @@ -303,7 +303,7 @@ jobs: - uses: ./.github/test-actions/test-verify-rust-wasm test-publish-jvm-project: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-enforce-branch-version @@ -317,7 +317,7 @@ jobs: jvm-token: FAKE_TOKEN test-publish-npm-package: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-enforce-branch-version @@ -329,7 +329,7 @@ jobs: - uses: ./.github/test-actions/test-publish-npm-package test-publish-rust-wasm: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-install-wasm-pack @@ -341,7 +341,7 @@ jobs: - uses: ./.github/test-actions/test-publish-rust-wasm test-verify-elvish-package: - if: true + if: false runs-on: ubuntu-24.04 needs: - test-check-project-license From cfaa7c08254587a9c06357a6d0eb0a5d592f5de5 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 19:36:31 +0200 Subject: [PATCH 50/63] setup-elvish-package --- .github/test-actions/test-setup-elvish-package/action.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index 610bc2887..a5b6444e7 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -22,11 +22,12 @@ runs: - shell: bash working-directory: ${{ env.elvish-package-path }} run: | - gitRef="$(git describe --tags --always --dirty --broken)" + gitBranch="$(git describe --tags --always --dirty --broken)" if [[ "$gitRef" == 'main' ]] then echo "✅The package is on the main branch, as expected!" else - echo "Current Git reference instead of main: '$gitRef'" + echo "❌Current Git reference instead of main: '$gitRef'" + exit 1 fi From 159b63bf3201ce4d2c9abbac85ea1e9d7b1ae828 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 19:38:20 +0200 Subject: [PATCH 51/63] setup-elvish-package --- .github/test-actions/test-setup-elvish-package/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index a5b6444e7..4203faff4 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -22,7 +22,7 @@ runs: - shell: bash working-directory: ${{ env.elvish-package-path }} run: | - gitBranch="$(git describe --tags --always --dirty --broken)" + gitRef="$(git describe --tags --always --dirty --broken)" if [[ "$gitRef" == 'main' ]] then From 39adf1d3567f66b296eb0eb26e2297a191dc8343 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 19:41:33 +0200 Subject: [PATCH 52/63] setup-elvish-package --- .github/test-actions/test-setup-elvish-package/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index 4203faff4..d79c40e92 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -22,12 +22,12 @@ runs: - shell: bash working-directory: ${{ env.elvish-package-path }} run: | - gitRef="$(git describe --tags --always --dirty --broken)" + gitBranch="$(git branch --show-current)" - if [[ "$gitRef" == 'main' ]] + if [[ "$gitBranch" == 'main' ]] then echo "✅The package is on the main branch, as expected!" else - echo "❌Current Git reference instead of main: '$gitRef'" + echo "❌Current Git branch instead of main: '$gitBranch'" exit 1 fi From ae5c0d807973f8027049f789fe52e7794ce782d9 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 19:44:20 +0200 Subject: [PATCH 53/63] setup-elvish-package --- .../test-setup-elvish-package/action.yml | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index d79c40e92..cdc3695c2 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -3,31 +3,54 @@ name: Test setup-elvish-package runs: using: composite steps: + - name: Setup the environment variables for the test + shell: elvish {0} + run: | + use epm + + var package-path = (epm:metadata 'github.com/giancosta86/velvet')[dst] + + echo elvish-package-path=$package-path >> (get-env GITHUB_ENV) + - shell: bash - run: echo "🎭 Test by installing Velvet from the main branch ..." + run: echo "🎭 Install the Velvet package from the main branch..." - uses: ./actions/setup-elvish-package with: package: github.com/giancosta86/velvet quiet: false - - shell: elvish {0} + - shell: bash + working-directory: ${{ env.elvish-package-path }} run: | - use epm + gitBranch="$(git branch --show-current)" - var package-path = (epm:metadata 'github.com/giancosta86/velvet')[dst] + if [[ "$gitBranch" == 'main' ]] + then + echo "✅The package is on the main branch, as expected!" + else + echo "❌Current Git branch instead of main: '$gitBranch'" + exit 1 + fi - echo elvish-package-path=$package-path >> (get-env GITHUB_ENV) + - shell: bash + run: echo "🎭 Setup the Velvet package to use tag v1..." + + - uses: ./actions/setup-elvish-package + with: + package: github.com/giancosta86/velvet + git-ref: v1 + quiet: false - shell: bash working-directory: ${{ env.elvish-package-path }} run: | gitBranch="$(git branch --show-current)" - if [[ "$gitBranch" == 'main' ]] + if [[ "$gitBranch" == 'v1' ]] then - echo "✅The package is on the main branch, as expected!" + echo "✅The package is on the v1 tag, as expected!" else - echo "❌Current Git branch instead of main: '$gitBranch'" + echo "❌Current Git branch instead of v1: '$gitBranch'" exit 1 fi From b55c94b1f8ddf2ee84e40a53cf3b34da3c3db561 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 21:41:05 +0200 Subject: [PATCH 54/63] setup-elvish-package --- .../test-setup-elvish-package/action.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index cdc3695c2..a25bf3d44 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -12,6 +12,8 @@ runs: echo elvish-package-path=$package-path >> (get-env GITHUB_ENV) + echo expectedTag=v1 >> (get-env GITHUB_ENV) + - shell: bash run: echo "🎭 Install the Velvet package from the main branch..." @@ -34,23 +36,23 @@ runs: fi - shell: bash - run: echo "🎭 Setup the Velvet package to use tag v1..." + run: echo "🎭 Setup the Velvet package to use tag $expectedTag..." - uses: ./actions/setup-elvish-package with: package: github.com/giancosta86/velvet - git-ref: v1 + git-ref: ${{ env.expectedTag }} quiet: false - shell: bash working-directory: ${{ env.elvish-package-path }} run: | - gitBranch="$(git branch --show-current)" + gitTag="$(git describe --tags --always)" - if [[ "$gitBranch" == 'v1' ]] + if [[ "$gitTag" == '$expectedTag' ]] then - echo "✅The package is on the v1 tag, as expected!" + echo "✅The package is on the $expectedTag tag, as expected!" else - echo "❌Current Git branch instead of v1: '$gitBranch'" + echo "❌Current Git tag instead of $expectedTag: '$gitTag'" exit 1 fi From ae836f7b4ed5124b9c45b9b8853a9b6a0e626a96 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 21:51:19 +0200 Subject: [PATCH 55/63] setup-elvish-package --- .github/test-actions/test-setup-elvish-package/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/test-actions/test-setup-elvish-package/action.yml b/.github/test-actions/test-setup-elvish-package/action.yml index a25bf3d44..f0c7e0944 100644 --- a/.github/test-actions/test-setup-elvish-package/action.yml +++ b/.github/test-actions/test-setup-elvish-package/action.yml @@ -49,7 +49,7 @@ runs: run: | gitTag="$(git describe --tags --always)" - if [[ "$gitTag" == '$expectedTag' ]] + if [[ "$gitTag" == "$expectedTag" ]] then echo "✅The package is on the $expectedTag tag, as expected!" else From b7f1e72b6b1438fdbc5256eefe8df05500527a37 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 22:00:47 +0200 Subject: [PATCH 56/63] setup-elvish-library --- actions/setup-elvish-package/action.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/actions/setup-elvish-package/action.yml b/actions/setup-elvish-package/action.yml index 41b4094db..b3184377d 100644 --- a/actions/setup-elvish-package/action.yml +++ b/actions/setup-elvish-package/action.yml @@ -91,6 +91,8 @@ runs: if: inputs.git-ref != '' shell: elvish {0} run: | + use os + var quiet = (==s '${{ inputs.quiet }}' true) if (not $quiet) { @@ -99,9 +101,11 @@ runs: cd (get-env elvish-package-path) - git clean -df + { + git clean -df - git checkout '${{ inputs.git-ref }}' + git checkout '${{ inputs.git-ref }}' + } > $os:dev-null 2> $os:dev-null if (not $quiet) { echo ✅ Switched to Git reference '${{ inputs.git-ref }}' From 2d774445efac6d1ff683a588923e01bb1402ab2b Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Fri, 11 Jul 2025 22:05:14 +0200 Subject: [PATCH 57/63] setup-elvish-package --- actions/setup-elvish-package/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup-elvish-package/action.yml b/actions/setup-elvish-package/action.yml index b3184377d..8d6635b2d 100644 --- a/actions/setup-elvish-package/action.yml +++ b/actions/setup-elvish-package/action.yml @@ -57,7 +57,7 @@ runs: - name: Declare the package is already installed if: env.elvish-package-installed == 'true' && inputs.quiet != 'true' shell: elvish {0} - run: echo 🌟📚Elvish package '${{ inputs.package }}' is already installed! + run: echo 🌟📚Elvish package '${{ inputs.package }}' is already installed! > &2 - name: Restore cached package if: env.elvish-package-installed != 'true' From 21a22ecff57d69981190a312b3b1354e9a70643d Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 11 Oct 2025 12:02:02 +0200 Subject: [PATCH 58/63] run-custom-tests --- actions/run-custom-tests/README.md | 4 +++- actions/run-custom-tests/action.yml | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/actions/run-custom-tests/README.md b/actions/run-custom-tests/README.md index 76befa89a..302b759cc 100644 --- a/actions/run-custom-tests/README.md +++ b/actions/run-custom-tests/README.md @@ -27,7 +27,7 @@ steps: 1. If **verify** (**.elv**, **.sh**, ...) exists in `working-directory` **and** can be run via [run-shell-script](../run-shell-script/README.md), invoke the action accordingly, passing the (optional) `script-shell`. - 1. If one or more files having `.test.elv` extension exist in the root directory, run the tests within them, assuming the test format introduced by [aurora-elvish](https://github.com/giancosta86/aurora-elvish) + 1. If one or more files having `.test.elv` extension exist within the root directory tree, run them using [velvet](https://github.com/giancosta86/velvet) 1. If a file named **package.json** exists in the root directory: @@ -70,6 +70,8 @@ steps: ## 🌐 Further references +- [velvet](https://github.com/giancosta86/velvet) + - [run-shell-script](../run-shell-script/README.md) - [setup-nodejs-context](../setup-nodejs-context/README.md) diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 1ca86c25e..4327bf652 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -43,14 +43,14 @@ runs: shell: ${{ inputs.script-shell }} working-directory: ${{ inputs.root-directory }} - - name: Run test files - if: env.strategy == 'test-runner' + - name: Run tests via Velvet + if: env.strategy == 'velvet' shell: elvish {0} working-directory: ${{ inputs.root-directory }} run: | - use github.com/giancosta86/aurora-elvish/testing + use github.com/giancosta86/velvet/main velvet - var failed-tests = (testing:test)[stats][total-failed] + var failed-tests = (velvet:velvet)[stats][failed] if (> $failed-tests 0) { exit 1 From 9a3fe836553fd4232f04a6a0865e697861620592 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sat, 11 Oct 2025 12:02:45 +0200 Subject: [PATCH 59/63] CORE --- core/custom-tests.elv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/custom-tests.elv b/core/custom-tests.elv index eff11a976..1b1002983 100644 --- a/core/custom-tests.elv +++ b/core/custom-tests.elv @@ -1,6 +1,6 @@ use os use github.com/giancosta86/aurora-elvish/console -use github.com/giancosta86/aurora-elvish/testing +use github.com/giancosta86/velvet/main velvet use ./ci-cd/env use ./script @@ -66,9 +66,9 @@ fn detect-strategy { |inputs| } } - if (testing:has-tests) { + if (velvet:has-test-scripts) { console:echo 📋 aurora-elvish .test.elv files found! - -set-strategy test-runner + -set-strategy velvet return } From a021986ab9736e4db878235c3d453d715d431efb Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 26 Oct 2025 12:33:42 +0100 Subject: [PATCH 60/63] Simplify call to velvet --- actions/run-custom-tests/action.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/actions/run-custom-tests/action.yml b/actions/run-custom-tests/action.yml index 4327bf652..7f1d18c68 100644 --- a/actions/run-custom-tests/action.yml +++ b/actions/run-custom-tests/action.yml @@ -50,11 +50,7 @@ runs: run: | use github.com/giancosta86/velvet/main velvet - var failed-tests = (velvet:velvet)[stats][failed] - - if (> $failed-tests 0) { - exit 1 - } + velvet:velvet &must-pass - name: Setup NodeJS context if: env.strategy == 'nodejs' From 0c878b5a0fca443b97a6c87fbe27cb7f5bf8786e Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Sun, 23 Nov 2025 19:07:50 +0100 Subject: [PATCH 61/63] Include echo --- actions/install-via-sdkman/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/install-via-sdkman/action.yml b/actions/install-via-sdkman/action.yml index eed5d498d..258dad2ec 100644 --- a/actions/install-via-sdkman/action.yml +++ b/actions/install-via-sdkman/action.yml @@ -23,6 +23,7 @@ runs: var version = (input:string version '${{ inputs.version }}') - curl:disable-non-error-output + console:echo 📢 Configuring curl so that it outputs errors only... + curl:display-errors-only sdkman:install-sdk $candidate $version From 52fc67630ac0e7bfa9509cef74f2d25b9d0f2e15 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Mon, 24 Nov 2025 03:10:41 +0100 Subject: [PATCH 62/63] CORE --- core/ci-cd/io.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ci-cd/io.elv b/core/ci-cd/io.elv index cc2504e4f..d9291e605 100644 --- a/core/ci-cd/io.elv +++ b/core/ci-cd/io.elv @@ -22,7 +22,7 @@ fn write { |target-channel key value| fn map { |target-channel source-map| map:entries $source-map | - seq:each-spread { |key value| + seq:spread { |key value| var value-kind = (kind-of $value) if (has-value $-supported-value-kinds $value-kind) { From 2b799a6dc996663dda1abbd014d0c0f6e858a348 Mon Sep 17 00:00:00 2001 From: Gianluca Costa Date: Thu, 29 Jan 2026 04:59:51 +0100 Subject: [PATCH 63/63] CORE --- core/rust/snippets.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rust/snippets.elv b/core/rust/snippets.elv index 18d8720da..43b2bde3a 100644 --- a/core/rust/snippets.elv +++ b/core/rust/snippets.elv @@ -47,7 +47,7 @@ fn -extract-snippets-to-files { |markdown-path test-filename-prefix| echo $updated-snippet > $snippet-path - set generated-test-paths = [$@generated-test-paths $snippet-path] + set generated-test-paths = (conj $generated-test-paths $snippet-path) } if (seq:is-non-empty $generated-test-paths) {