-
Notifications
You must be signed in to change notification settings - Fork 1
feat(translation-status): add reusable workflow for translated docs repos #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| # Reusable workflow for translated documentation repositories | ||
| # Called by: repo/.github/workflows/translation-status.yml | ||
| # | ||
| # See the translation-status.yml section of README.md for what it does, what a | ||
| # caller must provide, and the caller stanza to copy. | ||
| # | ||
| # The step order is deliberate. Everything that can fail the job runs after the | ||
| # comment is posted, so a red run always carries the explanation of why. | ||
| # | ||
| # What the gate enforces is that every translation carries the blob hash of the | ||
| # English page it was written against. It cannot read the translated text, so a | ||
| # commit that only rewrites the stamp turns it green and makes the staleness | ||
| # permanently invisible. That one is a reviewer's job, not this workflow's. | ||
|
|
||
| name: Translation Status | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| runs-on: | ||
| description: 'Runner to use' | ||
| required: false | ||
| default: 'ubuntu-latest' | ||
| type: string | ||
|
|
||
| # No permissions block: this workflow inherits the caller's token. Declaring one | ||
| # here would set the complete permission set for the job, so requesting | ||
| # pull-requests: write would abort a caller that grants only contents: read | ||
| # before a single step ran, reporting nothing but "a workflow file issue". The | ||
| # gate is the point of this workflow; the comment is best-effort. | ||
|
|
||
| jobs: | ||
| # Renaming this job renames the status check in every consuming repository, | ||
| # which silently breaks any branch protection rule naming it. | ||
| translation-status: | ||
| runs-on: ${{ inputs.runs-on }} | ||
| timeout-minutes: 20 | ||
| steps: | ||
| # pull_request_target checks out the base branch, so the gate would grade | ||
| # main and pass on every pull request without examining its changes. The | ||
| # plausible route there is a maintainer reaching for it to make the | ||
| # comment work on fork pull requests, which it does not. | ||
| - name: Refuse an event this gate cannot judge | ||
| if: >- | ||
| github.event_name != 'pull_request' | ||
| && github.event_name != 'push' | ||
| && github.event_name != 'workflow_dispatch' | ||
| run: | | ||
| echo "Triggered by ${{ github.event_name }}." >&2 | ||
| echo "This workflow must see the pull request's own content." >&2 | ||
| echo "pull_request_target grades the base branch and always passes." >&2 | ||
| exit 1 | ||
|
|
||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| # Full history: the report resolves each stamped blob to show what | ||
| # changed in English since a translation was written. A shallow | ||
| # clone has no such blob and degrades the diff to a hint. | ||
| fetch-depth: 0 | ||
| # Later steps run code this repository does not control at review | ||
| # time -- dependency build backends under uv sync, plugins under | ||
| # mkdocs build. Without this the job token stays readable by all of | ||
| # them in .git/config. Nothing here needs it after the clone. | ||
| persist-credentials: false | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v5 | ||
|
|
||
| - name: Install dependencies | ||
| # --locked, so a pyproject that has drifted from the lock file fails | ||
| # here instead of resolving to some other version of the checkers. The | ||
| # gate's meaning rests on their exit statuses. | ||
| run: uv sync --locked | ||
|
|
||
| - name: Report translation status | ||
| run: | | ||
| # tee, not plain redirection: a report only in the job summary is | ||
| # invisible in the logs, which is where you look when it misbehaves. | ||
| # PIPESTATUS because the runner shell is bash -e without pipefail, so | ||
| # a crashing reporter would otherwise pass as tee's own exit 0. | ||
| set +e | ||
| uv run translation-status --format markdown --diff 2>&1 | tee report.md | ||
| reported=${PIPESTATUS[0]} | ||
| set -e | ||
| cat report.md >> "$GITHUB_STEP_SUMMARY" | ||
| exit "$reported" | ||
|
|
||
| - name: Comment on the pull request | ||
| if: github.event_name == 'pull_request' | ||
| # A pull request from a fork gets a read-only token, and a docs | ||
| # contributor should get a review rather than a broken build. The | ||
| # comment is an explanation of the verdict, never the verdict itself. | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR: ${{ github.event.number }} | ||
| REPO: ${{ github.repository }} | ||
| run: | | ||
| uv run translation-status --comment > comment.md | ||
| # Match on author as well as marker: anyone who can comment on the | ||
| # pull request could otherwise post the marker first and have every | ||
| # run patch their text instead. | ||
| # | ||
| # Assigned without a pipe so that a failed listing fails this step | ||
| # rather than reading as "no comment yet" -- that would turn one | ||
| # transient API error into a second, permanent comment. --paginate | ||
| # because on a long pull request the marker is not on the first page. | ||
| ids=$(gh api --paginate "repos/$REPO/issues/$PR/comments" \ | ||
| --jq '.[] | ||
| | select(.user.login == "github-actions[bot]") | ||
| | select(.body | contains("<!-- translation-status -->")) | ||
| | .id') | ||
| existing=$(printf '%s\n' "$ids" | head -1) | ||
| if [ -n "$existing" ]; then | ||
| gh api "repos/$REPO/issues/comments/$existing" \ | ||
| -X PATCH -F body=@comment.md --silent | ||
| echo "Updated comment $existing" | ||
| else | ||
| gh api "repos/$REPO/issues/$PR/comments" \ | ||
| -F body=@comment.md --silent | ||
| echo "Created comment" | ||
| fi | ||
|
|
||
| - name: Build the site | ||
| run: uv run mkdocs build --strict | ||
|
|
||
| - name: Check anchors | ||
| id: anchors | ||
| # Tolerated here so that a broken anchor does not hide a stale | ||
| # translation: both are content faults, and an author would rather fix | ||
| # them in one pass than in two. The last step turns it back into a | ||
| # failure. GitHub renders a tolerated failure as a green check, which | ||
| # is why that step repeats the listing rather than pointing here. | ||
| continue-on-error: true | ||
| run: | | ||
| # 2>&1 because the checker reports "could not run" on stderr. Without | ||
| # it that case reaches the summary and the failing step as an empty | ||
| # block, and a red run with no stated cause is the one thing this | ||
| # step order exists to prevent. | ||
| set +e | ||
| uv run check-anchors site 2>&1 | tee anchors.txt | ||
| broken=${PIPESTATUS[0]} | ||
| set -e | ||
| { | ||
| echo "" | ||
| echo "## Anchor check" | ||
| echo "" | ||
| echo '```' | ||
| cat anchors.txt | ||
| echo '```' | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| exit "$broken" | ||
|
|
||
| - name: Gate on translation status | ||
| run: uv run translation-status --check | ||
|
|
||
| - name: Fail on broken anchors | ||
| # always(), so that a failing gate does not skip this and leave the | ||
| # anchor breakage visible only on a step GitHub renders green. | ||
| if: always() && steps.anchors.outcome == 'failure' | ||
| run: | | ||
| cat anchors.txt >&2 | ||
| exit 1 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,15 +1,20 @@ | ||||||||||||||||||||
| # Hat Labs Shared Workflows | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Reusable GitHub Actions workflows for the pr-main-release strategy used across Hat Labs repositories. | ||||||||||||||||||||
| Reusable GitHub Actions workflows for two classes of Hat Labs repository: those | ||||||||||||||||||||
| that build Debian packages, and documentation sites. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Overview | ||||||||||||||||||||
|
|
||||||||||||||||||||
| These workflows implement a standardized release process: | ||||||||||||||||||||
| Most of these workflows implement a standardized release process for Debian | ||||||||||||||||||||
| packages: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 1. **PR** → Run tests | ||||||||||||||||||||
| 2. **Merge to main** → Build .deb, create pre-release, dispatch to APT unstable | ||||||||||||||||||||
| 3. **Publish release** → Dispatch to APT stable | ||||||||||||||||||||
|
|
||||||||||||||||||||
| `translation-status.yml` is the exception. It serves translated documentation | ||||||||||||||||||||
| repositories and has nothing to do with packaging. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Workflows | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### pr-checks.yml | ||||||||||||||||||||
|
|
@@ -136,9 +141,84 @@ jobs: | |||||||||||||||||||
| | `apt-repository` | `hatlabs/apt.hatlabs.fi` | APT repo to dispatch to | | ||||||||||||||||||||
| | `version-pattern` | `^v([0-9]+\.[0-9]+\.[0-9]+)\+([0-9]+)$` | Tag validation regex | | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Repository Requirements | ||||||||||||||||||||
| ### translation-status.yml | ||||||||||||||||||||
|
|
||||||||||||||||||||
| For translated documentation repositories, not Debian packages. Reports which | ||||||||||||||||||||
| translations are behind their English source, posts that report as a pull | ||||||||||||||||||||
| request comment, builds the site, checks its anchors, and fails the run when any | ||||||||||||||||||||
| translation is stale, missing, unstamped or orphaned. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Copy the caller from `examples/docs-repo/.github/workflows/translation-status.yml`. | ||||||||||||||||||||
| The stanza that selects this workflow is: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Each repository using these workflows must have: | ||||||||||||||||||||
| ```yaml | ||||||||||||||||||||
| # The called workflow inherits this token. Omit pull-requests: write and the | ||||||||||||||||||||
| # run still gates; only the comment is skipped. | ||||||||||||||||||||
| permissions: | ||||||||||||||||||||
| contents: read | ||||||||||||||||||||
| pull-requests: write | ||||||||||||||||||||
|
|
||||||||||||||||||||
| jobs: | ||||||||||||||||||||
| translation-status: | ||||||||||||||||||||
| uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@main | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| **Inputs:** | ||||||||||||||||||||
| | Input | Default | Description | | ||||||||||||||||||||
| |-------|---------|-------------| | ||||||||||||||||||||
| | `runs-on` | `ubuntu-latest` | Runner to use | | ||||||||||||||||||||
|
Comment on lines
+166
to
+169
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a blank line before the Inputs table. Markdownlint reports MD058 at line 167. Add an empty line after Proposed fix **Inputs:**
+
| Input | Default | Description |📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 167-167: Tables should be surrounded by blank lines (MD058, blanks-around-tables) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||
|
|
||||||||||||||||||||
| **Requirements** — none of these is validated, so getting one wrong shows up as | ||||||||||||||||||||
| a failing step rather than a clear message: | ||||||||||||||||||||
| - `pyproject.toml` pins [halos-docs-tools](https://github.com/halos-org/docs-tools) | ||||||||||||||||||||
| to a tag, and `uv.lock` is committed. The workflow runs `uv sync --locked`, so | ||||||||||||||||||||
| the two must agree. | ||||||||||||||||||||
| - `mkdocs` and `mkdocs-static-i18n` are project dependencies. The package brings | ||||||||||||||||||||
| the checkers, not mkdocs. | ||||||||||||||||||||
| - `mkdocs.yml` configures `mkdocs-static-i18n` with a `docs/<locale>/` tree, and | ||||||||||||||||||||
| leaves `site_dir` at its default — the anchor check reads `site`. | ||||||||||||||||||||
| - The caller grants `pull-requests: write` if it wants the comment. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| **What it enforces.** Every translation carries the git blob hash of the English | ||||||||||||||||||||
| page it was written against. The checker compares hashes; it cannot read the | ||||||||||||||||||||
| translated text. A commit that only rewrites the stamp therefore turns the gate | ||||||||||||||||||||
| green and makes that page's staleness permanently invisible — and the bot | ||||||||||||||||||||
| comment prints the hash needed to do it, because that is also what an honest | ||||||||||||||||||||
| update needs. Catching a stamp-only diff is a reviewer's job. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| **Making it a gate.** Nothing here makes the check required; that is a branch | ||||||||||||||||||||
| protection rule in the consuming repository. The check is named `<your job id> / | ||||||||||||||||||||
| translation-status`. Two things to know before enabling it: the example | ||||||||||||||||||||
| deliberately carries no `paths` filter, because a required check that never runs | ||||||||||||||||||||
| on a PR touching none of the filtered paths leaves that PR unmergeable forever; | ||||||||||||||||||||
| and the gate reads the repository as it stood when the run started, so two | ||||||||||||||||||||
| independently green PRs can merge into a stale `main`. Require branches to be up | ||||||||||||||||||||
| to date before merging, or use a merge queue — which needs a `merge_group` | ||||||||||||||||||||
| trigger the example does not have. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| The checkers come from the package, so the same commands run on a laptop before | ||||||||||||||||||||
| you push. Glossaries and per-language rules stay in the documentation | ||||||||||||||||||||
| repository. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| This workflow builds and runs pull request code, including code from forks, so | ||||||||||||||||||||
| callers should leave it on GitHub-hosted runners and must not switch the trigger | ||||||||||||||||||||
| to `pull_request_target`. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| A repository without translations has no use for this workflow — the status | ||||||||||||||||||||
| checker needs the i18n configuration to know what to compare. Such a repository | ||||||||||||||||||||
| consumes the package directly from its own build job instead, for example to run | ||||||||||||||||||||
| `check-anchors` on the built site. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| `hatlabs` documentation repositories call this copy rather than the one in | ||||||||||||||||||||
| `hatlabs/shared-workflows`. The one-org-per-copy rule exists to keep the APT | ||||||||||||||||||||
| inputs straight, and this workflow has none; a single copy is deliberate. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ## Repository Requirements (Debian package workflows) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Each repository using `pr-checks.yml`, `build-release.yml` or | ||||||||||||||||||||
| `publish-stable.yml` must have the following. A documentation repository calling | ||||||||||||||||||||
| `translation-status.yml` needs none of them — its requirements are listed in | ||||||||||||||||||||
| that workflow's section above. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ### 1. VERSION file | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| name: Translation Status | ||
|
|
||
| # No paths filter. The gate is a property of the whole repository, not of a | ||
| # diff, and a required check that never runs on a pull request touching none of | ||
| # the filtered paths leaves that pull request unmergeable forever. | ||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| # The called workflow inherits this token, so the comment needs | ||
| # pull-requests: write here. Omit it and the run still gates; only the comment | ||
| # is skipped. | ||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: translation-status-${{ github.ref }} | ||
| # Pull requests only. On push, github.ref is refs/heads/main for every merge, | ||
| # so cancelling lets one merge kill the run checking the one before it -- and | ||
| # a cancelled run is grey, not red, so nobody is told. | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | ||
|
|
||
| jobs: | ||
| translation-status: | ||
| uses: halos-org/shared-workflows/.github/workflows/translation-status.yml@main |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: halos-org/shared-workflows
Length of output: 1236
🏁 Script executed:
Repository: halos-org/shared-workflows
Length of output: 10487
Pin every mutable
usesreference to a reviewed 40-character commit SHA.Mutable action tags and
@mainreusable-workflow references also occur in the other workflow files and README templates. Update every occurrence, including the examples, so upstream changes require a caller commit.📍 Affects 3 files
.github/workflows/translation-status.yml#L35-L35(this comment).github/workflows/translation-status.yml#L48-L48examples/docs-repo/.github/workflows/translation-status.yml#L27-L27README.md#L163-L163🤖 Prompt for AI Agents