Skip to content

ci: add clean-environment one-shot workflow to verify published Solo package install #2

ci: add clean-environment one-shot workflow to verify published Solo package install

ci: add clean-environment one-shot workflow to verify published Solo package install #2

##
# 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