Skip to content
Open
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
92 changes: 92 additions & 0 deletions .github/workflows/redline-action-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Redline Action self-test

# Exercises the composite action published from this repository's root
# (uses: JSv4/Python-Redlines@<ref>) in both of its modes. The action installs
# python-redlines from PyPI, so this validates the action plumbing rather than
# the working-tree Python packages (ci.yml covers those).

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
explicit-pair:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Redline the test fixtures
id: redline
uses: ./
with:
original: tests/fixtures/original.docx
modified: tests/fixtures/modified.docx
author: Action Self-Test
# 'auto' exercises the graceful-skip path until a Docx2Html release
# with --track-changes support is on NuGet, then starts rendering.
html-preview: auto
artifact-name: docx-redlines-explicit

- name: Assert outputs
env:
COUNT: ${{ steps.redline.outputs.count }}
ANY_CHANGES: ${{ steps.redline.outputs.any-changes }}
REDLINES: ${{ steps.redline.outputs.redlines }}
run: |
test "$COUNT" = "1"
test "$ANY_CHANGES" = "true"
echo "$REDLINES" | python3 -c '
import json, sys
[record] = json.load(sys.stdin)
assert record["status"] == "explicit", record
assert record["revisions"] == 9, record
'
test -s "redlines/tests/fixtures/modified.redline.docx"

auto-detect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Create a synthetic .docx change in the local history
run: |
git config user.name "Action Self-Test"
git config user.email "self-test@example.com"
mkdir -p contracts
cp tests/fixtures/original.docx contracts/agreement.docx
git add contracts
git commit -m "add agreement"
cp tests/fixtures/modified.docx contracts/agreement.docx
git commit -am "revise agreement"

- name: Redline the changed files
id: redline
uses: ./
with:
base-ref: HEAD~1
head-ref: HEAD
author: Action Self-Test
html-preview: 'false'
artifact-name: docx-redlines-auto

- name: Assert outputs
env:
COUNT: ${{ steps.redline.outputs.count }}
REDLINES: ${{ steps.redline.outputs.redlines }}
run: |
test "$COUNT" = "1"
echo "$REDLINES" | python3 -c '
import json, sys
[record] = json.load(sys.stdin)
assert record["path"] == "contracts/agreement.docx", record
assert record["status"] == "modified", record
assert record["revisions"] == 9, record
'
test -s "redlines/contracts/agreement.redline.docx"
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ prebuilt wheel, so end users never compile anything.
The repo root is **not** an installable project — its `pyproject.toml` holds only
shared pytest/coverage config.

## GitHub Action

The repo root also publishes a composite GitHub Action (`uses: JSv4/Python-Redlines@<ref>`),
issue #12. `action.yml` wires inputs/artifact-upload; the logic lives in
`action/redline_changed.py` (stdlib-only driver): auto-detects `.docx` files changed
between two commits (PR base/head, push before/after, or explicit `base-ref`/`head-ref`)
or takes an explicit `original`/`modified` pair, runs an engine via pip-installed
python-redlines (PyPI wheels, not the working tree), and optionally renders HTML previews
by invoking the Docxodus `Docx2Html` dotnet tool with `--track-changes` (requires
Docxodus ≥ 7.1.0; `html-preview: auto` skips gracefully below that). Script unit +
integration tests: `tests/test_action_script.py`; action-level self-test:
`.github/workflows/redline-action-test.yml` (runs the action from the checkout in both
modes and asserts on outputs).

## Commands

```bash
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,80 @@ not support.
> table anchoring). Redline output on the default path can therefore differ from 0.2.1 independently
> of this new flag. Diff a representative document if byte-level stability matters to you.

## GitHub Action

This repository doubles as a GitHub Action, so a repo that versions `.docx` files can get a
redline of every Word document a pull request changes — as a reviewable workflow artifact —
without anyone opening Word ([#12](https://github.com/JSv4/Python-Redlines/issues/12)).

```yaml
name: DOCX redlines
on: pull_request

permissions:
contents: read

jobs:
redline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # both sides of the comparison must be fetchable
- uses: JSv4/Python-Redlines@main
```

That's the whole workflow. For every `.docx` file the pull request modifies (or renames), the
action extracts the base and head versions from git, runs the redline engine, and:

- writes `<output-dir>/<path>/<name>.redline.docx` with native Word tracked changes,
- uploads the output directory as a workflow artifact,
- posts a job-summary table (file, revision count, output paths),
- when HTML previews are on, also writes a browser-viewable `.redline.html` per file, rendered
with the [Docxodus](https://github.com/JSv4/Docxodus) `Docx2Html` tool's `--track-changes`
mode so insertions/deletions/moves show as `ins`/`del` markup.

You can also compare an explicit pair of files instead of auto-detecting:

```yaml
- uses: JSv4/Python-Redlines@main
with:
original: docs/contract-v1.docx
modified: docs/contract-v2.docx
author: Legal Review
```

### Action inputs

| Input | Default | Purpose |
|---|---|---|
| `original` / `modified` | — | Explicit-pair mode: compare these two files instead of auto-detecting. |
| `files` | `**/*.docx` | Newline-separated git glob pattern(s) selecting which changed files to redline. |
| `base-ref` / `head-ref` | event-derived | Commits to compare. Defaults: PR base (merge-base) → head on `pull_request`, `before` → `after` on `push`, else `HEAD~1` → `HEAD`. |
| `author` | `python-redlines` | Author recorded on the tracked changes. |
| `engine` | `docxodus` | `docxodus` or `xmlpowertools`. |
| `comparison` | engine default | `wmlcomparer` or `docxdiff` (docxodus engine only). |
| `detect-moves` | `false` | Move detection (docxodus engine only). |
| `output-dir` | `redlines` | Where outputs are written (mirrors the source tree). |
| `html-preview` | `auto` | `auto` (render when the Docx2Html tool supports `--track-changes`, else warn and skip), `true` (require), `false` (skip — no .NET needed). |
| `summary` | `true` | Write the job-summary table. |
| `upload-artifact` / `artifact-name` | `true` / `docx-redlines` | Artifact upload controls. |
| `package-version` | latest | pip pin for python-redlines, e.g. `==0.3.0`. |
| `docx2html-version` | latest | NuGet pin for the Docx2Html preview tool. |

Outputs: `count` (redlines generated), `any-changes`, `redlines` (a JSON array of
`{path, previous_path, status, revisions, redline, html, error}` records), and `artifact-url`.

Notes:

- Added and deleted `.docx` files are listed in the summary but not redlined — a redline
needs both a base and a head version. Pure renames report zero revisions without
invoking the engine.
- HTML previews require a `Docx2Html` release with `--track-changes` support (Docxodus
≥ 7.1.0); until that is on NuGet, the default `auto` mode skips previews with a warning.
- The action installs python-redlines from PyPI with prebuilt engine binaries — it does
not build anything from the repository, so runs are fast on `ubuntu-latest` runners.

## Comparison Engines

Python-Redlines gives you **three ways to compare**, across two engine classes. `DocxodusEngine`
Expand Down
197 changes: 197 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: 'DOCX Redlines'
description: >-
Generate Word tracked-changes (redline) documents for .docx files changed in a
pull request or push — or for an explicit pair of files — using python-redlines.
Optionally renders browser-viewable HTML previews of each redline via Docxodus.
author: 'JSv4'
branding:
icon: 'file-text'
color: 'red'

inputs:
original:
description: >-
Path to the original .docx for explicit-pair mode. Must be set together with
'modified'. Leave both empty to auto-detect the .docx files changed between
two commits instead.
required: false
default: ''
modified:
description: 'Path to the modified .docx for explicit-pair mode.'
required: false
default: ''
files:
description: >-
Newline-separated glob pattern(s) selecting which changed .docx files to
redline in auto-detect mode (git pathspec globs, case-insensitive).
required: false
default: '**/*.docx'
base-ref:
description: >-
Base commit/ref to compare from in auto-detect mode. Must be set together with
'head-ref'. Defaults to the PR base (merge-base with the head when computable)
on pull_request events, the pre-push commit on push events, or HEAD~1.
required: false
default: ''
head-ref:
description: 'Head commit/ref to compare to in auto-detect mode.'
required: false
default: ''
author:
description: 'Author name recorded on the generated tracked changes.'
required: false
default: 'python-redlines'
engine:
description: "Redline engine: 'docxodus' or 'xmlpowertools'."
required: false
default: 'docxodus'
comparison:
description: >-
Comparison algorithm for the docxodus engine: 'wmlcomparer' (default) or
'docxdiff'.
required: false
default: ''
detect-moves:
description: "Enable move detection (docxodus engine only): 'true' or 'false'."
required: false
default: 'false'
output-dir:
description: 'Directory (relative to the workspace) where redlines are written.'
required: false
default: 'redlines'
html-preview:
description: >-
Render an HTML preview of each redline with the Docx2Html dotnet tool:
'auto' (render when the tool supports it, otherwise warn and skip),
'true' (require it, fail otherwise), or 'false' (skip; no .NET needed).
required: false
default: 'auto'
summary:
description: "Write a job-summary table of the generated redlines: 'true' or 'false'."
required: false
default: 'true'
upload-artifact:
description: "Upload the output directory as a workflow artifact: 'true' or 'false'."
required: false
default: 'true'
artifact-name:
description: 'Name of the uploaded artifact.'
required: false
default: 'docx-redlines'
package-version:
description: >-
pip version specifier for python-redlines, e.g. '==0.3.0'. Empty installs the
latest release.
required: false
default: ''
docx2html-version:
description: 'NuGet version of the Docx2Html dotnet tool. Empty installs the latest.'
required: false
default: ''

outputs:
count:
description: 'Number of redline documents generated.'
value: ${{ steps.redline.outputs.count }}
any-changes:
description: >-
'true' when at least one .docx change matched (including added/deleted files
that cannot be redlined), otherwise 'false'.
value: ${{ steps.redline.outputs.any-changes }}
redlines:
description: >-
JSON array describing each detected change:
[{"path", "previous_path", "status", "revisions", "redline", "html"}, ...].
value: ${{ steps.redline.outputs.redlines }}
artifact-url:
description: 'URL of the uploaded artifact (empty when nothing was uploaded).'
value: ${{ steps.upload.outputs.artifact-url }}

runs:
using: 'composite'
steps:
- name: Set up Python
id: python
uses: actions/setup-python@v5
with:
python-version: '3.12'
update-environment: false

- name: Install python-redlines
shell: bash
env:
INPUT_ENGINE: ${{ inputs.engine }}
INPUT_PACKAGE_VERSION: ${{ inputs.package-version }}
run: |
case "${INPUT_ENGINE}" in
docxodus) EXTRA="docxodus" ;;
xmlpowertools) EXTRA="ooxmlpowertools" ;;
*) echo "::error::Unknown engine '${INPUT_ENGINE}' (expected 'docxodus' or 'xmlpowertools')"; exit 1 ;;
esac
"${{ steps.python.outputs.python-path }}" -m pip install --quiet "python-redlines[${EXTRA}]${INPUT_PACKAGE_VERSION}"

- name: Set up .NET for HTML previews
if: ${{ inputs.html-preview != 'false' }}
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'

- name: Install the Docx2Html preview tool
if: ${{ inputs.html-preview != 'false' }}
shell: bash
env:
INPUT_HTML_PREVIEW: ${{ inputs.html-preview }}
INPUT_DOCX2HTML_VERSION: ${{ inputs.docx2html-version }}
run: |
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
VERSION_ARGS=()
if [ -n "${INPUT_DOCX2HTML_VERSION}" ]; then
VERSION_ARGS=(--version "${INPUT_DOCX2HTML_VERSION}")
fi
if [ "${INPUT_HTML_PREVIEW}" = "true" ]; then
dotnet tool update --global Docx2Html "${VERSION_ARGS[@]}"
else
dotnet tool update --global Docx2Html "${VERSION_ARGS[@]}" \
|| echo "::warning::Could not install the Docx2Html dotnet tool; HTML previews will be skipped."
fi

- name: Generate redlines
id: redline
shell: bash
env:
INPUT_ORIGINAL: ${{ inputs.original }}
INPUT_MODIFIED: ${{ inputs.modified }}
INPUT_FILES: ${{ inputs.files }}
INPUT_BASE_REF: ${{ inputs.base-ref }}
INPUT_HEAD_REF: ${{ inputs.head-ref }}
INPUT_AUTHOR: ${{ inputs.author }}
INPUT_ENGINE: ${{ inputs.engine }}
INPUT_COMPARISON: ${{ inputs.comparison }}
INPUT_DETECT_MOVES: ${{ inputs.detect-moves }}
INPUT_OUTPUT_DIR: ${{ inputs.output-dir }}
INPUT_HTML_PREVIEW: ${{ inputs.html-preview }}
INPUT_SUMMARY: ${{ inputs.summary }}
run: |
"${{ steps.python.outputs.python-path }}" "${GITHUB_ACTION_PATH}/action/redline_changed.py"

- name: Upload redlines artifact
id: upload
if: ${{ inputs.upload-artifact == 'true' && steps.redline.outputs.count != '0' }}
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact-name }}
path: ${{ inputs.output-dir }}
if-no-files-found: error

- name: Link the artifact from the job summary
if: ${{ inputs.upload-artifact == 'true' && inputs.summary == 'true' && steps.redline.outputs.count != '0' }}
shell: bash
env:
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
run: |
if [ -n "${ARTIFACT_URL}" ]; then
{
echo ""
echo "📦 [Download the redlines artifact](${ARTIFACT_URL})"
} >> "$GITHUB_STEP_SUMMARY"
fi
Loading
Loading