Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 88 additions & 35 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,54 +1,107 @@
name: Build Docs

on:
release:
types: [published]
workflow_dispatch:
pull_request:
paths: ['src/**', 'docs/**', 'tools/*docs.php', 'phpdoc.dist.xml', '.github/workflows/docs.yml']
push:
branches: [main]
paths: ['src/**', 'docs/**', 'tools/*docs.php', 'phpdoc.dist.xml', '.github/workflows/docs.yml']
workflow_dispatch:

permissions:
contents: write
contents: read

concurrency:
group: documentation-${{ github.ref }}
cancel-in-progress: false

jobs:
docs:
runs-on: ubuntu-latest
# A published release, a manual dispatch, or a push whose head commit message
# contains "build docs" - the convention DiscordPHP uses, so the site is not
# rebuilt and redeployed on every commit.
if: >-
github.event_name == 'release'
|| github.event_name == 'workflow_dispatch'
|| (github.event_name == 'push' && contains(github.event.head_commit.message, 'build docs'))
build:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Checkout source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@db91e1a0e48e84637d325a7ee4d2677e146ceec4 # 2.36.0
with:
php-version: '8.4'
extensions: iconv, mbstring
tools: composer

# The DiscordPHP family's build of phpDocumentor, carrying its patches for
# `?T|null` types. Not on Packagist, hence the explicit repository.
- name: Install phpDocumentor
coverage: none
tools: composer:2.9.8
- name: Install local PlantUML and Graphviz for the class diagram
run: |
sudo apt-get update -qq
sudo apt-get install --yes --no-install-recommends graphviz plantuml
- name: Checkout pinned phpDocumentor toolchain
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: discord-php/phpdoc-tool
ref: 40e5911a592e5fc4407096cebd5fb4aaf2e9df6f
path: phpdoc-tool
persist-credentials: false
- name: Install locked toolchain and reviewed nullable-type patches
run: >-
composer create-project discord-php/phpdoc-tool:^1.0 phpdoc-tool
--no-interaction --no-progress
--repository='{"type":"vcs","url":"https://github.com/discord-php/phpdoc-tool"}'

- name: Build the reference
run: php tools/build-docs.php

- name: Disable Jekyll
# Without this, GitHub Pages hides every path beginning with an underscore.
run: touch build/.nojekyll
composer install --working-dir=phpdoc-tool --no-dev --no-interaction
--no-scripts --prefer-dist --no-progress
- name: Generate and validate public output
env:
PHPDOC_PLANTUML: plantuml
PHPDOC_PLANTUML_BIN: /usr/bin/plantuml
run: |
php tools/build-docs.php
php tools/check-docs.php
- name: Archive only generated public files
run: tar --directory build -cf "$RUNNER_TEMP/artifact.tar" .
- name: Retain Pages artifact for one day
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: github-pages
path: ${{ runner.temp }}/artifact.tar
retention-days: 1
if-no-files-found: error

- name: Publish to gh-pages
uses: JamesIves/github-pages-deploy-action@v4
publish:
needs: build
if: >-
github.event_name != 'pull_request' &&
github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
runs-on: ubuntu-24.04
timeout-minutes: 8
permissions:
contents: write # GitHub cannot branch-scope this; only generated gh-pages is written.
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Checkout repository metadata for gh-pages publisher
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Download this run's generated site
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: github-pages
path: pages-artifact
- name: Restore the validated site
run: |
mkdir site
tar -xf pages-artifact/artifact.tar --directory site
- name: Preserve generated gh-pages copy
uses: JamesIves/github-pages-deploy-action@fa24774553152dd7873cd16ebd8d959b010c5445 # v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
folder: build
folder: site
clean: true
clean-exclude: CNAME
# GITHUB_TOKEN pushes do not themselves trigger a branch Pages build.
# Explicitly deploy the same artifact instead of assuming gh-pages is live.
- name: Publish the exact Pages artifact
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ reachable from PHP.

## Requirements

[Documentation and guides](https://valgorithms.github.io/YoutubePHP/) ·
[GitHub Actions uploader setup](https://valgorithms.github.io/YoutubePHP/guides/github-actions-uploader.html)

- PHP 8.4 or newer
- ext-json, ext-mbstring

Expand Down
2 changes: 1 addition & 1 deletion docs/github-actions-uploader.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ schedule. A PR producing a video must never receive the channel credentials.
YouTubePHP supplies the API client, OAuth refresh and multipart media calls. It
does **not** supply Google consent, GitHub environment configuration, artifact
provenance, human content approval or a durable upload journal. Those are the
repository integration's responsibility. The [small PHP example](../examples/ReviewedUpload.php)
repository integration's responsibility. The [small PHP example](https://github.com/Valgorithms/YoutubePHP/blob/main/examples/ReviewedUpload.php)
is tested with a fake transport; it deliberately has no executable CLI or default
journal. The [Civ13 example](civ13-uploader-example.md) links a complete, separately
tested integration, including its controller and setup helper. **Copying the two
Expand Down
15 changes: 15 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# YouTubePHP guides

YouTubePHP is an asynchronous ReactPHP client for the YouTube Data API v3.
The generated class reference documents the API surface. These guides explain
how to connect it to a reviewed application.

- [GitHub Actions uploader setup](github-actions-uploader.md): Google consent,
protected credentials, workflow templates, library calls and required controller
safeguards for a manually approved video upload.
- [Civ13 uploader case study](civ13-uploader-example.md): the exact tested source,
successful run, durable receipt and fixes behind a working deployment.
- [Building and publishing these docs](publishing.md): the pinned phpDocumentor
toolchain, local validation and bounded GitHub Pages deployment.

Source and issues: [Valgorithms/YoutubePHP](https://github.com/Valgorithms/YoutubePHP).
57 changes: 57 additions & 0 deletions docs/publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Building and publishing the documentation

`phpdoc.dist.xml` builds the API reference from `src/` and Markdown guides from
`docs/`. Only the generated `build/` directory is published. No credentials,
client JSON, source dependencies or private evidence trees belong in that output.

The existing DiscordPHP-family phpDocumentor toolchain is pinned in the workflow
to `40e5911a592e5fc4407096cebd5fb4aaf2e9df6f`, with its own committed Composer lock
and reviewed nullable-type patches. Its Composer patch plugin is intentional;
the uploader consumer's no-plugin installation policy is a different project.
Graphviz `dot` and local PlantUML must be installed for the generated class diagram.
Set `PHPDOC_PLANTUML=plantuml` and `PHPDOC_PLANTUML_BIN` to the local PlantUML
executable (CI uses `/usr/bin/plantuml`). The Composer distribution does not bundle
the binary assumed by its default renderer. No remote diagram service is used.
The configuration
also explicitly sets `graphs.enabled=true`; phpDocumentor disables graph rendering
by default even though its default template emits a diagram-page link.

For a local build, install that toolchain in `phpdoc-tool/` as its own project,
or point `PHPDOC` at an existing trusted toolchain's `vendor/bin/phpdoc`. Then run:

```bash
php tools/build-docs.php
php tools/check-docs.php
php -S 127.0.0.1:8000 -t build
```

The checker verifies required API/guide pages, all local href/src targets with
HTML base semantics, output size (32 MiB), regular files, and absence of common
private-file/diagnostic/token patterns. These are bounded smoke checks, not a
claim that an arbitrary secret is detectable. Review source content before merging.
The docs builder keeps tool diagnostics out of public HTML buffers.

The Build Docs workflow validates PRs without deploying. Default-branch docs/source
pushes and manual default-branch dispatches generate and validate the site, retain
one artifact for one day, then publish that same artifact. Builds use read-only
repository permissions; only the default-branch publish job receives Contents
write (for `gh-pages`), Pages write and the OIDC token used by `deploy-pages`.
Jobs use standard public Ubuntu runners, timeouts, no schedule or paid service.

The `gh-pages` branch contains generated files. `CNAME`, if present, is preserved;
do not add unrelated content to a generated branch. No custom domain is configured
by this change. GitHub-token pushes alone do not trigger branch-based Pages builds,
so the workflow also uses GitHub's Pages artifact deployment explicitly. Configure
the repository's Pages publishing source as **GitHub Actions**. The Pages settings
and `github-pages` environment are separate from any YouTube upload environment;
this workflow never accesses Google credentials or changes upload gates.

Before calling the site live, verify the deployment's source revision, successful
publish job, configured Pages URL and anonymous responses for the landing page,
API reference, uploader guide and case study. Source generation alone is not a
live deployment. The default address is
`https://valgorithms.github.io/YoutubePHP/`.

References: [phpDocumentor configuration](https://docs.phpdoc.org/guide/references/configuration.html),
[custom Pages workflows](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages),
[Pages publishing source](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site).
9 changes: 8 additions & 1 deletion phpdoc.dist.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
<source dsn=".">
<path>src</path>
</source>
<output>reference</output>
<output>.</output>
</api>
<guide format="md">
<source dsn=".">
<path>docs</path>
</source>
<output>guides</output>
</guide>
</version>
<setting name="graphs.enabled" value="true"/>
</phpdocumentor>
84 changes: 84 additions & 0 deletions tests/DocsOutputTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/*
* This file is a part of the YouTubePHP project.
*
* Copyright (c) 2026-present Valithor Obsidion <valithor@valgorithms.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/

namespace YouTube\Tests;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

require_once dirname(__DIR__) . '/tools/check-docs.php';

final class DocsOutputTest extends TestCase
{
private string $root;

protected function setUp(): void
{
$path = sys_get_temp_dir() . '/youtubephp-docs-' . bin2hex(random_bytes(8));
mkdir($path);
$this->root = realpath($path);
mkdir($this->root . '/guides');
mkdir($this->root . '/images');
foreach (['index.html', 'guides/page.html', 'images/icon.png'] as $file) {
file_put_contents($this->root . '/' . $file, 'offline fixture');
}
}

protected function tearDown(): void
{
foreach (['index.html', 'guides/page.html', 'images/icon.png'] as $file) {
unlink($this->root . '/' . $file);
}
rmdir($this->root . '/guides');
rmdir($this->root . '/images');
rmdir($this->root);
}

public function testPhpDocumentorBaseResolvesAssetsAndRootNavigation(): void
{
docsCheckLinks(
$this->root,
$this->root . '/guides/page.html',
'<html><head><base href="../"></head><body><img src="images/icon.png"><a href="index.html#anchor">Home</a></body></html>',
);
$this->assertSame($this->root, docsLocalTarget($this->root, $this->root . '/guides', '../'));
}

public function testWithoutBaseLinksRemainRelativeToDocument(): void
{
docsCheckLinks(
$this->root,
$this->root . '/guides/page.html',
'<html><body><img src="../images/icon.png"><a href="#anchor">Here</a></body></html>',
);
$this->assertSame(
$this->root . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'icon.png',
docsLocalTarget($this->root, $this->root . '/guides', '../images/icon.png'),
);
}

public static function invalidCases(): array
{
return [
['<base href="../"><img src="images/missing.png">'],
['<base href="../../"><img src="images/icon.png">'],
['<base href="https://example.invalid/"><img src="images/icon.png">'],
['<base href="../"><a href="%2e%2e/">escape</a>'],
];
}

#[DataProvider('invalidCases')]
public function testBrokenAssetsAndEscapingBasesStillFail(string $markup): void
{
$this->expectException(\RuntimeException::class);
docsCheckLinks($this->root, $this->root . '/guides/page.html', '<html><body>' . $markup . '</body></html>');
}
}
9 changes: 8 additions & 1 deletion tools/build-docs.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,14 @@
exit(1);
}

$arguments = array_map('escapeshellarg', [PHP_BINARY, $binary, '--config', $root . '/phpdoc.dist.xml', '--no-interaction']);
// Template output buffers must not include local diagnostic paths/stack traces.
// The pinned league/uri tool emits PHP 8.5 deprecations; other errors stay visible.
$arguments = array_map('escapeshellarg', [
PHP_BINARY,
'-d', 'display_errors=stderr',
'-d', 'error_reporting=' . (E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED),
$binary, '--config', $root . '/phpdoc.dist.xml', '--no-interaction',
]);

fwrite(STDERR, "Building the reference...\n");
passthru(implode(' ', $arguments), $status);
Expand Down
Loading
Loading