Update release_pipeline.yml
This commit is contained in:
94
.github/workflows/release_pipeline.yml
vendored
94
.github/workflows/release_pipeline.yml
vendored
@@ -23,14 +23,24 @@
|
|||||||
# INGROUP: MokoStandards.Release
|
# INGROUP: MokoStandards.Release
|
||||||
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||||
# PATH: /.github/workflows/release_pipeline.yml
|
# PATH: /.github/workflows/release_pipeline.yml
|
||||||
# VERSION: 01.01.00
|
# VERSION: 01.02.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 pushes version to main while retaining the version branch.
|
# 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.
|
||||||
# NOTE: Key controls: strict branch gating, mandatory source branch deletion after promotion, least privilege permissions, key-only SFTP, ZIP-only distribution, overwrite enabled, no checksum generation.
|
# NOTE: Controls: strict branch gating, mandatory source branch deletion after promotion, key-only SFTP with verbose logs, ZIP-only distribution with overwrite, no checksum generation.
|
||||||
#
|
#
|
||||||
name: Release Pipeline (dev to rc to version to main)
|
name: Release Pipeline (dev to rc to version to main)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
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:
|
release:
|
||||||
types:
|
types:
|
||||||
- created
|
- created
|
||||||
@@ -62,10 +72,14 @@ jobs:
|
|||||||
today_utc: ${{ steps.meta.outputs.today_utc }}
|
today_utc: ${{ steps.meta.outputs.today_utc }}
|
||||||
channel: ${{ steps.meta.outputs.channel }}
|
channel: ${{ steps.meta.outputs.channel }}
|
||||||
release_mode: ${{ steps.meta.outputs.release_mode }}
|
release_mode: ${{ steps.meta.outputs.release_mode }}
|
||||||
|
override: ${{ steps.meta.outputs.override }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Validate trigger and extract metadata
|
- name: Validate trigger and extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
|
env:
|
||||||
|
RELEASE_CLASSIFICATION: ${{ github.event.inputs.release_classification }}
|
||||||
|
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -80,6 +94,11 @@ jobs:
|
|||||||
CHANNEL=""
|
CHANNEL=""
|
||||||
RELEASE_MODE="none"
|
RELEASE_MODE="none"
|
||||||
|
|
||||||
|
OVERRIDE="${RELEASE_CLASSIFICATION:-auto}"
|
||||||
|
if [ -z "${OVERRIDE}" ]; then
|
||||||
|
OVERRIDE="auto"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
echo "${REF_NAME}" | grep -E '^(dev|rc)/[0-9]+[.][0-9]+[.][0-9]+$'
|
echo "${REF_NAME}" | grep -E '^(dev|rc)/[0-9]+[.][0-9]+[.][0-9]+$'
|
||||||
|
|
||||||
@@ -88,25 +107,36 @@ jobs:
|
|||||||
VERSION="${REF_NAME#*/}"
|
VERSION="${REF_NAME#*/}"
|
||||||
|
|
||||||
if [ "${SOURCE_PREFIX}" = "dev" ]; then
|
if [ "${SOURCE_PREFIX}" = "dev" ]; then
|
||||||
# dev -> rc, then prerelease
|
# dev -> rc
|
||||||
TARGET_BRANCH="rc/${VERSION}"
|
TARGET_BRANCH="rc/${VERSION}"
|
||||||
PROMOTED_BRANCH="rc/${VERSION}"
|
PROMOTED_BRANCH="rc/${VERSION}"
|
||||||
CHANNEL="rc"
|
CHANNEL="rc"
|
||||||
RELEASE_MODE="prerelease"
|
RELEASE_MODE="prerelease"
|
||||||
else
|
else
|
||||||
# rc -> version, then full release + push to main
|
# rc -> version
|
||||||
TARGET_BRANCH="version/${VERSION}"
|
TARGET_BRANCH="version/${VERSION}"
|
||||||
PROMOTED_BRANCH="version/${VERSION}"
|
PROMOTED_BRANCH="version/${VERSION}"
|
||||||
CHANNEL="stable"
|
CHANNEL="stable"
|
||||||
RELEASE_MODE="stable"
|
RELEASE_MODE="stable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Manual override: classification only. Promotion path does not change.
|
||||||
|
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
|
elif [ "${EVENT_NAME}" = "release" ]; then
|
||||||
TAG_NAME="${REF_NAME}"
|
TAG_NAME="${REF_NAME}"
|
||||||
VERSION="${TAG_NAME#v}"
|
VERSION="${TAG_NAME#v}"
|
||||||
echo "${VERSION}" | grep -E '^[0-9]+[.][0-9]+[.][0-9]+$'
|
echo "${VERSION}" | grep -E '^[0-9]+[.][0-9]+[.][0-9]+$'
|
||||||
|
|
||||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
if [ "${RELEASE_PRERELEASE:-false}" = "true" ]; then
|
||||||
CHANNEL="rc"
|
CHANNEL="rc"
|
||||||
RELEASE_MODE="prerelease"
|
RELEASE_MODE="prerelease"
|
||||||
else
|
else
|
||||||
@@ -114,6 +144,8 @@ jobs:
|
|||||||
RELEASE_MODE="stable"
|
RELEASE_MODE="stable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
OVERRIDE="auto"
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "ERROR: Unsupported trigger ${EVENT_NAME}" >> "${GITHUB_STEP_SUMMARY}"
|
echo "ERROR: Unsupported trigger ${EVENT_NAME}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -129,6 +161,7 @@ jobs:
|
|||||||
echo "today_utc=${TODAY_UTC}" >> "${GITHUB_OUTPUT}"
|
echo "today_utc=${TODAY_UTC}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "channel=${CHANNEL}" >> "${GITHUB_OUTPUT}"
|
echo "channel=${CHANNEL}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "release_mode=${RELEASE_MODE}" >> "${GITHUB_OUTPUT}"
|
echo "release_mode=${RELEASE_MODE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "override=${OVERRIDE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Guard report"
|
echo "### Guard report"
|
||||||
@@ -142,6 +175,7 @@ jobs:
|
|||||||
echo " \"promoted_branch\": \"${PROMOTED_BRANCH}\","
|
echo " \"promoted_branch\": \"${PROMOTED_BRANCH}\","
|
||||||
echo " \"channel\": \"${CHANNEL}\","
|
echo " \"channel\": \"${CHANNEL}\","
|
||||||
echo " \"release_mode\": \"${RELEASE_MODE}\","
|
echo " \"release_mode\": \"${RELEASE_MODE}\","
|
||||||
|
echo " \"override\": \"${OVERRIDE}\","
|
||||||
echo " \"today_utc\": \"${TODAY_UTC}\""
|
echo " \"today_utc\": \"${TODAY_UTC}\""
|
||||||
echo "}"
|
echo "}"
|
||||||
echo "```"
|
echo "```"
|
||||||
@@ -269,24 +303,11 @@ jobs:
|
|||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
echo "Working directory: $(pwd)" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
echo "Repo root listing:" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
ls -la >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
echo "Scripts folder listing:" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
if [ -d scripts ]; then
|
|
||||||
ls -la scripts >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
else
|
|
||||||
echo "scripts/ directory not found." >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enterprise control: repo-provided date normalization is mandatory.
|
# Enterprise control: repo-provided date normalization is mandatory.
|
||||||
# Allow standard locations inside the repo, but never a generated fallback.
|
|
||||||
CANDIDATES=(
|
CANDIDATES=(
|
||||||
"scripts/update_dates.sh"
|
"scripts/update_dates.sh"
|
||||||
"scripts/release/update_dates.sh"
|
"scripts/release/update_dates.sh"
|
||||||
"scripts/release/update_dates"
|
"scripts/release/update_dates"
|
||||||
"./scripts/update_dates.sh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SCRIPT=""
|
SCRIPT=""
|
||||||
@@ -298,7 +319,6 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "${SCRIPT}" ]; then
|
if [ -z "${SCRIPT}" ]; then
|
||||||
# Best-effort discovery for audit visibility.
|
|
||||||
FOUND="$(find . -maxdepth 3 -type f \( -name 'update_dates.sh' -o -name 'update-dates.sh' \) 2>/dev/null | head -n 5 || true)"
|
FOUND="$(find . -maxdepth 3 -type f \( -name 'update_dates.sh' -o -name 'update-dates.sh' \) 2>/dev/null | head -n 5 || true)"
|
||||||
{
|
{
|
||||||
echo "ERROR: Date normalization script not found in approved locations."
|
echo "ERROR: Date normalization script not found in approved locations."
|
||||||
@@ -315,20 +335,7 @@ jobs:
|
|||||||
echo "Using date script: ${SCRIPT}" >> "${GITHUB_STEP_SUMMARY}"
|
echo "Using date script: ${SCRIPT}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
chmod +x "${SCRIPT}"
|
chmod +x "${SCRIPT}"
|
||||||
"${SCRIPT}" "${TODAY}" "${VERSION}"
|
"${SCRIPT}" "${TODAY}" "${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: Commit and push date updates
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if git diff --quiet; then
|
|
||||||
echo "No date changes detected" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore(release): normalize dates for ${{ needs.guard.outputs.version }}"
|
|
||||||
git push origin "HEAD:${{ needs.guard.outputs.promoted_branch }}"
|
|
||||||
|
|
||||||
build_and_release:
|
build_and_release:
|
||||||
name: 03 Build ZIP, upload to SFTP, create GitHub release
|
name: 03 Build ZIP, upload to SFTP, create GitHub release
|
||||||
@@ -396,7 +403,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
EXT_TYPE="$(grep -o 'type=\"[^\"]*\"' "${MANIFEST}" | head -n 1 | cut -d '\"' -f2)"
|
EXT_TYPE="$(grep -o 'type=\"[^\"]*\"' "${MANIFEST}" | head -n 1 | cut -d '"' -f2)"
|
||||||
if [ -z "${EXT_TYPE}" ]; then
|
if [ -z "${EXT_TYPE}" ]; then
|
||||||
EXT_TYPE="unknown"
|
EXT_TYPE="unknown"
|
||||||
fi
|
fi
|
||||||
@@ -470,7 +477,6 @@ jobs:
|
|||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
ssh-keyscan -H "${FTP_HOST}" >> ~/.ssh/known_hosts
|
ssh-keyscan -H "${FTP_HOST}" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# Verbose SFTP session
|
|
||||||
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; open -u '${FTP_USER}', sftp://${HOSTPORT}; pwd; ls; mkdir -p '${REMOTE_PATH}'; cd '${REMOTE_PATH}'; pwd; put -E 'dist/${ZIP}'; ls; bye"
|
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; open -u '${FTP_USER}', sftp://${HOSTPORT}; pwd; ls; mkdir -p '${REMOTE_PATH}'; cd '${REMOTE_PATH}'; pwd; put -E 'dist/${ZIP}'; ls; bye"
|
||||||
|
|
||||||
- name: Create Git tag for release
|
- name: Create Git tag for release
|
||||||
@@ -498,14 +504,13 @@ jobs:
|
|||||||
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
|
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Generate release notes from CHANGELOG.md
|
- name: Generate release notes from CHANGELOG.md
|
||||||
id: notes
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
ZIP_ASSET="${{ steps.build.outputs.zip_name }}"
|
ZIP_ASSET="${{ steps.build.outputs.zip_name }}"
|
||||||
|
|
||||||
awk "/^## \[${VERSION}\]/{flag=1;next}/^## \[/ {flag=0}flag" CHANGELOG.md > RELEASE_NOTES.md || true
|
awk "/^## \[${VERSION}\]/{flag=1;next}/^## \[/{flag=0}flag" CHANGELOG.md > RELEASE_NOTES.md || true
|
||||||
|
|
||||||
if [ ! -s RELEASE_NOTES.md ]; then
|
if [ ! -s RELEASE_NOTES.md ]; then
|
||||||
echo "ERROR: Release notes extraction failed for ${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
echo "ERROR: Release notes extraction failed for ${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
@@ -545,14 +550,15 @@ jobs:
|
|||||||
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||||||
CHANNEL="${{ needs.guard.outputs.channel }}"
|
CHANNEL="${{ needs.guard.outputs.channel }}"
|
||||||
MODE="${{ needs.guard.outputs.release_mode }}"
|
MODE="${{ needs.guard.outputs.release_mode }}"
|
||||||
|
OVERRIDE="${{ needs.guard.outputs.override }}"
|
||||||
|
|
||||||
echo "### Release report (JSON)" >> "${GITHUB_STEP_SUMMARY}"
|
echo "### Release report (JSON)" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "```json" >> "${GITHUB_STEP_SUMMARY}"
|
echo "```json" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "{\"repository\":\"${REPO_FULL}\",\"version\":\"${VERSION}\",\"branch\":\"${BRANCH}\",\"tag\":\"${TAG}\",\"mode\":\"${MODE}\",\"channel\":\"${CHANNEL}\",\"zip\":\"${ZIP_NAME}\",\"sha\":null}" >> "${GITHUB_STEP_SUMMARY}"
|
echo "{\"repository\":\"${REPO_FULL}\",\"version\":\"${VERSION}\",\"branch\":\"${BRANCH}\",\"tag\":\"${TAG}\",\"mode\":\"${MODE}\",\"channel\":\"${CHANNEL}\",\"override\":\"${OVERRIDE}\",\"zip\":\"${ZIP_NAME}\"}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
echo "```" >> "${GITHUB_STEP_SUMMARY}"
|
echo "```" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
push_version_to_main:
|
push_version_to_main:
|
||||||
name: 04 Push version to main (stable only, keep version branch)
|
name: 04 Promote version branch to main (stable only, keep version branch)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- guard
|
- guard
|
||||||
@@ -586,12 +592,11 @@ jobs:
|
|||||||
|
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
||||||
TITLE="Release ${VERSION} to main"
|
|
||||||
|
|
||||||
gh pr create \
|
gh pr create \
|
||||||
--base main \
|
--base main \
|
||||||
--head "${HEAD}" \
|
--head "${HEAD}" \
|
||||||
--title "${TITLE}" \
|
--title "Release ${VERSION} to main" \
|
||||||
--body "Automated PR created by release pipeline. Version branch is retained by policy." \
|
--body "Automated PR created by release pipeline. Version branch is retained by policy." \
|
||||||
|| true
|
|| true
|
||||||
|
|
||||||
@@ -601,9 +606,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
|
||||||
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
HEAD="${{ needs.guard.outputs.promoted_branch }}"
|
||||||
|
|
||||||
PR_NUMBER="$(gh pr list --head "${HEAD}" --base main --json number --jq '.[0].number' || true)"
|
PR_NUMBER="$(gh pr list --head "${HEAD}" --base main --json number --jq '.[0].number' || true)"
|
||||||
|
|
||||||
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
|
if [ -z "${PR_NUMBER}" ] || [ "${PR_NUMBER}" = "null" ]; then
|
||||||
@@ -611,14 +614,13 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Enterprise-safe default: merge commit, no branch deletion.
|
|
||||||
gh pr merge "${PR_NUMBER}" --merge --delete-branch=false \
|
gh pr merge "${PR_NUMBER}" --merge --delete-branch=false \
|
||||||
|| echo "PR merge blocked by branch protection or policy" >> "${GITHUB_STEP_SUMMARY}"
|
|| echo "PR merge blocked by branch protection or policy" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Main branch promotion"
|
echo "### Main branch promotion"
|
||||||
echo "```json"
|
echo "```json"
|
||||||
echo "{\"version\":\"${VERSION}\",\"head\":\"${HEAD}\",\"pr\":${PR_NUMBER}}"
|
echo "{\"head\":\"${HEAD}\",\"pr\":${PR_NUMBER}}"
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user