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
4 changes: 3 additions & 1 deletion .github/workflows/cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ jobs:
timeout-minutes: 20
permissions:
contents: read
environment: docs-live
environment:
name: docs-live
deployment: false
steps:
- name: Require the trusted publisher main branch
run: test "$GITHUB_REPOSITORY" = msgspec/docs-deploy && test "$GITHUB_REF" = refs/heads/main
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
contents: read
environment:
name: docs-preview
url: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
deployment: false
concurrency:
group: docs-preview-${{ needs.metadata.outputs.branch }}
cancel-in-progress: true
Expand Down Expand Up @@ -156,7 +156,7 @@ jobs:
contents: write
environment:
name: docs-live
url: https://msgspec.dev
deployment: false
concurrency:
group: docs-production
cancel-in-progress: false
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Create this repository as public `msgspec/docs-deploy`, with `main` as its defau

Configure a GitHub App installed only on `msgspec/msgspec` and `msgspec/docs-deploy`. Its maximum repository permissions are Actions write, Checks write, Contents read, Pull requests read, and the required Metadata read permission. Do not grant Contents write, Workflows write, Secrets, Administration, or other permissions. Keep the App's private key only in the Worker and this trusted publisher; never store it in source CI. The Worker dispatches publication and reports its check after validating the result. The publisher narrows each minted token to the source repository and Actions read, Contents read, and Pull requests read. This token cannot write publisher code or source code. Only the production job's own `GITHUB_TOKEN` can write the publisher snapshot.

Set repository variable `DOCS_APP_ID` and repository secret `DOCS_APP_PRIVATE_KEY`. Create environments `docs-preview` and `docs-live`, and restrict both environments to deployments from `main`. In each environment, set secrets `CF_ACCOUNT_ID` and `CF_PAGES_TOKEN`. Scope the Cloudflare token to the intended account and Pages publishing capability. Confirm that the existing Pages project is `msgspec`, its production branch is `cf-pages`, and its production domain is `https://msgspec.dev`.
Set repository variable `DOCS_APP_ID` and repository secret `DOCS_APP_PRIVATE_KEY`. Create environments `docs-preview` and `docs-live`, and restrict both environments to jobs from `main`. In each environment, set secrets `CF_ACCOUNT_ID` and `CF_PAGES_TOKEN`. Scope the Cloudflare token to the intended account and Pages publishing capability. Confirm that the existing Pages project is `msgspec`, its production branch is `cf-pages`, and its production domain is `https://msgspec.dev`.

The preview, production, and cleanup jobs use named environments with `deployment: false` to avoid creating GitHub deployment history while retaining environment secrets and branch policies. Preview URLs and publication results remain available through the Worker's independent `Docs preview` check and validated result artifact. Custom deployment protection Apps require GitHub deployment objects and cannot be enabled on these environments. See [GitHub's environment configuration documentation](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/control-deployments#using-environments-without-deployments).

The source integration must invoke `.github/workflows/docs-pr.yml` for pull requests and pushes to `main`. It must upload exactly one artifact named `docs-site`, with a root `index.html`, after a successful build. Use artifact overwrite on full reruns so that the latest build replaces earlier data. A rerun of failed link checks may reuse a build artifact from an earlier attempt of the same run and SHA, provided the latest attempt succeeds. Keep the source artifact's retention at one day. Missing or expired artifacts cause an actionable failure; the publisher never selects another source run as a fallback.

Expand Down
35 changes: 34 additions & 1 deletion tests/test_workflow_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_cleanup_requires_expected_publisher_main_and_protected_environment(self
"repository: msgspec/docs-deploy\n ref: main", self.workflow
)
self.assertIn("persist-credentials: false", self.workflow)
self.assertIn("environment: docs-live", self.workflow)
self.assertIn("environment:\n name: docs-live", self.workflow)
self.assertIn("group: docs-cleanup\n cancel-in-progress: false", self.workflow)

def test_cleanup_token_is_read_only_source_scoped_and_cf_is_environment_scoped(
Expand All @@ -121,5 +121,38 @@ def test_cleanup_cannot_change_the_publication_result(self):
self.assertNotIn("docs-deployment", self.workflow)


class EnvironmentPolicyTests(unittest.TestCase):
def test_all_environment_jobs_keep_named_access_without_deployment_history(self):
expected = {
"publish.yml": {"preview": "docs-preview", "production": "docs-live"},
"cleanup.yml": {"cleanup": "docs-live"},
}
for filename, environments in expected.items():
workflow = (ROOT / ".github/workflows" / filename).read_text()
jobs = dict(
re.findall(
r"(?ms)^ ([a-z]+):\n(.*?)(?=^ [a-z]+:|\Z)",
workflow.split("\njobs:\n", 1)[1],
)
)
actual = {}
for job, content in jobs.items():
match = re.search(
r"(?m)^ environment:\n((?: [^\n]*\n)+)", content
)
if match:
actual[job] = dict(
re.findall(r"(?m)^ ([a-z]+): (.+)$", match[1])
)
with self.subTest(workflow=filename):
self.assertEqual(
actual,
{
job: {"name": name, "deployment": "false"}
for job, name in environments.items()
},
)


if __name__ == "__main__":
unittest.main()
Loading