diff --git a/docs/workflows/renovate-config-lint.md b/docs/workflows/renovate-config-lint.md index 52740f9..4a5f96b 100644 --- a/docs/workflows/renovate-config-lint.md +++ b/docs/workflows/renovate-config-lint.md @@ -5,6 +5,10 @@ This workflow validates `renovate.json`, resolves shared presets, checks dependency lookups, and reports Renovate health failures as a GitHub issue. +The template orchestrates three version-pinned actions from `netcracker/qubership-workflow-hub`: +`renovate-validate`, `renovate-lookup`, and `renovate-monitor`. The actions are designed to run together in this +workflow. The caller retains its schedule, Renovate image, permissions, and repository-specific policy tests. + ## Requirements - Add `renovate.json` to the repository root. @@ -41,10 +45,9 @@ The monitor checks the hosted Dependency Dashboard for repository problems, erro failures. It creates one `Renovate health check failed` issue with links to the failed health-check run and the Dependency Dashboard. -The monitor ignores only the two standard `minimumReleaseAgeBehaviour=timestamp-optional` notices that say missing -release timestamps for releases or upgrades are allowed to proceed. When these are the only repository problems, the -workflow summary reports `Dependency Dashboard only reports expected timestamp-optional notices`. Any other repository -problem remains actionable. +The monitor recognizes the two standard `minimumReleaseAgeBehaviour=timestamp-optional` notices and the hosted package +lookup warning. It keeps recognized warnings visible in the summary. A package lookup warning is healthy only when the +independent local lookup succeeds. Any other repository problem remains actionable. If GitHub Issues are disabled, the workflow skips Dashboard and incident-issue operations. Validation and dependency lookup still run, and their failures remain visible in the workflow run. @@ -68,6 +71,17 @@ The schedule runs from the default branch. GitHub Actions can delay scheduled wo - The monitor job uses `contents: read` and `issues: write`. - Pull request workflows do not run the issue-writing monitor. +## Action versions + +Pin each Workflow Hub action to the full commit SHA of a published SemVer release and keep the release tag in a trailing +comment. Renovate then updates the SHA and version comment when Workflow Hub publishes a compatible release. + +```yaml +uses: netcracker/qubership-workflow-hub/actions/renovate-validate@ # vX.Y.Z +``` + +Do not merge a template that points to a pull request commit or a branch. + ## Verification 1. Change `renovate.json` in a pull request and confirm that `Validate renovate.json` succeeds. diff --git a/workflow-templates/renovate-config-lint.yaml b/workflow-templates/renovate-config-lint.yaml index d4441f8..e6c2094 100644 --- a/workflow-templates/renovate-config-lint.yaml +++ b/workflow-templates/renovate-config-lint.yaml @@ -15,12 +15,14 @@ on: - cron: "17 8 * * 1" workflow_dispatch: {} +permissions: {} + jobs: validate-renovate-config: name: Validate renovate.json runs-on: ubuntu-latest outputs: - reason: ${{ steps.validate-config.outputs.reason || steps.resolve-presets.outputs.reason }} + reason: ${{ steps.validate.outputs.reason }} container: image: renovate/renovate:43.285.4@sha256:bd7d8f646bf9d22aea995eaad06ec26c3f4fd4efbc36826024f5653006c9e7b1 options: --user 0 @@ -32,62 +34,11 @@ jobs: with: persist-credentials: false - - name: Validate renovate.json - id: validate-config - shell: bash - run: | - set +e - renovate-config-validator --strict --no-global renovate.json - validation_status="$?" - set -e - - if [[ "$validation_status" -ne 0 ]]; then - echo 'reason=renovate.json failed strict validation' >> "$GITHUB_OUTPUT" - exit "$validation_status" - fi - - - name: Resolve shared Renovate presets - id: resolve-presets - shell: bash + - name: Validate configuration and resolve shared presets + id: validate + uses: netcracker/qubership-workflow-hub/actions/renovate-validate@77786061d16fabdb6e089c2ba1c5846da3efc62a # PR 951 env: GITHUB_COM_TOKEN: ${{ github.token }} - LOG_FORMAT: json - LOG_LEVEL: warn - run: | - set -o pipefail - log_file="$(mktemp)" - trap 'rm -f "$log_file"' EXIT - records_filter='split("\n") | map(fromjson?) | map(select(type == "object"))' - rate_limit_filter='select((.msg // "") | test("Rate limit exceeded"))' - validation_filter='select(.msg == "Repository has invalid config" or ((.err | type) == "object" and .err.validationError?))' - - set +e - renovate --platform=local --dry-run=extract 2>&1 | tee "$log_file" - renovate_status="${PIPESTATUS[0]}" - set -e - - rate_limit_count="$(jq -R -s "$records_filter | map($rate_limit_filter) | length" "$log_file")" - validation_count="$(jq -R -s "$records_filter | map($validation_filter) | length" "$log_file")" - - if [[ "$rate_limit_count" -ne 0 ]]; then - echo 'reason=GitHub API rate limit prevented shared preset resolution' >> "$GITHUB_OUTPUT" - echo "::error::GitHub API rate limit hit; preset resolution could not be verified" - jq -R -s -r "$records_filter | map($rate_limit_filter) | .[] | .msg" "$log_file" - exit 1 - fi - - if [[ "$validation_count" -ne 0 ]]; then - echo 'reason=Renovate could not resolve the repository configuration' >> "$GITHUB_OUTPUT" - echo "::error::Renovate could not resolve the repository configuration" - jq -R -s -r "$records_filter | map($validation_filter) | .[] | if (.err | type) == \"object\" then (.err.validationError // .msg) else .msg end" "$log_file" - exit 1 - fi - - if [[ "$renovate_status" -ne 0 ]]; then - echo "reason=Renovate preset resolution exited with code $renovate_status" >> "$GITHUB_OUTPUT" - echo "::error::Renovate preset resolution exited with code $renovate_status" - exit "$renovate_status" - fi lookup-renovate-dependencies: # Set the Actions variable RENOVATE_HEALTH_CHECK to false when this @@ -113,131 +64,9 @@ jobs: - name: Look up dependencies id: lookup - shell: bash + uses: netcracker/qubership-workflow-hub/actions/renovate-lookup@77786061d16fabdb6e089c2ba1c5846da3efc62a # PR 951 env: GITHUB_COM_TOKEN: ${{ github.token }} - LOG_FORMAT: json - LOG_LEVEL: debug - run: | - set -euo pipefail - log_file="$(mktemp)" - first_log_file="$(mktemp)" - cache_root="$(mktemp -d)" - trap 'rm -f "$log_file" "$first_log_file"; rm -rf "$cache_root"' EXIT - records_filter='split("\n") | map(fromjson?) | map(select(type == "object"))' - error_filter='select((.level // 0) >= 50 or .msg == "Repository has invalid config" or ((.err | type) == "object" and .err.validationError?))' - lookup_failure_filter='select((.msg // "") | startswith("Failed to look up"))' - rate_limit_filter='select((.msg // "") | test("Rate limit exceeded"))' - warning_filter='select((.level // 0) >= 40 and (.level // 0) < 50 and (((.msg // "") | test("Rate limit exceeded")) | not))' - - run_lookup() { - lookup_attempt_count=$((lookup_attempt_count + 1)) - attempt_cache_dir="$(mktemp -d "$cache_root/attempt.XXXXXX")" - set +e - RENOVATE_CACHE_DIR="$attempt_cache_dir" renovate --platform=local --dry-run=lookup > "$log_file" 2>&1 - renovate_status="$?" - set -e - } - - analyze_lookup() { - error_count="$(jq -R -s "$records_filter | map($error_filter) | length" "$log_file")" - lookup_failure_count="$(jq -R -s "$records_filter | map($lookup_failure_filter) | length" "$log_file")" - rate_limit_count="$(jq -R -s "$records_filter | map($rate_limit_filter) | length" "$log_file")" - warning_count="$(jq -R -s "$records_filter | map($warning_filter) | length" "$log_file")" - } - - lookup_attempt_count=0 - lookup_retry_count=0 - initial_lookup_failure_count=0 - retry_succeeded=false - run_lookup - analyze_lookup - - if [[ "$renovate_status" -eq 0 && "$lookup_failure_count" -ne 0 && "$rate_limit_count" -eq 0 && "$error_count" -eq 0 ]]; then - lookup_retry_count=1 - initial_lookup_failure_count="$lookup_failure_count" - cp "$log_file" "$first_log_file" - echo '::notice::Retrying dependency lookup after soft lookup failures' - run_lookup - analyze_lookup - if [[ "$renovate_status" -eq 0 && "$lookup_failure_count" -eq 0 && "$rate_limit_count" -eq 0 && "$error_count" -eq 0 ]]; then - retry_succeeded=true - fi - fi - - { - echo "### Local Renovate lookup" - echo - echo "- Exit code: $renovate_status" - echo "- Retries: $lookup_retry_count" - echo "- Retry recovered: $retry_succeeded" - echo "- Lookup failures: $lookup_failure_count" - echo "- Rate-limit records: $rate_limit_count" - echo "- Warning records: $warning_count" - echo "- Error records: $error_count" - echo "- Platform: experimental local lookup" - if [[ "$lookup_retry_count" -ne 0 ]]; then - echo - echo "#### Initial lookup failures" - jq -R -s -r "$records_filter | map($lookup_failure_filter) | .[:20][] | \"- \" + (.msg | tostring | gsub(\"[\\r\\n]+\"; \" \"))" "$first_log_file" - fi - if [[ "$lookup_failure_count" -ne 0 ]]; then - echo - echo "#### Lookup failures" - jq -R -s -r "$records_filter | map($lookup_failure_filter) | .[:20][] | \"- \" + (.msg | tostring | gsub(\"[\\r\\n]+\"; \" \"))" "$log_file" - fi - if [[ "$rate_limit_count" -ne 0 ]]; then - echo - echo "#### Rate limits" - jq -R -s -r "$records_filter | map($rate_limit_filter) | .[:20][] | \"- \" + (.msg | tostring | gsub(\"[\\r\\n]+\"; \" \"))" "$log_file" - fi - if [[ "$warning_count" -ne 0 ]]; then - echo - echo "#### Warnings" - jq -R -s -r "$records_filter | map($warning_filter) | .[:20][] | \"- \" + ((.msg // (if (.err | type) == \"object\" then .err.message else null end) // \"Renovate warning\") | tostring | gsub(\"[\\r\\n]+\"; \" \"))" "$log_file" - fi - if [[ "$error_count" -ne 0 ]]; then - echo - echo "#### Errors" - jq -R -s -r "$records_filter | map($error_filter) | .[:20][] | \"- \" + (((if (.err | type) == \"object\" then .err.validationError else null end) // .msg // (if (.err | type) == \"object\" then .err.message else null end) // \"Renovate error\") | tostring | gsub(\"[\\r\\n]+\"; \" \"))" "$log_file" - fi - } >> "$GITHUB_STEP_SUMMARY" - - failure_reason='' - failure_log_file="$log_file" - if [[ "$lookup_retry_count" -ne 0 && "$retry_succeeded" != true ]]; then - dependency_word='dependencies' - if [[ "$initial_lookup_failure_count" -eq 1 ]]; then - dependency_word='dependency' - fi - failure_reason="Renovate failed to look up $initial_lookup_failure_count $dependency_word" - failure_log_file="$first_log_file" - elif [[ "$rate_limit_count" -ne 0 ]]; then - failure_reason='GitHub API rate limit prevented dependency lookup' - elif [[ "$lookup_failure_count" -ne 0 ]]; then - dependency_word='dependencies' - if [[ "$lookup_failure_count" -eq 1 ]]; then - dependency_word='dependency' - fi - failure_reason="Renovate failed to look up $lookup_failure_count $dependency_word" - elif [[ "$renovate_status" -ne 0 ]]; then - failure_reason="Local Renovate dependency lookup exited with code $renovate_status" - elif [[ "$error_count" -ne 0 ]]; then - failure_reason="Local Renovate dependency lookup reported $error_count error records" - fi - - if [[ -n "$failure_reason" ]]; then - stop_marker="$(openssl rand -hex 32)" - echo '::group::Renovate debug log' - echo "::stop-commands::$stop_marker" - tail -c 1000000 "$failure_log_file" - echo - echo "::$stop_marker::" - echo '::endgroup::' - echo "reason=$failure_reason" >> "$GITHUB_OUTPUT" - echo "::error::Local Renovate dependency lookup failed" - exit 1 - fi monitor-renovate: name: Monitor Renovate health @@ -259,230 +88,15 @@ jobs: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} steps: - - name: Inspect Dependency Dashboard and report health - env: - LOOKUP_REASON: ${{ needs.lookup-renovate-dependencies.outputs.reason }} - LOOKUP_RESULT: ${{ needs.lookup-renovate-dependencies.result }} - VALIDATION_REASON: ${{ needs.validate-renovate-config.outputs.reason }} - VALIDATION_RESULT: ${{ needs.validate-renovate-config.result }} - run: | - set -euo pipefail - dashboard_title='Dependency Dashboard' - health_label='renovate-health' - health_title='Renovate health check failed' - health_marker='' - run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" - opt_out_hint="If this repository is not meant to run Renovate, set the Actions variable \`RENOVATE_HEALTH_CHECK\` to \`false\` to stop the health check. Config validation continues on changes and on the weekly schedule." - opt_out_hint_issue="$opt_out_hint Close this issue manually after setting the variable because only a passing check closes it automatically." - dashboard_url='' - reasons=() - notes=() - - issues_enabled="$(gh api "repos/$GH_REPO" --jq '.has_issues')" - dashboard_json='' - if [[ "$issues_enabled" == 'true' ]]; then - # The server-side author filter uses the raw renovate[bot] login, - # while the returned JSON uses app/renovate. The shared presets use - # Renovate's default title. Update all three values together. - dashboard_issues_json="$(gh issue list \ - --state open \ - --author 'renovate[bot]' \ - --limit 1000 \ - --json author,body,number,title,url)" - - dashboard_json="$(jq -c \ - --arg author 'app/renovate' \ - --arg title "$dashboard_title" \ - '[.[] | select(.title == $title and .author.login == $author)] | first // empty' \ - <<< "$dashboard_issues_json")" - else - notes+=("Issues are disabled, so Renovate cannot publish a Dependency Dashboard and this workflow cannot file a health issue. Dashboard inspection is skipped; validation and lookup results are still reported. $opt_out_hint") - fi - - if [[ -n "$dashboard_json" ]]; then - dashboard_body="$(jq -r '.body' <<< "$dashboard_json")" - dashboard_url="$(jq -r '.url' <<< "$dashboard_json")" - - if grep -q '^## Repository Problems[[:space:]]*$' <<< "$dashboard_body"; then - # Renovate records these notices as repository problems even though - # timestamp-optional allows the updates to proceed and the local job - # independently checks dependency lookups. - timestamp_optional_release_warning="⚠️ WARN: Some release(s) did not have a releaseTimestamp, but as we're running with minimumReleaseAgeBehaviour=timestamp-optional, proceeding. See debug logs for more information" - timestamp_optional_upgrade_warning="⚠️ WARN: Some upgrade(s) did not have a releaseTimestamp, but as we're running with minimumReleaseAgeBehaviour=timestamp-optional, proceeding. See debug logs for more information" - package_lookup_warning='⚠️ WARN: Package lookup failures' - repository_problems_intro_pattern='^These problems occurred while renovating this repository\. \[View logs\]\(https://developer\.mend\.io/[^[:space:])]+\)\.$' - ignored_repository_problem_count=0 - actionable_repository_problem_count=0 - - while IFS= read -r repository_problem_line; do - if [[ -z "$repository_problem_line" ]]; then - continue - fi - if [[ "$repository_problem_line" == 'These problems occurred while renovating this repository.' ]]; then - continue - fi - if [[ "$repository_problem_line" =~ $repository_problems_intro_pattern ]]; then - continue - fi - if [[ "$repository_problem_line" == " - $timestamp_optional_release_warning" || "$repository_problem_line" == " - $timestamp_optional_upgrade_warning" || "$repository_problem_line" == " - $package_lookup_warning" ]]; then - ignored_repository_problem_count=$((ignored_repository_problem_count + 1)) - else - actionable_repository_problem_count=$((actionable_repository_problem_count + 1)) - fi - done < <(awk ' - /^## Repository Problems[[:space:]]*$/ { in_repository_problems = 1; next } - in_repository_problems && /^## / { exit } - in_repository_problems { print } - ' <<< "$dashboard_body") - - if [[ "$ignored_repository_problem_count" -eq 0 || "$actionable_repository_problem_count" -ne 0 ]]; then - reasons+=('Dependency Dashboard reports repository problems') - elif [[ "$LOOKUP_RESULT" == 'success' ]]; then - notes+=('Dependency Dashboard only reports expected timestamp notices or lookup failures covered by the local check') - fi - fi - if grep -q '^## Errored[[:space:]]*$' <<< "$dashboard_body"; then - reasons+=('Dependency Dashboard reports errored updates') - fi - elif [[ "$issues_enabled" == 'true' ]]; then - reasons+=('Renovate Dependency Dashboard was not found') - fi - - if [[ "$VALIDATION_RESULT" != 'success' ]]; then - if [[ -n "$VALIDATION_REASON" ]]; then - reasons+=("$VALIDATION_REASON") - else - reasons+=("Renovate configuration validation job finished with: $VALIDATION_RESULT") - fi - fi - if [[ "$LOOKUP_RESULT" != 'success' ]]; then - if [[ -n "$LOOKUP_REASON" ]]; then - reasons+=("$LOOKUP_REASON") - else - reasons+=("Local Renovate lookup job finished with: $LOOKUP_RESULT") - fi - fi - - health_number='' - if [[ "$issues_enabled" == 'true' ]]; then - health_issues_json="$(gh issue list \ - --state open \ - --label "$health_label" \ - --limit 1000 \ - --json body,number,title,url)" - - health_number="$(jq -r \ - --arg marker "$health_marker" \ - --arg title "$health_title" \ - '[.[] | select(.title == $title and ((.body // "") | contains($marker)))] | first | .number // empty' \ - <<< "$health_issues_json")" - fi - - if ((${#reasons[@]} == 0)); then - { - echo '### Renovate health' - echo - echo 'Healthy' - echo - if ((${#notes[@]} != 0)); then - printf -- '- %s\n' "${notes[@]}" - fi - if [[ -n "$dashboard_url" ]]; then - echo "- [Renovate Dependency Dashboard]($dashboard_url)" - fi - echo "- [Renovate health-check run]($run_url)" - } >> "$GITHUB_STEP_SUMMARY" - - if [[ -n "$health_number" ]]; then - recovery_file="$(mktemp)" - trap 'rm -f "$recovery_file"' EXIT - { - echo 'The Renovate health check passed. This issue is closing automatically.' - echo - echo '### Details' - echo - echo "- [Successful Renovate health-check run]($run_url)" - if [[ -n "$dashboard_url" ]]; then - echo "- [Renovate Dependency Dashboard]($dashboard_url)" - fi - } > "$recovery_file" - gh issue comment "$health_number" --body-file "$recovery_file" - gh issue close "$health_number" --reason completed - fi - exit 0 - fi - - report_file="$(mktemp)" - trap 'rm -f "$report_file"' EXIT - if [[ -n "$health_number" ]]; then - { - echo 'The Renovate health check is still failing.' - echo - echo '### Problems' - echo - printf -- '- %s\n' "${reasons[@]}" - echo - echo '### Details' - echo - echo "- [Latest failed Renovate health-check run]($run_url)" - if [[ -n "$dashboard_url" ]]; then - echo "- [Renovate Dependency Dashboard]($dashboard_url)" - fi - echo - echo 'This issue remains open and will close automatically after a successful check.' - echo - echo "$opt_out_hint_issue" - } > "$report_file" - else - { - echo "$health_marker" - echo 'The Renovate health check needs attention.' - echo - echo '### Problems' - echo - printf -- '- %s\n' "${reasons[@]}" - echo - echo 'Review the details below, fix the listed problems, and rerun the health check. This issue updates after repeated failures and closes automatically after a successful check.' - echo - echo "$opt_out_hint_issue" - echo - echo '### Details' - echo - echo "- [Failed Renovate health-check run]($run_url)" - if [[ -n "$dashboard_url" ]]; then - echo "- [Renovate Dependency Dashboard]($dashboard_url)" - fi - } > "$report_file" - fi - - { - echo '### Renovate health' - echo - echo 'Unhealthy' - echo - printf -- '- %s\n' "${reasons[@]}" - echo - if ((${#notes[@]} != 0)); then - printf -- '- %s\n' "${notes[@]}" - echo - fi - if [[ -n "$dashboard_url" ]]; then - echo "- [Renovate Dependency Dashboard]($dashboard_url)" - fi - echo "- [Renovate health-check run]($run_url)" - } >> "$GITHUB_STEP_SUMMARY" - - if [[ "$issues_enabled" == 'true' ]]; then - if [[ -n "$health_number" ]]; then - gh issue comment "$health_number" --body-file "$report_file" - else - gh label create "$health_label" \ - --color D73A4A \ - --description 'Automated Renovate health incident' \ - --force - gh issue create --title "$health_title" --label "$health_label" --body-file "$report_file" - fi - fi + - name: Checkout sources + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - echo '::error::Renovate health check failed' - exit 1 + - name: Inspect Dependency Dashboard and report health + uses: netcracker/qubership-workflow-hub/actions/renovate-monitor@77786061d16fabdb6e089c2ba1c5846da3efc62a # PR 951 + with: + validation-result: ${{ needs.validate-renovate-config.result }} + lookup-result: ${{ needs.lookup-renovate-dependencies.result }} + validation-reason: ${{ needs.validate-renovate-config.outputs.reason }} + lookup-reason: ${{ needs.lookup-renovate-dependencies.outputs.reason }}