forked from airbytehq/airbyte
-
Notifications
You must be signed in to change notification settings - Fork 3
295 lines (268 loc) · 11.9 KB
/
Copy pathauto-merge-cron.yml
File metadata and controls
295 lines (268 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
name: Auto merge connector PRs Cron
# Dry-run mode is controlled by the org-level variable ENABLE_CONNECTOR_AUTO_MERGE.
# Set to "true" to enable real merges; any other value (or unset) runs in dry-run mode.
# In dry-run mode, PRs are still approved and promoted from draft, but merges are skipped.
# Manage at: https://github.com/organizations/airbytehq/settings/variables/actions
on:
schedule:
# Every 2 hours on the hour.
- cron: "0 */2 * * *"
workflow_dispatch:
# All repo operations use GitHub App tokens, not GITHUB_TOKEN.
permissions: {}
jobs:
# ---------- Job 1: Discover candidate PRs ----------
list-candidates:
name: List eligible PRs
runs-on: ubuntu-24.04
steps:
# ---------- Authentication ----------
- name: Authenticate as 'octavia-bot-hoard' GitHub App
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
id: get-hoard-token
with:
owner: "airbytehq"
repositories: "airbyte"
app-id: ${{ secrets.OCTAVIA_BOT_HOARD_APP_ID }}
private-key: ${{ secrets.OCTAVIA_BOT_HOARD_PRIVATE_KEY }}
# ---------- Find candidate PRs ----------
- name: Find auto-merge PRs
id: normal-prs
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: |
gh pr list \
--repo airbytehq/airbyte \
--label "auto-merge" \
--state open \
--base master \
--json number,title \
--limit 100 > /tmp/auto-merge.json
count=$(jq length /tmp/auto-merge.json)
echo "normal-matrix=$(cat /tmp/auto-merge.json)" | tee -a $GITHUB_OUTPUT
echo "normal-count=$count" | tee -a $GITHUB_OUTPUT
# ---------- Fetch required checks ----------
- name: Fetch required checks for master branch
id: required-checks
if: fromJSON(steps.normal-prs.outputs.normal-count) > 0
uses: actions/github-script@v7
with:
github-token: ${{ steps.get-hoard-token.outputs.token }}
script: |
const { data: rules } = await github.request(
'GET /repos/{owner}/{repo}/rules/branches/{branch}',
{ owner: 'airbytehq', repo: 'airbyte', branch: 'master' }
);
const checks = [];
for (const rule of rules) {
if (rule.type === 'required_status_checks') {
for (const check of (rule.parameters?.required_status_checks || [])) {
checks.push(check.context);
}
}
}
core.info(`Required checks (${checks.length}): ${JSON.stringify(checks)}`);
core.setOutput('checks', JSON.stringify(checks));
outputs:
normal-matrix: ${{ steps.normal-prs.outputs.normal-matrix }}
normal-count: ${{ steps.normal-prs.outputs.normal-count }}
required-checks: ${{ steps.required-checks.outputs.checks }}
# ---------- Job 2: Normal merge (verify CI first) ----------
normal-merge:
name: "Merge #${{ matrix.pr.number }}"
needs: list-candidates
if: fromJSON(needs.list-candidates.outputs.normal-count) > 0
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
pr: ${{ fromJSON(needs.list-candidates.outputs.normal-matrix) }}
env:
PR_NUMBER: ${{ matrix.pr.number }}
PR_TITLE: ${{ matrix.pr.title }}
steps:
# ---------- Authentication ----------
- name: Authenticate as 'octavia-bot-hoard' GitHub App
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
id: get-hoard-token
with:
owner: "airbytehq"
repositories: "airbyte"
app-id: ${{ secrets.OCTAVIA_BOT_HOARD_APP_ID }}
private-key: ${{ secrets.OCTAVIA_BOT_HOARD_PRIVATE_KEY }}
# ---------- Validation ----------
- name: Check PR is still open
id: check-open
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: |
state=$(gh pr view "$PR_NUMBER" --repo airbytehq/airbyte --json state --jq '.state')
echo "state=$state" | tee -a $GITHUB_OUTPUT
- name: Validate connector-only file paths
if: steps.check-open.outputs.state == 'OPEN'
id: validate-paths
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: |
set -e
gh pr diff "$PR_NUMBER" --repo airbytehq/airbyte --name-only > /tmp/changed-files.txt
valid=true
while IFS= read -r file; do
case "$file" in
airbyte-integrations/connectors/*) ;;
docs/integrations/sources/*) ;;
docs/integrations/destinations/*) ;;
docs/ai-agents/connectors/*) ;;
docs/ai-agents/reference/*) ;;
docs/developers/pyairbyte/*) ;;
docs/release_notes/*) ;;
docusaurus/src/data/*) ;;
*) valid=false; echo "::warning::Non-connector file: $file"; break ;;
esac
done < /tmp/changed-files.txt
echo "valid=$valid" | tee -a $GITHUB_OUTPUT
- name: Verify required status checks pass
if: steps.check-open.outputs.state == 'OPEN' && steps.validate-paths.outputs.valid == 'true'
id: verify-checks
uses: actions/github-script@v7
env:
REQUIRED_CHECKS_JSON: ${{ needs.list-candidates.outputs.required-checks }}
with:
github-token: ${{ steps.get-hoard-token.outputs.token }}
script: |
const prNum = Number(process.env.PR_NUMBER);
const requiredChecks = new Set(JSON.parse(process.env.REQUIRED_CHECKS_JSON || '[]'));
const { data: pr } = await github.rest.pulls.get({
owner: 'airbytehq', repo: 'airbyte', pull_number: prNum,
});
const sha = pr.head.sha;
const statuses = await github.paginate(
github.rest.repos.listCommitStatusesForRef,
{ owner: 'airbytehq', repo: 'airbyte', ref: sha, per_page: 100 }
);
const successStatuses = new Set(
statuses.filter(s => s.state === 'success').map(s => s.context)
);
const checkRuns = await github.paginate(
github.rest.checks.listForRef,
{ owner: 'airbytehq', repo: 'airbyte', ref: sha, per_page: 100 },
(response) => response.data
);
const successChecks = new Set(
checkRuns
.filter(cr => cr.conclusion === 'success' || cr.conclusion === 'skipped')
.map(cr => cr.name)
);
const allPassing = new Set([...successStatuses, ...successChecks]);
const missing = [...requiredChecks].filter(c => !allPassing.has(c));
if (missing.length > 0) {
core.info(`Missing checks: ${missing.join(', ')}`);
core.setOutput('ready', 'false');
} else {
core.info('All required checks passing');
core.setOutput('ready', 'true');
}
- name: Verify Vercel preview deployments are not failing
if: steps.check-open.outputs.state == 'OPEN' && steps.validate-paths.outputs.valid == 'true' && steps.verify-checks.outputs.ready == 'true'
id: verify-deployments
uses: actions/github-script@v7
with:
github-token: ${{ steps.get-hoard-token.outputs.token }}
script: |
const prNum = Number(process.env.PR_NUMBER);
const { data: pr } = await github.rest.pulls.get({
owner: 'airbytehq', repo: 'airbyte', pull_number: prNum,
});
const sha = pr.head.sha;
// Check commit statuses for failed Vercel deployments
const statuses = await github.paginate(
github.rest.repos.listCommitStatusesForRef,
{ owner: 'airbytehq', repo: 'airbyte', ref: sha, per_page: 100 }
);
// Deduplicate: GitHub returns all statuses; only the latest per
// context matters.
const latestByContext = new Map();
for (const s of statuses) {
if (!latestByContext.has(s.context) || new Date(s.updated_at) > new Date(latestByContext.get(s.context).updated_at)) {
latestByContext.set(s.context, s);
}
}
const vercelStatuses = [...latestByContext.values()]
.filter(s => s.context.toLowerCase().includes('vercel'));
const failed = vercelStatuses.filter(s => s.state === 'failure' || s.state === 'error');
const pending = vercelStatuses.filter(s => s.state === 'pending');
if (failed.length > 0) {
for (const d of failed) {
core.warning(`Vercel deployment failed: ${d.context} (${d.state})`);
}
core.setOutput('ready', 'false');
} else if (pending.length > 0) {
for (const d of pending) {
core.info(`Vercel deployment still building: ${d.context}`);
}
core.setOutput('ready', 'false');
} else {
core.info('All Vercel deployments succeeded');
core.setOutput('ready', 'true');
}
# ---------- Eligibility gate ----------
- name: Determine merge eligibility
id: eligible
run: >
echo "result=${{
steps.check-open.outputs.state == 'OPEN'
&& steps.validate-paths.outputs.valid == 'true'
&& steps.verify-checks.outputs.ready == 'true'
&& steps.verify-deployments.outputs.ready != 'false'
}}" | tee -a $GITHUB_OUTPUT
# ---------- Prepare PR for merge ----------
- name: Check if PR is a draft
if: steps.eligible.outputs.result == 'true'
id: check-draft
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: |
is_draft=$(gh pr view "$PR_NUMBER" --repo airbytehq/airbyte --json isDraft --jq '.isDraft')
echo "is_draft=$is_draft" | tee -a $GITHUB_OUTPUT
- name: Mark draft PR as ready for review
if: steps.eligible.outputs.result == 'true' && steps.check-draft.outputs.is_draft == 'true'
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: gh pr ready "$PR_NUMBER" --repo airbytehq/airbyte
# ---------- Approve and merge ----------
- name: Authenticate as 'octavia-bot-admin' GitHub App
if: steps.eligible.outputs.result == 'true'
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
id: get-admin-token
with:
owner: "airbytehq"
repositories: "airbyte"
app-id: ${{ secrets.OCTAVIA_BOT_ADMIN_APP_ID }}
private-key: ${{ secrets.OCTAVIA_BOT_ADMIN_PRIVATE_KEY }}
- name: Approve PR with admin bot
if: steps.eligible.outputs.result == 'true'
env:
GH_TOKEN: ${{ steps.get-admin-token.outputs.token }}
run: >
gh pr review "$PR_NUMBER"
--repo airbytehq/airbyte
--approve
--body "Auto-approved by auto-merge workflow."
- name: Squash merge PR
if: steps.eligible.outputs.result == 'true' && vars.ENABLE_CONNECTOR_AUTO_MERGE == 'true'
env:
GH_TOKEN: ${{ steps.get-hoard-token.outputs.token }}
run: |
for attempt in 1 2 3; do
if gh pr merge "$PR_NUMBER" --repo airbytehq/airbyte --squash; then
echo "::notice::Merged PR ${PR_NUMBER}: ${PR_TITLE}"
exit 0
fi
echo "::warning::Merge attempt $attempt/3 failed, retrying in 60s..."
sleep 60
done
echo "::error::Failed to merge PR #$PR_NUMBER after 3 attempts"
exit 1
- name: Dry-run notice
if: steps.eligible.outputs.result == 'true' && vars.ENABLE_CONNECTOR_AUTO_MERGE != 'true'
run: echo "::notice::DRY RUN -- would merge PR ${PR_NUMBER}"