diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 1e49091..1a16e74 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 174f9f8..018d98f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index d3b3b4d..4b117f6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/tests/test_workflow_policy.py b/tests/test_workflow_policy.py index 04e041e..f4554d5 100644 --- a/tests/test_workflow_policy.py +++ b/tests/test_workflow_policy.py @@ -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( @@ -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()