Skip to content

Commit 79801af

Browse files
authored
Main-is-released check, and the codified star-chart shape (#34)
* ci(standards): assert main points at a release tag Reusable workflow for the invariant behind "main is the released version, not the newest work": every commit on main is a tagged release, so an untagged main head is itself the alarm. Callers pin it by SHA and run it on a schedule plus push to main. It separates three states that all look like "not tagged" from the outside. A repository with zero tags cannot be evaluated at all and says so rather than reporting drift. A drifted main reports the newest reachable tag and how many commits it is behind. A prerelease on main is its own failure by default, since a release candidate on the default branch is the exact drift this exists to catch. Read-only: contents: read, egress blocked to github.com, and no credentials persisted through checkout. fetch-depth: 0 because tags only travel with full history and a shallow clone would fail for the wrong reason and read as real drift. * ci(starchart): render the codified chart shape in both themes Scott drew the target and it is now the renderer. The chart reads as native GitHub UI rather than as a third-party embed: a 900x460 card on GitHub's own border colour, sans for the words and mono for every number, a 2px accent line over a faint gradient, interior gridlines and a solid baseline. The accent is the repository's logo colour, passed as a new required input, and an accent that is not a colour now fails instead of drawing a chart with no line. Three behaviours the renderer decides rather than hard-codes, each because the naive version produced something wrong on a real repository. The y-axis searches step-and-tick-count pairs, since rounding the step alone put drydock's 239 stars on a 0-400 axis with the curve in the bottom 60% of the plot. The curve is a monotone cubic, since a cardinal spline overshoots on a curve this flat and an overshoot on a cumulative count draws a dip that never happened. X labels drop to day precision when month names collide, which is the actual condition rather than a guessed span threshold. Two files ship now, not one. GitHub's theme toggle does not reach a media query inside an <img>-embedded SVG, so a self-theming file shows a white card to anyone reading GitHub dark with a light OS. It does drive a <picture> element in the README, so the pair is generated from one fetch and the markup chooses. They commit together or not at all: a <picture> with a fresh light chart and a stale dark one shows two different histories depending on who is looking, and nothing reports it. The documented trigger moves from a cron to the release cut. A committed artifact refreshed on a schedule mutates underneath a tag, which is what the main-is-released rule forbids. The renderer block is generated from ops render-chart.mjs by splice-into-workflow.mjs rather than hand-copied, and byte parity with that module was verified against live drydock data before this landed. Also fixes an assertion in the main-is-released test that sliced the whole if-block as the decisive expression and so could never pass. * docs(onboarding): add the main-is-released caller to the section 4 checklist
1 parent 82f48ca commit 79801af

6 files changed

Lines changed: 639 additions & 64 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from pathlib import Path
2+
import re
3+
import unittest
4+
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
WORKFLOW = ROOT / ".github/workflows/main-is-released.yml"
8+
9+
10+
class MainIsReleasedContractTest(unittest.TestCase):
11+
def test_reusable_workflow_shape_and_read_only_permissions(self):
12+
workflow = self.read_workflow()
13+
14+
for expected in (
15+
" workflow_call:\n",
16+
"permissions: {}",
17+
" runs-on: ubuntu-24.04",
18+
"uses: step-security/harden-runner@",
19+
"egress-policy: block",
20+
"github.com:443",
21+
" contents: read",
22+
):
23+
self.assertIn(expected, workflow)
24+
25+
# This check only ever reads. Any write scope here would be a
26+
# scheduled job with credentials to change the thing it audits.
27+
self.assertNotIn(": write", workflow)
28+
self.assertNotIn("persist-credentials: true", workflow)
29+
30+
def test_the_invariant_is_an_exact_tag_match(self):
31+
workflow = self.read_workflow()
32+
33+
self.assertIn("git describe --exact-match --tags HEAD", workflow)
34+
# --abbrev=0 alone answers "what tag is nearest", which is true of a
35+
# drifted main too. It may only be used to report inside the failure
36+
# branch, never in the condition that decides pass or fail — so the
37+
# decisive slice is the condition line, not the whole if-block.
38+
decisive = workflow.split("if ! tag=", 1)[1].split("\n", 1)[0]
39+
self.assertIn("--exact-match", decisive)
40+
self.assertNotIn("--abbrev=0", decisive)
41+
42+
def test_no_tags_is_reported_as_unevaluable_not_as_drift(self):
43+
"""A repo with zero tags produces the same 'not tagged' as one that
44+
drifted. Those need different answers, so the measurement proves it
45+
could have worked before its result is trusted."""
46+
workflow = self.read_workflow()
47+
48+
self.assertIn('if [ -z "$(git tag)" ]; then', workflow)
49+
self.assertLess(
50+
workflow.index('if [ -z "$(git tag)" ]'),
51+
workflow.index("git describe --exact-match"),
52+
)
53+
54+
def test_shallow_checkout_would_break_the_measurement(self):
55+
"""describe needs tags and history; a shallow clone fails for the
56+
wrong reason and reads as real drift."""
57+
workflow = self.read_workflow()
58+
self.assertIn("fetch-depth: 0", workflow)
59+
60+
def test_a_prerelease_on_main_fails_by_default(self):
61+
"""A release candidate on the default branch is the exact drift this
62+
exists to catch: drydock's main sat on v1.7.0-rc.2."""
63+
workflow = self.read_workflow()
64+
65+
self.assertIn(" default: false\n", workflow)
66+
self.assertIn('if [ "$ALLOW_PRERELEASE" != "true" ]', workflow)
67+
self.assertIn("ALLOW_PRERELEASE: ${{ inputs.allow-prerelease }}", workflow)
68+
69+
def test_failures_say_what_to_do_next(self):
70+
workflow = self.read_workflow()
71+
72+
for expected in ("::error::", "cut a release", "dev branch"):
73+
self.assertIn(expected, workflow)
74+
self.assertIn("set -euo pipefail", workflow)
75+
76+
def test_workflow_pins_actions_and_is_run_by_standards_validation(self):
77+
workflow = self.read_workflow()
78+
actions = re.findall(r"^\s+uses: ([^\s#]+)", workflow, re.MULTILINE)
79+
80+
self.assertTrue(actions)
81+
for action in actions:
82+
self.assertRegex(action, r"^[^@]+@[0-9a-f]{40}$")
83+
84+
validation = (ROOT / ".github/workflows/standards-validation.yml").read_text()
85+
self.assertIn("main_is_released_contract_test.py", validation)
86+
87+
def read_workflow(self):
88+
self.assertTrue(WORKFLOW.is_file(), f"missing workflow: {WORKFLOW}")
89+
return WORKFLOW.read_text()
90+
91+
92+
if __name__ == "__main__":
93+
unittest.main()

.github/tests/starchart_refresh_contract_test.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def test_reusable_workflow_shape_and_narrow_permissions(self):
2020
" required: true\n",
2121
" output-path:\n",
2222
" default: docs/assets/star-history.svg\n",
23+
" accent:\n",
2324
" max-pages:\n",
2425
" type: number\n",
2526
"permissions: {}",
@@ -62,10 +63,12 @@ def test_untrusted_input_is_read_from_the_environment(self):
6263
for expected in (
6364
"TARGET_REPO: ${{ github.repository }}",
6465
"OUTPUT_PATH: ${{ inputs.output-path }}",
66+
"ACCENT: ${{ inputs.accent }}",
6567
"MAX_PAGES: ${{ inputs.max-pages }}",
6668
"TARGET_BRANCH: ${{ inputs.branch }}",
6769
"const repo = process.env.TARGET_REPO",
6870
"const out = process.env.OUTPUT_PATH",
71+
"const accent = process.env.ACCENT",
6972
):
7073
self.assertIn(expected, workflow)
7174

@@ -78,8 +81,13 @@ def test_chart_is_self_contained_with_no_external_references(self):
7881
workflow = self.read_workflow()
7982

8083
self.assertIn("<svg xmlns=", workflow)
81-
self.assertIn("prefers-color-scheme: light", workflow)
8284
self.assertIn('role="img"', workflow)
85+
86+
# No media query, deliberately. GitHub's theme toggle does not reach
87+
# one inside an <img>-embedded SVG, so a self-theming file shows a
88+
# white card to anyone reading GitHub dark with a light OS. Two files
89+
# and a README <picture> is the mechanism that does follow the toggle.
90+
self.assertNotIn("prefers-color-scheme", workflow)
8391
for forbidden in ("<script", "xlink:href", "<foreignObject", "@import"):
8492
self.assertNotIn(forbidden, workflow)
8593

@@ -136,6 +144,60 @@ def test_generator_rejects_traversal_and_a_non_positive_page_cap(self):
136144
self.assertNotIn("Math.min(Math.ceil(total / 100), maxPages)", workflow)
137145
self.assertNotIn("::warning::capping", workflow)
138146

147+
def test_an_accent_that_is_not_a_colour_fails_rather_than_drawing_nothing(self):
148+
"""An unset or malformed accent reaches the SVG as stroke="" and
149+
renders a chart with no line: a green run producing a broken image,
150+
which is the silent-success shape this whole workflow exists to kill.
151+
"""
152+
workflow = self.read_workflow()
153+
154+
self.assertIn("/^#[0-9a-fA-F]{6}$/.test(accent ?? '')", workflow)
155+
# Required with no default, so a caller cannot inherit someone else's
156+
# brand colour by forgetting to pass its own.
157+
accent_block = workflow.split(" accent:\n", 1)[1].split(" max-pages:", 1)[0]
158+
self.assertIn("required: true", accent_block)
159+
self.assertNotIn("default:", accent_block)
160+
161+
def test_both_themes_are_written_and_committed_together(self):
162+
"""A <picture> that gained a fresh light chart and kept a stale dark
163+
one shows two different histories depending on who is looking, and
164+
nothing reports it as wrong."""
165+
workflow = self.read_workflow()
166+
167+
self.assertIn("writeFileSync(target, light)", workflow)
168+
self.assertIn("writeFileSync(darkTarget, dark)", workflow)
169+
self.assertIn('DARK_PATH="${OUTPUT_PATH%.svg}-dark.svg"', workflow)
170+
self.assertIn('git add -- "$OUTPUT_PATH" "$DARK_PATH"', workflow)
171+
self.assertIn('git status --porcelain -- "$OUTPUT_PATH" "$DARK_PATH"', workflow)
172+
173+
# Both derivations strip a .svg suffix, so the input has to have one.
174+
self.assertIn("!out.endsWith('.svg')", workflow)
175+
176+
def test_the_documented_trigger_is_the_release_cut_not_a_cron(self):
177+
"""A committed artifact refreshed on a schedule mutates underneath a
178+
tag, which is what 'main is the released version' forbids."""
179+
workflow = self.read_workflow()
180+
181+
example = workflow.split("# on:\n", 1)[1].split("# permissions:", 1)[0]
182+
self.assertIn("release:", example)
183+
self.assertIn("types: [published]", example)
184+
self.assertIn('# accent: "#49bcfb"', workflow)
185+
self.assertNotIn("cron", example)
186+
self.assertNotIn("schedule:", example)
187+
188+
def test_the_embedded_renderer_names_its_source(self):
189+
"""The same renderer exists here and in ops. Hand-copying is how they
190+
drift, so the block is generated and says so."""
191+
workflow = self.read_workflow()
192+
193+
self.assertIn("// BEGIN GENERATED FROM ops scripts/starchart/render-chart.mjs", workflow)
194+
self.assertIn("// END GENERATED", workflow)
195+
self.assertIn("splice-into-workflow.mjs", workflow)
196+
self.assertLess(
197+
workflow.index("// BEGIN GENERATED"),
198+
workflow.index("// END GENERATED"),
199+
)
200+
139201
def test_too_few_stars_is_a_clean_exit_not_a_failure(self):
140202
"""A young repo having one star is a real state, not a broken build.
141203
Reporting red there trains people to ignore the signal."""
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Main Is Released
2+
3+
# Asserts the one invariant behind "main is the released version, not the
4+
# newest work": every commit on main is a tagged release, so an untagged main
5+
# head is itself the alarm (REPOSITORY_ONBOARDING.md section 3).
6+
#
7+
# Callers declare their own triggers and pin this file by full commit SHA:
8+
#
9+
# on:
10+
# schedule: [{cron: "23 7 * * *"}]
11+
# push:
12+
# branches: [main]
13+
# workflow_dispatch:
14+
# permissions: {}
15+
# jobs:
16+
# released:
17+
# uses: CodesWhat/.github/.github/workflows/main-is-released.yml@<full SHA>
18+
#
19+
# Continuously deployed repositories that never tag (a website, a meta repo)
20+
# should not call this at all rather than calling it with a carve-out input.
21+
# For them "main equals production" is enforced by the deploy, not by a tag.
22+
23+
on:
24+
workflow_call:
25+
inputs:
26+
allow-prerelease:
27+
description: >
28+
Accept a prerelease tag (-rc.N, -beta.N) as satisfying the invariant.
29+
Defaults false: a release candidate on main is the exact drift this
30+
check exists to catch.
31+
required: false
32+
default: false
33+
type: boolean
34+
35+
permissions: {}
36+
37+
jobs:
38+
released:
39+
name: Main Is Released
40+
runs-on: ubuntu-24.04
41+
timeout-minutes: 5
42+
permissions:
43+
contents: read # Read main and its tags.
44+
45+
steps:
46+
- name: Harden runner
47+
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
48+
with:
49+
egress-policy: block
50+
allowed-endpoints: >
51+
github.com:443
52+
53+
- name: Check out main with tags
54+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
55+
with:
56+
ref: main
57+
# Tags only travel with full history; a shallow clone would make
58+
# describe fail for the wrong reason and read as real drift.
59+
fetch-depth: 0
60+
persist-credentials: false
61+
62+
- name: Assert main points at a release tag
63+
env:
64+
ALLOW_PRERELEASE: ${{ inputs.allow-prerelease }}
65+
run: |
66+
set -euo pipefail
67+
68+
# Prove the measurement can work before trusting its result. A repo
69+
# with no tags at all reports the same "not tagged" as one that
70+
# drifted, and those need different answers.
71+
if [ -z "$(git tag)" ]; then
72+
echo "::error::no tags in this repository, so the invariant cannot be evaluated; a repository that never releases should not call this workflow" >&2
73+
exit 1
74+
fi
75+
76+
if ! tag="$(git describe --exact-match --tags HEAD 2>/dev/null)"; then
77+
latest="$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo '<none reachable>')"
78+
ahead="$(git rev-list --count "${latest}..HEAD" 2>/dev/null || echo '?')"
79+
echo "::error::main is not a tagged release. Newest reachable tag is ${latest}, and main is ${ahead} commit(s) past it. Either cut a release or move the unshipped work to a dev branch." >&2
80+
exit 1
81+
fi
82+
83+
case "$tag" in
84+
*-*)
85+
if [ "$ALLOW_PRERELEASE" != "true" ]; then
86+
echo "::error::main points at prerelease ${tag}. Prereleases belong on the dev branch; main carries what users actually run." >&2
87+
exit 1
88+
fi
89+
echo "::warning::main points at prerelease ${tag}, accepted because allow-prerelease is set" ;;
90+
esac
91+
92+
echo "main is released at ${tag}"

.github/workflows/standards-validation.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
python3 .github/tests/quality_report_contract_test.py
5555
python3 .github/tests/reusable_ci_contract_test.py
5656
python3 .github/tests/starchart_refresh_contract_test.py
57+
python3 .github/tests/main_is_released_contract_test.py
5758
5859
- name: Lint Markdown
5960
run: |

0 commit comments

Comments
 (0)