Compare commits

...

6 Commits

Author SHA1 Message Date
Evan
81e8b9c640 ci: open a tracking issue when the scheduled refresh fails
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:13:59 -07:00
Evan
5f3c4e7118 ci: gate scheduled --force-latest and harden has-secrets check
Only pass --force-latest on the scheduled refresh when the release flagged
"latest" is also the highest semver release, so a mis-click marking an older
maintenance release latest can't roll apache/superset:latest back a major
version. Also make the has-secrets gate an explicit == '1' comparison to match
tag-release.yml and avoid relying on string truthiness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:02:17 -07:00
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 183 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
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 }}
force-latest: ${{ steps.latest.outputs.force-latest }}
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"
# Only move `:latest` when the release flagged "latest" is also the
# highest semver release. This guards against a mis-click leaving an
# older maintenance release (e.g. a 5.x patch shipped after 6.0 GA)
# marked latest, which would otherwise roll `:latest` back a major
# version on the next cron run. If it isn't the newest, we still
# refresh that release's own version tag but leave `:latest` alone.
HIGHEST=$(gh api --paginate "repos/${REPOSITORY}/releases" \
--jq '.[] | select(.draft|not) | select(.prerelease|not) | .tag_name' \
| sed 's/^v//' | sort -V | tail -n1)
if [ "${TAG#v}" = "$HIGHEST" ]; then
echo "force-latest=1" >> "$GITHUB_OUTPUT"
else
echo "::warning::Latest-flagged release $TAG is not the highest semver ($HIGHEST); refreshing its version tag but leaving :latest untouched"
fi
docker-rebuild:
needs: config
if: needs.config.outputs.has-secrets == '1'
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 }}
FORCE_LATEST_FLAG: ${{ needs.config.outputs.force-latest == '1' && '--force-latest' || '' }}
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.
# `--force-latest` is only passed when the config job confirmed the
# fetched release is the newest one (see FORCE_LATEST_FLAG above).
supersetbot docker \
--push \
--preset "$BUILD_PRESET" \
--context release \
--context-ref "$LATEST_RELEASE" \
$FORCE_LATEST_FLAG \
--platform "linux/arm64" \
--platform "linux/amd64"
# The whole point of this cron is catching base-image CVEs, so a silent
# failure is the expensive case — a red X in the Actions tab nobody is
# watching on a Monday. File a tracked issue when any rebuild leg fails so
# a missed security refresh surfaces instead of sitting unnoticed.
notify-on-failure:
needs: [config, docker-rebuild]
if: failure() && needs.config.outputs.has-secrets == '1'
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
steps:
- name: Open a tracking issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
LATEST_RELEASE: ${{ needs.config.outputs.latest-release }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh issue create \
--repo "$REPOSITORY" \
--title "Scheduled Docker image refresh failed for ${LATEST_RELEASE}" \
--label "infra:container" \
--label "bug" \
--body "The weekly Docker base-image refresh failed for release \`${LATEST_RELEASE}\`. Published images may be missing upstream base-layer security patches until this is resolved.
Failed run: ${RUN_URL}"

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