This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e2a864d29c [feat](i18n) Add manual-only Korean translation workflow 
(#4358)
5e2a864d29c is described below

commit 5e2a864d29ca9c932c4e14f08a2f800db37493eb
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sun Oct 18 18:33:28 2026 +0100

    [feat](i18n) Add manual-only Korean translation workflow (#4358)
    
    Add a manual Korean translation workflow that defaults to a read-only
    dry run. It fixes `I18N_LOCALE=ko`, keeps Korean artifacts and temporary
    paths separate, and shares the existing `japanese-translation-ja`
    whole-run lock with Japanese translation.
    
    The workflow reuses the existing scan, Codex generation, auth refresh,
    and PR publisher. Validation requires an exact source file and limits
    generation to one page. It verifies `translate-ko` before loading model
    auth and retains normal PR admission checks and labeling. No schedule is
    added.
    
    Validation: 210 Node 20 tests ran on the integrated T04/T06 base: 209
    passed, none failed, and one existing Apache integration test was
    skipped. Three workflow contract tests cover manual defaults,
    locale/permissions/concurrency, exact-commit artifact handoff, and
    label/auth ordering. No real model call has been made; dry-run and
    bounded live validation follow after merge. This
    workflow/test/documentation-only change skips the website build under
    the existing scope rules.
    
    Co-authored-by: morningman <[email protected]>
---
 .github/workflows/build-check.yml               |   1 +
 .github/workflows/korean-translation.yml        | 226 ++++++++++++++++++++++++
 scripts/i18n/__tests__/korean-workflow.test.cjs |  69 ++++++++
 translations/README.md                          |  20 +++
 4 files changed, 316 insertions(+)

diff --git a/.github/workflows/build-check.yml 
b/.github/workflows/build-check.yml
index 4144efc41a3..dc6f3208719 100644
--- a/.github/workflows/build-check.yml
+++ b/.github/workflows/build-check.yml
@@ -114,6 +114,7 @@ jobs:
                   node --test scripts/i18n/__tests__/planner.test.cjs
                   node --test 
scripts/i18n/__tests__/translation-engine.test.cjs
                   node --test scripts/i18n/__tests__/korean-toolchain.test.cjs
+                  node --test scripts/i18n/__tests__/korean-workflow.test.cjs
                   node --test scripts/i18n/__tests__/workflow.test.cjs
                   node --test scripts/i18n/__tests__/codex-adapter.test.cjs
                   node --test 
scripts/i18n/__tests__/translate-document.test.cjs
diff --git a/.github/workflows/korean-translation.yml 
b/.github/workflows/korean-translation.yml
new file mode 100644
index 00000000000..1c07a2e0c55
--- /dev/null
+++ b/.github/workflows/korean-translation.yml
@@ -0,0 +1,226 @@
+name: Korean translation discovery
+
+on:
+  workflow_dispatch:
+    inputs:
+      mode:
+        description: Read-only dry run, one-page live validation, or batch 
generation
+        type: choice
+        options: [dry-run, validate, generate]
+        default: dry-run
+        required: true
+      plugin:
+        description: Optional plugin filter (default or community)
+        type: string
+      version:
+        description: Optional version filter (current or 4.x)
+        type: string
+      path:
+        description: Optional repository-relative source path prefix
+        type: string
+
+permissions:
+  contents: read
+  pull-requests: read
+
+concurrency:
+  # One entire run across scheduled/manual triggers, locales and modes.
+  # Keep this stable so an in-flight run also holds the same lock.
+  group: japanese-translation-ja
+  cancel-in-progress: false
+  queue: max
+
+jobs:
+  discover:
+    if: github.ref == 'refs/heads/master'
+    runs-on: ubuntu-latest
+    timeout-minutes: 20
+    env:
+      I18N_LOCALE: ko
+    outputs:
+      can_generate: ${{ steps.scan.outputs.can_generate }}
+      source_commit: ${{ steps.scan.outputs.source_commit }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          ref: master
+          fetch-depth: 0
+          persist-credentials: false
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+          cache: yarn
+      - run: yarn --frozen-lockfile
+      - name: Reconcile every open master PR with managed Korean files
+        env:
+          GH_TOKEN: ${{ github.token }}
+          GITHUB_REPOSITORY: ${{ github.repository }}
+        run: node scripts/i18n/workflow-prs.cjs 
"$RUNNER_TEMP/korean-translation"
+      - name: Plan all current and historical pending documents
+        id: scan
+        env:
+          I18N_MODE: ${{ inputs.mode || 'dry-run' }}
+          I18N_PLUGIN: ${{ inputs.plugin }}
+          I18N_VERSION: ${{ inputs.version }}
+          I18N_PATH: ${{ inputs.path }}
+          I18N_LIMIT: '20'
+        run: node scripts/i18n/workflow-scan.cjs 
"$RUNNER_TEMP/korean-translation"
+      - uses: actions/upload-artifact@v4
+        if: always()
+        with:
+          name: korean-translation-plan-${{ github.run_id }}
+          path: ${{ runner.temp }}/korean-translation/
+          if-no-files-found: warn
+          retention-days: 30
+
+  generate:
+    needs: discover
+    env:
+      I18N_LOCALE: ko
+    if: needs.discover.outputs.can_generate == 'true'
+    runs-on: ubuntu-latest
+    timeout-minutes: 120
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          ref: ${{ needs.discover.outputs.source_commit }}
+          fetch-depth: 0
+          persist-credentials: false
+      - uses: actions/setup-node@v4
+        with:
+          node-version: 20
+          cache: yarn
+      - uses: actions/setup-python@v4
+        with:
+          python-version: '3.9'
+      - run: yarn --frozen-lockfile
+      - uses: actions/download-artifact@v4
+        with:
+          name: korean-translation-plan-${{ github.run_id }}
+          path: ${{ runner.temp }}/korean-translation
+      - name: Check external configuration
+        id: config
+        env:
+          OSS_AK: ${{ secrets.OSS_AK }}
+          OSS_SK: ${{ secrets.OSS_SK }}
+          APP_ID: ${{ secrets.I18N_GITHUB_APP_ID }}
+          APP_PRIVATE_KEY: ${{ secrets.I18N_GITHUB_APP_PRIVATE_KEY }}
+          I18N_GITHUB_PAT: ${{ secrets.I18N_GITHUB_PAT }}
+          I18N_MODE: ${{ inputs.mode || 'dry-run' }}
+        run: |
+          if [[ -n "$APP_ID" && -n "$APP_PRIVATE_KEY" ]]; then
+            echo 'auth_kind=app' >> "$GITHUB_OUTPUT"
+          elif [[ -n "$I18N_GITHUB_PAT" ]]; then
+            echo 'auth_kind=pat' >> "$GITHUB_OUTPUT"
+          else
+            echo 'auth_kind=none' >> "$GITHUB_OUTPUT"
+          fi
+          if [[ -n "$OSS_AK" && -n "$OSS_SK" && ( -n "$APP_ID" && -n 
"$APP_PRIVATE_KEY" || -n "$I18N_GITHUB_PAT" || "$I18N_MODE" == 'validate' ) ]]; 
then
+            echo 'ready=true' >> "$GITHUB_OUTPUT"
+          else
+            echo 'ready=false' >> "$GITHUB_OUTPUT"
+            echo 'OSS Codex auth or a GitHub write credential is missing. No 
model call or PR write was attempted.' >> "$GITHUB_STEP_SUMMARY"
+          fi
+      - name: Verify personal token identity before model calls
+        if: steps.config.outputs.auth_kind == 'pat'
+        env:
+          GH_TOKEN: ${{ secrets.I18N_GITHUB_PAT }}
+        run: gh api user --jq '.login' > /dev/null
+      - name: Reconcile again immediately before model calls
+        if: steps.config.outputs.ready == 'true'
+        env:
+          GH_TOKEN: ${{ github.token }}
+          GITHUB_REPOSITORY: ${{ github.repository }}
+        run: node scripts/i18n/workflow-prs.cjs 
"$RUNNER_TEMP/korean-translation"
+      - name: Replan exact source and open PRs
+        if: steps.config.outputs.ready == 'true'
+        id: fresh
+        env:
+          I18N_MODE: ${{ inputs.mode || 'dry-run' }}
+          I18N_PLUGIN: ${{ inputs.plugin }}
+          I18N_VERSION: ${{ inputs.version }}
+          I18N_PATH: ${{ inputs.path }}
+          I18N_LIMIT: '20'
+        run: node scripts/i18n/workflow-scan.cjs 
"$RUNNER_TEMP/korean-translation"
+      - name: Verify the Korean PR label before model calls
+        if: steps.fresh.outputs.can_generate == 'true'
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: test "$(gh api repos/apache/doris-website/labels/translate-ko 
--jq .name)" = translate-ko
+      - name: Install ossutil for the pinned Codex package and auth pool
+        if: steps.fresh.outputs.can_generate == 'true'
+        run: |
+          temp_dir="$(mktemp -d)"
+          trap 'rm -rf "$temp_dir"' EXIT
+          curl -fsSL -o "$temp_dir/ossutil.zip" 
https://gosspublic.alicdn.com/ossutil/1.7.19/ossutil-v1.7.19-linux-amd64.zip
+          unzip -q "$temp_dir/ossutil.zip" -d "$temp_dir"
+          sudo install -m 0755 "$temp_dir/ossutil-v1.7.19-linux-amd64/ossutil" 
/usr/local/bin/ossutil
+      - name: Install the verified Codex runtime used by Doris review
+        if: steps.fresh.outputs.can_generate == 'true'
+        env:
+          OSS_AK: ${{ secrets.OSS_AK }}
+          OSS_SK: ${{ secrets.OSS_SK }}
+        run: |
+          archive="$RUNNER_TEMP/codex-exec-goal.tar.gz"
+          package="$RUNNER_TEMP/codex-exec-goal"
+          ossutil -i "$OSS_AK" -k "$OSS_SK" -e oss-cn-hongkong.aliyuncs.com \
+            cp -f 
oss://doris-community-ci/codex/releases/codex-exec-goal-0.156.1-a310d33f7ebb-x86_64-unknown-linux-gnu.tar.gz
 "$archive"
+          echo 
"2ee3073479bc8c94436684b55a13d7a3f299e768d04436d90e0427814a3954bc  $archive" | 
sha256sum --check
+          mkdir -p "$package"
+          tar -xzf "$archive" -C "$package"
+          (cd "$package" && sha256sum --check SHA256SUMS)
+          test "$(jq -r .version "$package/codex-package.json")" = 0.156.1
+          "$package/bin/codex" exec --help | grep -q -- '--output-schema'
+          "$package/bin/codex" exec --help | grep -q -- '--disable'
+          "$package/bin/codex" exec --help | grep -q -- '--ignore-user-config'
+          "$package/bin/codex" exec --help | grep -q -- '--ephemeral'
+          echo "CODEX_BIN=$package/bin/codex" >> "$GITHUB_ENV"
+      - name: Load an eligible Codex auth snapshot
+        if: steps.fresh.outputs.can_generate == 'true'
+        id: auth
+        env:
+          OSS_AK: ${{ secrets.OSS_AK }}
+          OSS_SK: ${{ secrets.OSS_SK }}
+        run: node scripts/i18n/codex-auth.cjs load
+      - name: Generate a bounded, validated batch
+        if: steps.auth.outcome == 'success'
+        id: translation
+        env:
+          I18N_CODEX_MODEL: ${{ vars.I18N_CODEX_MODEL || 'gpt-6-sol' }}
+          I18N_MODE: ${{ inputs.mode || 'dry-run' }}
+          I18N_PATH: ${{ inputs.path }}
+          I18N_MAX_PAGES: ${{ inputs.mode == 'validate' && '1' || '10' }}
+          GITHUB_REPOSITORY: ${{ github.repository }}
+        run: node scripts/i18n/workflow-generate.cjs 
"$RUNNER_TEMP/korean-translation"
+      - uses: actions/create-github-app-token@v2
+        if: steps.translation.outputs.created == 'true' && 
steps.config.outputs.auth_kind == 'app'
+        id: app
+        with:
+          app-id: ${{ secrets.I18N_GITHUB_APP_ID }}
+          private-key: ${{ secrets.I18N_GITHUB_APP_PRIVATE_KEY }}
+          owner: apache
+          repositories: doris-website
+          permission-contents: write
+          permission-pull-requests: write
+      - name: Run shared PR admission checks and open reviewable PR
+        if: steps.translation.outputs.created == 'true' && 
steps.config.outputs.auth_kind != 'none'
+        env:
+          GH_TOKEN: ${{ steps.app.outputs.token || secrets.I18N_GITHUB_PAT }}
+          I18N_PR_AUTH_KIND: ${{ steps.config.outputs.auth_kind }}
+        run: node scripts/i18n/workflow-publish.cjs 
"$RUNNER_TEMP/korean-translation"
+      - name: Report staged validation without GitHub write credentials
+        if: steps.translation.outputs.created == 'true' && 
steps.config.outputs.auth_kind == 'none'
+        run: echo 'One-page Codex candidate is in the generation artifact. A 
GitHub App or personal token is required for a translation PR.' >> 
"$GITHUB_STEP_SUMMARY"
+      - name: Sync refreshed Codex auth without overwriting a newer snapshot
+        if: always() && steps.auth.outcome == 'success'
+        env:
+          OSS_AK: ${{ secrets.OSS_AK }}
+          OSS_SK: ${{ secrets.OSS_SK }}
+        run: node scripts/i18n/codex-auth.cjs sync
+      - uses: actions/upload-artifact@v4
+        if: always()
+        with:
+          name: korean-translation-generation-${{ github.run_id }}
+          path: ${{ runner.temp }}/korean-translation/
+          if-no-files-found: warn
+          retention-days: 30
diff --git a/scripts/i18n/__tests__/korean-workflow.test.cjs 
b/scripts/i18n/__tests__/korean-workflow.test.cjs
new file mode 100644
index 00000000000..640f3956b2c
--- /dev/null
+++ b/scripts/i18n/__tests__/korean-workflow.test.cjs
@@ -0,0 +1,69 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const yaml = require('js-yaml');
+const workflows = path.resolve(__dirname, '../../../.github/workflows');
+const load = name => yaml.load(fs.readFileSync(path.join(workflows, name), 
'utf8'));
+const korean = load('korean-translation.yml');
+const japanese = load('japanese-translation.yml');
+
+test('Korean generation requires a manual choice and shares the existing 
whole-run lock', () => {
+    assert.deepEqual(Object.keys(korean.on), ['workflow_dispatch']);
+    assert.equal(korean.on.workflow_dispatch.inputs.mode.default, 'dry-run');
+    assert.deepEqual(korean.on.workflow_dispatch.inputs.mode.options, 
['dry-run', 'validate', 'generate']);
+    assert.equal(korean.on.workflow_dispatch.inputs.locale, undefined);
+    assert.deepEqual(korean.permissions, {'contents':'read', 
'pull-requests':'read'});
+    assert.deepEqual(korean.concurrency, japanese.concurrency);
+    assert.equal(korean.concurrency.group, 'japanese-translation-ja');
+    assert.equal(korean.jobs.discover.if, "github.ref == 'refs/heads/master'");
+    assert.equal(korean.jobs.generate.needs, 'discover');
+    assert.equal(korean.jobs.generate.if, "needs.discover.outputs.can_generate 
== 'true'");
+    for (const job of Object.values(korean.jobs)) {
+        assert.equal(job.env.I18N_LOCALE, 'ko');
+        for (const step of job.steps) {
+            if (step.env?.I18N_MODE) assert.equal(step.env.I18N_MODE, "${{ 
inputs.mode || 'dry-run' }}");
+            if (step.uses?.startsWith('actions/checkout@')) {
+                assert.equal(step.with['fetch-depth'], 0);
+                assert.equal(step.with['persist-credentials'], false);
+            }
+        }
+    }
+});
+
+test('Korean discovery and generation exchange Korean artifacts at the exact 
scanned commit', () => {
+    const discover = korean.jobs.discover.steps;
+    const generate = korean.jobs.generate.steps;
+    assert.equal(generate.find(step => 
step.uses?.startsWith('actions/checkout@')).with.ref,
+        '${{ needs.discover.outputs.source_commit }}');
+    const upload = discover.find(step => 
step.uses?.startsWith('actions/upload-artifact@'));
+    const download = generate.find(step => 
step.uses?.startsWith('actions/download-artifact@'));
+    assert.equal(upload.with.name, 'korean-translation-plan-${{ github.run_id 
}}');
+    assert.equal(download.with.name, upload.with.name);
+    assert.equal(download.with.path, '${{ runner.temp }}/korean-translation');
+    const scripts = [...discover, ...generate].filter(step => /node 
scripts\/i18n\/workflow-(prs|scan|generate|publish)/.test(step.run ?? ''));
+    assert.equal(scripts.length, 6);
+    assert(scripts.every(step => 
step.run.includes('"$RUNNER_TEMP/korean-translation"')));
+    const execute = generate.find(step => step.id === 'translation');
+    assert.equal(execute.env.I18N_MAX_PAGES, "${{ inputs.mode == 'validate' && 
'1' || '10' }}");
+    assert.equal(execute.env.I18N_PATH, '${{ inputs.path }}');
+    assert.equal(execute.env.I18N_CODEX_MODEL, 
japanese.jobs.generate.steps.find(step => step.id === 
'translation').env.I18N_CODEX_MODEL);
+});
+
+test('Korean publication checks the label before auth loading and preserves 
the verified runtime and refresh path', () => {
+    const steps = korean.jobs.generate.steps;
+    const label = steps.findIndex(step => step.name === 'Verify the Korean PR 
label before model calls');
+    const auth = steps.findIndex(step => step.id === 'auth');
+    assert(label >= 0 && label < auth);
+    assert.equal(steps[label].if, "steps.fresh.outputs.can_generate == 
'true'");
+    assert.equal(steps[label].env.GH_TOKEN, '${{ github.token }}');
+    assert(steps[label].run.includes('labels/translate-ko'));
+    const runtime = steps.find(step => step.name === 'Install the verified 
Codex runtime used by Doris review');
+    assert.deepEqual(runtime, japanese.jobs.generate.steps.find(step => 
step.name === runtime.name));
+    const sync = steps.find(step => step.name === 'Sync refreshed Codex auth 
without overwriting a newer snapshot');
+    assert.equal(sync.if, "always() && steps.auth.outcome == 'success'");
+    assert.equal(sync.run, 'node scripts/i18n/codex-auth.cjs sync');
+    const publish = steps.find(step => 
step.run?.includes('workflow-publish.cjs'));
+    assert.equal(publish.if, "steps.translation.outputs.created == 'true' && 
steps.config.outputs.auth_kind != 'none'");
+    assert.equal(publish.env.GH_TOKEN, '${{ steps.app.outputs.token || 
secrets.I18N_GITHUB_PAT }}');
+});
diff --git a/translations/README.md b/translations/README.md
index 1db4180b3d1..c27d3018da7 100644
--- a/translations/README.md
+++ b/translations/README.md
@@ -103,6 +103,26 @@ existing source/technical hash algorithms unchanged. 
Offline Korean fixtures
 exercise these boundaries without real credentials or model calls; actual
 single-page generation is a separate validation step.
 
+The `Korean translation discovery` workflow is manual-only and defaults to a
+read-only dry run. It fixes the locale to `ko`, uses separate Korean artifacts,
+and shares the existing `japanese-translation-ja` lock with Japanese runs.
+After the workflow is merged, run discovery on master:
+
+```bash
+gh workflow run korean-translation.yml --repo apache/doris-website --ref 
master -f mode=dry-run
+```
+
+For a bounded real validation, select one eligible English file from the plan
+and provide its exact repository-relative `.md` or `.mdx` path with
+`-f mode=validate -f path=<exact-source-file>`. Validation sets the page limit
+to one and rechecks active PRs before calling Codex. Generation checks the
+`translate-ko` label before loading auth, then uses the existing verified
+runtime and auth pool. A write credential enables a normal labeled PR after
+shared admission checks; otherwise a successful one-page validation remains
+an artifact and does not establish the PR publication path. Inspect generation
+warnings and normal PR checks before merging. There is no Korean generation
+schedule in this workflow.
+
 ## Search, navigation and four-language validation
 
 `check-configuration.cjs --all` validates each managed locale's scope and 
glossary


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to