From 96db9dd5675fdb05bc70cb26513892144e8ca8cc Mon Sep 17 00:00:00 2001 From: vklimontovich Date: Mon, 11 May 2026 10:58:43 -0400 Subject: [PATCH 1/4] feat(actions): add slack-notify, install-yq, install-kustomize All composite. Intended to back the new infra deploy workflow in jitsucom/jitsu-cloud-infra. slack-notify is moved here from the in-tree copy in jitsucom/jitsu (existing callers keep their local copy until a later cleanup). --- .github/actions/install-kustomize/action.yml | 32 +++++++ .github/actions/install-yq/action.yml | 27 ++++++ .github/actions/slack-notify/action.yml | 99 ++++++++++++++++++++ README.md | 26 +++++ 4 files changed, 184 insertions(+) create mode 100644 .github/actions/install-kustomize/action.yml create mode 100644 .github/actions/install-yq/action.yml create mode 100644 .github/actions/slack-notify/action.yml diff --git a/.github/actions/install-kustomize/action.yml b/.github/actions/install-kustomize/action.yml new file mode 100644 index 0000000..9acc25c --- /dev/null +++ b/.github/actions/install-kustomize/action.yml @@ -0,0 +1,32 @@ +name: Install kustomize +description: Install the standalone kustomize CLI with a pinned version + sha256 checksum. + +inputs: + version: + description: "kustomize version (without leading 'v'), e.g. 5.8.1." + required: false + default: "5.8.1" + sha256: + description: "Expected sha256 of the linux_amd64 tarball. Lookup at https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv/checksums.txt" + required: false + default: "029a7f0f4e1932c52a0476cf02a0fd855c0bb85694b82c338fc648dcb53a819d" + +runs: + using: composite + steps: + - name: Install kustomize + shell: bash + env: + VERSION: ${{ inputs.version }} + SHA256: ${{ inputs.sha256 }} + run: | + set -eu + # Pinned + checksum-verified so a compromised installer script can't + # silently swap the binary. + URL="https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${VERSION}/kustomize_v${VERSION}_linux_amd64.tar.gz" + curl -fsSL -o /tmp/kustomize.tgz "$URL" + echo "${SHA256} /tmp/kustomize.tgz" | sha256sum -c - + tar -xzf /tmp/kustomize.tgz -C /tmp kustomize + sudo install -m 0755 /tmp/kustomize /usr/local/bin/kustomize + rm -f /tmp/kustomize.tgz /tmp/kustomize + kustomize version diff --git a/.github/actions/install-yq/action.yml b/.github/actions/install-yq/action.yml new file mode 100644 index 0000000..1f9be56 --- /dev/null +++ b/.github/actions/install-yq/action.yml @@ -0,0 +1,27 @@ +name: Install yq +description: Install the yq CLI (mikefarah/yq) into /usr/local/bin. + +inputs: + version: + description: "yq version to install (e.g. v4.44.3). Default: latest." + required: false + default: latest + +runs: + using: composite + steps: + - name: Install yq + shell: bash + env: + VERSION: ${{ inputs.version }} + run: | + set -eu + if [ "$VERSION" = "latest" ]; then + URL="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" + else + URL="https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_linux_amd64" + fi + curl -fsSL -o /tmp/yq "$URL" + chmod +x /tmp/yq + sudo mv /tmp/yq /usr/local/bin/yq + yq --version diff --git a/.github/actions/slack-notify/action.yml b/.github/actions/slack-notify/action.yml new file mode 100644 index 0000000..98d3727 --- /dev/null +++ b/.github/actions/slack-notify/action.yml @@ -0,0 +1,99 @@ +name: Send Slack Notification +description: Send a formatted notification to Slack with custom blocks + +inputs: + slack_webhook_url: + description: "Slack webhook URL" + required: true + color: + description: "Notification color (good, warning, danger, or hex color)" + required: true + header: + description: "Header text for the notification" + required: true + blocks: + description: "YAML array of block objects with title, value, url (optional), is_code (optional)" + required: true + +runs: + using: composite + steps: + - name: 🔄 Convert YAML to JSON + id: convert + shell: bash + env: + BLOCKS_YAML: ${{ inputs.blocks }} + run: | + wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + chmod +x /tmp/yq + BLOCKS_JSON=$(/tmp/yq -o=json '.' <<< "$BLOCKS_YAML") + echo "blocks_json<> $GITHUB_OUTPUT + echo "$BLOCKS_JSON" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: 💬 Send Slack notification + uses: actions/github-script@v9 + env: + SLACK_WEBHOOK_URL: ${{ inputs.slack_webhook_url }} + COLOR: ${{ inputs.color }} + HEADER: ${{ inputs.header }} + BLOCKS_JSON: ${{ steps.convert.outputs.blocks_json }} + with: + script: | + const blocks = JSON.parse(process.env.BLOCKS_JSON); + + // Build single text with all blocks as bullet points + const lines = blocks.map(block => { + let line = `• *${block.title}:* `; + + if (block.is_code) { + line += `\n\`\`\`${block.value}\`\`\``; + } else if (block.url) { + line += `<${block.url}|${block.value}>`; + } else { + line += block.value; + } + + return line; + }); + + const text = lines.join('\n'); + + // Build complete payload. `text` (top-level) and `fallback` (attachment) drive + // Slack's notification preview / link-unfurl summary; without them the unfurl + // shows "[no preview available]". Top-level `text` also renders above the + // attachment, so we drop the in-attachment header block to avoid duplication. + const payload = { + text: process.env.HEADER, + attachments: [ + { + color: process.env.COLOR, + fallback: process.env.HEADER, + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: text + } + } + ] + } + ] + }; + + // Send to Slack + const response = await fetch(process.env.SLACK_WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Slack webhook failed: ${response.statusText} - ${text}`); + } + + console.log('✅ Slack notification sent successfully'); diff --git a/README.md b/README.md index 23b0d32..25ed3bb 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,29 @@ Use the `review_instructions` input to focus the review on what matters for your #### Updating All consuming repos pick up changes automatically on the next run — no changes needed per repo. + +## Composite actions + +Reusable composite actions live under `.github/actions/`. Consume them by path: + +```yaml +- uses: jitsucom/github-workflows/.github/actions/@ +``` + +### `slack-notify` — Slack webhook notification + +Sends a formatted notification to Slack with title + bullet blocks. Used by the +deploy workflows. Inputs: `slack_webhook_url`, `color`, `header`, `blocks` +(YAML array). See [`action.yml`](.github/actions/slack-notify/action.yml). + +### `install-yq` — Install the yq CLI + +Installs `mikefarah/yq` to `/usr/local/bin`. Inputs: `version` (defaults to +`latest`). See [`action.yml`](.github/actions/install-yq/action.yml). + +### `install-kustomize` — Install the kustomize CLI + +Installs the standalone kustomize CLI with a pinned version + sha256 checksum. +Inputs: `version`, `sha256` (both have safe defaults). See +[`action.yml`](.github/actions/install-kustomize/action.yml). + From eaf48ab977a9e05f167e4b253ec78c69eab6d145 Mon Sep 17 00:00:00 2001 From: vklimontovich Date: Mon, 11 May 2026 11:11:48 -0400 Subject: [PATCH 2/4] fix(actions): pin yq version + verify sha256 Address review feedback on PR #5. install-yq now defaults to a pinned v4.53.2 with sha256 input (mirrors install-kustomize). slack-notify inlines the same pin+verify install instead of downloading from releases/latest without integrity checks. --- .github/actions/install-yq/action.yml | 23 +++++++++++++---------- .github/actions/slack-notify/action.yml | 9 ++++++++- README.md | 5 +++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/actions/install-yq/action.yml b/.github/actions/install-yq/action.yml index 1f9be56..10c88d7 100644 --- a/.github/actions/install-yq/action.yml +++ b/.github/actions/install-yq/action.yml @@ -1,11 +1,15 @@ name: Install yq -description: Install the yq CLI (mikefarah/yq) into /usr/local/bin. +description: Install the yq CLI (mikefarah/yq) with a pinned version + sha256 checksum. inputs: version: - description: "yq version to install (e.g. v4.44.3). Default: latest." + description: "yq version (without leading 'v'), e.g. 4.53.2." required: false - default: latest + default: "4.53.2" + sha256: + description: "Expected sha256 of the linux_amd64 binary. Lookup at https://github.com/mikefarah/yq/releases/download/v/checksums (SHA-256 row — see checksums_hashes_order)." + required: false + default: "d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b" runs: using: composite @@ -14,14 +18,13 @@ runs: shell: bash env: VERSION: ${{ inputs.version }} + SHA256: ${{ inputs.sha256 }} run: | set -eu - if [ "$VERSION" = "latest" ]; then - URL="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" - else - URL="https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_linux_amd64" - fi + # Pinned + checksum-verified so a compromised release can't silently swap the binary. + URL="https://github.com/mikefarah/yq/releases/download/v${VERSION}/yq_linux_amd64" curl -fsSL -o /tmp/yq "$URL" - chmod +x /tmp/yq - sudo mv /tmp/yq /usr/local/bin/yq + echo "${SHA256} /tmp/yq" | sha256sum -c - + sudo install -m 0755 /tmp/yq /usr/local/bin/yq + rm -f /tmp/yq yq --version diff --git a/.github/actions/slack-notify/action.yml b/.github/actions/slack-notify/action.yml index 98d3727..be8d1b1 100644 --- a/.github/actions/slack-notify/action.yml +++ b/.github/actions/slack-notify/action.yml @@ -23,8 +23,15 @@ runs: shell: bash env: BLOCKS_YAML: ${{ inputs.blocks }} + YQ_VERSION: "4.53.2" + YQ_SHA256: "d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b" run: | - wget -qO /tmp/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + set -eu + # Pinned + checksum-verified yq install. Keep in sync with .github/actions/install-yq + # (we inline here so this action stays self-contained for external callers). + URL="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64" + curl -fsSL -o /tmp/yq "$URL" + echo "${YQ_SHA256} /tmp/yq" | sha256sum -c - chmod +x /tmp/yq BLOCKS_JSON=$(/tmp/yq -o=json '.' <<< "$BLOCKS_YAML") echo "blocks_json<> $GITHUB_OUTPUT diff --git a/README.md b/README.md index 25ed3bb..a3f0d32 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,9 @@ deploy workflows. Inputs: `slack_webhook_url`, `color`, `header`, `blocks` ### `install-yq` — Install the yq CLI -Installs `mikefarah/yq` to `/usr/local/bin`. Inputs: `version` (defaults to -`latest`). See [`action.yml`](.github/actions/install-yq/action.yml). +Installs `mikefarah/yq` to `/usr/local/bin` with a pinned version + sha256 +checksum. Inputs: `version`, `sha256` (both have safe defaults). See +[`action.yml`](.github/actions/install-yq/action.yml). ### `install-kustomize` — Install the kustomize CLI From 929305f237cabab0754605897fc19e1f99524bbc Mon Sep 17 00:00:00 2001 From: vklimontovich Date: Mon, 11 May 2026 11:13:32 -0400 Subject: [PATCH 3/4] feat(slack-notify): add defaults for webhook, color, blocks - slack_webhook_url: optional, falls back to SLACK_WEBHOOK_URL env var (caller sets it from secrets.CI_SLACK_WEBHOOK at job level) - color: defaults to 'good' (green) - blocks: optional; when omitted, sends header alone (skips yq install) Composite actions can't read org secrets directly, so the env-var fallback is the standard pattern for reducing per-step boilerplate. --- .github/actions/slack-notify/action.yml | 63 +++++++++++++++---------- README.md | 29 ++++++++++-- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/.github/actions/slack-notify/action.yml b/.github/actions/slack-notify/action.yml index be8d1b1..d2d7dc1 100644 --- a/.github/actions/slack-notify/action.yml +++ b/.github/actions/slack-notify/action.yml @@ -1,25 +1,29 @@ name: Send Slack Notification -description: Send a formatted notification to Slack with custom blocks +description: Send a formatted notification to Slack with optional bullet blocks inputs: slack_webhook_url: - description: "Slack webhook URL" - required: true + description: "Slack webhook URL. If empty, falls back to the SLACK_WEBHOOK_URL env var (set this in the job from secrets.CI_SLACK_WEBHOOK)." + required: false + default: "" color: - description: "Notification color (good, warning, danger, or hex color)" - required: true + description: "Attachment color: good, warning, danger, or hex (e.g. #36a64f)." + required: false + default: "good" header: - description: "Header text for the notification" + description: "Header text for the notification (required)." required: true blocks: - description: "YAML array of block objects with title, value, url (optional), is_code (optional)" - required: true + description: "YAML array of block objects with title, value, url (optional), is_code (optional). Omit to send the header alone." + required: false + default: "" runs: using: composite steps: - name: 🔄 Convert YAML to JSON id: convert + if: ${{ inputs.blocks != '' }} shell: bash env: BLOCKS_YAML: ${{ inputs.blocks }} @@ -41,13 +45,25 @@ runs: - name: 💬 Send Slack notification uses: actions/github-script@v9 env: - SLACK_WEBHOOK_URL: ${{ inputs.slack_webhook_url }} + # Webhook precedence: explicit input wins, else fall back to env var + # (typically set at job level from secrets.CI_SLACK_WEBHOOK — composite + # actions can't read org secrets directly). + WEBHOOK_INPUT: ${{ inputs.slack_webhook_url }} COLOR: ${{ inputs.color }} HEADER: ${{ inputs.header }} BLOCKS_JSON: ${{ steps.convert.outputs.blocks_json }} with: script: | - const blocks = JSON.parse(process.env.BLOCKS_JSON); + const webhook = process.env.WEBHOOK_INPUT || process.env.SLACK_WEBHOOK_URL; + if (!webhook) { + throw new Error( + "slack-notify: no webhook URL. Pass `slack_webhook_url` input or " + + "set SLACK_WEBHOOK_URL env in the job (from secrets.CI_SLACK_WEBHOOK)." + ); + } + + const blocksJson = process.env.BLOCKS_JSON; + const blocks = blocksJson ? JSON.parse(blocksJson) : []; // Build single text with all blocks as bullet points const lines = blocks.map(block => { @@ -70,27 +86,22 @@ runs: // Slack's notification preview / link-unfurl summary; without them the unfurl // shows "[no preview available]". Top-level `text` also renders above the // attachment, so we drop the in-attachment header block to avoid duplication. + const attachment = { + color: process.env.COLOR, + fallback: process.env.HEADER, + }; + if (text) { + attachment.blocks = [ + { type: "section", text: { type: "mrkdwn", text: text } } + ]; + } const payload = { text: process.env.HEADER, - attachments: [ - { - color: process.env.COLOR, - fallback: process.env.HEADER, - blocks: [ - { - type: "section", - text: { - type: "mrkdwn", - text: text - } - } - ] - } - ] + attachments: [attachment], }; // Send to Slack - const response = await fetch(process.env.SLACK_WEBHOOK_URL, { + const response = await fetch(webhook, { method: 'POST', headers: { 'Content-Type': 'application/json' diff --git a/README.md b/README.md index a3f0d32..4a70a49 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,32 @@ Reusable composite actions live under `.github/actions/`. Consume them by path: ### `slack-notify` — Slack webhook notification -Sends a formatted notification to Slack with title + bullet blocks. Used by the -deploy workflows. Inputs: `slack_webhook_url`, `color`, `header`, `blocks` -(YAML array). See [`action.yml`](.github/actions/slack-notify/action.yml). +Sends a formatted notification to Slack with title + optional bullet blocks. +Used by the deploy workflows. + +Inputs: + +- `header` — required. +- `slack_webhook_url` — optional. If empty, falls back to the + `SLACK_WEBHOOK_URL` env var. The standard pattern is to set this once at the + job level from the org secret: + + ```yaml + jobs: + notify: + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }} + steps: + - uses: jitsucom/github-workflows/.github/actions/slack-notify@main + with: + header: "Deploy started" + ``` +- `color` — optional, defaults to `good` (green). Accepts `good`, `warning`, + `danger`, or a hex color like `#36a64f`. +- `blocks` — optional. YAML array of block objects with `title`, `value`, + `url` (optional), `is_code` (optional). Omit to send the header alone. + +See [`action.yml`](.github/actions/slack-notify/action.yml). ### `install-yq` — Install the yq CLI From 25043abbe3ad87bfcad6d3f374f98cf2d106ad33 Mon Sep 17 00:00:00 2001 From: vklimontovich Date: Mon, 11 May 2026 11:21:09 -0400 Subject: [PATCH 4/4] feat(slack-notify): add reusable-workflow wrapper, prefer env over input The composite action now prefers SLACK_WEBHOOK_URL env (typically set from secrets.CI_SLACK_WEBHOOK at job level) over the slack_webhook_url input; the input is a fallback for ad-hoc testing. Adds .github/workflows/slack-notify.yml as a thin reusable-workflow wrapper that pulls the secret and forwards to the composite. Useful for callers that prefer 'secrets: inherit' and for triggering the action via workflow_dispatch from the GitHub UI. --- .github/actions/slack-notify/action.yml | 15 ++++--- .github/workflows/slack-notify.yml | 57 +++++++++++++++++++++++++ README.md | 50 ++++++++++++++++------ 3 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/slack-notify.yml diff --git a/.github/actions/slack-notify/action.yml b/.github/actions/slack-notify/action.yml index d2d7dc1..6645097 100644 --- a/.github/actions/slack-notify/action.yml +++ b/.github/actions/slack-notify/action.yml @@ -3,7 +3,7 @@ description: Send a formatted notification to Slack with optional bullet blocks inputs: slack_webhook_url: - description: "Slack webhook URL. If empty, falls back to the SLACK_WEBHOOK_URL env var (set this in the job from secrets.CI_SLACK_WEBHOOK)." + description: "Slack webhook URL (override). Normally leave empty and set SLACK_WEBHOOK_URL env in the job from secrets.CI_SLACK_WEBHOOK; this input only takes effect if that env var is unset (useful for ad-hoc testing)." required: false default: "" color: @@ -45,20 +45,21 @@ runs: - name: 💬 Send Slack notification uses: actions/github-script@v9 env: - # Webhook precedence: explicit input wins, else fall back to env var - # (typically set at job level from secrets.CI_SLACK_WEBHOOK — composite - # actions can't read org secrets directly). + # Webhook precedence: SLACK_WEBHOOK_URL env var wins (typically set at + # job level from secrets.CI_SLACK_WEBHOOK — composite actions can't read + # org secrets directly). The `slack_webhook_url` input is a fallback for + # ad-hoc testing or direct invocation. WEBHOOK_INPUT: ${{ inputs.slack_webhook_url }} COLOR: ${{ inputs.color }} HEADER: ${{ inputs.header }} BLOCKS_JSON: ${{ steps.convert.outputs.blocks_json }} with: script: | - const webhook = process.env.WEBHOOK_INPUT || process.env.SLACK_WEBHOOK_URL; + const webhook = process.env.SLACK_WEBHOOK_URL || process.env.WEBHOOK_INPUT; if (!webhook) { throw new Error( - "slack-notify: no webhook URL. Pass `slack_webhook_url` input or " + - "set SLACK_WEBHOOK_URL env in the job (from secrets.CI_SLACK_WEBHOOK)." + "slack-notify: no webhook URL. Set SLACK_WEBHOOK_URL env in the " + + "job (from secrets.CI_SLACK_WEBHOOK) or pass `slack_webhook_url` input." ); } diff --git a/.github/workflows/slack-notify.yml b/.github/workflows/slack-notify.yml new file mode 100644 index 0000000..ea7364e --- /dev/null +++ b/.github/workflows/slack-notify.yml @@ -0,0 +1,57 @@ +name: Slack notify (reusable) + +# Thin wrapper around .github/actions/slack-notify. The composite action can't +# read org secrets directly, so this reusable workflow pulls +# secrets.CI_SLACK_WEBHOOK and passes it through. Two use cases: +# +# 1. Manual testing of the slack-notify action via the Actions tab +# (workflow_dispatch). +# 2. Callers that prefer `secrets: inherit` over setting SLACK_WEBHOOK_URL +# at job level. Note the trade-off: each call spins up its own runner. +# For inline notifications inside an existing job, use the composite +# action directly. + +on: + workflow_call: + inputs: + header: + type: string + required: true + color: + type: string + required: false + default: "good" + blocks: + type: string + required: false + default: "" + workflow_dispatch: + inputs: + header: + type: string + required: true + default: "Test notification from slack-notify" + color: + type: string + required: false + default: "good" + blocks: + type: string + required: false + default: "" + +jobs: + send: + runs-on: ubuntu-latest + steps: + # Composite action is pinned to @main — `actions/checkout` in a reusable + # workflow checks out the *caller's* repo, so we can't use a `./` path + # without an extra clone of jitsucom/github-workflows. If this wrapper + # ever ships in tagged releases, bump this @ref alongside the tag. + - uses: jitsucom/github-workflows/.github/actions/slack-notify@main + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }} + with: + header: ${{ inputs.header }} + color: ${{ inputs.color }} + blocks: ${{ inputs.blocks }} diff --git a/README.md b/README.md index 4a70a49..9c666fe 100644 --- a/README.md +++ b/README.md @@ -101,27 +101,49 @@ Used by the deploy workflows. Inputs: - `header` — required. -- `slack_webhook_url` — optional. If empty, falls back to the - `SLACK_WEBHOOK_URL` env var. The standard pattern is to set this once at the - job level from the org secret: - - ```yaml - jobs: - notify: - env: - SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }} - steps: - - uses: jitsucom/github-workflows/.github/actions/slack-notify@main - with: - header: "Deploy started" - ``` - `color` — optional, defaults to `good` (green). Accepts `good`, `warning`, `danger`, or a hex color like `#36a64f`. - `blocks` — optional. YAML array of block objects with `title`, `value`, `url` (optional), `is_code` (optional). Omit to send the header alone. +- `slack_webhook_url` — optional override. Normally leave empty; the action + reads `SLACK_WEBHOOK_URL` env first (see below) and only uses this input as + a fallback, mainly for ad-hoc testing. + +The composite action can't read org secrets directly. Standard pattern: set +`SLACK_WEBHOOK_URL` once at the job level from `secrets.CI_SLACK_WEBHOOK`. + +```yaml +jobs: + notify: + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }} + steps: + - uses: jitsucom/github-workflows/.github/actions/slack-notify@main + with: + header: "Deploy started" +``` See [`action.yml`](.github/actions/slack-notify/action.yml). +#### Reusable-workflow wrapper + +For callers that prefer `secrets: inherit` over wiring the env var, or for +testing the action directly from the GitHub UI (`workflow_dispatch`), there's +a thin wrapper at [`.github/workflows/slack-notify.yml`](.github/workflows/slack-notify.yml): + +```yaml +jobs: + notify: + uses: jitsucom/github-workflows/.github/workflows/slack-notify.yml@main + secrets: inherit + with: + header: "Deploy started" +``` + +Trade-off: each invocation runs as its own job on a fresh runner (~30–60s +startup) and can't share workspace state with sibling steps. For inline +notifications inside an existing deploy job, use the composite action directly. + ### `install-yq` — Install the yq CLI Installs `mikefarah/yq` to `/usr/local/bin` with a pinned version + sha256