diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index a95ea0c..a794928 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - name: Setup Node.js 24.x - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: '24' diff --git a/.github/workflows/docker-publish-reusable.yml b/.github/workflows/docker-publish-reusable.yml new file mode 100644 index 0000000..2307302 --- /dev/null +++ b/.github/workflows/docker-publish-reusable.yml @@ -0,0 +1,109 @@ +# Reusable: build a Docker image, push it to GHCR, and trigger an instant +# Komodo deploy. Used by the bot repos (asia-trip-bot, kurwa_bot). +# +# Each caller keeps its own `on: push` trigger with a repo-specific `paths:` +# filter (the set of source paths that should rebuild the image), then +# delegates here: +# +# jobs: +# publish: +# uses: roleme/workflows/.github/workflows/docker-publish-reusable.yml@main +# permissions: +# contents: read +# packages: write +# with: +# image: ghcr.io// +# komodo-stack: +# komodo-host: +# secrets: inherit +# +# Deploy trigger: after the GHCR push, the final step curls Komodo's per-stack +# GitHub listener, HMAC-signing the body with KOMODO_WEBHOOK_SECRET (the +# stack's webhook_secret). Stacks have webhook_force_deploy=true so Komodo runs +# a real DeployStack (pull) — a new :latest leaves compose.yaml unchanged, so +# DeployStackIfChanged would no-op. Without this step the stack still updates +# via Komodo's poll; the webhook just makes it near-instant. +name: docker-publish (reusable) + +on: + workflow_call: + inputs: + image: + description: Fully-qualified GHCR image name (e.g. ghcr.io/owner/repo) + required: true + type: string + komodo-stack: + description: Komodo stack name for the deploy listener URL + required: true + type: string + komodo-host: + description: Komodo host for the deploy listener (e.g. komodo.example.com) + required: true + type: string + context: + description: Docker build context + required: false + type: string + default: . + secrets: + KOMODO_WEBHOOK_SECRET: + description: HMAC secret for the Komodo stack webhook + required: true + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - name: Log in to GitHub Container Registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract image metadata (tags + labels) + id: meta + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 + with: + images: ${{ inputs.image }} + tags: | + type=raw,value=latest + + - name: Set up Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + - name: Build and push + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + with: + context: ${{ inputs.context }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Trigger Komodo deploy + # All interpolated values come from workflow inputs/secrets (trusted + # caller config, not event payload). They are passed via env and quoted + # so they are never spliced into the command line. + env: + KOMODO_WEBHOOK_SECRET: ${{ secrets.KOMODO_WEBHOOK_SECRET }} + KOMODO_HOST: ${{ inputs.komodo-host }} + KOMODO_STACK: ${{ inputs.komodo-stack }} + run: | + BODY='{"ref":"refs/heads/main"}' + SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$KOMODO_WEBHOOK_SECRET" | awk '{print $2}')" + curl -fsSL -X POST \ + "https://${KOMODO_HOST}/listener/github/stack/${KOMODO_STACK}/deploy" \ + -H "Content-Type: application/json" \ + -H "X-Hub-Signature-256: $SIG" \ + --data "$BODY" diff --git a/.github/workflows/validate-renovate-reusable.yml b/.github/workflows/validate-renovate-reusable.yml new file mode 100644 index 0000000..b0cc549 --- /dev/null +++ b/.github/workflows/validate-renovate-reusable.yml @@ -0,0 +1,44 @@ +# Reusable Renovate config validation. +# +# Validates renovate.json against the Renovate schema so a broken config is +# caught in a PR instead of silently failing at Renovate runtime. Callers add +# a thin wrapper triggered by pull_request touching renovate.json: +# +# jobs: +# validate-renovate: +# uses: roleme/workflows/.github/workflows/validate-renovate-reusable.yml@main +# +# Node is required for renovate-config-validator (shipped in the renovate npm +# package). +name: validate-renovate (reusable) + +on: + workflow_call: + inputs: + config-file: + description: Path to the Renovate config file to validate + required: false + type: string + default: renovate.json + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 24 + + - name: Validate Renovate config + # The config path comes from a workflow input (trusted caller), but pass + # it via env and quote it so it is never spliced into the command line. + env: + CONFIG_FILE: ${{ inputs.config-file }} + run: npx --yes --package renovate@43 renovate-config-validator "$CONFIG_FILE" diff --git a/.github/workflows/zizmor-reusable.yml b/.github/workflows/zizmor-reusable.yml new file mode 100644 index 0000000..abae6a8 --- /dev/null +++ b/.github/workflows/zizmor-reusable.yml @@ -0,0 +1,31 @@ +# Reusable zizmor (GitHub Actions security) scan. +# +# Callers add a thin wrapper that triggers on pull_request touching +# .github/workflows/** and delegates here: +# +# jobs: +# zizmor: +# uses: roleme/workflows/.github/workflows/zizmor-reusable.yml@main +# +# advanced-security: false surfaces results as workflow annotations (not a +# SARIF upload), so no security-events permission is needed — contents: read +# is enough. +name: zizmor (reusable) + +on: + workflow_call: {} + +permissions: + contents: read + +jobs: + zizmor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + - name: Run zizmor + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + advanced-security: false diff --git a/renovate-presets/default.json b/renovate-presets/default.json index 07019f4..6b9c8aa 100644 --- a/renovate-presets/default.json +++ b/renovate-presets/default.json @@ -4,5 +4,13 @@ "extends": ["config:recommended", "helpers:pinGitHubActionDigests"], "minimumReleaseAge": "10 days", "transitiveRemediation": true, - "labels": ["dependencies", "renovate"] + "labels": ["dependencies", "renovate"], + "packageRules": [ + { + "description": "GitHub Actions patch/minor — automerge. Renovate is the primary updater for actions (Dependabot is only a backup); the 10-day minimumReleaseAge above still applies before the merge.", + "matchManagers": ["github-actions"], + "matchUpdateTypes": ["patch", "minor"], + "automerge": true + } + ] }