From 7300bf8466efc44adc756d1f759084f3a5c91cac Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:22:03 +0100 Subject: [PATCH 01/12] feat: update argocd-diff action --- .../argocd-diff-helm-template/action.yml | 44 +++++ .github/actions/argocd-diff-run/action.yml | 127 ++++++++++++ .github/workflows/argocd-diff-preview.yml | 181 +++++++----------- docs/actions/argocd-diff-preview.md | 99 ++++++++++ docs/index.md | 5 + docs/workflows/actionlint.md | 9 + docs/workflows/argocd-diff-preview.md | 18 +- mkdocs.yml | 1 + 8 files changed, 371 insertions(+), 113 deletions(-) create mode 100644 .github/actions/argocd-diff-helm-template/action.yml create mode 100644 .github/actions/argocd-diff-run/action.yml create mode 100644 docs/actions/argocd-diff-preview.md diff --git a/.github/actions/argocd-diff-helm-template/action.yml b/.github/actions/argocd-diff-helm-template/action.yml new file mode 100644 index 0000000..65ff12c --- /dev/null +++ b/.github/actions/argocd-diff-helm-template/action.yml @@ -0,0 +1,44 @@ +name: Argocd diff — prepare manifests +description: helm template or copy repo_path into argocd-diff base/target folder +inputs: + helm_chart_path: + description: If set, run helm template; otherwise copy repo_path + required: false + default: '' + helm_values_files: + description: Space-separated -f values for helm + required: false + default: '' + repo_path: + description: Directory to copy when not using Helm + required: false + default: '.' + output_dir: + description: e.g. /tmp/argocd-diff/target + required: true +runs: + using: composite + steps: + - name: Set up Helm + if: inputs.helm_chart_path != '' + uses: azure/setup-helm@v4 + + - name: Render or copy + shell: bash + env: + HELM_CHART_PATH: ${{ inputs.helm_chart_path }} + VALUES_FILES: ${{ inputs.helm_values_files }} + REPO_PATH: ${{ inputs.repo_path }} + OUTPUT_DIR: ${{ inputs.output_dir }} + run: | + mkdir -p "$OUTPUT_DIR" + if [[ -n "$HELM_CHART_PATH" ]]; then + values_args=() + for f in $VALUES_FILES; do + [[ -n "$f" ]] || continue + values_args+=(-f "$f") + done + helm template "$HELM_CHART_PATH" "${values_args[@]}" --output-dir "$OUTPUT_DIR" + else + cp -r "$REPO_PATH/." "$OUTPUT_DIR" + fi diff --git a/.github/actions/argocd-diff-run/action.yml b/.github/actions/argocd-diff-run/action.yml new file mode 100644 index 0000000..5372f98 --- /dev/null +++ b/.github/actions/argocd-diff-run/action.yml @@ -0,0 +1,127 @@ +name: Argocd diff — secrets and docker +description: yq secrets, optional traverse/render flags, run dagandersen/argocd-diff-preview +inputs: + repo: + description: GitHub repository owner/name for argocd-diff-preview + required: true + target_branch: + description: PR head branch name + required: true + base_branch: + description: Base branch to compare against + required: true + github_token: + description: PAT with repo read (tool uses GitHub API) + required: true + timeout: + description: Timeout in seconds for argocd-diff-preview + required: true + argocd_chart_version: + description: Argo CD Helm chart version; empty uses latest + required: false + default: '' + render_method: + description: cli | server-api | repo-server-api; empty uses tool default + required: false + default: '' + traverse_app_of_apps: + description: Enable experimental app-of-apps traversal (requires repo-server-api) + required: false + default: 'false' + file_regex: + description: Optional file-regex filter passed to the tool + required: false + default: '' + ssh_private_key: + description: Optional — Argo CD Git SSH private key; set from a workflow secret in the caller + required: false + default: '' + repo_ssh_url: + description: Optional — SSH repo URL (e.g. git@github.com:org/repo.git) + required: false + default: '' + sops_age_key: + description: Optional — SOPS age key material for helm-secrets; set from a workflow secret in the caller + required: false + default: '' +runs: + using: composite + steps: + - name: Write K8s secrets for Argo CD + shell: bash + env: + SSH_PRIVATE_KEY: ${{ inputs.ssh_private_key }} + REPO_SSH_URL: ${{ inputs.repo_ssh_url }} + SOPS_AGE_KEY: ${{ inputs.sops_age_key }} + run: | + mkdir -p /tmp/argocd-diff/secrets + if [[ -n "${SSH_PRIVATE_KEY:-}" && -n "${REPO_SSH_URL:-}" ]]; then + yq -n ' + .apiVersion = "v1" | + .kind = "Secret" | + .metadata.name = "private-repo" | + .metadata.namespace = "argocd" | + .metadata.labels["argocd.argoproj.io/secret-type"] = "repository" | + .stringData.type = "git" | + .stringData.url = strenv(REPO_SSH_URL) | + .stringData.sshPrivateKey = strenv(SSH_PRIVATE_KEY) + ' > /tmp/argocd-diff/secrets/repo-creds.yaml + fi + if [[ -n "${SOPS_AGE_KEY:-}" ]]; then + yq -n ' + .apiVersion = "v1" | + .kind = "Secret" | + .metadata.name = "sops-age-key" | + .metadata.namespace = "argocd" | + .stringData."age-key.txt" = strenv(SOPS_AGE_KEY) + ' > /tmp/argocd-diff/secrets/sops-age-key.yaml + fi + + - name: Run argocd-diff-preview + shell: bash + env: + REPO: ${{ inputs.repo }} + TARGET_BRANCH: ${{ inputs.target_branch }} + BASE_BRANCH: ${{ inputs.base_branch }} + GITHUB_TOKEN: ${{ inputs.github_token }} + TIMEOUT: ${{ inputs.timeout }} + ARGOCD_CHART_VERSION: ${{ inputs.argocd_chart_version }} + RENDER_METHOD_IN: ${{ inputs.render_method }} + TRAVERSE_APP_OF_APPS: ${{ inputs.traverse_app_of_apps }} + FILE_REGEX: ${{ inputs.file_regex }} + run: | + mkdir -p /tmp/argocd-diff/output + docker_args=( + --rm --network=host + -v /var/run/docker.sock:/var/run/docker.sock + -v /tmp/argocd-diff/base:/base-branch + -v /tmp/argocd-diff/target:/target-branch + -v /tmp/argocd-diff/output:/output + -v /tmp/argocd-diff/secrets:/secrets + -e REPO -e TARGET_BRANCH -e BASE_BRANCH -e GITHUB_TOKEN -e TIMEOUT + ) + if [[ -n "${ARGOCD_CHART_VERSION:-}" ]]; then + docker_args+=(-e ARGOCD_CHART_VERSION) + fi + if [[ -d /tmp/argocd-diff/argocd-config-custom ]]; then + docker_args+=(-v /tmp/argocd-diff/argocd-config-custom:/argocd-config) + fi + + RENDER_METHOD="${RENDER_METHOD_IN:-}" + if [[ "${TRAVERSE_APP_OF_APPS:-false}" == "true" ]]; then + docker_args+=(-e TRAVERSE_APP_OF_APPS=true) + if [[ -z "$RENDER_METHOD" ]]; then + RENDER_METHOD=repo-server-api + elif [[ "$RENDER_METHOD" != repo-server-api ]]; then + echo "::error::traverse_app_of_apps requires render_method=repo-server-api (got: $RENDER_METHOD)" + exit 1 + fi + fi + if [[ -n "$RENDER_METHOD" ]]; then + docker_args+=(-e "RENDER_METHOD=$RENDER_METHOD") + fi + if [[ -n "${FILE_REGEX:-}" ]]; then + docker_args+=(-e "FILE_REGEX=$FILE_REGEX") + fi + + docker run "${docker_args[@]}" dagandersen/argocd-diff-preview:latest diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index 0c76e5c..af0ec94 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -1,59 +1,73 @@ -# Reusable workflow for generating ArgoCD diff previews on pull requests. -# Supports two modes: -# - Plain YAML repos: set repo_path to the directory containing Application manifests (default: repo root) -# - Helm-rendered repos: set helm_chart_path (and optionally helm_values_files) to pre-render before diffing -# Posts the diff as a PR comment (updated in place on subsequent runs). -# -# For private repos, pass SSH_PRIVATE_KEY and REPO_SSH_URL secrets. -# The caller is responsible for obtaining these (e.g. decrypting from SOPS) before calling this workflow. -# -# Requirements: -# - Caller must set `pull-requests: write` permission -# - Caller must pass a GitHub PAT (repo read access) via the GH_PAT secret +# Reusable workflow: Argo CD manifest diff on PRs (argocd-diff-preview). +# Callers need pull-requests: write and secret GH_PAT (repo read). +# Details: docs/workflows/argocd-diff-preview.md name: ArgoCD Diff Preview on: workflow_call: inputs: repo_path: - description: 'Directory containing Application manifests (used when not rendering via Helm). Defaults to repo root.' + description: Directory of Application YAML when not using Helm pre-render required: false type: string default: '.' helm_chart_path: - description: 'Path to the Helm chart to render into Application manifests. When set, Helm rendering is enabled.' + description: Helm chart path for pre-render; empty copies repo_path only required: false type: string default: '' helm_values_files: - description: 'Space-separated list of values files to pass to helm template (e.g. "apps/values/base.yaml apps/values/prod.yaml"). Only used when helm_chart_path is set.' + description: Space-separated values files for helm template required: false type: string default: '' base_branch: - description: 'Base branch to compare against' + description: Branch to diff against required: false type: string default: 'main' argocd_version: - description: 'ArgoCD Helm chart version to install in the ephemeral cluster. Defaults to latest.' + description: Pin Argo CD Helm chart version; empty uses latest required: false type: string default: '' timeout: - description: 'Timeout in seconds for argocd-diff-preview' + description: argocd-diff-preview timeout in seconds required: false type: number default: 300 + argocd_config_dir: + description: Repo-relative dir (PR branch) mounted as /argocd-config (custom Argo CD Helm values) + required: false + type: string + default: '' + render_method: + description: argocd-diff-preview render method (cli | server-api | repo-server-api) + required: false + type: string + default: '' + traverse_app_of_apps: + description: Experimental app-of-apps expansion (needs repo-server-api) + required: false + type: boolean + default: false + file_regex: + description: Optional --file-regex for argocd-diff-preview + required: false + type: string + default: '' secrets: GH_PAT: - description: 'GitHub PAT with repo read access (used by argocd-diff-preview to clone the private repo)' + description: PAT with repo read for argocd-diff-preview GitHub API required: true SSH_PRIVATE_KEY: - description: 'SSH private key for ArgoCD to clone the repo. Required for private repos.' + description: Optional SSH key for private Git clone in Argo CD required: false REPO_SSH_URL: - description: 'SSH URL of the repo (e.g. git@github.com:org/repo.git). Required when SSH_PRIVATE_KEY is set.' + description: Optional SSH repo URL when using SSH_PRIVATE_KEY + required: false + SOPS_AGE_KEY: + description: Optional SOPS age key (helm-secrets) for ephemeral Argo CD required: false jobs: @@ -71,25 +85,23 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 - - name: Set up Helm - if: inputs.helm_chart_path != '' - uses: azure/setup-helm@v4 + - uses: ./.github/actions/argocd-diff-helm-template + with: + helm_chart_path: ${{ inputs.helm_chart_path }} + helm_values_files: ${{ inputs.helm_values_files }} + repo_path: ${{ inputs.repo_path }} + output_dir: /tmp/argocd-diff/target - - name: Prepare manifests — target branch + - name: Stage Argo CD Helm values from PR branch + if: inputs.argocd_config_dir != '' env: - HELM_CHART_PATH: ${{ inputs.helm_chart_path }} - VALUES_FILES: ${{ inputs.helm_values_files }} - REPO_PATH: ${{ inputs.repo_path }} + SRC_DIR: ${{ inputs.argocd_config_dir }} run: | - mkdir -p /tmp/argocd-diff/target - if [ -n "$HELM_CHART_PATH" ]; then - values_args=() - for f in $VALUES_FILES; do - values_args+=(-f "$f") - done - helm template "$HELM_CHART_PATH" "${values_args[@]}" --output-dir /tmp/argocd-diff/target + if [[ -d "$SRC_DIR" ]]; then + rm -rf /tmp/argocd-diff/argocd-config-custom + cp -a "$SRC_DIR" /tmp/argocd-diff/argocd-config-custom else - cp -r "$REPO_PATH/." /tmp/argocd-diff/target + echo "::warning::argocd_config_dir ($SRC_DIR) missing on PR branch — using default image config." fi - name: Checkout base branch @@ -98,70 +110,27 @@ jobs: ref: ${{ inputs.base_branch }} clean: false - - name: Prepare manifests — base branch - env: - HELM_CHART_PATH: ${{ inputs.helm_chart_path }} - VALUES_FILES: ${{ inputs.helm_values_files }} - REPO_PATH: ${{ inputs.repo_path }} - run: | - mkdir -p /tmp/argocd-diff/base - if [ -n "$HELM_CHART_PATH" ]; then - values_args=() - for f in $VALUES_FILES; do - values_args+=(-f "$f") - done - helm template "$HELM_CHART_PATH" "${values_args[@]}" --output-dir /tmp/argocd-diff/base - else - cp -r "$REPO_PATH/." /tmp/argocd-diff/base - fi - - - name: Prepare ArgoCD repo credentials - env: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} - REPO_SSH_URL: ${{ secrets.REPO_SSH_URL }} - run: | - mkdir -p /tmp/argocd-diff/secrets - if [ -n "$SSH_PRIVATE_KEY" ] && [ -n "$REPO_SSH_URL" ]; then - yq -n ' - .apiVersion = "v1" | - .kind = "Secret" | - .metadata.name = "private-repo" | - .metadata.namespace = "argocd" | - .metadata.labels["argocd.argoproj.io/secret-type"] = "repository" | - .stringData.type = "git" | - .stringData.url = strenv(REPO_SSH_URL) | - .stringData.sshPrivateKey = strenv(SSH_PRIVATE_KEY) - ' > /tmp/argocd-diff/secrets/repo-creds.yaml - fi + - uses: ./.github/actions/argocd-diff-helm-template + with: + helm_chart_path: ${{ inputs.helm_chart_path }} + helm_values_files: ${{ inputs.helm_values_files }} + repo_path: ${{ inputs.repo_path }} + output_dir: /tmp/argocd-diff/base - - name: Run argocd-diff-preview - env: - REPO: ${{ github.repository }} - TARGET_BRANCH: ${{ github.head_ref }} - BASE_BRANCH: ${{ inputs.base_branch }} - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - ARGOCD_CHART_VERSION: ${{ inputs.argocd_version }} - TIMEOUT: ${{ inputs.timeout }} - run: | - mkdir -p /tmp/argocd-diff/output - docker_args=( - --rm - --network=host - -v /var/run/docker.sock:/var/run/docker.sock - -v /tmp/argocd-diff/base:/base-branch - -v /tmp/argocd-diff/target:/target-branch - -v /tmp/argocd-diff/output:/output - -v /tmp/argocd-diff/secrets:/secrets - -e REPO - -e TARGET_BRANCH - -e BASE_BRANCH - -e GITHUB_TOKEN - -e TIMEOUT - ) - if [ -n "$ARGOCD_CHART_VERSION" ]; then - docker_args+=(-e ARGOCD_CHART_VERSION) - fi - docker run "${docker_args[@]}" dagandersen/argocd-diff-preview:latest + - uses: ./.github/actions/argocd-diff-run + with: + repo: ${{ github.repository }} + target_branch: ${{ github.head_ref }} + base_branch: ${{ inputs.base_branch }} + github_token: ${{ secrets.GH_PAT }} + timeout: ${{ inputs.timeout }} + argocd_chart_version: ${{ inputs.argocd_version }} + render_method: ${{ inputs.render_method }} + traverse_app_of_apps: ${{ inputs.traverse_app_of_apps }} + file_regex: ${{ inputs.file_regex }} + ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} + repo_ssh_url: ${{ secrets.REPO_SSH_URL }} + sops_age_key: ${{ secrets.SOPS_AGE_KEY }} - name: Post diff as PR comment if: always() @@ -170,27 +139,21 @@ jobs: DIFF_PATH: /tmp/argocd-diff/output/diff.md with: github-token: ${{ secrets.GITHUB_TOKEN }} + # Inline: caller workspace is not reusable-cicd; cannot require repo-local JS. script: | const fs = require('fs'); const diffPath = process.env.DIFF_PATH; - - let body; - if (fs.existsSync(diffPath)) { - body = fs.readFileSync(diffPath, 'utf8'); - } else { - body = '> **argocd-diff-preview**: no diff output found — check the workflow logs.'; - } - + let body = fs.existsSync(diffPath) + ? fs.readFileSync(diffPath, 'utf8') + : '> **argocd-diff-preview**: no diff output found — check the workflow logs.'; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); - const existing = comments.find( (c) => c.user.type === 'Bot' && c.body.includes('argocd-diff-preview') ); - if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, diff --git a/docs/actions/argocd-diff-preview.md b/docs/actions/argocd-diff-preview.md new file mode 100644 index 0000000..9c2667d --- /dev/null +++ b/docs/actions/argocd-diff-preview.md @@ -0,0 +1,99 @@ +# ArgoCD Diff Preview (composite actions) + +Two [composite actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) back the reusable workflow [Argo CD Diff Preview](../workflows/argocd-diff-preview.md). You can call them directly from other jobs if you need the same behaviour outside that workflow. + +**Source in this repository** + +| Action | Path | +| :--- | :--- | +| Prepare manifests (Helm or copy) | [`.github/actions/argocd-diff-helm-template/action.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/argocd-diff-helm-template/action.yml) | +| Secrets + `argocd-diff-preview` Docker | [`.github/actions/argocd-diff-run/action.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/argocd-diff-run/action.yml) | + +## argocd-diff-helm-template + +Runs `helm template … --output-dir` when `helm_chart_path` is set; otherwise copies `repo_path` into the output directory. Installs Helm only when needed. + +### Inputs + +| Input | Description | Required | Default | +| :--- | :--- | :--- | :--- | +| `helm_chart_path` | Chart path (relative to the checked-out repo). Empty ⇒ copy mode. | No | `''` | +| `helm_values_files` | Space-separated values files (passed as `-f` to Helm). | No | `''` | +| `repo_path` | Directory to copy when not using Helm. | No | `.` | +| `output_dir` | Absolute destination (e.g. `/tmp/argocd-diff/target`). | **Yes** | — | + +### Direct usage + +```yaml +- uses: actions/checkout@v4 + +- uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@main + with: + helm_chart_path: apps/ + helm_values_files: apps/values/prod.yaml + output_dir: /tmp/argocd-diff/target +``` + +Pin `@main` to the same ref as your reusable workflow (branch or tag). + +## argocd-diff-run + +Installs `yq`, optionally writes `repo-creds` and `sops-age-key` under `/tmp/argocd-diff/secrets/`, and runs the `dagandersen/argocd-diff-preview` image. + +Expects you have already prepared: + +- `/tmp/argocd-diff/base` and `/tmp/argocd-diff/target` (typically two invocations of **argocd-diff-helm-template** after different checkouts). +- Optionally `/tmp/argocd-diff/argocd-config-custom` (bind-mounted to `/argocd-config` in the container) if an earlier step copied your [custom Argo CD Helm values](https://dag-andersen.github.io/argocd-diff-preview/getting-started/custom-argo-cd-installation/) there. + +### Inputs + +| Input | Description | Required | Default | +| :--- | :--- | :--- | :--- | +| `repo` | GitHub repo `owner/name`. | **Yes** | — | +| `target_branch` | PR head branch name. | **Yes** | — | +| `base_branch` | Base branch to compare. | **Yes** | — | +| `github_token` | PAT with repo read access (tool GitHub API). | **Yes** | — | +| `timeout` | Timeout in seconds for `argocd-diff-preview`. | **Yes** | — | +| `argocd_chart_version` | Argo CD Helm chart version to install; empty ⇒ latest. | No | `''` | +| `render_method` | `cli`, `server-api`, or `repo-server-api`; empty ⇒ tool default. | No | `''` | +| `traverse_app_of_apps` | `true` / `false`. When `true`, forces `repo-server-api` if `render_method` is empty. | No | `false` | +| `file_regex` | File regex (passed as `FILE_REGEX` to the container). | No | `''` | +| `ssh_private_key` | Optional sensitive value — pass `${{ secrets.… }}` from the workflow. | No | `''` | +| `repo_ssh_url` | Optional; pair with `ssh_private_key` for private Git over SSH. | No | `''` | +| `sops_age_key` | Optional; pass `${{ secrets.… }}` (age key material) for helm-secrets. | No | `''` | + +Composite actions do not use a top-level `secrets:` block in `action.yml`; treat the three fields above as **inputs** whose values you set from the caller’s `secrets` context. GitHub masks them in logs when sourced from `secrets.*`. + +### Direct usage (skeleton) + +```yaml +- uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-run@main + with: + repo: ${{ github.repository }} + target_branch: ${{ github.head_ref }} + base_branch: main + github_token: ${{ secrets.GH_PAT }} + timeout: '300' + argocd_chart_version: '' + render_method: '' + traverse_app_of_apps: 'false' + file_regex: '' + ssh_private_key: ${{ secrets.ARGOCD_SSH_PRIVATE_KEY }} + repo_ssh_url: ${{ secrets.ARGOCD_REPO_SSH_URL }} + sops_age_key: ${{ secrets.SOPS_AGE_KEY }} +``` + +Omit optional `with` keys or pass empty strings if not needed. + +## How this relates to the reusable workflow + +[`.github/workflows/argocd-diff-preview.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/workflows/argocd-diff-preview.yml) checks out the PR branch, runs the Helm composite for **target**, optionally copies `argocd_config_dir` to `/tmp/argocd-diff/argocd-config-custom`, checks out the base branch, runs the Helm composite for **base**, then runs **argocd-diff-run**. For most repos, **call the workflow** instead of wiring these actions yourself. + +## Linting + +These files are **composite actions**, not workflows. Running [`actionlint`](https://github.com/rhysd/actionlint) on `action.yml` will fail with spurious errors (actionlint treats every argument as a workflow file). Lint only `.github/workflows/*.yml` instead. See [Actionlint → Scope](../workflows/actionlint.md#scope-workflows-only). + +## Runner requirements + +- Docker available (`ubuntu-latest` on GitHub-hosted runners is fine). +- Jobs using `AutomationDojo/reusable-cicd/.github/actions/...` need at least `contents: read` (plus whatever else you need for PR comments if you post the diff manually). diff --git a/docs/index.md b/docs/index.md index e5c1bd4..4bdd38b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,11 @@ A collection of reusable GitHub Actions workflows and composite actions for comm - [Helm Releaser](workflows/helm-releaser.md) — Automate packaging and publishing of Helm charts to GitHub Pages. - [ArgoCD Diff Preview](workflows/argocd-diff-preview.md) — Generate ArgoCD manifest diffs on pull requests and post them as PR comments. +## Composite actions + +- [Helm Repo Init](actions/helm-repo-init.md) — Bootstrap a chart repository from the helm chart template. +- [ArgoCD Diff Preview](actions/argocd-diff-preview.md) — Building blocks for the Argo CD diff workflow (Helm prep + Docker run). + ## GitHub App requirements Most workflows that push commits, tags, or releases use a GitHub App token instead of the default `GITHUB_TOKEN`. They expect two secrets to be configured at the repository or organization level: diff --git a/docs/workflows/actionlint.md b/docs/workflows/actionlint.md index 9475965..151486f 100644 --- a/docs/workflows/actionlint.md +++ b/docs/workflows/actionlint.md @@ -4,6 +4,15 @@ Reusable workflow that runs [actionlint](https://github.com/rhysd/actionlint) on This workflow also runs directly on this repository's PRs. +## Scope (workflows only) + +`actionlint` validates **workflow** YAML (`on`, `jobs`, …). It does **not** understand composite or JavaScript action metadata (`action.yml` with `runs:` / `inputs:`). If you run `actionlint .github/actions/foo/action.yml`, you will get false positives such as missing `jobs` — that invocation is invalid. Use: + +- `actionlint` (no arguments) — discovers `.github/workflows` in the repo, or +- `actionlint .github/workflows/some-workflow.yml` + +Do not pass `.github/actions/**/action.yml` as workflow files. For YAML/schema checks on composite actions, use another validator if needed. + ## Inputs | Name | Type | Default | Description | diff --git a/docs/workflows/argocd-diff-preview.md b/docs/workflows/argocd-diff-preview.md index fe7a76f..702bdce 100644 --- a/docs/workflows/argocd-diff-preview.md +++ b/docs/workflows/argocd-diff-preview.md @@ -2,12 +2,16 @@ Reusable workflow that generates an ArgoCD manifest diff for pull requests and posts it as a PR comment. It uses [argocd-diff-preview](https://github.com/dag-andersen/argocd-diff-preview) to spin up an ephemeral Kind cluster, render manifests through ArgoCD, and compute the diff. +This workflow is built from two **composite actions** (`argocd-diff-helm-template`, `argocd-diff-run`); see [**ArgoCD Diff Preview (composite actions)**](../actions/argocd-diff-preview.md) for inputs, secrets, and direct `uses:` examples. + The PR comment is created on first run and **updated in place** on subsequent runs — no comment spam. Supports two modes: - **Plain YAML** — point it at a directory that already contains `Application` manifests -- **Helm-rendered** — provide a chart path and values files; the workflow pre-renders the chart before diffing +- **Helm-rendered** — provide a chart path and values files; the workflow pre-renders the chart before diffing (this is the recommended way to handle “generated” child Applications — see [App of apps](https://dag-andersen.github.io/argocd-diff-preview/app-of-apps/), Option 1). + +Optional **`traverse_app_of_apps`** + **`render_method: repo-server-api`** matches [Option 2 in the same doc](https://dag-andersen.github.io/argocd-diff-preview/app-of-apps/) when you cannot pre-render; it is experimental and slower. For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller is responsible for obtaining these values (e.g. extracting from a secrets manager) before calling this workflow. @@ -15,9 +19,10 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller 1. Checkout the PR branch → prepare manifests (copy or `helm template`) → saved to a temp folder 2. Checkout the base branch → same preparation → saved to another temp folder -3. If `SSH_PRIVATE_KEY` and `REPO_SSH_URL` are provided, generate an ArgoCD repository secret YAML in `/secrets/` -4. `argocd-diff-preview` spins up a Kind cluster, installs ArgoCD (with the repo credentials), renders both sets of manifests, and produces a `diff.md` -5. The diff is posted (or updated) as a PR comment +3. If `SSH_PRIVATE_KEY` and `REPO_SSH_URL` are provided, generate an ArgoCD repository secret YAML in `/secrets/`; if `SOPS_AGE_KEY` is set, generate `sops-age-key` for helm-secrets in ephemeral Argo CD +4. Optionally mounts `argocd_config_dir` on `/argocd-config` (see [custom Argo CD installation](https://dag-andersen.github.io/argocd-diff-preview/getting-started/custom-argo-cd-installation/)) +5. `argocd-diff-preview` spins up a Kind cluster, installs ArgoCD (with the repo credentials), renders both sets of manifests, and produces a `diff.md` +6. The diff is posted (or updated) as a PR comment > **Note**: The ephemeral cluster adds ~60–90 seconds to each run. @@ -31,6 +36,10 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller | `base_branch` | Branch to compare against. | No | `main` | | `argocd_version` | ArgoCD Helm chart version to install in the ephemeral cluster. When empty, uses the latest. | No | — | | `timeout` | Timeout in seconds for argocd-diff-preview. | No | `300` | +| `argocd_config_dir` | Directory on the PR branch to mount as `/argocd-config` (e.g. `values.yaml` + `values-override.yaml` for helm-secrets / CMPs). | No | — | +| `render_method` | `cli`, `server-api`, or `repo-server-api`. Empty = tool default. Required `repo-server-api` if `traverse_app_of_apps` is true (enforced when traverse is set and this is empty). | No | — | +| `traverse_app_of_apps` | Experimental expansion of child Applications (requires `repo-server-api`). Prefer Helm pre-render when children are templated. | No | `false` | +| `file_regex` | Passed as `--file-regex` (e.g. only root app YAML when using traverse). | No | — | ## Secrets @@ -39,6 +48,7 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller | `GH_PAT` | Yes | GitHub PAT with `repo` (read) scope. Required for argocd-diff-preview to interact with the GitHub API on private repositories. | | `SSH_PRIVATE_KEY` | No | SSH private key for ArgoCD to clone the repo. Required for private repos. | | `REPO_SSH_URL` | No | SSH URL of the repo (e.g. `git@github.com:org/repo.git`). Required when `SSH_PRIVATE_KEY` is set. | +| `SOPS_AGE_KEY` | No | Contents of your SOPS age private key file (`age-key.txt`). Required if ephemeral Argo CD is configured for helm-secrets. | ## Caller permissions diff --git a/mkdocs.yml b/mkdocs.yml index ba60ad5..32a5d1c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,7 @@ nav: - Home: index.md - Actions: - Helm Repo Init: actions/helm-repo-init.md + - ArgoCD Diff Preview: actions/argocd-diff-preview.md - Workflows: - Actionlint: workflows/actionlint.md - Cloudflare Pages Cleanup: workflows/cloudflare-pages-cleanup.md From 70b18d7802c37037915b8b6f721c22372466fb10 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:28:50 +0100 Subject: [PATCH 02/12] feat: update argocd-diff action --- .github/workflows/argocd-diff-preview.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index af0ec94..f039b2b 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -85,7 +85,9 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 - - uses: ./.github/actions/argocd-diff-helm-template + # workflow_call uses the caller repo as workspace; ./.github/actions would be wrong. + # Composites are pinned to @main; for local tests of action changes, temporarily use your branch. + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@fix/argocd-diff with: helm_chart_path: ${{ inputs.helm_chart_path }} helm_values_files: ${{ inputs.helm_values_files }} @@ -110,14 +112,14 @@ jobs: ref: ${{ inputs.base_branch }} clean: false - - uses: ./.github/actions/argocd-diff-helm-template + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@fix/argocd-diff with: helm_chart_path: ${{ inputs.helm_chart_path }} helm_values_files: ${{ inputs.helm_values_files }} repo_path: ${{ inputs.repo_path }} output_dir: /tmp/argocd-diff/base - - uses: ./.github/actions/argocd-diff-run + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-run@fix/argocd-diff with: repo: ${{ github.repository }} target_branch: ${{ github.head_ref }} From 678a7700d9029fdc15b3aa4a7ccbeaa54ef04d93 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:38:22 +0100 Subject: [PATCH 03/12] feat: update argocd-diff action --- .github/actions/argocd-diff-run/action.yml | 6 + .github/scripts/post-argocd-diff-comment.js | 203 +++++++++++++++++--- .github/workflows/argocd-diff-preview.yml | 57 +++--- 3 files changed, 213 insertions(+), 53 deletions(-) diff --git a/.github/actions/argocd-diff-run/action.yml b/.github/actions/argocd-diff-run/action.yml index 5372f98..8370970 100644 --- a/.github/actions/argocd-diff-run/action.yml +++ b/.github/actions/argocd-diff-run/action.yml @@ -44,6 +44,10 @@ inputs: description: Optional — SOPS age key material for helm-secrets; set from a workflow secret in the caller required: false default: '' + max_diff_length: + description: argocd-diff-preview --max-diff-length (chars in diff.md); PR comments are split separately to fit GitHub API + required: false + default: '1048576' runs: using: composite steps: @@ -89,6 +93,7 @@ runs: RENDER_METHOD_IN: ${{ inputs.render_method }} TRAVERSE_APP_OF_APPS: ${{ inputs.traverse_app_of_apps }} FILE_REGEX: ${{ inputs.file_regex }} + MAX_DIFF_LENGTH: ${{ inputs.max_diff_length }} run: | mkdir -p /tmp/argocd-diff/output docker_args=( @@ -99,6 +104,7 @@ runs: -v /tmp/argocd-diff/output:/output -v /tmp/argocd-diff/secrets:/secrets -e REPO -e TARGET_BRANCH -e BASE_BRANCH -e GITHUB_TOKEN -e TIMEOUT + -e MAX_DIFF_LENGTH ) if [[ -n "${ARGOCD_CHART_VERSION:-}" ]]; then docker_args+=(-e ARGOCD_CHART_VERSION) diff --git a/.github/scripts/post-argocd-diff-comment.js b/.github/scripts/post-argocd-diff-comment.js index e02436d..436c40b 100644 --- a/.github/scripts/post-argocd-diff-comment.js +++ b/.github/scripts/post-argocd-diff-comment.js @@ -1,41 +1,194 @@ -// Posts (or updates) an argocd-diff-preview diff as a PR comment. -// If a bot comment containing "argocd-diff-preview" already exists, it is updated in place. -// Expected env vars: DIFF_PATH (path to diff.md), GITHUB_TOKEN (set by Actions automatically) +// Posts argocd-diff-preview output as PR comment(s). Prefers one GitHub comment per Application +// (
block in diff.md); falls back to size-based chunking. Markers allow re-runs to sync. const fs = require('fs'); +const MARKER_RE = //; + +/** Stay under GitHub's ~65536 cap including per-part header */ +const MAX_CHUNK_CHARS = 65200; + +/** + * argocd-diff-preview renders each app as
+ * (see upstream pkg/diff/markdown.go). Split into: [preamble, app1, app2, …] + optional trailer + * after the last
(selection_changes, stats info_box). + */ +function splitByApplicationDetails(raw) { + const firstIdx = raw.indexOf('
'); + if (firstIdx === -1) return null; + + const preamble = raw.slice(0, firstIdx).trimEnd(); + const fromFirst = raw.slice(firstIdx); + + const re = /
[\s\S]*?<\/details>\n*/g; + const blocks = []; + let m; + let endPos = 0; + while ((m = re.exec(fromFirst)) !== null) { + blocks.push(m[0].trim()); + endPos = re.lastIndex; + } + if (blocks.length === 0) return null; + + const trailer = fromFirst.slice(endPos).trim(); + const chunks = []; + if (preamble.length) chunks.push(preamble); + for (const b of blocks) chunks.push(b); + if (trailer.length) { + const last = chunks[chunks.length - 1]; + chunks[chunks.length - 1] = `${last}\n\n${trailer}`; + } + return chunks; +} + +function splitContent(text) { + if (text.length <= MAX_CHUNK_CHARS) return [text]; + + const lines = text.split('\n'); + const chunks = []; + let cur = ''; + + const flush = () => { + if (cur.length) { + chunks.push(cur); + cur = ''; + } + }; + + for (const line of lines) { + const sep = cur ? '\n' : ''; + const candidate = cur + sep + line; + + if (candidate.length > MAX_CHUNK_CHARS && cur.length > 0) { + flush(); + cur = line; + while (cur.length > MAX_CHUNK_CHARS) { + chunks.push(cur.slice(0, MAX_CHUNK_CHARS)); + cur = cur.slice(MAX_CHUNK_CHARS); + } + } else if (candidate.length > MAX_CHUNK_CHARS && cur.length === 0) { + let rest = line; + while (rest.length > MAX_CHUNK_CHARS) { + chunks.push(rest.slice(0, MAX_CHUNK_CHARS)); + rest = rest.slice(MAX_CHUNK_CHARS); + } + cur = rest; + } else { + cur = candidate; + } + } + flush(); + return chunks; +} + +/** One comment per app when structure matches; oversized sections still split by size */ +function chunkForPosting(raw) { + const byApp = splitByApplicationDetails(raw); + if (!byApp) { + return splitContent(raw); + } + const out = []; + for (const piece of byApp) { + if (piece.length <= MAX_CHUNK_CHARS) { + out.push(piece); + } else { + out.push(...splitContent(piece)); + } + } + return out; +} + +function buildBodies(chunks) { + const n = chunks.length; + return chunks.map((content, idx) => { + const i = idx + 1; + const header = + n <= 1 + ? '\n\n' + : `\n\n_(${i}/${n}) · **argocd-diff-preview**_\n\n`; + return header + content; + }); +} + +function partOrder(body) { + const m = body.match(MARKER_RE); + return m ? parseInt(m[1], 10) : 0; +} + module.exports = async ({ github, context }) => { const diffPath = process.env.DIFF_PATH; - - let body; - if (fs.existsSync(diffPath)) { - body = fs.readFileSync(diffPath, 'utf8'); + let raw; + if (diffPath && fs.existsSync(diffPath)) { + raw = fs.readFileSync(diffPath, 'utf8'); } else { - body = '> **argocd-diff-preview**: no diff output found — check the workflow logs.'; + raw = '> **argocd-diff-preview**: no diff output found — check the workflow logs.'; } - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, + const chunks = chunkForPosting(raw); + const bodies = buildBodies(chunks); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.issue.number; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, }); - const existing = comments.find( - (c) => c.user.type === 'Bot' && c.body.includes('argocd-diff-preview') + const ours = comments + .filter((c) => c.user.type === 'Bot' && MARKER_RE.test(c.body)) + .sort((a, b) => partOrder(a.body) - partOrder(b.body)); + + const legacy = comments.find( + (c) => + c.user.type === 'Bot' && + c.body.includes('argocd-diff-preview') && + !MARKER_RE.test(c.body) ); - if (existing) { + if (ours.length === 0 && legacy) { await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, + owner, + repo, + comment_id: legacy.id, + body: bodies[0], }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, + for (let j = 1; j < bodies.length; j++) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: bodies[j], + }); + } + return; + } + + for (let i = 0; i < bodies.length; i++) { + if (i < ours.length) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: ours[i].id, + body: bodies[i], + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: bodies[i], + }); + } + } + + for (let i = bodies.length; i < ours.length; i++) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: ours[i].id, }); } }; diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index f039b2b..e5c47ed 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -56,6 +56,16 @@ on: required: false type: string default: '' + max_diff_length: + description: Max characters in diff.md before argocd-diff-preview truncates (PR comment text is split to fit GitHub API) + required: false + type: number + default: 1048576 + reusable_cicd_ref: + description: Ref for shallow checkout of AutomationDojo/reusable-cicd (loads PR comment script only) + required: false + type: string + default: 'main' secrets: GH_PAT: description: PAT with repo read for argocd-diff-preview GitHub API @@ -77,6 +87,7 @@ jobs: permissions: contents: read pull-requests: write + issues: write steps: - name: Checkout target branch (PR) @@ -133,6 +144,16 @@ jobs: ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} repo_ssh_url: ${{ secrets.REPO_SSH_URL }} sops_age_key: ${{ secrets.SOPS_AGE_KEY }} + max_diff_length: ${{ inputs.max_diff_length }} + + - name: Checkout reusable-cicd (PR comment script) + if: always() + uses: actions/checkout@v4 + with: + repository: AutomationDojo/reusable-cicd + ref: ${{ inputs.reusable_cicd_ref }} + path: _reusable-cicd + token: ${{ secrets.GH_PAT }} - name: Post diff as PR comment if: always() @@ -141,33 +162,13 @@ jobs: DIFF_PATH: /tmp/argocd-diff/output/diff.md with: github-token: ${{ secrets.GITHUB_TOKEN }} - # Inline: caller workspace is not reusable-cicd; cannot require repo-local JS. script: | - const fs = require('fs'); - const diffPath = process.env.DIFF_PATH; - let body = fs.existsSync(diffPath) - ? fs.readFileSync(diffPath, 'utf8') - : '> **argocd-diff-preview**: no diff output found — check the workflow logs.'; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find( - (c) => c.user.type === 'Bot' && c.body.includes('argocd-diff-preview') + const path = require('path'); + const scriptPath = path.join( + process.env.GITHUB_WORKSPACE, + '_reusable-cicd', + '.github', + 'scripts', + 'post-argocd-diff-comment.js' ); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } + await require(scriptPath)({ github, context }); From 983019f7b49d6692a576cbc7670f65026a090190 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:44:15 +0100 Subject: [PATCH 04/12] feat: update argocd-diff action --- .github/workflows/argocd-diff-preview.yml | 31 +++-------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index e5c47ed..0c34808 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -61,11 +61,6 @@ on: required: false type: number default: 1048576 - reusable_cicd_ref: - description: Ref for shallow checkout of AutomationDojo/reusable-cicd (loads PR comment script only) - required: false - type: string - default: 'main' secrets: GH_PAT: description: PAT with repo read for argocd-diff-preview GitHub API @@ -146,29 +141,9 @@ jobs: sops_age_key: ${{ secrets.SOPS_AGE_KEY }} max_diff_length: ${{ inputs.max_diff_length }} - - name: Checkout reusable-cicd (PR comment script) - if: always() - uses: actions/checkout@v4 - with: - repository: AutomationDojo/reusable-cicd - ref: ${{ inputs.reusable_cicd_ref }} - path: _reusable-cicd - token: ${{ secrets.GH_PAT }} - - name: Post diff as PR comment if: always() - uses: actions/github-script@v7 - env: - DIFF_PATH: /tmp/argocd-diff/output/diff.md + uses: AutomationDojo/reusable-cicd/.github/actions/post-argocd-diff-comment@fix/argocd-diff with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const path = require('path'); - const scriptPath = path.join( - process.env.GITHUB_WORKSPACE, - '_reusable-cicd', - '.github', - 'scripts', - 'post-argocd-diff-comment.js' - ); - await require(scriptPath)({ github, context }); + github_token: ${{ secrets.GITHUB_TOKEN }} + diff_path: /tmp/argocd-diff/output/diff.md From 6cf9fc28089e9c056b1de4cd8780dcab52ff5aa1 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:45:10 +0100 Subject: [PATCH 05/12] feat: update argocd-diff action --- .../post-argocd-diff-comment/action.yml | 26 +++++++++++++++++++ .../post-argocd-diff-comment.js | 0 2 files changed, 26 insertions(+) create mode 100644 .github/actions/post-argocd-diff-comment/action.yml rename .github/{scripts => actions/post-argocd-diff-comment}/post-argocd-diff-comment.js (100%) diff --git a/.github/actions/post-argocd-diff-comment/action.yml b/.github/actions/post-argocd-diff-comment/action.yml new file mode 100644 index 0000000..4867a22 --- /dev/null +++ b/.github/actions/post-argocd-diff-comment/action.yml @@ -0,0 +1,26 @@ +name: Post Argo CD diff preview PR comments +description: >- + Publish argocd-diff-preview diff.md as one or more issue comments (split by
app and GitHub size limit). + +inputs: + github_token: + description: Token for issues API on the caller repo (e.g. secrets.GITHUB_TOKEN) + required: true + diff_path: + description: Absolute path to diff.md on the runner + required: true + +runs: + using: composite + steps: + - name: Post diff as PR comment(s) + uses: actions/github-script@v7 + env: + DIFF_PATH: ${{ inputs.diff_path }} + ADP_COMMENT_SCRIPT_DIR: ${{ github.action_path }} + with: + github-token: ${{ inputs.github_token }} + script: | + const path = require('path'); + const scriptPath = path.join(process.env.ADP_COMMENT_SCRIPT_DIR, 'post-argocd-diff-comment.js'); + await require(scriptPath)({ github, context }); diff --git a/.github/scripts/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js similarity index 100% rename from .github/scripts/post-argocd-diff-comment.js rename to .github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js From 6557e3b02045a6e850fb3f0c75f95ae4e488ce9c Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:46:38 +0100 Subject: [PATCH 06/12] feat: update docs --- docs/actions/argocd-diff-preview.md | 37 ++++++++++++++++++++++++--- docs/index.md | 2 +- docs/workflows/argocd-diff-preview.md | 19 +++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/docs/actions/argocd-diff-preview.md b/docs/actions/argocd-diff-preview.md index 9c2667d..f8e1bfa 100644 --- a/docs/actions/argocd-diff-preview.md +++ b/docs/actions/argocd-diff-preview.md @@ -1,6 +1,6 @@ # ArgoCD Diff Preview (composite actions) -Two [composite actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) back the reusable workflow [Argo CD Diff Preview](../workflows/argocd-diff-preview.md). You can call them directly from other jobs if you need the same behaviour outside that workflow. +Three [composite actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action) back the reusable workflow [Argo CD Diff Preview](../workflows/argocd-diff-preview.md). You can call them directly from other jobs if you need the same behaviour outside that workflow. **Source in this repository** @@ -8,6 +8,7 @@ Two [composite actions](https://docs.github.com/en/actions/creating-actions/crea | :--- | :--- | | Prepare manifests (Helm or copy) | [`.github/actions/argocd-diff-helm-template/action.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/argocd-diff-helm-template/action.yml) | | Secrets + `argocd-diff-preview` Docker | [`.github/actions/argocd-diff-run/action.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/argocd-diff-run/action.yml) | +| Post `diff.md` as PR comment(s) | [`.github/actions/post-argocd-diff-comment/action.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/post-argocd-diff-comment/action.yml) | ## argocd-diff-helm-template @@ -61,8 +62,9 @@ Expects you have already prepared: | `ssh_private_key` | Optional sensitive value — pass `${{ secrets.… }}` from the workflow. | No | `''` | | `repo_ssh_url` | Optional; pair with `ssh_private_key` for private Git over SSH. | No | `''` | | `sops_age_key` | Optional; pass `${{ secrets.… }}` (age key material) for helm-secrets. | No | `''` | +| `max_diff_length` | Passed as `MAX_DIFF_LENGTH` to the container (`--max-diff-length` in the tool). Default avoids truncating large monorepo diffs too early. | No | `1048576` | -Composite actions do not use a top-level `secrets:` block in `action.yml`; treat the three fields above as **inputs** whose values you set from the caller’s `secrets` context. GitHub masks them in logs when sourced from `secrets.*`. +Composite actions do not use a top-level `secrets:` block in `action.yml`; treat the SSH/SOPS fields above as **inputs** whose values you set from the caller’s `secrets` context. GitHub masks them in logs when sourced from `secrets.*`. ### Direct usage (skeleton) @@ -85,9 +87,38 @@ Composite actions do not use a top-level `secrets:` block in `action.yml`; treat Omit optional `with` keys or pass empty strings if not needed. +## post-argocd-diff-comment + +Wraps [`actions/github-script`](https://github.com/actions/github-script) and loads [`post-argocd-diff-comment.js`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js) from the action directory (`github.action_path`), so callers do **not** need a separate checkout of `reusable-cicd`. + +### Inputs + +| Input | Description | Required | Default | +| :--- | :--- | :--- | :--- | +| `github_token` | Token for the **caller** repository (e.g. `secrets.GITHUB_TOKEN`) — needs permission to list/create/update/delete issue comments on the PR. | **Yes** | — | +| `diff_path` | Absolute path to `diff.md` (e.g. `/tmp/argocd-diff/output/diff.md`). | **Yes** | — | + +### Behaviour (summary) + +- Splits content into multiple comments when needed: preferably **one GitHub comment per Argo CD Application** (each `
` section produced by argocd-diff-preview), plus the summary block before the first `
`, and merges trailing stats/selection text into the last app chunk. +- If a section still exceeds GitHub’s per-comment size limit, splits that section by size. +- Uses HTML markers in each comment body so re-runs update the same set of comments and remove extras. + +### Direct usage + +```yaml +- uses: AutomationDojo/reusable-cicd/.github/actions/post-argocd-diff-comment@main + if: always() + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + diff_path: /tmp/argocd-diff/output/diff.md +``` + +Callers need `issues: write` (and `pull-requests: write` as usual) when using `GITHUB_TOKEN` — see the [workflow doc](../workflows/argocd-diff-preview.md#caller-permissions). + ## How this relates to the reusable workflow -[`.github/workflows/argocd-diff-preview.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/workflows/argocd-diff-preview.yml) checks out the PR branch, runs the Helm composite for **target**, optionally copies `argocd_config_dir` to `/tmp/argocd-diff/argocd-config-custom`, checks out the base branch, runs the Helm composite for **base**, then runs **argocd-diff-run**. For most repos, **call the workflow** instead of wiring these actions yourself. +[`.github/workflows/argocd-diff-preview.yml`](https://github.com/AutomationDojo/reusable-cicd/blob/main/.github/workflows/argocd-diff-preview.yml) checks out the PR branch, runs the Helm composite for **target**, optionally copies `argocd_config_dir` to `/tmp/argocd-diff/argocd-config-custom`, checks out the base branch, runs the Helm composite for **base**, then runs **argocd-diff-run**, then **post-argocd-diff-comment**. For most repos, **call the workflow** instead of wiring these actions yourself. ## Linting diff --git a/docs/index.md b/docs/index.md index 4bdd38b..e828426 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ A collection of reusable GitHub Actions workflows and composite actions for comm ## Composite actions - [Helm Repo Init](actions/helm-repo-init.md) — Bootstrap a chart repository from the helm chart template. -- [ArgoCD Diff Preview](actions/argocd-diff-preview.md) — Building blocks for the Argo CD diff workflow (Helm prep + Docker run). +- [ArgoCD Diff Preview](actions/argocd-diff-preview.md) — Building blocks for the Argo CD diff workflow (Helm prep + Docker run + PR comments). ## GitHub App requirements diff --git a/docs/workflows/argocd-diff-preview.md b/docs/workflows/argocd-diff-preview.md index 702bdce..18c1cb0 100644 --- a/docs/workflows/argocd-diff-preview.md +++ b/docs/workflows/argocd-diff-preview.md @@ -2,9 +2,9 @@ Reusable workflow that generates an ArgoCD manifest diff for pull requests and posts it as a PR comment. It uses [argocd-diff-preview](https://github.com/dag-andersen/argocd-diff-preview) to spin up an ephemeral Kind cluster, render manifests through ArgoCD, and compute the diff. -This workflow is built from two **composite actions** (`argocd-diff-helm-template`, `argocd-diff-run`); see [**ArgoCD Diff Preview (composite actions)**](../actions/argocd-diff-preview.md) for inputs, secrets, and direct `uses:` examples. +This workflow is built from three **composite actions** (`argocd-diff-helm-template`, `argocd-diff-run`, `post-argocd-diff-comment`); see [**ArgoCD Diff Preview (composite actions)**](../actions/argocd-diff-preview.md) for inputs, secrets, and direct `uses:` examples. -The PR comment is created on first run and **updated in place** on subsequent runs — no comment spam. +**PR comments:** the diff is published with the **issue comments** API (same thread as the PR conversation). The first run creates one or more comments; later runs **update** those comments when possible. If a new run produces **fewer** chunks than before (e.g. fewer applications), surplus bot comments are **deleted** — that requires `issues: write` (see [Caller permissions](#caller-permissions)). Comments are split by default **one per Argo CD Application** (each `
` block in `diff.md`), with an extra chunk for the summary preamble; very large apps may be split further to stay under GitHub’s ~64 KiB body limit per comment. Supports two modes: @@ -21,8 +21,8 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller 2. Checkout the base branch → same preparation → saved to another temp folder 3. If `SSH_PRIVATE_KEY` and `REPO_SSH_URL` are provided, generate an ArgoCD repository secret YAML in `/secrets/`; if `SOPS_AGE_KEY` is set, generate `sops-age-key` for helm-secrets in ephemeral Argo CD 4. Optionally mounts `argocd_config_dir` on `/argocd-config` (see [custom Argo CD installation](https://dag-andersen.github.io/argocd-diff-preview/getting-started/custom-argo-cd-installation/)) -5. `argocd-diff-preview` spins up a Kind cluster, installs ArgoCD (with the repo credentials), renders both sets of manifests, and produces a `diff.md` -6. The diff is posted (or updated) as a PR comment +5. `argocd-diff-preview` spins up a Kind cluster, installs ArgoCD (with the repo credentials), renders both sets of manifests, and produces a `diff.md` (length capped by `max_diff_length`, forwarded as `MAX_DIFF_LENGTH` to the tool) +6. **`post-argocd-diff-comment`** reads `diff.md` and creates/updates/deletes PR comments as needed > **Note**: The ephemeral cluster adds ~60–90 seconds to each run. @@ -40,6 +40,7 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller | `render_method` | `cli`, `server-api`, or `repo-server-api`. Empty = tool default. Required `repo-server-api` if `traverse_app_of_apps` is true (enforced when traverse is set and this is empty). | No | — | | `traverse_app_of_apps` | Experimental expansion of child Applications (requires `repo-server-api`). Prefer Helm pre-render when children are templated. | No | `false` | | `file_regex` | Passed as `--file-regex` (e.g. only root app YAML when using traverse). | No | — | +| `max_diff_length` | Max size (characters) of `diff.md` before [argocd-diff-preview](https://github.com/dag-andersen/argocd-diff-preview) truncates (`--max-diff-length`). Raise for large repos; PR comment bodies are still split to fit GitHub’s API. | No | `1048576` | ## Secrets @@ -52,14 +53,17 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller ## Caller permissions -The calling workflow must set: +The calling workflow must set at least: ```yaml permissions: contents: read pull-requests: write + issues: write ``` +`pull-requests: write` covers the PR; **`issues: write`** is needed because conversation comments are created/updated/deleted via the [issue comments](https://docs.github.com/en/rest/issues/comments) API (`issues.createComment`, `issues.updateComment`, `issues.deleteComment`). This is not the Issues tab — PRs are issues under the hood. + ## Usage ### Plain YAML repo (public) @@ -74,6 +78,7 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: argocd-diff: @@ -101,6 +106,7 @@ on: permissions: contents: read pull-requests: write + issues: write jobs: argocd-diff: @@ -118,4 +124,5 @@ jobs: - `SSH_PRIVATE_KEY` and `REPO_SSH_URL` should be stored as GitHub Actions secrets in the caller repository (**Settings → Secrets → Actions**). - The workflow uses Docker and requires a `ubuntu-latest` runner with Docker available (default on GitHub-hosted runners). -- ArgoCD version can be pinned via `argocd_version` to ensure consistent rendering across runs. \ No newline at end of file +- ArgoCD version can be pinned via `argocd_version` to ensure consistent rendering across runs. +- Composite actions in this repo are referenced as `AutomationDojo/reusable-cicd/.github/actions/...@ref` from the workflow file — pin `@main` (or a release tag) in production; the ref must expose the same workflow and action versions you expect. \ No newline at end of file From df091d2cee86b27e78657d0987c547b9b2c2bca1 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:51:34 +0100 Subject: [PATCH 07/12] feat: update argocd-diff action --- .../post-argocd-diff-comment.js | 59 ++++++++++++++----- docs/actions/argocd-diff-preview.md | 5 +- docs/workflows/argocd-diff-preview.md | 2 +- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js index 436c40b..adede88 100644 --- a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js +++ b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js @@ -1,11 +1,12 @@ -// Posts argocd-diff-preview output as PR comment(s). Prefers one GitHub comment per Application -// (
block in diff.md); falls back to size-based chunking. Markers allow re-runs to sync. +// Posts argocd-diff-preview output as PR comment(s). Parses per-app
blocks, then packs +// multiple sections into one comment until ~64KiB (GitHub API limit). Splits further only when needed. +// Markers allow re-runs to sync. const fs = require('fs'); const MARKER_RE = //; -/** Stay under GitHub's ~65536 cap including per-part header */ -const MAX_CHUNK_CHARS = 65200; +/** Body budget per comment (GitHub ~65536; leave margin for HTML marker + title line) */ +const MAX_CHUNK_CHARS = 64500; /** * argocd-diff-preview renders each app as
@@ -80,21 +81,49 @@ function splitContent(text) { return chunks; } -/** One comment per app when structure matches; oversized sections still split by size */ +/** + * Merge consecutive sections into one comment until `budget` chars; only then start a new comment. + * A single section larger than `budget` is split with splitContent (last resort). + */ +function packSegments(segments, budget) { + const buckets = []; + let cur = ''; + + const flush = () => { + if (cur.length) { + buckets.push(cur); + cur = ''; + } + }; + + for (const seg of segments) { + if (!seg.length) continue; + + if (seg.length > budget) { + flush(); + buckets.push(...splitContent(seg)); + continue; + } + + const sep = cur.length ? '\n\n' : ''; + const candidate = cur + sep + seg; + if (candidate.length <= budget) { + cur = candidate; + } else { + flush(); + cur = seg; + } + } + flush(); + return buckets; +} + function chunkForPosting(raw) { const byApp = splitByApplicationDetails(raw); if (!byApp) { return splitContent(raw); } - const out = []; - for (const piece of byApp) { - if (piece.length <= MAX_CHUNK_CHARS) { - out.push(piece); - } else { - out.push(...splitContent(piece)); - } - } - return out; + return packSegments(byApp, MAX_CHUNK_CHARS); } function buildBodies(chunks) { @@ -104,7 +133,7 @@ function buildBodies(chunks) { const header = n <= 1 ? '\n\n' - : `\n\n_(${i}/${n}) · **argocd-diff-preview**_\n\n`; + : `\n\n**argocd-diff-preview** · part ${i} of ${n} _(same run; split for GitHub ~64KB limit per comment)_\n\n`; return header + content; }); } diff --git a/docs/actions/argocd-diff-preview.md b/docs/actions/argocd-diff-preview.md index f8e1bfa..0e6cf74 100644 --- a/docs/actions/argocd-diff-preview.md +++ b/docs/actions/argocd-diff-preview.md @@ -100,8 +100,9 @@ Wraps [`actions/github-script`](https://github.com/actions/github-script) and lo ### Behaviour (summary) -- Splits content into multiple comments when needed: preferably **one GitHub comment per Argo CD Application** (each `
` section produced by argocd-diff-preview), plus the summary block before the first `
`, and merges trailing stats/selection text into the last app chunk. -- If a section still exceeds GitHub’s per-comment size limit, splits that section by size. +- Parses `diff.md` into segments (summary preamble, then each `
` app section, with trailing stats merged into the last segment as produced by argocd-diff-preview). +- **Packs** consecutive segments into one GitHub comment until the ~64 KiB body limit, so you usually get a few comments instead of dozens. +- If one segment alone is still too large, splits only that segment by size. - Uses HTML markers in each comment body so re-runs update the same set of comments and remove extras. ### Direct usage diff --git a/docs/workflows/argocd-diff-preview.md b/docs/workflows/argocd-diff-preview.md index 18c1cb0..2e33f6a 100644 --- a/docs/workflows/argocd-diff-preview.md +++ b/docs/workflows/argocd-diff-preview.md @@ -4,7 +4,7 @@ Reusable workflow that generates an ArgoCD manifest diff for pull requests and p This workflow is built from three **composite actions** (`argocd-diff-helm-template`, `argocd-diff-run`, `post-argocd-diff-comment`); see [**ArgoCD Diff Preview (composite actions)**](../actions/argocd-diff-preview.md) for inputs, secrets, and direct `uses:` examples. -**PR comments:** the diff is published with the **issue comments** API (same thread as the PR conversation). The first run creates one or more comments; later runs **update** those comments when possible. If a new run produces **fewer** chunks than before (e.g. fewer applications), surplus bot comments are **deleted** — that requires `issues: write` (see [Caller permissions](#caller-permissions)). Comments are split by default **one per Argo CD Application** (each `
` block in `diff.md`), with an extra chunk for the summary preamble; very large apps may be split further to stay under GitHub’s ~64 KiB body limit per comment. +**PR comments:** the diff is published with the **issue comments** API (same thread as the PR conversation). The first run creates one or more comments; later runs **update** those comments when possible. If a new run produces **fewer** chunks than before, surplus bot comments are **deleted** — that requires `issues: write` (see [Caller permissions](#caller-permissions)). The post step parses each Argo CD app as a `
` block in `diff.md`, then **packs** several blocks into the same GitHub comment until the ~64 KiB API limit, so you typically get a **small number** of comments instead of one per app. Only when a single app (or the summary) is larger than that limit is it split across multiple comments. Supports two modes: From 9089edf9a0bbd337e690cbd51c60383fdcba05a5 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 19:56:35 +0100 Subject: [PATCH 08/12] feat: update argocd-diff action --- .../post-argocd-diff-comment.js | 86 ++++++++++++++----- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js index adede88..204e702 100644 --- a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js +++ b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js @@ -41,43 +41,83 @@ function splitByApplicationDetails(raw) { return chunks; } +/** Room to append closing ``` and start ```diff on the next chunk without exceeding GitHub limit */ +const FENCE_SPLIT_RESERVE = 200; + +function isFenceLine(line) { + return /^\s*```/.test(line); +} + +/** + * Split oversized markdown without breaking fenced code blocks (```diff … ```). + * Plain line splits corrupt GitHub rendering from the first broken fence onward. + */ function splitContent(text) { if (text.length <= MAX_CHUNK_CHARS) return [text]; const lines = text.split('\n'); const chunks = []; let cur = ''; + let inFence = false; - const flush = () => { - if (cur.length) { - chunks.push(cur); - cur = ''; - } - }; + function tickFenceForLine(line) { + if (isFenceLine(line)) inFence = !inFence; + } + + function pushChunk(body) { + if (body.length) chunks.push(body); + } for (const line of lines) { - const sep = cur ? '\n' : ''; + const sep = cur.length ? '\n' : ''; const candidate = cur + sep + line; + const softLimit = inFence ? MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE : MAX_CHUNK_CHARS; - if (candidate.length > MAX_CHUNK_CHARS && cur.length > 0) { - flush(); - cur = line; - while (cur.length > MAX_CHUNK_CHARS) { - chunks.push(cur.slice(0, MAX_CHUNK_CHARS)); - cur = cur.slice(MAX_CHUNK_CHARS); - } - } else if (candidate.length > MAX_CHUNK_CHARS && cur.length === 0) { - let rest = line; - while (rest.length > MAX_CHUNK_CHARS) { - chunks.push(rest.slice(0, MAX_CHUNK_CHARS)); - rest = rest.slice(MAX_CHUNK_CHARS); - } - cur = rest; - } else { + if (candidate.length <= softLimit) { cur = candidate; + tickFenceForLine(line); + continue; + } + + if (cur.length === 0) { + if (line.length <= MAX_CHUNK_CHARS) { + cur = line; + tickFenceForLine(line); + } else { + if (inFence) { + let rest = line; + while (rest.length > MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE) { + const take = MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE; + pushChunk(`${rest.slice(0, take)}\n\`\`\``); + rest = `\`\`\`diff\n${rest.slice(take)}`; + } + cur = rest; + } else { + let rest = line; + while (rest.length > MAX_CHUNK_CHARS) { + pushChunk(rest.slice(0, MAX_CHUNK_CHARS)); + rest = rest.slice(MAX_CHUNK_CHARS); + } + cur = rest; + } + } + continue; } + + if (!inFence) { + pushChunk(cur); + cur = line; + tickFenceForLine(line); + continue; + } + + // Inside a fenced block: close fence, then continue same diff in a new fence + pushChunk(`${cur}\n\`\`\`\n`); + cur = `\`\`\`diff\n${line}`; + inFence = true; } - flush(); + + pushChunk(cur); return chunks; } From 95ee46f3361cc51d7cc3ad517cbbd8855077813b Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 20:04:38 +0100 Subject: [PATCH 09/12] feat: update argocd-diff action --- .../post-argocd-diff-comment.js | 157 +++++++++++------- docs/actions/argocd-diff-preview.md | 6 +- docs/workflows/argocd-diff-preview.md | 2 +- 3 files changed, 102 insertions(+), 63 deletions(-) diff --git a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js index 204e702..8d37b0a 100644 --- a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js +++ b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js @@ -1,17 +1,18 @@ -// Posts argocd-diff-preview output as PR comment(s). Parses per-app
blocks, then packs -// multiple sections into one comment until ~64KiB (GitHub API limit). Splits further only when needed. -// Markers allow re-runs to sync. +// Posts argocd-diff-preview output as PR comment(s). +// - One GitHub comment per Application = one
block (argocd-diff-preview format). +// - Summary preamble and trailing stats are separate comments when present. +// - If one app exceeds ~64KB, split its inner body only and repeat
on each part +// so every comment keeps the collapsible “spoiler”. +// - splitContent() keeps ```diff fences balanced when splitting. const fs = require('fs'); const MARKER_RE = //; -/** Body budget per comment (GitHub ~65536; leave margin for HTML marker + title line) */ +/** Max characters for the markdown body passed to buildBodies (header is added on top). */ const MAX_CHUNK_CHARS = 64500; /** - * argocd-diff-preview renders each app as
- * (see upstream pkg/diff/markdown.go). Split into: [preamble, app1, app2, …] + optional trailer - * after the last
(selection_changes, stats info_box). + * argocd-diff-preview: preamble, then one
per app, then optional trailer (stats). */ function splitByApplicationDetails(raw) { const firstIdx = raw.indexOf('
'); @@ -34,26 +35,42 @@ function splitByApplicationDetails(raw) { const chunks = []; if (preamble.length) chunks.push(preamble); for (const b of blocks) chunks.push(b); - if (trailer.length) { - const last = chunks[chunks.length - 1]; - chunks[chunks.length - 1] = `${last}\n\n${trailer}`; - } + if (trailer.length) chunks.push(trailer); return chunks; } -/** Room to append closing ``` and start ```diff on the next chunk without exceeding GitHub limit */ +/** Match pkg/diff/markdown.go: markdownSectionHeader + body + markdownSectionFooter */ +function parseDetailsBlock(block) { + const re = + /^
\s*\n([\s\S]*?)<\/summary>\s*\n
\s*\n+([\s\S]*?)<\/details>\s*$/i; + const m = block.match(re); + if (!m) return null; + return { summaryInner: m[1], body: m[2] }; +} + const FENCE_SPLIT_RESERVE = 200; function isFenceLine(line) { return /^\s*```/.test(line); } +function peelTrailingFenceOpener(cur) { + const lines = cur.split('\n'); + let i = lines.length - 1; + while (i >= 0 && lines[i].trim() === '') i--; + if (i < 0) return { splitPrefix: cur, splitOpener: '' }; + if (!isFenceLine(lines[i])) return { splitPrefix: cur, splitOpener: '' }; + const splitOpener = lines.slice(i).join('\n'); + const splitPrefix = lines.slice(0, i).join('\n').trimEnd(); + return { splitPrefix, splitOpener }; +} + /** - * Split oversized markdown without breaking fenced code blocks (```diff … ```). - * Plain line splits corrupt GitHub rendering from the first broken fence onward. + * @param {string} text + * @param {number} [maxLen] */ -function splitContent(text) { - if (text.length <= MAX_CHUNK_CHARS) return [text]; +function splitContent(text, maxLen = MAX_CHUNK_CHARS) { + if (text.length <= maxLen) return [text]; const lines = text.split('\n'); const chunks = []; @@ -71,7 +88,7 @@ function splitContent(text) { for (const line of lines) { const sep = cur.length ? '\n' : ''; const candidate = cur + sep + line; - const softLimit = inFence ? MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE : MAX_CHUNK_CHARS; + const softLimit = inFence ? maxLen - FENCE_SPLIT_RESERVE : maxLen; if (candidate.length <= softLimit) { cur = candidate; @@ -80,23 +97,24 @@ function splitContent(text) { } if (cur.length === 0) { - if (line.length <= MAX_CHUNK_CHARS) { + if (line.length <= maxLen) { cur = line; tickFenceForLine(line); } else { if (inFence) { let rest = line; - while (rest.length > MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE) { - const take = MAX_CHUNK_CHARS - FENCE_SPLIT_RESERVE; + while (rest.length > maxLen - FENCE_SPLIT_RESERVE) { + const take = maxLen - FENCE_SPLIT_RESERVE; pushChunk(`${rest.slice(0, take)}\n\`\`\``); rest = `\`\`\`diff\n${rest.slice(take)}`; } cur = rest; + inFence = true; } else { let rest = line; - while (rest.length > MAX_CHUNK_CHARS) { - pushChunk(rest.slice(0, MAX_CHUNK_CHARS)); - rest = rest.slice(MAX_CHUNK_CHARS); + while (rest.length > maxLen) { + pushChunk(rest.slice(0, maxLen)); + rest = rest.slice(maxLen); } cur = rest; } @@ -111,7 +129,23 @@ function splitContent(text) { continue; } - // Inside a fenced block: close fence, then continue same diff in a new fence + const peeled = peelTrailingFenceOpener(cur); + if (peeled.splitOpener) { + if (peeled.splitPrefix) pushChunk(peeled.splitPrefix); + let opener = peeled.splitOpener.endsWith('\n') ? peeled.splitOpener : `${peeled.splitOpener}\n`; + let rest = line; + while (rest.length > 0) { + const maxBody = maxLen - FENCE_SPLIT_RESERVE - opener.length - 5; + const take = Math.min(maxBody, rest.length); + pushChunk(`${opener}${rest.slice(0, take)}\n\`\`\``); + rest = rest.slice(take); + opener = '```diff\n'; + } + cur = '```diff\n'; + inFence = true; + continue; + } + pushChunk(`${cur}\n\`\`\`\n`); cur = `\`\`\`diff\n${line}`; inFence = true; @@ -121,49 +155,54 @@ function splitContent(text) { return chunks; } +function wrapDetailsPart(summaryInner, bodyChunk, partIndex, totalParts) { + const sum = + totalParts > 1 + ? `${summaryInner} (part ${partIndex + 1}/${totalParts})` + : summaryInner; + return `
\n${sum}\n
\n\n${bodyChunk.trim()}\n
`; +} + /** - * Merge consecutive sections into one comment until `budget` chars; only then start a new comment. - * A single section larger than `budget` is split with splitContent (last resort). + * One comment per app; if too large, several comments each with full
spoiler. */ -function packSegments(segments, budget) { - const buckets = []; - let cur = ''; - - const flush = () => { - if (cur.length) { - buckets.push(cur); - cur = ''; - } - }; +function splitOversizedDetailsBlock(block) { + const parsed = parseDetailsBlock(block); + if (!parsed) return splitContent(block); + + const { summaryInner, body } = parsed; + const wrapClose = '\n
'; + const worstSummary = + summaryInner.length + ' (part 99/99)'.length + '
\n\n
\n\n'.length; + const innerBudget = Math.max(12000, MAX_CHUNK_CHARS - worstSummary - wrapClose.length - 400); + + let innerParts = body.length <= innerBudget ? [body] : splitContent(body, innerBudget); + let wrapped = innerParts.map((inner, i) => wrapDetailsPart(summaryInner, inner, i, innerParts.length)); + + for (let attempt = 0; attempt < 8 && wrapped.some((w) => w.length > MAX_CHUNK_CHARS); attempt++) { + const tighter = Math.floor(innerBudget * 0.82); + innerParts = splitContent(body, Math.max(4000, tighter)); + wrapped = innerParts.map((inner, i) => wrapDetailsPart(summaryInner, inner, i, innerParts.length)); + } - for (const seg of segments) { - if (!seg.length) continue; + return wrapped; +} - if (seg.length > budget) { - flush(); - buckets.push(...splitContent(seg)); - continue; - } +function chunkForPosting(raw) { + const segments = splitByApplicationDetails(raw); + if (!segments) return splitContent(raw); - const sep = cur.length ? '\n\n' : ''; - const candidate = cur + sep + seg; - if (candidate.length <= budget) { - cur = candidate; + const out = []; + for (const seg of segments) { + const trimmed = seg.trim(); + if (!trimmed) continue; + if (trimmed.startsWith('
')) { + out.push(...splitOversizedDetailsBlock(trimmed)); } else { - flush(); - cur = seg; + out.push(...splitContent(trimmed)); } } - flush(); - return buckets; -} - -function chunkForPosting(raw) { - const byApp = splitByApplicationDetails(raw); - if (!byApp) { - return splitContent(raw); - } - return packSegments(byApp, MAX_CHUNK_CHARS); + return out; } function buildBodies(chunks) { diff --git a/docs/actions/argocd-diff-preview.md b/docs/actions/argocd-diff-preview.md index 0e6cf74..8d1b73a 100644 --- a/docs/actions/argocd-diff-preview.md +++ b/docs/actions/argocd-diff-preview.md @@ -100,9 +100,9 @@ Wraps [`actions/github-script`](https://github.com/actions/github-script) and lo ### Behaviour (summary) -- Parses `diff.md` into segments (summary preamble, then each `
` app section, with trailing stats merged into the last segment as produced by argocd-diff-preview). -- **Packs** consecutive segments into one GitHub comment until the ~64 KiB body limit, so you usually get a few comments instead of dozens. -- If one segment alone is still too large, splits only that segment by size. +- Parses `diff.md` into segments: summary preamble, each `
` app block, then trailing stats (not merged into the last app block). +- **One GitHub comment per app** (each full `
` block). If one app exceeds ~64 KiB, splits **only that app’s inner body** and posts multiple comments, each with its own `
` (continuations are labelled `part 2/n` in the summary) so collapsible sections stay valid. +- When splitting inner markdown, **closes and reopens ` ```diff ` fences** so rendering does not break mid-block. - Uses HTML markers in each comment body so re-runs update the same set of comments and remove extras. ### Direct usage diff --git a/docs/workflows/argocd-diff-preview.md b/docs/workflows/argocd-diff-preview.md index 2e33f6a..e8dad79 100644 --- a/docs/workflows/argocd-diff-preview.md +++ b/docs/workflows/argocd-diff-preview.md @@ -4,7 +4,7 @@ Reusable workflow that generates an ArgoCD manifest diff for pull requests and p This workflow is built from three **composite actions** (`argocd-diff-helm-template`, `argocd-diff-run`, `post-argocd-diff-comment`); see [**ArgoCD Diff Preview (composite actions)**](../actions/argocd-diff-preview.md) for inputs, secrets, and direct `uses:` examples. -**PR comments:** the diff is published with the **issue comments** API (same thread as the PR conversation). The first run creates one or more comments; later runs **update** those comments when possible. If a new run produces **fewer** chunks than before, surplus bot comments are **deleted** — that requires `issues: write` (see [Caller permissions](#caller-permissions)). The post step parses each Argo CD app as a `
` block in `diff.md`, then **packs** several blocks into the same GitHub comment until the ~64 KiB API limit, so you typically get a **small number** of comments instead of one per app. Only when a single app (or the summary) is larger than that limit is it split across multiple comments. +**PR comments:** the diff is published with the **issue comments** API (same thread as the PR conversation). The first run creates one or more comments; later runs **update** those comments when possible. If a new run produces **fewer** chunks than before, surplus bot comments are **deleted** — that requires `issues: write` (see [Caller permissions](#caller-permissions)). The post step uses **one GitHub comment per Argo CD Application** (each `
` block), plus separate comments for the summary preamble and trailing stats when present. If a single app is larger than ~64 KiB, only that app’s inner markdown is split into several comments, and each part repeats the `
` wrapper so the collapsible section (“spoiler”) still works. Supports two modes: From 24108595dcb07eda9d7f84ed26d9c20209e30b1b Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 20:04:48 +0100 Subject: [PATCH 10/12] feat: update argocd-diff action --- .../post-argocd-diff-comment.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js index 8d37b0a..485ce42 100644 --- a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js +++ b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js @@ -171,17 +171,22 @@ function splitOversizedDetailsBlock(block) { if (!parsed) return splitContent(block); const { summaryInner, body } = parsed; - const wrapClose = '\n
'; - const worstSummary = - summaryInner.length + ' (part 99/99)'.length + '
\n\n
\n\n'.length; - const innerBudget = Math.max(12000, MAX_CHUNK_CHARS - worstSummary - wrapClose.length - 400); + const smallWorst = ' (part 99/99)'; + const overhead = + '
\n'.length + + summaryInner.length + + smallWorst.length + + '\n
\n\n'.length + + '\n
'.length + + 400; + let innerBudget = Math.max(12000, MAX_CHUNK_CHARS - overhead); let innerParts = body.length <= innerBudget ? [body] : splitContent(body, innerBudget); let wrapped = innerParts.map((inner, i) => wrapDetailsPart(summaryInner, inner, i, innerParts.length)); for (let attempt = 0; attempt < 8 && wrapped.some((w) => w.length > MAX_CHUNK_CHARS); attempt++) { - const tighter = Math.floor(innerBudget * 0.82); - innerParts = splitContent(body, Math.max(4000, tighter)); + innerBudget = Math.max(4000, Math.floor(innerBudget * 0.82)); + innerParts = splitContent(body, innerBudget); wrapped = innerParts.map((inner, i) => wrapDetailsPart(summaryInner, inner, i, innerParts.length)); } From 036d2fbb1d3a288e215be449fd6a68e3609493ad Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 20:13:28 +0100 Subject: [PATCH 11/12] feat: update argocd-diff action --- .github/actions/argocd-diff-run/action.yml | 2 +- .../actions/post-argocd-diff-comment/action.yml | 10 ++++++++++ .../post-argocd-diff-comment.js | 9 ++++++++- .github/workflows/argocd-diff-preview.yml | 15 +++++++++++++-- docs/actions/argocd-diff-preview.md | 6 ++++-- docs/workflows/argocd-diff-preview.md | 9 +++++++-- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.github/actions/argocd-diff-run/action.yml b/.github/actions/argocd-diff-run/action.yml index 8370970..ace8d17 100644 --- a/.github/actions/argocd-diff-run/action.yml +++ b/.github/actions/argocd-diff-run/action.yml @@ -47,7 +47,7 @@ inputs: max_diff_length: description: argocd-diff-preview --max-diff-length (chars in diff.md); PR comments are split separately to fit GitHub API required: false - default: '1048576' + default: '20971520' runs: using: composite steps: diff --git a/.github/actions/post-argocd-diff-comment/action.yml b/.github/actions/post-argocd-diff-comment/action.yml index 4867a22..ce635c3 100644 --- a/.github/actions/post-argocd-diff-comment/action.yml +++ b/.github/actions/post-argocd-diff-comment/action.yml @@ -9,6 +9,14 @@ inputs: diff_path: description: Absolute path to diff.md on the runner required: true + workflow_run_url: + description: URL of this workflow run (first PR comment links here for full artifact download); empty skips the notice + required: false + default: '' + artifact_name: + description: Name of the uploaded artifact bundle (must match upload-artifact name) + required: false + default: argocd-diff-preview runs: using: composite @@ -18,6 +26,8 @@ runs: env: DIFF_PATH: ${{ inputs.diff_path }} ADP_COMMENT_SCRIPT_DIR: ${{ github.action_path }} + WORKFLOW_RUN_URL: ${{ inputs.workflow_run_url }} + ARTIFACT_NAME: ${{ inputs.artifact_name }} with: github-token: ${{ inputs.github_token }} script: | diff --git a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js index 485ce42..aa74034 100644 --- a/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js +++ b/.github/actions/post-argocd-diff-comment/post-argocd-diff-comment.js @@ -212,13 +212,20 @@ function chunkForPosting(raw) { function buildBodies(chunks) { const n = chunks.length; + const runUrl = (process.env.WORKFLOW_RUN_URL || '').trim(); + const artifactName = (process.env.ARTIFACT_NAME || 'argocd-diff-preview').trim(); + const linkBlock = + runUrl.length > 0 + ? `📎 **Full output** (Markdown/HTML, etc.): open [this workflow run](${runUrl}), then download the **${artifactName}** artifact.\n\n---\n\n` + : ''; return chunks.map((content, idx) => { const i = idx + 1; const header = n <= 1 ? '\n\n' : `\n\n**argocd-diff-preview** · part ${i} of ${n} _(same run; split for GitHub ~64KB limit per comment)_\n\n`; - return header + content; + const body = idx === 0 && linkBlock ? linkBlock + content : content; + return header + body; }); } diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index 0c34808..5d9191e 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -57,10 +57,10 @@ on: type: string default: '' max_diff_length: - description: Max characters in diff.md before argocd-diff-preview truncates (PR comment text is split to fit GitHub API) + description: Max characters in diff.md before argocd-diff-preview truncates (--max-diff-length); raise for large monorepos (PR comments are still split to ~64KB each) required: false type: number - default: 1048576 + default: 20971520 secrets: GH_PAT: description: PAT with repo read for argocd-diff-preview GitHub API @@ -83,6 +83,7 @@ jobs: contents: read pull-requests: write issues: write + actions: write steps: - name: Checkout target branch (PR) @@ -141,9 +142,19 @@ jobs: sops_age_key: ${{ secrets.SOPS_AGE_KEY }} max_diff_length: ${{ inputs.max_diff_length }} + - name: Upload argocd-diff-preview output + if: always() + uses: actions/upload-artifact@v4 + with: + name: argocd-diff-preview + path: /tmp/argocd-diff/output + if-no-files-found: warn + - name: Post diff as PR comment if: always() uses: AutomationDojo/reusable-cicd/.github/actions/post-argocd-diff-comment@fix/argocd-diff with: github_token: ${{ secrets.GITHUB_TOKEN }} diff_path: /tmp/argocd-diff/output/diff.md + workflow_run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + artifact_name: argocd-diff-preview diff --git a/docs/actions/argocd-diff-preview.md b/docs/actions/argocd-diff-preview.md index 8d1b73a..5a6632d 100644 --- a/docs/actions/argocd-diff-preview.md +++ b/docs/actions/argocd-diff-preview.md @@ -62,7 +62,7 @@ Expects you have already prepared: | `ssh_private_key` | Optional sensitive value — pass `${{ secrets.… }}` from the workflow. | No | `''` | | `repo_ssh_url` | Optional; pair with `ssh_private_key` for private Git over SSH. | No | `''` | | `sops_age_key` | Optional; pass `${{ secrets.… }}` (age key material) for helm-secrets. | No | `''` | -| `max_diff_length` | Passed as `MAX_DIFF_LENGTH` to the container (`--max-diff-length` in the tool). Default avoids truncating large monorepo diffs too early. | No | `1048576` | +| `max_diff_length` | Passed as `MAX_DIFF_LENGTH` to the container (`--max-diff-length` in the tool). Default 20 MiB; raise if the run still logs truncation. | No | `20971520` | Composite actions do not use a top-level `secrets:` block in `action.yml`; treat the SSH/SOPS fields above as **inputs** whose values you set from the caller’s `secrets` context. GitHub masks them in logs when sourced from `secrets.*`. @@ -97,6 +97,8 @@ Wraps [`actions/github-script`](https://github.com/actions/github-script) and lo | :--- | :--- | :--- | :--- | | `github_token` | Token for the **caller** repository (e.g. `secrets.GITHUB_TOKEN`) — needs permission to list/create/update/delete issue comments on the PR. | **Yes** | — | | `diff_path` | Absolute path to `diff.md` (e.g. `/tmp/argocd-diff/output/diff.md`). | **Yes** | — | +| `workflow_run_url` | `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` so the first PR comment can link to **Artifacts**. | No | `''` | +| `artifact_name` | Must match `upload-artifact` `name`; shown in the PR notice. | No | `argocd-diff-preview` | ### Behaviour (summary) @@ -115,7 +117,7 @@ Wraps [`actions/github-script`](https://github.com/actions/github-script) and lo diff_path: /tmp/argocd-diff/output/diff.md ``` -Callers need `issues: write` (and `pull-requests: write` as usual) when using `GITHUB_TOKEN` — see the [workflow doc](../workflows/argocd-diff-preview.md#caller-permissions). +Callers need `issues: write`, `actions: write` (artifact upload in the reusable workflow), and `pull-requests: write` as usual — see the [workflow doc](../workflows/argocd-diff-preview.md#caller-permissions). ## How this relates to the reusable workflow diff --git a/docs/workflows/argocd-diff-preview.md b/docs/workflows/argocd-diff-preview.md index e8dad79..6cbb8ef 100644 --- a/docs/workflows/argocd-diff-preview.md +++ b/docs/workflows/argocd-diff-preview.md @@ -22,7 +22,8 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller 3. If `SSH_PRIVATE_KEY` and `REPO_SSH_URL` are provided, generate an ArgoCD repository secret YAML in `/secrets/`; if `SOPS_AGE_KEY` is set, generate `sops-age-key` for helm-secrets in ephemeral Argo CD 4. Optionally mounts `argocd_config_dir` on `/argocd-config` (see [custom Argo CD installation](https://dag-andersen.github.io/argocd-diff-preview/getting-started/custom-argo-cd-installation/)) 5. `argocd-diff-preview` spins up a Kind cluster, installs ArgoCD (with the repo credentials), renders both sets of manifests, and produces a `diff.md` (length capped by `max_diff_length`, forwarded as `MAX_DIFF_LENGTH` to the tool) -6. **`post-argocd-diff-comment`** reads `diff.md` and creates/updates/deletes PR comments as needed +6. The full `/tmp/argocd-diff/output` directory is uploaded as the **`argocd-diff-preview`** workflow artifact (`actions: write` on the job) +7. **`post-argocd-diff-comment`** reads `diff.md` and creates/updates/deletes PR comments as needed; the **first** comment includes a link to the workflow run so reviewers can download the artifact (full Markdown/HTML, etc.) while still browsing the split thread on the PR > **Note**: The ephemeral cluster adds ~60–90 seconds to each run. @@ -40,7 +41,7 @@ For private repos, pass `SSH_PRIVATE_KEY` and `REPO_SSH_URL` secrets. The caller | `render_method` | `cli`, `server-api`, or `repo-server-api`. Empty = tool default. Required `repo-server-api` if `traverse_app_of_apps` is true (enforced when traverse is set and this is empty). | No | — | | `traverse_app_of_apps` | Experimental expansion of child Applications (requires `repo-server-api`). Prefer Helm pre-render when children are templated. | No | `false` | | `file_regex` | Passed as `--file-regex` (e.g. only root app YAML when using traverse). | No | — | -| `max_diff_length` | Max size (characters) of `diff.md` before [argocd-diff-preview](https://github.com/dag-andersen/argocd-diff-preview) truncates (`--max-diff-length`). Raise for large repos; PR comment bodies are still split to fit GitHub’s API. | No | `1048576` | +| `max_diff_length` | Max size (characters) of `diff.md` before [argocd-diff-preview](https://github.com/dag-andersen/argocd-diff-preview) truncates (`--max-diff-length`). Defaults to 20 MiB for large app-of-apps repos; increase further if you still see the tool’s truncation warning. PR comment bodies are split separately (~64 KB per comment). | No | `20971520` | ## Secrets @@ -60,6 +61,7 @@ permissions: contents: read pull-requests: write issues: write + actions: write ``` `pull-requests: write` covers the PR; **`issues: write`** is needed because conversation comments are created/updated/deleted via the [issue comments](https://docs.github.com/en/rest/issues/comments) API (`issues.createComment`, `issues.updateComment`, `issues.deleteComment`). This is not the Issues tab — PRs are issues under the hood. @@ -79,6 +81,7 @@ permissions: contents: read pull-requests: write issues: write + actions: write jobs: argocd-diff: @@ -107,6 +110,7 @@ permissions: contents: read pull-requests: write issues: write + actions: write jobs: argocd-diff: @@ -125,4 +129,5 @@ jobs: - `SSH_PRIVATE_KEY` and `REPO_SSH_URL` should be stored as GitHub Actions secrets in the caller repository (**Settings → Secrets → Actions**). - The workflow uses Docker and requires a `ubuntu-latest` runner with Docker available (default on GitHub-hosted runners). - ArgoCD version can be pinned via `argocd_version` to ensure consistent rendering across runs. +- The workflow uploads **`argocd-diff-preview`** (full `/tmp/argocd-diff/output`); the first PR comment links to the run so you can download it alongside the chunked comments. - Composite actions in this repo are referenced as `AutomationDojo/reusable-cicd/.github/actions/...@ref` from the workflow file — pin `@main` (or a release tag) in production; the ref must expose the same workflow and action versions you expect. \ No newline at end of file From 014bcafdf0674047826f3ca701372d0b8e226a84 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Fri, 3 Apr 2026 20:46:10 +0100 Subject: [PATCH 12/12] feat: update argocd-diff action --- .github/workflows/argocd-diff-preview.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/argocd-diff-preview.yml b/.github/workflows/argocd-diff-preview.yml index 5d9191e..fce911f 100644 --- a/.github/workflows/argocd-diff-preview.yml +++ b/.github/workflows/argocd-diff-preview.yml @@ -94,7 +94,7 @@ jobs: # workflow_call uses the caller repo as workspace; ./.github/actions would be wrong. # Composites are pinned to @main; for local tests of action changes, temporarily use your branch. - - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@fix/argocd-diff + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@main with: helm_chart_path: ${{ inputs.helm_chart_path }} helm_values_files: ${{ inputs.helm_values_files }} @@ -119,14 +119,14 @@ jobs: ref: ${{ inputs.base_branch }} clean: false - - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@fix/argocd-diff + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-helm-template@main with: helm_chart_path: ${{ inputs.helm_chart_path }} helm_values_files: ${{ inputs.helm_values_files }} repo_path: ${{ inputs.repo_path }} output_dir: /tmp/argocd-diff/base - - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-run@fix/argocd-diff + - uses: AutomationDojo/reusable-cicd/.github/actions/argocd-diff-run@main with: repo: ${{ github.repository }} target_branch: ${{ github.head_ref }} @@ -152,7 +152,7 @@ jobs: - name: Post diff as PR comment if: always() - uses: AutomationDojo/reusable-cicd/.github/actions/post-argocd-diff-comment@fix/argocd-diff + uses: AutomationDojo/reusable-cicd/.github/actions/post-argocd-diff-comment@main with: github_token: ${{ secrets.GITHUB_TOKEN }} diff_path: /tmp/argocd-diff/output/diff.md