Release workflow guardrails (related to #695)

This commit is contained in:
rcourtman 2025-11-11 22:34:00 +00:00
parent 3e90737448
commit ea6cad10ce
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 type: string
jobs: 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: build-docker-images:
needs:
- version_guard
- preflight-tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
permissions: permissions:
@ -57,10 +150,10 @@ jobs:
push: true push: true
provenance: false provenance: false
tags: | tags: |
rcourtman/pulse:latest
rcourtman/pulse:${{ steps.set_version.outputs.tag }} 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:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse:${{ inputs.version }}
labels: | labels: |
org.opencontainers.image.title=Pulse org.opencontainers.image.title=Pulse
org.opencontainers.image.description=Proxmox monitoring system org.opencontainers.image.description=Proxmox monitoring system
@ -81,10 +174,10 @@ jobs:
push: true push: true
provenance: false provenance: false
tags: | tags: |
rcourtman/pulse-docker-agent:latest
rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }} 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:${{ steps.set_version.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/pulse-docker-agent:${{ inputs.version }}
labels: | labels: |
org.opencontainers.image.title=Pulse Docker Agent org.opencontainers.image.title=Pulse Docker Agent
org.opencontainers.image.description=Docker container monitoring agent for Pulse org.opencontainers.image.description=Docker container monitoring agent for Pulse
@ -101,11 +194,11 @@ jobs:
echo "" echo ""
echo "Server images:" echo "Server images:"
echo " - rcourtman/pulse:${{ steps.set_version.outputs.tag }}" echo " - rcourtman/pulse:${{ steps.set_version.outputs.tag }}"
echo " - rcourtman/pulse:latest" echo " - rcourtman/pulse:${{ inputs.version }}"
echo "" echo ""
echo "Agent images:" echo "Agent images:"
echo " - rcourtman/pulse-docker-agent:${{ steps.set_version.outputs.tag }}" 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: create-release:
needs: build-docker-images needs: build-docker-images
@ -113,6 +206,11 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
permissions: permissions:
contents: write 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: steps:
- name: Checkout repository - name: Checkout repository
@ -123,6 +221,10 @@ jobs:
- name: Fetch all tags - name: Fetch all tags
run: git fetch --tags --force run: git fetch --tags --force
- name: Capture commit metadata
id: commit_metadata
run: echo "commitish=$GITHUB_SHA" >> $GITHUB_OUTPUT
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
@ -147,20 +249,6 @@ jobs:
echo "Building release v${{ inputs.version }}..." echo "Building release v${{ inputs.version }}..."
./scripts/build-release.sh ${{ 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 - name: Generate release notes
id: generate_notes id: generate_notes
env: env:
@ -233,6 +321,7 @@ jobs:
rm -f "$NOTES_FILE" rm -f "$NOTES_FILE"
echo "release_url=$(gh release view ${TAG} --json url -q .url)" >> $GITHUB_OUTPUT 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 echo "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Upload checksums.txt - name: Upload checksums.txt
@ -287,7 +376,18 @@ jobs:
echo "2. Update the release notes with changes since last release" echo "2. Update the release notes with changes since last release"
echo "3. Publish the release manually when ready" echo "3. Publish the release manually when ready"
echo "" 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 "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 "" 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 name: Validate Release Assets
on: on:
workflow_run: workflow_call:
workflows: ["Release"] inputs:
types: [completed] 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: 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: jobs:
validate: validate:
# Only run for draft releases to act as a safety gate before publishing
if: github.event.release.draft == true
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write # Needed to delete assets and update release contents: write
issues: write # Needed to add labels and comments issues: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Extract version from tag - name: Determine release context
id: version 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: | run: |
TAG="${{ github.event.release.tag_name }}" python <<'PY' > context.env
# Remove 'v' prefix if present import json, os, sys
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT event_name = os.environ.get("EVENT_NAME", "")
echo "tag=$TAG" >> $GITHUB_OUTPUT result = {}
echo "Validating release: $TAG (version: $VERSION)"
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 - name: Download all release assets
if: steps.context.outputs.should_run == 'true'
id: download id: download
run: | 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 mkdir -p release
cd 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/${{ steps.context.outputs.release_id }}/assets" --jq '.[].browser_download_url')
ASSETS=$(gh api "repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" \
--jq '.[].browser_download_url')
if [ -z "$ASSETS" ]; then if [ -z "$ASSETS" ]; then
echo "::error::No assets found in release" echo "::error::No assets found in release"
@ -54,13 +133,11 @@ jobs:
echo "Found assets:" echo "Found assets:"
echo "$ASSETS" echo "$ASSETS"
# Download each asset
echo "$ASSETS" | while read -r url; do echo "$ASSETS" | while read -r url; do
if [ -n "$url" ]; then if [ -n "$url" ]; then
filename=$(basename "$url") filename=$(basename "$url")
echo "Downloading $filename..." echo "Downloading $filename..."
# Use gh CLI to download (avoids token exposure in logs) gh release download "${{ steps.context.outputs.tag }}" \
gh release download "${{ github.event.release.tag_name }}" \
--pattern "$filename" --dir . --clobber --pattern "$filename" --dir . --clobber
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
@ -77,15 +154,15 @@ jobs:
ls -lh ls -lh
- name: Install Docker - name: Install Docker
run: | if: steps.context.outputs.should_run == 'true'
# Docker is pre-installed on ubuntu-latest runners run: docker --version
docker --version
- name: Pull Docker image (if available) - name: Pull Docker image (if available)
if: steps.context.outputs.should_run == 'true'
id: docker id: docker
continue-on-error: true continue-on-error: true
run: | run: |
IMAGE="rcourtman/pulse:${{ steps.version.outputs.tag }}" IMAGE="rcourtman/pulse:${{ steps.context.outputs.tag }}"
echo "Attempting to pull Docker image: $IMAGE" echo "Attempting to pull Docker image: $IMAGE"
if docker pull "$IMAGE" 2>/dev/null; then if docker pull "$IMAGE" 2>/dev/null; then
@ -99,44 +176,34 @@ jobs:
fi fi
- name: Run validation script - name: Run validation script
if: steps.context.outputs.should_run == 'true'
id: validate id: validate
run: | run: |
set +e # Don't exit on error, we want to capture the output set +e
echo "Running validation script..." echo "Running validation script..."
chmod +x scripts/validate-release.sh chmod +x scripts/validate-release.sh
# Create output file for validation results
OUTPUT_FILE=$(mktemp) OUTPUT_FILE=$(mktemp)
if [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then if [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then
# Full validation with Docker image
echo "Running full validation (Docker + assets)..." echo "Running full validation (Docker + assets)..."
scripts/validate-release.sh \ scripts/validate-release.sh \
"${{ steps.version.outputs.version }}" \ "${{ steps.context.outputs.version }}" \
"${{ steps.docker.outputs.image }}" \ "${{ steps.docker.outputs.image }}" \
"release" 2>&1 | tee "$OUTPUT_FILE" "release" 2>&1 | tee "$OUTPUT_FILE"
else else
# Modified validation script that skips Docker checks
echo "Running assets-only validation (Docker image not available)..." 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 \ scripts/validate-release.sh \
"${{ steps.version.outputs.version }}" \ "${{ steps.context.outputs.version }}" \
"rcourtman/pulse:${{ steps.version.outputs.tag }}" \ "rcourtman/pulse:${{ steps.context.outputs.tag }}" \
"release" 2>&1 | tee "$OUTPUT_FILE" || true "release" 2>&1 | tee "$OUTPUT_FILE" || true
fi fi
VALIDATION_EXIT_CODE=$? VALIDATION_EXIT_CODE=$?
# Save output for later use in comments
echo "VALIDATION_OUTPUT<<EOF" >> $GITHUB_OUTPUT echo "VALIDATION_OUTPUT<<EOF" >> $GITHUB_OUTPUT
cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
# Check validation result
if [ $VALIDATION_EXIT_CODE -eq 0 ]; then if [ $VALIDATION_EXIT_CODE -eq 0 ]; then
echo "validation_passed=true" >> $GITHUB_OUTPUT echo "validation_passed=true" >> $GITHUB_OUTPUT
echo "✅ Validation PASSED" echo "✅ Validation PASSED"
@ -148,12 +215,12 @@ jobs:
exit $VALIDATION_EXIT_CODE exit $VALIDATION_EXIT_CODE
- name: Set commit status - Success - 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: | run: |
curl -X POST \ curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -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 '{ -d '{
"state": "success", "state": "success",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
@ -162,19 +229,16 @@ jobs:
}' }'
- name: Update release body - Success - 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: | run: |
echo "✅ Validation passed - updating release description" echo "✅ Validation passed - updating release description"
# Get current release body
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 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 // ""') | jq -r '.body // ""')
# Remove any existing validation status block
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d') 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 read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START --> <!-- VALIDATION_STATUS_START -->
## ✅ Release Asset Validation: PASSED ## ✅ Release Asset Validation: PASSED
@ -194,29 +258,26 @@ jobs:
<!-- VALIDATION_STATUS_END --> <!-- VALIDATION_STATUS_END -->
EOF EOF
# Replace placeholders
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}" 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_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 NEW_BODY="$VALIDATION_BLOCK
$CURRENT_BODY" $CURRENT_BODY"
# Update release
curl -X PATCH \ curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -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}')" -d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
- name: Set commit status - Failure - 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: | run: |
curl -X POST \ curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -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 '{ -d '{
"state": "failure", "state": "failure",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
@ -225,13 +286,12 @@ jobs:
}' }'
- name: Delete all release assets on failure - 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: | run: |
echo "❌ Validation failed - deleting all release assets" echo "❌ Validation failed - deleting all release assets"
# Delete all assets from the release
ASSET_IDS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 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') | jq -r '.[].id')
if [ -n "$ASSET_IDS" ]; then if [ -n "$ASSET_IDS" ]; then
@ -241,12 +301,6 @@ jobs:
curl -X DELETE \ curl -X DELETE \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id" "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 fi
done done
echo "✓ Asset deletion process completed" echo "✓ Asset deletion process completed"
@ -255,19 +309,16 @@ jobs:
fi fi
- name: Update release body - Failure - 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: | run: |
echo "❌ Validation failed - updating release description" echo "❌ Validation failed - updating release description"
# Get current release body
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 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 // ""') | jq -r '.body // ""')
# Remove any existing validation status block
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d') 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 }}" VALIDATION_OUTPUT="${{ steps.validate.outputs.VALIDATION_OUTPUT }}"
if [ -n "$VALIDATION_OUTPUT" ]; then if [ -n "$VALIDATION_OUTPUT" ]; then
ERRORS=$(echo "$VALIDATION_OUTPUT" | grep -i '\[ERROR\]' | head -20 || echo "See workflow logs for details") 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." ERRORS="Validation script failed to run. Check workflow logs."
fi fi
# Create new validation status block using heredoc
read -r -d '' VALIDATION_BLOCK <<'EOF' || true read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START --> <!-- VALIDATION_STATUS_START -->
## ❌ Release Asset Validation: FAILED ## ❌ Release Asset Validation: FAILED
@ -309,26 +359,23 @@ jobs:
<!-- VALIDATION_STATUS_END --> <!-- VALIDATION_STATUS_END -->
EOF EOF
# Replace placeholders
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}" 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_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//WORKFLOW_URL_PLACEHOLDER/${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//ERRORS_PLACEHOLDER/$ERRORS}" VALIDATION_BLOCK="${VALIDATION_BLOCK//ERRORS_PLACEHOLDER/$ERRORS}"
# Combine validation block with original body
NEW_BODY="$VALIDATION_BLOCK NEW_BODY="$VALIDATION_BLOCK
$CURRENT_BODY" $CURRENT_BODY"
# Update release
curl -X PATCH \ curl -X PATCH \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \ -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}')" -d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
- name: Fail the workflow - 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: | run: |
echo "::error::Release asset validation failed. All assets have been deleted." echo "::error::Release asset validation failed. All assets have been deleted."
exit 1 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 { Portal } from 'solid-js/web';
import type { NodeConfig } from '@/types/nodes'; import type { NodeConfig } from '@/types/nodes';
import type { SecurityStatus } from '@/types/config'; import type { SecurityStatus } from '@/types/config';

View file

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