@1aboveio/skills 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/runtime/skills/distribution/generated/recipes.json +52 -32
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
- package/runtime/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
- package/skills/cicd-pipeline/mergify/references/watch-contract.md +2 -2
- package/skills/cicd-pipeline/mergify/scripts/watch-pr-delivery-core.mjs +48 -4
- package/skills/data-science/airflow/SKILL.md +198 -0
- package/skills/data-science/pyspark/assets/templates/etl.py +1 -0
- package/skills/data-science/pyspark/references/etl-contract.md +3 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +65 -15
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +1 -1
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +23 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +55 -11
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +4 -4
- package/skills/engineering/resolve-release/SKILL.md +2 -1
- package/skills/engineering/resolve-release/agents/openai.yaml +9 -0
- package/skills/engineering/resolve-release/references/related-skills.md +1 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +179 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +9 -0
- package/skills/engineering/rush-issues/evals/evals.json +65 -0
- package/skills/engineering/rush-issues/references/canary.md +45 -0
- package/skills/engineering/rush-issues/references/cicd.md +37 -0
- package/skills/engineering/rush-issues/references/combine.md +51 -0
- package/skills/engineering/rush-issues/references/expire.md +55 -0
- package/skills/engineering/rush-issues/references/exploration.md +53 -0
- package/skills/engineering/rush-issues/references/implementation.md +66 -0
- package/skills/engineering/rush-issues/references/preflight.md +31 -0
- package/skills/engineering/rush-issues/references/profiling.md +78 -0
- package/skills/engineering/rush-issues/references/review.md +48 -0
- package/skills/engineering/rush-issues/references/shared-modules.md +66 -0
- package/skills/engineering/rush-issues/references/task-plan.md +110 -0
- package/skills/engineering/rush-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/rush-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/rush-issues/scripts/preflight-models.mjs +466 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +99 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/evals/evals.json +44 -0
- package/skills/engineering/rush-release/references/candidate.md +30 -0
- package/skills/engineering/rush-release/references/cut.md +66 -0
- package/skills/engineering/rush-release/references/preflight.md +47 -0
- package/skills/engineering/rush-release/references/publish.md +100 -0
- package/skills/engineering/rush-release/scripts/apply.mjs +185 -0
- package/skills/engineering/rush-release/scripts/green-head.mjs +231 -0
- package/skills/engineering/rush-release/scripts/plan.mjs +264 -0
- package/skills/fullstack/zod-v4/SKILL.md +1 -1
- package/skills/data-science/airflow-dag-develop/SKILL.md +0 -111
- /package/skills/data-science/{airflow-dag-develop → airflow}/LICENSE +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Changelog + SemVer plan from baseline tag..SHA.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node plan.mjs --sha <sha> [--trunk main] [--json] [--strict]
|
|
6
|
+
//
|
|
7
|
+
// Exit: 0 ok · 1 git/inspect failed · 2 usage
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
12
|
+
|
|
13
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/
|
|
14
|
+
const TITLE_RE = /^\s*([a-zA-Z]+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/
|
|
15
|
+
const SECTION_BY_TYPE = {
|
|
16
|
+
feat: 'Added',
|
|
17
|
+
fix: 'Fixed',
|
|
18
|
+
perf: 'Changed',
|
|
19
|
+
refactor: 'Changed',
|
|
20
|
+
revert: 'Removed',
|
|
21
|
+
security: 'Security',
|
|
22
|
+
}
|
|
23
|
+
const INTERNAL_TYPES = new Set(['build', 'chore', 'ci', 'docs', 'style', 'test'])
|
|
24
|
+
const SECTION_ORDER = ['Breaking', 'Added', 'Changed', 'Removed', 'Fixed', 'Security']
|
|
25
|
+
|
|
26
|
+
export function parseSemver(input) {
|
|
27
|
+
const match = SEMVER_RE.exec(String(input || '').trim())
|
|
28
|
+
if (!match) return null
|
|
29
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatVersion(version) {
|
|
33
|
+
return `${version.major}.${version.minor}.${version.patch}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compareVersions(left, right) {
|
|
37
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
38
|
+
if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1
|
|
39
|
+
}
|
|
40
|
+
return 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function latestReleaseTag(tags) {
|
|
44
|
+
const parsed = tags
|
|
45
|
+
.map((tag) => ({ tag, version: parseSemver(tag) }))
|
|
46
|
+
.filter((item) => item.version)
|
|
47
|
+
if (parsed.length === 0) return null
|
|
48
|
+
parsed.sort((left, right) => compareVersions(left.version, right.version))
|
|
49
|
+
return parsed[parsed.length - 1]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function classifySubject(subject, body = '') {
|
|
53
|
+
const title = String(subject || '').trim()
|
|
54
|
+
const match = TITLE_RE.exec(title)
|
|
55
|
+
const type = match ? match[1].toLowerCase() : 'other'
|
|
56
|
+
const breaking = Boolean(match && match[3]) || /(^|\n)BREAKING[ -]CHANGE:/.test(body)
|
|
57
|
+
const internal = INTERNAL_TYPES.has(type)
|
|
58
|
+
const section = breaking ? 'Breaking' : internal ? null : (SECTION_BY_TYPE[type] || 'Changed')
|
|
59
|
+
return {
|
|
60
|
+
title,
|
|
61
|
+
type,
|
|
62
|
+
breaking,
|
|
63
|
+
internal,
|
|
64
|
+
section,
|
|
65
|
+
scope: match ? match[2] || null : null,
|
|
66
|
+
summary: match ? match[4] : title,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function bumpFor(changes, current, { strict = false } = {}) {
|
|
71
|
+
const zeroVer = !strict && current.major === 0
|
|
72
|
+
if (changes.some((change) => change.breaking)) {
|
|
73
|
+
return { bump: zeroVer ? 'minor' : 'major', reason: 'breaking change in the release content' }
|
|
74
|
+
}
|
|
75
|
+
if (changes.some((change) => change.type === 'feat')) {
|
|
76
|
+
return { bump: 'minor', reason: 'new feature in the release content' }
|
|
77
|
+
}
|
|
78
|
+
return { bump: 'patch', reason: 'fixes and internal changes only' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function nextVersion(current, bump) {
|
|
82
|
+
if (bump === 'major') return { major: current.major + 1, minor: 0, patch: 0 }
|
|
83
|
+
if (bump === 'minor') return { major: current.major, minor: current.minor + 1, patch: 0 }
|
|
84
|
+
return { major: current.major, minor: current.minor, patch: current.patch + 1 }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderChangelog({ version, date, changes }) {
|
|
88
|
+
const groups = new Map(SECTION_ORDER.map((name) => [name, []]))
|
|
89
|
+
for (const change of changes) {
|
|
90
|
+
if (!change.section) continue
|
|
91
|
+
const list = groups.get(change.section)
|
|
92
|
+
if (list) list.push(change)
|
|
93
|
+
}
|
|
94
|
+
const lines = [`## [${version}] - ${date}`]
|
|
95
|
+
for (const name of SECTION_ORDER) {
|
|
96
|
+
const items = groups.get(name)
|
|
97
|
+
if (!items.length) continue
|
|
98
|
+
lines.push('', `### ${name}`)
|
|
99
|
+
for (const item of items) lines.push(`- ${item.summary}`)
|
|
100
|
+
}
|
|
101
|
+
if (lines.length === 1) {
|
|
102
|
+
lines.push('', '- See the commits included in this tag.')
|
|
103
|
+
}
|
|
104
|
+
return `${lines.join('\n')}\n`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function discoverVersionFiles(root) {
|
|
108
|
+
const files = []
|
|
109
|
+
for (const name of ['package.json', 'package-lock.json', 'pyproject.toml', 'VERSION']) {
|
|
110
|
+
if (existsSync(join(root, name))) files.push(name)
|
|
111
|
+
}
|
|
112
|
+
return files
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function currentVersionFromFiles(root, files = discoverVersionFiles(root)) {
|
|
116
|
+
if (files.includes('package.json')) {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
118
|
+
const parsed = parseSemver(pkg.version)
|
|
119
|
+
if (parsed) return formatVersion(parsed)
|
|
120
|
+
}
|
|
121
|
+
if (files.includes('pyproject.toml')) {
|
|
122
|
+
const text = readFileSync(join(root, 'pyproject.toml'), 'utf8')
|
|
123
|
+
const match = text.match(/^version\s*=\s*"([^"]+)"/m)
|
|
124
|
+
const parsed = match && parseSemver(match[1])
|
|
125
|
+
if (parsed) return formatVersion(parsed)
|
|
126
|
+
}
|
|
127
|
+
if (files.includes('VERSION')) {
|
|
128
|
+
const parsed = parseSemver(readFileSync(join(root, 'VERSION'), 'utf8'))
|
|
129
|
+
if (parsed) return formatVersion(parsed)
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function git(args, cwd) {
|
|
135
|
+
const result = spawnSync('git', args, { encoding: 'utf8', cwd })
|
|
136
|
+
if (result.status !== 0) {
|
|
137
|
+
const err = new Error(result.stderr.trim() || `git ${args.join(' ')} failed`)
|
|
138
|
+
err.exitCode = 1
|
|
139
|
+
throw err
|
|
140
|
+
}
|
|
141
|
+
return result.stdout
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function versionFilesAt(sha, cwd) {
|
|
145
|
+
const tree = new Set(git(['ls-tree', '-r', '--name-only', sha], cwd)
|
|
146
|
+
.split('\n')
|
|
147
|
+
.map((line) => line.trim())
|
|
148
|
+
.filter(Boolean))
|
|
149
|
+
return ['package.json', 'package-lock.json', 'pyproject.toml', 'VERSION']
|
|
150
|
+
.filter((name) => tree.has(name))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function versionFromGitFiles(sha, files, cwd) {
|
|
154
|
+
const contents = (path) => git(['show', `${sha}:${path}`], cwd)
|
|
155
|
+
if (files.includes('package.json')) {
|
|
156
|
+
const parsed = parseSemver(JSON.parse(contents('package.json')).version)
|
|
157
|
+
if (parsed) return formatVersion(parsed)
|
|
158
|
+
}
|
|
159
|
+
if (files.includes('pyproject.toml')) {
|
|
160
|
+
const match = contents('pyproject.toml').match(/^version\s*=\s*"([^"]+)"/m)
|
|
161
|
+
const parsed = match && parseSemver(match[1])
|
|
162
|
+
if (parsed) return formatVersion(parsed)
|
|
163
|
+
}
|
|
164
|
+
if (files.includes('VERSION')) {
|
|
165
|
+
const parsed = parseSemver(contents('VERSION'))
|
|
166
|
+
if (parsed) return formatVersion(parsed)
|
|
167
|
+
}
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseLog(stdout) {
|
|
172
|
+
return stdout
|
|
173
|
+
.split('\u001e')
|
|
174
|
+
.map((chunk) => chunk.trim())
|
|
175
|
+
.filter(Boolean)
|
|
176
|
+
.map((chunk) => {
|
|
177
|
+
const [subject, ...rest] = chunk.split('\n')
|
|
178
|
+
return classifySubject(subject, rest.join('\n'))
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function planRelease({ sha, trunk = 'main', strict = false, cwd = process.cwd(), now = new Date() } = {}) {
|
|
183
|
+
if (!sha) {
|
|
184
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
185
|
+
err.exitCode = 2
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
git(['rev-parse', '--verify', sha], cwd)
|
|
189
|
+
const tags = git(['tag', '--merged', sha, '--list', 'v*'], cwd)
|
|
190
|
+
.split('\n')
|
|
191
|
+
.map((line) => line.trim())
|
|
192
|
+
.filter((tag) => parseSemver(tag))
|
|
193
|
+
const baseline = latestReleaseTag(tags)
|
|
194
|
+
const range = baseline ? `${baseline.tag}..${sha}` : sha
|
|
195
|
+
const logArgs = baseline
|
|
196
|
+
? ['log', range, '--first-parent', '--format=%s%n%b%x1e']
|
|
197
|
+
: ['log', sha, '--first-parent', '-n', '50', '--format=%s%n%b%x1e']
|
|
198
|
+
const changes = parseLog(git(logArgs, cwd))
|
|
199
|
+
const files = versionFilesAt(sha, cwd)
|
|
200
|
+
const fileVersion = versionFromGitFiles(sha, files, cwd)
|
|
201
|
+
const current = parseSemver(fileVersion) || baseline?.version || { major: 0, minor: 0, patch: 0 }
|
|
202
|
+
const { bump, reason } = bumpFor(changes, current, { strict })
|
|
203
|
+
const next = nextVersion(current, bump)
|
|
204
|
+
const version = formatVersion(next)
|
|
205
|
+
const date = now.toISOString().slice(0, 10)
|
|
206
|
+
return {
|
|
207
|
+
trunk,
|
|
208
|
+
sha,
|
|
209
|
+
baselineTag: baseline?.tag || null,
|
|
210
|
+
currentVersion: formatVersion(current),
|
|
211
|
+
bump,
|
|
212
|
+
reason,
|
|
213
|
+
nextVersion: version,
|
|
214
|
+
tag: `v${version}`,
|
|
215
|
+
versionFiles: files,
|
|
216
|
+
changes,
|
|
217
|
+
date,
|
|
218
|
+
changelogMarkdown: renderChangelog({ version, date, changes }),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseArgs(argv) {
|
|
223
|
+
const opts = { json: false, strict: false, trunk: 'main', sha: null }
|
|
224
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
225
|
+
const arg = argv[i]
|
|
226
|
+
if (arg === '--json') opts.json = true
|
|
227
|
+
else if (arg === '--strict') opts.strict = true
|
|
228
|
+
else if (arg === '--sha') opts.sha = argv[++i]
|
|
229
|
+
else if (arg === '--trunk') opts.trunk = argv[++i]
|
|
230
|
+
else if (arg === '--help' || arg === '-h') opts.help = true
|
|
231
|
+
else {
|
|
232
|
+
const err = new Error(`unknown argument: ${arg}`)
|
|
233
|
+
err.exitCode = 2
|
|
234
|
+
throw err
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (opts.help) return opts
|
|
238
|
+
if (!opts.sha || !opts.trunk) {
|
|
239
|
+
const err = new Error('usage: plan.mjs --sha <sha> [--trunk main] [--json] [--strict]')
|
|
240
|
+
err.exitCode = 2
|
|
241
|
+
throw err
|
|
242
|
+
}
|
|
243
|
+
return opts
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function main(argv = process.argv.slice(2)) {
|
|
247
|
+
const opts = parseArgs(argv)
|
|
248
|
+
if (opts.help) {
|
|
249
|
+
process.stdout.write('plan.mjs --sha <sha> [--trunk main] [--json] [--strict]\n')
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
const result = planRelease(opts)
|
|
253
|
+
process.stdout.write(opts.json ? `${JSON.stringify(result, null, 2)}\n` : `${result.tag}\n${result.changelogMarkdown}`)
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isMainModule(import.meta.url)) {
|
|
258
|
+
try {
|
|
259
|
+
process.exitCode = main()
|
|
260
|
+
} catch (error) {
|
|
261
|
+
process.stderr.write(`${error.message}\n`)
|
|
262
|
+
process.exitCode = error.exitCode || 1
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -22,7 +22,7 @@ Use these skills by name when the Zod change is part of a larger stack-specific
|
|
|
22
22
|
- `web-design-guidelines` for UI review after validation-driven form changes
|
|
23
23
|
- `vercel-react-best-practices` for React/Next.js performance-sensitive form patterns
|
|
24
24
|
- `app-debug` for runtime failures caused by validation or payload mismatches
|
|
25
|
-
- `airflow
|
|
25
|
+
- `airflow` only when Zod-related config or payload contracts affect DAG tooling
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: airflow-dag-develop
|
|
3
|
-
description: Airflow DAG architecture and conventions. Read when designing, creating, or debugging DAGs — covers CLI validation, environment-based scheduling, and pytest integrity tests.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Airflow DAG Development Patterns
|
|
8
|
-
|
|
9
|
-
## 1. Quick DAG Validation with Airflow CLI
|
|
10
|
-
|
|
11
|
-
Use these commands for fast feedback during development:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# List import errors (fastest check - catches syntax and import issues)
|
|
15
|
-
airflow dags list-import-errors
|
|
16
|
-
|
|
17
|
-
# Full lint check (comprehensive - catches best practice violations)
|
|
18
|
-
airflow dags lint
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
**When to use:**
|
|
22
|
-
- `list-import-errors`: During active development for quick syntax/import validation
|
|
23
|
-
- `lint`: Before commits to catch style and best practice issues
|
|
24
|
-
|
|
25
|
-
## 2. Environment-Based Schedule Override
|
|
26
|
-
|
|
27
|
-
Disable schedules in local development to prevent accidental runs:
|
|
28
|
-
|
|
29
|
-
```python
|
|
30
|
-
import os
|
|
31
|
-
from datetime import datetime
|
|
32
|
-
from airflow import DAG
|
|
33
|
-
|
|
34
|
-
def get_schedule(production_schedule: str) -> str | None:
|
|
35
|
-
"""Return None for local dev, actual schedule for deployed environments.
|
|
36
|
-
|
|
37
|
-
Set AIRFLOW_ENV=local in .env for local development.
|
|
38
|
-
Cloud Run deployments set AIRFLOW_ENV=dev|staging|prod.
|
|
39
|
-
"""
|
|
40
|
-
env = os.getenv("AIRFLOW_ENV", "local")
|
|
41
|
-
if env == "local":
|
|
42
|
-
return None
|
|
43
|
-
return production_schedule
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
**Usage in DAG:**
|
|
47
|
-
```python
|
|
48
|
-
with DAG(
|
|
49
|
-
dag_id="my_dag",
|
|
50
|
-
schedule=get_schedule("0 4 * * *"), # Daily 4 AM in prod, None locally
|
|
51
|
-
start_date=datetime(2024, 1, 1),
|
|
52
|
-
catchup=False,
|
|
53
|
-
) as dag:
|
|
54
|
-
...
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Environment setup:**
|
|
58
|
-
- **Local:** Add `AIRFLOW_ENV=local` to `.env`
|
|
59
|
-
- **Cloud Run:** Set via cloudbuild.yaml substitutions (`_AIRFLOW_ENV: dev|staging|prod`)
|
|
60
|
-
|
|
61
|
-
## 3. Pytest DAG Integrity Tests
|
|
62
|
-
|
|
63
|
-
Standard pytest patterns for validating DAG quality:
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
# tests/test_dag_integrity.py
|
|
67
|
-
import pytest
|
|
68
|
-
from airflow.models import DagBag
|
|
69
|
-
|
|
70
|
-
@pytest.fixture(scope="session")
|
|
71
|
-
def dagbag():
|
|
72
|
-
return DagBag(dag_folder="dags/", include_examples=False)
|
|
73
|
-
|
|
74
|
-
def test_no_import_errors(dagbag):
|
|
75
|
-
"""Verify all DAGs load without import errors."""
|
|
76
|
-
assert not dagbag.import_errors, f"DAG import errors: {dagbag.import_errors}"
|
|
77
|
-
|
|
78
|
-
def test_dags_have_tags(dagbag):
|
|
79
|
-
"""Verify all DAGs have at least one tag for organization."""
|
|
80
|
-
for dag_id, dag in dagbag.dags.items():
|
|
81
|
-
assert dag.tags, f"DAG {dag_id} has no tags"
|
|
82
|
-
|
|
83
|
-
def test_dags_have_owner(dagbag):
|
|
84
|
-
"""Verify all DAGs have an owner in default_args."""
|
|
85
|
-
for dag_id, dag in dagbag.dags.items():
|
|
86
|
-
assert dag.default_args.get("owner"), f"DAG {dag_id} missing owner"
|
|
87
|
-
|
|
88
|
-
def test_no_cycles(dagbag):
|
|
89
|
-
"""Verify DAGs don't have circular dependencies."""
|
|
90
|
-
for dag_id, dag in dagbag.dags.items():
|
|
91
|
-
# DagBag already validates this, but explicit test for clarity
|
|
92
|
-
assert dag.topo_sort(), f"DAG {dag_id} has cycles"
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Run tests:**
|
|
96
|
-
```bash
|
|
97
|
-
# Run all DAG tests
|
|
98
|
-
pytest tests/test_dag_integrity.py -v
|
|
99
|
-
|
|
100
|
-
# Run specific test
|
|
101
|
-
pytest tests/test_dag_integrity.py::test_no_import_errors -v
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Quick Reference
|
|
105
|
-
|
|
106
|
-
| Task | Command |
|
|
107
|
-
|------|---------|
|
|
108
|
-
| Check import errors | `airflow dags list-import-errors` |
|
|
109
|
-
| Lint DAGs | `airflow dags lint` |
|
|
110
|
-
| Run integrity tests | `pytest tests/test_dag_integrity.py -v` |
|
|
111
|
-
| Trigger manual run | `airflow dags trigger <dag_id>` |
|
|
File without changes
|