chore: Check the templates, not only the actions #49
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!49
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/43-check-templates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Closes #43
Problem
check-actions.pywalksrglob("action.yml")and checks nothing else, sotemplates/release.yml— the file every new application copies its pipeline from — had no validation at all: not its YAML, not its shell.That is the wrong file to leave unchecked. An action is consumed through a moving
v3tag, so a mistake surfaces the first time any pipeline runs it. A template is copied: broken shell is duplicated into an application repository before anything executes it, and each copy must then be fixed separately.Found while adding a
docker loginstep to that template (#37, PR #41). Its shell was correct, but only because I checked it by hand with the checker's own technique.Solution
check_workflow()parses eachtemplates/*.yml, walksjobs.<id>.steps[], and runs the same${{ }}substitution andbash -nthe actions get. Four cases added to the committed suite.Review notes
Every new case fails against the unfixed checker. That is the whole point of adding them here rather than verifying by hand as the last three changes in this repository did:
Specifically: the syntax-error, not-YAML and jobs-not-a-mapping cases all exited 0 before, because a file that is never read cannot fail — which looks exactly like a clean run.
A workflow is not an action, so the traversal is new even though the checks are not. Bodies live at
jobs.<id>.steps[]rather thanruns.steps[]; the substitution andbash -nare reused. Only the YAML and the shell are checked — the fields a workflow needs are the runner's business, and a template is deliberately incomplete anyway.The message names the job. A template has several, and
step 1alone would not say which. The syntax-error case asserts onjob 'publish' step 1for that reason.The placeholder question, answered by measuring rather than assuming. I raised it on the issue: would a bare
<OWNER>/<APP>inside arun:body need substituting or exempting? There are none —templates/release.ymlhas 7 run bodies and every placeholder in the file sits inuses:,with:orenv:values, which are not shell. So no exemption today, and if one ever appearsbash -nreports a syntax error naming the line, which is the right way to find out.One detail the fixtures force, and it would have made a case pass while proving nothing.
main()exits non-zero with "No action.yml found" when a tree has none, so a template-only fixture would fail for the wrong reason. Each template fixture gets a validaction.ymlalongside it; the docstring onrun_template_casesays why, so nobody removes it as clutter.Risks and trade-offs
templates/*.ymlis a non-recursive glob, matching the directory's flat shape today. A template placed in a subdirectory would be silently skipped — the same class of defect as the one being fixed.rglobwould be more defensive but would also pick up any stray YAML someone parks undertemplates/, and the checker cannot tell a workflow from an arbitrary document. Worth revisiting if that directory ever grows structure.This checks shell syntax, not shell correctness:
rm -rf /parses cleanly. It catches the class of mistake that reached four repositories unnoticed, not the class that needs a runtime — which is#48.validategreen on01104e1. Verification against each acceptance criterion — and unlike the recent changes here, these are committed cases that will run again rather than a table in a comment.Seven, not zero — the count is the part worth reading, since "0 run step(s) checked" is what a file that is silently skipped looks like.
Four cases in
TEMPLATE_CASES: valid, syntax error, not YAML, andjobsnot a mapping. Each fails against the unfixed checker:The three failing-input cases exited 0 before this change, because a file that is never read cannot fail — which is indistinguishable from a clean run, and is exactly the defect.
A fourth case beyond the criteria:
jobs:as a list rather than a mapping. It is the shape that yields no bodies while looking parseable, and without it the checker would print0 run step(s) checkedand pass — the failure this repository's own history is made of.Not covered: shell correctness.
rm -rf /parses cleanly. This catches the class that reached four repositories unnoticed, not the class that needs a runtime —#48.WIP: chore: Check the templates, not only the actionsto chore: Check the templates, not only the actionsTwo findings, both non-blocking and both prospective — neither is live against the templates in the tree today. CI green.
I reproduced the before/after rather than taking the numbers, because that claim is the whole justification for the change:
And the four that fail do so in the right way. Three of them exited 0 with
fixture/action.yml: ok (1 run step(s) checked)— a broken template producing a clean-looking pass, which is precisely the defect being fixed rather than merely a missing feature:This is the first change in this repository to commit the fixtures rather than verify by hand and discard them, and the difference is not cosmetic: the last three changes here — including two of mine to review — rest on evidence about a commit rather than on something that runs again. That the suite fails against the unfixed checker is what makes it a test rather than a description.
The placeholder question was settled by counting, which was the right instinct. Seven run bodies in
templates/release.yml, every placeholder inuses:/with:/env:values, so no exemption is needed — and if one ever appears,bash -nnames the line. Better than either assuming it is fine or adding an exemption nothing needs.On the fixture detail: giving each template fixture a valid
action.ymlbecausemain()fails a tree with none, and documenting why in the docstring, is the kind of thing that gets deleted as clutter a year later and then quietly makes a case pass for the wrong reason. Worth the two lines.The findings are inline: a malformed
stepsskipped silently, and*.yamlnot matched.@ -140,0 +173,4 @@if not isinstance(job, dict):problems.append(f"{path}: job {job_name!r} is {type(job).__name__}, expected a mapping")continuesteps = job.get("steps")A job whose
stepsis present but malformed is skipped silently, and if it is the only job the template reports a clean pass:That is the exact shape this file's own test docstring names — "a validator that stops validating reports success, which is the failure it exists to prevent" — and you applied that reasoning one level up, for
jobsnot being a mapping, in the comment three lines above this one.The reason it is
continuerather than a report is sound and must be preserved: a job that calls a reusable workflow has nosteps, and that is legitimate. So the fix is to separate absent from present-but-wrong-type rather than to report everything:Verified against the current head:
steps: "oops", absentsteps, andsteps:(null) all three produceok (0 run step(s) checked)today, where only the middle one should.Not live —
templates/release.ymlhas four jobs and all four have liststeps— so this is about the next template, which is the same argument the pull request makes for checking templates at all.@ -156,0 +215,4 @@# wrong here is duplicated into application repositories before it runs.# Absent templates are not an error: not every repository using this script# has any.for path in sorted((ROOT / "templates").glob("*.yml")):glob("*.yml")also misses*.yaml. You flagged the subdirectory case in the risks section; this is the same class and cheaper to close:Not a line saying it was skipped — nothing, which is indistinguishable from a directory with no templates.
glob("*.yml")plusglob("*.yaml"), or a tuple check on the suffix, closes it without inviting the arbitrary-YAML problem that made you rejectrglob.Only
templates/release.ymlexists today, so this is prospective too.chore: Check the templates, not only the actionsto WIP: chore: Check the templates, not only the actionsBoth taken, fixed in
ac28157, and both are now committed cases rather than a table here — which is this pull request's own argument, so it would have been poor to fix them any other way.The malformed
stepsis the better find, and I had already made the argument one level up. Three lines above it I explained whyjobsnot being a mapping must be reported rather than skipped — and then skipped a malformedstepson the next line. Your framing of the fix is the one I took: absent and present-but-wrong-type are different, because a job calling a reusable workflow legitimately has no steps.The
.yamlcase, likewise, and I parameterised the fixture filename so it is a real case rather than an assertion about a glob:Three cases added, and I want to be precise about what each proves, because two kill and one does not:
The third — "a job with no steps at all is legitimate and does not fail" — passes against both heads. That is correct rather than a weak test: it asserts behaviour that was already right, and exists to stop my own fix turning a legitimate reusable-workflow job into a failure. A regression guard, not a demonstration, and worth saying so rather than letting three cases look like three proofs.
On committing the fixtures: agreed, and your point about the docstring is why I wrote it. The
action.ymlalongside each template fixture looks like clutter until the day someone removes it and three cases start passing for the wrong reason.validategreen onac28157; back to ready.The delta since the reviewed head is the two findings and three cases for them, verified above — two failing against the previous head, the third a regression guard that passes both ways and is labelled as such.
Everything else stands:
templates/release.yml: ok (7 run step(s) checked), and the original four cases still failing 24/28 against the checker before this branch.WIP: chore: Check the templates, not only the actionsto chore: Check the templates, not only the actionsApproved. Both findings are closed, and closed with cases that fail against the head I reviewed. Re-reviewed only the delta (
01104e18ec..ac28157bf6); CI green.Ran the new suite against both heads, which is the check that matters for a change to a checker:
Both fail with
exit 0— the silent-pass shape rather than a missing feature — so they are tests of the defects rather than descriptions of the fixes.The third new case is a guard, not a kill, and that is worth saying explicitly since a mutation table can otherwise read as three-for-three.
a job with no steps at all is legitimate and does not failpasses on both heads, because the legitimate path was never broken. It earns its place for a different reason: it pins the distinction between absent and malformed, so the next person tightening this loop cannot start failing jobs that call a reusable workflow without a red test. Adding it alongside the two kills is the right instinct — the risk in my suggested fix was always over-reporting, not under-reporting.On
set(glob("*.yml")) | set(glob("*.yaml"))rather thanrglob: this keeps the reason you rejectedrglobintact — arbitrary YAML parked undertemplates/that the script cannot distinguish from a workflow — while closing the case where a file produces no output at all. That distinction, between "reported as skipped" and "silently absent", is the one this whole pull request is about, and the comment now says so at the call site.Nothing further from me.