Skip to content

Commit 106e64d

Browse files
Stabilize wiki retry handling and release refreshes (#310)
* Stabilize wiki retry handling and release refreshes (#309) * Address workflow review feedback for wiki release handling * Update wiki submodule pointer for PR #310 * Fix release wiki skip summary without local action checkout * Track wiki pointer drift without republishing content * Clean up release wiki preview branches on merge --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 7a7e837 commit 106e64d

11 files changed

Lines changed: 1038 additions & 131 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Retry Transient Workflow Failures
2+
description: Inspect failed workflow jobs for transient GitHub-side failures and request a rerun when every failed job matches the configured signatures.
3+
4+
inputs:
5+
run-id:
6+
description: Workflow run identifier to inspect.
7+
required: true
8+
run-attempt:
9+
description: Current workflow run attempt number.
10+
required: true
11+
workflow-name:
12+
description: Human-readable workflow name for summaries.
13+
required: true
14+
max-run-attempts:
15+
description: Maximum workflow run attempts before retry is skipped.
16+
required: false
17+
default: '2'
18+
19+
outputs:
20+
status:
21+
description: Retry decision status.
22+
value: ${{ steps.retry.outputs.status }}
23+
summary:
24+
description: Markdown summary for the retry decision.
25+
value: ${{ steps.retry.outputs.summary }}
26+
27+
runs:
28+
using: composite
29+
steps:
30+
- id: retry
31+
shell: bash
32+
env:
33+
INPUT_RUN_ID: ${{ inputs.run-id }}
34+
INPUT_RUN_ATTEMPT: ${{ inputs.run-attempt }}
35+
INPUT_WORKFLOW_NAME: ${{ inputs.workflow-name }}
36+
INPUT_MAX_RUN_ATTEMPTS: ${{ inputs.max-run-attempts }}
37+
run: ${{ github.action_path }}/run.sh
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
run_id="${INPUT_RUN_ID}"
5+
run_attempt="${INPUT_RUN_ATTEMPT}"
6+
workflow_name="${INPUT_WORKFLOW_NAME}"
7+
max_run_attempts="${INPUT_MAX_RUN_ATTEMPTS:-2}"
8+
9+
if [ -z "${GH_TOKEN:-}" ]; then
10+
echo "GH_TOKEN is required." >&2
11+
12+
exit 1
13+
fi
14+
15+
failed_jobs_csv=""
16+
matched_jobs_csv=""
17+
uninspectable_jobs_csv=""
18+
19+
csv_append() {
20+
local current="$1"
21+
local value="$2"
22+
23+
if [ -z "${current}" ]; then
24+
printf '%s' "${value}"
25+
26+
return
27+
fi
28+
29+
printf '%s,%s' "${current}" "${value}"
30+
}
31+
32+
csv_to_summary_list() {
33+
local csv="$1"
34+
local rendered=()
35+
local item=""
36+
37+
if [ -z "${csv}" ]; then
38+
return
39+
fi
40+
41+
IFS=',' read -r -a rendered <<< "${csv}"
42+
43+
for item in "${rendered[@]}"; do
44+
printf '`%s`' "${item}"
45+
46+
if [ "${item}" != "${rendered[${#rendered[@]}-1]}" ]; then
47+
printf ', '
48+
fi
49+
done
50+
}
51+
52+
build_summary() {
53+
local status="$1"
54+
local lines=(
55+
"## Transient Failure Retry Summary"
56+
""
57+
"- Workflow: \`${workflow_name}\`"
58+
"- Run ID: \`${run_id}\`"
59+
"- Run attempt: \`${run_attempt}\`"
60+
"- Retry status: \`${status}\`"
61+
)
62+
63+
if [ -n "${failed_jobs_csv}" ]; then
64+
lines+=("- Failed jobs inspected: $(csv_to_summary_list "${failed_jobs_csv}")")
65+
fi
66+
67+
if [ -n "${matched_jobs_csv}" ]; then
68+
lines+=("- Jobs with transient GitHub failure signatures: $(csv_to_summary_list "${matched_jobs_csv}")")
69+
fi
70+
71+
if [ -n "${uninspectable_jobs_csv}" ]; then
72+
lines+=("- Failed jobs with unreadable logs: $(csv_to_summary_list "${uninspectable_jobs_csv}")")
73+
fi
74+
75+
case "${status}" in
76+
rerun-requested)
77+
lines+=("- Action: Requested a rerun of failed jobs because every inspectable failed job matched transient GitHub-side error signatures.")
78+
;;
79+
skipped-run-attempt-limit)
80+
lines+=("- Action: Skipped rerun because the workflow already reached the configured retry limit.")
81+
;;
82+
skipped-no-failed-jobs)
83+
lines+=("- Action: Skipped rerun because the workflow reported failure without failed jobs to inspect.")
84+
;;
85+
skipped-no-transient-match)
86+
lines+=("- Action: Skipped rerun because at least one failed job did not match the transient GitHub-side signatures.")
87+
;;
88+
skipped-uninspectable-logs)
89+
lines+=("- Action: Skipped rerun because at least one failed job log could not be downloaded through the GitHub Actions API.")
90+
;;
91+
esac
92+
93+
printf '%s\n' "${lines[@]}"
94+
}
95+
96+
write_summary_output() {
97+
local summary="$1"
98+
local delimiter="SUMMARY_$(date +%s%N)"
99+
100+
{
101+
printf 'summary<<%s\n' "${delimiter}"
102+
printf '%s\n' "${summary}"
103+
printf '%s\n' "${delimiter}"
104+
} >> "${GITHUB_OUTPUT}"
105+
}
106+
107+
write_status_and_summary() {
108+
local status="$1"
109+
local summary
110+
111+
summary="$(build_summary "${status}")"
112+
113+
printf 'status=%s\n' "${status}" >> "${GITHUB_OUTPUT}"
114+
write_summary_output "${summary}"
115+
}
116+
117+
log_matches_transient_signature() {
118+
local log_file="$1"
119+
120+
grep -Eiq \
121+
"RPC failed; HTTP 5[0-9][0-9]|expected flush after ref listing|expected 'packfile'|remote:[[:space:]]+Internal Server Error|requested URL returned error:[[:space:]]*5[0-9][0-9]|fatal:[[:space:]]+unable to access 'https://github\\.com/.*': The requested URL returned error:[[:space:]]*5[0-9][0-9]" \
122+
"${log_file}"
123+
}
124+
125+
download_job_logs() {
126+
local job_id="$1"
127+
local output_file="$2"
128+
129+
curl \
130+
-sS -L \
131+
-H "Accept: application/vnd.github+json" \
132+
-H "Authorization: Bearer ${GH_TOKEN}" \
133+
-H "X-GitHub-Api-Version: 2022-11-28" \
134+
-o "${output_file}" \
135+
-w '%{http_code}' \
136+
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/jobs/${job_id}/logs"
137+
}
138+
139+
if [ "${run_attempt}" -ge "${max_run_attempts}" ]; then
140+
write_status_and_summary "skipped-run-attempt-limit"
141+
142+
exit 0
143+
fi
144+
145+
jobs_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100")"
146+
failed_jobs_json="$(jq -c '.jobs[] | select(.conclusion == "failure")' <<< "${jobs_json}")"
147+
148+
if [ -z "${failed_jobs_json}" ]; then
149+
write_status_and_summary "skipped-no-failed-jobs"
150+
151+
exit 0
152+
fi
153+
154+
while IFS= read -r failed_job; do
155+
[ -n "${failed_job}" ] || continue
156+
157+
job_id="$(jq -r '.id' <<< "${failed_job}")"
158+
job_name="$(jq -r '.name' <<< "${failed_job}")"
159+
failed_jobs_csv="$(csv_append "${failed_jobs_csv}" "${job_name}")"
160+
161+
temporary_log_file="$(mktemp)"
162+
163+
log_status_code="$(download_job_logs "${job_id}" "${temporary_log_file}")"
164+
165+
if [ "${log_status_code}" != "200" ]; then
166+
uninspectable_jobs_csv="$(csv_append "${uninspectable_jobs_csv}" "${job_name} (${log_status_code})")"
167+
rm -f "${temporary_log_file}"
168+
write_status_and_summary "skipped-uninspectable-logs"
169+
170+
exit 0
171+
fi
172+
173+
if ! log_matches_transient_signature "${temporary_log_file}"; then
174+
rm -f "${temporary_log_file}"
175+
write_status_and_summary "skipped-no-transient-match"
176+
177+
exit 0
178+
fi
179+
180+
matched_jobs_csv="$(csv_append "${matched_jobs_csv}" "${job_name}")"
181+
rm -f "${temporary_log_file}"
182+
done <<< "${failed_jobs_json}"
183+
184+
gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/rerun-failed-jobs" >/dev/null
185+
186+
write_status_and_summary "rerun-requested"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Refresh Release Wiki Pointer
2+
description: Rebuild the authoritative wiki branch from the merged release state and expose whether the parent repository submodule pointer changed.
3+
4+
inputs:
5+
target:
6+
description: Wiki target directory or submodule path.
7+
required: false
8+
default: .github/wiki
9+
publish-branch:
10+
description: Wiki branch to refresh from the merged release state.
11+
required: false
12+
default: master
13+
commit-message:
14+
description: Commit message used for the wiki branch refresh.
15+
required: false
16+
default: Refresh wiki docs after merged release
17+
18+
outputs:
19+
published:
20+
description: Whether the wiki publish branch received a new commit.
21+
value: ${{ steps.refresh.outputs.published }}
22+
pointer-changed:
23+
description: Whether the parent repository submodule pointer changed after the wiki publish branch refresh.
24+
value: ${{ steps.refresh.outputs.pointer-changed }}
25+
publish-sha:
26+
description: Final commit SHA at the refreshed wiki publish branch head.
27+
value: ${{ steps.refresh.outputs.publish-sha }}
28+
29+
runs:
30+
using: composite
31+
steps:
32+
- id: refresh
33+
shell: bash
34+
env:
35+
INPUT_TARGET: ${{ inputs.target }}
36+
INPUT_PUBLISH_BRANCH: ${{ inputs.publish-branch }}
37+
INPUT_COMMIT_MESSAGE: ${{ inputs.commit-message }}
38+
run: ${{ github.action_path }}/run.sh
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Isolate nested Git commands from caller-specific repository environment such as hooks.
5+
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_INTERNAL_SUPER_PREFIX GIT_COMMON_DIR
6+
7+
target="${INPUT_TARGET:-.github/wiki}"
8+
publish_branch="${INPUT_PUBLISH_BRANCH:-master}"
9+
commit_message="${INPUT_COMMIT_MESSAGE:-Refresh wiki docs after merged release}"
10+
11+
git -C "${target}" fetch origin "${publish_branch}"
12+
13+
if ! git -C "${target}" switch -C "${publish_branch}" --track "origin/${publish_branch}" >/dev/null 2>&1; then
14+
git -C "${target}" switch "${publish_branch}" >/dev/null 2>&1
15+
fi
16+
17+
git -C "${target}" reset --hard "origin/${publish_branch}"
18+
git -C "${target}" clean -fd
19+
20+
dev-tools wiki --target="${target}"
21+
22+
if [ -z "$(git -C "${target}" status --porcelain)" ]; then
23+
pointer_changed="false"
24+
25+
if ! git diff --quiet -- "${target}"; then
26+
pointer_changed="true"
27+
fi
28+
29+
{
30+
echo "published=false"
31+
echo "pointer-changed=${pointer_changed}"
32+
echo "publish-sha=$(git -C "${target}" rev-parse HEAD)"
33+
} >> "${GITHUB_OUTPUT}"
34+
35+
exit 0
36+
fi
37+
38+
git -C "${target}" config user.name "${GIT_AUTHOR_NAME:-github-actions[bot]}"
39+
git -C "${target}" config user.email "${GIT_AUTHOR_EMAIL:-41898282+github-actions[bot]@users.noreply.github.com}"
40+
git -C "${target}" add -A
41+
git -C "${target}" commit -m "${commit_message}"
42+
git -C "${target}" push origin "HEAD:${publish_branch}"
43+
44+
pointer_changed="false"
45+
46+
if ! git diff --quiet -- "${target}"; then
47+
pointer_changed="true"
48+
fi
49+
50+
{
51+
echo "published=true"
52+
echo "pointer-changed=${pointer_changed}"
53+
echo "publish-sha=$(git -C "${target}" rev-parse HEAD)"
54+
} >> "${GITHUB_OUTPUT}"

.github/wiki

Submodule wiki updated from 9cb08e1 to c30fa7c

.github/workflows/changelog.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ jobs:
298298
token: ${{ github.token }}
299299
ref: ${{ github.event.pull_request.base.ref }}
300300
fetch-depth: 0
301+
submodules: recursive
301302
- name: Checkout dev-tools workflow action source
302303
uses: actions/checkout@v6
303304
with:
@@ -311,6 +312,8 @@ jobs:
311312
php-version: ${{ needs.resolve_php.outputs.php-version }}
312313
root-version: ${{ env.CHANGELOG_ROOT_VERSION }}
313314
install-options: --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts
315+
safe-directories: |
316+
${{ github.workspace }}/.github/wiki
314317
315318
- name: Resolve merged release version
316319
id: version
@@ -332,6 +335,30 @@ jobs:
332335
version: ${{ steps.version.outputs.value }}
333336
target: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
334337

338+
- name: Refresh release wiki branch from merged main
339+
id: refresh_release_wiki
340+
uses: ./.dev-tools-actions/.github/actions/wiki/refresh-release-pointer
341+
with:
342+
commit-message: Refresh wiki docs after release v${{ steps.version.outputs.value }}
343+
344+
- name: Commit release wiki submodule pointer
345+
if: ${{ steps.refresh_release_wiki.outputs.pointer-changed == 'true' }}
346+
id: release_wiki_pointer_commit
347+
uses: EndBug/add-and-commit@v10
348+
with:
349+
add: .github/wiki
350+
message: Refresh wiki submodule pointer after release v${{ steps.version.outputs.value }}
351+
default_author: github_actions
352+
pull: "--rebase --autostash"
353+
push: true
354+
355+
- name: Dispatch tests for release wiki pointer commit
356+
if: ${{ steps.refresh_release_wiki.outputs.pointer-changed == 'true' }}
357+
env:
358+
GH_TOKEN: ${{ github.token }}
359+
BASE_REF: ${{ github.event.pull_request.base.ref }}
360+
run: gh workflow run tests.yml --ref "${BASE_REF}" -f publish-required-statuses=true
361+
335362
- uses: actions/checkout@v6
336363
- name: Checkout dev-tools workflow action source
337364
uses: actions/checkout@v6
@@ -359,6 +386,9 @@ jobs:
359386
- Published tag: `v${{ steps.version.outputs.value }}`
360387
- Release operation: `${{ steps.publish_release.outputs.operation }}`
361388
- Release URL: ${{ steps.publish_release.outputs.url }}
389+
- Wiki publish refresh: `${{ steps.refresh_release_wiki.outputs.published == 'true' && 'published' || 'unchanged' }}`
390+
- Wiki pointer reconciliation: `${{ steps.refresh_release_wiki.outputs.pointer-changed == 'true' && 'updated' || 'unchanged' }}`
391+
- Required test dispatch: `${{ steps.refresh_release_wiki.outputs.pointer-changed == 'true' && 'requested' || 'not needed' }}`
362392
- Project items released: `${{ steps.release_project_status.outputs.moved-count }}`
363393
- Project items skipped: `${{ steps.release_project_status.outputs.skipped-count }}`
364394
- Project source statuses: `${{ steps.release_project_status.outputs.source-statuses }}`

0 commit comments

Comments
 (0)