Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions .github/workflows/build-from-source.yml

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions .github/workflows/fold-on-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# UNIVERSAL fold-on-push trigger — byte-identical across every Model B fork.
#
# Purpose: when a push lands on ANY branch other than the default branch or
# intarweb-dev, dispatch sync-upstream.yml to re-fold + re-publish. This is
# the event-driven counterpart to the hourly schedule — covers "new commit
# landed on a PR head branch" and "operator pushed a new branch they're
# about to PR" instantly, without waiting up to 1 hour.
#
# Why every-branch-except-defaults: PR head branches use varied naming
# (feat/*, fix/*, test/*, docs/*, etc). Enumerating patterns means missing
# new ones (build-overlay.yml's pattern misses feat/* and docs/*, MEASURED
# 2026-06-10). Wildcard with explicit exclusions for self-trigger paths is
# the simplest correct shape.
#
# Loop prevention: branches-ignore covers EVERY ref this workflow's downstream
# (sync-upstream.yml) pushes — main, master, intarweb-dev. Pushes from the
# hard-reset/ops-overlay/intarweb-dev-regen logic therefore do NOT re-fire
# this workflow.
#
# Cadence bypass: the dispatch into sync-upstream.yml fires it as a
# workflow_dispatch event. sync-upstream's cadence gate explicitly bypasses
# for non-schedule events (see its ⏱️ Cadence gate step). So a push triggers
# an immediate fold regardless of vars.SYNC_CADENCE_HOURS.
#
# Codified in oss-contributing:ghcr-fork-mirror skill honest-fact #64.

name: Fold on PR head-branch push

on:
push:
branches-ignore:
- main
- master
- intarweb-dev

permissions:
actions: write # required for `gh workflow run` dispatch into sync-upstream

jobs:
dispatch-sync:
runs-on: ubuntu-latest
steps:
- name: 🤖 Mint org-bot app token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.SYNC_APP_ID }}
private-key: ${{ secrets.SYNC_APP_PRIVATE_KEY }}

- name: 🌿 Dispatch sync-upstream (full re-fold)
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
echo " push detected on branch: ${{ github.ref_name }}"
echo " triggering full re-fold via sync-upstream.yml"
# The workflow_dispatch event is whitelisted by sync-upstream's
# cadence gate — runs immediately, bypassing SYNC_CADENCE_HOURS.
# --ref points at the workflow file location (default branch),
# NOT the branch that was pushed. sync-upstream itself always
# operates on default branch + folds open PRs regardless of
# which branch triggered this dispatch.
gh workflow run "Sync from upstream + auto-regen intarweb-dev" \
--repo "${{ github.repository }}" \
--ref "${{ github.event.repository.default_branch }}"
115 changes: 115 additions & 0 deletions .github/workflows/fork-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# UNIVERSAL fork-publish workflow — BYTE-IDENTICAL across every fork.
#
# Mirrors upstream's released versioned tags to ghcr.io/${{ github.repository }}
# (auto-lowercased by docker/metadata-action). Same image namespace as
# build-from-source.yml — ONE concept, no per-fork vanity names.
#
# :latest is owned by build-from-source.yml (built from intarweb-dev).
# This workflow handles VERSIONED tags only (1.2.3, 1.2, 1, etc).
#
# Codified in oss-contributing:ghcr-fork-mirror skill honest-fact #57.

name: Fork-publish (mirror upstream → GHCR)

on:
schedule:
- cron: '17 6 * * *' # daily 06:17 UTC — well before sync-upstream's :00 cron
workflow_dispatch:

permissions:
contents: read
packages: write

env:
# Per-fork override via repo Variable IMAGE_NAME — used when upstream's published
# image name differs from our repo name (e.g. docker-autoheal repo → autoheal image).
# Falls back to repo name. ONE image per fork, shared with build-from-source.yml.
IMAGE: ghcr.io/${{ github.repository_owner }}/${{ vars.IMAGE_NAME || github.event.repository.name }}

jobs:
mirror:
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3

- name: 🔑 Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 🔍 Discover upstream tags
id: tags
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# GitHub repo (for `gh release list` to discover versioned tags)
UPSTREAM_REPO="joeblack2k/SGM-Helper"
# Docker image source (where imagetools mirrors FROM). Defaults to
# GitHub repo path; override via repo Variable UPSTREAM_IMAGE for forks
# where Docker Hub path differs (e.g. acmesh-official/acme.sh on GitHub
# but neilpang/acme.sh on Docker Hub).
UPSTREAM_IMAGE="${{ vars.UPSTREAM_IMAGE || format('{0}/{1}', 'joeblack2k', 'SGM-Helper') }}"
RELEASES=$(gh release list --repo "$UPSTREAM_REPO" --limit 3 --json tagName \
--jq '.[].tagName | sub("^v"; "")' 2>/dev/null | tr '\n' ' ' || echo "")
# Fallback: if upstream uses raw git tags without GH Release objects
# (e.g. neo4j/docker-neo4j), gh release list returns empty. Use the
# git tags API and filter for version-shaped tags (must contain at least
# one N.N component; skips purely descriptive words like
# community/latest/enterprise/nightly/dev/main). Honest-fact #86.
if [ -z "$(echo "$RELEASES" | tr -d ' ')" ]; then
RELEASES=$(gh api "repos/$UPSTREAM_REPO/tags?per_page=10" \
--jq '.[].name' 2>/dev/null \
| grep -E '[0-9]+\.[0-9]+' \
| sed 's/^v//' \
| head -3 \
| tr '\n' ' ' || echo "")
[ -n "$(echo "$RELEASES" | tr -d ' ')" ] && \
echo " (release-list was empty; fell back to git tags API)"
fi
MAJORS=$(for r in $RELEASES; do echo "${r%%.*}"; done | sort -u | tr '\n' ' ')
TAGS="$RELEASES $MAJORS"
echo "upstream_image=$UPSTREAM_IMAGE" >> "$GITHUB_OUTPUT"
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
echo " will mirror: $TAGS"

- name: 🪞 Mirror tags to GHCR
run: |
set -euo pipefail
UPSTREAM="${{ steps.tags.outputs.upstream_image }}"
# Lowercase the image (Docker requires lowercase repo names; metadata-action
# does this automatically for build-push, we do it manually here for imagetools).
TARGET=$(echo "${IMAGE}" | tr '[:upper:]' '[:lower:]')
NEWEST_VERSIONED=""
for tag in ${{ steps.tags.outputs.tags }}; do
[ -z "$tag" ] && continue
echo "::group::Mirror $UPSTREAM:$tag → $TARGET:$tag"
if docker buildx imagetools inspect "$UPSTREAM:$tag" >/dev/null 2>&1; then
docker buildx imagetools create --tag "$TARGET:$tag" "$UPSTREAM:$tag"
echo " ✓ mirrored $tag"
# Track the first (newest) versioned tag for :latest aliasing below
if [ -z "$NEWEST_VERSIONED" ]; then NEWEST_VERSIONED="$tag"; fi
else
echo " - upstream tag $tag does not exist, skipping"
fi
echo "::endgroup::"
done

# Also publish :latest pointing to the newest successfully-mirrored version,
# ONLY if this fork is plain-mirror (i.e. has no source-build). Source-built
# forks (vars.IS_SOURCE_BUILT=true) own :latest via build-from-source.yml,
# which builds from intarweb-dev (= upstream + open PRs cherry-picked + ops
# overlay). Letting fork-publish stomp :latest on those forks would clobber
# our patch stack with upstream-pristine, breaking the contract.
# Plain-mirror forks (var unset) keep the historical behavior — fork-publish
# IS the :latest source. Honest-fact #72.
if [ -n "$NEWEST_VERSIONED" ] && [ "${{ vars.IS_SOURCE_BUILT }}" != "true" ]; then
echo "::group::Alias :latest → :$NEWEST_VERSIONED"
docker buildx imagetools create --tag "$TARGET:latest" "$UPSTREAM:$NEWEST_VERSIONED"
echo " ✓ :latest → :$NEWEST_VERSIONED"
echo "::endgroup::"
elif [ "${{ vars.IS_SOURCE_BUILT }}" = "true" ]; then
echo " - skipping :latest alias (IS_SOURCE_BUILT=true; build-from-source.yml owns :latest)"
fi
Loading
Loading