Compare commits

...

4 Commits

Author SHA1 Message Date
Evan
e8beee0caa ci: align actions/checkout pin with repo standard (v6.0.3) to satisfy zizmor
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:37:47 -07:00
Evan
a55c42fa0c ci(security): pass template expressions via env in scheduled-docker-image-refresh
Move ${{ github.repository }}, ${{ matrix.build_preset }}, and
${{ needs.config.outputs.latest-release }} out of shell run scripts
and into env vars, eliminating the zizmor template-injection findings
without changing runtime behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:17:35 -07:00
Claude Code
3c34068b7e ci: add concurrency group to tag-release.yml to prevent race with scheduled refresh
Both workflows push to the same Docker Hub tags. The concurrency lock only
works when both workflows participate in the same group — without this,
tag-release.yml and the scheduled refresh could race on apache/superset:latest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 16:17:35 -07:00
Claude Code
dfd1a76978 ci: schedule a weekly Docker image rebuild against the latest release
Adds a cron-triggered workflow that re-runs the Docker image build
against the most-recent published release every Monday at 06:00 UTC
(and on manual workflow_dispatch when an operator wants to force it).
The Superset code being built doesn't change — but the base image
layers (python:*-slim-trixie and the Debian OS packages underneath)
DO receive upstream security patches between Superset releases. Without
a rebuild, apache/superset:<latest> ships those CVEs unfixed for as
long as the inter-release gap (typically 3–6 weeks).

Why this approach over the alternatives:

- Tied to releases: defeats the purpose — the gap we're trying to close
  IS the inter-release window. Release-triggered rebuilds happen exactly
  when we already get them.
- Swap to Chainguard / distroless: would also close the gap, but at the
  cost of a backward-incompatible package-manager change for downstream
  operators who extend `apache/superset:<tag>` with their own apt
  install lines for custom drivers. A scheduled rebuild captures most
  of the CVE-cycling benefit without that breakage.
- Daily cadence: probably overkill — Debian's security tree updates on
  a roughly weekly rhythm and a daily rebuild would just churn the
  registry without adding much.

Implementation: deliberately reuses the same `supersetbot docker`
invocation as `tag-release.yml` (same matrix of build presets, same
`--context release --context-ref <tag> --force-latest` flags), so the
resulting tags are byte-equivalent to what a manual release dispatch
would produce — only the base layer changes. Concurrency group
shared with the release publisher so the two can't race each other
on the Docker Hub push.

Tag mutability note: the rebuild overwrites both the rolling tags
(`apache/superset:latest`) AND the version-specific tag of the latest
release (e.g. `apache/superset:5.0.0`). This is intentional and
matches how the upstream `python:*-slim-trixie` images themselves
behave — version tags reflect content + latest patches, not a frozen
SHA. Users who need a frozen reference should pin by image digest.
2026-06-10 16:17:35 -07:00
2 changed files with 136 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
name: Scheduled Docker image refresh
# Re-runs the Docker image build against the latest published release on a
# weekly cadence. The code being built doesn't change — but the base image
# layers (python:*-slim-trixie and its OS packages) DO get upstream
# security patches between Superset releases, and those patches don't
# reach our published images unless we rebuild.
#
# Without this workflow, `apache/superset:<latest>` lags behind upstream
# Debian/Python base patches by whatever interval falls between Superset
# releases (typically 36 weeks). With it, the lag drops to at most one
# week regardless of release cadence.
#
# This is a security-hygiene cron, not a release. It overwrites the
# existing tags for the most recent release (e.g. `apache/superset:5.0.0`
# and `apache/superset:latest`) with bit-for-bit-equivalent contents
# layered on a refreshed base. Image digests change; everything users
# actually pin against (image content, code, deps) does not.
on:
schedule:
# Mondays at 06:00 UTC — gives the weekend for upstream patches to
# settle and surfaces failures at the start of the work week so a
# human can react.
- cron: "0 6 * * 1"
# Manual trigger so operators can force a refresh on demand (e.g.
# immediately after a high-severity base-image CVE drops).
workflow_dispatch: {}
permissions:
contents: read
# Serialize with itself and with the release publisher (tag-release.yml) —
# both push to the same Docker Hub tags, so a race could end with stale
# layers winning. Both workflows must declare this group for the lock to work.
concurrency:
group: docker-publish-latest-release
cancel-in-progress: false
jobs:
config:
runs-on: ubuntu-24.04
outputs:
has-secrets: ${{ steps.check.outputs.has-secrets }}
latest-release: ${{ steps.latest.outputs.tag }}
steps:
- name: Check for Docker Hub secrets
id: check
shell: bash
run: |
if [ -n "${DOCKERHUB_USER}" ]; then
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
fi
env:
DOCKERHUB_USER: ${{ (secrets.DOCKERHUB_USER != '' && secrets.DOCKERHUB_TOKEN != '') || '' }}
- name: Look up latest published release
id: latest
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
run: |
# `releases/latest` returns the latest non-prerelease, non-draft
# release — which is exactly what `apache/superset:latest`
# should reflect.
TAG=$(gh api "repos/${REPOSITORY}/releases/latest" --jq .tag_name)
if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then
echo "::error::Could not determine latest release tag"
exit 1
fi
echo "Latest release: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
docker-rebuild:
needs: config
if: needs.config.outputs.has-secrets
name: docker-rebuild
runs-on: ubuntu-24.04
strategy:
# Mirror the same matrix the release publisher uses so every variant
# operators consume from Docker Hub gets the refreshed base.
matrix:
build_preset: ["dev", "lean", "py310", "websocket", "dockerize", "py311", "py312"]
fail-fast: false
steps:
- name: "Checkout release tag: ${{ needs.config.outputs.latest-release }}"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.config.outputs.latest-release }}
fetch-depth: 0
persist-credentials: false
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker
with:
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
install-docker-compose: "false"
build: "true"
- name: Use Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 20
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
- name: Rebuild and push
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_PRESET: ${{ matrix.build_preset }}
LATEST_RELEASE: ${{ needs.config.outputs.latest-release }}
run: |
# Reuses the same supersetbot invocation as the release
# publisher (`tag-release.yml`), so the resulting tags are
# identical to what a manual release dispatch would produce —
# just with a freshly-pulled base image layer underneath.
supersetbot docker \
--push \
--preset "$BUILD_PRESET" \
--context release \
--context-ref "$LATEST_RELEASE" \
--force-latest \
--platform "linux/arm64" \
--platform "linux/amd64"

View File

@@ -24,6 +24,12 @@ on:
permissions:
contents: read
# Serialize with the scheduled Docker image refresh — both workflows push
# to the same Docker Hub tags and must not race on apache/superset:latest.
concurrency:
group: docker-publish-latest-release
cancel-in-progress: false
jobs:
config:
runs-on: ubuntu-24.04