diff --git a/.github/workflows/release_pipeline.yml b/.github/workflows/release_pipeline.yml index 92766fd..3cd044e 100644 --- a/.github/workflows/release_pipeline.yml +++ b/.github/workflows/release_pipeline.yml @@ -56,7 +56,6 @@ defaults: run: shell: bash -# Default permissions are minimized; jobs elevate as needed. permissions: contents: read @@ -79,7 +78,6 @@ jobs: permissions: contents: read actions: read - # Required for permissions check via REST API pull-requests: read steps: @@ -203,28 +201,28 @@ jobs: { echo "### Guard report" echo "```json" - echo "{" - echo " \"repository\": \"${GITHUB_REPOSITORY}\"," - echo " \"workflow\": \"${GITHUB_WORKFLOW}\"," - echo " \"job\": \"${GITHUB_JOB}\"," - echo " \"run_id\": ${GITHUB_RUN_ID}," - echo " \"run_number\": ${GITHUB_RUN_NUMBER}," - echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT}," - echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\"," - echo " \"actor\": \"${GITHUB_ACTOR}\"," - echo " \"actor_permission\": \"${{ steps.auth.outputs.permission }}\"," - echo " \"sha\": \"${GITHUB_SHA}\"," - echo " \"event\": \"${EVENT_NAME}\"," - echo " \"ref\": \"${REF_NAME}\"," - echo " \"version\": \"${VERSION}\"," - echo " \"source_branch\": \"${SOURCE_BRANCH}\"," - echo " \"target_branch\": \"${TARGET_BRANCH}\"," - echo " \"promoted_branch\": \"${PROMOTED_BRANCH}\"," - echo " \"channel\": \"${CHANNEL}\"," - echo " \"release_mode\": \"${RELEASE_MODE}\"," - echo " \"override\": \"${OVERRIDE}\"," - echo " \"today_utc\": \"${TODAY_UTC}\"" - echo "}" + echo "{" + echo " \"repository\": \"${GITHUB_REPOSITORY}\"," + echo " \"workflow\": \"${GITHUB_WORKFLOW}\"," + echo " \"job\": \"${GITHUB_JOB}\"," + echo " \"run_id\": ${GITHUB_RUN_ID}," + echo " \"run_number\": ${GITHUB_RUN_NUMBER}," + echo " \"run_attempt\": ${GITHUB_RUN_ATTEMPT}," + echo " \"run_url\": \"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}\"," + echo " \"actor\": \"${GITHUB_ACTOR}\"," + echo " \"actor_permission\": \"${{ steps.auth.outputs.permission }}\"," + echo " \"sha\": \"${GITHUB_SHA}\"," + echo " \"event\": \"${EVENT_NAME}\"," + echo " \"ref\": \"${REF_NAME}\"," + echo " \"version\": \"${VERSION}\"," + echo " \"source_branch\": \"${SOURCE_BRANCH}\"," + echo " \"target_branch\": \"${TARGET_BRANCH}\"," + echo " \"promoted_branch\": \"${PROMOTED_BRANCH}\"," + echo " \"channel\": \"${CHANNEL}\"," + echo " \"release_mode\": \"${RELEASE_MODE}\"," + echo " \"override\": \"${OVERRIDE}\"," + echo " \"today_utc\": \"${TODAY_UTC}\"" + echo "}" echo "```" } >> "${GITHUB_STEP_SUMMARY}" @@ -250,7 +248,8 @@ jobs: printf '"sha":"%s",' "${GITHUB_SHA}" printf '"runner_os":"%s",' "${RUNNER_OS}" printf '"runner_name":"%s"' "${RUNNER_NAME}" - printf '}\n' + printf '} +' echo "```" } >> "${GITHUB_STEP_SUMMARY}" @@ -348,7 +347,8 @@ jobs: printf '"event":"%s",' "${GITHUB_EVENT_NAME}" printf '"ref_name":"%s",' "${GITHUB_REF_NAME}" printf '"sha":"%s"' "${GITHUB_SHA}" - printf '}\n' + printf '} +' echo "```" } >> "${GITHUB_STEP_SUMMARY}" @@ -424,7 +424,8 @@ jobs: { echo "ERROR: Date normalization script not found in approved locations." echo "Approved locations:" - printf '%s\n' "${CANDIDATES[@]}" + printf '%s +' "${CANDIDATES[@]}" echo "Discovered candidates (first 5):" echo "${FOUND:-}" echo "Required action: add scripts/release/update_dates.sh (preferred) to the repo." @@ -506,6 +507,7 @@ jobs: FTP_PORT: ${{ secrets.FTP_PORT }} FTP_PATH_SUFFIX: ${{ vars.FTP_PATH_SUFFIX }} CHANNEL: ${{ needs.guard.outputs.channel }} + DEPLOY_DRY_RUN: ${{ vars.DEPLOY_DRY_RUN }} run: | set -euo pipefail @@ -513,7 +515,6 @@ jobs: [ -n "${FTP_HOST:-}" ] || missing+=("FTP_HOST") [ -n "${FTP_USER:-}" ] || missing+=("FTP_USER") - [ -n "${FTP_KEY:-}" ] || missing+=("FTP_KEY") [ -n "${FTP_PATH:-}" ] || missing+=("FTP_PATH") proto="${FTP_PROTOCOL:-sftp}" @@ -521,18 +522,23 @@ jobs: missing+=("FTP_PROTOCOL_INVALID") fi - first_line="$(printf '%s' "${FTP_KEY:-}" | head -n 1 || true)" + key_present=false if [ -n "${FTP_KEY:-}" ]; then - if printf '%s' "${first_line}" | grep -q '^PuTTY-User-Key-File-'; then - key_format="ppk" - elif printf '%s' "${first_line}" | grep -q '^-----BEGIN '; then - key_format="openssh" - else - key_format="unknown" - missing+=("FTP_KEY_FORMAT") - fi - else - key_format="missing" + key_present=true + fi + + pw_present=false + if [ -n "${FTP_PASSWORD:-}" ]; then + pw_present=true + fi + + auth_mode="password" + if [ "${key_present}" = "true" ]; then + auth_mode="key" + fi + + if [ "${auth_mode}" = "password" ] && [ "${pw_present}" != "true" ]; then + missing+=("FTP_PASSWORD_REQUIRED") fi { @@ -542,9 +548,13 @@ jobs: sep="" for m in "${missing[@]}"; do printf '%s"%s"' "${sep}" "${m}" - sep="," + sep=","; done - printf '],"key_format":"%s","channel":"%s"}\n' "${key_format}" "${CHANNEL}" + printf '],"channel":"%s","deploy_dry_run":"%s","credential_presence":{"FTP_KEY":"%s","FTP_PASSWORD":"%s"}} +' \ + "${CHANNEL}" "${DEPLOY_DRY_RUN:-false}" \ + "$( [ "${key_present}" = "true" ] && echo present || echo missing )" \ + "$( [ "${pw_present}" = "true" ] && echo present || echo missing )" echo "```" } >> "${GITHUB_STEP_SUMMARY}" @@ -587,9 +597,10 @@ jobs: sep="" for m in "${missing[@]}"; do printf '%s"%s"' "${sep}" "${m}" - sep="," + sep=","; done - printf ']}\n' + printf ']} +' echo "```" } >> "${GITHUB_STEP_SUMMARY}" exit 1 @@ -621,31 +632,32 @@ jobs: sep="" for s in "${required_scripts[@]}"; do printf '%s"%s"' "${sep}" "${s}" - sep="," + sep=","; done printf '],"optional":[' sep="" for s in "${optional_scripts[@]}"; do printf '%s"%s"' "${sep}" "${s}" - sep="," + sep=","; done printf '],"ran":[' sep="" for s in "${ran[@]}"; do printf '%s"%s"' "${sep}" "${s}" - sep="," + sep=","; done printf '],"skipped_optional":[' sep="" for s in "${skipped[@]}"; do printf '%s"%s"' "${sep}" "${s}" - sep="," + sep=","; done - printf ']}\n' + printf ']} +' echo "```" } >> "${GITHUB_STEP_SUMMARY}" @@ -663,7 +675,6 @@ jobs: DIST_DIR="${GITHUB_WORKSPACE}/dist" mkdir -p "${DIST_DIR}" - # Detect manifest inside src for type naming only. MANIFEST="" if [ -f "src/templateDetails.xml" ]; then MANIFEST="src/templateDetails.xml" @@ -691,7 +702,6 @@ jobs: EXT_TYPE="unknown" fi - # Policy: archive must include ONLY the src directory tree (no repo root files). ZIP="${REPO_NAME}-${VERSION}-${CHANNEL}-${EXT_TYPE}.zip" zip -r -X "${DIST_DIR}/${ZIP}" src \ @@ -775,7 +785,6 @@ jobs: REMOTE_PATH="${FTP_PATH%/}/${CHANNEL}" fi - # Guardrails: remote path safety. if [ -z "${REMOTE_PATH}" ] || [ "${REMOTE_PATH}" = "/" ]; then echo "ERROR: Unsafe REMOTE_PATH resolved (${REMOTE_PATH:-})" >> "${GITHUB_STEP_SUMMARY}" exit 1 @@ -785,18 +794,15 @@ jobs: exit 1 fi - AUTH_MODE="" + AUTH_MODE="password" 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 + if [ "${AUTH_MODE}" = "password" ] && [ "${PASSWORD_PRESENT}" != "true" ]; then echo "ERROR: FTP_PASSWORD required when FTP_KEY is not provided" >> "${GITHUB_STEP_SUMMARY}" exit 1 fi @@ -879,7 +885,6 @@ jobs: ZIP_BYTES_LOCAL="$(stat -c%s "${DIST_DIR}/${ZIP}")" - # Preflight: remote collision detection and directory validation. set +e preflight_log="$(mktemp)" lftp -d -e "\ @@ -898,7 +903,12 @@ jobs: set -e if [ "${preflight_rc}" -ne 0 ]; then - cat "${preflight_log}" >> "${GITHUB_STEP_SUMMARY}" || true + { + echo "### SFTP preflight log" + echo "```" + tail -n 400 "${preflight_log}" || true + echo "```" + } >> "${GITHUB_STEP_SUMMARY}" || true exit "${preflight_rc}" fi @@ -920,7 +930,6 @@ jobs: exit 0 fi - # Upload + verify with failure classification. set +e upload_log="$(mktemp)" lftp -d -e "\ @@ -955,7 +964,6 @@ jobs: fi fi - # Always attach upload log to summary (bounded by job log limits, but critical for audit). { echo "### SFTP session log" echo "```" @@ -973,14 +981,12 @@ jobs: 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" @@ -1171,15 +1177,15 @@ jobs: { 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 "{" + 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}"