Update release_pipeline.yml
This commit is contained in:
571
.github/workflows/release_pipeline.yml
vendored
571
.github/workflows/release_pipeline.yml
vendored
@@ -25,6 +25,7 @@
|
|||||||
# PATH: /.github/workflows/release_pipeline.yml
|
# PATH: /.github/workflows/release_pipeline.yml
|
||||||
# VERSION: 03.05.00
|
# VERSION: 03.05.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: 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:
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: Release Pipeline (dev > rc > version > main)
|
name: Release Pipeline (dev > rc > version > main)
|
||||||
@@ -55,56 +56,70 @@ defaults:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
# Default permissions are minimized; jobs elevate as needed.
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
- name: Report run context (always)
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
{
|
jobs:
|
||||||
echo "### Run context"
|
guard:
|
||||||
echo "```json"
|
name: 00 Guardrails and metadata
|
||||||
printf '{'
|
runs-on: ubuntu-latest
|
||||||
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
|
||||||
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
|
||||||
printf '"job":"%s",' "${GITHUB_JOB}"
|
|
||||||
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
|
||||||
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
|
||||||
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
|
||||||
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
||||||
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
|
||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
|
||||||
printf '"sha":"%s",' "${GITHUB_SHA}"
|
|
||||||
printf '"runner_os":"%s",' "${RUNNER_OS}"
|
|
||||||
printf '"runner_name":"%s"' "${RUNNER_NAME}"
|
|
||||||
printf '}
|
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
outputs:
|
||||||
echo "### Git snapshot"
|
version: ${{ steps.meta.outputs.version }}
|
||||||
echo "```"
|
source_branch: ${{ steps.meta.outputs.source_branch }}
|
||||||
git --version || true
|
source_prefix: ${{ steps.meta.outputs.source_prefix }}
|
||||||
git status --porcelain=v1 || true
|
target_branch: ${{ steps.meta.outputs.target_branch }}
|
||||||
git log -1 --pretty=fuller || true
|
promoted_branch: ${{ steps.meta.outputs.promoted_branch }}
|
||||||
echo "```"
|
today_utc: ${{ steps.meta.outputs.today_utc }}
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
channel: ${{ steps.meta.outputs.channel }}
|
||||||
|
release_mode: ${{ steps.meta.outputs.release_mode }}
|
||||||
|
override: ${{ steps.meta.outputs.override }}
|
||||||
|
|
||||||
if [ "${PERMISSION}" != "admin" ] && [ "${PERMISSION}" != "maintain" ]; then
|
permissions:
|
||||||
echo "ERROR: Actor ${ACTOR} lacks required role (admin or maintain)." >> "${GITHUB_STEP_SUMMARY}"
|
contents: read
|
||||||
exit 1
|
actions: read
|
||||||
fi
|
# Required for permissions check via REST API
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout (best effort)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Actor authorization (admin or maintain)
|
||||||
|
id: auth
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const owner = context.repo.owner;
|
||||||
|
const repo = context.repo.repo;
|
||||||
|
const username = context.actor;
|
||||||
|
|
||||||
|
const res = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
const perm = (res?.data?.permission || '').toLowerCase();
|
||||||
|
const allowed = (perm === 'admin' || perm === 'maintain');
|
||||||
|
|
||||||
|
core.setOutput('permission', perm || 'unknown');
|
||||||
|
core.setOutput('allowed', allowed ? 'true' : 'false');
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
core.setFailed(`Actor ${username} lacks required role (admin or maintain). Detected permission: ${perm || 'unknown'}.`);
|
||||||
|
}
|
||||||
|
|
||||||
- name: Validate trigger and extract metadata
|
- name: Validate trigger and extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
env:
|
env:
|
||||||
RELEASE_CLASSIFICATION: "${{ github.event.inputs.release_classification }}"
|
RELEASE_CLASSIFICATION: ${{ github.event.inputs.release_classification }}
|
||||||
RELEASE_PRERELEASE: "${{ github.event.release.prerelease }}"
|
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
EVENT_NAME="${GITHUB_EVENT_NAME}"
|
EVENT_NAME="${GITHUB_EVENT_NAME}"
|
||||||
REF_NAME="${GITHUB_REF_NAME}"
|
REF_NAME="${GITHUB_REF_NAME}"
|
||||||
@@ -123,7 +138,7 @@ permissions:
|
|||||||
fi
|
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]+$' >/dev/null
|
||||||
|
|
||||||
SOURCE_BRANCH="${REF_NAME}"
|
SOURCE_BRANCH="${REF_NAME}"
|
||||||
SOURCE_PREFIX="${REF_NAME%%/*}"
|
SOURCE_PREFIX="${REF_NAME%%/*}"
|
||||||
@@ -153,9 +168,10 @@ permissions:
|
|||||||
|
|
||||||
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}"
|
||||||
VERSION="${VERSION%-rc}"
|
VERSION="${VERSION%-rc}"
|
||||||
echo "${VERSION}" | grep -E '^[0-9]+[.][0-9]+[.][0-9]+$'
|
echo "${VERSION}" | grep -E '^[0-9]+[.][0-9]+[.][0-9]+$' >/dev/null
|
||||||
|
|
||||||
if [ "${RELEASE_PRERELEASE:-false}" = "true" ]; then
|
if [ "${RELEASE_PRERELEASE:-false}" = "true" ]; then
|
||||||
CHANNEL="rc"
|
CHANNEL="rc"
|
||||||
@@ -196,6 +212,7 @@ permissions:
|
|||||||
echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT},"
|
echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT},"
|
||||||
echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\","
|
echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\","
|
||||||
echo " \"actor\": \"${GITHUB_ACTOR}\","
|
echo " \"actor\": \"${GITHUB_ACTOR}\","
|
||||||
|
echo " \"actor_permission\": \"${{ steps.auth.outputs.permission }}\","
|
||||||
echo " \"sha\": \"${GITHUB_SHA}\","
|
echo " \"sha\": \"${GITHUB_SHA}\","
|
||||||
echo " \"event\": \"${EVENT_NAME}\","
|
echo " \"event\": \"${EVENT_NAME}\","
|
||||||
echo " \"ref\": \"${REF_NAME}\","
|
echo " \"ref\": \"${REF_NAME}\","
|
||||||
@@ -211,6 +228,41 @@ permissions:
|
|||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
- name: Report run context (always)
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Run context"
|
||||||
|
echo "```json"
|
||||||
|
printf '{'
|
||||||
|
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
||||||
|
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
||||||
|
printf '"job":"%s",' "${GITHUB_JOB}"
|
||||||
|
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
||||||
|
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
||||||
|
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
||||||
|
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||||
|
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
||||||
|
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
||||||
|
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
||||||
|
printf '"sha":"%s",' "${GITHUB_SHA}"
|
||||||
|
printf '"runner_os":"%s",' "${RUNNER_OS}"
|
||||||
|
printf '"runner_name":"%s"' "${RUNNER_NAME}"
|
||||||
|
printf '}\n'
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Git snapshot"
|
||||||
|
echo "```"
|
||||||
|
git --version || true
|
||||||
|
git status --porcelain=v1 || true
|
||||||
|
git log -1 --pretty=fuller || true
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
promote_branch:
|
promote_branch:
|
||||||
name: 01 Promote branch and delete source
|
name: 01 Promote branch and delete source
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -230,14 +282,14 @@ permissions:
|
|||||||
|
|
||||||
- name: Configure Git identity
|
- name: Configure Git identity
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
- name: Enforce promotion preconditions
|
- name: Enforce promotion preconditions
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SRC="${{ needs.guard.outputs.source_branch }}"
|
SRC="${{ needs.guard.outputs.source_branch }}"
|
||||||
DST="${{ needs.guard.outputs.target_branch }}"
|
DST="${{ needs.guard.outputs.target_branch }}"
|
||||||
@@ -261,7 +313,7 @@ permissions:
|
|||||||
|
|
||||||
- name: Promote and delete source
|
- name: Promote and delete source
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SRC="${{ needs.guard.outputs.source_branch }}"
|
SRC="${{ needs.guard.outputs.source_branch }}"
|
||||||
DST="${{ needs.guard.outputs.target_branch }}"
|
DST="${{ needs.guard.outputs.target_branch }}"
|
||||||
@@ -270,12 +322,17 @@ permissions:
|
|||||||
git push origin "${DST}"
|
git push origin "${DST}"
|
||||||
git push origin --delete "${SRC}"
|
git push origin --delete "${SRC}"
|
||||||
|
|
||||||
$1
|
{
|
||||||
|
echo "### Promotion report"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"source\":\"${SRC}\",\"target\":\"${DST}\",\"status\":\"ok\"}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: Report run context (always)
|
- name: Report run context (always)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Run context"
|
echo "### Run context"
|
||||||
echo "```json"
|
echo "```json"
|
||||||
@@ -291,16 +348,7 @@ permissions:
|
|||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
||||||
printf '"sha":"%s"' "${GITHUB_SHA}"
|
printf '"sha":"%s"' "${GITHUB_SHA}"
|
||||||
printf '}
|
printf '}\n'
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Git snapshot"
|
|
||||||
echo "```"
|
|
||||||
git status --porcelain=v1 || true
|
|
||||||
git log -1 --pretty=fuller || true
|
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
@@ -325,14 +373,14 @@ permissions:
|
|||||||
|
|
||||||
- name: Configure Git identity
|
- name: Configure Git identity
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
- name: Validate repo prerequisites
|
- name: Validate repo prerequisites
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
test -d src || (echo "ERROR: src directory missing" && exit 1)
|
test -d src || (echo "ERROR: src directory missing" && exit 1)
|
||||||
test -f CHANGELOG.md || (echo "ERROR: CHANGELOG.md missing" && exit 1)
|
test -f CHANGELOG.md || (echo "ERROR: CHANGELOG.md missing" && exit 1)
|
||||||
|
|
||||||
@@ -345,7 +393,7 @@ permissions:
|
|||||||
|
|
||||||
- name: Normalize dates using repository script only
|
- name: Normalize dates using repository script only
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TODAY="${{ needs.guard.outputs.today_utc }}"
|
TODAY="${{ needs.guard.outputs.today_utc }}"
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
@@ -384,7 +432,7 @@ permissions:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Using date script: ${SCRIPT} (expected under scripts/release/)" >> "${GITHUB_STEP_SUMMARY}"
|
echo "Using date script: ${SCRIPT}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
chmod +x "${SCRIPT}"
|
chmod +x "${SCRIPT}"
|
||||||
"${SCRIPT}" "${TODAY}" "${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
"${SCRIPT}" "${TODAY}" "${VERSION}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
@@ -398,40 +446,19 @@ permissions:
|
|||||||
|
|
||||||
- name: Commit normalized dates (if changed)
|
- name: Commit normalized dates (if changed)
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
if git diff --quiet; then
|
if git diff --quiet; then
|
||||||
echo "No date changes to commit" >> "${GITHUB_STEP_SUMMARY}"
|
echo "No date changes to commit" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "chore(release): normalize dates" || true
|
git commit -m "chore(release): normalize dates" || true
|
||||||
$1
|
git push origin "HEAD:${{ needs.guard.outputs.promoted_branch }}"
|
||||||
|
|
||||||
- name: Report run context (always)
|
- name: Report run context (always)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Run context"
|
|
||||||
echo "```json"
|
|
||||||
printf '{'
|
|
||||||
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
|
||||||
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
|
||||||
printf '"job":"%s",' "${GITHUB_JOB}"
|
|
||||||
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
|
||||||
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
|
||||||
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
|
||||||
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
||||||
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
|
||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
|
||||||
printf '"sha":"%s"' "${GITHUB_SHA}"
|
|
||||||
printf '}
|
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Git snapshot"
|
echo "### Git snapshot"
|
||||||
echo "```"
|
echo "```"
|
||||||
@@ -463,22 +490,22 @@ permissions:
|
|||||||
|
|
||||||
- name: Configure Git identity
|
- name: Configure Git identity
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
- name: Validate required secrets and variables
|
- name: Validate required secrets and variables
|
||||||
env:
|
env:
|
||||||
FTP_HOST: "${{ secrets.FTP_HOST }}"
|
FTP_HOST: ${{ secrets.FTP_HOST }}
|
||||||
FTP_USER: "${{ secrets.FTP_USER }}"
|
FTP_USER: ${{ secrets.FTP_USER }}
|
||||||
FTP_KEY: "${{ secrets.FTP_KEY }}"
|
FTP_KEY: ${{ secrets.FTP_KEY }}
|
||||||
FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}"
|
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||||
FTP_PATH: "${{ secrets.FTP_PATH }}"
|
FTP_PATH: ${{ secrets.FTP_PATH }}
|
||||||
FTP_PROTOCOL: "${{ secrets.FTP_PROTOCOL }}"
|
FTP_PROTOCOL: ${{ secrets.FTP_PROTOCOL }}
|
||||||
FTP_PORT: "${{ secrets.FTP_PORT }}"
|
FTP_PORT: ${{ secrets.FTP_PORT }}
|
||||||
FTP_PATH_SUFFIX: "${{ vars.FTP_PATH_SUFFIX }}"
|
FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }}
|
||||||
CHANNEL: "${{ needs.guard.outputs.channel }}"
|
CHANNEL: ${{ needs.guard.outputs.channel }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -515,7 +542,7 @@ permissions:
|
|||||||
sep=""
|
sep=""
|
||||||
for m in "${missing[@]}"; do
|
for m in "${missing[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${m}"
|
printf '%s"%s"' "${sep}" "${m}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
printf '],"key_format":"%s","channel":"%s"}\n' "${key_format}" "${CHANNEL}"
|
printf '],"key_format":"%s","channel":"%s"}\n' "${key_format}" "${CHANNEL}"
|
||||||
echo "```"
|
echo "```"
|
||||||
@@ -527,7 +554,7 @@ permissions:
|
|||||||
|
|
||||||
- name: Run repository validation scripts (workflow-controlled)
|
- name: Run repository validation scripts (workflow-controlled)
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
required_scripts=(
|
required_scripts=(
|
||||||
"scripts/validate/validate_manifest.sh"
|
"scripts/validate/validate_manifest.sh"
|
||||||
@@ -560,7 +587,7 @@ permissions:
|
|||||||
sep=""
|
sep=""
|
||||||
for m in "${missing[@]}"; do
|
for m in "${missing[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${m}"
|
printf '%s"%s"' "${sep}" "${m}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
printf ']}\n'
|
printf ']}\n'
|
||||||
echo "```"
|
echo "```"
|
||||||
@@ -594,39 +621,38 @@ permissions:
|
|||||||
sep=""
|
sep=""
|
||||||
for s in "${required_scripts[@]}"; do
|
for s in "${required_scripts[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${s}"
|
printf '%s"%s"' "${sep}" "${s}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '],"optional":['
|
printf '],"optional":['
|
||||||
sep=""
|
sep=""
|
||||||
for s in "${optional_scripts[@]}"; do
|
for s in "${optional_scripts[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${s}"
|
printf '%s"%s"' "${sep}" "${s}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '],"ran":['
|
printf '],"ran":['
|
||||||
sep=""
|
sep=""
|
||||||
for s in "${ran[@]}"; do
|
for s in "${ran[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${s}"
|
printf '%s"%s"' "${sep}" "${s}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '],"skipped_optional":['
|
printf '],"skipped_optional":['
|
||||||
sep=""
|
sep=""
|
||||||
for s in "${skipped[@]}"; do
|
for s in "${skipped[@]}"; do
|
||||||
printf '%s"%s"' "${sep}" "${s}"
|
printf '%s"%s"' "${sep}" "${s}"
|
||||||
sep=",";
|
sep=","
|
||||||
done
|
done
|
||||||
|
|
||||||
printf ']}
|
printf ']}\n'
|
||||||
'
|
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: Build Joomla ZIP (extension type aware)
|
- name: Build Joomla ZIP (extension type aware, src-only archive)
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
REPO_NAME="${{ github.event.repository.name }}"
|
REPO_NAME="${{ github.event.repository.name }}"
|
||||||
@@ -637,6 +663,7 @@ permissions:
|
|||||||
DIST_DIR="${GITHUB_WORKSPACE}/dist"
|
DIST_DIR="${GITHUB_WORKSPACE}/dist"
|
||||||
mkdir -p "${DIST_DIR}"
|
mkdir -p "${DIST_DIR}"
|
||||||
|
|
||||||
|
# Detect manifest inside src for type naming only.
|
||||||
MANIFEST=""
|
MANIFEST=""
|
||||||
if [ -f "src/templateDetails.xml" ]; then
|
if [ -f "src/templateDetails.xml" ]; then
|
||||||
MANIFEST="src/templateDetails.xml"
|
MANIFEST="src/templateDetails.xml"
|
||||||
@@ -664,19 +691,18 @@ permissions:
|
|||||||
EXT_TYPE="unknown"
|
EXT_TYPE="unknown"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ROOT="$(dirname "${MANIFEST}")"
|
# Policy: archive must include ONLY the src directory tree (no repo root files).
|
||||||
|
|
||||||
ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${EXT_TYPE}.zip"
|
ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${EXT_TYPE}.zip"
|
||||||
|
|
||||||
(cd "${ROOT}" && zip -r -X "${DIST_DIR}/${ZIP}" . \
|
zip -r -X "${DIST_DIR}/${ZIP}" src \
|
||||||
-x "**/.git/**" \
|
-x "src/**/.git/**" \
|
||||||
-x "**/.github/**" \
|
-x "src/**/.github/**" \
|
||||||
-x "**/.DS_Store" \
|
-x "src/**/.DS_Store" \
|
||||||
-x "**/__MACOSX/**")
|
-x "src/**/__MACOSX/**"
|
||||||
|
|
||||||
echo "zip_name=${ZIP}" >> "${GITHUB_OUTPUT}"
|
echo "zip_name=${ZIP}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "dist_dir=${DIST_DIR}" >> "${GITHUB_OUTPUT}"
|
echo "dist_dir=${DIST_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "root=${ROOT}" >> "${GITHUB_OUTPUT}"
|
echo "root=src" >> "${GITHUB_OUTPUT}"
|
||||||
echo "manifest=${MANIFEST}" >> "${GITHUB_OUTPUT}"
|
echo "manifest=${MANIFEST}" >> "${GITHUB_OUTPUT}"
|
||||||
echo "ext_type=${EXT_TYPE}" >> "${GITHUB_OUTPUT}"
|
echo "ext_type=${EXT_TYPE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
@@ -685,13 +711,13 @@ permissions:
|
|||||||
{
|
{
|
||||||
echo "### Build report"
|
echo "### Build report"
|
||||||
echo "```json"
|
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 "{\"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\":\"src\",\"manifest\":\"${MANIFEST}\",\"extension_type\":\"${EXT_TYPE}\",\"zip\":\"${DIST_DIR}/${ZIP}\",\"zip_bytes\":${ZIP_BYTES},\"archive_policy\":\"src_only\"}"
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: ZIP inventory (audit)
|
- name: ZIP inventory (audit)
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
DIST_DIR="${{ steps.build.outputs.dist_dir }}"
|
DIST_DIR="${{ steps.build.outputs.dist_dir }}"
|
||||||
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||||||
@@ -706,25 +732,27 @@ permissions:
|
|||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: Upload ZIP to SFTP (key-only, overwrite, verbose)
|
- name: Upload ZIP to SFTP (key-preferred, password-fallback, overwrite, verified, classified)
|
||||||
|
id: sftp
|
||||||
env:
|
env:
|
||||||
FTP_HOST: "${{ secrets.FTP_HOST }}"
|
FTP_HOST: ${{ secrets.FTP_HOST }}
|
||||||
FTP_USER: "${{ secrets.FTP_USER }}"
|
FTP_USER: ${{ secrets.FTP_USER }}
|
||||||
FTP_KEY: "${{ secrets.FTP_KEY }}"
|
FTP_KEY: ${{ secrets.FTP_KEY }}
|
||||||
FTP_PASSWORD: "${{ secrets.FTP_PASSWORD }}"
|
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||||
FTP_PATH: "${{ secrets.FTP_PATH }}"
|
FTP_PATH: ${{ secrets.FTP_PATH }}
|
||||||
FTP_PROTOCOL: "${{ secrets.FTP_PROTOCOL }}"
|
FTP_PROTOCOL: ${{ secrets.FTP_PROTOCOL }}
|
||||||
FTP_PORT: "${{ secrets.FTP_PORT }}"
|
FTP_PORT: ${{ secrets.FTP_PORT }}
|
||||||
FTP_PATH_SUFFIX: "${{ vars.FTP_PATH_SUFFIX }}"
|
FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }}
|
||||||
CHANNEL: "${{ needs.guard.outputs.channel }}"
|
CHANNEL: ${{ needs.guard.outputs.channel }}
|
||||||
|
DEPLOY_DRY_RUN: ${{ vars.DEPLOY_DRY_RUN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ZIP="${{ steps.build.outputs.zip_name }}"
|
ZIP="${{ steps.build.outputs.zip_name }}"
|
||||||
|
DIST_DIR="${{ steps.build.outputs.dist_dir }}"
|
||||||
|
|
||||||
: "${FTP_HOST:?Missing secret FTP_HOST}"
|
: "${FTP_HOST:?Missing secret FTP_HOST}"
|
||||||
: "${FTP_USER:?Missing secret FTP_USER}"
|
: "${FTP_USER:?Missing secret FTP_USER}"
|
||||||
: "${FTP_KEY:?Missing secret FTP_KEY}"
|
|
||||||
: "${FTP_PATH:?Missing secret FTP_PATH}"
|
: "${FTP_PATH:?Missing secret FTP_PATH}"
|
||||||
|
|
||||||
PROTOCOL="${FTP_PROTOCOL:-sftp}"
|
PROTOCOL="${FTP_PROTOCOL:-sftp}"
|
||||||
@@ -746,28 +774,71 @@ permissions:
|
|||||||
else
|
else
|
||||||
REMOTE_PATH="${FTP_PATH%/}/${CHANNEL}"
|
REMOTE_PATH="${FTP_PATH%/}/${CHANNEL}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Guardrails: remote path safety.
|
||||||
|
if [ -z "${REMOTE_PATH}" ] || [ "${REMOTE_PATH}" = "/" ]; then
|
||||||
|
echo "ERROR: Unsafe REMOTE_PATH resolved (${REMOTE_PATH:-<empty>})" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if printf '%s' "${REMOTE_PATH}" | awk -F/ '{print NF-1}' | grep -Eq '^[01]$'; then
|
||||||
|
echo "ERROR: Remote path lacks depth guardrail: ${REMOTE_PATH}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AUTH_MODE=""
|
||||||
|
if [ -n "${FTP_KEY:-}" ]; then
|
||||||
|
AUTH_MODE="key"
|
||||||
|
else
|
||||||
|
AUTH_MODE="password"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Credential precedence: key wins when both are present.
|
||||||
|
PASSWORD_PRESENT="$( [ -n "${FTP_PASSWORD:-}" ] && echo true || echo false )"
|
||||||
|
KEY_PRESENT="$( [ -n "${FTP_KEY:-}" ] && echo true || echo false )"
|
||||||
|
|
||||||
|
if [ "${AUTH_MODE}" = "password" ] && [ -z "${FTP_PASSWORD:-}" ]; then
|
||||||
|
echo "ERROR: FTP_PASSWORD required when FTP_KEY is not provided" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DRY_RUN="${DEPLOY_DRY_RUN:-false}"
|
||||||
|
if [ "${DRY_RUN}" != "true" ]; then
|
||||||
|
DRY_RUN="false"
|
||||||
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Deployment intent"
|
echo "### Deployment intent"
|
||||||
echo "```json"
|
echo "```json"
|
||||||
printf '{'
|
printf '{'
|
||||||
printf '"protocol":"sftp",'
|
printf '"protocol":"sftp",'
|
||||||
|
printf '"auth_mode":"%s",' "${AUTH_MODE}"
|
||||||
printf '"host":"%s",' "${FTP_HOST}"
|
printf '"host":"%s",' "${FTP_HOST}"
|
||||||
printf '"port":"%s",' "${PORT:-default}"
|
printf '"port":"%s",' "${PORT:-default}"
|
||||||
printf '"remote_path":"%s",' "${REMOTE_PATH}"
|
printf '"remote_path":"%s",' "${REMOTE_PATH}"
|
||||||
printf '"overwrite":true,'
|
printf '"overwrite_policy":"same_filename_only",'
|
||||||
printf '"key_only":true'
|
printf '"cleanup_policy":"disabled",'
|
||||||
|
printf '"dry_run":%s,' "${DRY_RUN}"
|
||||||
|
printf '"zip":"%s",' "${ZIP}"
|
||||||
|
printf '"credential_presence":{'
|
||||||
|
printf '"FTP_KEY":"%s",' "$( [ "${KEY_PRESENT}" = "true" ] && echo present || echo missing )"
|
||||||
|
printf '"FTP_PASSWORD":"%s"' "$( [ "${PASSWORD_PRESENT}" = "true" ] && echo present || echo missing )"
|
||||||
|
printf '}'
|
||||||
printf '}
|
printf '}
|
||||||
'
|
'
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
echo "SFTP target: sftp://${HOSTPORT}${REMOTE_PATH}" >> "${GITHUB_STEP_SUMMARY}"
|
if [ "${KEY_PRESENT}" = "true" ] && [ "${PASSWORD_PRESENT}" = "true" ]; then
|
||||||
|
echo "Password provided but ignored because key auth is in use." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y lftp openssh-client putty-tools
|
sudo apt-get install -y lftp openssh-client putty-tools
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
|
||||||
|
if [ "${AUTH_MODE}" = "key" ]; then
|
||||||
if printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then
|
if printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then
|
||||||
printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk
|
printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk
|
||||||
chmod 600 ~/.ssh/key.ppk
|
chmod 600 ~/.ssh/key.ppk
|
||||||
@@ -779,7 +850,7 @@ permissions:
|
|||||||
echo "ERROR: Encrypted PPK detected but FTP_PASSWORD not provided" >> "${GITHUB_STEP_SUMMARY}"
|
echo "ERROR: Encrypted PPK detected but FTP_PASSWORD not provided" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
PPK_PASSPHRASE="${FTP_PASSWORD:-}"
|
PPK_PASSPHRASE="${FTP_PASSWORD}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${PPK_PASSPHRASE}" ]; then
|
if [ -n "${PPK_PASSPHRASE}" ]; then
|
||||||
@@ -788,46 +859,149 @@ permissions:
|
|||||||
puttygen ~/.ssh/key.ppk -O private-openssh -o ~/.ssh/id_rsa
|
puttygen ~/.ssh/key.ppk -O private-openssh -o ~/.ssh/id_rsa
|
||||||
fi
|
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
|
rm -f ~/.ssh/key.ppk
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
else
|
else
|
||||||
printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa
|
printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
ssh-keyscan -H "${FTP_HOST}" >> ~/.ssh/known_hosts
|
ssh-keyscan -H "${FTP_HOST}" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
if [ "${AUTH_MODE}" = "key" ]; then
|
||||||
|
CONNECT="set sftp:connect-program 'ssh -a -x -i ~/.ssh/id_rsa -o PubkeyAuthentication=yes -o PasswordAuthentication=no'"
|
||||||
|
OPEN="open -u '${FTP_USER}', sftp://${HOSTPORT}"
|
||||||
|
else
|
||||||
|
CONNECT="set sftp:connect-program 'ssh -a -x -o PubkeyAuthentication=no -o PasswordAuthentication=yes'"
|
||||||
|
OPEN="open -u '${FTP_USER}','${FTP_PASSWORD}', sftp://${HOSTPORT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ZIP_BYTES_LOCAL="$(stat -c%s "${DIST_DIR}/${ZIP}")"
|
||||||
|
|
||||||
|
# Preflight: remote collision detection and directory validation.
|
||||||
|
set +e
|
||||||
|
preflight_log="$(mktemp)"
|
||||||
lftp -d -e "\
|
lftp -d -e "\
|
||||||
set sftp:auto-confirm yes; \
|
set sftp:auto-confirm yes; \
|
||||||
set cmd:trace yes; \
|
set cmd:trace yes; \
|
||||||
set net:timeout 30; \
|
set net:timeout 30; \
|
||||||
set net:max-retries 3; \
|
set net:max-retries 3; \
|
||||||
set net:reconnect-interval-base 5; \
|
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'; \
|
${CONNECT}; \
|
||||||
open -u '${FTP_USER}', sftp://${HOSTPORT}; \
|
${OPEN}; \
|
||||||
mkdir -p '${REMOTE_PATH}'; \
|
mkdir -p '${REMOTE_PATH}'; \
|
||||||
cd '${REMOTE_PATH}'; \
|
cd '${REMOTE_PATH}'; \
|
||||||
put -E '${{ steps.build.outputs.dist_dir }}/${ZIP}'; \
|
ls -la; \
|
||||||
ls; \
|
bye" >"${preflight_log}" 2>&1
|
||||||
bye"
|
preflight_rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "${preflight_rc}" -ne 0 ]; then
|
||||||
|
cat "${preflight_log}" >> "${GITHUB_STEP_SUMMARY}" || true
|
||||||
|
exit "${preflight_rc}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -F " ${ZIP}" "${preflight_log}" >/dev/null 2>&1; then
|
||||||
|
echo "Remote file already exists and will be overwritten (same filename policy): ${ZIP}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
else
|
||||||
|
echo "Remote file not present, proceeding with first publish: ${ZIP}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${DRY_RUN}" = "true" ]; then
|
||||||
|
{
|
||||||
|
echo "### Dry run"
|
||||||
|
echo "Dry run enabled. Upload skipped."
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
echo "auth_mode=${AUTH_MODE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "remote_path=${REMOTE_PATH}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "host=${FTP_HOST}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "port=${PORT:-default}" >> "${GITHUB_OUTPUT}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload + verify with failure classification.
|
||||||
|
set +e
|
||||||
|
upload_log="$(mktemp)"
|
||||||
|
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; \
|
||||||
|
${CONNECT}; \
|
||||||
|
${OPEN}; \
|
||||||
|
cd '${REMOTE_PATH}'; \
|
||||||
|
put -E '${DIST_DIR}/${ZIP}'; \
|
||||||
|
ls -l; \
|
||||||
|
bye" >"${upload_log}" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
failure_class="none"
|
||||||
|
if [ "${rc}" -ne 0 ]; then
|
||||||
|
if grep -Ei 'auth|authentication|login failed' "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
failure_class="auth_failure"
|
||||||
|
elif grep -Ei 'name or service not known|temporary failure in name resolution|no such host' "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
failure_class="dns_failure"
|
||||||
|
elif grep -Ei 'connection timed out|timeout' "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
failure_class="timeout"
|
||||||
|
elif grep -Ei 'no route to host|network is unreachable|connection refused' "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
failure_class="network_failure"
|
||||||
|
elif grep -Ei 'permission denied' "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
failure_class="permission_denied"
|
||||||
|
else
|
||||||
|
failure_class="unknown"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always attach upload log to summary (bounded by job log limits, but critical for audit).
|
||||||
|
{
|
||||||
|
echo "### SFTP session log"
|
||||||
|
echo "```"
|
||||||
|
tail -n 400 "${upload_log}" || true
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}" || true
|
||||||
|
|
||||||
|
if [ "${rc}" -ne 0 ]; then
|
||||||
|
{
|
||||||
|
echo "### SFTP failure classification"
|
||||||
|
echo "```json"
|
||||||
|
echo "{\"status\":\"fail\",\"class\":\"${failure_class}\",\"exit_code\":${rc}}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit "${rc}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verification: ensure ZIP appears in directory listing.
|
||||||
|
if ! grep -F " ${ZIP}" "${upload_log}" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: Upload completed but verification failed. ZIP not visible in remote listing." >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ZIP_BYTES_REMOTE="unknown"
|
||||||
|
# Best-effort size extraction from ls -l output (platform dependent).
|
||||||
|
ZIP_BYTES_REMOTE="$(awk -v z="${ZIP}" '$NF==z {print $(NF-4)}' "${upload_log}" | tail -n 1 || true)"
|
||||||
|
if [ -z "${ZIP_BYTES_REMOTE}" ]; then
|
||||||
|
ZIP_BYTES_REMOTE="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
ZIP_BYTES="$(stat -c%s "${{ steps.build.outputs.dist_dir }}/${ZIP}")"
|
|
||||||
{
|
{
|
||||||
echo "### SFTP upload report"
|
echo "### SFTP upload report"
|
||||||
echo "```json"
|
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 "{\"status\":\"ok\",\"protocol\":\"sftp\",\"auth_mode\":\"${AUTH_MODE}\",\"host\":\"${FTP_HOST}\",\"port\":\"${PORT:-default}\",\"remote_path\":\"${REMOTE_PATH}\",\"zip\":\"${ZIP}\",\"zip_bytes_local\":${ZIP_BYTES_LOCAL},\"zip_bytes_remote\":\"${ZIP_BYTES_REMOTE}\",\"overwrite\":true,\"cleanup_policy\":\"disabled\"}"
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
|
echo "auth_mode=${AUTH_MODE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "remote_path=${REMOTE_PATH}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "host=${FTP_HOST}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "port=${PORT:-default}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Create Git tag
|
- name: Create Git tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${{ needs.guard.outputs.version }}"
|
VERSION="${{ needs.guard.outputs.version }}"
|
||||||
MODE="${{ needs.guard.outputs.release_mode }}"
|
MODE="${{ needs.guard.outputs.release_mode }}"
|
||||||
@@ -850,7 +1024,7 @@ permissions:
|
|||||||
|
|
||||||
- name: Generate release notes from CHANGELOG.md
|
- name: Generate release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
set -euxo 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 }}"
|
||||||
@@ -866,6 +1040,12 @@ permissions:
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Assets:"
|
echo "Assets:"
|
||||||
echo "- ${ZIP_ASSET}"
|
echo "- ${ZIP_ASSET}"
|
||||||
|
echo ""
|
||||||
|
echo "Deployment metadata:"
|
||||||
|
echo "- auth_mode: ${{ steps.sftp.outputs.auth_mode || 'unknown' }}"
|
||||||
|
echo "- remote_path: ${{ steps.sftp.outputs.remote_path || 'unknown' }}"
|
||||||
|
echo "- host: ${{ steps.sftp.outputs.host || 'unknown' }}"
|
||||||
|
echo "- port: ${{ steps.sftp.outputs.port || 'unknown' }}"
|
||||||
} >> RELEASE_NOTES.md
|
} >> RELEASE_NOTES.md
|
||||||
|
|
||||||
- name: Create GitHub release and attach ZIP
|
- name: Create GitHub release and attach ZIP
|
||||||
@@ -884,33 +1064,10 @@ permissions:
|
|||||||
subject-path: |
|
subject-path: |
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
|
|
||||||
$1
|
|
||||||
|
|
||||||
- name: Report run context (always)
|
- name: Report run context (always)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Run context"
|
|
||||||
echo "```json"
|
|
||||||
printf '{'
|
|
||||||
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
|
||||||
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
|
||||||
printf '"job":"%s",' "${GITHUB_JOB}"
|
|
||||||
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
|
||||||
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
|
||||||
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
|
||||||
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
||||||
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
|
||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
|
||||||
printf '"sha":"%s"' "${GITHUB_SHA}"
|
|
||||||
printf '}
|
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Git snapshot"
|
echo "### Git snapshot"
|
||||||
echo "```"
|
echo "```"
|
||||||
@@ -941,14 +1098,14 @@ permissions:
|
|||||||
|
|
||||||
- name: Configure Git identity
|
- name: Configure Git identity
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euo pipefail
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
git config --global --add safe.directory "${GITHUB_WORKSPACE}"
|
||||||
|
|
||||||
- name: Create PR from version branch to main
|
- name: Create PR from version branch to main
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: "${{ github.token }}"
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -962,9 +1119,9 @@ permissions:
|
|||||||
--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
|
||||||
|
|
||||||
- name: Attempt to merge PR (best-effort)
|
- name: Attempt to merge PR (best effort)
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: "${{ github.token }}"
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -979,38 +1136,14 @@ permissions:
|
|||||||
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}"
|
||||||
|
|
||||||
$1
|
|
||||||
|
|
||||||
- name: Report run context (always)
|
- name: Report run context (always)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Run context"
|
echo "### Main promotion report"
|
||||||
echo "```json"
|
echo "```json"
|
||||||
printf '{'
|
echo "{\"head\":\"${{ needs.guard.outputs.promoted_branch }}\",\"base\":\"main\",\"release_mode\":\"${{ needs.guard.outputs.release_mode }}\"}"
|
||||||
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
|
||||||
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
|
||||||
printf '"job":"%s",' "${GITHUB_JOB}"
|
|
||||||
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
|
||||||
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
|
||||||
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
|
||||||
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
||||||
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
|
||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
|
||||||
printf '"sha":"%s"' "${GITHUB_SHA}"
|
|
||||||
printf '}
|
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Git snapshot"
|
|
||||||
echo "```"
|
|
||||||
git status --porcelain=v1 || true
|
|
||||||
git log -1 --pretty=fuller || true
|
|
||||||
echo "```"
|
echo "```"
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
@@ -1029,35 +1162,31 @@ permissions:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
fetch-depth: 0
|
fetch-depth: 1
|
||||||
|
|
||||||
$1
|
- name: Release event telemetry
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Release event telemetry"
|
||||||
|
echo "```json"
|
||||||
|
echo "{"
|
||||||
|
echo " \"repository\": \"${GITHUB_REPOSITORY}\","
|
||||||
|
echo " \"event\": \"${GITHUB_EVENT_NAME}\","
|
||||||
|
echo " \"ref_name\": \"${GITHUB_REF_NAME}\","
|
||||||
|
echo " \"sha\": \"${GITHUB_SHA}\","
|
||||||
|
echo " \"channel\": \"${{ needs.guard.outputs.channel }}\","
|
||||||
|
echo " \"release_mode\": \"${{ needs.guard.outputs.release_mode }}\","
|
||||||
|
echo " \"version\": \"${{ needs.guard.outputs.version }}\""
|
||||||
|
echo "}"
|
||||||
|
echo "```"
|
||||||
|
} >> "${GITHUB_STEP_SUMMARY}"
|
||||||
|
|
||||||
- name: Report run context (always)
|
- name: Report run context (always)
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Run context"
|
|
||||||
echo "```json"
|
|
||||||
printf '{'
|
|
||||||
printf '"repository":"%s",' "${GITHUB_REPOSITORY}"
|
|
||||||
printf '"workflow":"%s",' "${GITHUB_WORKFLOW}"
|
|
||||||
printf '"job":"%s",' "${GITHUB_JOB}"
|
|
||||||
printf '"run_id":%s,' "${GITHUB_RUN_ID}"
|
|
||||||
printf '"run_number":%s,' "${GITHUB_RUN_NUMBER}"
|
|
||||||
printf '"run_attempt":%s,' "${GITHUB_RUN_ATTEMPT}"
|
|
||||||
printf '"run_url":"%s",' "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
|
||||||
printf '"actor":"%s",' "${GITHUB_ACTOR}"
|
|
||||||
printf '"event":"%s",' "${GITHUB_EVENT_NAME}"
|
|
||||||
printf '"ref_name":"%s",' "${GITHUB_REF_NAME}"
|
|
||||||
printf '"sha":"%s"' "${GITHUB_SHA}"
|
|
||||||
printf '}
|
|
||||||
'
|
|
||||||
echo "```"
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "### Git snapshot"
|
echo "### Git snapshot"
|
||||||
echo "```"
|
echo "```"
|
||||||
|
|||||||
Reference in New Issue
Block a user