Skip to content

fix(ci): use repository token for docs deploy - #48

Merged
HsiangNianian merged 1 commit into
mainfrom
fix/docs-github-token
Jul 30, 2026
Merged

fix(ci): use repository token for docs deploy#48
HsiangNianian merged 1 commit into
mainfrom
fix/docs-github-token

Conversation

@HsiangNianian

@HsiangNianian HsiangNianian commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the personal BOT_TOKEN in docs deployment with the repository-scoped GITHUB_TOKEN
  • grant contents: write only to the deploy job
  • add a workflow contract test so personal-token deployment cannot return

Root cause

The v1.3.5 docs build succeeded, but the personal token push to gh-pages was rejected because its owning account has no verified email.

Validation

  • 10 quality/release workflow contract tests
  • Ruff lint and format checks
  • strict Sphinx HTML build

Related to #34.

Summary by Sourcery

Use the repository-scoped GitHub token with minimal permissions for docs deployment and enforce this via workflow tests.

New Features:

  • Add a workflow contract test to ensure docs deployment uses the repository-scoped GITHUB_TOKEN with write access only on contents.

Enhancements:

  • Restrict docs build job permissions to contents: write for the deployment workflow.

CI:

  • Update docs GitHub Actions workflow to deploy with GITHUB_TOKEN instead of a personal BOT_TOKEN.

@HsiangNianian
HsiangNianian requested a review from fu050409 as a code owner July 30, 2026 17:24
@sourcery-ai

sourcery-ai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Updates the docs deployment workflow to use the repository-scoped GITHUB_TOKEN with minimal permissions and adds a workflow contract test to prevent regressions back to a personal token.

Flow diagram for updated docs deployment workflow authentication

flowchart LR
  A[Docs workflow on main] --> B[build job]
  B --> C[permissions contents: write]
  B --> D[peaceiris/actions-gh-pages]
  D --> E[Use secrets.GITHUB_TOKEN]
  E --> F[Push to gh-pages branch]
Loading

File-Level Changes

Change Details Files
Constrain docs build job permissions and switch deployment to use repository-scoped GitHub token.
  • Add explicit permissions block granting only contents: write to the docs build job.
  • Update the GitHub Pages deployment step to use secrets.GITHUB_TOKEN instead of a personal BOT_TOKEN.
  • Keep existing deployment configuration (branch, publish directory, bot identity) unchanged.
.github/workflows/docs.yml
Add a workflow contract test ensuring docs deployment uses the scoped repository token and minimal permissions.
  • Load the docs.yml workflow in tests and assert that the build job has contents permission set to write.
  • Assert that the GitHub Pages deployment step is wired to use ${{ secrets.GITHUB_TOKEN }}.
  • Reuse existing helper functions for workflow loading and job inspection.
tests/test_quality_gate.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The workflow contract test for docs deployment is quite brittle because it relies on the exact step name 'Deploy to GitHub Pages'; consider keying off the uses: peaceiris/actions-gh-pages@v4 action (and maybe an id) instead so future copy changes to the name don't break the test.
  • In test_docs_deployment_uses_scoped_github_token, accessing build['permissions']['contents'] and the deploy step fields with direct indexing will raise KeyError/StopIteration with a less-clear traceback; wrapping these in helper functions or using .get plus explicit assertion messages would make failures easier to interpret when the workflow changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The workflow contract test for docs deployment is quite brittle because it relies on the exact step name 'Deploy to GitHub Pages'; consider keying off the `uses: peaceiris/actions-gh-pages@v4` action (and maybe an `id`) instead so future copy changes to the name don't break the test.
- In `test_docs_deployment_uses_scoped_github_token`, accessing `build['permissions']['contents']` and the deploy step fields with direct indexing will raise KeyError/StopIteration with a less-clear traceback; wrapping these in helper functions or using `.get` plus explicit assertion messages would make failures easier to interpret when the workflow changes.

## Individual Comments

### Comment 1
<location path=".github/workflows/docs.yml" line_range="17-18" />
<code_context>
   build:
     needs: quality
     runs-on: ubuntu-latest
+    permissions:
+      contents: write

     steps:
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Consider tightening permissions scope to the minimum required for the publish step.

Currently the entire `build` job has `contents: write`. Since only the gh-pages publish step needs write access, consider setting `permissions: contents: read` at the workflow level and granting `contents: write` only to that publish step or a dedicated publish job to limit the impact of a compromised step.

Suggested implementation:

```
  build:
    needs: quality
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Publish docs to GitHub Pages
        if: github.ref == 'refs/heads/main'
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GH_PAGES_TOKEN }}
          publish_dir: ./docs/_build/html
          publish_branch: gh-pages
          user_name: "hydroroll-bot"

```

1. At the workflow root (top of `.github/workflows/docs.yml`), set default permissions to read-only:
   ```yaml
   permissions:
     contents: read
   ```
   so all jobs default to read unless explicitly overridden.
2. Create and configure a fine-grained PAT (e.g., `GH_PAGES_TOKEN`) with the minimum required repository permissions to push to the `gh-pages` branch, and add it as a repository secret.
3. Ensure other jobs in this workflow do not rely on `GH_PAGES_TOKEN`; they should continue to use the default read-only `GITHUB_TOKEN`, keeping write access effectively limited to the publish step.
</issue_to_address>

### Comment 2
<location path="tests/test_quality_gate.py" line_range="66-77" />
<code_context>
         self.assertIn("quality", job_needs(changelog["jobs"]["verify"]))
         self.assertIn("verify", job_needs(changelog["jobs"]["publish"]))

+    def test_docs_deployment_uses_scoped_github_token(self):
+        workflow = load_workflow("docs.yml")
+        build = workflow["jobs"]["build"]
+        self.assertEqual(build["permissions"]["contents"], "write")
+
+        deploy = next(
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the contract to assert that only the docs deploy job has `contents: write` permissions

The PR description states that only the deploy job should have `contents: write`. This test currently checks that the docs job has `contents: write` but doesn’t ensure that no other jobs in `docs.yml` have this permission. To align the test with the contract, iterate over `workflow["jobs"]` and assert that all non-deploy jobs either omit `permissions.contents` or use a more restrictive value, so future accidental grants of `contents: write` are caught.

```suggestion
    def test_docs_deployment_uses_scoped_github_token(self):
        workflow = load_workflow("docs.yml")
        jobs = workflow["jobs"]

        # Only the docs deploy job should have contents: write
        for job_name, job in jobs.items():
            contents_permission = job.get("permissions", {}).get("contents")

            if job_name == "build":
                self.assertEqual(
                    contents_permission,
                    "write",
                    f"build job is expected to have contents: write, got {contents_permission!r}",
                )
            else:
                # Non-deploy jobs must not have contents: write
                self.assertIn(
                    contents_permission,
                    (None, "read"),
                    f"job {job_name} must not have contents: write, got {contents_permission!r}",
                )

        build = jobs["build"]
        deploy = next(
            step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
        )
        self.assertEqual(
            deploy["with"]["github_token"],
            "${{ secrets.GITHUB_TOKEN }}",
        )
```
</issue_to_address>

### Comment 3
<location path="tests/test_quality_gate.py" line_range="71-76" />
<code_context>
+        build = workflow["jobs"]["build"]
+        self.assertEqual(build["permissions"]["contents"], "write")
+
+        deploy = next(
+            step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
+        )
+        self.assertEqual(
+            deploy["with"]["github_token"],
+            "${{ secrets.GITHUB_TOKEN }}",
+        )
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a negative assertion that the legacy personal token (e.g. `BOT_TOKEN`) is not referenced anywhere in `docs.yml`

The current test verifies use of `secrets.GITHUB_TOKEN`, but the workflow contract also requires avoiding the legacy personal token. Consider adding an assertion that the workflow definition does not reference `BOT_TOKEN` (or other personal-token secrets) anywhere, e.g. by checking that `"BOT_TOKEN"` is absent from all `env`, `with`, and `secrets` sections. This would strengthen the test as a guardrail against regressions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +17 to +18
permissions:
contents: write

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Consider tightening permissions scope to the minimum required for the publish step.

Currently the entire build job has contents: write. Since only the gh-pages publish step needs write access, consider setting permissions: contents: read at the workflow level and granting contents: write only to that publish step or a dedicated publish job to limit the impact of a compromised step.

Suggested implementation:

  build:
    needs: quality
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Publish docs to GitHub Pages
        if: github.ref == 'refs/heads/main'
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GH_PAGES_TOKEN }}
          publish_dir: ./docs/_build/html
          publish_branch: gh-pages
          user_name: "hydroroll-bot"

  1. At the workflow root (top of .github/workflows/docs.yml), set default permissions to read-only:
    permissions:
      contents: read
    so all jobs default to read unless explicitly overridden.
  2. Create and configure a fine-grained PAT (e.g., GH_PAGES_TOKEN) with the minimum required repository permissions to push to the gh-pages branch, and add it as a repository secret.
  3. Ensure other jobs in this workflow do not rely on GH_PAGES_TOKEN; they should continue to use the default read-only GITHUB_TOKEN, keeping write access effectively limited to the publish step.

Comment on lines +66 to +77
def test_docs_deployment_uses_scoped_github_token(self):
workflow = load_workflow("docs.yml")
build = workflow["jobs"]["build"]
self.assertEqual(build["permissions"]["contents"], "write")

deploy = next(
step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
)
self.assertEqual(
deploy["with"]["github_token"],
"${{ secrets.GITHUB_TOKEN }}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen the contract to assert that only the docs deploy job has contents: write permissions

The PR description states that only the deploy job should have contents: write. This test currently checks that the docs job has contents: write but doesn’t ensure that no other jobs in docs.yml have this permission. To align the test with the contract, iterate over workflow["jobs"] and assert that all non-deploy jobs either omit permissions.contents or use a more restrictive value, so future accidental grants of contents: write are caught.

Suggested change
def test_docs_deployment_uses_scoped_github_token(self):
workflow = load_workflow("docs.yml")
build = workflow["jobs"]["build"]
self.assertEqual(build["permissions"]["contents"], "write")
deploy = next(
step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
)
self.assertEqual(
deploy["with"]["github_token"],
"${{ secrets.GITHUB_TOKEN }}",
)
def test_docs_deployment_uses_scoped_github_token(self):
workflow = load_workflow("docs.yml")
jobs = workflow["jobs"]
# Only the docs deploy job should have contents: write
for job_name, job in jobs.items():
contents_permission = job.get("permissions", {}).get("contents")
if job_name == "build":
self.assertEqual(
contents_permission,
"write",
f"build job is expected to have contents: write, got {contents_permission!r}",
)
else:
# Non-deploy jobs must not have contents: write
self.assertIn(
contents_permission,
(None, "read"),
f"job {job_name} must not have contents: write, got {contents_permission!r}",
)
build = jobs["build"]
deploy = next(
step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
)
self.assertEqual(
deploy["with"]["github_token"],
"${{ secrets.GITHUB_TOKEN }}",
)

Comment on lines +71 to +76
deploy = next(
step for step in build["steps"] if step["name"] == "Deploy to GitHub Pages"
)
self.assertEqual(
deploy["with"]["github_token"],
"${{ secrets.GITHUB_TOKEN }}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a negative assertion that the legacy personal token (e.g. BOT_TOKEN) is not referenced anywhere in docs.yml

The current test verifies use of secrets.GITHUB_TOKEN, but the workflow contract also requires avoiding the legacy personal token. Consider adding an assertion that the workflow definition does not reference BOT_TOKEN (or other personal-token secrets) anywhere, e.g. by checking that "BOT_TOKEN" is absent from all env, with, and secrets sections. This would strengthen the test as a guardrail against regressions.

@HsiangNianian
HsiangNianian merged commit 57cade8 into main Jul 30, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant