Skip to content

Commit 0702891

Browse files
committed
chore: fix workflow to run on changed files
1 parent cf2dad0 commit 0702891

File tree

2 files changed

+241
-99
lines changed

2 files changed

+241
-99
lines changed

.github/workflows/lint.yaml

Lines changed: 0 additions & 99 deletions
This file was deleted.

.github/workflows/validate.yaml

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
name: Validate
3+
4+
on:
5+
push:
6+
pull_request:
7+
branches:
8+
- main
9+
10+
concurrency:
11+
group: ${{ github.ref }}-${{ github.workflow }}
12+
cancel-in-progress: true
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
list-configs:
19+
name: Discover changed ESPHome configs
20+
runs-on: ubuntu-latest
21+
outputs:
22+
configs: ${{ steps.collect.outputs.configs }}
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Get changed files
28+
id: changed-files
29+
uses: bjw-s-labs/action-changed-files@1a5aeab1bfa64d0c4e786f501d5a3f1fad4a24da # v0.4.1
30+
with:
31+
patterns: |-
32+
**/*.yaml
33+
**/*.yml
34+
35+
- name: Collect config files
36+
id: collect
37+
env:
38+
CHANGED_FILES: ${{ steps.changed-files.outputs.changed_files }}
39+
run: |
40+
python <<'PY'
41+
import json
42+
import os
43+
from pathlib import Path
44+
45+
changed_files_str = os.environ.get("CHANGED_FILES", "[]")
46+
try:
47+
changed_files = json.loads(changed_files_str)
48+
except json.JSONDecodeError:
49+
changed_files = []
50+
51+
if not isinstance(changed_files, list):
52+
changed_files = []
53+
54+
def first_friendly_name(path_str: str) -> str | None:
55+
try:
56+
path = Path(path_str)
57+
if not path.exists():
58+
return None
59+
with path.open("r", encoding="utf-8", errors="ignore") as f:
60+
for raw in f:
61+
line = raw.strip()
62+
if not line or line.startswith("#"):
63+
continue
64+
if line.startswith("friendly_name:"):
65+
value = line.split(":", 1)[1].strip()
66+
if (value.startswith('"') and value.endswith('"')) or (
67+
value.startswith("'") and value.endswith("'")
68+
):
69+
value = value[1:-1].strip()
70+
return value or None
71+
except Exception:
72+
pass
73+
return None
74+
75+
configs = []
76+
77+
for f in changed_files:
78+
friendly_name = first_friendly_name(f)
79+
if not friendly_name:
80+
continue
81+
82+
configs.append(
83+
{
84+
"file": f,
85+
"friendly_name": friendly_name,
86+
}
87+
)
88+
89+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
90+
fh.write(f"configs={json.dumps(configs)}\n")
91+
PY
92+
93+
- name: Generate Summary
94+
uses: actions/github-script@v7
95+
env:
96+
CONFIGS: ${{ steps.collect.outputs.configs }}
97+
CHANGED_FILES: ${{ steps.changed-files.outputs.changed_files }}
98+
with:
99+
script: |
100+
const configs = JSON.parse(process.env.CONFIGS || '[]');
101+
const changedFiles = JSON.parse(process.env.CHANGED_FILES || '[]');
102+
103+
// Find files that were changed but don't have friendly_name (not ESPHome configs)
104+
const skippedFiles = changedFiles.filter(file =>
105+
!configs.some(config => config.file === file) &&
106+
(file.endsWith('.yaml') || file.endsWith('.yml')) &&
107+
!file.includes('secrets.yaml') &&
108+
!file.includes('.github/')
109+
);
110+
111+
let summaryText = '';
112+
113+
if (configs.length === 0) {
114+
summaryText += '## 🔍 ESPHome Configuration Validation\n\n';
115+
summaryText += '### ℹ️ No ESPHome configs to validate\n\n';
116+
if (changedFiles.length > 0) {
117+
summaryText += `**Changed files:** ${changedFiles.length}\n`;
118+
summaryText += `**ESPHome configs:** 0\n\n`;
119+
}
120+
} else {
121+
const configList = configs.map(config => `- **${config.friendly_name}** (\`${config.file}\`)`);
122+
123+
summaryText += '## 🔍 ESPHome Configuration Validation\n\n';
124+
summaryText += '### 📋 Configs to validate:\n\n';
125+
summaryText += configList.join('\n') + '\n\n';
126+
summaryText += `**Total configs:** ${configs.length}\n\n`;
127+
}
128+
129+
if (skippedFiles.length > 0) {
130+
summaryText += '### ⚠️ Skipped files (not ESPHome configs):\n\n';
131+
skippedFiles.forEach(file => {
132+
summaryText += `- \`${file}\` (no friendly_name found)\n`;
133+
core.warning(`File ${file} appears to be a YAML file but doesn't contain a friendly_name - skipping validation`);
134+
});
135+
summaryText += '\n';
136+
}
137+
138+
core.summary.addRaw(summaryText).write();
139+
140+
validate:
141+
name: Validate ESPHome configs
142+
runs-on: ubuntu-latest
143+
needs: list-configs
144+
if: needs.list-configs.outputs.configs != '[]'
145+
strategy:
146+
fail-fast: false
147+
matrix:
148+
config: ${{ fromJson(needs.list-configs.outputs.configs) }}
149+
150+
steps:
151+
- name: Checkout
152+
uses: actions/checkout@v4
153+
154+
- name: Set up Python
155+
uses: actions/setup-python@v5
156+
with:
157+
python-version: "3.13"
158+
159+
- name: Install ESPHome
160+
run: pip install esphome
161+
162+
- name: Provide stub secrets.yaml
163+
run: |
164+
cat > secrets.yaml <<'EOF'
165+
wifi_ssid: "dummy-ssid"
166+
wifi_password: "dummy-password"
167+
api_key: "NTaJhzu9F3FQQW/TBqjSkr50euZwR/Po6Op50tmceiw="
168+
ota_password: "dummy-ota-password"
169+
EOF
170+
171+
- name: Validate config
172+
id: validate
173+
run: |
174+
set -o pipefail
175+
esphome config "${{ matrix.config.file }}" 2>&1 | tee esphome.log
176+
177+
- name: Save resolved config
178+
if: success()
179+
run: |
180+
esphome config "${{ matrix.config.file }}" > resolved-config.txt
181+
182+
- name: Upload log
183+
if: always()
184+
uses: actions/upload-artifact@v4
185+
with:
186+
name: esphome-log-${{ matrix.config.friendly_name }}
187+
path: esphome.log
188+
if-no-files-found: ignore
189+
190+
- name: Upload resolved config
191+
if: success()
192+
uses: actions/upload-artifact@v4
193+
with:
194+
name: esphome-config-${{ matrix.config.friendly_name }}
195+
path: resolved-config.txt
196+
if-no-files-found: ignore
197+
198+
- name: Add result to summary
199+
if: always()
200+
uses: actions/github-script@v7
201+
env:
202+
CONFIG_NAME: ${{ matrix.config.friendly_name }}
203+
CONFIG_FILE: ${{ matrix.config.file }}
204+
JOB_STATUS: ${{ job.status }}
205+
with:
206+
script: |-
207+
const fs = require('fs');
208+
const {
209+
CONFIG_NAME,
210+
CONFIG_FILE,
211+
JOB_STATUS,
212+
GITHUB_SERVER_URL,
213+
GITHUB_REPOSITORY,
214+
GITHUB_RUN_ID,
215+
} = process.env;
216+
217+
const ok = JOB_STATUS === 'success';
218+
const icon = ok ? '✅' : '❌';
219+
220+
let summary = `${icon} ${CONFIG_NAME} (\`${CONFIG_FILE}\`) - ${JOB_STATUS}\n\n`;
221+
222+
if (!ok) {
223+
try {
224+
const lines = fs.readFileSync('esphome.log', 'utf8').split('\n');
225+
const tail = lines.slice(-40).join('\n');
226+
summary += 'Last lines of log:\n\n';
227+
summary += '```text\n' + tail + '\n```\n\n';
228+
} catch (err) {
229+
core.warning(`Could not read esphome.log: ${err.message}`);
230+
}
231+
}
232+
233+
const runUrl = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`;
234+
summary += `[View full logs for this run](${runUrl})\n\n`;
235+
summary += `Artifacts:\n`;
236+
summary += `- \`esphome-log-${CONFIG_NAME}\` (full log)\n`;
237+
if (ok) {
238+
summary += `- \`esphome-config-${CONFIG_NAME}\` (resolved/validated config)\n`;
239+
}
240+
241+
core.summary.addRaw(summary).write();

0 commit comments

Comments
 (0)