Update release_pipeline.yml
This commit is contained in:
968
.github/workflows/release_pipeline.yml
vendored
968
.github/workflows/release_pipeline.yml
vendored
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
@@ -18,103 +16,909 @@
|
|||||||
# GNU General Public License for more details.
|
# GNU General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program (./LICENSE.md).
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
# ============================================================================
|
#
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# ============================================================================
|
# DEFGROUP: GitHub.Workflow
|
||||||
# DEFGROUP: Script.Library
|
# INGROUP: MokoStandards.Release
|
||||||
# INGROUP: RepoHealth
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
# REPO: https://github.com/mokoconsulting-tech
|
# PATH: /.github/workflows/release_pipeline.yml
|
||||||
# PATH: /scripts/lib/find_files.sh
|
# VERSION: 03.05.00
|
||||||
# VERSION: 01.00.00
|
# BRIEF: Enterprise release pipeline enforcing dev to rc to version to main. Creates prerelease when rc is created. Creates full release when version is created and promotes to main while retaining the version branch.
|
||||||
# BRIEF: Find files by glob patterns with standard ignore rules for CI checks
|
|
||||||
# NOTE:
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
set -eu
|
name: Release Pipeline (dev > rc > version > main)
|
||||||
|
|
||||||
# Shared utilities
|
on:
|
||||||
. "$(dirname "$0")/common.sh"
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_classification:
|
||||||
|
description: "Manual override for classification. auto follows branch policy; rc forces prerelease behavior; stable forces full release behavior."
|
||||||
|
required: true
|
||||||
|
default: auto
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- auto
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
- prereleased
|
||||||
|
- published
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
concurrency:
|
||||||
# Purpose:
|
group: release-pipeline-${{ github.ref_name }}
|
||||||
# - Provide a consistent, reusable file discovery primitive for repo scripts.
|
cancel-in-progress: false
|
||||||
# - Support multiple glob patterns.
|
|
||||||
# - Apply standard ignore rules to reduce noise (vendor, node_modules, .git).
|
|
||||||
# - Output one path per line, relative to repo root.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/lib/find_files.sh <glob> [<glob> ...]
|
|
||||||
#
|
|
||||||
# Examples:
|
|
||||||
# ./scripts/lib/find_files.sh "*.yml" "*.yaml"
|
|
||||||
# ./scripts/lib/find_files.sh "src/**/*.php" "tests/**/*.php"
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
ROOT="$(script_root)"
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
if [ "${1:-}" = "" ]; then
|
permissions:
|
||||||
die "Usage: $0 <glob> [<glob> ...]"
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
guard:
|
||||||
|
name: 00 Guard and derive promotion metadata
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.meta.outputs.version }}
|
||||||
|
source_branch: ${{ steps.meta.outputs.source_branch }}
|
||||||
|
source_prefix: ${{ steps.meta.outputs.source_prefix }}
|
||||||
|
target_branch: ${{ steps.meta.outputs.target_branch }}
|
||||||
|
promoted_branch: ${{ steps.meta.outputs.promoted_branch }}
|
||||||
|
today_utc: ${{ steps.meta.outputs.today_utc }}
|
||||||
|
channel: ${{ steps.meta.outputs.channel }}
|
||||||
|
release_mode: ${{ steps.meta.outputs.release_mode }}
|
||||||
|
override: ${{ steps.meta.outputs.override }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify actor has admin or maintain role
|
||||||
|
env:
|
||||||
|
GH_TOKEN: "${{ github.token }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTOR="${GITHUB_ACTOR}"
|
||||||
|
REPO="${GITHUB_REPOSITORY}"
|
||||||
|
|
||||||
|
PERMISSION="$(gh api "/repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.permission')"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Authorization check"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"actor\":\"${ACTOR}\",\"permission\":\"${PERMISSION}\"}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${PERMISSION}" != "admin" ] && [ "${PERMISSION}" != "maintain" ]; then
|
||||||
|
echo "ERROR: Actor ${ACTOR} lacks required role (admin or maintain)." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
require_cmd find
|
- name: Validate trigger and extract metadata
|
||||||
require_cmd sed
|
id: meta
|
||||||
|
env:
|
||||||
|
RELEASE_CLASSIFICATION: "${{ github.event.inputs.release_classification }}"
|
||||||
|
RELEASE_PRERELEASE: "${{ github.event.release.prerelease }}"
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
# Standard excludes (pragmatic defaults for CI)
|
EVENT_NAME="${GITHUB_EVENT_NAME}"
|
||||||
# Note: Keep these broad to avoid scanning generated or third-party content.
|
REF_NAME="${GITHUB_REF_NAME}"
|
||||||
EXCLUDES='
|
|
||||||
-path "*/.git/*" -o
|
|
||||||
-path "*/.github/*/node_modules/*" -o
|
|
||||||
-path "*/node_modules/*" -o
|
|
||||||
-path "*/vendor/*" -o
|
|
||||||
-path "*/dist/*" -o
|
|
||||||
-path "*/build/*" -o
|
|
||||||
-path "*/cache/*" -o
|
|
||||||
-path "*/tmp/*" -o
|
|
||||||
-path "*/.tmp/*" -o
|
|
||||||
-path "*/.cache/*"
|
|
||||||
'
|
|
||||||
|
|
||||||
# Convert a glob (bash-like) to a find -path pattern.
|
VERSION=""
|
||||||
# - Supports ** for "any directories" by translating to *
|
SOURCE_BRANCH=""
|
||||||
# - Ensures leading */ so patterns apply anywhere under repo root
|
SOURCE_PREFIX=""
|
||||||
glob_to_find_path() {
|
TARGET_BRANCH=""
|
||||||
g="$1"
|
PROMOTED_BRANCH=""
|
||||||
|
CHANNEL=""
|
||||||
|
RELEASE_MODE="none"
|
||||||
|
|
||||||
# normalize path separators for WSL/CI compatibility
|
OVERRIDE="${RELEASE_CLASSIFICATION:-auto}"
|
||||||
g="$(normalize_path "$g")"
|
if [ -z "${OVERRIDE}" ]; then
|
||||||
|
OVERRIDE="auto"
|
||||||
|
fi
|
||||||
|
|
||||||
# translate ** to * (find -path uses shell glob semantics)
|
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
g="$(printf '%s' "$g" | sed 's|\*\*|*|g')"
|
echo "${REF_NAME}" | grep -E '^(dev|rc)/[0-9]+[.][0-9]+[.][0-9]+$'
|
||||||
|
|
||||||
case "$g" in
|
SOURCE_BRANCH="${REF_NAME}"
|
||||||
/*) printf '%s\n' "$g" ;;
|
SOURCE_PREFIX="${REF_NAME%%/*}"
|
||||||
*) printf '%s\n' "*/$g" ;;
|
VERSION="${REF_NAME#*/}"
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build a single find invocation that ORs all patterns.
|
if [ "${SOURCE_PREFIX}" = "dev" ]; then
|
||||||
# Shell portability note: avoid arrays; build an expression string.
|
TARGET_BRANCH="rc/${VERSION}"
|
||||||
PAT_EXPR=""
|
PROMOTED_BRANCH="rc/${VERSION}"
|
||||||
for GLOB in "$@"; do
|
CHANNEL="rc"
|
||||||
P="$(glob_to_find_path "$GLOB")"
|
RELEASE_MODE="prerelease"
|
||||||
if [ -z "$PAT_EXPR" ]; then
|
|
||||||
PAT_EXPR="-path \"$P\""
|
|
||||||
else
|
else
|
||||||
PAT_EXPR="$PAT_EXPR -o -path \"$P\""
|
TARGET_BRANCH="version/${VERSION}"
|
||||||
|
PROMOTED_BRANCH="version/${VERSION}"
|
||||||
|
CHANNEL="stable"
|
||||||
|
RELEASE_MODE="stable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${OVERRIDE}" = "rc" ]; then
|
||||||
|
CHANNEL="rc"
|
||||||
|
RELEASE_MODE="prerelease"
|
||||||
|
elif [ "${OVERRIDE}" = "stable" ]; then
|
||||||
|
CHANNEL="stable"
|
||||||
|
RELEASE_MODE="stable"
|
||||||
|
else
|
||||||
|
OVERRIDE="auto"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [ "${EVENT_NAME}" = "release" ]; then
|
||||||
|
TAG_NAME="${REF_NAME}"
|
||||||
|
VERSION="${TAG_NAME#v}"
|
||||||
|
VERSION="${VERSION%-rc}"
|
||||||
|
echo "${VERSION}" | grep -E '^[0-9]+[.][0-9]+[.][0-9]+$'
|
||||||
|
|
||||||
|
if [ "${RELEASE_PRERELEASE:-false}" = "true" ]; then
|
||||||
|
CHANNEL="rc"
|
||||||
|
RELEASE_MODE="prerelease"
|
||||||
|
else
|
||||||
|
CHANNEL="stable"
|
||||||
|
RELEASE_MODE="stable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OVERRIDE="auto"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "ERROR: Unsupported trigger ${EVENT_NAME}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TODAY_UTC="$(date -u +%Y-%m-%d)"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "source_branch=${SOURCE_BRANCH}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "source_prefix=${SOURCE_PREFIX}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "target_branch=${TARGET_BRANCH}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "promoted_branch=${PROMOTED_BRANCH}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "today_utc=${TODAY_UTC}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "channel=${CHANNEL}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "release_mode=${RELEASE_MODE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "override=${OVERRIDE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Guard report"
|
||||||
|
echo "```json"
|
||||||
|
echo "{"
|
||||||
|
echo " \"repository\": \"${GITHUB_REPOSITORY}\","
|
||||||
|
echo " \"workflow\": \"${GITHUB_WORKFLOW}\","
|
||||||
|
echo " \"job\": \"${GITHUB_JOB}\","
|
||||||
|
echo " \"run_id\": ${GITHUB_RUN_ID},"
|
||||||
|
echo " \"run_number\": ${GITHUB_RUN_NUMBER},"
|
||||||
|
echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT},"
|
||||||
|
echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\","
|
||||||
|
echo " \"actor\": \"${GITHUB_ACTOR}\","
|
||||||
|
echo " \"sha\": \"${GITHUB_SHA}\","
|
||||||
|
echo " \"event\": \"${EVENT_NAME}\","
|
||||||
|
echo " \"ref\": \"${REF_NAME}\","
|
||||||
|
echo " \"version\": \"${VERSION}\","
|
||||||
|
echo " \"source_branch\": \"${SOURCE_BRANCH}\","
|
||||||
|
echo " \"target_branch\": \"${TARGET_BRANCH}\","
|
||||||
|
echo " \"promoted_branch\": \"${PROMOTED_BRANCH}\","
|
||||||
|
echo " \"channel\": \"${CHANNEL}\","
|
||||||
|
echo " \"release_mode\": \"${RELEASE_MODE}\","
|
||||||
|
echo " \"override\": \"${OVERRIDE}\","
|
||||||
|
echo " \"today_utc\": \"${TODAY_UTC}\""
|
||||||
|
echo "}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
promote_branch:
|
||||||
|
name: 01 Promote branch and delete source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: guard
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.guard.outputs.source_branch }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git identity
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
|
- name: Enforce promotion preconditions
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
SRC="${{ needs.guard.outputs.source_branch }}"
|
||||||
|
DST="${{ needs.guard.outputs.target_branch }}"
|
||||||
|
|
||||||
|
git fetch origin --prune
|
||||||
|
|
||||||
|
if [ -z "${SRC}" ] || [ -z "${DST}" ]; then
|
||||||
|
echo "ERROR: guard did not emit SRC or DST" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git show-ref --verify --quiet "refs/remotes/origin/${SRC}"; then
|
||||||
|
echo "ERROR: origin/${SRC} not found" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet "refs/remotes/origin/${DST}"; then
|
||||||
|
echo "ERROR: origin/${DST} already exists" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Promote and delete source
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
SRC="${{ needs.guard.outputs.source_branch }}"
|
||||||
|
DST="${{ needs.guard.outputs.target_branch }}"
|
||||||
|
|
||||||
|
git checkout -B "${DST}" "origin/${SRC}"
|
||||||
|
git push origin "${DST}"
|
||||||
|
git push origin --delete "${SRC}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Promotion report"
|
||||||
|
echo "```json"
|
||||||
|
echo "{"
|
||||||
|
echo " \"repository\": \"${GITHUB_REPOSITORY}\","
|
||||||
|
echo " \"workflow\": \"${GITHUB_WORKFLOW}\","
|
||||||
|
echo " \"job\": \"${GITHUB_JOB}\","
|
||||||
|
echo " \"run_id\": ${GITHUB_RUN_ID},"
|
||||||
|
echo " \"run_number\": ${GITHUB_RUN_NUMBER},"
|
||||||
|
echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT},"
|
||||||
|
echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\","
|
||||||
|
echo " \"actor\": \"${GITHUB_ACTOR}\","
|
||||||
|
echo " \"sha\": \"${GITHUB_SHA}\","
|
||||||
|
echo " \"promoted\": \"${SRC} -> ${DST}\","
|
||||||
|
echo " \"deleted\": \"${SRC}\""
|
||||||
|
echo "}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
normalize_dates:
|
||||||
|
name: 02 Normalize dates on promoted branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- guard
|
||||||
|
- promote_branch
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout promoted branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.guard.outputs.promoted_branch }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git identity
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
|
- name: Validate repo prerequisites
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
test -d src || (echo "ERROR: src directory missing" && exit 1)
|
||||||
|
test -f CHANGELOG.md || (echo "ERROR: CHANGELOG.md missing" && exit 1)
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
|
||||||
|
if ! grep -F "## [${VERSION}] " CHANGELOG.md >/dev/null; then
|
||||||
|
echo "ERROR: CHANGELOG.md missing heading for version [${VERSION}]" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Normalize dates using repository script only
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
TODAY="${{ needs.guard.outputs.today_utc }}"
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Date normalization (repo script only)"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"today_utc\":\"${TODAY}\",\"version\":\"${VERSION}\"}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
CANDIDATES=(
|
||||||
|
"scripts/update_dates.sh"
|
||||||
|
"scripts/release/update_dates.sh"
|
||||||
|
"scripts/release/update_dates"
|
||||||
|
)
|
||||||
|
|
||||||
|
SCRIPT=""
|
||||||
|
for c in "${CANDIDATES[@]}"; do
|
||||||
|
if [ -f "${c}" ]; then
|
||||||
|
SCRIPT="${c}"
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Execute find and emit relative paths.
|
if [ -z "${SCRIPT}" ]; then
|
||||||
# - Use eval to apply the constructed predicate string safely as a single expression.
|
FOUND="$(find . -maxdepth 3 -type f \( -name 'update_dates.sh' -o -name 'update-dates.sh' \) 2>/dev/null | head -n 5 || true)"
|
||||||
# - We scope to files only.
|
{
|
||||||
# - We prune excluded directories.
|
echo "ERROR: Date normalization script not found in approved locations."
|
||||||
cd "$ROOT"
|
echo "Approved locations:"
|
||||||
|
printf '%s
|
||||||
|
' "${CANDIDATES[@]}"
|
||||||
|
echo "Discovered candidates (first 5):"
|
||||||
|
echo "${FOUND:-<none>}"
|
||||||
|
echo "Required action: add scripts/update_dates.sh (or scripts/release/update_dates.sh) to the repo."
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
echo "Using date script: ${SCRIPT}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
eval "find . \\( $EXCLUDES \\) -prune -o -type f \\( $PAT_EXPR \\) -print" \
|
|
||||||
| sed 's|^\./||' \
|
chmod +x "${SCRIPT}"
|
||||||
| sed '/^$/d' \
|
"${SCRIPT}" "${TODAY}" "${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
| sort -u
|
|
||||||
|
{
|
||||||
|
echo "### Date normalization diffstat"
|
||||||
|
echo "```"
|
||||||
|
git diff --stat || true
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Commit normalized dates (if changed)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No date changes to commit" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore(release): normalize dates" || true
|
||||||
|
git push origin "HEAD:${{ needs.guard.outputs.promoted_branch }}"
|
||||||
|
|
||||||
|
build_and_release:
|
||||||
|
name: 03 Build ZIP, upload to SFTP, create GitHub release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- guard
|
||||||
|
- normalize_dates
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout promoted branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.guard.outputs.promoted_branch }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git identity
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
|
- name: Validate required secrets and variables
|
||||||
|
env:
|
||||||
|
FTP_HOST: "${{ secrets.FTP_HOST }}"
|
||||||
|
FTP_USER: "${{ secrets.FTP_USER }}"
|
||||||
|
FTP_KEY: "${{ secrets.FTP_KEY }}"
|
||||||
|
FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}"
|
||||||
|
FTP_PATH: "${{ secrets.FTP_PATH }}"
|
||||||
|
FTP_PROTOCOL: "${{ secrets.FTP_PROTOCOL }}"
|
||||||
|
FTP_PORT: "${{ secrets.FTP_PORT }}"
|
||||||
|
FTP_PATH_SUFFIX: "${{ vars.FTP_PATH_SUFFIX }}"
|
||||||
|
CHANNEL: "${{ needs.guard.outputs.channel }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
|
||||||
|
[ -n "${FTP_HOST:-}" ] || missing+=("FTP_HOST")
|
||||||
|
[ -n "${FTP_USER:-}" ] || missing+=("FTP_USER")
|
||||||
|
[ -n "${FTP_KEY:-}" ] || missing+=("FTP_KEY")
|
||||||
|
[ -n "${FTP_PATH:-}" ] || missing+=("FTP_PATH")
|
||||||
|
|
||||||
|
proto="${FTP_PROTOCOL:-sftp}"
|
||||||
|
if [ -n "${FTP_PROTOCOL:-}" ] && [ "${proto}" != "sftp" ]; then
|
||||||
|
missing+=("FTP_PROTOCOL_INVALID")
|
||||||
|
fi
|
||||||
|
|
||||||
|
first_line="$(printf '%s' "${FTP_KEY:-}" | head -n 1 || true)"
|
||||||
|
if [ -n "${FTP_KEY:-}" ]; then
|
||||||
|
if printf '%s' "${first_line}" | grep -q '^PuTTY-User-Key-File-'; then
|
||||||
|
key_format="ppk"
|
||||||
|
elif printf '%s' "${first_line}" | grep -q '^-----BEGIN '; then
|
||||||
|
key_format="openssh"
|
||||||
|
else
|
||||||
|
key_format="unknown"
|
||||||
|
missing+=("FTP_KEY_FORMAT")
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
key_format="missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Configuration guardrails"
|
||||||
|
echo "```json"
|
||||||
|
printf '{"status":"%s","missing":[' "$( [ "${#missing[@]}" -gt 0 ] && echo fail || echo ok )"
|
||||||
|
sep=""
|
||||||
|
for m in "${missing[@]}"; do
|
||||||
|
printf '%s"%s"' "${sep}" "${m}"
|
||||||
|
sep=",";
|
||||||
|
done
|
||||||
|
printf '],"key_format":"%s","channel":"%s"}
|
||||||
|
' "${key_format}" "${CHANNEL}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run repository validation scripts (workflow-controlled)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
required_scripts=(
|
||||||
|
"scripts/validate_manifest.sh"
|
||||||
|
"scripts/validate_xml_wellformed.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
optional_scripts=(
|
||||||
|
"scripts/validate_changelog.sh"
|
||||||
|
"scripts/validate_tabs.sh"
|
||||||
|
"scripts/validate_paths.sh"
|
||||||
|
"scripts/validate_version_alignment.sh"
|
||||||
|
"scripts/validate_language_structure.sh"
|
||||||
|
"scripts/validate_php_syntax.sh"
|
||||||
|
"scripts/validate_no_secrets.sh"
|
||||||
|
"scripts/validate_license_headers.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
missing=()
|
||||||
|
for s in "${required_scripts[@]}"; do
|
||||||
|
if [ ! -f "${s}" ]; then
|
||||||
|
missing+=("${s}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#missing[@]}" -gt 0 ]; then
|
||||||
|
{
|
||||||
|
echo "### Script guardrails"
|
||||||
|
echo "```json"
|
||||||
|
printf '{"status":"fail","missing_required_scripts":['
|
||||||
|
sep=""
|
||||||
|
for m in "${missing[@]}"; do
|
||||||
|
printf '%s"%s"' "${sep}" "${m}"
|
||||||
|
sep=",";
|
||||||
|
done
|
||||||
|
printf ']}
|
||||||
|
'
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ran=()
|
||||||
|
skipped=()
|
||||||
|
|
||||||
|
for s in "${required_scripts[@]}" "${optional_scripts[@]}"; do
|
||||||
|
if [ -f "${s}" ]; then
|
||||||
|
chmod +x "${s}"
|
||||||
|
"${s}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
ran+=("${s}")
|
||||||
|
else
|
||||||
|
skipped+=("${s}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Script execution report"
|
||||||
|
echo "```json"
|
||||||
|
printf '{"ran":['
|
||||||
|
sep=""
|
||||||
|
for r in "${ran[@]}"; do
|
||||||
|
printf '%s"%s"' "${sep}" "${r}"
|
||||||
|
sep=",";
|
||||||
|
done
|
||||||
|
printf '],"skipped_optional":['
|
||||||
|
sep=""
|
||||||
|
for k in "${skipped[@]}"; do
|
||||||
|
printf '%s"%s"' "${sep}" "${k}"
|
||||||
|
sep=",";
|
||||||
|
done
|
||||||
|
printf ']}
|
||||||
|
'
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Build Joomla ZIP (extension type aware)
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
|
CHANNEL="${{ needs.guard.outputs.channel }}"
|
||||||
|
|
||||||
|
test -d src || (echo "ERROR: src directory missing" && exit 1)
|
||||||
|
|
||||||
|
DIST_DIR="${GITHUB_WORKSPACE}/dist"
|
||||||
|
mkdir -p "${DIST_DIR}"
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
if [ -f "src/templateDetails.xml" ]; then
|
||||||
|
MANIFEST="src/templateDetails.xml"
|
||||||
|
elif find src -maxdepth 4 -type f -name 'templateDetails.xml' | head -n 1 | grep -q .; then
|
||||||
|
MANIFEST="$(find src -maxdepth 4 -type f -name 'templateDetails.xml' | head -n 1)"
|
||||||
|
elif find src -maxdepth 4 -type f -name 'pkg_*.xml' | head -n 1 | grep -q .; then
|
||||||
|
MANIFEST="$(find src -maxdepth 4 -type f -name 'pkg_*.xml' | head -n 1)"
|
||||||
|
elif find src -maxdepth 4 -type f -name 'com_*.xml' | head -n 1 | grep -q .; then
|
||||||
|
MANIFEST="$(find src -maxdepth 4 -type f -name 'com_*.xml' | head -n 1)"
|
||||||
|
elif find src -maxdepth 4 -type f -name 'mod_*.xml' | head -n 1 | grep -q .; then
|
||||||
|
MANIFEST="$(find src -maxdepth 4 -type f -name 'mod_*.xml' | head -n 1)"
|
||||||
|
elif find src -maxdepth 6 -type f -name 'plg_*.xml' | head -n 1 | grep -q .; then
|
||||||
|
MANIFEST="$(find src -maxdepth 6 -type f -name 'plg_*.xml' | head -n 1)"
|
||||||
|
else
|
||||||
|
MANIFEST="$(grep -Rsl --include='*.xml' '<extension' src | head -n 1 || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${MANIFEST}" ]; then
|
||||||
|
echo "ERROR: No Joomla manifest XML found under src" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXT_TYPE="$(grep -Eo 'type="[^"]+"' "${MANIFEST}" | head -n 1 | cut -d '"' -f2 || true)"
|
||||||
|
if [ -z "${EXT_TYPE}" ]; then
|
||||||
|
EXT_TYPE="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT="$(dirname "${MANIFEST}")"
|
||||||
|
|
||||||
|
ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${EXT_TYPE}.zip"
|
||||||
|
|
||||||
|
(cd "${ROOT}" && zip -r -X "${DIST_DIR}/${ZIP}" . \
|
||||||
|
-x "**/.git/**" \
|
||||||
|
-x "**/.github/**" \
|
||||||
|
-x "**/.DS_Store" \
|
||||||
|
-x "**/__MACOSX/**")
|
||||||
|
|
||||||
|
echo "zip_name=${ZIP}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "dist_dir=${DIST_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "root=${ROOT}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "manifest=${MANIFEST}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "ext_type=${EXT_TYPE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
ZIP_BYTES="$(stat -c%s "${DIST_DIR}/${ZIP}")"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Build report"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"repository\":\"${GITHUB_REPOSITORY}\",\"workflow\":\"${GITHUB_WORKFLOW}\",\"job\":\"${GITHUB_JOB}\",\"run_id\":${GITHUB_RUN_ID},\"run_number\":${GITHUB_RUN_NUMBER},\"run_attempt\":${GITHUB_RUN_ATTEMPT},\"run_url\":\"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\",\"actor\":\"${GITHUB_ACTOR}\",\"sha\":\"${GITHUB_SHA}\",\"root\":\"${ROOT}\",\"manifest\":\"${MANIFEST}\",\"extension_type\":\"${EXT_TYPE}\",\"zip\":\"${DIST_DIR}/${ZIP}\",\"zip_bytes\":${ZIP_BYTES}}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Upload ZIP to SFTP (key-only, overwrite, verbose)
|
||||||
|
env:
|
||||||
|
FTP_HOST: "${{ secrets.FTP_HOST }}"
|
||||||
|
FTP_USER: "${{ secrets.FTP_USER }}"
|
||||||
|
FTP_KEY: "${{ secrets.FTP_KEY }}"
|
||||||
|
FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}"
|
||||||
|
FTP_PATH: "${{ secrets.FTP_PATH }}"
|
||||||
|
FTP_PROTOCOL: "${{ secrets.FTP_PROTOCOL }}"
|
||||||
|
FTP_PORT: "${{ secrets.FTP_PORT }}"
|
||||||
|
FTP_PATH_SUFFIX: "${{ vars.FTP_PATH_SUFFIX }}"
|
||||||
|
CHANNEL: "${{ needs.guard.outputs.channel }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ZIP="${{ steps.build.outputs.zip_name }}"
|
||||||
|
|
||||||
|
: "${FTP_HOST:?Missing secret FTP_HOST}"
|
||||||
|
: "${FTP_USER:?Missing secret FTP_USER}"
|
||||||
|
: "${FTP_KEY:?Missing secret FTP_KEY}"
|
||||||
|
: "${FTP_PATH:?Missing secret FTP_PATH}"
|
||||||
|
|
||||||
|
PROTOCOL="${FTP_PROTOCOL:-sftp}"
|
||||||
|
if [ "${PROTOCOL}" != "sftp" ]; then
|
||||||
|
echo "ERROR: Only SFTP permitted" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PORT="${FTP_PORT:-}"
|
||||||
|
if [ -n "${PORT}" ]; then
|
||||||
|
HOSTPORT="${FTP_HOST}:${PORT}"
|
||||||
|
else
|
||||||
|
HOSTPORT="${FTP_HOST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SUFFIX="${FTP_PATH_SUFFIX:-}"
|
||||||
|
if [ -n "${SUFFIX}" ]; then
|
||||||
|
REMOTE_PATH="${FTP_PATH%/}/${SUFFIX%/}/${CHANNEL}"
|
||||||
|
else
|
||||||
|
REMOTE_PATH="${FTP_PATH%/}/${CHANNEL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SFTP target: sftp://${HOSTPORT}${REMOTE_PATH}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y lftp openssh-client putty-tools
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
|
||||||
|
if printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then
|
||||||
|
printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk
|
||||||
|
chmod 600 ~/.ssh/key.ppk
|
||||||
|
|
||||||
|
if grep -Eq '^Encryption: *none[[:space:]]*$' ~/.ssh/key.ppk; then
|
||||||
|
PPK_PASSPHRASE=""
|
||||||
|
else
|
||||||
|
if [ -z "${FTP_PASSWORD:-}" ]; then
|
||||||
|
echo "ERROR: Encrypted PPK detected but FTP_PASSWORD not provided" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PPK_PASSPHRASE="${FTP_PASSWORD:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${PPK_PASSPHRASE}" ]; then
|
||||||
|
puttygen ~/.ssh/key.ppk -O private-openssh --passphrase "${PPK_PASSPHRASE}" -o ~/.ssh/id_rsa
|
||||||
|
else
|
||||||
|
puttygen ~/.ssh/key.ppk -O private-openssh -o ~/.ssh/id_rsa
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s ~/.ssh/id_rsa ]; then
|
||||||
|
echo "ERROR: PPK conversion failed" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
rm -f ~/.ssh/key.ppk
|
||||||
|
else
|
||||||
|
printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh-keyscan -H "${FTP_HOST}" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
lftp -d -e "\
|
||||||
|
set sftp:auto-confirm yes; \
|
||||||
|
set cmd:trace yes; \
|
||||||
|
set net:timeout 30; \
|
||||||
|
set net:max-retries 3; \
|
||||||
|
set net:reconnect-interval-base 5; \
|
||||||
|
set sftp:connect-program 'ssh -a -x -i ~/.ssh/id_rsa -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no -o ChallengeResponseAuthentication=no -o PubkeyAuthentication=yes'; \
|
||||||
|
open -u '${FTP_USER}', sftp://${HOSTPORT}; \
|
||||||
|
mkdir -p '${REMOTE_PATH}'; \
|
||||||
|
cd '${REMOTE_PATH}'; \
|
||||||
|
put -E '${{ steps.build.outputs.dist_dir }}/${ZIP}'; \
|
||||||
|
ls; \
|
||||||
|
bye"
|
||||||
|
|
||||||
|
ZIP_BYTES="$(stat -c%s "${{ steps.build.outputs.dist_dir }}/${ZIP}")"
|
||||||
|
{
|
||||||
|
echo "### SFTP upload report"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"protocol\":\"sftp\",\"host\":\"${FTP_HOST}\",\"port\":\"${PORT:-default}\",\"remote_path\":\"${REMOTE_PATH}\",\"zip\":\"${ZIP}\",\"zip_bytes\":${ZIP_BYTES},\"overwrite\":true,\"key_only\":true}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
id: tag
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
MODE="${{ needs.guard.outputs.release_mode }}"
|
||||||
|
|
||||||
|
if [ "${MODE}" = "prerelease" ]; then
|
||||||
|
TAG="v${VERSION}-rc"
|
||||||
|
else
|
||||||
|
TAG="v${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git fetch --tags
|
||||||
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||||
|
echo "Tag ${TAG} already exists" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
else
|
||||||
|
git tag -a "${TAG}" -m "${MODE} ${VERSION}"
|
||||||
|
git push origin "refs/tags/${TAG}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Generate release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
ZIP_ASSET="${{ steps.build.outputs.zip_name }}"
|
||||||
|
|
||||||
|
awk "/^## \[${VERSION}\]/{flag=1;next}/^## \[/ {flag=0}flag" CHANGELOG.md > RELEASE_NOTES.md || true
|
||||||
|
|
||||||
|
if [ ! -s RELEASE_NOTES.md ]; then
|
||||||
|
echo "ERROR: Release notes extraction failed for ${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "Assets:"
|
||||||
|
echo "- ${ZIP_ASSET}"
|
||||||
|
} >> RELEASE_NOTES.md
|
||||||
|
|
||||||
|
- name: Create GitHub release and attach ZIP
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.tag.outputs.tag }}
|
||||||
|
name: ${{ needs.guard.outputs.release_mode }} ${{ needs.guard.outputs.version }}
|
||||||
|
prerelease: ${{ needs.guard.outputs.release_mode == 'prerelease' }}
|
||||||
|
body_path: RELEASE_NOTES.md
|
||||||
|
files: |
|
||||||
|
dist/*.zip
|
||||||
|
|
||||||
|
- name: Attest build provenance
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: |
|
||||||
|
dist/*.zip
|
||||||
|
|
||||||
|
- name: Publish release report
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
echo "### Release report (JSON)" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "```json" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
printf '{"repository":"%s","workflow":"%s","job":"%s","run_id":%s,"run_number":%s,"run_attempt":%s,"run_url":"%s","actor":"%s","sha":"%s","version":"%s","branch":"%s","tag":"%s","mode":"%s","channel":"%s","override":"%s","zip":"%s"}
|
||||||
|
' \
|
||||||
|
"${GITHUB_REPOSITORY}" \
|
||||||
|
"${GITHUB_WORKFLOW}" \
|
||||||
|
"${GITHUB_JOB}" \
|
||||||
|
"${GITHUB_RUN_ID}" \
|
||||||
|
"${GITHUB_RUN_NUMBER}" \
|
||||||
|
"${GITHUB_RUN_ATTEMPT}" \
|
||||||
|
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||||
|
"${GITHUB_ACTOR}" \
|
||||||
|
"${GITHUB_SHA}" \
|
||||||
|
"${{ needs.guard.outputs.version }}" \
|
||||||
|
"${{ needs.guard.outputs.promoted_branch }}" \
|
||||||
|
"${{ steps.tag.outputs.tag }}" \
|
||||||
|
"${{ needs.guard.outputs.release_mode }}" \
|
||||||
|
"${{ needs.guard.outputs.channel }}" \
|
||||||
|
"${{ needs.guard.outputs.override }}" \
|
||||||
|
"${{ steps.build.outputs.zip_name }}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "```" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
push_version_to_main:
|
||||||
|
name: 04 Promote version branch to main (stable only, keep version branch)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- guard
|
||||||
|
- build_and_release
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' && needs.guard.outputs.release_mode == 'stable' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git identity
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
|
- name: Create PR from version branch to main
|
||||||
|
env:
|
||||||
|
GH_TOKEN: "${{ github.token }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
||||||
|
|
||||||
|
gh pr create \
|
||||||
|
--base main \
|
||||||
|
--head "${HEAD}" \
|
||||||
|
--title "Release ${VERSION} to main" \
|
||||||
|
--body "Automated PR created by release pipeline. Version branch is retained by policy." \
|
||||||
|
|| true
|
||||||
|
|
||||||
|
- name: Attempt to merge PR (best-effort)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: "${{ github.token }}"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
||||||
|
PR_NUMBER="$(gh pr list --head "${HEAD}" --base main --json number --jq '.[0].number' || true)"
|
||||||
|
|
||||||
|
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
|
||||||
|
echo "ERROR: PR not found for head ${HEAD}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh pr merge "${PR_NUMBER}" --merge --delete-branch=false \
|
||||||
|
|| echo "PR merge blocked by branch protection or policy" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Main branch promotion"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"head\":\"${HEAD}\",\"pr\":${PR_NUMBER}}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
release_event_report:
|
||||||
|
name: 99 Release event report (GitHub UI created release)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: guard
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout tag
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Publish JSON report to job summary
|
||||||
|
env:
|
||||||
|
IS_PRERELEASE: "${{ github.event.release.prerelease }}"
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
|
TAG="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
echo "### Release event report (JSON)" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "```json" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
printf '{"repository":"%s","workflow":"%s","job":"%s","run_id":%s,"run_number":%s,"run_attempt":%s,"run_url":"%s","actor":"%s","sha":"%s","version":"%s","tag":"%s","prerelease":%s}
|
||||||
|
' \
|
||||||
|
"${GITHUB_REPOSITORY}" \
|
||||||
|
"${GITHUB_WORKFLOW}" \
|
||||||
|
"${GITHUB_JOB}" \
|
||||||
|
"${GITHUB_RUN_ID}" \
|
||||||
|
"${GITHUB_RUN_NUMBER}" \
|
||||||
|
"${GITHUB_RUN_ATTEMPT}" \
|
||||||
|
"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||||
|
"${GITHUB_ACTOR}" \
|
||||||
|
"${GITHUB_SHA}" \
|
||||||
|
"${VERSION}" \
|
||||||
|
"${TAG}" \
|
||||||
|
"${IS_PRERELEASE}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "```" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|||||||
Reference in New Issue
Block a user