ci: add clean-environment one-shot workflow to verify published Solo package install #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## | |
| # Copyright (C) 2023-2026 Hedera Hashgraph, LLC | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| ## | |
| name: "Clean Environment One-Shot Deploy and Destroy" | |
| # The purpose of this workflow is to validate that Solo can install and run in a clean environment, | |
| # reproducing a first-time user experience (no prior setup, no cached images, no residual state). | |
| # | |
| # What makes this different from the standard-runner one-shot workflow (which builds from source and | |
| # runs `npm run solo` from the repo root): | |
| # | |
| # 1. Clean proof — before installing, we uninstall any prior Solo, remove ~/.solo, and delete | |
| # leftover Kind clusters, then assert that solo/helm/kind/kubectl/crane are absent from PATH and | |
| # ~/.solo does not exist. Docker stays: it is a required prerequisite, not Solo-managed state. | |
| # 2. Faithful package — we build the tarball with the same release packaging code that produces the | |
| # published artifact (`scripts/Taskfile.release.yml` dual-publish tasks), so `npm install -g` | |
| # installs `@hiero-ledger/solo` under its real published name, exercising the path a first-time | |
| # user hits (packaging, bin, postinstall). | |
| # 3. Separate directory — every `solo` command runs from a directory outside the checkout, surfacing | |
| # any code that resolves bundled resources relative to the current working directory instead of | |
| # the package. | |
| # 4. No cached images — SOLO_NO_CACHE skips the postinstall image-cache pull and ENABLE_IMAGE_CACHE | |
| # forces the deploy to pull images fresh. | |
| # | |
| # Note: postinstall only pre-pulls container images; the ~/.solo/bin toolchain (helm/kind/kubectl) is | |
| # downloaded lazily at runtime on first use. | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: | |
| - opened | |
| - reopened | |
| - synchronize | |
| - ready_for_review | |
| defaults: | |
| run: | |
| shell: bash | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| env: | |
| STEP_TIMEOUT_MINUTES: '5' | |
| jobs: | |
| clean-environment-one-shot: | |
| name: Clean Environment Deploy and Destroy | |
| runs-on: hiero-solo-linux-large | |
| timeout-minutes: 40 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout Code | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install Task | |
| uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0 | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| with: | |
| version: 3.49.1 | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Make Clean | |
| # Best-effort teardown of anything a prior job on this reused self-hosted runner left behind. | |
| # Docker Engine is intentionally left installed: it is a required prerequisite for Kind. | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| run: | | |
| npm rm -g @hashgraph/solo @hiero-ledger/solo 2>/dev/null || true | |
| rm -rf ~/.solo | |
| # Remove leftover Kind clusters, targeted by Kind's own container label so we never touch | |
| # unrelated containers on the shared runner. | |
| docker ps -aq --filter "label=io.x-k8s.kind.cluster" | xargs -r docker rm -f || true | |
| docker network rm kind 2>/dev/null || true | |
| - name: Assert Clean Environment | |
| # Demonstrable proof that the environment is clean before we install Solo. Fails loudly if any | |
| # Solo-managed component or residual state is present. | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| run: | | |
| echo "Container runtime (required prerequisite, expected present):" | |
| docker --version | |
| status=0 | |
| for tool in solo helm kind kubectl crane; do | |
| if command -v "${tool}" >/dev/null 2>&1; then | |
| echo "NOT CLEAN: '${tool}' is present on PATH at $(command -v "${tool}")" | |
| status=1 | |
| else | |
| echo "clean: '${tool}' is absent from PATH" | |
| fi | |
| done | |
| if [ -e ~/.solo ]; then | |
| echo "NOT CLEAN: ~/.solo exists (residual toolchain, cache, or state)" | |
| status=1 | |
| else | |
| echo "clean: ~/.solo does not exist" | |
| fi | |
| if [ "${status}" -ne 0 ]; then | |
| echo "ERROR: environment is not clean; see the failures above." | |
| exit 1 | |
| fi | |
| echo "Environment is clean." | |
| - name: Pack Solo as the Published Package | |
| # Use the same release packaging code that produces the published artifact, renaming to the | |
| # @hiero-ledger/solo namespace so `npm install -g` emulates a real user install. `dual-publish:pack` | |
| # runs npm install + task build + npm pack; the build runs after the rename, so dist/package.json | |
| # is renamed too, matching the real artifact. `dual-publish:restore` runs `git restore .`. | |
| timeout-minutes: 10 | |
| run: | | |
| task -t scripts/Taskfile.release.yml dual-publish:rename | |
| task -t scripts/Taskfile.release.yml dual-publish:pack OUTPUT_DIR=output/hl | |
| task -t scripts/Taskfile.release.yml dual-publish:restore | |
| - name: Globally Install Solo | |
| # SOLO_NO_CACHE skips the postinstall image-cache population so no images are cached before deploy. | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| env: | |
| SOLO_NO_CACHE: 'true' | |
| run: | | |
| npm install -g ./output/hl/hiero-ledger-solo-*.tgz | |
| echo "Installed Solo version:" | |
| solo --version | |
| - name: Create Isolated Run Directory | |
| # Every subsequent `solo` command runs from here, outside the repo checkout, to surface any | |
| # code that resolves bundled resources relative to the current working directory. | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| run: mkdir -p "${{ runner.temp }}/solo-clean-run" | |
| - name: One-Shot Single Deploy | |
| timeout-minutes: 20 | |
| working-directory: ${{ runner.temp }}/solo-clean-run | |
| # ENABLE_IMAGE_CACHE=false forces the deploy to pull images fresh instead of using a cache. | |
| env: | |
| ENABLE_IMAGE_CACHE: 'false' | |
| run: | | |
| export PATH=~/.solo/bin:${PATH} | |
| solo one-shot single deploy --dev | |
| - name: Check Port Forwarding Still Works After Deploy | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| working-directory: ${{ runner.temp }}/solo-clean-run | |
| run: | | |
| export PATH=~/.solo/bin:${PATH} | |
| solo deployment diagnostics connections -d one-shot --check | |
| - name: Verify Mirror REST API | |
| timeout-minutes: 10 | |
| run: | | |
| echo "Checking mirror node REST API at http://localhost:38081/api/v1/accounts ..." | |
| for i in $(seq 1 30); do | |
| response=$(curl -sf http://localhost:38081/api/v1/accounts 2>/dev/null || true) | |
| if echo "${response}" | grep -q '"accounts"'; then | |
| echo "Mirror REST API is up and returned accounts." | |
| exit 0 | |
| fi | |
| echo "Attempt ${i}/30: not ready yet, retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "ERROR: Mirror REST API did not become available after 5 minutes." | |
| exit 1 | |
| - name: Collect Diagnostics on Failure | |
| if: ${{ failure() }} | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| working-directory: ${{ runner.temp }}/solo-clean-run | |
| run: | | |
| export PATH=~/.solo/bin:${PATH} | |
| solo deployment diagnostics logs -q --dev || true | |
| - name: One-Shot Single Destroy | |
| if: always() | |
| timeout-minutes: 15 | |
| working-directory: ${{ runner.temp }}/solo-clean-run | |
| run: | | |
| export PATH=~/.solo/bin:${PATH} | |
| solo one-shot single destroy --quiet-mode --dev || true | |
| - name: Upload Logs | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| timeout-minutes: ${{ fromJSON(env.STEP_TIMEOUT_MINUTES) }} | |
| with: | |
| name: clean-environment-one-shot-logs | |
| path: ~/.solo/logs/* | |
| overwrite: true | |
| if-no-files-found: warn |