Release workflow guardrails (related to #695)

This commit is contained in:
rcourtman
2025-11-11 22:34:00 +00:00
parent cc595da28b
commit 8a5be2c0da
5 changed files with 348 additions and 101 deletions

View File

@@ -0,0 +1,100 @@
name: Promote Floating Tags
on:
release:
types: [published]
jobs:
promote-images:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
OWNER: ${{ github.repository_owner }}
TAG: ${{ github.event.release.tag_name }}
PRERELEASE: ${{ github.event.release.prerelease }}
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Promote Pulse server image tags
run: |
set -euo pipefail
TAG="${TAG}"
VERSION="${TAG#v}"
BASE_VERSION="${VERSION%%-*}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
MINOR=${MINOR:-0}
MAJOR_MINOR="$MAJOR.$MINOR"
if [ "$PRERELEASE" = "true" ]; then
echo "Promoting prerelease tags for ${TAG}"
docker buildx imagetools create \
-t rcourtman/pulse:rc \
rcourtman/pulse:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse:rc \
ghcr.io/${OWNER}/pulse:${TAG}
else
echo "Promoting stable tags for ${TAG}"
docker buildx imagetools create \
-t rcourtman/pulse:latest \
-t rcourtman/pulse:${MAJOR_MINOR} \
-t rcourtman/pulse:${MAJOR} \
rcourtman/pulse:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse:latest \
-t ghcr.io/${OWNER}/pulse:${MAJOR_MINOR} \
-t ghcr.io/${OWNER}/pulse:${MAJOR} \
ghcr.io/${OWNER}/pulse:${TAG}
fi
- name: Promote Docker agent image tags
run: |
set -euo pipefail
TAG="${TAG}"
VERSION="${TAG#v}"
BASE_VERSION="${VERSION%%-*}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
MINOR=${MINOR:-0}
MAJOR_MINOR="$MAJOR.$MINOR"
if [ "$PRERELEASE" = "true" ]; then
docker buildx imagetools create \
-t rcourtman/pulse-docker-agent:rc \
rcourtman/pulse-docker-agent:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse-docker-agent:rc \
ghcr.io/${OWNER}/pulse-docker-agent:${TAG}
else
docker buildx imagetools create \
-t rcourtman/pulse-docker-agent:latest \
-t rcourtman/pulse-docker-agent:${MAJOR_MINOR} \
-t rcourtman/pulse-docker-agent:${MAJOR} \
rcourtman/pulse-docker-agent:${TAG}
docker buildx imagetools create \
-t ghcr.io/${OWNER}/pulse-docker-agent:latest \
-t ghcr.io/${OWNER}/pulse-docker-agent:${MAJOR_MINOR} \
-t ghcr.io/${OWNER}/pulse-docker-agent:${MAJOR} \
ghcr.io/${OWNER}/pulse-docker-agent:${TAG}
fi
- name: Promotion summary
run: |
if [ "$PRERELEASE" = "true" ]; then
echo "Updated rc tags to point to ${TAG} for both server and agent images."
else
echo "Updated latest/${MAJOR_MINOR}/${MAJOR} tags to point to ${TAG} for both server and agent images."
fi

View File

@@ -9,7 +9,100 @@ on:
type: string
jobs:
version_guard:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure VERSION matches requested release
run: |
FILE_VERSION=$(cat VERSION | tr -d '\n')
REQUESTED_VERSION="${{ inputs.version }}"
if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then
echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)."
echo "Update the VERSION file and commit the change before running the release workflow."
exit 1
fi
echo "VERSION file matches requested release ($REQUESTED_VERSION)."
preflight-tests:
needs: version_guard
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install frontend dependencies
run: npm --prefix frontend-modern ci
- name: Build frontend bundle for Go embed
run: |
npm --prefix frontend-modern run build
rm -rf internal/api/frontend-modern
mkdir -p internal/api/frontend-modern
cp -r frontend-modern/dist internal/api/frontend-modern/
- name: Lint frontend
run: npm --prefix frontend-modern run lint
- name: Install docker-compose
run: |
sudo apt-get update
sudo apt-get install -y docker-compose
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Run backend tests
run: go test ./...
- name: Prepare integration test dependencies
working-directory: tests/integration
run: |
npm ci
npx playwright install --with-deps chromium
- name: Build Pulse for integration tests
run: make build
- name: Build Docker images for integration tests
run: |
docker build -t pulse-mock-github:test tests/integration/mock-github-server
docker build -t pulse:test -f Dockerfile .
- name: Run update integration smoke tests
working-directory: tests/integration
env:
MOCK_CHECKSUM_ERROR: "false"
MOCK_NETWORK_ERROR: "false"
MOCK_RATE_LIMIT: "false"
MOCK_STALE_RELEASE: "false"
run: |
docker-compose -f docker-compose.test.yml up -d
# Allow services to settle before running Playwright
sleep 20
npx playwright test tests/01-happy-path.spec.ts tests/02-bad-checksums.spec.ts --reporter=list
docker-compose -f docker-compose.test.yml down -v
- name: Cleanup integration environment
if: always()
working-directory: tests/integration
run: docker-compose -f docker-compose.test.yml down -v || true
build-docker-images:
needs:
- version_guard
- preflight-tests
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
@@ -57,10 +150,10 @@ jobs:
push: true
provenance: false
tags: |
rcourtman/pulse:latest
rcourtman/pulse:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:latest
rcourtman/pulse:${{ inputs.version }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ inputs.version }}
labels: |
org.opencontainers.image.title=Pulse
org.opencontainers.image.description=Proxmox monitoring system
@@ -81,10 +174,10 @@ jobs:
push: true
provenance: false
tags: |
rcourtman/pulse-docker-agent:latest
rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:latest
rcourtman/pulse-docker-agent:${{ inputs.version }}
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ inputs.version }}
labels: |
org.opencontainers.image.title=Pulse Docker Agent
org.opencontainers.image.description=Docker container monitoring agent for Pulse
@@ -101,11 +194,11 @@ jobs:
echo ""
echo "Server images:"
echo " - rcourtman/pulse:${{ steps.set_version.outputs.tag }}"
echo " - rcourtman/pulse:latest"
echo " - rcourtman/pulse:${{ inputs.version }}"
echo ""
echo "Agent images:"
echo " - rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }}"
echo " - rcourtman/pulse-docker-agent:latest"
echo " - rcourtman/pulse-docker-agent:${{ inputs.version }}"
create-release:
needs: build-docker-images
@@ -113,6 +206,11 @@ jobs:
timeout-minutes: 30
permissions:
contents: write
outputs:
tag: ${{ steps.create_release.outputs.tag }}
release_id: ${{ steps.create_release.outputs.release_id }}
release_url: ${{ steps.create_release.outputs.release_url }}
target_commitish: ${{ steps.commit_metadata.outputs.commitish }}
steps:
- name: Checkout repository
@@ -123,6 +221,10 @@ jobs:
- name: Fetch all tags
run: git fetch --tags --force
- name: Capture commit metadata
id: commit_metadata
run: echo "commitish=$GITHUB_SHA" >> $GITHUB_OUTPUT
- name: Set up Go
uses: actions/setup-go@v5
with:
@@ -147,20 +249,6 @@ jobs:
echo "Building release v${{ inputs.version }}..."
./scripts/build-release.sh ${{ inputs.version }}
- name: Validate release artifacts
run: |
echo "Validating release v${{ inputs.version }}..."
# Note: Docker image validation is skipped in this workflow
# since we're only validating the built artifacts (tarballs, binaries, checksums)
# Docker images were built and validated in previous job
# Run validation with --skip-docker flag
./scripts/validate-release.sh ${{ inputs.version }} --skip-docker || {
echo "❌ Release validation failed!"
echo "The workflow will now fail. No release will be created."
exit 1
}
- name: Generate release notes
id: generate_notes
env:
@@ -233,6 +321,7 @@ jobs:
rm -f "$NOTES_FILE"
echo "release_url=$(gh release view ${TAG} --json url -q .url)" >> $GITHUB_OUTPUT
echo "release_id=$(gh release view ${TAG} --json id -q .id)" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Upload checksums.txt
@@ -287,7 +376,18 @@ jobs:
echo "2. Update the release notes with changes since last release"
echo "3. Publish the release manually when ready"
echo ""
echo "All artifacts have been uploaded and validated."
echo "All artifacts have been uploaded."
echo "Docker images are available at Docker Hub and GHCR."
echo "checksums.txt matches all uploaded binaries."
echo "Automated validation will now verify the release assets."
echo ""
validate-release-assets:
needs: create-release
uses: ./.github/workflows/validate-release-assets.yml
secrets: inherit
with:
tag: ${{ needs.create-release.outputs.tag }}
version: ${{ inputs.version }}
release_id: ${{ needs.create-release.outputs.release_id }}
draft: true
target_commitish: ${{ needs.create-release.outputs.target_commitish }}

View File

@@ -1,48 +1,127 @@
name: Validate Release Assets
on:
workflow_run:
workflows: ["Release"]
types: [completed]
workflow_call:
inputs:
tag:
description: 'Release tag (e.g., v4.29.0)'
required: true
type: string
version:
description: 'Version number without v prefix (e.g., 4.29.0)'
required: true
type: string
release_id:
description: 'GitHub release ID'
required: true
type: string
draft:
description: 'Whether the release is still a draft'
required: true
type: boolean
target_commitish:
description: 'Commit SHA associated with the release'
required: true
type: string
release:
types: [edited] # Still validate on manual edits
types: [edited]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v4.29.0)'
required: true
type: string
version:
description: 'Version number without v prefix (e.g., 4.29.0)'
required: true
type: string
release_id:
description: 'GitHub release ID'
required: true
type: string
draft:
description: 'Set to true to run against a draft release'
required: true
type: boolean
target_commitish:
description: 'Commit SHA associated with the release'
required: true
type: string
jobs:
validate:
# Only run for draft releases to act as a safety gate before publishing
if: github.event.release.draft == true
runs-on: ubuntu-latest
permissions:
contents: write # Needed to delete assets and update release
issues: write # Needed to add labels and comments
contents: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract version from tag
id: version
- name: Determine release context
id: context
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
INPUT_DRAFT: ${{ inputs.draft }}
INPUT_COMMIT: ${{ inputs.target_commitish }}
run: |
TAG="${{ github.event.release.tag_name }}"
# Remove 'v' prefix if present
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Validating release: $TAG (version: $VERSION)"
python <<'PY' > context.env
import json, os, sys
event_name = os.environ.get("EVENT_NAME", "")
result = {}
if event_name == "release":
with open(os.environ["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as handle:
data = json.load(handle)
release = data.get("release") or {}
result["tag"] = release.get("tag_name", "")
tag = result["tag"]
result["version"] = tag[1:] if tag.startswith("v") else tag
result["release_id"] = str(release.get("id", ""))
result["target_commitish"] = release.get("target_commitish", "")
result["draft"] = str(release.get("draft", False)).lower()
else:
result["tag"] = os.environ.get("INPUT_TAG", "")
result["version"] = os.environ.get("INPUT_VERSION", "")
result["release_id"] = os.environ.get("INPUT_RELEASE_ID", "")
result["target_commitish"] = os.environ.get("INPUT_COMMIT", "")
draft_value = os.environ.get("INPUT_DRAFT", "false")
result["draft"] = str(draft_value).lower()
if not result["tag"] or not result["release_id"]:
sys.stderr.write("::error::Release metadata is missing. Provide tag, version, release_id, and target_commitish.\n")
sys.exit(1)
should_run = "true"
if event_name == "release" and result["draft"] != "true":
should_run = "false"
result["should_run"] = should_run
for key, value in result.items():
print(f"{key}={value}")
PY
cat context.env >> "$GITHUB_OUTPUT"
cat context.env
- name: Skip validation for published releases
if: steps.context.outputs.should_run != 'true'
run: echo "Release is already published; skipping validation checks."
- name: Download all release assets
if: steps.context.outputs.should_run == 'true'
id: download
run: |
echo "Downloading all assets from release ${{ steps.version.outputs.tag }}..."
echo "Downloading all assets from release ${{ steps.context.outputs.tag }}..."
# Create release directory
mkdir -p release
cd release
# Get list of all assets for this release (using gh CLI to avoid token exposure)
ASSETS=$(gh api "repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" \
--jq '.[].browser_download_url')
ASSETS=$(gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets" --jq '.[].browser_download_url')
if [ -z "$ASSETS" ]; then
echo "::error::No assets found in release"
@@ -54,13 +133,11 @@ jobs:
echo "Found assets:"
echo "$ASSETS"
# Download each asset
echo "$ASSETS" | while read -r url; do
if [ -n "$url" ]; then
filename=$(basename "$url")
echo "Downloading $filename..."
# Use gh CLI to download (avoids token exposure in logs)
gh release download "${{ github.event.release.tag_name }}" \
gh release download "${{ steps.context.outputs.tag }}" \
--pattern "$filename" --dir . --clobber
if [ $? -eq 0 ]; then
@@ -77,15 +154,15 @@ jobs:
ls -lh
- name: Install Docker
run: |
# Docker is pre-installed on ubuntu-latest runners
docker --version
if: steps.context.outputs.should_run == 'true'
run: docker --version
- name: Pull Docker image (if available)
if: steps.context.outputs.should_run == 'true'
id: docker
continue-on-error: true
run: |
IMAGE="rcourtman/pulse:${{ steps.version.outputs.tag }}"
IMAGE="rcourtman/pulse:${{ steps.context.outputs.tag }}"
echo "Attempting to pull Docker image: $IMAGE"
if docker pull "$IMAGE" 2>/dev/null; then
@@ -99,44 +176,34 @@ jobs:
fi
- name: Run validation script
if: steps.context.outputs.should_run == 'true'
id: validate
run: |
set +e # Don't exit on error, we want to capture the output
set +e
echo "Running validation script..."
chmod +x scripts/validate-release.sh
# Create output file for validation results
OUTPUT_FILE=$(mktemp)
if [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then
# Full validation with Docker image
echo "Running full validation (Docker + assets)..."
scripts/validate-release.sh \
"${{ steps.version.outputs.version }}" \
"${{ steps.context.outputs.version }}" \
"${{ steps.docker.outputs.image }}" \
"release" 2>&1 | tee "$OUTPUT_FILE"
else
# Modified validation script that skips Docker checks
echo "Running assets-only validation (Docker image not available)..."
# We'll need to skip Docker validation - create a temporary modified script
# For now, we'll just run the script and let Docker validation fail
# A more robust solution would be to modify the script or create a separate assets-only version
scripts/validate-release.sh \
"${{ steps.version.outputs.version }}" \
"rcourtman/pulse:${{ steps.version.outputs.tag }}" \
"${{ steps.context.outputs.version }}" \
"rcourtman/pulse:${{ steps.context.outputs.tag }}" \
"release" 2>&1 | tee "$OUTPUT_FILE" || true
fi
VALIDATION_EXIT_CODE=$?
# Save output for later use in comments
echo "VALIDATION_OUTPUT<<EOF" >> $GITHUB_OUTPUT
cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Check validation result
if [ $VALIDATION_EXIT_CODE -eq 0 ]; then
echo "validation_passed=true" >> $GITHUB_OUTPUT
echo "✅ Validation PASSED"
@@ -148,12 +215,12 @@ jobs:
exit $VALIDATION_EXIT_CODE
- name: Set commit status - Success
if: steps.validate.outputs.validation_passed == 'true'
if: steps.context.outputs.should_run == 'true' && steps.validate.outputs.validation_passed == 'true'
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.release.target_commitish }}" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
-d '{
"state": "success",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
@@ -162,19 +229,16 @@ jobs:
}'
- name: Update release body - Success
if: steps.validate.outputs.validation_passed == 'true'
if: steps.context.outputs.should_run == 'true' && steps.validate.outputs.validation_passed == 'true'
run: |
echo "✅ Validation passed - updating release description"
# Get current release body
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
| jq -r '.body // ""')
# Remove any existing validation status block
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d')
# Create new validation status block using heredoc
read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START -->
## ✅ Release Asset Validation: PASSED
@@ -194,29 +258,26 @@ jobs:
<!-- VALIDATION_STATUS_END -->
EOF
# Replace placeholders
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_LINK_PLACEHOLDER/[${{ github.workflow }} #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})}"
# Combine validation block with original body
NEW_BODY="$VALIDATION_BLOCK
$CURRENT_BODY"
# Update release
curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
-d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
- name: Set commit status - Failure
if: failure() || steps.validate.outputs.validation_passed == 'false'
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.release.target_commitish }}" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
-d '{
"state": "failure",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
@@ -225,13 +286,12 @@ jobs:
}'
- name: Delete all release assets on failure
if: failure() || steps.validate.outputs.validation_passed == 'false'
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
run: |
echo "❌ Validation failed - deleting all release assets"
# Delete all assets from the release
ASSET_IDS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets" \
| jq -r '.[].id')
if [ -n "$ASSET_IDS" ]; then
@@ -241,12 +301,6 @@ jobs:
curl -X DELETE \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id"
if [ $? -eq 0 ]; then
echo "✓ Deleted asset $asset_id"
else
echo "⚠️ Failed to delete asset $asset_id"
fi
fi
done
echo "✓ Asset deletion process completed"
@@ -255,19 +309,16 @@ jobs:
fi
- name: Update release body - Failure
if: failure() || steps.validate.outputs.validation_passed == 'false'
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
run: |
echo "❌ Validation failed - updating release description"
# Get current release body
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
| jq -r '.body // ""')
# Remove any existing validation status block
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d')
# Extract validation errors if available
VALIDATION_OUTPUT="${{ steps.validate.outputs.VALIDATION_OUTPUT }}"
if [ -n "$VALIDATION_OUTPUT" ]; then
ERRORS=$(echo "$VALIDATION_OUTPUT" | grep -i '\[ERROR\]' | head -20 || echo "See workflow logs for details")
@@ -275,7 +326,6 @@ jobs:
ERRORS="Validation script failed to run. Check workflow logs."
fi
# Create new validation status block using heredoc
read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START -->
## ❌ Release Asset Validation: FAILED
@@ -309,26 +359,23 @@ jobs:
<!-- VALIDATION_STATUS_END -->
EOF
# Replace placeholders
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_LINK_PLACEHOLDER/[${{ github.workflow }} #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_URL_PLACEHOLDER/${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//ERRORS_PLACEHOLDER/$ERRORS}"
# Combine validation block with original body
NEW_BODY="$VALIDATION_BLOCK
$CURRENT_BODY"
# Update release
curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
-d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
- name: Fail the workflow
if: failure() || steps.validate.outputs.validation_passed == 'false'
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
run: |
echo "::error::Release asset validation failed. All assets have been deleted."
exit 1

View File

@@ -1,4 +1,4 @@
import { Component, Show, createSignal, createEffect } from 'solid-js';
import { Component, Show, For, createSignal, createEffect } from 'solid-js';
import { Portal } from 'solid-js/web';
import type { NodeConfig } from '@/types/nodes';
import type { SecurityStatus } from '@/types/config';

View File

@@ -335,7 +335,7 @@ else
printf '%s\n' "$checksum_output" > checksums.txt
# Emit per-file .sha256 artifacts for backward compatibility while legacy installers transition off them
while IFS= read -r checksum filename; do
while read -r checksum filename; do
printf '%s %s\n' "$checksum" "$filename" > "${filename}.sha256"
done <<< "$checksum_output"