Update release_pipeline.yml

This commit is contained in:
2025-12-30 18:09:37 -06:00
parent 589b4e2f12
commit 622395faac

View File

@@ -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,88 +774,234 @@ 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 printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then if [ "${AUTH_MODE}" = "key" ]; then
printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk if printf '%s' "${FTP_KEY}" | head -n 1 | grep -q '^PuTTY-User-Key-File-'; then
chmod 600 ~/.ssh/key.ppk printf '%s' "${FTP_KEY}" > ~/.ssh/key.ppk
chmod 600 ~/.ssh/key.ppk
if grep -Eq '^Encryption: *none[[:space:]]*$' ~/.ssh/key.ppk; then if grep -Eq '^Encryption: *none[[:space:]]*$' ~/.ssh/key.ppk; then
PPK_PASSPHRASE="" PPK_PASSPHRASE=""
else else
if [ -z "${FTP_PASSWORD:-}" ]; then if [ -z "${FTP_PASSWORD:-}" ]; then
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
PPK_PASSPHRASE="${FTP_PASSWORD}"
fi fi
PPK_PASSPHRASE="${FTP_PASSWORD:-}"
fi
if [ -n "${PPK_PASSPHRASE}" ]; then if [ -n "${PPK_PASSPHRASE}" ]; then
puttygen ~/.ssh/key.ppk -O private-openssh --passphrase "${PPK_PASSPHRASE}" -o ~/.ssh/id_rsa 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
rm -f ~/.ssh/key.ppk
chmod 600 ~/.ssh/id_rsa
else else
puttygen ~/.ssh/key.ppk -O private-openssh -o ~/.ssh/id_rsa printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.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
else
printf '%s' "${FTP_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
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 "```"