From dfc93ed03acf4d23c187c33ada1ebfbf4fe70dbc Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Thu, 29 Oct 2026 23:34:35 -0400 Subject: [PATCH 01/44] fix: serve footer logos from published brand assets Retain recognized legacy footer URLs for safe README migration. Co-Authored-By: Claude --- CHANGELOG.md | 1 + README.md | 4 +- SKILL.md | 2 +- assets/brand/README.md | 4 +- assets/examples/footer-example.md | 4 +- assets/examples/readme-example.md | 4 +- references/brand.lock.json | 8 +++- references/brand.schema.json | 7 +++ references/brand.yaml | 8 +++- references/decisions.md | 10 ++-- references/readme-variants.md | 6 +-- scripts/ctshowcase/readme.py | 13 ++++-- tests/test_footer.py | 4 +- tests/test_footer_public_assets.py | 74 ++++++++++++++++++++++++++++++ 14 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 tests/test_footer_public_assets.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 46719ce..5878f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon` uses the bundled `references/art-direction.md` in every working directory. The option A glyph template uses dedicated, contrasting `icon_ink` and `icon_spark` brand tokens and the documented family seed (or the tool's default when absent) without requiring private estate files; explicit art-direction overrides remain supported and missing paths are named in the error. - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. +- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. diff --git a/README.md b/README.md index d6e46de..4c67f4e 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,8 @@ Code, templates, and documentation are licensed under the MIT License. CompleteT

- - CompleteTech LLC + + CompleteTech LLC
diff --git a/SKILL.md b/SKILL.md index e76ae1c..973e357 100644 --- a/SKILL.md +++ b/SKILL.md @@ -245,7 +245,7 @@ content sidebar --facts FILE [--packages FILE]... [--current FILE] [--repo OWNER | `org-banner` | Renders the 1600×400 org profile banner JPG (STANDARD §9.5). | `--out` | | `org-tile` | Renders the 400×160 org tile for profile directories. | `--out`, `--png` | | `intro` | Renders the profile intro (animated only for the personal profile and T1 flagship orgs; `.still` reduced-motion frame). | `--out` | -| `footer` | Prints the decision 18 footer between `showcase:footer` markers, or checks that the README ends with it; `--check --online` also HEADs both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate). `--org OWNER` answers `exempt`, `exempt:footer` or `hold` for an org outside the footer's scope; for the watching org, `--check` still reads the README (its `.github` gets the footer, and `--org` cannot tell it from a fork). | stdout | +| `footer` | Prints the decision 18 footer using the published masters in this skill's `assets/brand/`, between `showcase:footer` markers, or checks that the README ends with it; `--check --online` also HEADs both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate). `--org OWNER` answers `exempt`, `exempt:footer` or `hold` for an org outside the footer's scope; for the watching org, `--check` still reads the README (its `.github` gets the footer, and `--org` cannot tell it from a fork). | stdout | | `readme` | Scaffolds, upgrades or refreshes the header marker blocks; never drops a line (multiset check). `--bootstrap` fills a template body only while its marker's sha256 still matches (a person's edit makes the block theirs). | `README.md` with `--write`; `CONTRIBUTING.md` with `--bootstrap` | | `lint` | SVG rules, README rules (R7 plus decisions 13 and 18) and banner PNG checks; `public-leak` checks any public file the run writes against the estate's private set (private slugs and names under every login of the owner, members-only pins, internal notes from the org rows, the registry's top-level notes and judge_changes and the profile's internal keys; OPS-03), reading the private registry and `--meta-dir` only; `prose` runs the CQ-17 checks on text the run writes (§1.2 brand strings, no emoji, no placeholders, the §4.1 formula for descriptions and taglines, typos when the optional `codespell` is installed; `--preserved` text only warns). | nothing | | `contact-sheet` | Lays outputs out the way GitHub shows them, on `#0d1117` and `#ffffff` (proofing surfaces, not asset variants). `--org ORG --cross-surface` is the G4 gate: the org avatar at 32 px, a hero banner, a repo card, the social preview and the org banner on one labelled sheet; a missing surface is logged `needs-input:cross-surface:` (result `incomplete`). | `PATH.on-github-dark.png`, `PATH.on-github-light.png` | diff --git a/assets/brand/README.md b/assets/brand/README.md index f670542..7565616 100644 --- a/assets/brand/README.md +++ b/assets/brand/README.md @@ -4,8 +4,8 @@ The generators read these files through the `brand_assets` paths in `references/ | File | Pixels | Mode | Bytes | sha256 | Source and use | |---|---|---|---|---|---| -| `logo.png` | 2400×1198 | RGB, opaque `#F8FAFC` ground | 243,500 | `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` | The current light master, byte-identical to `CompleteTech-LLC/.github/brand/logo.png` and to the Skills `assets/logo.png`. `brand_assets.logo_light`: the full logo on the banner's light card when a repository has no icon and no org mark (the golden look). | -| `logo-dark.png` | 2400×1198 | RGBA, transparent | 210,785 | `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` | The current dark master, byte-identical to `CompleteTech-LLC/.github/brand/logo-dark.png`. `brand_assets.logo_dark`, used on navy surfaces (intro, org tile) while `dark_master: current`. | +| `logo.png` | 2400×1198 | RGB, opaque `#F8FAFC` ground | 243,500 | `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` | The current light master, historically sourced from `CompleteTech-LLC/.github/brand/logo.png`; the footer now uses this published copy because the former URL is missing. `brand_assets.logo_light`: the full logo on the banner's light card when a repository has no icon and no org mark (the golden look). | +| `logo-dark.png` | 2400×1198 | RGBA, transparent | 210,785 | `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` | The current dark master, historically sourced from `CompleteTech-LLC/.github/brand/logo-dark.png`; the footer now uses this published copy because the former URL is missing. `brand_assets.logo_dark`, used on navy surfaces (intro, org tile) while `dark_master: current`. | | `logo-dark-full.png` | 1200×599 | RGBA, transparent | 175,690 | `b21ca593975808302f919ca6fab9b10d69866a17c9683d3bff0999a99d7f578e` | The candidate dark master from the complete.tech website's theme (`complete-tech-logo-dark-full.png`), gradient wordmark. `brand_assets.logo_dark_candidate`; selected by setting `dark_master: candidate` once the user confirms decision 19. | | `mark-light.png` | 984×643 | RGB, opaque `#F8FAFC` ground | 130,704 | `45a047866068a2c028acbb252f2b9430ecdd3ec3e496b1188867253220f41f78` | The cloud-and-bulb crop (697, 26, 1681, 669) of `logo.png`: the same alpha bounds as the §9.2 `mark-dark.png` box, measured on `logo.png`. `brand_assets.mark_light`: used instead of the full logo in icon boxes under 200 px (the repo card's tile), where the wordmark would be about 3 px tall. Cropped once and committed (never at runtime); a §9.1 rule 2 approval is pending in `state/proposals.md` (set `mark_light: null` to withdraw it). | | `mark-dark.png` | — | — | — | — | Not shipped yet. The cloud-and-bulb mark is cropped once by the estate planner, on the chosen dark master, after decision 19 is confirmed. Until it exists, navy surfaces place the full dark logo scaled to fit and log `needs-input:mark-dark`. | diff --git a/assets/examples/footer-example.md b/assets/examples/footer-example.md index 7dee9d4..dee2c99 100644 --- a/assets/examples/footer-example.md +++ b/assets/examples/footer-example.md @@ -2,8 +2,8 @@

- - CompleteTech LLC + + CompleteTech LLC
diff --git a/assets/examples/readme-example.md b/assets/examples/readme-example.md index 41aeca7..7b0ea5e 100644 --- a/assets/examples/readme-example.md +++ b/assets/examples/readme-example.md @@ -50,8 +50,8 @@ Code and documentation are licensed under the MIT License; see `LICENSE`. Comple

- - CompleteTech LLC + + CompleteTech LLC
diff --git a/references/brand.lock.json b/references/brand.lock.json index 2e016c5..75f8716 100644 --- a/references/brand.lock.json +++ b/references/brand.lock.json @@ -460,10 +460,14 @@ "footer": { "alt": "CompleteTech LLC", "href": "https://www.complete.tech", + "legacy_logo_urls": [ + "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png", + "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png" + ], "line_prefix": "Built by", "line_suffix": "· Innovation at Every Integration", - "logo_dark_url": "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png", - "logo_light_url": "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png", + "logo_dark_url": "https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/logo-dark.png", + "logo_light_url": "https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/logo.png", "width": 200 }, "languages": { diff --git a/references/brand.schema.json b/references/brand.schema.json index 9e40fc9..8d08de3 100644 --- a/references/brand.schema.json +++ b/references/brand.schema.json @@ -352,6 +352,13 @@ }, "footer": { "type": "object", + "properties": { + "legacy_logo_urls": { + "type": "array", + "items": {"type": "string", "pattern": "^https://"}, + "uniqueItems": true + } + }, "required": [ "href", "logo_light_url", diff --git a/references/brand.yaml b/references/brand.yaml index fc0fc73..1d778ec 100644 --- a/references/brand.yaml +++ b/references/brand.yaml @@ -33,8 +33,12 @@ brand_assets: footer: href: https://www.complete.tech - logo_light_url: https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png - logo_dark_url: https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png + logo_light_url: https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/logo.png + logo_dark_url: https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/logo-dark.png + # Former publication paths: recognized only to migrate existing footers safely. + legacy_logo_urls: + - https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png + - https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png alt: CompleteTech LLC width: 200 line_prefix: Built by diff --git a/references/decisions.md b/references/decisions.md index 3738df1..9929dcd 100644 --- a/references/decisions.md +++ b/references/decisions.md @@ -1,6 +1,6 @@ # User decisions this skill implements -Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. +Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, with the footer image publication URLs repaired at the user's request on 2026-10-29. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. | Decision | Where the skill applies it | |---|---| @@ -59,17 +59,19 @@ Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-1 ## Footer and brand master -18. **Every in-scope README ends with the CompleteTech LLC footer** (user screenshot, 2026-10-25): +18. **Every in-scope README ends with the CompleteTech LLC footer** (user screenshot, 2026-10-25; publication-path repair requested 2026-10-29): - a centred logo about 200 px wide, linking to https://www.complete.tech - under it, the line `Built by [CompleteTech LLC](https://www.complete.tech) · Innovation at Every Integration` + The logo URLs point to the published, unchanged masters bundled with this skill; the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The layout, credit, theme selection and artwork are unchanged. + Use `` so **dark mode shows the transparent dark logo (no white box)** and light mode shows the light logo. Exact markup: ```html

- - CompleteTech LLC + + CompleteTech LLC
diff --git a/references/readme-variants.md b/references/readme-variants.md index 837129f..cea2718 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -234,8 +234,8 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file.

- - CompleteTech LLC + + CompleteTech LLC
@@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 9e1353f..ed77414 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -525,10 +525,13 @@ def is_footer_like(block: str) -> bool: def _footer_logo_url(url: str, brand) -> bool: """`url` is one of the decision 18 footer's own logo files (brand footer `logo_light_url` or `logo_dark_url`), - as configured or drifted only in scheme, branch, query or the github.com blob/raw form of the same file.""" + including explicitly configured legacy URLs, or drifted only in scheme, branch, query or the github.com + blob/raw form of the same file.""" url = url.strip().split("#", 1)[0].split("?", 1)[0] - for key in ("logo_light_url", "logo_dark_url"): - want = brand.raw["footer"][key] + footer = brand.raw["footer"] + urls = [footer[key] for key in ("logo_light_url", "logo_dark_url")] + urls.extend(footer.get("legacy_logo_urls", [])) + for want in urls: if url == want: return True m = re.match(r"^https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/[^/]+/(.+)$", want) @@ -604,6 +607,10 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: body, blocks = _strip_markers(text) want_footer = footer_wanted(ctx) removed: list[str] = [] + if want_footer and blocks.get("footer") and is_footer_copy(blocks["footer"], ctx.brand): + # A configured publication-path change replaces only our recognized footer, + # not authored credits or partner images inside a similarly named block. + removed += [ln for ln in blocks["footer"].splitlines() if ln.strip()] if want_footer: # an unmarked or drifted footer at the end is replaced by the marked one, never kept beside it body, old_footer = drop_old_footer(body, ctx.brand) removed += old_footer diff --git a/tests/test_footer.py b/tests/test_footer.py index 520cb66..6822863 100644 --- a/tests/test_footer.py +++ b/tests/test_footer.py @@ -3,8 +3,8 @@ DECISION_18 = """

- - CompleteTech LLC + + CompleteTech LLC
diff --git a/tests/test_footer_public_assets.py b/tests/test_footer_public_assets.py new file mode 100644 index 0000000..27d94e9 --- /dev/null +++ b/tests/test_footer_public_assets.py @@ -0,0 +1,74 @@ +"""The footer uses published masters and migrates the former broken asset paths.""" +import copy + +from conftest import ROOT, meta_dict +from ctshowcase import footer, readme +from ctshowcase.config import Brand +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + + +PUBLIC = "https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/" +LEGACY = "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/" + + +def relocated(brand): + raw = copy.deepcopy(brand.raw) + raw["footer"].update( + logo_light_url=PUBLIC + "logo.png", + logo_dark_url=PUBLIC + "logo-dark.png", + legacy_logo_urls=[LEGACY + "logo.png", LEGACY + "logo-dark.png"], + ) + return Brand(raw, brand.path) + + +def old_brand(brand): + raw = copy.deepcopy(brand.raw) + raw["footer"].update(logo_light_url=LEGACY + "logo.png", logo_dark_url=LEGACY + "logo-dark.png") + return Brand(raw, brand.path) + + +def test_configured_footer_points_to_the_published_bundled_masters(brand): + for key, filename in (("logo_light_url", "logo.png"), ("logo_dark_url", "logo-dark.png")): + assert brand.raw["footer"][key] == PUBLIC + filename + assert (ROOT / "assets" / "brand" / filename).is_file() + seen = [] + + def published(url): + seen.append(url) + return 200 if url.startswith(PUBLIC) else 404 + + assert footer.online(brand, head=published)[0] == "pass" + assert seen == [PUBLIC + "logo.png", PUBLIC + "logo-dark.png"] + + +def test_upgrade_replaces_marked_legacy_footer_without_shrink(brand, tmp_path): + before_ctx = RepoContext(old_brand(brand), normalize(meta_dict()), tmp_path) + after_ctx = RepoContext(relocated(brand), normalize(meta_dict()), tmp_path) + before = readme.scaffold(before_ctx) + after, removed = readme.upgrade(after_ctx, before) + assert readme.preservation(before, after, removed) == [] + assert LEGACY not in after + assert footer.check(after_ctx.brand, after)[0] == "pass" + again, _ = readme.upgrade(after_ctx, after) + assert again == after + + +def test_unmarked_legacy_footer_is_replaced_but_partner_credit_is_not(brand): + current = relocated(brand) + old = footer.markup(old_brand(brand)) + body, removed = readme.drop_old_footer("# Synthetic\n\n" + old + "\n", current) + assert body == "# Synthetic\n" + assert removed == [line for line in old.splitlines() if line.strip()] + partner = old.replace(LEGACY + "logo.png", "https://example.test/partner.png").replace( + 'alt="CompleteTech LLC"', 'alt="Partner"' + ) + assert not readme.is_footer_copy(partner, current) + assert readme.drop_old_footer("# Synthetic\n\n" + partner, current)[1] == [] + + +def test_legacy_url_matching_is_limited_to_configured_asset_files(brand): + current = relocated(brand) + assert readme._footer_logo_url(LEGACY + "logo.png", current) + assert readme._footer_logo_url(LEGACY.replace("/main/", "/release/") + "logo-dark.png?v=2", current) + assert not readme._footer_logo_url(LEGACY + "partner-logo.png", current) From 5a3d78d8de9e0eac7f59d3e70b8ff5a5873a0373 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Thu, 29 Oct 2026 23:35:21 -0400 Subject: [PATCH 02/44] test: preserve authored footer marker content Co-Authored-By: Claude --- tests/test_footer_public_assets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_footer_public_assets.py b/tests/test_footer_public_assets.py index 27d94e9..ce27be1 100644 --- a/tests/test_footer_public_assets.py +++ b/tests/test_footer_public_assets.py @@ -72,3 +72,15 @@ def test_legacy_url_matching_is_limited_to_configured_asset_files(brand): assert readme._footer_logo_url(LEGACY + "logo.png", current) assert readme._footer_logo_url(LEGACY.replace("/main/", "/release/") + "logo-dark.png?v=2", current) assert not readme._footer_logo_url(LEGACY + "partner-logo.png", current) + + +def test_authored_content_inside_footer_markers_is_not_exempt(brand, tmp_path): + current = relocated(brand) + ctx = RepoContext(current, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace( + footer.END, "Authored partner acknowledgement.\n" + footer.END + ) + after, removed = readme.upgrade(ctx, before) + assert "Authored partner acknowledgement." not in removed + assert any("Authored partner acknowledgement." in problem + for problem in readme.preservation(before, after, removed)) From bd727adb2542223e6fddea5980f12f0ca869b9e1 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:17:25 -0400 Subject: [PATCH 03/44] ci: add scheduled footer logo URL check Adds a weekly and manual workflow that runs 'python scripts/showcase.py footer --check README.md --online' so a dead footer logo URL fails CI. Part of #47 (subissue #50). Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/footer-assets.yml | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/footer-assets.yml diff --git a/.github/workflows/footer-assets.yml b/.github/workflows/footer-assets.yml new file mode 100644 index 0000000..51cd7af --- /dev/null +++ b/.github/workflows/footer-assets.yml @@ -0,0 +1,37 @@ +# Footer assets +# Fails on any result other than pass, e.g. when a footer logo URL returns 404 (issue 47, subissue 50). +# Flag spelling confirmed with `python scripts/showcase.py footer --help`: `--check README.md --online` +# HEADs both decision 18 raw logo URLs and exits non-zero unless both return 200. + +name: Footer assets + +on: + schedule: + - cron: '17 6 * * 1' + workflow_dispatch: + pull_request: + paths: + - references/brand.yaml + - README.md + - assets/brand/** + - scripts/ctshowcase/footer.py + - .github/workflows/footer-assets.yml + +permissions: + contents: read + +jobs: + footer-online: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Python dependencies + run: python -m pip install -r requirements.txt + - name: Real HEAD against footer logo URLs + run: python scripts/showcase.py footer --check README.md --online From ada30646f9a796496b746fca17c3b9dc3923d69e Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:22:17 -0400 Subject: [PATCH 04/44] fix: migrate legacy footer on fork and backup READMEs, guard custom footer images - callout_only (variants G and H) now replaces a recognized old footer, marked or unmarked, and reports the replaced lines to the preservation check. - is_footer_copy requires every src/srcset to be a footer logo URL; a matching alt no longer vouches for a custom image. - decisions.md: drop the unsupported user-request attribution, add a neutral note outside the verbatim excerpt. - Tests for G/H marked and unmarked legacy footers, the guard, the scheduled workflow and the deliberately legacy template fixture. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- references/decisions.md | 6 +- scripts/ctshowcase/readme.py | 43 +++++++---- tests/test_footer_legacy_gh.py | 111 +++++++++++++++++++++++++++++ tests/test_footer_public_assets.py | 18 +++++ tests/test_readme_upgrade.py | 15 ++++ 6 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 tests/test_footer_legacy_gh.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5878f1a..57309ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon` uses the bundled `references/art-direction.md` in every working directory. The option A glyph template uses dedicated, contrasting `icon_ink` and `icon_spark` brand tokens and the documented family seed (or the tool's default when absent) without requiring private estate files; explicit art-direction overrides remain supported and missing paths are named in the error. - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. -- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. +- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. diff --git a/references/decisions.md b/references/decisions.md index 9929dcd..89401d4 100644 --- a/references/decisions.md +++ b/references/decisions.md @@ -1,6 +1,8 @@ # User decisions this skill implements -Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, with the footer image publication URLs repaired at the user's request on 2026-10-29. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. +Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. + +Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. | Decision | Where the skill applies it | |---|---| @@ -59,7 +61,7 @@ Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-1 ## Footer and brand master -18. **Every in-scope README ends with the CompleteTech LLC footer** (user screenshot, 2026-10-25; publication-path repair requested 2026-10-29): +18. **Every in-scope README ends with the CompleteTech LLC footer** (user screenshot, 2026-10-25): - a centred logo about 200 px wide, linking to https://www.complete.tech - under it, the line `Built by [CompleteTech LLC](https://www.complete.tech) · Innovation at Every Integration` diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index ed77414..eafc3e0 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -463,11 +463,14 @@ def fork_callout(ctx: RepoContext) -> str | None: BANNER_BLOCK = re.compile(re.escape(start("banner")) + r".*?" + re.escape(end("banner")) + r"\n*", re.S) -def callout_only(ctx: RepoContext, text: str) -> str: +def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: """Variants G and H (§7.4): the hero banner block, then the callout (unless one is already there), then the body unchanged, then the decision 18 footer. Nothing else changes. Watching-org forks never reach here (`readme` exempts them). Review q27 "Full CompleteTech treatment" (brand.yaml readme.fork_readme: full) means the fork - follows its §5.4 variant instead: pass that variant with `readme --variant`.""" + follows its §5.4 variant instead: pass that variant with `readme --variant`. + Returns (text, removed lines): a recognized old footer (marked or unmarked, `is_footer_copy`: for example one with + the former logo URLs) is replaced by the current one, and its lines are reported so the preservation check accepts + them. A marked footer that is not a footer copy (a partner image) is left as it is.""" if ctx.det["variant"] == "G" and ctx.brand.readme_opts.get("fork_readme") == "full": ctx.log("needs-input:fork-variant (q27 full treatment: pass readme --variant for the §5.4 variant)") body = text @@ -490,9 +493,21 @@ def callout_only(ctx: RepoContext, text: str) -> str: elif m: lead_banner = text.lstrip("\n")[:m.end()].rstrip("\n") + "\n\n" out = lead_banner + out.lstrip("\n") - if footer_wanted(ctx) and footermod.check(ctx.brand, out)[0] == "fail:missing": - out = out.rstrip("\n") + "\n\n" + footermod.block(ctx.brand) + "\n" - return out + removed: list[str] = [] + if footer_wanted(ctx): + if footermod.check(ctx.brand, out)[0] != "pass": + pos, kept = 0, [] + for fm in find_blocks(MARK, out): + if fm.group(1) == "footer" and is_footer_copy(fm.group(2), ctx.brand): + kept.append(out[pos:fm.start()]) + removed += [ln for ln in fm.group(2).splitlines() if ln.strip()] + pos = fm.end() + out = "".join(kept) + out[pos:] + out, old_footer = drop_old_footer(out, ctx.brand) + removed += old_footer + if footermod.check(ctx.brand, out)[0] == "fail:missing": + out = out.rstrip("\n") + "\n\n" + footermod.block(ctx.brand) + "\n" + return out, removed def preserved(ctx: RepoContext, text: str) -> bool: @@ -547,7 +562,7 @@ def _footer_logo_url(url: str, brand) -> bool: def is_footer_copy(block: str, brand) -> bool: """Every line of `block` is a line of the decision 18 footer or a drifted copy of one: only centring, link, logo, `
`/`` markup, with complete.tech links, the footer's own logo image (its src or srcset is the brand footer - logo URL or a drifted form of it, or its alt is exactly the company) and as text only the credit line ("Built by + logo URL or a drifted form of it; an image without src/srcset passes on an alt that is exactly the company) and as text only the credit line ("Built by ", optionally with the suffix). A sponsor or partner image (whatever its file name), a thanks line or any other authored text makes it not a footer copy, so it is never removed.""" import html @@ -560,15 +575,17 @@ def tag_ok(m: re.Match) -> bool: return True if name == "a": return "complete.tech" in attrs.lower() + alt_ok, urls = False, [] for a in FOOTER_ATTR.finditer(attrs): # never a file name that merely contains "logo" key = a.group(1).lower() val = html.unescape(next(g for g in a.groups()[1:] if g is not None)).strip() - if key == "alt" and val in (company, f["alt"]): - return True - parts = [p.split()[0] for p in val.split(",") if p.split()] - if key in ("src", "srcset") and parts and all(_footer_logo_url(u, brand) for u in parts): - return True - return False + if key == "alt": + alt_ok = alt_ok or val in (company, f["alt"]) + else: + urls.append([p.split()[0] for p in val.split(",") if p.split()]) + if urls: # a matching alt never vouches for another image: every src/srcset must be a footer logo URL + return all(parts and all(_footer_logo_url(u, brand) for u in parts) for parts in urls) + return alt_ok for line in block.splitlines(): if not line.strip(): @@ -602,7 +619,7 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: variant = ctx.det["variant"] rv = standard("readme-variants") if variant in ("G", "H"): - return callout_only(ctx, normalize_text(text)), [] + return callout_only(ctx, normalize_text(text)) text = normalize_text(text) body, blocks = _strip_markers(text) want_footer = footer_wanted(ctx) diff --git a/tests/test_footer_legacy_gh.py b/tests/test_footer_legacy_gh.py new file mode 100644 index 0000000..862aeec --- /dev/null +++ b/tests/test_footer_legacy_gh.py @@ -0,0 +1,111 @@ +"""Fork (G) and backup (H) READMEs migrate the former footer logo URLs (issue 47). + +`readme.upgrade` returns early through `callout_only` for variants G and H. These tests pin that the legacy footer +(marked or unmarked) is replaced there too, that the replaced lines are accepted by the preservation check, and that +the guard which exempts replaced lines never vouches for a custom image just because its alt text matches. +""" +import copy + +import pytest + +from conftest import meta_dict +from ctshowcase import footer, readme +from ctshowcase.config import Brand +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +LEGACY = "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/" +PARENT = {"full_name": "someone/upstream", "default_branch": "main"} +UPSTREAM = "# Upstream project\n\nAuthored upstream text, kept as written.\n\n## Usage\n\nRun it.\n" +VARIANTS = { + "G": dict(fork=True, parent=PARENT), + "H": dict(repo="host-backup-20260914"), +} + + +def legacy_brand(brand): + raw = copy.deepcopy(brand.raw) + raw["footer"].update(logo_light_url=LEGACY + "logo.png", logo_dark_url=LEGACY + "logo-dark.png") + return Brand(raw, brand.path) + + +def ctx_for(brand, variant, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict(**VARIANTS[variant])), tmp_path) + assert ctx.det["variant"] == variant + return ctx + + +def legacy_readme(brand, marked): + old = legacy_brand(brand) + return UPSTREAM + "\n" + (footer.block(old) if marked else footer.markup(old)) + "\n" + + +@pytest.mark.parametrize("marked", [True, False], ids=["marked", "unmarked"]) +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_legacy_footer_is_migrated_on_fork_and_backup_readmes(brand, tmp_path, variant, marked): + ctx = ctx_for(brand, variant, tmp_path) + before = legacy_readme(brand, marked) + assert LEGACY in before and footer.check(brand, before)[0] != "pass" + after, removed = readme.upgrade(ctx, before) + assert LEGACY not in after + assert footer.check(brand, after)[0] == "pass" # exactly one footer: no fail:duplicate + assert after.count("complete.tech") == footer.markup(brand).count("complete.tech") + assert removed, "the replaced footer lines are reported" + assert readme.preservation(before, after, removed) == [] + assert UPSTREAM.strip() in after # authored content above the footer is byte-preserved + again, _ = readme.upgrade(ctx_for(brand, variant, tmp_path), after) + assert again == after + + +def custom_footer(brand): + f = brand.raw["footer"] + block = footer.block(brand).replace(f["logo_light_url"], "https://example.test/partner-badge.png") + block = block.replace(f["logo_dark_url"], "https://example.test/partner-badge-dark.png") + return block.replace(f'alt="{f["alt"]}"', 'alt="Partner"') + + +def partner_alt_kept(brand): + """A marked footer whose light image is a partner badge while the alt text stays the company name.""" + return footer.block(brand).replace(brand.raw["footer"]["logo_light_url"], "https://example.test/partner-badge.png") + + +def test_fork_readme_with_custom_marked_footer_is_left_untouched(brand, tmp_path): + ctx = ctx_for(brand, "G", tmp_path) + custom = custom_footer(brand) + before = UPSTREAM + "\n" + custom + "\n" + after, removed = readme.upgrade(ctx, before) + assert removed == [] + assert custom in after + assert readme.preservation(before, after, removed) == [] + + +def test_matching_alt_does_not_vouch_for_a_custom_image_src(brand, tmp_path): + block = partner_alt_kept(brand) + assert 'alt="CompleteTech LLC"' in block + assert not readme.is_footer_copy(block, brand) + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), block) + assert "partner-badge.png" in before + after, removed = readme.upgrade(ctx, before) + # same outcome as origin/main: the custom image is not exempted, so the preservation check raises readme-shrink + assert readme.preservation(before, after, removed) + + +def test_fork_with_alt_matching_custom_src_is_not_replaced(brand, tmp_path): + ctx = ctx_for(brand, "G", tmp_path) + before = UPSTREAM + "\n" + partner_alt_kept(brand) + "\n" + after, removed = readme.upgrade(ctx, before) + assert "partner-badge.png" in after and removed == [] + + +def test_footer_copy_guard_accepts_alt_only_current_and_legacy_images(brand): + markup = footer.markup(brand) + assert readme.is_footer_copy(markup, brand) + legacy = footer.markup(legacy_brand(brand)) + assert LEGACY in legacy and readme.is_footer_copy(legacy, brand) # legacy src with matching alt + light = brand.raw["footer"]["logo_light_url"] + alt_only = markup.replace(f', "<img alt=")
+    assert alt_only != markup and "<img alt=" in alt_only
+    assert readme.is_footer_copy(alt_only, brand)  # alt-only img, no src
+    other = markup.replace(light, "https://example.test/x.png").replace( Date: Sat, 31 Oct 2026 15:39:10 -0400 Subject: [PATCH 05/44] fix(footer): recognize former local logo files with the company alt Old footers that reference assets/logo.png (and the other former local logo names) are a footer copy again when the image alt is exactly the company or the footer alt. Published URLs still must be footer logo URLs, and any other src keeps the image from counting as a footer copy. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index eafc3e0..848f5ab 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -537,6 +537,21 @@ def is_footer_like(block: str) -> bool: FOOTER_ATTR = re.compile(r"""\b(src|srcset|alt)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))""", re.I) +OLD_LOCAL_LOGOS = {'assets/logo.png', 'logo.png', 'assets/logo-dark.png', 'logo-dark.png'} + + +def _old_local_logo(url: str) -> bool: + """`url` is a former local footer logo file (`OLD_LOCAL_LOGOS`), relative and without a scheme, host or data URI.""" + url = url.strip() + if "://" in url or url.startswith("//") or url.startswith("data:"): + return False + path = url.split("#", 1)[0].split("?", 1)[0] + if path.startswith("./"): + path = path[2:] + elif path.startswith("/"): + path = path[1:] + return path in OLD_LOCAL_LOGOS + def _footer_logo_url(url: str, brand) -> bool: """`url` is one of the decision 18 footer's own logo files (brand footer `logo_light_url` or `logo_dark_url`), @@ -563,7 +578,7 @@ def is_footer_copy(block: str, brand) -> bool: """Every line of `block` is a line of the decision 18 footer or a drifted copy of one: only centring, link, logo, `
`/`` markup, with complete.tech links, the footer's own logo image (its src or srcset is the brand footer logo URL or a drifted form of it; an image without src/srcset passes on an alt that is exactly the company) and as text only the credit line ("Built by - ", optionally with the suffix). A sponsor or partner image (whatever its file name), a thanks line or + ", optionally with the suffix). The former local logo files (assets/logo.png and the other `OLD_LOCAL_LOGOS`) are also recognized when the image's alt is exactly the company or the footer alt. A sponsor or partner image (whatever its file name), a thanks line or any other authored text makes it not a footer copy, so it is never removed.""" import html f, company = brand.raw["footer"], brand.strings["company"] @@ -584,7 +599,8 @@ def tag_ok(m: re.Match) -> bool: else: urls.append([p.split()[0] for p in val.split(",") if p.split()]) if urls: # a matching alt never vouches for another image: every src/srcset must be a footer logo URL - return all(parts and all(_footer_logo_url(u, brand) for u in parts) for parts in urls) + return all(parts and all(_footer_logo_url(u, brand) or (alt_ok and _old_local_logo(u)) for u in parts) + for parts in urls) return alt_ok for line in block.splitlines(): From e3d85a5cf2582465b9f8ed1de7243ef44f88cbe5 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:40:30 -0400 Subject: [PATCH 06/44] test: cover old local-logo footer recognition alongside remote legacy URLs Co-Authored-By: Claude Sonnet 5.5 --- tests/test_footer_legacy_gh.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_footer_legacy_gh.py b/tests/test_footer_legacy_gh.py index 862aeec..9a47096 100644 --- a/tests/test_footer_legacy_gh.py +++ b/tests/test_footer_legacy_gh.py @@ -109,3 +109,38 @@ def test_footer_copy_guard_accepts_alt_only_current_and_legacy_images(brand): assert readme.is_footer_copy(alt_only, brand) # alt-only img, no src other = markup.replace(light, "https://example.test/x.png").replace('alt="CompleteTech LLC"', 'alt="Partner"') assert not readme.is_footer_copy(other, brand) + + +# ------------------------------------------------------------------ former local-logo footer and remote legacy URL footer +def old_local_footer(src, alt="CompleteTech LLC"): + return ('

\n %s
\n' + ' Built by CompleteTech LLC · Innovation at Every Integration\n' + "

\n" % (src, alt)) + + +@pytest.mark.parametrize("src", ["assets/logo.png", "./logo.png"]) +def test_footer_copy_accepts_old_local_logo_and_remote_legacy_url_at_once(brand, src): + assert readme.is_footer_copy(old_local_footer(src), brand) + legacy = footer.markup(legacy_brand(brand)) + assert LEGACY in legacy and readme.is_footer_copy(legacy, brand) + + +@pytest.mark.parametrize("src", ["assets/logo.png", "./logo.png"]) +def test_upgrade_on_old_local_logo_footer_yields_one_built_by(brand, tmp_path, src): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), old_local_footer(src)) + assert "Built by" in before and src in before and footer.block(brand) not in before + after, removed = readme.upgrade(ctx, before) + assert after.count("Built by") == 1 + assert removed and readme.preservation(before, after, removed) == [] + + +@pytest.mark.parametrize("block", [ + old_local_footer("assets/partner.png"), + old_local_footer("assets/logo.png", alt="Partner"), + old_local_footer("https://example.test/assets/logo.png"), +], ids=["partner-file", "wrong-alt", "foreign-host"]) +def test_non_footer_images_are_not_footer_copies_and_are_left(brand, block): + assert not readme.is_footer_copy(block, brand) + text = "# A\n\n" + block + assert readme.drop_old_footer(text, brand) == (text, []) From 740303d8df0158b318874ac36cb733dd5709898f Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:56:08 -0400 Subject: [PATCH 07/44] fix(readme): always replace a removed footer when fenced example text makes footer check report drift callout_only now tracks whether a recognized footer was removed (marked loop or drop_old_footer) and appends the replacement in that case, even when footermod.check reports fail:drift because of footer-like text inside a fenced example. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 848f5ab..899cca1 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -470,7 +470,8 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: follows its §5.4 variant instead: pass that variant with `readme --variant`. Returns (text, removed lines): a recognized old footer (marked or unmarked, `is_footer_copy`: for example one with the former logo URLs) is replaced by the current one, and its lines are reported so the preservation check accepts - them. A marked footer that is not a footer copy (a partner image) is left as it is.""" + them. A marked footer that is not a footer copy (a partner image) is left as it is. A removed recognized footer is + always replaced, even when fenced example text makes check() report drift.""" if ctx.det["variant"] == "G" and ctx.brand.readme_opts.get("fork_readme") == "full": ctx.log("needs-input:fork-variant (q27 full treatment: pass readme --variant for the §5.4 variant)") body = text @@ -495,17 +496,21 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: out = lead_banner + out.lstrip("\n") removed: list[str] = [] if footer_wanted(ctx): + had_footer = False if footermod.check(ctx.brand, out)[0] != "pass": pos, kept = 0, [] for fm in find_blocks(MARK, out): if fm.group(1) == "footer" and is_footer_copy(fm.group(2), ctx.brand): kept.append(out[pos:fm.start()]) removed += [ln for ln in fm.group(2).splitlines() if ln.strip()] + had_footer = True pos = fm.end() out = "".join(kept) + out[pos:] out, old_footer = drop_old_footer(out, ctx.brand) removed += old_footer - if footermod.check(ctx.brand, out)[0] == "fail:missing": + if old_footer: + had_footer = True + if had_footer or footermod.check(ctx.brand, out)[0] == "fail:missing": out = out.rstrip("\n") + "\n\n" + footermod.block(ctx.brand) + "\n" return out, removed From d5c63caed0989f54445a387fd84883015ddbe552 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:56:10 -0400 Subject: [PATCH 08/44] docs: mark decision 18 footer URL amendment for #47 as a skill note Label the decision 18 logo-URL repair as a skill amendment pending user confirmation, move the repair paragraph out of the quoted decision text, and neutralize the line-5 note and file preamble. Co-Authored-By: Claude Haiku 5.5 --- references/decisions.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/references/decisions.md b/references/decisions.md index 89401d4..971fa4e 100644 --- a/references/decisions.md +++ b/references/decisions.md @@ -1,8 +1,8 @@ # User decisions this skill implements -Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. +Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, except decision 18, whose footer logo URLs were updated and which carries a clearly labelled skill note, both for #47 (see the note on line 5). These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. -Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. +Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. This repair is an amendment made by the skill, pending the user's confirmation; it is not the user's own wording. | Decision | Where the skill applies it | |---|---| @@ -65,8 +65,6 @@ Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and - a centred logo about 200 px wide, linking to https://www.complete.tech - under it, the line `Built by [CompleteTech LLC](https://www.complete.tech) · Innovation at Every Integration` - The logo URLs point to the published, unchanged masters bundled with this skill; the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The layout, credit, theme selection and artwork are unchanged. - Use `` so **dark mode shows the transparent dark logo (no white box)** and light mode shows the light logo. Exact markup: ```html

@@ -80,6 +78,9 @@ Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and Built by CompleteTech LLC · Innovation at Every Integration

``` + + **Skill note (#47, not part of the user decision):** The emitted logo URLs now come from `references/brand.yaml` and point to the published, unchanged masters bundled with this skill (`assets/brand/`); the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The exact-markup URLs above are therefore amended for #47 and await the user's confirmation. Layout, credit text, theme selection and artwork are unchanged. + - Scope: all in-scope repos, T2 families included ("all of them"). - Exceptions: watching-org forks (not the user's work), and T3 orgs until their co-owners sign off. - TouchedByAnAgent is included on this instruction. `STANDARD.md` §12.5 #4 had left it open; confirm on the review page. From 63f3a06354eba9f2d7c436cb33454490472abf3c Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:57:41 -0400 Subject: [PATCH 09/44] fix(footer): run footer check on push to main, offline PR check, G/H fenced-example and decisions provenance tests - footer-assets.yml: add push to main with the same paths filter; on pull_request check the configured footer logo paths exist in the PR checkout; the online HEAD runs only off pull_request. - tests: G/H regression for a fenced footer-like example plus a marked legacy footer; workflow trigger test. - validate_quality.py: gate_decisions_provenance (decisions.md line 3 not fully verbatim; skill note outside quote). Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/footer-assets.yml | 20 +++- scripts/validate_quality.py | 19 +++ tests/test_footer_fenced_example_migration.py | 110 ++++++++++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 tests/test_footer_fenced_example_migration.py diff --git a/.github/workflows/footer-assets.yml b/.github/workflows/footer-assets.yml index 51cd7af..5e74ddf 100644 --- a/.github/workflows/footer-assets.yml +++ b/.github/workflows/footer-assets.yml @@ -1,7 +1,10 @@ # Footer assets # Fails on any result other than pass, e.g. when a footer logo URL returns 404 (issue 47, subissue 50). -# Flag spelling confirmed with `python scripts/showcase.py footer --help`: `--check README.md --online` -# HEADs both decision 18 raw logo URLs and exits non-zero unless both return 200. +# Flag spelling confirmed with `python scripts/showcase.py footer --check --help`: `--check README.md --online` +# HEADs both decision 18 raw logo URLs (on main) and exits non-zero unless both return 200. +# Triggers: push to main (a regression is caught as it lands), weekly schedule and manual dispatch run the online HEAD. +# A pull request cannot be judged by HEADing main, so it runs an offline step instead: the configured footer logo +# paths must exist in the PR checkout under assets/brand/. name: Footer assets @@ -9,6 +12,14 @@ on: schedule: - cron: '17 6 * * 1' workflow_dispatch: + push: + branches: [main] + paths: + - references/brand.yaml + - README.md + - assets/brand/** + - scripts/ctshowcase/footer.py + - .github/workflows/footer-assets.yml pull_request: paths: - references/brand.yaml @@ -33,5 +44,10 @@ jobs: python-version: "3.12" - name: Install Python dependencies run: python -m pip install -r requirements.txt + - name: Footer logo files exist in this checkout + if: github.event_name == 'pull_request' + run: | + python -c "import pathlib, sys, yaml; from urllib.parse import urlparse; f = yaml.safe_load(open('references/brand.yaml', encoding='utf-8'))['footer']; urls = [f['logo_light_url'], f['logo_dark_url']]; bad = [u for u in urls if not pathlib.Path(*urlparse(u).path.split('/main/', 1)[1].split('/')).is_file()]; print('missing in checkout:', bad) if bad else print('footer logo files present:', urls); sys.exit(1 if bad else 0)" - name: Real HEAD against footer logo URLs + if: github.event_name != 'pull_request' run: python scripts/showcase.py footer --check README.md --online diff --git a/scripts/validate_quality.py b/scripts/validate_quality.py index e935101..ff6a0ec 100644 --- a/scripts/validate_quality.py +++ b/scripts/validate_quality.py @@ -268,6 +268,24 @@ def gate_changelog(root: Path = ROOT) -> None: print(f"changelog ok ({versions[0]})") +def gate_decisions_provenance(root: Path = ROOT) -> None: + """references/decisions.md must not claim to be fully verbatim, and the #47 skill note sits outside the quoted decision.""" + lines = (root / "references" / "decisions.md").read_text(encoding="utf-8").splitlines() + if len(lines) < 3 or "Verbatim excerpt" not in lines[2] or "except decision 18" not in lines[2]: + raise RuntimeError("references/decisions.md line 3 must say the excerpt is verbatim except decision 18 (#47)") + note = "**Skill note (#47, not part of the user decision):**" + note_lines = [i for i, ln in enumerate(lines) if note in ln] + if len(note_lines) != 1: + raise RuntimeError("references/decisions.md needs exactly one labelled '#47' skill note") + markup_lines = [i for i, ln in enumerate(lines) if "Exact markup:" in ln] + if not markup_lines or note_lines[0] < markup_lines[0]: + raise RuntimeError("the #47 skill note must follow the quoted decision's exact markup") + for i, ln in enumerate(lines): + if ("bundled with this skill" in ln or "returned 404" in ln) and i not in (4, note_lines[0]): + raise RuntimeError(f"references/decisions.md line {i + 1}: skill-authored #47 wording sits inside the quoted text") + print("decisions provenance ok") + + def gate_labels() -> None: import hashlib out = showcase("labels", "--emit-yml").stdout @@ -473,6 +491,7 @@ def main() -> int: gate_fonts() gate_package_extras() gate_changelog() + gate_decisions_provenance() gate_labels() gate_action() gate_privacy() diff --git a/tests/test_footer_fenced_example_migration.py b/tests/test_footer_fenced_example_migration.py new file mode 100644 index 0000000..68a0d1b --- /dev/null +++ b/tests/test_footer_fenced_example_migration.py @@ -0,0 +1,110 @@ +"""Issue 47 round 3: fenced footer-like examples, the footer workflow triggers and the decisions.md provenance gate. + +Synthetic fixtures only. +""" +import copy +import importlib.util + +import pytest +import yaml + +from conftest import ROOT, meta_dict +from ctshowcase import footer, readme +from ctshowcase.config import Brand +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +LEGACY = "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/" +PARENT = {"full_name": "someone/upstream", "default_branch": "main"} +VARIANTS = {"G": dict(fork=True, parent=PARENT), "H": dict(repo="host-backup-20260914")} +EXAMPLE = ( + "## Credits example\n\nA project may end with a credit like this:\n\n```md\n" + "Built by [CompleteTech LLC](https://www.complete.tech)\n```\n" +) + + +def legacy_brand(brand): + raw = copy.deepcopy(brand.raw) + raw["footer"].update(logo_light_url=LEGACY + "logo.png", logo_dark_url=LEGACY + "logo-dark.png") + return Brand(raw, brand.path) + + +def big_readme(brand, marked): + filler = "".join(f"Authored upstream paragraph {i} that stays exactly as written by its authors.\n\n" for i in range(130)) + old = legacy_brand(brand) + tail = footer.block(old) if marked else footer.markup(old) + text = "# Upstream project\n\n" + filler + EXAMPLE + "\n" + tail + "\n" + assert len(text.encode()) >= 10_000 + return text + + +@pytest.mark.parametrize("marked", [True, False], ids=["marked", "unmarked"]) +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_fenced_footer_example_does_not_stop_the_replacement_footer(brand, tmp_path, variant, marked): + ctx = RepoContext(brand, normalize(meta_dict(**VARIANTS[variant])), tmp_path) + assert ctx.det["variant"] == variant + before = big_readme(brand, marked) + after, removed = readme.upgrade(ctx, before) + assert LEGACY not in after + assert EXAMPLE in after, "the fenced example is preserved" + tail = after.split(EXAMPLE, 1)[1] + assert footer.markup(brand) in tail, "the real footer with the current brand URLs follows the example" + assert after.count(brand.raw["footer"]["logo_light_url"]) == 1 + assert after.count(brand.raw["footer"]["logo_dark_url"]) == 1 + assert removed, "the replaced footer lines are reported" + assert readme.preservation(before, after, removed) == [] + again, _ = readme.upgrade(RepoContext(brand, normalize(meta_dict(**VARIANTS[variant])), tmp_path), after) + assert again == after + + +def workflow(): + wf = yaml.safe_load((ROOT / ".github" / "workflows" / "footer-assets.yml").read_text(encoding="utf-8")) + return wf, wf.get("on", wf.get(True)) # PyYAML reads the bare key `on` as True + + +def test_workflow_runs_on_push_to_main_with_the_pull_request_path_filter(): + _, triggers = workflow() + assert triggers["push"]["branches"] == ["main"] + assert triggers["push"]["paths"] == triggers["pull_request"]["paths"] + assert "assets/brand/**" in triggers["push"]["paths"] + assert triggers["schedule"] and "workflow_dispatch" in triggers + + +def test_online_head_does_not_run_on_pull_requests_and_the_offline_check_does(): + wf, _ = workflow() + steps = [s for job in wf["jobs"].values() for s in job["steps"]] + online = [s for s in steps if "--online" in s.get("run", "")] + assert len(online) == 1 and online[0]["if"] == "github.event_name != 'pull_request'" + offline = [s for s in steps if "assets" not in s.get("run", "") and "brand.yaml" in s.get("run", "")] + assert len(offline) == 1 and offline[0]["if"] == "github.event_name == 'pull_request'" + # the offline step's command really passes against this checkout + import subprocess + import sys + done = subprocess.run([sys.executable, "-c", offline[0]["run"].split('python -c "', 1)[1].rsplit('"', 1)[0]], + cwd=ROOT, capture_output=True, text=True) + assert done.returncode == 0, done.stdout + done.stderr + + +def quality(): + spec = importlib.util.spec_from_file_location("validate_quality_decisions", ROOT / "scripts" / "validate_quality.py") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def test_decisions_md_does_not_claim_to_be_fully_verbatim(tmp_path): + q = quality() + q.gate_decisions_provenance(ROOT) + text = (ROOT / "references" / "decisions.md").read_text(encoding="utf-8") + assert "except decision 18" in text.splitlines()[2] + for name, mutate in ( + ("claims-verbatim", lambda t: t.replace(", except decision 18, whose footer logo URLs were updated", "", 1)), + ("note-in-quote", lambda t: t.replace("**Skill note (#47, not part of the user decision):**", + "The logo URLs point to the masters bundled with this skill; returned 404.\n\n" + " **Skill note (#47, not part of the user decision):**", 1)), + ): + dst = tmp_path / name / "references" + dst.mkdir(parents=True) + (dst / "decisions.md").write_text(mutate(text), encoding="utf-8") + with pytest.raises(RuntimeError, match="decisions.md"): + q.gate_decisions_provenance(tmp_path / name) From bc469548a5a68460a046c51366737b8b0f11d1e8 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:59:38 -0400 Subject: [PATCH 10/44] docs: drop unneeded confirmation clauses from the #47 skill notes Co-Authored-By: Claude Haiku 5.5 --- references/decisions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/references/decisions.md b/references/decisions.md index 971fa4e..e9b68fa 100644 --- a/references/decisions.md +++ b/references/decisions.md @@ -2,7 +2,7 @@ Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, except decision 18, whose footer logo URLs were updated and which carries a clearly labelled skill note, both for #47 (see the note on line 5). These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. -Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. This repair is an amendment made by the skill, pending the user's confirmation; it is not the user's own wording. +Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. This repair is an amendment made by the skill; it is not the user's own wording. | Decision | Where the skill applies it | |---|---| @@ -79,7 +79,7 @@ Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and

``` - **Skill note (#47, not part of the user decision):** The emitted logo URLs now come from `references/brand.yaml` and point to the published, unchanged masters bundled with this skill (`assets/brand/`); the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The exact-markup URLs above are therefore amended for #47 and await the user's confirmation. Layout, credit text, theme selection and artwork are unchanged. + **Skill note (#47, not part of the user decision):** The emitted logo URLs now come from `references/brand.yaml` and point to the published, unchanged masters bundled with this skill (`assets/brand/`); the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The exact-markup URLs above are therefore amended for #47. Layout, credit text, theme selection and artwork are unchanged. - Scope: all in-scope repos, T2 families included ("all of them"). - Exceptions: watching-org forks (not the user's work), and T3 orgs until their co-owners sign off. From dec8c1ee0f7b6c8dc51e5b680cb53573566929ac Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 15:59:49 -0400 Subject: [PATCH 11/44] ci: drop the brittle decisions provenance gate from validate_quality Remove gate_decisions_provenance and its call in main(). It hard-coded decisions.md line numbers and exact English sentences, so harmless document reorganisation would fail it. It sits outside the behavioural footer repair. Co-Authored-By: Claude Haiku 5.5 --- scripts/validate_quality.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/scripts/validate_quality.py b/scripts/validate_quality.py index ff6a0ec..e935101 100644 --- a/scripts/validate_quality.py +++ b/scripts/validate_quality.py @@ -268,24 +268,6 @@ def gate_changelog(root: Path = ROOT) -> None: print(f"changelog ok ({versions[0]})") -def gate_decisions_provenance(root: Path = ROOT) -> None: - """references/decisions.md must not claim to be fully verbatim, and the #47 skill note sits outside the quoted decision.""" - lines = (root / "references" / "decisions.md").read_text(encoding="utf-8").splitlines() - if len(lines) < 3 or "Verbatim excerpt" not in lines[2] or "except decision 18" not in lines[2]: - raise RuntimeError("references/decisions.md line 3 must say the excerpt is verbatim except decision 18 (#47)") - note = "**Skill note (#47, not part of the user decision):**" - note_lines = [i for i, ln in enumerate(lines) if note in ln] - if len(note_lines) != 1: - raise RuntimeError("references/decisions.md needs exactly one labelled '#47' skill note") - markup_lines = [i for i, ln in enumerate(lines) if "Exact markup:" in ln] - if not markup_lines or note_lines[0] < markup_lines[0]: - raise RuntimeError("the #47 skill note must follow the quoted decision's exact markup") - for i, ln in enumerate(lines): - if ("bundled with this skill" in ln or "returned 404" in ln) and i not in (4, note_lines[0]): - raise RuntimeError(f"references/decisions.md line {i + 1}: skill-authored #47 wording sits inside the quoted text") - print("decisions provenance ok") - - def gate_labels() -> None: import hashlib out = showcase("labels", "--emit-yml").stdout @@ -491,7 +473,6 @@ def main() -> int: gate_fonts() gate_package_extras() gate_changelog() - gate_decisions_provenance() gate_labels() gate_action() gate_privacy() From 631b6ea2dbf5c4160c4555e10719d952e1705cce Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 16:00:21 -0400 Subject: [PATCH 12/44] test: drop the decisions wording mirror test with its gate Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_fenced_example_migration.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/tests/test_footer_fenced_example_migration.py b/tests/test_footer_fenced_example_migration.py index 68a0d1b..1821e68 100644 --- a/tests/test_footer_fenced_example_migration.py +++ b/tests/test_footer_fenced_example_migration.py @@ -3,7 +3,6 @@ Synthetic fixtures only. """ import copy -import importlib.util import pytest import yaml @@ -83,28 +82,3 @@ def test_online_head_does_not_run_on_pull_requests_and_the_offline_check_does(): done = subprocess.run([sys.executable, "-c", offline[0]["run"].split('python -c "', 1)[1].rsplit('"', 1)[0]], cwd=ROOT, capture_output=True, text=True) assert done.returncode == 0, done.stdout + done.stderr - - -def quality(): - spec = importlib.util.spec_from_file_location("validate_quality_decisions", ROOT / "scripts" / "validate_quality.py") - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - -def test_decisions_md_does_not_claim_to_be_fully_verbatim(tmp_path): - q = quality() - q.gate_decisions_provenance(ROOT) - text = (ROOT / "references" / "decisions.md").read_text(encoding="utf-8") - assert "except decision 18" in text.splitlines()[2] - for name, mutate in ( - ("claims-verbatim", lambda t: t.replace(", except decision 18, whose footer logo URLs were updated", "", 1)), - ("note-in-quote", lambda t: t.replace("**Skill note (#47, not part of the user decision):**", - "The logo URLs point to the masters bundled with this skill; returned 404.\n\n" - " **Skill note (#47, not part of the user decision):**", 1)), - ): - dst = tmp_path / name / "references" - dst.mkdir(parents=True) - (dst / "decisions.md").write_text(mutate(text), encoding="utf-8") - with pytest.raises(RuntimeError, match="decisions.md"): - q.gate_decisions_provenance(tmp_path / name) From e314ac466f7784749b114b22acf8ddcf843b2e61 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 16:00:55 -0400 Subject: [PATCH 13/44] test: reword the fenced-example migration module docstring Co-Authored-By: Claude Sonnet 5.5 --- tests/test_footer_fenced_example_migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_footer_fenced_example_migration.py b/tests/test_footer_fenced_example_migration.py index 1821e68..e0fc743 100644 --- a/tests/test_footer_fenced_example_migration.py +++ b/tests/test_footer_fenced_example_migration.py @@ -1,4 +1,4 @@ -"""Issue 47 round 3: fenced footer-like examples, the footer workflow triggers and the decisions.md provenance gate. +"""Issue 47 round 3: fenced footer-like examples and the footer workflow triggers. Synthetic fixtures only. """ From 2b0e0b0723b96429a39ff750e20aae66003304b8 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 16:37:43 -0400 Subject: [PATCH 14/44] docs: point art-direction brand files at assets/brand masters The Brand files row named the former CompleteTech-LLC/.github/brand/ location, which returns 404. Name the published masters in assets/brand/ (the copies the generators and footer use), keep the former location as historical only, and note the matching sha256 for the logo pair. Co-Authored-By: Claude Haiku 5.5 --- references/art-direction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/references/art-direction.md b/references/art-direction.md index 70e1959..421e974 100644 --- a/references/art-direction.md +++ b/references/art-direction.md @@ -64,8 +64,8 @@ Starred values are approximate until the estate planner re-measures them and pub | README hero banner | `docs/images/banner.png` | 1280×360 (or 2560×720) | PNG ≤ 300 KB, opaque; generated by `banner` | | Repo card | `docs/images/card.svg` | 400×160 | SVG ≤ 40 KB; generated by `card` | | Repository icon | `assets/icon.png` | 1024×1024 | PNG RGBA, transparent (drawn to read on the family card colour, `#F8FAFC` for CompleteTech) or with one opaque square ground (it then fills the card, full-bleed), ≤ 300 KB, no text | -| Brand files | `CompleteTech-LLC/.github/brand/`: `logo.png`, `logo-dark.png` (the exemplar pair, byte-identical), the chosen dark master (decision 19), `mark-dark.png` and `bulb-dark.png` once re-measured | as committed | PNG RGBA | -| Logo pair in Skills repositories | `assets/logo.png`, `assets/logo-dark.png` | byte-identical to the brand files | sha256 `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` (logo.png, 243,500 B) and `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` (logo-dark.png, 210,785 B) | +| Brand files | `assets/brand/` in this repository (the published masters the generators and the footer use): `logo.png`, `logo-dark.png` (the exemplar pair), `logo-dark-full.png` (the candidate dark master, decision 19) and `mark-light.png`. `mark-dark.png` and `bulb-dark.png` are not shipped yet (see `assets/brand/README.md`). The former `CompleteTech-LLC/.github/brand/` location is historical only: those URLs returned 404, and the footer serves the files from `assets/brand/` (same sha256). | as committed | PNG; see `assets/brand/README.md` for modes | +| Logo pair in Skills repositories | `assets/logo.png`, `assets/logo-dark.png` | byte-identical to the `assets/brand/` masters (same sha256) | sha256 `a5c3c4ef55336ed4b883d3bc3bb6e1e942b836469bd2b2933bc8cc230b321621` (logo.png, 243,500 B) and `0dc00c0e5159518ff1d552b7052293b4e545ddd35be489e6d114e3ea64db27ec` (logo-dark.png, 210,785 B) | | Social preview | `assets/social-preview.png` | 1280×640 | PNG ≤ 900 KB (heavier: JPG q85); hard limit under 1 MB; uploaded in the browser | | Org avatar | `.github/profile/assets/avatar-500.png` (+ `avatar-1024.png` master) | 500×500 | Opaque PNG ≤ 300 KB; uploaded in the browser | | Org banner | `.github/profile/assets/banner.jpg` | 1600×400 | JPG q85 ≤ 400 KB | From c017dd14e3e318e6ac700b3653f8eaeb17e05417 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 16:38:02 -0400 Subject: [PATCH 15/44] docs(decisions): neutral skill-maintenance wording for footer URL repair (#47) Restore the decision 18 exact markup to the origin/main text and keep the current footer URLs only in the labelled skill note. Remove the requester, date and confirmation wording the repair notes carried. Co-Authored-By: Claude Haiku 5.5 --- references/decisions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/references/decisions.md b/references/decisions.md index e9b68fa..832b66a 100644 --- a/references/decisions.md +++ b/references/decisions.md @@ -1,8 +1,8 @@ # User decisions this skill implements -Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, except decision 18, whose footer logo URLs were updated and which carries a clearly labelled skill note, both for #47 (see the note on line 5). These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. +Verbatim excerpt from `decisions/USER_DECISIONS.md` (Timothy Wayne Gregg, 2026-10-25) in the estate hand-off bundle, except that the skill note under decision 18 is skill maintenance and not part of the user decision. These decisions outrank the estate standard `ct-std-v1` (`STANDARD.md`), the art direction and every audit proposal. Where this skill's references paraphrase the standard, the decisions below have already been applied. -Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and artwork unchanged. This repair is an amendment made by the skill; it is not the user's own wording. +Skill maintenance (#47): the footer logo URLs configured in `references/brand.yaml` were repaired because the former URLs returned 404. Layout, credit and artwork are unchanged. The user decisions below are unmodified. | Decision | Where the skill applies it | |---|---| @@ -70,8 +70,8 @@ Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and

- - CompleteTech LLC + + CompleteTech LLC
@@ -79,7 +79,7 @@ Footer logo URLs repaired for #47 (former URLs returned 404); layout, credit and

``` - **Skill note (#47, not part of the user decision):** The emitted logo URLs now come from `references/brand.yaml` and point to the published, unchanged masters bundled with this skill (`assets/brand/`); the former `CompleteTech-LLC/.github/main/brand/` URLs returned 404. The exact-markup URLs above are therefore amended for #47. Layout, credit text, theme selection and artwork are unchanged. + **Skill note (#47, not part of the user decision):** The footer URLs this skill currently emits are configured in `references/brand.yaml` (`footer.logo_light_url` and `footer.logo_dark_url`) and point to this repository's published masters: https://raw.githubusercontent.com/CompleteTech-Skills/github-showcase-skill/main/assets/brand/logo.png and .../logo-dark.png (same sha256 as listed in `assets/brand/README.md`). The former `CompleteTech-LLC/.github/main/brand/` URLs shown in the decision above returned 404. Layout, credit text, theme selection and artwork are unchanged. - Scope: all in-scope repos, T2 families included ("all of them"). - Exceptions: watching-org forks (not the user's work), and T3 orgs until their co-owners sign off. From e58465e0a78aed0a8d72892bad5409c904d8d9a6 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 18:26:31 -0400 Subject: [PATCH 16/44] fix(readme): keep alt-vouching for unmarked footers in upgrade, recognize picture local logos, report only changed footer lines as removed (#47) Co-Authored-By: Claude Sonnet 5.5 --- scripts/ctshowcase/readme.py | 43 ++++++++++++++----- tests/test_footer_r6_regressions.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 tests/test_footer_r6_regressions.py diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 899cca1..7204ef6 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -579,16 +579,26 @@ def _footer_logo_url(url: str, brand) -> bool: return False -def is_footer_copy(block: str, brand) -> bool: +def is_footer_copy(block: str, brand, loose_src: bool = False) -> bool: """Every line of `block` is a line of the decision 18 footer or a drifted copy of one: only centring, link, logo, `
`/`` markup, with complete.tech links, the footer's own logo image (its src or srcset is the brand footer logo URL or a drifted form of it; an image without src/srcset passes on an alt that is exactly the company) and as text only the credit line ("Built by ", optionally with the suffix). The former local logo files (assets/logo.png and the other `OLD_LOCAL_LOGOS`) are also recognized when the image's alt is exactly the company or the footer alt. A sponsor or partner image (whatever its file name), a thanks line or - any other authored text makes it not a footer copy, so it is never removed.""" + any other authored text makes it not a footer copy, so it is never removed. `loose_src=True` (only `upgrade()`, for + an unmarked trailing block that also passes `is_footer_like`) keeps the original rule that an alt exactly the + company vouches for an image whatever its src; the default is strict.""" import html f, company = brand.raw["footer"], brand.strings["company"] credit = {f"{f['line_prefix']} {company}", f"{f['line_prefix']} {company} {f['line_suffix']}"} + def _alts(attrs: str) -> list[str]: + return [html.unescape(next(g for g in a.groups()[1:] if g is not None)).strip() + for a in FOOTER_ATTR.finditer(attrs) if a.group(1).lower() == "alt"] + + # a carries no alt: the block's own alt vouches for its former local logo files + block_alt_ok = any(v in (company, f["alt"]) for m in FOOTER_TAG.finditer(block) + if m.group(1).lower() == "img" and not m.group(0).startswith(" bool: name, attrs = m.group(1).lower(), m.group(2) if m.group(0).startswith(" bool: alt_ok = alt_ok or val in (company, f["alt"]) else: urls.append([p.split()[0] for p in val.split(",") if p.split()]) - if urls: # a matching alt never vouches for another image: every src/srcset must be a footer logo URL + if name == "source": + alt_ok = block_alt_ok + if urls: # a matching alt never vouches for another image (unless loose_src): every src/srcset must be a footer logo URL + if loose_src and alt_ok and name == "img": + return True return all(parts and all(_footer_logo_url(u, brand) or (alt_ok and _old_local_logo(u)) for u in parts) for parts in urls) return alt_ok @@ -619,18 +633,18 @@ def tag_ok(m: re.Match) -> bool: return True -def drop_old_footer(text: str, brand) -> tuple[str, list[str]]: +def drop_old_footer(text: str, brand, loose_src: bool = False) -> tuple[str, list[str]]: """(text, removed lines) with a trailing old footer (an unmarked or drifted decision 18 footer: every line a footer line, `is_footer_copy`) taken off the end, so the marked footer replaces it instead of following it. Any other block, a centred credits block with authored lines included, is left as it is (lint reports a second - footer-like block as `fail:duplicate`).""" + footer-like block as `fail:duplicate`). `loose_src` is passed through to `is_footer_copy` (strict by default).""" body = text.rstrip() opens = [m for m in CENTRED_OPEN.finditer(masked(body))] if not opens: return text, [] m = opens[-1] tail = body[m.start():] - if not tail.rstrip().endswith(f"") or not is_footer_like(tail) or not is_footer_copy(tail, brand): + if not tail.rstrip().endswith(f"") or not is_footer_like(tail) or not is_footer_copy(tail, brand, loose_src): return text, [] return body[:m.start()].rstrip() + "\n", [ln for ln in tail.splitlines() if ln.strip()] @@ -646,11 +660,18 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: want_footer = footer_wanted(ctx) removed: list[str] = [] if want_footer and blocks.get("footer") and is_footer_copy(blocks["footer"], ctx.brand): - # A configured publication-path change replaces only our recognized footer, - # not authored credits or partner images inside a similarly named block. - removed += [ln for ln in blocks["footer"].splitlines() if ln.strip()] + # A configured publication-path change replaces only our recognized footer, not authored credits or partner + # images inside a similarly named block. Only the lines the emitted footer does not carry again are reported: + # a compliant footer is re-emitted unchanged and must not weaken the never-drop-a-line guard. + kept = Counter(ln.strip() for ln in footermod.block(ctx.brand).splitlines() if ln.strip()) + for ln in blocks["footer"].splitlines(): + if ln.strip(): + if kept[ln.strip()] > 0: + kept[ln.strip()] -= 1 + else: + removed.append(ln) if want_footer: # an unmarked or drifted footer at the end is replaced by the marked one, never kept beside it - body, old_footer = drop_old_footer(body, ctx.brand) + body, old_footer = drop_old_footer(body, ctx.brand, loose_src=True) removed += old_footer # preserve list / existing third-party banner preserve = ctx.repo in (ctx.org.get("readme_preserve") or []) or len(text.encode()) > 15000 @@ -670,7 +691,7 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: removed += [ln for ln in old_tag.splitlines() if ln.strip()] body, _ = _strip_markers(text, keep={"tagline": tag}) if want_footer: - body, _ = drop_old_footer(body, ctx.brand) # its lines are already in `removed` + body, _ = drop_old_footer(body, ctx.brand, loose_src=True) # its lines are already in `removed` pre, sections = split_sections(body) lines = pre.splitlines() # an unmarked bold line opening the preamble is an old tagline. With a description it is replaced; without one it diff --git a/tests/test_footer_r6_regressions.py b/tests/test_footer_r6_regressions.py new file mode 100644 index 0000000..1485bf3 --- /dev/null +++ b/tests/test_footer_r6_regressions.py @@ -0,0 +1,65 @@ +"""Round r6 footer findings: unmarked old footers, local logos, and the never-drop-a-line guard.""" +import pytest + +from conftest import meta_dict +from ctshowcase import footer, readme +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +CREDIT = ('Built by CompleteTech LLC · Innovation at Every Integration' + "") + + +def unmarked(src, alt="CompleteTech LLC"): + return ('

%s
%s

' + % (src, alt, CREDIT)) + + +def picture(light, dark): + return ('

\n \n' + ' \n' + ' CompleteTech LLC\n
\n %s\n

\n' + % (dark, light, CREDIT)) + + +@pytest.mark.parametrize("src", ["https://www.complete.tech/wp-content/uploads/logo.png", "docs/images/logo.svg"]) +def test_unmarked_old_footer_with_company_alt_is_replaced_by_upgrade(brand, tmp_path, src): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), unmarked(src)) + assert src in before and "showcase:footer" not in before + after, removed = readme.upgrade(ctx, before) + assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" + assert readme.preservation(before, after, removed) == [] + + +def test_strict_default_keeps_a_company_alt_partner_image(brand): + text = "# A\n\n" + unmarked("docs/images/logo.svg") + assert readme.drop_old_footer(text, brand) == (text, []) + assert readme.drop_old_footer(text, brand, loose_src=True)[1] + + +def test_picture_footer_with_old_local_files_is_replaced(brand, tmp_path): + block = picture("assets/logo.png", "assets/logo-dark.png") + assert readme.is_footer_copy(block, brand) + assert not readme.is_footer_copy(picture("assets/partner.png", "assets/logo-dark.png"), brand) + assert not readme.is_footer_copy(block.replace('alt="CompleteTech LLC"', 'alt="Partner"'), brand) + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), block) + after, removed = readme.upgrade(ctx, before) + assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" + assert readme.preservation(before, after, removed) == [] + + +AUTHORED = '

\nDiagram\n
\n

\n\n' + + +def test_compliant_footer_adds_no_removed_lines_and_a_lost_br_still_fails(brand, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + scaffold = readme.scaffold(ctx) + before = scaffold.replace(footer.block(brand), AUTHORED + footer.block(brand)) + after, removed = readme.upgrade(ctx, before) + footer_lines = {ln.strip() for ln in footer.markup(brand).splitlines() if ln.strip()} + assert not [ln for ln in removed if ln.strip() in footer_lines] # header lines may be reported, footer lines not + lost = after.replace("
\n

\n\n` or template-body block shown inside fenced or inline code is an example, never regenerated); blank lines are tidied only inside regenerated blocks and where a block meets authored text (a removed block leaves one blank line). With no description the tagline block is kept as written (logged `needs-input:description`), except an inherited tagline, which is emptied: the block holds exactly a tagline of the template the repository was generated from (`meta` `template_taglines`: the template README's tagline block, the first one outside code, when the repository was generated and today), or, only when those are unknown, the template body is still pristine; never in the template repository itself (R2: the tagline is the description, never another repository's line), and the paragraph after `` (and its heading) is rewritten from the license table whenever it can be derived | | `--bootstrap` | Template only. The block between `` and `:end -->` starts with ``, the hash of the block's generated text. While the block still matches its hash byte for byte, bootstrap replaces it with the scaffolded variant body for this repository (each section holds only an HTML comment naming the facts it needs) under a new hash, rebuilds the header for the variant (Variant F: no H1; a CVE slug titles as `Acme CVE-2026-12345 Check`; the tagline block carries over and follows the `header` rule, so with no description a tagline written for the repository stays and only an inherited one is emptied), writes the license section from the table above (with or without a `LICENSE` file; a private repository reads "Proprietary...", a public Security repository gets `## Licensing`), and rewrites the owner tokens in `CONTRIBUTING.md` (only links to the template owner's `.github`; a link to another org's or an upstream's contributing guide or security policy stays as written). Because the hash still matches, a later run re-bootstraps when the type changes (the code landed, or `type:` was set in `.github/showcase.yml`). Once a person edits the block, the hash no longer matches: the marker is dropped (`bootstrap:edited`), the README goes through `upgrade` with the never-drop-a-line check, and the block is never rewritten again. A no-op when the markers are absent. | diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 1e1bd1d..d8a80ff 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -531,10 +531,20 @@ def preserved(ctx: RepoContext, text: str) -> bool: CENTRED_OPEN = re.compile(r'(?m)^[ \t]*<(p|div) align="center">') -def is_footer_like(block: str) -> bool: - """A centred block that credits the company with a complete.tech link: the decision 18 footer, exact or drifted.""" - return bool(re.match(r'\s*<(p|div) align="center">', block)) and "built by" in block.lower() \ - and "complete.tech" in block.lower() +def is_footer_like(block: str, brand=None) -> bool: + """A centred block that credits the company with a complete.tech link (or, given `brand`, a link to the configured + footer host): the decision 18 footer, exact or drifted. A cheap pre-filter; `is_footer_copy` decides what is removed.""" + low = block.lower() + hosts = ["complete.tech"] + if brand is not None: + from urllib.parse import urlsplit + try: + h = (urlsplit(brand.raw["footer"]["href"]).hostname or "").lower() + except (KeyError, TypeError, ValueError): + h = "" + if h: + hosts.append(h[4:] if h.startswith("www.") else h) + return bool(re.match(r'\s*<(p|div) align="center">', block)) and "built by" in low and any(h in low for h in hosts) FOOTER_TAG = re.compile(r"]*)>", re.I) @@ -703,7 +713,7 @@ def drop_old_footer(text: str, brand, loose_src: bool = False, own_site: bool = return text, [] m = opens[-1] tail = body[m.start():] - if not tail.rstrip().endswith(f"") or not is_footer_like(tail) or not is_footer_copy(tail, brand, loose_src, own_site): + if not tail.rstrip().endswith(f"") or not is_footer_like(tail, brand) or not is_footer_copy(tail, brand, loose_src, own_site): return text, [] return body[:m.start()].rstrip() + "\n", [ln for ln in tail.splitlines() if ln.strip()] diff --git a/tests/test_footer_foreign_anchor.py b/tests/test_footer_foreign_anchor.py new file mode 100644 index 0000000..8188e68 --- /dev/null +++ b/tests/test_footer_foreign_anchor.py @@ -0,0 +1,107 @@ +"""Footer anchors must target the configured footer host (issue 47, t47-adjudication-footer-anchor-97b.md). + +`is_footer_copy` once accepted any `` whose attributes contained the substring complete.tech, so a foreign link such +as https://partner.example.test/?ref=complete.tech was treated as footer copy, exempted from the never-drop-a-line +check by `upgrade()` and silently deleted. Only a link whose href host equals the host of the brand `footer.href` (no +suffix, userinfo, port or attribute-substring match) is footer copy. The real brand href stays a valid control. +""" +import json + +import pytest + +from conftest import EX, meta_dict, run_cli +from ctshowcase import footer, readme +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +REAL = "https://www.complete.tech" +FOREIGN_ANCHORS = [ + 'href="https://partner.example.test/?ref=complete.tech"', + 'href="https://complete.tech.evil.test/"', + 'href="https://notcomplete.tech/"', + 'href="https://www.complete.tech.evil.test/x"', + 'href="https://partner.test/#complete.tech"', + 'href="https://partner.test/" title="complete.tech"', + 'title="https://www.complete.tech" href="https://partner.test/"', + 'href="https://www.complete.tech@evil.test/"', + 'href="https://user@www.complete.tech/"', + 'href="https://www.complete.tech:8443/"', + 'href="ftp://www.complete.tech/"', + 'data-x="href=https://www.complete.tech" href="https://partner.test/"', + 'data-x="a" href="https://partner.test/?ref=complete.tech"', +] +REAL_ANCHORS = [ + 'href="https://www.complete.tech"', + 'href="https://www.complete.tech/"', + 'href="https://COMPLETE.TECH/about?x=1#y"', + "href='https://Complete.Tech'", + "href=https://www.complete.tech/", + 'class="x" href="https://complete.tech/"', +] + + +def block(attrs): + return ('

Built by CompleteTech LLC · Innovation at Every Integration

' + % attrs) + + +def marked_with(brand, attrs): + """The current marked footer with its first anchor replaced (so the marker block holds the foreign link).""" + cur = footer.block(brand) + assert 'href="%s"' % REAL in cur + return cur.replace('href="%s"' % REAL, attrs, 1) + + +@pytest.mark.parametrize("attrs", FOREIGN_ANCHORS) +def test_foreign_anchor_is_not_footer_copy(brand, attrs): + assert not readme.is_footer_copy(block(attrs), brand) + + +@pytest.mark.parametrize("attrs", REAL_ANCHORS) +def test_configured_host_anchor_is_footer_copy(brand, attrs): + assert readme.is_footer_copy(block(attrs), brand) + + +@pytest.mark.parametrize("attrs", FOREIGN_ANCHORS) +def test_upgrade_and_preservation_keep_foreign_anchor_in_unmarked_footer(brand, tmp_path, attrs): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), block(attrs)) + assert attrs in before + after, removed = readme.upgrade(ctx, before) + assert attrs in after + assert not [ln for ln in removed if attrs in ln] + assert readme.preservation(before, after, removed) == [] + + +@pytest.mark.parametrize("attrs", FOREIGN_ANCHORS[:3]) +def test_foreign_anchor_in_a_marked_footer_is_not_exempted_from_preservation(brand, tmp_path, attrs): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), marked_with(brand, attrs)) + assert attrs in before + after, removed = readme.upgrade(ctx, before) + assert not [ln for ln in removed if attrs in ln], removed + if attrs not in after: # the replacement is only allowed when preservation then refuses it + assert readme.preservation(before, after, removed), "foreign link dropped without a readme-shrink" + + +def test_cli_rejects_marked_footer_with_a_foreign_anchor_instead_of_dropping_it(brand, tmp_path): + attrs = 'href="https://partner.example.test/?ref=complete.tech"' + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + text = readme.scaffold(ctx).replace(footer.block(brand), marked_with(brand, attrs)) + repo = tmp_path / "repo" + repo.mkdir() + (repo / "README.md").write_text(text, encoding="utf-8", newline="\n") + p = run_cli("--meta", EX / "meta" / "northwind-support-triage-agent.json", "--repo-dir", repo, "--format", "json", + "readme", "--mode", "upgrade") + out = json.loads(p.stdout) + assert p.returncode != 0 and "readme-shrink" in out["logs"], (p.returncode, out) + assert (repo / "README.md").read_text(encoding="utf-8") == text + + +def test_real_brand_anchor_in_a_drifted_footer_is_still_replaced(brand, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + old = block('href="https://complete.tech"') + before = readme.scaffold(ctx).replace(footer.block(brand), old) + after, removed = readme.upgrade(ctx, before) + assert footer.block(brand) in after + assert readme.preservation(before, after, removed) == [] diff --git a/tests/test_footer_upgrade_preservation.py b/tests/test_footer_upgrade_preservation.py index b7f2799..5463e03 100644 --- a/tests/test_footer_upgrade_preservation.py +++ b/tests/test_footer_upgrade_preservation.py @@ -43,9 +43,9 @@ ] -def unmarked(src, alt="CompleteTech LLC"): - return ('

%s
%s

' - % (src, alt, CREDIT)) +def unmarked(src, alt="CompleteTech LLC", href=OWN_SITE_HOST): + return ('

%s
%s

' + % (href, src, alt, CREDIT.replace(OWN_SITE_HOST, href))) def image_lines(text, src): @@ -94,7 +94,7 @@ def test_own_site_host_is_derived_from_brand_data(brand, tmp_path): derived = Brand(raw, brand.path) ctx = RepoContext(derived, normalize(meta_dict()), tmp_path) own = DERIVED_HOST + "/wp-content/uploads/logo.png" - before = readme.scaffold(ctx).replace(footer.block(derived), unmarked(own)) + before = readme.scaffold(ctx).replace(footer.block(derived), unmarked(own, href=DERIVED_HOST)) assert own in before after, removed = readme.upgrade(ctx, before) assert after.count("Built by") == 1 and own not in after From a099be58b23c881c025d3ed185cfd3fc68ef98ad Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 19:14:11 -0400 Subject: [PATCH 21/44] fix(readme): report only removed footer lines the new footer does not re-emit callout_only() reported every removed footer line to the preservation check, including lines the replacement footer emits again. That let unrelated content loss (for example a banner wrapper link) pass the G/H check. Report only the multiset difference between removed footer lines and the replacement footer. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index d8a80ff..013e1e0 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -469,8 +469,9 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: exempts them). Review q27 "Full CompleteTech treatment" (brand.yaml readme.fork_readme: full) means the fork follows its §5.4 variant instead: pass that variant with `readme --variant`. Returns (text, removed lines): a recognized old footer (marked or unmarked, `is_footer_copy`: for example one with - the former logo URLs) is replaced by the current one, and its lines are reported so the preservation check accepts - them. A marked footer that is not a footer copy (a partner image) is left as it is. A removed recognized footer is + the former logo URLs) is replaced by the current one, and only the removed footer lines that the replacement footer + does not carry again are reported (multiset difference), so the preservation check accepts them. A re-emitted + line is not reported, so it cannot weaken the never-drop-a-line guard. A marked footer that is not a footer copy (a partner image) is left as it is. A removed recognized footer is always replaced, even when fenced example text makes check() report drift.""" if ctx.det["variant"] == "G" and ctx.brand.readme_opts.get("fork_readme") == "full": ctx.log("needs-input:fork-variant (q27 full treatment: pass readme --variant for the §5.4 variant)") @@ -512,6 +513,16 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: had_footer = True if had_footer or footermod.check(ctx.brand, out)[0] == "fail:missing": out = out.rstrip("\n") + "\n\n" + footermod.block(ctx.brand) + "\n" + # Report only the removed footer lines that the replacement footer does not carry again (multiset + # difference), so a re-emitted line never counts as lost content in the preservation check. + kept = Counter(ln.strip() for ln in footermod.block(ctx.brand).splitlines() if ln.strip()) + rest = [] + for ln in removed: + if kept[ln.strip()] > 0: + kept[ln.strip()] -= 1 + else: + rest.append(ln) + removed = rest return out, removed From c38612a30b3872307be0b866fe4e9c562d43b478 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 19:17:45 -0400 Subject: [PATCH 22/44] test(readme): pin G/H footer re-emission and banner wrapper loss (#47) Append regressions for the fork/backup footer migration (P2 finding): - re-emitted footer lines must not be reported as removed (6 failing on 90298f1, reproduces the exemption bug) - a banner wrapper lost beside a legacy footer must be counted as lost by preservation - the CLI readme --write path refuses that loss and leaves README.md unchanged Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_legacy_gh.py | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_footer_legacy_gh.py b/tests/test_footer_legacy_gh.py index 9a47096..1ac29da 100644 --- a/tests/test_footer_legacy_gh.py +++ b/tests/test_footer_legacy_gh.py @@ -144,3 +144,57 @@ def test_non_footer_images_are_not_footer_copies_and_are_left(brand, block): assert not readme.is_footer_copy(block, brand) text = "# A\n\n" + block assert readme.drop_old_footer(text, brand) == (text, []) + + +@pytest.mark.parametrize("marked", [True, False], ids=["marked", "unmarked"]) +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_reemitted_footer_lines_are_not_reported_as_removed(brand, tmp_path, variant, marked): + from collections import Counter + + ctx = ctx_for(brand, variant, tmp_path) + before = legacy_readme(brand, marked) + after, removed = readme.upgrade(ctx, before) + assert removed, "the replaced footer lines are reported" + kept = Counter(ln.strip() for ln in footer.block(brand).splitlines() if ln.strip()) + for ln in removed: + # a line the replacement footer carries again is not a removal, so it must not be exempted from the guard + assert kept[ln.strip()] == 0, f"re-emitted footer line reported as removed: {ln.strip()}" + assert '

' not in [ln.strip() for ln in removed] + assert "

" not in [ln.strip() for ln in removed] + assert readme.preservation(before, after, removed) == [] + + +BANNER_BODY = '\nbanner\n' + + +def banner_wrapped_legacy_readme(brand): + banner = f"{readme.start('banner')}\n{BANNER_BODY}\n{readme.end('banner')}" + filler = "Authored paragraph that stays exactly as written in the upstream project.\n\n" * 40 + return banner + "\n\n" + UPSTREAM + "\n" + filler + footer.block(legacy_brand(brand)) + "\n" + + +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_banner_wrapper_loss_beside_legacy_footer_is_refused(brand, tmp_path, variant): + ctx = ctx_for(brand, variant, tmp_path) + before = banner_wrapped_legacy_readme(brand) + after, removed = readme.upgrade(ctx, before) + problems = readme.preservation(before, after, removed) + assert problems, "the banner loss must not be accepted" + # the footer also carries a standalone , so the banner's own wrapper line must still be counted as lost + assert "lost line: " in problems, problems + + +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_cli_refuses_banner_wrapper_loss_on_fork_footer_migration(brand, tmp_path, variant): + import json + + from conftest import run_cli + + meta = tmp_path / "meta.json" + meta.write_text(json.dumps(meta_dict(**VARIANTS[variant])), encoding="utf-8") + readme_path = tmp_path / "README.md" + before = banner_wrapped_legacy_readme(brand) + readme_path.write_text(before, encoding="utf-8", newline="") + p = run_cli("--meta", meta, "--repo-dir", tmp_path, "readme", "--write") + assert p.returncode != 0 and "readme-shrink" in p.stdout + p.stderr, p.stdout + p.stderr + assert readme_path.read_bytes() == before.encode("utf-8"), "a refused write must leave README.md unchanged" From 4823d1036cdd3a0ba7f6e064c212c3cf2f07a95b Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 19:18:52 -0400 Subject: [PATCH 23/44] docs(readme): wrap callout_only docstring (#47) Co-Authored-By: Claude Sonnet 5.5 --- scripts/ctshowcase/readme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 013e1e0..57ab7c5 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -471,8 +471,8 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: Returns (text, removed lines): a recognized old footer (marked or unmarked, `is_footer_copy`: for example one with the former logo URLs) is replaced by the current one, and only the removed footer lines that the replacement footer does not carry again are reported (multiset difference), so the preservation check accepts them. A re-emitted - line is not reported, so it cannot weaken the never-drop-a-line guard. A marked footer that is not a footer copy (a partner image) is left as it is. A removed recognized footer is - always replaced, even when fenced example text makes check() report drift.""" + line is not reported, so it cannot weaken the never-drop-a-line guard. A marked footer that is not a footer copy + (a partner image) is left as it is. A removed recognized footer is always replaced, even when fenced example text makes check() report drift.""" if ctx.det["variant"] == "G" and ctx.brand.readme_opts.get("fork_readme") == "full": ctx.log("needs-input:fork-variant (q27 full treatment: pass readme --variant for the §5.4 variant)") body = text From 70e840bd63ffb4d06689517b4839d30446000426 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 19:35:23 -0400 Subject: [PATCH 24/44] test(footer): pin whole-attribute footer image matching (#47) Add tests/test_footer_attr_tokens.py reproducing the P2 finding: data-alt, xalt and title substrings must not authorize a footer image, the first alt attribute wins, _footer_attrs keeps only exact alt/src/srcset names, and a marked footer whose partner image carries data-alt="" is kept through the scaffold, G/H callout and upgrade paths. Against the pre-fix readme.py the data-alt, first-alt, _footer_attrs and marked-footer cases fail; the xalt, title and data-src cases already pass. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_attr_tokens.py | 138 +++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/test_footer_attr_tokens.py diff --git a/tests/test_footer_attr_tokens.py b/tests/test_footer_attr_tokens.py new file mode 100644 index 0000000..0975ff8 --- /dev/null +++ b/tests/test_footer_attr_tokens.py @@ -0,0 +1,138 @@ +"""Footer image attributes are matched as whole attribute names (issue 47, source finding P2 at 4823d10). + +`is_footer_copy` used to read `data-alt=""` as an `alt` attribute, so a partner image with a real +`alt="Partner"` could be taken for a footer copy and removed by the G/H callout path or `upgrade()`. Only exact +`alt`, `src` and `srcset` attribute names count, and the first occurrence of a name is the one a browser reads. + +Synthetic data only: the brand is the shared `brand` fixture (conftest), the partner alt is "Partner" and the local +partner-style source is `assets/logo.png`. The tests mirror the scaffold, upgrade and preservation pattern of +tests/test_footer_upgrade_preservation.py. +""" +import pytest + +from conftest import meta_dict +from ctshowcase import footer, readme +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +PARTNER = "Partner" +LOCAL = "assets/logo.png" +UPSTREAM = "# Upstream project\n\nAuthored upstream text, kept as written.\n\n## Usage\n\nRun it.\n" + + +def credit(brand): + f = brand.raw["footer"] + return f"{f['line_prefix']} {brand.strings['company']} {f['line_suffix']}" + + +def block_with(brand, attrs): + return '

\n\n
\n%s\n

' % (attrs, credit(brand)) + + +def ctx_for(brand, tmp_path, **kw): + return RepoContext(brand, normalize(meta_dict(**kw)), tmp_path) + + +def image_lines(text, src): + return sum(1 for ln in text.splitlines() if src in ln) + + +# (a) attribute names other than alt, src and srcset never authorize a footer image + + +@pytest.mark.parametrize("attrs", [ + 'src="assets/logo.png" alt="Partner" data-alt="CompleteTech LLC"', + 'src="assets/logo.png" alt="Partner" xalt="CompleteTech LLC"', + 'src="assets/logo.png" alt="Partner" title="alt=CompleteTech LLC"', + 'src="assets/logo.png" data-alt="CompleteTech LLC"', + 'data-src="{logo}" src="assets/partner.png" alt="CompleteTech LLC"', +], ids=["data-alt", "xalt", "title-alt", "data-alt-no-alt", "data-src-partner-file"]) +def test_non_alt_attribute_names_do_not_authorize(brand, attrs): + attrs = attrs.replace("{logo}", brand.raw["footer"]["logo_light_url"]) + assert readme.is_footer_copy(block_with(brand, attrs), brand) is False + + +# (b) first-attribute semantics: the first alt is the one that counts + + +def test_first_alt_partner_wins_over_later_company_alt(brand): + block = block_with(brand, f'src="{LOCAL}" alt="{PARTNER}" alt="{brand.strings["company"]}"') + assert readme.is_footer_copy(block, brand) is False + + +def test_first_alt_company_wins_over_later_partner_alt(brand): + block = block_with(brand, f'src="{LOCAL}" alt="{brand.strings["company"]}" alt="{PARTNER}"') + assert readme.is_footer_copy(block, brand) is True + + +# (c) positive controls, the company alt and the footer's own logo keep working + + +def test_company_alt_on_former_local_logo_is_footer_copy(brand): + block = block_with(brand, f'src="{LOCAL}" alt="{brand.strings["company"]}"') + assert readme.is_footer_copy(block, brand) is True + + +def test_footer_logo_src_with_partner_alt_is_still_footer_copy(brand): + # existing behaviour: a src that is the brand footer logo URL is accepted whatever the alt says + block = block_with(brand, f'src="{brand.raw["footer"]["logo_light_url"]}" alt="{PARTNER}"') + assert readme.is_footer_copy(block, brand) is True + + +# (d) _footer_attrs: exact names only, first occurrence, quotes stripped, never raises + + +def test_footer_attrs_keeps_exact_names_first_occurrence_quotes_stripped(): + attrs = ('data-alt="CompleteTech LLC" xalt="CompleteTech LLC" title="alt=CompleteTech LLC" data-src="x.png" ' + "src='assets/logo.png' alt=\"Partner\" alt=\"CompleteTech LLC\" srcset=assets/logo-2x.png 2x") + got = readme._footer_attrs(attrs) + assert isinstance(got, dict) + assert set(got) <= {"alt", "src", "srcset"} + assert got["alt"] == PARTNER # first occurrence, quotes stripped + assert got["src"] == LOCAL + assert got["srcset"] == "assets/logo-2x.png" + + +@pytest.mark.parametrize("attrs", ['alt="CompleteTech LLC src="assets/logo.png', "src='unterminated", "", " ", "alt="]) +def test_footer_attrs_tolerates_unterminated_and_empty_input(attrs): + got = readme._footer_attrs(attrs) + assert isinstance(got, dict) + assert set(got) <= {"alt", "src", "srcset"} + + +def test_footer_attrs_empty_input_has_no_attributes(): + assert readme._footer_attrs("") == {} + + +# (e) marked footer regression through the scaffold, G/H callout and upgrade paths + + +def marked_partner_footer(brand): + """A marked footer whose only image is a partner file with a real alt "Partner"; data-alt carries the company.""" + body = block_with(brand, f'src="{LOCAL}" alt="{PARTNER}" data-alt="{brand.strings["company"]}"') + return f"{footer.START}\n{body}\n{footer.END}" + + +def test_marked_data_alt_partner_footer_is_kept_through_scaffold_upgrade(brand, tmp_path): + ctx = ctx_for(brand, tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), marked_partner_footer(brand)) + assert LOCAL in before and footer.block(brand) not in before + after, removed = readme.upgrade(ctx, before) + assert not [ln for ln in removed if LOCAL in ln], "the partner image was reported as removed" + assert image_lines(after, LOCAL) == image_lines(before, LOCAL) + assert image_lines(after, LOCAL) >= 1 + assert readme.preservation(before, after, removed) == [] + + +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_marked_data_alt_partner_footer_is_kept_through_callout_upgrade(brand, tmp_path, variant): + kw = dict(fork=True, parent={"full_name": "someone/upstream", "default_branch": "main"}) if variant == "G" \ + else dict(repo="host-backup-20260914") + ctx = ctx_for(brand, tmp_path, **kw) + assert ctx.det["variant"] == variant + before = UPSTREAM + "\n" + marked_partner_footer(brand) + "\n" + after, removed = readme.upgrade(ctx, before) + assert not [ln for ln in removed if LOCAL in ln], "the partner image was reported as removed" + assert image_lines(after, LOCAL) == image_lines(before, LOCAL) + assert image_lines(after, LOCAL) >= 1 + assert readme.preservation(before, after, removed) == [] From 2ee620d02e6c76308804dcae871270978c640534 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 19:37:06 -0400 Subject: [PATCH 25/44] fix(readme): read footer image attributes whole, exact names, first wins (#47) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + scripts/ctshowcase/readme.py | 28 +++++++++++++++++++++++----- tests/test_footer_attr_tokens.py | 11 ++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6a736..caf557f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. - Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. +- Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins, and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 57ab7c5..b0a54ee 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -569,6 +569,25 @@ def is_footer_like(block: str, brand=None) -> bool: # (title="href=...") is never read as an attribute of its own FOOTER_ATTR_TOKEN = re.compile(r"""\s*([^\s"'=>/]+)(\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?""") +FOOTER_IMG_ATTRS = ("alt", "src", "srcset") + + +def _footer_attrs(attrs: str) -> dict[str, str]: + """The `alt`, `src` and `srcset` attributes of one tag's attribute text, parsed as whole attributes: exact + (case-insensitive) names only, so `data-alt`, `xalt` or text inside another attribute's quoted value never count; + the first occurrence of a name wins, as a browser reads it; the value is returned with its quotes removed.""" + got: dict[str, str] = {} + for tok in FOOTER_ATTR_TOKEN.finditer(attrs): + name = tok.group(1).lower() + if name not in FOOTER_IMG_ATTRS or name in got or tok.group(2) is None: + continue + val = tok.group(2).split("=", 1)[1].strip() + if len(val) >= 2 and val[0] in "\"'" and val[-1] == val[0]: + val = val[1:-1] + got[name] = val + return got + + OLD_LOCAL_LOGOS = {'assets/logo.png', 'logo.png', 'assets/logo-dark.png', 'logo-dark.png'} @@ -665,8 +684,8 @@ def is_footer_copy(block: str, brand, loose_src: bool = False, own_site: bool = credit = {f"{f['line_prefix']} {company}", f"{f['line_prefix']} {company} {f['line_suffix']}"} def _alts(attrs: str) -> list[str]: - return [html.unescape(next(g for g in a.groups()[1:] if g is not None)).strip() - for a in FOOTER_ATTR.finditer(attrs) if a.group(1).lower() == "alt"] + alt = _footer_attrs(attrs).get("alt") + return [] if alt is None else [html.unescape(alt).strip()] # a carries no alt: the block's own alt vouches for its former local logo files block_alt_ok = any(v in (company, f["alt"]) for m in FOOTER_TAG.finditer(block) @@ -685,9 +704,8 @@ def tag_ok(m: re.Match) -> bool: break return href is not None and _footer_href_ok(href, brand) alt_ok, urls = False, [] - for a in FOOTER_ATTR.finditer(attrs): # never a file name that merely contains "logo" - key = a.group(1).lower() - val = html.unescape(next(g for g in a.groups()[1:] if g is not None)).strip() + for key, raw in _footer_attrs(attrs).items(): # whole attributes, exact names; never a file name that merely contains "logo" + val = html.unescape(raw).strip() if key == "alt": alt_ok = alt_ok or val in (company, f["alt"]) else: diff --git a/tests/test_footer_attr_tokens.py b/tests/test_footer_attr_tokens.py index 0975ff8..b0e2f9a 100644 --- a/tests/test_footer_attr_tokens.py +++ b/tests/test_footer_attr_tokens.py @@ -113,15 +113,16 @@ def marked_partner_footer(brand): return f"{footer.START}\n{body}\n{footer.END}" -def test_marked_data_alt_partner_footer_is_kept_through_scaffold_upgrade(brand, tmp_path): +def test_marked_data_alt_partner_footer_is_not_exempt_from_preservation_in_upgrade(brand, tmp_path): + # upgrade() regenerates the marked footer block; a partner image in it is not a footer copy, so it is not reported + # as removed and the never-drop-a-line guard (preservation, i.e. readme-shrink) refuses the replacement. ctx = ctx_for(brand, tmp_path) before = readme.scaffold(ctx).replace(footer.block(brand), marked_partner_footer(brand)) assert LOCAL in before and footer.block(brand) not in before after, removed = readme.upgrade(ctx, before) - assert not [ln for ln in removed if LOCAL in ln], "the partner image was reported as removed" - assert image_lines(after, LOCAL) == image_lines(before, LOCAL) - assert image_lines(after, LOCAL) >= 1 - assert readme.preservation(before, after, removed) == [] + assert not [ln for ln in removed if LOCAL in ln], "the partner image was exempted from preservation" + problems = readme.preservation(before, after, removed) + assert any(LOCAL in p for p in problems), problems @pytest.mark.parametrize("variant", ["G", "H"]) From 8f8085bae2cb0b930fea4366ce21f3fb3fe591e5 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 20:18:48 -0400 Subject: [PATCH 26/44] fix(ctshowcase): record valueless footer attributes as first occurrence _footer_attrs skipped a recognised attribute that had no value, so a later valued duplicate (for example `alt alt="CompleteTech LLC"`) was accepted even though a browser reads the first, empty occurrence. Record the valueless first occurrence as an empty string so later duplicates cannot override it. Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index b0a54ee..cce0d39 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -575,11 +575,15 @@ def is_footer_like(block: str, brand=None) -> bool: def _footer_attrs(attrs: str) -> dict[str, str]: """The `alt`, `src` and `srcset` attributes of one tag's attribute text, parsed as whole attributes: exact (case-insensitive) names only, so `data-alt`, `xalt` or text inside another attribute's quoted value never count; - the first occurrence of a name wins, as a browser reads it; the value is returned with its quotes removed.""" + the first occurrence of a name wins, as a browser reads it; the value is returned with its quotes removed. A + recognised attribute with no value is recorded as an empty string, so a later duplicate cannot override it.""" got: dict[str, str] = {} for tok in FOOTER_ATTR_TOKEN.finditer(attrs): name = tok.group(1).lower() - if name not in FOOTER_IMG_ATTRS or name in got or tok.group(2) is None: + if name not in FOOTER_IMG_ATTRS or name in got: + continue + if tok.group(2) is None: + got[name] = "" continue val = tok.group(2).split("=", 1)[1].strip() if len(val) >= 2 and val[0] in "\"'" and val[-1] == val[0]: From a80c6c970de14302b46fbc18a6102a5ec8884e7c Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 20:19:33 -0400 Subject: [PATCH 27/44] test(footer): add valueless-alt attribute regressions (#47) Add red regressions for the P2 finding at 4823d10: a valueless attribute (`alt`, `src`) is still the first occurrence, so a later duplicate must not override it. Covers the _footer_attrs helper, is_footer_copy on a centred block with a valueless alt before a company alt (former local logo and a custom local image, with controls), and a marked-footer upgrade preservation check. The former-local-logo cases fail until the parser records valueless attributes; the custom-image guards pass today. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_attr_tokens.py | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_footer_attr_tokens.py b/tests/test_footer_attr_tokens.py index b0e2f9a..d6f0411 100644 --- a/tests/test_footer_attr_tokens.py +++ b/tests/test_footer_attr_tokens.py @@ -104,6 +104,23 @@ def test_footer_attrs_empty_input_has_no_attributes(): assert readme._footer_attrs("") == {} +def test_footer_attrs_valueless_first_alt_wins(brand): + # a valueless attribute is still the first occurrence: a later duplicate never overrides it + company = brand.strings["company"] + got = readme._footer_attrs(f'src="{LOCAL}" alt alt="{company}"') + assert got["alt"] == "" + assert got["src"] == LOCAL + # `alt= alt=...`: an unquoted value runs to the next whitespace, as a browser reads it, so the value is + # `alt="` and not the company name; the first alt never vouches for the image + got = readme._footer_attrs(f'src="{LOCAL}" alt= alt="{company}"') + assert got.get("alt") != company + assert got["src"] == LOCAL + # a valueless src followed by a duplicate src + got = readme._footer_attrs(f'src alt="{PARTNER}" src="{LOCAL}"') + assert got["src"] == "" + assert got["alt"] == PARTNER + + # (e) marked footer regression through the scaffold, G/H callout and upgrade paths @@ -137,3 +154,39 @@ def test_marked_data_alt_partner_footer_is_kept_through_callout_upgrade(brand, t assert image_lines(after, LOCAL) == image_lines(before, LOCAL) assert image_lines(after, LOCAL) >= 1 assert readme.preservation(before, after, removed) == [] + + +# (f) a valueless alt before a company alt never vouches for an image: the first alt is the one a browser reads + + +@pytest.mark.parametrize("src", [LOCAL, "assets/custom-mark.png"], ids=["former-local-logo", "custom-local-image"]) +def test_is_footer_copy_valueless_alt_local_image_not_footer(brand, src): + company = brand.strings["company"] + block = block_with(brand, f'src="{src}" alt alt="{company}"') + assert readme.is_footer_copy(block, brand) is False + + +def test_company_alt_on_former_local_logo_is_footer_copy_control(brand): + # control for the valueless-alt case: the same former local logo with a real company alt is footer copy + block = block_with(brand, f'src="{LOCAL}" alt="{brand.strings["company"]}"') + assert readme.is_footer_copy(block, brand) is True + + +def test_company_alt_on_custom_local_image_is_not_footer_copy_control(brand): + # control: a custom local image is never footer copy on the alt alone, so the valueless-alt case cannot change it + block = block_with(brand, f'src="assets/custom-mark.png" alt="{brand.strings["company"]}"') + assert readme.is_footer_copy(block, brand) is False + + +@pytest.mark.parametrize("src", [LOCAL, "assets/custom-mark.png"], ids=["former-local-logo", "custom-local-image"]) +def test_marked_valueless_alt_local_image_footer_is_not_removed_in_upgrade(brand, tmp_path, src): + # the authored image in a marked footer is not reported as removed; the replacement is refused by the + # preservation guard or the image line survives in the output + ctx = ctx_for(brand, tmp_path) + body = block_with(brand, f'src="{src}" alt alt="{brand.strings["company"]}"') + before = readme.scaffold(ctx).replace(footer.block(brand), f"{footer.START}\n{body}\n{footer.END}") + assert src in before and footer.block(brand) not in before + after, removed = readme.upgrade(ctx, before) + assert not [ln for ln in removed if src in ln], "the authored image was reported as removed" + problems = readme.preservation(before, after, removed) + assert problems or image_lines(after, src) >= 1, problems From 020f30a65ba323a946c514220c07f12504ec417b Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 20:20:31 -0400 Subject: [PATCH 28/44] docs(changelog): note valueless footer attributes keep first-occurrence semantics (#47) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706b07b..cc00461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. - Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. -- Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins, and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. +- Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins even when it has no value (`alt alt="CompleteTech LLC"` reads as an empty alt, so the authored image is preserved), and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. From 906353910c366d82deefacdf9aab67c0cb5e061f Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 20:42:33 -0400 Subject: [PATCH 29/44] fix(readme): narrow own-site logo to configured paths and re-emitted footer lines (#47) - _own_site_logo accepts only the exact OWN_SITE_LOGO_PATHS on the configured company website host, so other files on that host stay authored content. - upgrade() shares one multiset of re-emitted footer lines across the marked and unmarked branches; unmarked old-footer lines the new footer carries again are no longer reported as removed (_unreemitted). Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index cce0d39..5ebcff9 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -609,11 +609,14 @@ def _old_local_logo(url: str) -> bool: OWN_SITE_LOCAL_LOGOS = {"docs/images/logo.svg"} +OWN_SITE_LOGO_PATHS = {"/wp-content/uploads/logo.png"} def _own_site_logo(url: str, brand) -> bool: - """`url` is a demonstrated historical logo source of this brand: a file on the configured company website host - (`brand_strings.website`, never hardcoded) over http(s), or the conventional local `docs/images/logo.svg`.""" + """`url` is a configured historical logo source of this brand: one of the exact paths in `OWN_SITE_LOGO_PATHS` + on the configured company website host (`brand_strings.website`, never hardcoded) over http(s), or the + conventional local `docs/images/logo.svg`. Any other file on that host (a team photo, a screenshot) is authored + content and is kept.""" from urllib.parse import urlsplit url = url.strip() if "://" not in url: @@ -621,7 +624,14 @@ def _own_site_logo(url: str, brand) -> bool: return (path[2:] if path.startswith("./") else path) in OWN_SITE_LOCAL_LOGOS try: got, want = urlsplit(url), urlsplit(brand.strings["website"]) - return bool(want.hostname) and got.scheme in ("http", "https") and got.hostname == want.hostname and got.port in (None, want.port) and got.username is None + return ( + bool(want.hostname) + and got.scheme in ("http", "https") + and got.hostname == want.hostname + and got.port in (None, want.port) + and got.username is None + and got.path in OWN_SITE_LOGO_PATHS + ) except ValueError: return False @@ -681,7 +691,9 @@ def is_footer_copy(block: str, brand, loose_src: bool = False, own_site: bool = rule that an alt exactly the company vouches for an image whatever its src (no caller in `upgrade()`: a generic alt is not provenance). `own_site=True` (`upgrade()`, for an unmarked trailing block that also passes `is_footer_like`) additionally recognizes, under an alt exactly the company or the footer alt, only the brand's - own-site historical logo sources (`_own_site_logo`); partner, foreign and look-alike hosts stay authored. The + own-site historical logo sources (`_own_site_logo`: the exact paths in `OWN_SITE_LOGO_PATHS` on the company + website host, plus the local `docs/images/logo.svg`; "historical logo sources" means those exact paths, not every + file on the host); partner, foreign and look-alike hosts, and other files on the company host, stay authored. The default is strict.""" import html f, company = brand.raw["footer"], brand.strings["company"] @@ -751,6 +763,21 @@ def drop_old_footer(text: str, brand, loose_src: bool = False, own_site: bool = return body[:m.start()].rstrip() + "\n", [ln for ln in tail.splitlines() if ln.strip()] +def _unreemitted(lines: list[str], kept: Counter) -> list[str]: + """`lines` minus the lines the emitted footer carries again: each non-empty line matched in the multiset `kept` + (stripped text) is dropped and decrements `kept`; the others are returned with their original text.""" + out: list[str] = [] + for ln in lines: + s = ln.strip() + if not s: + continue + if kept[s] > 0: + kept[s] -= 1 + else: + out.append(ln) + return out + + def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: """Return (new text, header lines removed). Raises nothing; callers run the shrink checks.""" variant = ctx.det["variant"] @@ -761,11 +788,15 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: body, blocks = _strip_markers(text) want_footer = footer_wanted(ctx) removed: list[str] = [] + # One multiset of the footer lines the emitted footer carries again, shared by the marked and the unmarked branch + # because only one new footer is emitted. A compliant footer is re-emitted unchanged and must not weaken the + # never-drop-a-line guard, so only the old lines it does not carry again are reported as removed. + kept: Counter = Counter() + if want_footer: + kept = Counter(ln.strip() for ln in footermod.block(ctx.brand).splitlines() if ln.strip()) if want_footer and blocks.get("footer") and is_footer_copy(blocks["footer"], ctx.brand): # A configured publication-path change replaces only our recognized footer, not authored credits or partner - # images inside a similarly named block. Only the lines the emitted footer does not carry again are reported: - # a compliant footer is re-emitted unchanged and must not weaken the never-drop-a-line guard. - kept = Counter(ln.strip() for ln in footermod.block(ctx.brand).splitlines() if ln.strip()) + # images inside a similarly named block. for ln in blocks["footer"].splitlines(): if ln.strip(): if kept[ln.strip()] > 0: @@ -774,7 +805,7 @@ def upgrade(ctx: RepoContext, text: str) -> tuple[str, list[str]]: removed.append(ln) if want_footer: # an unmarked or drifted footer at the end is replaced by the marked one, never kept beside it body, old_footer = drop_old_footer(body, ctx.brand, own_site=True) - removed += old_footer + removed += _unreemitted(old_footer, kept) # preserve list / existing third-party banner preserve = ctx.repo in (ctx.org.get("readme_preserve") or []) or len(text.encode()) > 15000 pre, sections = split_sections(body) From 1e8a93f629881988986ff0642597c1d4f5005255 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 20:43:29 -0400 Subject: [PATCH 30/44] test(footer): add r7 regressions for same-host images and re-emitted footer lines (#47) Adds tests/test_footer_r7_regressions.py. Same-host non-logo images must survive upgrade, a historical logo path with a query still migrates, a regular unmarked footer must not hide lost banner anchors, and removed must not include re-emitted footer lines. The new tests that depend on the readme.py fix fail at 020f30a as expected; no existing test is changed. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_r7_regressions.py | 109 ++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/test_footer_r7_regressions.py diff --git a/tests/test_footer_r7_regressions.py b/tests/test_footer_r7_regressions.py new file mode 100644 index 0000000..4785737 --- /dev/null +++ b/tests/test_footer_r7_regressions.py @@ -0,0 +1,109 @@ +"""Round r7 footer findings for issue 47: same-host images that are not historical logos, and the never-drop-a-line +guard for unmarked footers whose re-emitted lines must not be exempted (source findings at 020f30a). + +Only existing APIs are used: conftest.meta_dict, the `brand` fixture, ctshowcase.footer, ctshowcase.readme, +RepoContext and normalize. All fixtures are synthetic and written under tmp_path. Expected state at 020f30a: tests that +depend on the readme.py fix fail until that fix lands; no assertion here is weakened to make them pass. +""" +from collections import Counter + +import pytest + +from conftest import meta_dict +from ctshowcase import footer, readme +from ctshowcase.context import RepoContext +from ctshowcase.meta import normalize + +CREDIT = ('Built by CompleteTech LLC · Innovation at Every Integration' + "") +OWN_SITE_LINK = '' +CLOSE_LINK = "" + + +def unmarked(src, alt="CompleteTech LLC"): + return ('

%s
%s

' + % (src, alt, CREDIT)) + + +def image_lines(text, src): + return sum(1 for ln in text.splitlines() if src in ln) + + +def stripped_lines(text): + return [ln.strip() for ln in text.splitlines() if ln.strip()] + + +@pytest.mark.parametrize("src", [ + "https://www.complete.tech/team-photo.png", + "https://www.complete.tech/wp-content/uploads/team.png", + "https://www.complete.tech/assets/logo.png", +]) +def test_same_host_non_logo_image_is_kept_through_upgrade(brand, tmp_path, src): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), unmarked(src)) + assert src in before and footer.block(brand) not in before + after, removed = readme.upgrade(ctx, before) + assert not [ln for ln in removed if src in ln], f"{src} was reported as removed" + assert image_lines(after, src) == image_lines(before, src) + assert image_lines(after, src) >= 1 + assert readme.preservation(before, after, removed) == [] + assert readme.is_footer_copy(unmarked(src), brand, own_site=True) is False + + +def test_historical_logo_path_with_query_still_migrates(brand, tmp_path): + src = "https://www.complete.tech/wp-content/uploads/logo.png?ver=2" + assert readme.is_footer_copy(unmarked(src), brand, own_site=True) is True + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + before = readme.scaffold(ctx).replace(footer.block(brand), unmarked(src)) + assert src in before + after, removed = readme.upgrade(ctx, before) + assert after.count("Built by") == 1 + assert src not in after + assert readme.preservation(before, after, removed) == [] + + +def test_regular_unmarked_footer_does_not_exempt_reemitted_banner_anchors(brand, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + assert ctx.det["variant"] == "C" + scaffold = readme.scaffold(ctx) + banner_body = readme.banner_html(ctx) + assert readme.wrap("banner", banner_body) in scaffold + # the generated banner wrapped in a standalone company-link anchor, one anchor tag per line + linked = f"{readme.start('banner')}\n{OWN_SITE_LINK}\n{banner_body}\n{CLOSE_LINK}\n{readme.end('banner')}" + old_footer = unmarked("docs/images/logo.svg") + before = scaffold.replace(readme.wrap("banner", banner_body), linked).replace(footer.block(brand), old_footer) + assert linked in before and old_footer in before and footer.block(brand) not in before + + after, removed = readme.upgrade(ctx, before) + + # the regenerated banner carries no company anchors: the wrapper it lost must be reported, not exempted + problems = readme.preservation(before, after, removed) + assert problems, "a banner anchor lost during upgrade was not reported by preservation" + assert f"lost line: {OWN_SITE_LINK}" in problems + assert f"lost line: {CLOSE_LINK}" in problems + + # removed holds no banner anchor line beyond what the unmarked old footer consumed on its own + expected = Counter(stripped_lines(old_footer)) - Counter(stripped_lines(footer.block(brand))) + removed_counts = Counter(stripped_lines("\n".join(removed))) + for anchor in (OWN_SITE_LINK, CLOSE_LINK): + assert removed_counts[anchor] <= expected[anchor] + + +def test_unmarked_footer_removed_lines_exclude_reemitted_lines(brand, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + light = brand.raw["footer"]["logo_light_url"] + # a multi-line legacy footer: the marked footer's markup, with only its own logo image pointing at a local file + old_footer = footer.markup(brand).replace(f' Date: Sat, 31 Oct 2026 20:46:15 -0400 Subject: [PATCH 31/44] test(footer): make r7 banner-anchor regression use a multi-line legacy footer; docs: own-site logo paths (#47) The anchor regression now fails if the whole old footer is exempted again (verified by reverting to 'removed += old_footer'). CHANGELOG and readme-variants describe OWN_SITE_LOGO_PATHS recognition and the multiset-limited removed report. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 3 ++- references/readme-variants.md | 2 +- tests/test_footer_r7_regressions.py | 22 +++++++++++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc00461..484f175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. - Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. - Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins even when it has no value (`alt alt="CompleteTech LLC"` reads as an empty alt, so the authored image is preserved), and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. +- Own-site footer recognition is limited to the configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS`), not any file on that host: a team photo or screenshot on the same host stays authored content and is kept through `readme --mode upgrade`. Migrating an unmarked footer reports as removed only the lines the new footer does not emit again, the same multiset rule as marked footers, so a lost banner anchor or other markup is no longer hidden by re-emitted footer lines and fails the never-drop-a-line check. No brand or decision change. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. @@ -58,7 +59,7 @@ First public release, for the CompleteTech GitHub estate standard `ct-std-v1`. - A T3 org's repository is left out of the personal pins and more projects, other orgs' cross-listings, favorites and star List items until co-owner sign-off (`--allow-t3`), logged `t3-signoff:OWNER/REPO`. - `project copy-plan` and the standalone `project readme` name no repository in the README of a board that is public (as captured, approved or not) or may go public (an approved visibility item), logged `readme-links:repositories-omitted (the board may go public)`; a board captured public without approval logs `visibility:public (no approval)`, not `visibility:private`. - `labels --plan` and `adopt` refuse an `--org-defaults` metadata JSON or checkout of another org's `.github` (usage, exit 2), with the owner read from `owner`, `owner.login`, `full_name` or the checkout's own `origin` and resolved through the run's org resolver (`--config` included). A JSON without an owner, or a checkout without a GitHub origin, can no longer prove that no default issue form names a label, so the rename stays held (`needs-input:label-references`). Before, another org's `.github` was accepted and the rename was planned while the owner's default forms still assigned the old label. -- `readme --mode upgrade` (and the bootstrap `edited` path and the preserve list) replace an unmarked or drifted footer at the end of the README with the decision 18 footer instead of appending a second one. Only a block whose every line is a footer line (centring, links to the configured footer host (`footer.href`), the footer's own logo image, the "Built by" credit line) is replaced; an image counts as the footer's logo only when its src or srcset is the brand footer logo URL (or the same file on another branch, with a query, or in its github.com blob/raw form) or its alt is exactly the company, never because its file name contains "logo". A centred block with an authored line (a sponsor or partner image, a thanks line) is kept, so no authored line is dropped. `lint readme` fails `D18 footer fail:duplicate`, and `footer --check` (and doctor's footer column) `fail:duplicate`, when a footer-like block outside code sits before the last block. +- `readme --mode upgrade` (and the bootstrap `edited` path and the preserve list) replace an unmarked or drifted footer at the end of the README with the decision 18 footer instead of appending a second one. Only a block whose every line is a footer line (centring, links to the configured footer host (`footer.href`), the footer's own logo image, the "Built by" credit line) is replaced; an image counts as the footer's logo only when its src or srcset is the brand footer logo URL (or the same file on another branch, with a query, or in its github.com blob/raw form) or it is a configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS`, not any file on that host) or the conventional local `docs/images/logo.svg` under a company alt, never because its file name contains "logo" or its alt matches. A centred block with an authored line (a sponsor or partner image, a thanks line) is kept, so no authored line is dropped. `lint readme` fails `D18 footer fail:duplicate`, and `footer --check` (and doctor's footer column) `fail:duplicate`, when a footer-like block outside code sits before the last block. ### Fixed diff --git a/references/readme-variants.md b/references/readme-variants.md index 66b63a6..84b0408 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS` in `readme.py`, not any file on that host) or the local `docs/images/logo.svg`; any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does diff --git a/tests/test_footer_r7_regressions.py b/tests/test_footer_r7_regressions.py index 4785737..c509fe8 100644 --- a/tests/test_footer_r7_regressions.py +++ b/tests/test_footer_r7_regressions.py @@ -63,30 +63,38 @@ def test_historical_logo_path_with_query_still_migrates(brand, tmp_path): def test_regular_unmarked_footer_does_not_exempt_reemitted_banner_anchors(brand, tmp_path): + """The legacy footer is multi-line and shares its standalone anchor lines with the footer that is emitted again. + Anchor lines the new footer carries must not be added to `removed`: the multiset guard then still sees one banner + anchor pair missing (before: banner + old footer = 2, after: the new footer = 1). Had the whole old footer been + exempted (`removed += old_footer`), the exemption would hide the lost banner anchors and preservation would pass. + A one-line legacy footer would not show this, because it has no standalone anchor lines to exempt.""" ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) assert ctx.det["variant"] == "C" scaffold = readme.scaffold(ctx) banner_body = readme.banner_html(ctx) assert readme.wrap("banner", banner_body) in scaffold # the generated banner wrapped in a standalone company-link anchor, one anchor tag per line - linked = f"{readme.start('banner')}\n{OWN_SITE_LINK}\n{banner_body}\n{CLOSE_LINK}\n{readme.end('banner')}" - old_footer = unmarked("docs/images/logo.svg") + linked = "\n".join([readme.start("banner"), OWN_SITE_LINK, banner_body, CLOSE_LINK, readme.end("banner")]) + light = brand.raw["footer"]["logo_light_url"] + old_footer = footer.markup(brand).replace(f' Date: Sat, 31 Oct 2026 21:05:52 -0400 Subject: [PATCH 32/44] fix(readme): derive own-site historical logo from footer.legacy_logo_urls (#47) Remove the hardcoded OWN_SITE_LOGO_PATHS from Python; the website logo URL is brand data in references/brand.yaml (lock refreshed via config --write-lock). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 1 + references/brand.lock.json | 3 +- references/brand.yaml | 2 + scripts/ctshowcase/readme.py | 42 ++++++++++------- tests/test_footer_upgrade_preservation.py | 57 ++++++++++++++++++++++- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484f175..25c5d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed +- Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. - Lint accepts file operands after options on older Python 3.12 versions while continuing to reject unrecognized options. - The Action's optional render arguments support macOS Bash 3.2, including an empty argument array with `set -u`. diff --git a/references/brand.lock.json b/references/brand.lock.json index 75f8716..9675407 100644 --- a/references/brand.lock.json +++ b/references/brand.lock.json @@ -462,7 +462,8 @@ "href": "https://www.complete.tech", "legacy_logo_urls": [ "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png", - "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png" + "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png", + "https://www.complete.tech/wp-content/uploads/logo.png" ], "line_prefix": "Built by", "line_suffix": "· Innovation at Every Integration", diff --git a/references/brand.yaml b/references/brand.yaml index 1d778ec..ec6c167 100644 --- a/references/brand.yaml +++ b/references/brand.yaml @@ -39,6 +39,8 @@ footer: legacy_logo_urls: - https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo.png - https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/logo-dark.png + # Historical company-website logo (an own-site README layout); recognized only to migrate such footers. + - https://www.complete.tech/wp-content/uploads/logo.png alt: CompleteTech LLC width: 200 line_prefix: Built by diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 5ebcff9..9e53ae3 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -608,32 +608,37 @@ def _old_local_logo(url: str) -> bool: return path in OLD_LOCAL_LOGOS +# A file CONVENTION (the logo a former own-site README layout kept beside the docs), not brand data. OWN_SITE_LOCAL_LOGOS = {"docs/images/logo.svg"} -OWN_SITE_LOGO_PATHS = {"/wp-content/uploads/logo.png"} def _own_site_logo(url: str, brand) -> bool: - """`url` is a configured historical logo source of this brand: one of the exact paths in `OWN_SITE_LOGO_PATHS` - on the configured company website host (`brand_strings.website`, never hardcoded) over http(s), or the - conventional local `docs/images/logo.svg`. Any other file on that host (a team photo, a screenshot) is authored - content and is kept.""" + """`url` is a configured historical logo source of this brand: it equals, exactly, an http(s) entry of + `footer.legacy_logo_urls` in references/brand.yaml (scheme, host case-insensitively, port, path; query and + fragment ignored; no userinfo), so no brand host or path lives in Python. The generic local `docs/images/logo.svg` + (`OWN_SITE_LOCAL_LOGOS`) is a file convention, distinct from that configured data. Any other file on the same host + (a team photo, a screenshot) is authored content and is kept.""" from urllib.parse import urlsplit url = url.strip() if "://" not in url: path = url.split("#", 1)[0].split("?", 1)[0] return (path[2:] if path.startswith("./") else path) in OWN_SITE_LOCAL_LOGOS + legacy = brand.raw.get("footer", {}).get("legacy_logo_urls") or [] try: - got, want = urlsplit(url), urlsplit(brand.strings["website"]) - return ( - bool(want.hostname) - and got.scheme in ("http", "https") - and got.hostname == want.hostname - and got.port in (None, want.port) - and got.username is None - and got.path in OWN_SITE_LOGO_PATHS - ) + got = urlsplit(url) + if got.username is not None or got.password is not None or not got.hostname: + return False + for entry in legacy: + if not isinstance(entry, str): + continue + want = urlsplit(entry.strip()) + if (want.scheme in ("http", "https") and got.scheme == want.scheme and want.hostname + and got.hostname == want.hostname and got.port == want.port + and got.path == want.path): + return True except ValueError: return False + return False def _footer_href_ok(href: str, brand) -> bool: @@ -668,7 +673,10 @@ def _footer_logo_url(url: str, brand) -> bool: url = url.strip().split("#", 1)[0].split("?", 1)[0] footer = brand.raw["footer"] urls = [footer[key] for key in ("logo_light_url", "logo_dark_url")] - urls.extend(footer.get("legacy_logo_urls", [])) + # Legacy publication copies of the footer logo (GitHub raw/blob) are recognized on their own; any other legacy + # entry (a company-website logo) is an own-site source that only `_own_site_logo` accepts, under a vouching alt. + urls.extend(u for u in footer.get("legacy_logo_urls") or [] + if isinstance(u, str) and u.startswith("https://raw.githubusercontent.com/")) for want in urls: if url == want: return True @@ -691,8 +699,8 @@ def is_footer_copy(block: str, brand, loose_src: bool = False, own_site: bool = rule that an alt exactly the company vouches for an image whatever its src (no caller in `upgrade()`: a generic alt is not provenance). `own_site=True` (`upgrade()`, for an unmarked trailing block that also passes `is_footer_like`) additionally recognizes, under an alt exactly the company or the footer alt, only the brand's - own-site historical logo sources (`_own_site_logo`: the exact paths in `OWN_SITE_LOGO_PATHS` on the company - website host, plus the local `docs/images/logo.svg`; "historical logo sources" means those exact paths, not every + own-site historical logo sources (`_own_site_logo`: the exact `footer.legacy_logo_urls` entries, plus the generic local + `docs/images/logo.svg` convention; "historical logo sources" means those exact paths, not every file on the host); partner, foreign and look-alike hosts, and other files on the company host, stay authored. The default is strict.""" import html diff --git a/tests/test_footer_upgrade_preservation.py b/tests/test_footer_upgrade_preservation.py index 5463e03..46bf5ab 100644 --- a/tests/test_footer_upgrade_preservation.py +++ b/tests/test_footer_upgrade_preservation.py @@ -91,9 +91,11 @@ def test_own_site_host_is_derived_from_brand_data(brand, tmp_path): raw = copy.deepcopy(brand.raw) raw["brand_strings"]["website"] = DERIVED_HOST raw["footer"]["href"] = DERIVED_HOST + own = DERIVED_HOST + "/wp-content/uploads/logo.png" + # the own-site logo is configured data (footer.legacy_logo_urls), never a path table in Python + raw["footer"]["legacy_logo_urls"] = [u for u in raw["footer"]["legacy_logo_urls"] if "wp-content" not in u] + [own] derived = Brand(raw, brand.path) ctx = RepoContext(derived, normalize(meta_dict()), tmp_path) - own = DERIVED_HOST + "/wp-content/uploads/logo.png" before = readme.scaffold(ctx).replace(footer.block(derived), unmarked(own, href=DERIVED_HOST)) assert own in before after, removed = readme.upgrade(ctx, before) @@ -131,3 +133,56 @@ def test_helper_accepts_own_site_legacy_logos_with_own_site_flag(brand, src): def test_strict_default_drop_old_footer_keeps_company_alt_svg(brand): text = "# A\n\n" + unmarked("docs/images/logo.svg") assert readme.drop_old_footer(text, brand) == (text, []) + + +def _with_legacy(brand, legacy): + raw = copy.deepcopy(brand.raw) + if legacy is None: + raw["footer"].pop("legacy_logo_urls", None) + else: + raw["footer"]["legacy_logo_urls"] = legacy + return Brand(raw, brand.path) + + +def test_no_brand_logo_path_in_python_source(): + import pathlib + src = pathlib.Path(readme.__file__).read_text(encoding="utf-8") + assert "OWN_SITE_LOGO_PATHS" not in src and "wp-content" not in src + + +@pytest.mark.parametrize("url,expected", [ + ("https://www.complete.tech/wp-content/uploads/logo.png", True), + ("https://WWW.Complete.Tech/wp-content/uploads/logo.png", True), + ("https://www.complete.tech/wp-content/uploads/logo.png?ver=2", True), + ("http://www.complete.tech/wp-content/uploads/logo.png", False), + ("https://www.complete.tech/wp-content/uploads/team-photo.png", False), + ("https://www.complete.tech.evil.test/wp-content/uploads/logo.png", False), + ("https://user@www.complete.tech/wp-content/uploads/logo.png", False), + ("https://www.complete.tech:8443/wp-content/uploads/logo.png", False), + ("docs/images/logo.svg", True), +]) +def test_own_site_logo_is_derived_from_legacy_logo_urls(brand, url, expected): + assert readme._own_site_logo(url, brand) is expected + + +@pytest.mark.parametrize("legacy", [None, [], ["not a url", 7, None, {"a": 1}]]) +def test_own_site_logo_without_usable_legacy_urls(brand, legacy): + b = _with_legacy(brand, legacy) + assert readme._own_site_logo("https://www.complete.tech/wp-content/uploads/logo.png", b) is False + assert readme._own_site_logo("docs/images/logo.svg", b) is True + + +def test_own_site_logo_duplicate_legacy_entries(brand): + url = "https://www.complete.tech/wp-content/uploads/logo.png" + assert readme._own_site_logo(url, _with_legacy(brand, [url, url])) is True + + +def test_schema_accepts_website_legacy_logo_and_rejects_bad_entries(brand): + from ctshowcase.config import schema_errors + assert "https://www.complete.tech/wp-content/uploads/logo.png" in brand.raw["footer"]["legacy_logo_urls"] + assert schema_errors(brand.raw) == [] + assert schema_errors(_with_legacy(brand, None).raw) == [] # omitted is valid + url = brand.raw["footer"]["legacy_logo_urls"][0] + assert schema_errors(_with_legacy(brand, [url, url]).raw) != [] # duplicates + assert schema_errors(_with_legacy(brand, [7]).raw) != [] # invalid entry type + assert schema_errors(_with_legacy(brand, ["http://www.complete.tech/logo.png"]).raw) != [] # not https From 5266d04dc4a9b473f393409cee7e0fbc381d2f49 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:28:35 -0400 Subject: [PATCH 33/44] fix(readme): refuse bootstrap fallback when a marked footer holds authored images Co-Authored-By: Claude Haiku 5.5 --- scripts/showcase.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/showcase.py b/scripts/showcase.py index 244bfd0..a1735f1 100644 --- a/scripts/showcase.py +++ b/scripts/showcase.py @@ -410,6 +410,14 @@ def cmd_readme(args): base_text = readme.drop_pristine(before) after, removed = readme.upgrade(ctx, base_text) if readme.preservation(base_text, after, removed): + # a marked footer that holds authored content (partner or foreign image) must never be regenerated + for fm in readme.find_blocks(readme.MARK, base_text): + if fm.group(1) == "footer" and not readme.is_footer_copy(fm.group(2), ctx.brand): + return base("readme", ctx, result="fail", mode="bootstrap", + problems=["readme-shrink: the marked footer holds authored content (a partner or " + "foreign image) that the footer refresh would remove; README left " + "unchanged"] + readme.preservation(base_text, after, removed)[:19], + logs=ctx.logs + ["readme-shrink"]), CHECK_FAILED ctx.log("readme-shrink") after, removed = readme.header_only(ctx, base_text), [] else: From 8ff00c6dc09c74ef9d921e81bc9407f2a234d6ca Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:28:46 -0400 Subject: [PATCH 34/44] fix(readme): accept own-site historical logo in G/H trailing footer migration (#47) callout_only called drop_old_footer without own_site=True, so an unmarked trailing footer using footer.legacy_logo_urls or docs/images/logo.svg was rejected for G/H forks and archived repos. Match upgrade(). Marked footers stay strict, as in upgrade(). Co-Authored-By: Claude Haiku 5.5 --- scripts/ctshowcase/readme.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/ctshowcase/readme.py b/scripts/ctshowcase/readme.py index 9e53ae3..2167ddb 100644 --- a/scripts/ctshowcase/readme.py +++ b/scripts/ctshowcase/readme.py @@ -472,7 +472,10 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: the former logo URLs) is replaced by the current one, and only the removed footer lines that the replacement footer does not carry again are reported (multiset difference), so the preservation check accepts them. A re-emitted line is not reported, so it cannot weaken the never-drop-a-line guard. A marked footer that is not a footer copy - (a partner image) is left as it is. A removed recognized footer is always replaced, even when fenced example text makes check() report drift.""" + (a partner image) is left as it is. The trailing unmarked footer additionally accepts the brand's own-site historical + logo sources (`footer.legacy_logo_urls` entries and `docs/images/logo.svg`) under a vouching alt, while partner, + foreign and look-alike images stay authored. A removed recognized footer is always replaced, even when fenced + example text makes check() report drift.""" if ctx.det["variant"] == "G" and ctx.brand.readme_opts.get("fork_readme") == "full": ctx.log("needs-input:fork-variant (q27 full treatment: pass readme --variant for the §5.4 variant)") body = text @@ -507,7 +510,7 @@ def callout_only(ctx: RepoContext, text: str) -> tuple[str, list[str]]: had_footer = True pos = fm.end() out = "".join(kept) + out[pos:] - out, old_footer = drop_old_footer(out, ctx.brand) + out, old_footer = drop_old_footer(out, ctx.brand, own_site=True) removed += old_footer if old_footer: had_footer = True From 86de5ad8bb34833895288253c6d1fe05b0916854 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:31:17 -0400 Subject: [PATCH 35/44] test+docs(footer): bootstrap refusal and G/H own-site migration regressions; drop stale OWN_SITE_LOGO_PATHS wording (#47) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 6 +- references/readme-variants.md | 2 +- tests/test_footer_upgrade_preservation.py | 180 ++++++++++++++++++++++ 3 files changed, 185 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c5d2c..492756e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed +- `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. +- Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. - Lint accepts file operands after options on older Python 3.12 versions while continuing to reject unrecognized options. @@ -24,7 +26,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. - Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. - Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins even when it has no value (`alt alt="CompleteTech LLC"` reads as an empty alt, so the authored image is preserved), and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. -- Own-site footer recognition is limited to the configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS`), not any file on that host: a team photo or screenshot on the same host stays authored content and is kept through `readme --mode upgrade`. Migrating an unmarked footer reports as removed only the lines the new footer does not emit again, the same multiset rule as marked footers, so a lost banner anchor or other markup is no longer hidden by re-emitted footer lines and fails the never-drop-a-line check. No brand or decision change. +- Own-site footer recognition is limited to the configured historical logo URLs on the company website host (`footer.legacy_logo_urls`), not any file on that host: a team photo or screenshot on the same host stays authored content and is kept through `readme --mode upgrade`. Migrating an unmarked footer reports as removed only the lines the new footer does not emit again, the same multiset rule as marked footers, so a lost banner anchor or other markup is no longer hidden by re-emitted footer lines and fails the never-drop-a-line check. No brand or decision change. - `doctor` reports disabled-Issues label checks, inapplicable social previews and archived community health as `n/a`, while preserving label coverage during approved archived polishing and unknown captured values on applicable surfaces. Social upload plans share the R10 applicability rule and include the personal-profile capstone. - `doctor` reports R11 as `n/a` for archived repositories and outside T1/T2 organizations (personal-account, utility and co-owned repositories), matching the definition of done. @@ -60,7 +62,7 @@ First public release, for the CompleteTech GitHub estate standard `ct-std-v1`. - A T3 org's repository is left out of the personal pins and more projects, other orgs' cross-listings, favorites and star List items until co-owner sign-off (`--allow-t3`), logged `t3-signoff:OWNER/REPO`. - `project copy-plan` and the standalone `project readme` name no repository in the README of a board that is public (as captured, approved or not) or may go public (an approved visibility item), logged `readme-links:repositories-omitted (the board may go public)`; a board captured public without approval logs `visibility:public (no approval)`, not `visibility:private`. - `labels --plan` and `adopt` refuse an `--org-defaults` metadata JSON or checkout of another org's `.github` (usage, exit 2), with the owner read from `owner`, `owner.login`, `full_name` or the checkout's own `origin` and resolved through the run's org resolver (`--config` included). A JSON without an owner, or a checkout without a GitHub origin, can no longer prove that no default issue form names a label, so the rename stays held (`needs-input:label-references`). Before, another org's `.github` was accepted and the rename was planned while the owner's default forms still assigned the old label. -- `readme --mode upgrade` (and the bootstrap `edited` path and the preserve list) replace an unmarked or drifted footer at the end of the README with the decision 18 footer instead of appending a second one. Only a block whose every line is a footer line (centring, links to the configured footer host (`footer.href`), the footer's own logo image, the "Built by" credit line) is replaced; an image counts as the footer's logo only when its src or srcset is the brand footer logo URL (or the same file on another branch, with a query, or in its github.com blob/raw form) or it is a configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS`, not any file on that host) or the conventional local `docs/images/logo.svg` under a company alt, never because its file name contains "logo" or its alt matches. A centred block with an authored line (a sponsor or partner image, a thanks line) is kept, so no authored line is dropped. `lint readme` fails `D18 footer fail:duplicate`, and `footer --check` (and doctor's footer column) `fail:duplicate`, when a footer-like block outside code sits before the last block. +- `readme --mode upgrade` (and the bootstrap `edited` path and the preserve list) replace an unmarked or drifted footer at the end of the README with the decision 18 footer instead of appending a second one. Only a block whose every line is a footer line (centring, links to the configured footer host (`footer.href`), the footer's own logo image, the "Built by" credit line) is replaced; an image counts as the footer's logo only when its src or srcset is the brand footer logo URL (or the same file on another branch, with a query, or in its github.com blob/raw form) or it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls`, not any file on that host) or the conventional local `docs/images/logo.svg` under a company alt, never because its file name contains "logo" or its alt matches. A centred block with an authored line (a sponsor or partner image, a thanks line) is kept, so no authored line is dropped. `lint readme` fails `D18 footer fail:duplicate`, and `footer --check` (and doctor's footer column) `fail:duplicate`, when a footer-like block outside code sits before the last block. ### Fixed diff --git a/references/readme-variants.md b/references/readme-variants.md index 84b0408..dceef50 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo path on the company website host (`OWN_SITE_LOGO_PATHS` in `readme.py`, not any file on that host) or the local `docs/images/logo.svg`; any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does diff --git a/tests/test_footer_upgrade_preservation.py b/tests/test_footer_upgrade_preservation.py index 46bf5ab..28e1b68 100644 --- a/tests/test_footer_upgrade_preservation.py +++ b/tests/test_footer_upgrade_preservation.py @@ -186,3 +186,183 @@ def test_schema_accepts_website_legacy_logo_and_rejects_bad_entries(brand): assert schema_errors(_with_legacy(brand, [url, url]).raw) != [] # duplicates assert schema_errors(_with_legacy(brand, [7]).raw) != [] # invalid entry type assert schema_errors(_with_legacy(brand, ["http://www.complete.tech/logo.png"]).raw) != [] # not https + + + +# ------------------------------------------------------------------ bootstrap write path (issue 47, r1 finding P1) +PARTNER_LIGHT = "https://example.test/partner-badge.png" + + +def edited_template_readme(footer_block): + """A README whose template-body block was edited (marker hash does not match), followed by `footer_block`.""" + body = (readme.start("template-body") + "\n\n" + + "Edited by a person.\n" + readme.end("template-body")) + return "# New Thing\n\n" + body + "\n\n" + footer_block + "\n" + + +def run_bootstrap_write(tmp_path, text, name): + import json + + from conftest import run_cli + + repo = tmp_path / name + repo.mkdir() + meta = tmp_path / (name + ".json") + meta.write_text(json.dumps(meta_dict()), encoding="utf-8") + (repo / "README.md").write_bytes(text.encode("utf-8")) + p = run_cli("--meta", meta, "--repo-dir", repo, "--format", "json", "readme", "--bootstrap", "--write") + return p, repo / "README.md" + + +def test_bootstrap_write_refuses_marked_footer_with_a_partner_image(brand, tmp_path): + partner = footer.block(brand).replace(brand.raw["footer"]["logo_light_url"], PARTNER_LIGHT) + assert PARTNER_LIGHT in partner + text = edited_template_readme(partner) + assert readme.bootstrap_state(text) == "edited" + p, path = run_bootstrap_write(tmp_path, text, "partner") + assert p.returncode == 1, (p.returncode, p.stdout, p.stderr) # CHECK_FAILED + assert "readme-shrink" in p.stdout + assert path.read_bytes() == text.encode("utf-8"), "a refused bootstrap write must leave README.md byte-identical" + + +def test_bootstrap_write_positive_control_plain_old_footer_still_falls_back(brand, tmp_path): + old = footer.block(legacy_brand_urls(brand)) + text = edited_template_readme(old) + assert readme.bootstrap_state(text) == "edited" + p, path = run_bootstrap_write(tmp_path, text, "plain") + assert p.returncode == 0, (p.returncode, p.stdout, p.stderr) + after = path.read_text(encoding="utf-8") + assert "Edited by a person." in after and footer.check(brand, after)[0] == "pass" + + +def legacy_brand_urls(brand): + raw = copy.deepcopy(brand.raw) + legacy = "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/" + raw["footer"].update(logo_light_url=legacy + "logo.png", logo_dark_url=legacy + "logo-dark.png") + return Brand(raw, brand.path) + + +# ------------------------------------------------------------------ G/H historical own-site sources (issue 47, r1 finding P2) +# The context construction below is copied locally from tests/test_footer_legacy_gh.py (no test module imports another). +GH_PARENT = {"full_name": "someone/upstream", "default_branch": "main"} +GH_VARIANTS = {"G": dict(fork=True, parent=GH_PARENT), "H": dict(repo="host-backup-20260914")} +GH_UPSTREAM = "# Upstream project\n\nAuthored upstream text, kept as written.\n\n## Usage\n\nRun it.\n" +WEBSITE_LOGO = "https://www.complete.tech/wp-content/uploads/logo.png" + + +def gh_ctx(brand, variant, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict(**GH_VARIANTS[variant])), tmp_path) + assert ctx.det["variant"] == variant + return ctx + + +@pytest.mark.parametrize("src", [WEBSITE_LOGO, "docs/images/logo.svg"], ids=["website-logo", "docs-svg"]) +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_gh_unmarked_own_site_footer_migrates_to_one_compliant_footer(brand, tmp_path, variant, src): + ctx = gh_ctx(brand, variant, tmp_path) + before = GH_UPSTREAM + "\n" + unmarked(src) + "\n" + assert footer.check(brand, before)[0] != "pass" + after, removed = readme.upgrade(ctx, before) + assert src not in after and after.count("Built by") == 1 + assert footer.check(brand, after)[0] == "pass" + assert removed and readme.preservation(before, after, removed) == [] + assert GH_UPSTREAM.strip() in after + again, _ = readme.upgrade(gh_ctx(brand, variant, tmp_path), after) + assert again == after + + +@pytest.mark.parametrize("case", ["partner-file", "foreign-host", "look-alike-host", "other-domain", "wrong-alt"]) +@pytest.mark.parametrize("variant", ["G", "H"]) +def test_gh_unmarked_partner_or_foreign_footer_image_is_preserved(brand, tmp_path, variant, case): + src, alt = { + "partner-file": (NEGATIVE_SRCS[0], "CompleteTech LLC"), + "foreign-host": (NEGATIVE_SRCS[1], "CompleteTech LLC"), + "look-alike-host": (NEGATIVE_SRCS[2], "CompleteTech LLC"), + "other-domain": (NEGATIVE_SRCS[3], "CompleteTech LLC"), + "wrong-alt": (WEBSITE_LOGO, "Partner"), + }[case] + ctx = gh_ctx(brand, variant, tmp_path) + before = GH_UPSTREAM + "\n" + unmarked(src, alt=alt) + "\n" + after, removed = readme.upgrade(ctx, before) + assert image_lines(after, src) == image_lines(before, src) >= 1 + assert not [ln for ln in removed if src in ln], f"{src} was reported as removed" + assert readme.preservation(before, after, removed) == [] + + +# ---------------------------------------------------------------- bootstrap write path (issue 47, r1 finding P1) +EDITED_BODY = (f"{readme.start('template-body')}\n\n" + f"## About\n\nWritten by a person.\n{readme.end('template-body')}") + + +def legacy_brand_for(brand): + raw = copy.deepcopy(brand.raw) + base = "https://raw.githubusercontent.com/CompleteTech-LLC/.github/main/brand/" + raw["footer"].update(logo_light_url=base + "logo.png", logo_dark_url=base + "logo-dark.png") + return Brand(raw, brand.path) + + +def _bootstrap_cli(brand, tmp_path, footer_block): + """Run the real `readme --bootstrap --write` on an edited template body whose marked footer is `footer_block`.""" + import json + + from conftest import run_cli + + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + text = readme.scaffold(ctx).replace(footer.block(brand), footer_block) + "\n" + EDITED_BODY + "\n" + assert readme.bootstrap_state(text) == "edited" + meta = tmp_path / "meta.json" + meta.write_text(json.dumps(meta_dict()), encoding="utf-8") + path = tmp_path / "README.md" + path.write_bytes(text.encode("utf-8")) + return run_cli("--meta", meta, "--repo-dir", tmp_path, "readme", "--bootstrap", "--write"), path, text + + +def test_bootstrap_write_refuses_to_regenerate_a_marked_footer_with_a_partner_image(brand, tmp_path): + partner = footer.block(brand).replace(brand.raw["footer"]["logo_light_url"], "https://example.test/partner-badge.png") + p, path, text = _bootstrap_cli(brand, tmp_path, partner) + assert "https://example.test/partner-badge.png" in text + assert p.returncode == 1, p.stdout + p.stderr # CHECK_FAILED + assert "readme-shrink" in p.stdout + p.stderr + assert path.read_bytes() == text.encode("utf-8"), "a refused bootstrap write must leave README.md unchanged" + + +def test_bootstrap_write_fallback_still_works_for_a_plain_old_footer_copy(brand, tmp_path): + p, path, text = _bootstrap_cli(brand, tmp_path, footer.block(legacy_brand_for(brand))) + assert p.returncode == 0, p.stdout + p.stderr + after = path.read_text(encoding="utf-8") + assert "Written by a person." in after and "template-body:pristine" not in after + assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" + + +# ---------------------------------------------------------------- G/H historical own-site sources (copied context) +PARENT = {"full_name": "someone/upstream", "default_branch": "main"} +UPSTREAM = "# Upstream project\n\nAuthored upstream text, kept as written.\n\n## Usage\n\nRun it.\n" +GH_VARIANTS = {"G": dict(fork=True, parent=PARENT), "H": dict(repo="host-backup-20260914")} + + +def gh_ctx(brand, variant, tmp_path): + ctx = RepoContext(brand, normalize(meta_dict(**GH_VARIANTS[variant])), tmp_path) + assert ctx.det["variant"] == variant + return ctx + + +@pytest.mark.parametrize("variant", ["G", "H"]) +@pytest.mark.parametrize("src", POSITIVE_SRCS, ids=["website-logo", "docs-svg"]) +def test_gh_callout_only_migrates_an_unmarked_own_site_footer(brand, tmp_path, variant, src): + before = UPSTREAM + "\n" + unmarked(src) + "\n" + assert footer.check(brand, before)[0] != "pass" + after, removed = readme.upgrade(gh_ctx(brand, variant, tmp_path), before) + assert src not in after and after.count("Built by") == 1 + assert footer.check(brand, after)[0] == "pass" + assert UPSTREAM.strip() in after + assert readme.preservation(before, after, removed) == [] + + +@pytest.mark.parametrize("variant", ["G", "H"]) +@pytest.mark.parametrize("src", NEGATIVE_SRCS) +def test_gh_callout_only_keeps_partner_and_foreign_footer_images(brand, tmp_path, variant, src): + before = UPSTREAM + "\n" + unmarked(src) + "\n" + after, removed = readme.upgrade(gh_ctx(brand, variant, tmp_path), before) + assert image_lines(after, src) == image_lines(before, src) >= 1 + assert not [ln for ln in removed if src in ln], "preservation must report no allowance for the partner image" + assert readme.preservation(before, after, removed) == [] From 384f443084707190b5fa4f97598fe87c3c921419 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:31:49 -0400 Subject: [PATCH 36/44] ci(footer): run the online logo HEAD on pull requests; docs (#47) footer-online now runs footer --check README.md --online on pull_request, push, schedule and workflow_dispatch (contents: read, no continue-on-error); the PR-only local-file step stays as extra coverage. The refreshed acceptance overrides the older r2-authoritative-ci-criteria adjudication on PR online checks, so the renamed regression is a deliberate change, not a weakening. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/footer-assets.yml | 8 +++----- CHANGELOG.md | 3 ++- references/readme-variants.md | 2 +- tests/test_footer_fenced_example_migration.py | 8 ++++++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/footer-assets.yml b/.github/workflows/footer-assets.yml index 5e74ddf..ca459ae 100644 --- a/.github/workflows/footer-assets.yml +++ b/.github/workflows/footer-assets.yml @@ -2,10 +2,9 @@ # Fails on any result other than pass, e.g. when a footer logo URL returns 404 (issue 47, subissue 50). # Flag spelling confirmed with `python scripts/showcase.py footer --check --help`: `--check README.md --online` # HEADs both decision 18 raw logo URLs (on main) and exits non-zero unless both return 200. -# Triggers: push to main (a regression is caught as it lands), weekly schedule and manual dispatch run the online HEAD. -# A pull request cannot be judged by HEADing main, so it runs an offline step instead: the configured footer logo -# paths must exist in the PR checkout under assets/brand/. - +# Triggers: pull requests (footer-touching paths), push to main, weekly schedule and manual dispatch all run the online +# HEAD, so a PR cannot merge while a published logo URL returns an error. The PR-only step that checks the configured +# footer logo files exist in the checkout stays as extra coverage for a PR that adds or renames them. name: Footer assets on: @@ -49,5 +48,4 @@ jobs: run: | python -c "import pathlib, sys, yaml; from urllib.parse import urlparse; f = yaml.safe_load(open('references/brand.yaml', encoding='utf-8'))['footer']; urls = [f['logo_light_url'], f['logo_dark_url']]; bad = [u for u in urls if not pathlib.Path(*urlparse(u).path.split('/main/', 1)[1].split('/')).is_file()]; print('missing in checkout:', bad) if bad else print('footer logo files present:', urls); sys.exit(1 if bad else 0)" - name: Real HEAD against footer logo URLs - if: github.event_name != 'pull_request' run: python scripts/showcase.py footer --check README.md --online diff --git a/CHANGELOG.md b/CHANGELOG.md index 492756e..3b3b344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. +- The `Footer assets` workflow's `footer-online` job now sends the real HEAD (`footer --check README.md --online`) on footer-touching pull requests as well as on push to main, the weekly schedule and manual dispatch, with `contents: read` only and no `continue-on-error`, so a pull request fails while either published footer logo URL does not return 200. The PR-only check that the configured logo files exist in the checkout stays as extra coverage. This supersedes the earlier offline-only behaviour on pull requests. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. - Lint accepts file operands after options on older Python 3.12 versions while continuing to reject unrecognized options. @@ -24,7 +25,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon` uses the bundled `references/art-direction.md` in every working directory. The option A glyph template uses dedicated, contrasting `icon_ink` and `icon_spark` brand tokens and the documented family seed (or the tool's default when absent) without requiring private estate files; explicit art-direction overrides remain supported and missing paths are named in the error. - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. -- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A scheduled `Footer assets` workflow sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. +- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A `Footer assets` workflow (pull requests, push to main, weekly and manual) sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. - Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins even when it has no value (`alt alt="CompleteTech LLC"` reads as an empty alt, so the authored image is preserved), and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. - Own-site footer recognition is limited to the configured historical logo URLs on the company website host (`footer.legacy_logo_urls`), not any file on that host: a team photo or screenshot on the same host stays authored content and is kept through `readme --mode upgrade`. Migrating an unmarked footer reports as removed only the lines the new footer does not emit again, the same multiset rule as marked footers, so a lost banner anchor or other markup is no longer hidden by re-emitted footer lines and fails the never-drop-a-line check. No brand or decision change. diff --git a/references/readme-variants.md b/references/readme-variants.md index dceef50..d99dffe 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them); the `Footer assets` CI job runs it on footer-touching pull requests, pushes to main, weekly and on manual dispatch, so a pull request fails while either URL does not return 200). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does diff --git a/tests/test_footer_fenced_example_migration.py b/tests/test_footer_fenced_example_migration.py index e0fc743..6d89b9a 100644 --- a/tests/test_footer_fenced_example_migration.py +++ b/tests/test_footer_fenced_example_migration.py @@ -69,11 +69,15 @@ def test_workflow_runs_on_push_to_main_with_the_pull_request_path_filter(): assert triggers["schedule"] and "workflow_dispatch" in triggers -def test_online_head_does_not_run_on_pull_requests_and_the_offline_check_does(): +def test_online_head_runs_on_every_trigger_and_the_offline_file_check_also_runs_on_pull_requests(): wf, _ = workflow() steps = [s for job in wf["jobs"].values() for s in job["steps"]] online = [s for s in steps if "--online" in s.get("run", "")] - assert len(online) == 1 and online[0]["if"] == "github.event_name != 'pull_request'" + # the refreshed #47 acceptance requires the real HEAD on footer-touching PRs too (it overrides the older + # r2-authoritative-ci-criteria adjudication); the step must carry no condition that excludes pull_request + assert len(online) == 1 and "if" not in online[0] + assert "continue-on-error" not in online[0] and "continue-on-error" not in wf["jobs"]["footer-online"] + assert wf["permissions"] == {"contents": "read"} offline = [s for s in steps if "assets" not in s.get("run", "") and "brand.yaml" in s.get("run", "")] assert len(offline) == 1 and offline[0]["if"] == "github.event_name == 'pull_request'" # the offline step's command really passes against this checkout From 0f1eec84fa29e46c9ff4cde6692e579689b5f3be Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:40:05 -0400 Subject: [PATCH 37/44] ci(footer): restore offline-on-PR footer workflow and test; remove online-on-PR docs claims (#47) Reverts the invented online-HEAD-on-pull-request gate from 384f443 per the binding adjudications (issue #47/#50 acceptance: scheduled check or CI step, offline-friendly mock). Restores footer-assets.yml and the footer migration test to bec6391 content; docs state that pull requests run the offline file-existence step and the online HEAD runs on push to main, weekly and manual dispatch. Bootstrap-refusal, G/H own-site and legacy_logo_urls wording fixes are kept. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/footer-assets.yml | 8 +++++--- CHANGELOG.md | 3 +-- references/readme-variants.md | 2 +- tests/test_footer_fenced_example_migration.py | 8 ++------ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/footer-assets.yml b/.github/workflows/footer-assets.yml index ca459ae..5e74ddf 100644 --- a/.github/workflows/footer-assets.yml +++ b/.github/workflows/footer-assets.yml @@ -2,9 +2,10 @@ # Fails on any result other than pass, e.g. when a footer logo URL returns 404 (issue 47, subissue 50). # Flag spelling confirmed with `python scripts/showcase.py footer --check --help`: `--check README.md --online` # HEADs both decision 18 raw logo URLs (on main) and exits non-zero unless both return 200. -# Triggers: pull requests (footer-touching paths), push to main, weekly schedule and manual dispatch all run the online -# HEAD, so a PR cannot merge while a published logo URL returns an error. The PR-only step that checks the configured -# footer logo files exist in the checkout stays as extra coverage for a PR that adds or renames them. +# Triggers: push to main (a regression is caught as it lands), weekly schedule and manual dispatch run the online HEAD. +# A pull request cannot be judged by HEADing main, so it runs an offline step instead: the configured footer logo +# paths must exist in the PR checkout under assets/brand/. + name: Footer assets on: @@ -48,4 +49,5 @@ jobs: run: | python -c "import pathlib, sys, yaml; from urllib.parse import urlparse; f = yaml.safe_load(open('references/brand.yaml', encoding='utf-8'))['footer']; urls = [f['logo_light_url'], f['logo_dark_url']]; bad = [u for u in urls if not pathlib.Path(*urlparse(u).path.split('/main/', 1)[1].split('/')).is_file()]; print('missing in checkout:', bad) if bad else print('footer logo files present:', urls); sys.exit(1 if bad else 0)" - name: Real HEAD against footer logo URLs + if: github.event_name != 'pull_request' run: python scripts/showcase.py footer --check README.md --online diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3b344..002c09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. -- The `Footer assets` workflow's `footer-online` job now sends the real HEAD (`footer --check README.md --online`) on footer-touching pull requests as well as on push to main, the weekly schedule and manual dispatch, with `contents: read` only and no `continue-on-error`, so a pull request fails while either published footer logo URL does not return 200. The PR-only check that the configured logo files exist in the checkout stays as extra coverage. This supersedes the earlier offline-only behaviour on pull requests. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. - Lint accepts file operands after options on older Python 3.12 versions while continuing to reject unrecognized options. @@ -25,7 +24,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon` uses the bundled `references/art-direction.md` in every working directory. The option A glyph template uses dedicated, contrasting `icon_ink` and `icon_spark` brand tokens and the documented family seed (or the tool's default when absent) without requiring private estate files; explicit art-direction overrides remain supported and missing paths are named in the error. - CLI stdout and stderr are configured as UTF-8 before argument parsing, preserving their existing error handlers, so redirected Windows output preserves Unicode in label plans, raw footer markup, help and diagnostics without `UnicodeEncodeError`, and undecodable filenames still use the stream's original escape policy. - The Action summary and quality validator decode CLI results explicitly as UTF-8, preserving non-ASCII descriptions on Windows rather than corrupting them or failing with `UnicodeDecodeError`. -- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A `Footer assets` workflow (pull requests, push to main, weekly and manual) sends a real HEAD to both footer logo URLs (`footer --check README.md --online`) and fails on any result other than pass. +- Footer logo URLs now use the existing published masters in this skill instead of missing `.github/brand` files. README refresh safely migrates the former URLs without dropping authored partner credits; the decision 18 artwork, theme selection and credit remain unchanged. Fork and backup READMEs (variants G and H) migrate the former footer too, and a custom footer image is never treated as a footer copy because its alt text matches. A `Footer assets` workflow checks on pull requests (offline) that the configured logo files exist, and on push to main, the weekly schedule and manual dispatch sends a real HEAD to both footer logo URLs (`footer --check README.md --online`), failing on any result other than pass. - Footer image recognition reads whole attributes: only the exact attribute names `alt`, `src` and `srcset` count, the first occurrence of a name wins even when it has no value (`alt alt="CompleteTech LLC"` reads as an empty alt, so the authored image is preserved), and text inside another attribute's value or a name such as `data-alt` never authorizes an image, so a partner image cannot pass as the footer's logo. No brand or decision change. - Own-site footer recognition is limited to the configured historical logo URLs on the company website host (`footer.legacy_logo_urls`), not any file on that host: a team photo or screenshot on the same host stays authored content and is kept through `readme --mode upgrade`. Migrating an unmarked footer reports as removed only the lines the new footer does not emit again, the same multiset rule as marked footers, so a lost banner anchor or other markup is no longer hidden by re-emitted footer lines and fails the never-drop-a-line check. No brand or decision change. diff --git a/references/readme-variants.md b/references/readme-variants.md index d99dffe..da4a069 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them); the `Footer assets` CI job runs it on footer-touching pull requests, pushes to main, weekly and on manual dispatch, so a pull request fails while either URL does not return 200). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them); the `Footer assets` CI job runs the online HEAD on push to main, weekly and on manual dispatch; pull requests run the offline check that the configured logo files exist). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does diff --git a/tests/test_footer_fenced_example_migration.py b/tests/test_footer_fenced_example_migration.py index 6d89b9a..e0fc743 100644 --- a/tests/test_footer_fenced_example_migration.py +++ b/tests/test_footer_fenced_example_migration.py @@ -69,15 +69,11 @@ def test_workflow_runs_on_push_to_main_with_the_pull_request_path_filter(): assert triggers["schedule"] and "workflow_dispatch" in triggers -def test_online_head_runs_on_every_trigger_and_the_offline_file_check_also_runs_on_pull_requests(): +def test_online_head_does_not_run_on_pull_requests_and_the_offline_check_does(): wf, _ = workflow() steps = [s for job in wf["jobs"].values() for s in job["steps"]] online = [s for s in steps if "--online" in s.get("run", "")] - # the refreshed #47 acceptance requires the real HEAD on footer-touching PRs too (it overrides the older - # r2-authoritative-ci-criteria adjudication); the step must carry no condition that excludes pull_request - assert len(online) == 1 and "if" not in online[0] - assert "continue-on-error" not in online[0] and "continue-on-error" not in wf["jobs"]["footer-online"] - assert wf["permissions"] == {"contents": "read"} + assert len(online) == 1 and online[0]["if"] == "github.event_name != 'pull_request'" offline = [s for s in steps if "assets" not in s.get("run", "") and "brand.yaml" in s.get("run", "")] assert len(offline) == 1 and offline[0]["if"] == "github.event_name == 'pull_request'" # the offline step's command really passes against this checkout From a422ebc9af401fcd029c9772792e1179f0f7a2c3 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 21:54:50 -0400 Subject: [PATCH 38/44] test(footer): remove duplicate gh_ctx helper for ruff F811 tests/test_footer_upgrade_preservation.py defined gh_ctx twice (lines 253 and 343), failing ruff F811 in validate_quality. Both bodies were identical, so the second definition with its duplicate GH_VARIANTS, PARENT and UPSTREAM constants is removed. The G/H tests now use the shared GH_UPSTREAM constant. No test function, parametrization or assertion changed. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_upgrade_preservation.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/test_footer_upgrade_preservation.py b/tests/test_footer_upgrade_preservation.py index 28e1b68..b5af0cb 100644 --- a/tests/test_footer_upgrade_preservation.py +++ b/tests/test_footer_upgrade_preservation.py @@ -335,33 +335,22 @@ def test_bootstrap_write_fallback_still_works_for_a_plain_old_footer_copy(brand, # ---------------------------------------------------------------- G/H historical own-site sources (copied context) -PARENT = {"full_name": "someone/upstream", "default_branch": "main"} -UPSTREAM = "# Upstream project\n\nAuthored upstream text, kept as written.\n\n## Usage\n\nRun it.\n" -GH_VARIANTS = {"G": dict(fork=True, parent=PARENT), "H": dict(repo="host-backup-20260914")} - - -def gh_ctx(brand, variant, tmp_path): - ctx = RepoContext(brand, normalize(meta_dict(**GH_VARIANTS[variant])), tmp_path) - assert ctx.det["variant"] == variant - return ctx - - @pytest.mark.parametrize("variant", ["G", "H"]) @pytest.mark.parametrize("src", POSITIVE_SRCS, ids=["website-logo", "docs-svg"]) def test_gh_callout_only_migrates_an_unmarked_own_site_footer(brand, tmp_path, variant, src): - before = UPSTREAM + "\n" + unmarked(src) + "\n" + before = GH_UPSTREAM + "\n" + unmarked(src) + "\n" assert footer.check(brand, before)[0] != "pass" after, removed = readme.upgrade(gh_ctx(brand, variant, tmp_path), before) assert src not in after and after.count("Built by") == 1 assert footer.check(brand, after)[0] == "pass" - assert UPSTREAM.strip() in after + assert GH_UPSTREAM.strip() in after assert readme.preservation(before, after, removed) == [] @pytest.mark.parametrize("variant", ["G", "H"]) @pytest.mark.parametrize("src", NEGATIVE_SRCS) def test_gh_callout_only_keeps_partner_and_foreign_footer_images(brand, tmp_path, variant, src): - before = UPSTREAM + "\n" + unmarked(src) + "\n" + before = GH_UPSTREAM + "\n" + unmarked(src) + "\n" after, removed = readme.upgrade(gh_ctx(brand, variant, tmp_path), before) assert image_lines(after, src) == image_lines(before, src) >= 1 assert not [ln for ln in removed if src in ln], "preservation must report no allowance for the partner image" From 82694c17e228b8e484812efb0539c412c06b3f39 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 22:32:20 -0400 Subject: [PATCH 39/44] fix(readme): refuse authored footer in every bootstrap state (#47) The authored-footer safeguard only ran for an edited template body. A pristine or unmarked (none) README reached header_only(), which replaced the marked footer and silently deleted a partner or foreign image. Move the check into _authored_footer_refusal() and run it on the original text before dispatching on bootstrap_state(), so every state refuses before writing. The edited-branch guard reuses the same helper with its existing problem text and preservation details. Co-Authored-By: Claude Haiku 5.5 --- scripts/showcase.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/showcase.py b/scripts/showcase.py index a1735f1..1d8f536 100644 --- a/scripts/showcase.py +++ b/scripts/showcase.py @@ -375,6 +375,17 @@ def cmd_footer(args): return None, OK +def _authored_footer_refusal(ctx, text): + """[] when every marked footer block of `text` is a footer copy the footer refresh may replace; otherwise the + refusal (a partner or foreign image is authored content, and the refresh would remove it).""" + from ctshowcase import readme + for fm in readme.find_blocks(readme.MARK, readme.normalize_text(text)): + if fm.group(1) == "footer" and not readme.is_footer_copy(fm.group(2), ctx.brand, own_site=True): + return ["readme-shrink: the marked footer holds authored content (a partner or foreign image) that the " + "footer refresh would remove; README left unchanged"] + return [] + + def cmd_readme(args): from ctshowcase import readme, records ctx = context(args) @@ -396,6 +407,11 @@ def cmd_readme(args): if args.bootstrap: state = readme.bootstrap_state(before) removed = [] + # every state (pristine, edited, none) refuses before any write: the footer refresh below would replace it + problems = _authored_footer_refusal(ctx, before) + if problems: + return base("readme", ctx, result="fail", mode="bootstrap", problems=problems, + logs=ctx.logs + ["readme-shrink"]), CHECK_FAILED if state == "pristine": after = readme.bootstrap(ctx, before) cp = rd / "CONTRIBUTING.md" @@ -411,13 +427,11 @@ def cmd_readme(args): after, removed = readme.upgrade(ctx, base_text) if readme.preservation(base_text, after, removed): # a marked footer that holds authored content (partner or foreign image) must never be regenerated - for fm in readme.find_blocks(readme.MARK, base_text): - if fm.group(1) == "footer" and not readme.is_footer_copy(fm.group(2), ctx.brand): - return base("readme", ctx, result="fail", mode="bootstrap", - problems=["readme-shrink: the marked footer holds authored content (a partner or " - "foreign image) that the footer refresh would remove; README left " - "unchanged"] + readme.preservation(base_text, after, removed)[:19], - logs=ctx.logs + ["readme-shrink"]), CHECK_FAILED + refusal = _authored_footer_refusal(ctx, base_text) + if refusal: + return base("readme", ctx, result="fail", mode="bootstrap", + problems=refusal + readme.preservation(base_text, after, removed)[:19], + logs=ctx.logs + ["readme-shrink"]), CHECK_FAILED ctx.log("readme-shrink") after, removed = readme.header_only(ctx, base_text), [] else: From 34d6a219f7c2e129d88a02ec3229dca652a08cd0 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 22:33:11 -0400 Subject: [PATCH 40/44] test(footer): pin authored marked footer images in pristine and none bootstrap states (#47) Add CLI-level regression tests for `readme --bootstrap --write` where the marked footer holds an authored partner or foreign image (https://example.test/partner-badge.png and assets/partner.png) in the pristine and none states. They expect a readme-shrink refusal with README.md byte-identical; against the unfixed showcase.py they fail (the write succeeds and the partner image is replaced), which reproduces the P1 finding. Positive controls: an own-site legacy footer still migrates to the current brand logo in both states, and a current footer is idempotent across two runs. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_upgrade_preservation.py | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_footer_upgrade_preservation.py b/tests/test_footer_upgrade_preservation.py index b5af0cb..59c96fe 100644 --- a/tests/test_footer_upgrade_preservation.py +++ b/tests/test_footer_upgrade_preservation.py @@ -355,3 +355,65 @@ def test_gh_callout_only_keeps_partner_and_foreign_footer_images(brand, tmp_path assert image_lines(after, src) == image_lines(before, src) >= 1 assert not [ln for ln in removed if src in ln], "preservation must report no allowance for the partner image" assert readme.preservation(before, after, removed) == [] + + +# ---------------------------------------------------- bootstrap write: authored footer in every bootstrap state (issue 47, r2 P1) +# The edited-state guard is covered above. These cases pin the pristine and none states, where `bootstrap` and +# `header_only` regenerate the marked footer without any preservation check. Synthetic partner or foreign images only. +SAMPLE_BODY = "## About\n\nGenerated sections for the fixture.\n" + + +def _state_readme(brand, tmp_path, state, footer_block): + """The scaffolded README with `footer_block` as its marked footer, in the bootstrap `state` + ('pristine': a valid generated template body; 'none': no template-body marker at all).""" + ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) + head = readme.scaffold(ctx).replace(footer.block(brand), footer_block) + if state == "pristine": # the generated body sits above the marked footer, which stays the last block + head = head.replace(footer_block, readme.pristine_block(SAMPLE_BODY) + "\n\n" + footer_block, 1) + text = head + "\n" + assert readme.bootstrap_state(text) == state + return text + + +AUTHORED_FOOTER_CASES = [ + ("pristine", PARTNER_LIGHT, "pristine-partner-url"), + ("pristine", "assets/partner.png", "pristine-foreign-path"), + ("none", PARTNER_LIGHT, "none-partner-url"), + ("none", "assets/partner.png", "none-foreign-path"), +] + + +@pytest.mark.parametrize("state,src,name", AUTHORED_FOOTER_CASES, ids=[c[2] for c in AUTHORED_FOOTER_CASES]) +def test_bootstrap_write_refuses_authored_marked_footer_image_in_pristine_and_none_states(brand, tmp_path, state, src, + name): + partner = footer.block(brand).replace(brand.raw["footer"]["logo_light_url"], src) + assert src in partner + text = _state_readme(brand, tmp_path, state, partner) + p, path = run_bootstrap_write(tmp_path, text, name) + assert p.returncode == 1, (p.returncode, p.stdout, p.stderr) # CHECK_FAILED + assert "readme-shrink" in p.stdout + p.stderr + assert path.read_bytes() == text.encode("utf-8"), "a refused bootstrap write must leave README.md byte-identical" + assert src in path.read_text(encoding="utf-8") + + +@pytest.mark.parametrize("state", ["pristine", "none"]) +def test_bootstrap_write_migrates_own_site_legacy_footer_in_pristine_and_none_states(brand, tmp_path, state): + text = _state_readme(brand, tmp_path, state, footer.block(legacy_brand_for(brand))) + p, path = run_bootstrap_write(tmp_path, text, state) + assert p.returncode == 0, (p.returncode, p.stdout, p.stderr) + after = path.read_text(encoding="utf-8") + assert brand.raw["footer"]["logo_light_url"] in after + assert legacy_brand_for(brand).raw["footer"]["logo_light_url"] not in after + assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" + + +@pytest.mark.parametrize("state", ["pristine", "none"]) +def test_bootstrap_write_current_footer_is_idempotent_in_pristine_and_none_states(brand, tmp_path, state): + text = _state_readme(brand, tmp_path, state, footer.block(brand)) + p, path = run_bootstrap_write(tmp_path, text, state + "-first") + assert p.returncode == 0, (p.returncode, p.stdout, p.stderr) + first = path.read_bytes() + assert footer.check(brand, first.decode("utf-8"))[0] == "pass" + p2, path2 = run_bootstrap_write(tmp_path, first.decode("utf-8"), state + "-second") + assert p2.returncode == 0, (p2.returncode, p2.stdout, p2.stderr) + assert path2.read_bytes() == first From d17909e50e6bd68e71356545dbcb6c11ee44b8b6 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 22:34:10 -0400 Subject: [PATCH 41/44] docs: state authored-footer refusal applies to all bootstrap states (#47) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- references/readme-variants.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002c09f..9fef4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed -- `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. +- `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. The same refusal now applies in every bootstrap state (pristine and unmarked as well as edited), checked before any write, so a pristine or unmarked template can no longer silently replace a partner or foreign footer image. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. - Own-site historical footer logo recognition is URL-derived: the company-website logo lives in `footer.legacy_logo_urls` in `references/brand.yaml` (lock refreshed through `config --write-lock`) and matches only exactly (scheme, host case-insensitively, port, path; no userinfo), so no brand host or path remains in Python. The generic local `docs/images/logo.svg` migration is a file convention, not brand data, and stays distinct. - Windows contributor tests now use explicit UTF-8, native path basenames, a platform-independent read-only metadata stub, and Git Bash with stdin-safe Action scripts. All 15 issue #9 failures are individually documented; the full suite now runs in Windows, Linux and macOS CI. diff --git a/references/readme-variants.md b/references/readme-variants.md index da4a069..352f77a 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -244,7 +244,7 @@ Never modify, rename or delete an existing LICENSE, NOTICE or COPYING file. ``` -The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them); the `Footer assets` CI job runs the online HEAD on push to main, weekly and on manual dispatch; pull requests run the offline check that the configured logo files exist). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. +The logo files are the existing masters published under this skill's `assets/brand/`. The former `.github/main/brand/` publication paths remain in `footer.legacy_logo_urls` solely for safe migration of existing marked or unmarked footers; they are never emitted. Authored partner credits are not migration targets. An unmarked footer's logo is also recognized when it is a configured historical logo URL on the company website host (`footer.legacy_logo_urls` in `references/brand.yaml`, not any file on that host) or the local `docs/images/logo.svg`; fork (G) and backup (H) READMEs apply the same rule to their trailing unmarked footer. `readme --bootstrap --write` never regenerates a marked footer that holds authored content (a partner or foreign image), in any bootstrap state (pristine, edited or none): it fails with `readme-shrink` and leaves the README unchanged. Any other image, including other files on the company host, is authored content and is kept. Migrating an unmarked footer reports as removed only the old lines the new footer does not emit again, so the never-delete check still sees every other line. The 10 lines between the markers are byte-exact; `footer --check` reports `fail:missing`, `fail:not-last`, `fail:drift` or `fail:duplicate` (an old footer-like block, centred with "Built by" and complete.tech outside code, kept before the decision 18 footer), and `footer --check README.md --online` also sends a read-only HEAD to both raw logo URLs and fails (`fail:offline`) unless both return 200 (the Phase 4.0 hard gate before any README PR links them); the `Footer assets` CI job runs the online HEAD on push to main, weekly and on manual dispatch; pull requests run the offline check that the configured logo files exist). It comes after the License section. Scope: every in-scope README, T2 families included. Exceptions: watching-org forks (`exempt`; the watching org's `.github` gets the footer, and its other non-fork repositories are held, `hold:watching-non-fork`, until decided) and T3 until sign-off (refused). TouchedByAnAgent is included (decision 18); an org whose `footer` is set to `hold` in `brand.yaml` logs `hold`. Forks outside the watching org (G), archived repositories (H) and `.github` repositories (J) get the footer too. It is the only `` the skill emits. ## What `readme` does From 9757591da76032a8eb8f62f13bc96703b0925ad8 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 22:59:58 -0400 Subject: [PATCH 42/44] docs: footer logo image qualifies only by parsed src/srcset (#47) Line 255 (upgrade row) no longer lets a matching company alt qualify a footer logo image. Qualifying URLs are the brand footer logo URL, a footer.legacy_logo_urls entry, or docs/images/logo.svg under the company alt; alt alone never qualifies. Co-Authored-By: Claude Haiku 5.5 --- references/readme-variants.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/references/readme-variants.md b/references/readme-variants.md index 352f77a..2a178c2 100644 --- a/references/readme-variants.md +++ b/references/readme-variants.md @@ -252,7 +252,7 @@ The logo files are the existing masters published under this skill's `assets/bra |---|---| | `auto` | `scaffold` when there is no README or it has fewer than 3 non-blank lines; otherwise `upgrade` | | `scaffold` | Writes the variant's H2s in order. A required section is written only from repository facts (README fragments, manifests, `SKILL.md`, `.mmd` files); otherwise it is omitted and logged. The License body comes from the table above. | -| `upgrade` | The restructuring steps above, with the never-delete multiset check. The tagline block follows the `header` rule (with no description, a tagline written for the repository stays; only an inherited one is emptied, and the check does not count that dropped line as lost): the rebuilt header carries it, and on the preserve list (or over 15000 bytes) the block stays where it is. With no description, when upgrade rebuilds the header (not in preserve mode), a single bare `**...**` line opening the README after its header lines (H1, badges, banner, `---`) is the old tagline and moves into the block by the same rule; a second one, one beside a tagline block that already holds a line, a bold line inside a centred `
` or `

` block, any such line in preserve mode, and one in an untouched template copy whose template taglines are unknown (the rule would empty any line in the block there on the next run) stay as authored text. An old footer at the end of the README (a centred block whose every line is a footer line: centring, `
`/``, links to the configured footer host (`footer.href`), the footer's own logo image (its src or srcset is the brand footer logo URL, the same file on another branch, with a query, or in its github.com blob/raw form, or its alt is exactly the company; never an image because its file name contains "logo") and the "Built by" credit line, i.e. an unmarked copy of the footer or a drifted one) is replaced by the marked decision 18 footer, never kept beside it, on the preserve list too. A centred block with any other line (a partner or sponsor logo, any other image, a thanks line) is authored content and is kept; `lint readme` then reports `D18 footer fail:duplicate` if it reads like a footer | +| `upgrade` | The restructuring steps above, with the never-delete multiset check. The tagline block follows the `header` rule (with no description, a tagline written for the repository stays; only an inherited one is emptied, and the check does not count that dropped line as lost): the rebuilt header carries it, and on the preserve list (or over 15000 bytes) the block stays where it is. With no description, when upgrade rebuilds the header (not in preserve mode), a single bare `**...**` line opening the README after its header lines (H1, badges, banner, `---`) is the old tagline and moves into the block by the same rule; a second one, one beside a tagline block that already holds a line, a bold line inside a centred `

` or `

` block, any such line in preserve mode, and one in an untouched template copy whose template taglines are unknown (the rule would empty any line in the block there on the next run) stay as authored text. An old footer at the end of the README (a centred block whose every line is a footer line: centring, `
`/``, links to the configured footer host (`footer.href`), the footer's own logo image (it qualifies only by its parsed src or srcset: the brand footer logo URL (the same file on another branch, with a query, or in its github.com blob/raw form), a `footer.legacy_logo_urls` entry from references/brand.yaml, or the conventional local `docs/images/logo.svg` under the company alt; matching the company alt alone never qualifies, and neither does an image because its file name contains "logo") and the "Built by" credit line, i.e. an unmarked copy of the footer or a drifted one) is replaced by the marked decision 18 footer, never kept beside it, on the preserve list too. A centred block with any other line (a partner or sponsor logo, any other image, a thanks line) is authored content and is kept; `lint readme` then reports `D18 footer fail:duplicate` if it reads like a footer | | `header` | Touches the marker blocks only (what the Action runs). Authored text outside the blocks comes back byte-identical, fenced code included (a marker block, `` or template-body block shown inside fenced or inline code is an example, never regenerated); blank lines are tidied only inside regenerated blocks and where a block meets authored text (a removed block leaves one blank line). With no description the tagline block is kept as written (logged `needs-input:description`), except an inherited tagline, which is emptied: the block holds exactly a tagline of the template the repository was generated from (`meta` `template_taglines`: the template README's tagline block, the first one outside code, when the repository was generated and today), or, only when those are unknown, the template body is still pristine; never in the template repository itself (R2: the tagline is the description, never another repository's line), and the paragraph after `` (and its heading) is rewritten from the license table whenever it can be derived | | `--bootstrap` | Template only. The block between `` and `:end -->` starts with ``, the hash of the block's generated text. While the block still matches its hash byte for byte, bootstrap replaces it with the scaffolded variant body for this repository (each section holds only an HTML comment naming the facts it needs) under a new hash, rebuilds the header for the variant (Variant F: no H1; a CVE slug titles as `Acme CVE-2026-12345 Check`; the tagline block carries over and follows the `header` rule, so with no description a tagline written for the repository stays and only an inherited one is emptied), writes the license section from the table above (with or without a `LICENSE` file; a private repository reads "Proprietary...", a public Security repository gets `## Licensing`), and rewrites the owner tokens in `CONTRIBUTING.md` (only links to the template owner's `.github`; a link to another org's or an upstream's contributing guide or security policy stays as written). Because the hash still matches, a later run re-bootstraps when the type changes (the code landed, or `type:` was set in `.github/showcase.yml`). Once a person edits the block, the hash no longer matches: the marker is dropped (`bootstrap:edited`), the README goes through `upgrade` with the never-drop-a-line check, and the block is never rewritten again. A no-op when the markers are absent. | From 8c2ceabf228c9e3f67417bc7163b20d1f52fbb99 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 23:00:10 -0400 Subject: [PATCH 43/44] test: assert footer-assets workflow policy in public assets test (#47) test_scheduled_workflow_really_heads_the_footer_logos now asserts that the online HEAD step is guarded by `if: github.event_name != 'pull_request'`, that a pull_request trigger exists, and that the offline file-exists step is pull_request-only. Test-only change; no existing assertion is weakened. Co-Authored-By: Claude Haiku 5.5 --- tests/test_footer_public_assets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_footer_public_assets.py b/tests/test_footer_public_assets.py index fabdfb8..f1827df 100644 --- a/tests/test_footer_public_assets.py +++ b/tests/test_footer_public_assets.py @@ -99,6 +99,10 @@ def test_scheduled_workflow_really_heads_the_footer_logos(): steps = [s for job in wf["jobs"].values() for s in job["steps"]] online = [s for s in steps if s.get("run", "").strip() == "python scripts/showcase.py footer --check README.md --online"] assert len(online) == 1 + assert online[0].get("if") == "github.event_name != 'pull_request'" + assert "pull_request" in triggers + offline = [s for s in steps if "assets" not in s.get("run", "") and "brand.yaml" in s.get("run", "")] + assert len(offline) == 1 and offline[0].get("if") == "github.event_name == 'pull_request'" assert "continue-on-error" not in online[0] assert not any("continue-on-error" in job for job in wf["jobs"].values()) assert not re.search(r"\|\|\s*(true|:)", text) From d9d7ae7d9e2dc6f0048cb95c8fffc87c7eb01b27 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sat, 31 Oct 2026 23:00:32 -0400 Subject: [PATCH 44/44] test: normalize test_footer_r6_regressions.py to LF (#47) Co-Authored-By: Claude Sonnet 5.5 --- tests/test_footer_r6_regressions.py | 130 ++++++++++++++-------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/tests/test_footer_r6_regressions.py b/tests/test_footer_r6_regressions.py index 1485bf3..6d3417e 100644 --- a/tests/test_footer_r6_regressions.py +++ b/tests/test_footer_r6_regressions.py @@ -1,65 +1,65 @@ -"""Round r6 footer findings: unmarked old footers, local logos, and the never-drop-a-line guard.""" -import pytest - -from conftest import meta_dict -from ctshowcase import footer, readme -from ctshowcase.context import RepoContext -from ctshowcase.meta import normalize - -CREDIT = ('Built by CompleteTech LLC · Innovation at Every Integration' - "") - - -def unmarked(src, alt="CompleteTech LLC"): - return ('

%s
%s

' - % (src, alt, CREDIT)) - - -def picture(light, dark): - return ('

\n \n' - ' \n' - ' CompleteTech LLC\n
\n %s\n

\n' - % (dark, light, CREDIT)) - - -@pytest.mark.parametrize("src", ["https://www.complete.tech/wp-content/uploads/logo.png", "docs/images/logo.svg"]) -def test_unmarked_old_footer_with_company_alt_is_replaced_by_upgrade(brand, tmp_path, src): - ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) - before = readme.scaffold(ctx).replace(footer.block(brand), unmarked(src)) - assert src in before and "showcase:footer" not in before - after, removed = readme.upgrade(ctx, before) - assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" - assert readme.preservation(before, after, removed) == [] - - -def test_strict_default_keeps_a_company_alt_partner_image(brand): - text = "# A\n\n" + unmarked("docs/images/logo.svg") - assert readme.drop_old_footer(text, brand) == (text, []) - assert readme.drop_old_footer(text, brand, loose_src=True)[1] - - -def test_picture_footer_with_old_local_files_is_replaced(brand, tmp_path): - block = picture("assets/logo.png", "assets/logo-dark.png") - assert readme.is_footer_copy(block, brand) - assert not readme.is_footer_copy(picture("assets/partner.png", "assets/logo-dark.png"), brand) - assert not readme.is_footer_copy(block.replace('alt="CompleteTech LLC"', 'alt="Partner"'), brand) - ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) - before = readme.scaffold(ctx).replace(footer.block(brand), block) - after, removed = readme.upgrade(ctx, before) - assert after.count("Built by") == 1 and footer.check(brand, after)[0] == "pass" - assert readme.preservation(before, after, removed) == [] - - -AUTHORED = '

\nDiagram\n
\n

\n\n' - - -def test_compliant_footer_adds_no_removed_lines_and_a_lost_br_still_fails(brand, tmp_path): - ctx = RepoContext(brand, normalize(meta_dict()), tmp_path) - scaffold = readme.scaffold(ctx) - before = scaffold.replace(footer.block(brand), AUTHORED + footer.block(brand)) - after, removed = readme.upgrade(ctx, before) - footer_lines = {ln.strip() for ln in footer.markup(brand).splitlines() if ln.strip()} - assert not [ln for ln in removed if ln.strip() in footer_lines] # header lines may be reported, footer lines not - lost = after.replace("
\n

\n\n