Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .github/workflows/ci-repeat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: CI Repeat

on:
# 监听名为 CI 的 workflow;只要它完整结束一次,就触发本 workflow
workflow_run:
workflows: ["CI"]
types: [completed]

permissions:
# 需要写 Actions,才能发起 swap job 的 rerun
actions: write
contents: read
# 需要写 PR comment,记录每一轮 swap/test 的结果
pull-requests: write

jobs:
repeat-swap:
# 仅处理 PR 场景;cancelled 不继续
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion != 'cancelled'
runs-on: ubuntu-24.04
env:
# 最多自动重跑多少轮;达到上限后只记录结果,不再继续触发
MAX_ATTEMPTS: "10"
steps:
- name: Record swap result and rerun if needed
uses: actions/github-script@v7
with:
script: |
const run = context.payload.workflow_run;
const owner = context.repo.owner;
const repo = context.repo.repo;
const runId = Number(run.id);
const runAttempt = Number(run.run_attempt || "1");
const maxAttempts = Number(process.env.MAX_ATTEMPTS || "10");
const marker = '<!-- ci-repeat-status -->';

if (!run.pull_requests || run.pull_requests.length === 0) {
core.info('No pull request is associated with this run.');
return;
}

const pr = run.pull_requests[0];
const jobs = await github.paginate(
github.rest.actions.listJobsForWorkflowRun,
{
owner,
repo,
run_id: runId,
per_page: 100,
}
);

const sanitizeLog = text => text.replace(/```/g, '``\u200b`');
const pickLogSnippet = text => {
const lines = text.replace(/\r\n/g, '\n').split('\n');
const needle = "couldn't open socket: connection refused";
const hit = lines.findIndex(line => line.includes(needle));

if (hit !== -1) {
const start = Math.max(0, hit - 200);
return lines.slice(start, hit + 1).join('\n').trimEnd();
}

return lines.slice(-200).join('\n').trimEnd();
};

const getJobLog = async jobId => {
try {
const response = await github.request(
'GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs',
{
owner,
repo,
job_id: jobId,
request: { redirect: 'manual' },
}
);

const logUrl = response.headers.location;
if (!logUrl) {
return '(failed to get log download url)';
}

const logResponse = await fetch(logUrl, {
headers: {
'User-Agent': 'actions/github-script',
},
});

if (!logResponse.ok) {
return `(failed to fetch logs: ${logResponse.status})`;
}

return await logResponse.text();
} catch (error) {
return `(failed to fetch logs: ${error.message})`;
}
};

const swapJob = jobs.find(job => /^swap(?:\s|\(|$)/.test(job.name));
const testStep = swapJob?.steps?.find(step => step.name?.toLowerCase() === 'test');

let swapLogSection = '_swap job not found_';
if (swapJob) {
const swapJobLog = await getJobLog(swapJob.id);
const snippet = sanitizeLog(pickLogSnippet(swapJobLog || ''));
swapLogSection =
`<details><summary>${swapJob.name} / test</summary>\n\n` +
'```text\n' +
`${snippet || '(empty log)'}\n` +
'```\n' +
'</details>';
}

const body = [
marker,
'## CI repeat status',
'',
`- PR: #${pr.number}`,
`- Commit: \`${run.head_sha.slice(0, 7)}\``,
`- Attempt: ${runAttempt}/${maxAttempts}`,
`- Workflow run: [CI #${run.run_number} / attempt ${runAttempt}](${run.html_url})`,
'',
`- swap job: **${swapJob ? (swapJob.conclusion || swapJob.status) : 'not found'}**`,
`- swap / test step: **${testStep ? (testStep.conclusion || testStep.status) : 'not found'}**`,
'',
'## swap / test log snippet',
'',
swapLogSection,
].join('\n');

const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: pr.number,
per_page: 100,
}
);

const existing = comments.find(comment =>
comment.user?.type === 'Bot' && comment.body?.includes(marker)
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body,
});
}

if (!swapJob) {
core.info('swap job not found; no rerun requested.');
return;
}

if (runAttempt >= maxAttempts) {
core.info(`Reached max attempts (${maxAttempts}); no rerun requested.`);
return;
}

await github.request(
'POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun',
{
owner,
repo,
job_id: swapJob.id,
}
);
core.info(`Requested rerun for swap job ${swapJob.id}.`);
172 changes: 172 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,175 @@ jobs:
run: make SWAP=1 SANITIZER=address -j8
- name: make test-asan
run: LD_LIBRARY_PATH=/home/runner/work/Redis-On-Rocks/Redis-On-Rocks/deps/rocksdb/_build_folly/libs/glog/lib:/home/runner/work/Redis-On-Rocks/Redis-On-Rocks/deps/rocksdb/_build_folly/libs/libevent/lib make SWAP=1 test-asan -j8

repeat-swap:
if: ${{ always() && github.event_name == 'pull_request' }}
needs: [unit, mem, mem-asan, swap, swap-asan]
runs-on: ubuntu-24.04
permissions:
actions: write
contents: read
pull-requests: write
env:
MAX_ATTEMPTS: "10"
steps:
- name: Record swap result and rerun if needed
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const runId = Number(process.env.GITHUB_RUN_ID);
const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT || "1");
const maxAttempts = Number(process.env.MAX_ATTEMPTS || "10");
const pr = context.payload.pull_request;
const marker = '<!-- ci-repeat-status -->';

if (!pr) {
core.info('No pull request is associated with this run.');
return;
}

const jobs = await github.paginate(
github.rest.actions.listJobsForWorkflowRun,
{
owner,
repo,
run_id: runId,
per_page: 100,
}
);

const sanitizeLog = text => text.replace(/```/g, '``\u200b`');
const pickLogSnippet = text => {
const lines = text.replace(/\r\n/g, '\n').split('\n');
const needle = "couldn't open socket: connection refused";
const hit = lines.findIndex(line => line.includes(needle));

if (hit !== -1) {
const start = Math.max(0, hit - 200);
return lines.slice(start, hit + 1).join('\n').trimEnd();
}

return lines.slice(-200).join('\n').trimEnd();
};

const getJobLog = async jobId => {
try {
const response = await github.request(
'GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs',
{
owner,
repo,
job_id: jobId,
request: { redirect: 'manual' },
}
);

const logUrl = response.headers.location;
if (!logUrl) {
return '(failed to get log download url)';
}

const logResponse = await fetch(logUrl, {
headers: {
'User-Agent': 'actions/github-script',
},
});

if (!logResponse.ok) {
return `(failed to fetch logs: ${logResponse.status})`;
}

return await logResponse.text();
} catch (error) {
return `(failed to fetch logs: ${error.message})`;
}
};

const swapJob = jobs.find(job => /^swap(?:\s|\(|$)/.test(job.name));
const testStep = swapJob?.steps?.find(step => step.name?.toLowerCase() === 'test');

let swapLogSection = '_swap job not found_';
if (swapJob) {
const swapJobLog = await getJobLog(swapJob.id);
const snippet = sanitizeLog(pickLogSnippet(swapJobLog || ''));
swapLogSection =
`<details><summary>${swapJob.name} / test</summary>\n\n` +
'```text\n' +
`${snippet || '(empty log)'}\n` +
'```\n' +
'</details>';
}

const body = [
marker,
'## CI repeat status',
'',
`- PR: #${pr.number}`,
`- Commit: \`${context.sha.slice(0, 7)}\``,
`- Attempt: ${runAttempt}/${maxAttempts}`,
`- Workflow run: ${context.runNumber}`,
'',
`- swap job: **${swapJob ? (swapJob.conclusion || swapJob.status) : 'not found'}**`,
`- swap / test step: **${testStep ? (testStep.conclusion || testStep.status) : 'not found'}**`,
'',
'## swap / test log snippet',
'',
swapLogSection,
].join('\n');

const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: pr.number,
per_page: 100,
}
);

const existing = comments.find(comment =>
comment.user?.type === 'Bot' && comment.body?.includes(marker)
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body,
});
}

if (!swapJob) {
core.info('swap job not found; no rerun requested.');
return;
}

if (swapJob.conclusion !== 'failure') {
core.info(`swap job conclusion is ${swapJob.conclusion || swapJob.status}; no rerun requested.`);
return;
}

if (runAttempt >= maxAttempts) {
core.info(`Reached max attempts (${maxAttempts}); no rerun requested.`);
return;
}

await github.request(
'POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun',
{
owner,
repo,
job_id: swapJob.id,
}
);
core.info(`Requested rerun for swap job ${swapJob.id}.`);
12 changes: 8 additions & 4 deletions tests/gtid/master_restart.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,15 @@ proc restart_test {master_gtid_enabled slave_gtid_enabled restat_master_gtid_ena
}

wait_for_condition 1000 30 {
[$master dbsize] eq [$slave dbsize]
&& [$slave dbsize] eq 0
[dbsize_loadsafe $master master_dbsize] &&
[dbsize_loadsafe $slave slave_dbsize] &&
$master_dbsize eq $slave_dbsize &&
$slave_dbsize eq 0
} else {
puts [$master dbsize]
puts [$slave dbsize]
puts [dbsize_loadsafe $master master_dbsize]
puts $master_dbsize
puts [dbsize_loadsafe $slave slave_dbsize]
puts $slave_dbsize
fail "slave dbszie != 0"
}

Expand Down
6 changes: 4 additions & 2 deletions tests/gtid/sync.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ start_server {overrides {gtid-enabled yes}} {
$R(1) slaveof $R_host(0) $R_port(0)
wait_for_condition 50 1000 {
[status $R(1) master_link_status] == "up" &&
[$R(1) dbsize] == 1
[dbsize_loadsafe $R(1) replica_dbsize] &&
$replica_dbsize == 1
} else {
fail "Replicas not replicating from master"
}
Expand Down Expand Up @@ -155,7 +156,8 @@ start_server {overrides {gtid-enabled yes}} {
$R(1) slaveof $R_host(0) $R_port(0)
wait_for_condition 50 1000 {
[status $R(1) master_link_status] == "up" &&
[$R(1) dbsize] == 1
[dbsize_loadsafe $R(1) replica_dbsize] &&
$replica_dbsize == 1
} else {
fail "Replicas not replicating from master"
}
Expand Down
Loading
Loading