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..10c88d7 --- /dev/null +++ b/.github/actions/install-yq/action.yml @@ -0,0 +1,30 @@ +name: Install yq +description: Install the yq CLI (mikefarah/yq) with a pinned version + sha256 checksum. + +inputs: + version: + description: "yq version (without leading 'v'), e.g. 4.53.2." + required: false + 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 + steps: + - name: Install yq + shell: bash + env: + VERSION: ${{ inputs.version }} + SHA256: ${{ inputs.sha256 }} + run: | + set -eu + # 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" + 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 new file mode 100644 index 0000000..6645097 --- /dev/null +++ b/.github/actions/slack-notify/action.yml @@ -0,0 +1,118 @@ +name: Send Slack Notification +description: Send a formatted notification to Slack with optional bullet blocks + +inputs: + slack_webhook_url: + 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: + description: "Attachment color: good, warning, danger, or hex (e.g. #36a64f)." + required: false + default: "good" + header: + description: "Header text for the notification (required)." + required: true + blocks: + 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 }} + YQ_VERSION: "4.53.2" + YQ_SHA256: "d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b" + run: | + 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 + echo "$BLOCKS_JSON" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: 💬 Send Slack notification + uses: actions/github-script@v9 + env: + # 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.SLACK_WEBHOOK_URL || process.env.WEBHOOK_INPUT; + if (!webhook) { + throw new Error( + "slack-notify: no webhook URL. Set SLACK_WEBHOOK_URL env in the " + + "job (from secrets.CI_SLACK_WEBHOOK) or pass `slack_webhook_url` input." + ); + } + + 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 => { + 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 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: [attachment], + }; + + // Send to Slack + const response = await fetch(webhook, { + 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/.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 23b0d32..9c666fe 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,75 @@ 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 + optional bullet blocks. +Used by the deploy workflows. + +Inputs: + +- `header` — required. +- `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 +checksum. Inputs: `version`, `sha256` (both have safe defaults). 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). +