name: Pulse Release Pipeline # Optimized: parallel jobs, fast RC path on: workflow_dispatch: inputs: version: description: 'Version number (e.g., 4.30.0)' required: true type: string release_notes: description: 'Release notes (markdown) - generated by Claude' required: true type: string promoted_from_tag: description: 'Stable only: RC tag being promoted (for example 6.0.0-rc.2)' required: false type: string rollback_version: description: 'Required: prior stable version to pin for rollback (for example 5.1.14 or v5.1.14)' required: false type: string ga_date: description: 'First stable v6.0.0 GA only: exact GA publish date (YYYY-MM-DD)' required: false type: string v5_eos_date: description: 'First stable v6.0.0 GA only: Pulse v5 end-of-support date (YYYY-MM-DD)' required: false type: string hotfix_exception: description: 'Stable only: bypass the 72-hour RC soak for urgent customer harm' required: false type: boolean default: false hotfix_reason: description: 'Stable only: reason for hotfix soak exception' required: false type: string draft_only: description: 'Create draft release only (do not publish)' required: false type: boolean default: false concurrency: group: release-${{ github.event.inputs.version || github.ref || github.run_id }} cancel-in-progress: false jobs: # Combined version extraction and validation (saves a checkout) prepare: runs-on: ubuntu-latest timeout-minutes: 5 outputs: version: ${{ steps.extract.outputs.version }} tag: ${{ steps.extract.outputs.tag }} is_prerelease: ${{ steps.extract.outputs.is_prerelease }} source_branch: ${{ steps.extract.outputs.source_branch }} required_branch: ${{ steps.branch_policy.outputs.required_branch }} promoted_from_tag: ${{ steps.promotion.outputs.promoted_from_tag }} rollback_tag: ${{ steps.promotion.outputs.rollback_tag }} rollback_command: ${{ steps.promotion.outputs.rollback_command }} ga_date: ${{ steps.promotion.outputs.ga_date }} v5_eos_date: ${{ steps.promotion.outputs.v5_eos_date }} hotfix_exception: ${{ steps.promotion.outputs.hotfix_exception }} hotfix_reason: ${{ steps.promotion.outputs.hotfix_reason }} steps: - name: Extract version id: extract run: | VERSION=$(jq -r '.inputs.version // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "") if [ -z "$VERSION" ]; then echo "::error::workflow_dispatch must include a version input" exit 1 fi TAG="v${VERSION}" IS_PRERELEASE="false" if [[ "$VERSION" =~ -rc\.[0-9]+$ ]] || [[ "$VERSION" =~ -alpha\.[0-9]+$ ]] || [[ "$VERSION" =~ -beta\.[0-9]+$ ]]; then IS_PRERELEASE="true" echo "Detected prerelease version: ${VERSION}" fi if [[ "${GITHUB_REF}" != refs/heads/* ]]; then echo "::error::Release workflow must be dispatched from a branch ref (current ref: ${GITHUB_REF})." exit 1 fi SOURCE_BRANCH="${GITHUB_REF_NAME}" echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "is_prerelease=${IS_PRERELEASE}" >> $GITHUB_OUTPUT echo "source_branch=${SOURCE_BRANCH}" >> $GITHUB_OUTPUT echo "Version: ${VERSION}, Tag: ${TAG}, Prerelease: ${IS_PRERELEASE}, Branch: ${SOURCE_BRANCH}" - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 sparse-checkout: | VERSION docs/release-control/control_plane.json scripts/release_control/control_plane.py scripts/release_control/repo_file_io.py - name: Resolve required release branch id: branch_policy run: | REQUIRED_BRANCH="$(python3 scripts/release_control/control_plane.py --branch-for-version "${{ steps.extract.outputs.version }}")" if [ "${{ steps.extract.outputs.source_branch }}" != "$REQUIRED_BRANCH" ]; then echo "::error::Invalid release line. Version ${{ steps.extract.outputs.version }} must run from ${REQUIRED_BRANCH}, but workflow ref is ${{ steps.extract.outputs.source_branch }}." exit 1 fi echo "required_branch=${REQUIRED_BRANCH}" >> "$GITHUB_OUTPUT" echo "[OK] Governed release branch for ${{ steps.extract.outputs.version }} is ${REQUIRED_BRANCH}" - name: Validate VERSION file run: | FILE_VERSION=$(cat VERSION | tr -d '\n') REQUESTED_VERSION="${{ steps.extract.outputs.version }}" if [ "$FILE_VERSION" != "$REQUESTED_VERSION" ]; then echo "::error::VERSION file ($FILE_VERSION) does not match requested version ($REQUESTED_VERSION)." echo "The VERSION file must be updated and committed before running release." exit 1 fi echo "[OK] VERSION file matches requested version ($REQUESTED_VERSION)" - name: Validate promotion policy id: promotion env: VERSION: ${{ steps.extract.outputs.version }} TAG: ${{ steps.extract.outputs.tag }} IS_PRERELEASE: ${{ steps.extract.outputs.is_prerelease }} PROMOTED_FROM_TAG_INPUT: ${{ github.event.inputs.promoted_from_tag }} ROLLBACK_VERSION_INPUT: ${{ github.event.inputs.rollback_version }} GA_DATE_INPUT: ${{ github.event.inputs.ga_date }} V5_EOS_DATE_INPUT: ${{ github.event.inputs.v5_eos_date }} HOTFIX_EXCEPTION_INPUT: ${{ github.event.inputs.hotfix_exception }} HOTFIX_REASON_INPUT: ${{ github.event.inputs.hotfix_reason }} run: | set -euo pipefail git fetch --prune origin main "${REQUIRED_BRANCH}" --tags RELEASE_NOTES_INPUT="$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH")" NOTES_FILE="$(mktemp)" printf '%s\n' "$RELEASE_NOTES_INPUT" > "$NOTES_FILE" HELPER_ARGS=( --version "${VERSION}" --promoted-from-tag "${PROMOTED_FROM_TAG_INPUT:-}" --rollback-version "${ROLLBACK_VERSION_INPUT:-}" --ga-date "${GA_DATE_INPUT:-}" --v5-eos-date "${V5_EOS_DATE_INPUT:-}" --hotfix-reason "${HOTFIX_REASON_INPUT:-}" --release-notes-file "$NOTES_FILE" ) if [ "${HOTFIX_EXCEPTION_INPUT:-false}" = "true" ]; then HELPER_ARGS+=(--hotfix-exception) fi python3 scripts/release_control/resolve_release_promotion.py "${HELPER_ARGS[@]}" > "$RUNNER_TEMP/promotion-metadata.out" rm -f "$NOTES_FILE" { cat "$RUNNER_TEMP/promotion-metadata.out" } >> "$GITHUB_OUTPUT" echo "[OK] Promotion policy validated for ${TAG}" # Frontend checks run in parallel with backend tests frontend_checks: needs: prepare runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: 'frontend-modern/package-lock.json' - name: Install dependencies run: npm --prefix frontend-modern ci - name: Lint frontend run: npm --prefix frontend-modern run lint - name: Audit header composition run: npm --prefix frontend-modern run lint:headers - name: Check frontend copy-paste duplication run: npm --prefix frontend-modern run lint:cpd # Backend tests run in parallel with frontend checks backend_tests: needs: prepare runs-on: ubuntu-latest timeout-minutes: 30 env: FRONTEND_DIST: frontend-modern/dist steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: 'frontend-modern/package-lock.json' - name: Restore frontend build cache id: frontend-cache uses: actions/cache@v4 with: path: frontend-modern/dist key: frontend-build-${{ hashFiles('frontend-modern/package-lock.json', 'frontend-modern/src/**/*', 'frontend-modern/index.html', 'frontend-modern/postcss.config.cjs', 'frontend-modern/tailwind.config.cjs') }} - name: Build frontend (if not cached) if: steps.frontend-cache.outputs.cache-hit != 'true' run: | npm --prefix frontend-modern ci npm --prefix frontend-modern run build - name: Copy frontend to embed location run: | rm -rf internal/api/frontend-modern mkdir -p internal/api/frontend-modern cp -r frontend-modern/dist internal/api/frontend-modern/ - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.25.7' cache: true - name: Run backend tests env: PULSE_DATA_DIR: /tmp/pulse-test-data run: make test # Docker build - amd64 only for prereleases, multi-arch for stable docker_build: needs: prepare runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up QEMU if: needs.prepare.outputs.is_prerelease != 'true' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build Docker image (verify only) uses: docker/build-push-action@v6 with: context: . target: runtime # amd64 only for prereleases (faster), multi-arch for stable releases platforms: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: false # Don't push staging images, just verify build provenance: false cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max build-args: | PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }} VERSION=${{ needs.prepare.outputs.tag }} - name: Build Pulse agent image (verify only) uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile target: agent_runtime platforms: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: false provenance: false cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache,mode=max build-args: | PULSE_LICENSE_PUBLIC_KEY=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }} VERSION=${{ needs.prepare.outputs.tag }} # Integration tests - skipped for prereleases (they've been tested in CI) integration_tests: needs: - prepare - backend_tests if: ${{ needs.prepare.outputs.is_prerelease != 'true' }} runs-on: ubuntu-latest timeout-minutes: 45 env: FRONTEND_DIST: frontend-modern/dist steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: 'frontend-modern/package-lock.json' - name: Restore frontend build cache uses: actions/cache@v4 with: path: frontend-modern/dist key: frontend-build-${{ hashFiles('frontend-modern/package-lock.json', 'frontend-modern/src/**/*', 'frontend-modern/index.html', 'frontend-modern/postcss.config.cjs', 'frontend-modern/tailwind.config.cjs') }} - name: Copy frontend to embed location run: | rm -rf internal/api/frontend-modern mkdir -p internal/api/frontend-modern cp -r frontend-modern/dist internal/api/frontend-modern/ - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.25.7' cache: true - name: Build Pulse Docker image for integration tests run: docker build -t pulse:test --target runtime . - name: Build mock GitHub server run: docker build -t pulse-mock-github:test tests/integration/mock-github-server - name: Install integration test dependencies working-directory: tests/integration run: | npm ci npx playwright install --with-deps chromium - name: Run integration tests working-directory: tests/integration env: MOCK_CHECKSUM_ERROR: "false" MOCK_NETWORK_ERROR: "false" MOCK_RATE_LIMIT: "false" MOCK_STALE_RELEASE: "false" PULSE_MULTI_TENANT_ENABLED: "true" PULSE_E2E_ENTITLEMENT_PROFILE: "multi-tenant" PULSE_E2E_BOOTSTRAP_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef run: | docker compose -f docker-compose.test.yml up -d echo "Waiting for services to be healthy..." timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-mock-github | grep -q "healthy"; do sleep 2; done' timeout 60 sh -c 'until docker inspect --format="{{json .State.Health.Status}}" pulse-test-server | grep -q "healthy"; do sleep 2; done' for i in 1 2 3 4 5; do if curl -f -s http://localhost:7655/api/health > /dev/null 2>&1; then echo "Pulse server is reachable" break elif [ $i -eq 5 ]; then docker logs pulse-test-server || true exit 1 fi sleep 2 done node scripts/apply-entitlement-profile.mjs echo "Running update API route smoke check..." STATUS=$(curl -s -o /tmp/update-status.json -w "%{http_code}" http://localhost:7655/api/updates/status || true) echo "Update status endpoint returned HTTP ${STATUS}" case "${STATUS}" in 200|401|403) ;; *) echo "Unexpected response from /api/updates/status" cat /tmp/update-status.json || true exit 1 ;; esac echo "Running multi-tenant E2E suite..." npx playwright test tests/03-multi-tenant.spec.ts --project=chromium --reporter=list docker compose -f docker-compose.test.yml down -v - name: Cleanup if: always() working-directory: tests/integration run: docker compose -f docker-compose.test.yml down -v || true # Create release after all checks pass create_release: needs: - prepare - frontend_checks - backend_tests - docker_build - integration_tests # Run if integration_tests passed OR was skipped (prereleases) if: ${{ always() && needs.frontend_checks.result == 'success' && needs.backend_tests.result == 'success' && needs.docker_build.result == 'success' && (needs.integration_tests.result == 'success' || needs.integration_tests.result == 'skipped') }} runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: write outputs: release_id: ${{ steps.create_release.outputs.release_id }} release_url: ${{ steps.create_release.outputs.release_url }} target_commitish: ${{ github.sha }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.25.7' cache: true - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: 'frontend-modern/package-lock.json' - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y zip - name: Set up Helm uses: azure/setup-helm@v4 with: version: 'v3.15.2' - name: Build release artifacts run: | echo "Building release ${{ needs.prepare.outputs.tag }}..." ./scripts/build-release.sh ${{ needs.prepare.outputs.version }} env: PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }} - name: Post-build health check run: | if [ -x ./pulse ]; then ./pulse --version elif [ -x ./cmd/pulse/pulse ]; then ./cmd/pulse/pulse --version fi - name: Prepare release notes id: generate_notes run: | VERSION="${{ needs.prepare.outputs.version }}" RELEASE_NOTES_INPUT=$(jq -r '.inputs.release_notes // ""' "$GITHUB_EVENT_PATH" 2>/dev/null || echo "") NOTES_FILE=$(mktemp) if [ -n "$RELEASE_NOTES_INPUT" ]; then printf "%s\n" "$RELEASE_NOTES_INPUT" > "$NOTES_FILE" else echo "Release $VERSION" > "$NOTES_FILE" echo "" >> "$NOTES_FILE" echo "See commit history for changes." >> "$NOTES_FILE" fi { echo "" echo "## Installation" echo "" echo "**Docker (recommended):**" echo '```bash' echo "docker pull rcourtman/pulse:${VERSION}" echo '```' echo "" echo "**Docker Compose:**" echo "Update your \`docker-compose.yml\` to use \`rcourtman/pulse:${VERSION}\`" echo "" echo "See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions." echo "" echo "## Promotion Metadata" echo "" echo "- Promotion channel: ${{ needs.prepare.outputs.is_prerelease == 'true' && 'rc' || 'stable' }}" echo "- Candidate stable tag: ${{ needs.prepare.outputs.tag }}" if [ -n "${{ needs.prepare.outputs.promoted_from_tag }}" ]; then echo "- Promoted RC tag: ${{ needs.prepare.outputs.promoted_from_tag }}" else echo "- Promoted RC tag: n/a" fi echo "- Rollback target: ${{ needs.prepare.outputs.rollback_tag }}" echo "- Rollback command: \`${{ needs.prepare.outputs.rollback_command }}\`" if [ -n "${{ needs.prepare.outputs.ga_date }}" ]; then echo "- Planned GA date: ${{ needs.prepare.outputs.ga_date }}" fi if [ -n "${{ needs.prepare.outputs.v5_eos_date }}" ]; then echo "- Planned v5 end-of-support date: ${{ needs.prepare.outputs.v5_eos_date }}" fi echo "- Hotfix exception: ${{ needs.prepare.outputs.hotfix_exception }}" if [ -n "${{ needs.prepare.outputs.hotfix_reason }}" ]; then echo "- Hotfix reason: ${{ needs.prepare.outputs.hotfix_reason }}" fi } >> "$NOTES_FILE" echo "notes_file=${NOTES_FILE}" >> $GITHUB_OUTPUT - name: Create tag env: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" HEAD_SHA=$(git rev-parse HEAD) REMOTE_TAG_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}" | awk '{print $1}') if [ -n "$REMOTE_TAG_SHA" ]; then REMOTE_COMMIT_SHA=$(git ls-remote --tags origin "refs/tags/${TAG}^{}" | awk '{print $1}') [ -z "$REMOTE_COMMIT_SHA" ] && REMOTE_COMMIT_SHA="$REMOTE_TAG_SHA" if [ "$REMOTE_COMMIT_SHA" = "$HEAD_SHA" ]; then echo "Tag ${TAG} already exists and points to HEAD - continuing" else echo "::error::Tag ${TAG} already exists but points to ${REMOTE_COMMIT_SHA}, not HEAD (${HEAD_SHA}). Delete the tag first: git push origin --delete ${TAG}" exit 1 fi else echo "Creating tag ${TAG}..." git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "${TAG}" -m "Release ${TAG}" git push origin "${TAG}" fi - name: Create draft release id: create_release env: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" NOTES_FILE="${{ steps.generate_notes.outputs.notes_file }}" IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}" EXISTING_RELEASE=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" 2>/dev/null || echo "") RELEASE_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty') if [ -n "$RELEASE_ID" ]; then RELEASE_URL=$(echo "$EXISTING_RELEASE" | jq -r '.html_url') IS_DRAFT=$(echo "$EXISTING_RELEASE" | jq -r '.draft') if [ "$IS_DRAFT" = "true" ]; then echo "Updating existing draft release for ${TAG}" gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \ -X PATCH \ -F body="$(cat $NOTES_FILE)" \ -F prerelease=${IS_PRERELEASE} > /dev/null else echo "::error::Published release already exists for ${TAG}." exit 1 fi else echo "Creating draft release for ${TAG}..." RELEASE_JSON=$(gh api "repos/${{ github.repository }}/releases" \ -X POST \ -F tag_name="${TAG}" \ -F name="Pulse ${TAG}" \ -F body="$(cat $NOTES_FILE)" \ -F draft=true \ -F prerelease=${IS_PRERELEASE}) RELEASE_ID=$(echo "$RELEASE_JSON" | jq -r '.id') RELEASE_URL=$(echo "$RELEASE_JSON" | jq -r '.html_url') fi rm -f "$NOTES_FILE" echo "release_url=${RELEASE_URL}" >> $GITHUB_OUTPUT echo "release_id=${RELEASE_ID}" >> $GITHUB_OUTPUT echo "[OK] Draft release: ${TAG} (ID: ${RELEASE_ID})" - name: Upload checksums env: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" gh release upload "${TAG}" release/checksums.txt --clobber gh release upload "${TAG}" release/*.sha256 --clobber - name: Upload release assets env: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" gh release upload "${TAG}" release/*.tar.gz --clobber gh release upload "${TAG}" release/*.zip --clobber if ls release/*.tgz 1> /dev/null 2>&1; then gh release upload "${TAG}" release/*.tgz --clobber fi for bare_agent in \ release/pulse-agent-linux-amd64 \ release/pulse-agent-linux-arm64 \ release/pulse-agent-linux-armv7 \ release/pulse-agent-linux-armv6 \ release/pulse-agent-linux-386 \ release/pulse-agent-freebsd-amd64 \ release/pulse-agent-freebsd-arm64 \ release/pulse-agent-windows-amd64.exe \ release/pulse-agent-windows-arm64.exe \ release/pulse-agent-windows-386.exe; do if [ -f "${bare_agent}" ]; then gh release upload "${TAG}" "${bare_agent}" --clobber fi done gh release upload "${TAG}" release/install.sh --clobber if [ -f release/install.ps1 ]; then gh release upload "${TAG}" release/install.ps1 --clobber fi gh release upload "${TAG}" release/install-docker.sh --clobber gh release upload "${TAG}" release/pulse-auto-update.sh --clobber - name: Publish release if: ${{ github.event.inputs.draft_only != 'true' }} env: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prepare.outputs.tag }}" RELEASE_ID="${{ steps.create_release.outputs.release_id }}" IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}" if [ "$IS_PRERELEASE" = "true" ]; then gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \ -X PATCH -F draft=false -F make_latest=false echo "[OK] Published as prerelease: ${TAG}" else gh api "repos/${{ github.repository }}/releases/${RELEASE_ID}" \ -X PATCH -F draft=false -F make_latest=true echo "[OK] Published as latest: ${TAG}" fi - name: Skip publish (draft only) if: ${{ github.event.inputs.draft_only == 'true' }} run: 'echo "Draft-only mode: ${{ steps.create_release.outputs.release_url }}"' - name: Trigger Docker image publish if: ${{ github.event.inputs.draft_only != 'true' }} continue-on-error: true env: GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} run: | gh workflow run publish-docker.yml -f tag="${{ needs.prepare.outputs.tag }}" echo "[OK] Docker publish workflow dispatched" - name: Trigger demo server update if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease != 'true' }} continue-on-error: true env: GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} run: | gh workflow run update-demo-server.yml -f tag="${{ needs.prepare.outputs.tag }}" echo "[OK] Demo server update dispatched" - name: Skip demo server update for prerelease if: ${{ github.event.inputs.draft_only != 'true' && needs.prepare.outputs.is_prerelease == 'true' }} run: echo "Skipping demo server update for prerelease tag ${{ needs.prepare.outputs.tag }}" - name: Summary run: | echo "[SUCCESS] Release published!" echo "Release: ${{ needs.prepare.outputs.tag }}" echo "URL: ${{ steps.create_release.outputs.release_url }}" validate_release_assets: needs: - prepare - create_release uses: ./.github/workflows/validate-release-assets.yml secrets: inherit with: tag: ${{ needs.prepare.outputs.tag }} version: ${{ needs.prepare.outputs.version }} release_id: ${{ needs.create_release.outputs.release_id }} draft: false target_commitish: ${{ needs.create_release.outputs.target_commitish }}