Supply-chain hardening for the CI pipeline. 23 workflow files
modified, 59 mutable-tag refs replaced with commit SHAs.
The risk
Every `uses:` reference in .github/workflows/*.yml was pinned to a
mutable tag (e.g., `actions/checkout@v4`). A maintainer of an
action — or a compromised maintainer account — can repoint that
tag to malicious code, and our pipelines silently pull it on the
next run. The tj-actions/changed-files compromise of March 2025 is
the canonical example: maintainer credential leak, attacker
repointed several `@v<N>` tags to a payload that exfiltrated
repository secrets. Repos that pinned to SHAs were unaffected.
The fix
Replace each `@v<N>` with `@<commit-sha> # v<N>`. The trailing
comment preserves human readability ("ah, this is v4"); the SHA
makes the reference immutable.
Actions covered (10 distinct):
actions/{checkout,setup-go,setup-python,setup-node,upload-artifact,github-script}
docker/{login-action,setup-buildx-action,build-push-action}
github/codeql-action/{init,autobuild,analyze}
dorny/paths-filter
imjasonh/setup-crane
pnpm/action-setup (already pinned in molecule-app, listed here for completeness)
Excluded:
Molecule-AI/molecule-ci/.github/workflows/disable-auto-merge-on-push.yml@main
— internal org reusable workflow; we control its repo, threat model
is different from third-party actions. Conventional to pin to @main
rather than SHA for internal reusables.
The maintenance cost
SHA pinning means upstream fixes require manual SHA bumps. Without
automation, pinned SHAs go stale. So this PR also enables Dependabot
across four ecosystems:
- github-actions (workflows)
- gomod (workspace-server)
- npm (canvas)
- pip (workspace runtime requirements)
Weekly cadence — the supply-chain attack window is "minutes between
repoint and pull"; weekly auto-bumps don't help with zero-days
regardless. The point is to pull in non-zero-day fixes without
operator effort.
Aligns with user-stated principle: "long-term, robust, fully-
automated, eliminate human error."
Companion PR: Molecule-AI/molecule-controlplane#308 (same pattern,
smaller surface).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
2.9 KiB
YAML
86 lines
2.9 KiB
YAML
name: promote-latest
|
|
|
|
# Manually retag ghcr.io/molecule-ai/platform:staging-<sha> → :latest
|
|
# (and the same for the tenant image). Use this to:
|
|
#
|
|
# 1. Promote a :staging-<sha> to prod before the canary fleet is live
|
|
# (one-off during the initial rollout).
|
|
# 2. Roll back :latest to a prior known-good digest after a bad
|
|
# promotion slipped past canary (use scripts/rollback-latest.sh
|
|
# for a local / emergency path; this workflow is for scheduled
|
|
# or from-browser promotions).
|
|
#
|
|
# Running this workflow needs no extra secrets — GitHub's default
|
|
# GITHUB_TOKEN has write:packages for repo-owned GHCR images, which
|
|
# is all we need for a remote retag via `crane tag`.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
sha:
|
|
description: 'Short sha to promote (e.g. 4c1d56e). Must match an existing :staging-<sha> tag.'
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
env:
|
|
IMAGE_NAME: ghcr.io/molecule-ai/platform
|
|
TENANT_IMAGE_NAME: ghcr.io/molecule-ai/platform-tenant
|
|
|
|
jobs:
|
|
promote:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4
|
|
|
|
- name: GHCR login
|
|
run: |
|
|
echo "${{ secrets.GITHUB_TOKEN }}" \
|
|
| crane auth login ghcr.io -u "${{ github.actor }}" --password-stdin
|
|
|
|
- name: Retag platform image
|
|
run: |
|
|
set -eu
|
|
SRC="${IMAGE_NAME}:staging-${{ inputs.sha }}"
|
|
if ! crane digest "$SRC" >/dev/null 2>&1; then
|
|
echo "::error::$SRC not found in registry — double-check the sha."
|
|
exit 1
|
|
fi
|
|
EXPECTED=$(crane digest "$SRC")
|
|
crane tag "$SRC" latest
|
|
ACTUAL=$(crane digest "${IMAGE_NAME}:latest")
|
|
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
|
echo "::error::retag digest mismatch (expected $EXPECTED, got $ACTUAL)"
|
|
exit 1
|
|
fi
|
|
echo "OK ${IMAGE_NAME}:latest → $ACTUAL"
|
|
|
|
- name: Retag tenant image
|
|
run: |
|
|
set -eu
|
|
SRC="${TENANT_IMAGE_NAME}:staging-${{ inputs.sha }}"
|
|
if ! crane digest "$SRC" >/dev/null 2>&1; then
|
|
echo "::error::$SRC not found — tenant image may not have built for this sha."
|
|
exit 1
|
|
fi
|
|
EXPECTED=$(crane digest "$SRC")
|
|
crane tag "$SRC" latest
|
|
ACTUAL=$(crane digest "${TENANT_IMAGE_NAME}:latest")
|
|
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
|
echo "::error::tenant retag digest mismatch"
|
|
exit 1
|
|
fi
|
|
echo "OK ${TENANT_IMAGE_NAME}:latest → $ACTUAL"
|
|
|
|
- name: Summary
|
|
run: |
|
|
{
|
|
echo "## :latest promoted to staging-${{ inputs.sha }}"
|
|
echo
|
|
echo "Both platform + tenant images retagged. Prod tenants"
|
|
echo "will auto-pull within their 5-min update cycle."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|