From 0bbac81492ac915f0e24fc75b0950a7c04fa5a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 13:59:44 -0300 Subject: [PATCH 1/3] PHPAY-124: chore(ci): check the documentation links in the gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documentation is 17 files since the README was split, and the links between them are relative. A relative link breaks in silence: markdown does not complain, PHPStan does not look at it, and GitHub only shows the 404 to whoever clicks. The script checks that every relative link points at something that exists, and that every #anchor matches a heading that really produces it. A heading inside a code block does not count — that is exactly how the unclosed fence in the Cielo example stayed hidden for months, taking ten links of the table of contents with it, and a checker that ignored fences would have said everything was fine. It runs as composer test:links, inside composer test, and as a step of the quality job. Verified to fail: a file that does not exist and an anchor that does not exist both come back with the file and line, and the exit code is 1. --- .github/scripts/check-links.php | 177 ++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 6 ++ composer.json | 4 +- 3 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .github/scripts/check-links.php diff --git a/.github/scripts/check-links.php b/.github/scripts/check-links.php new file mode 100644 index 0000000..0cc40bf --- /dev/null +++ b/.github/scripts/check-links.php @@ -0,0 +1,177 @@ + número da linha => conteúdo + */ +function linhasForaDeBloco(string $arquivo): array +{ + $fora = []; + $cerca = false; + + foreach (explode("\n", (string) file_get_contents($arquivo)) as $i => $linha) { + if (str_starts_with(ltrim($linha), '```')) { + $cerca = !$cerca; + + continue; + } + + if (!$cerca) { + $fora[$i + 1] = $linha; + } + } + + return $fora; +} + +/** + * âncoras que os títulos daquele arquivo geram. + * + * @param string $arquivo + * @return array + */ +function ancorasDe(string $arquivo): array +{ + $ancoras = []; + + foreach (linhasForaDeBloco($arquivo) as $linha) { + if (preg_match('/^#{1,6} (.+)$/u', $linha, $m) === 1) { + $ancoras[] = ancoraDe($m[1]); + } + } + + return $ancoras; +} + +/* todo markdown do repositório, menos o que não é nosso */ +$arquivos = []; +$arvore = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator($raiz, FilesystemIterator::SKIP_DOTS), + static fn (SplFileInfo $item): bool => !in_array($item->getFilename(), $pular, true) + ) +); + +foreach ($arvore as $item) { + if ($item instanceof SplFileInfo && $item->getExtension() === 'md') { + $arquivos[] = $item->getPathname(); + } +} + +sort($arquivos); + +$cache = []; +$conferidos = 0; +$quebrados = []; + +foreach ($arquivos as $arquivo) { + foreach (linhasForaDeBloco($arquivo) as $numero => $linha) { + if (preg_match_all('/\[[^\]]*\]\(([^)\s]+)\)/u', $linha, $achados) === 0) { + continue; + } + + foreach ($achados[1] as $destino) { + if (preg_match('#^(https?|mailto|tel):#i', $destino) === 1) { + continue; + } + + $conferidos++; + [$caminho, $ancora] = array_pad(explode('#', $destino, 2), 2, ''); + $relativo = str_replace($raiz . '/', '', $arquivo); + + if ($caminho === '') { + $alvo = $arquivo; + } else { + $alvo = realpath(dirname($arquivo) . '/' . $caminho); + + if ($alvo === false) { + $quebrados[] = sprintf('%s:%d %s — não existe', $relativo, $numero, $destino); + + continue; + } + + if (!str_starts_with($alvo, $raiz)) { + $quebrados[] = sprintf('%s:%d %s — sai do repositório', $relativo, $numero, $destino); + + continue; + } + } + + if ($ancora === '' || !str_ends_with($alvo, '.md')) { + continue; + } + + $cache[$alvo] ??= ancorasDe($alvo); + + if (!in_array($ancora, $cache[$alvo], true)) { + $quebrados[] = sprintf( + '%s:%d %s — nenhum título gera essa âncora em %s', + $relativo, + $numero, + $destino, + str_replace($raiz . '/', '', $alvo) + ); + } + } + } +} + +printf("%d arquivos, %d links relativos conferidos\n", count($arquivos), $conferidos); + +if ($quebrados !== []) { + printf("\n%d quebrado(s):\n", count($quebrados)); + + foreach ($quebrados as $quebrado) { + printf(" %s\n", $quebrado); + } + + exit(1); +} + +echo "nenhum link quebrado\n"; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98e3c18..da590ce 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,6 +43,12 @@ jobs: - name: Static analysis (PHPStan) run: composer test:types + # a documentação são 17 arquivos com link relativo entre eles; link + # quebrado não aparece em teste nem em análise estática, só no 404 de + # quem clicar. + - name: Links da documentação + run: composer test:links + test: name: PHP ${{ matrix.php }} - ${{ matrix.dependencies }} runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 6310bb9..bda77a9 100644 --- a/composer.json +++ b/composer.json @@ -73,10 +73,12 @@ "test:lint": "pint --test -v", "test:unit": "pest --colors=always", "test:types": "phpstan analyse", + "test:links": "@php .github/scripts/check-links.php", "test": [ "@test:lint", "@test:unit", - "@test:types" + "@test:types", + "@test:links" ] } } From 7e2dc006ebd6e5071c29c977d4af35258e14cbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 13:59:44 -0300 Subject: [PATCH 2/3] PHPAY-124: doc: record the documentation split in the changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Não lançado section had the CI work from PHPAY-111 but not the README split, which is the change a user can feel: an external link to an anchor of the old README stopped working. Also recorded from the same PR: the capability matrix gaining the webhook signature column it had been missing since v2.3.0, the code fence that made the README render inverted from Rede down to Efí, and the dead link in SUPPORT.md. --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 623d364..7334c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,32 @@ O de-para completo da v1 para a v2, quebra por quebra, está no ([#111], [#112]). - Cobertura de testes medida no CI, num job só, com o número no log e no resumo do job. Sem mínimo que reprove o build ([#111], [#112]). +- Verificador de links da documentação no gate, em `composer test:links`. Ele + confere que todo link relativo aponta para arquivo que existe e que toda + âncora corresponde a um título de verdade — título dentro de bloco de código + não conta, que foi como o erro abaixo passou tanto tempo escondido + ([#124]). + +### Alterado + +- **A documentação saiu do README para `docs/`**, uma página por conceito e uma + por gateway, com índice. O README fica com o essencial: o que é, a matriz de + capacidades, requisitos, instalação e início rápido — de 46 KB para 11 KB. + **Link externo que apontava para âncora do README deixa de funcionar**; o + índice de [`docs/`](docs/) cobre todos os destinos ([#113], [#114]). +- A matriz de capacidades ganhou a coluna de **assinatura de webhook**, que + entrou na v2.3.0 e tinha ficado fora da tabela — quem lia era informado de + que a biblioteca fazia menos do que faz ([#113], [#114]). + +### Corrigido + +- **O README renderizava errado da Rede até a Efí.** O bloco de código do + exemplo de recorrência da Cielo nunca era fechado, e cercas alternam: o + título `### Rede` e a prosa embaixo dele saíam como código, os exemplos + seguintes saíam como prosa. Por causa disso, **dez links do sumário** + apontavam para âncoras que não existiam ([#113], [#114]). +- O link para o README no `.github/SUPPORT.md` apontava para `.github/README.md` + ([#113], [#114]). ## [2.3.0] - 2026-10-22 @@ -438,3 +464,6 @@ Primeira versão publicada no Packagist, sob licença MIT. [#102]: https://github.com/phpay-io/phpay/pull/102 [#111]: https://github.com/phpay-io/phpay/issues/111 [#112]: https://github.com/phpay-io/phpay/pull/112 +[#113]: https://github.com/phpay-io/phpay/issues/113 +[#114]: https://github.com/phpay-io/phpay/pull/114 +[#124]: https://github.com/phpay-io/phpay/issues/124 From 683a1a5a09daa9b30f7b347027e119d07d2dbb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Lucas?= Date: Thu, 22 Oct 2026 14:00:45 -0300 Subject: [PATCH 3/3] PHPAY-124: doc: list test:links among the commands CLAUDE.md described composer test as lint + unit + types and did not mention the new script. It is the file an agent reads before touching this repository, so a stale command list there is worse than in the README. --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6f9eb3a..e840a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,10 +89,11 @@ Rodar direto no host (requer PHP 8.2+ e Composer): ```bash composer install -composer test # lint + unit + types — mesmo gate do CI +composer test # lint + unit + types + links — mesmo gate do CI composer test:lint # pint --test (PSR-12 + regras do pint.json) composer test:unit # pest composer test:types # phpstan (nível e phpVersion vêm do phpstan.neon) +composer test:links # links relativos e âncoras dos markdowns (.github/scripts/check-links.php) composer lint # pint -v (corrige) ```