diff --git a/.github/workflows/bootstrap-fog-version-check-stub-sweep.yml b/.github/workflows/bootstrap-fog-version-check-stub-sweep.yml new file mode 100644 index 0000000..f40df20 --- /dev/null +++ b/.github/workflows/bootstrap-fog-version-check-stub-sweep.yml @@ -0,0 +1,66 @@ +name: Sweep for branches missing the FOG version check stub + +# Runs daily. Lists fogproject branches, filters to the ones +# check-fog-version.yml watches (working-1.6/dev-branch/rc-*/feature-*, +# `stable` excluded), and hands off any that are missing the stub to +# bootstrap-fog-version-check-stub.yml. +# +# This exists instead of a create-triggered stub in fogproject because +# GitHub only resolves a `create`-event workflow from the repo's default +# branch, which for fogproject is `stable` - and this project's automation +# is kept off of `stable` entirely. A daily delay before a brand-new branch +# starts being watched is an acceptable trade for that: it only delays when +# watching starts, it doesn't cause a missed version fix on a branch +# already being watched. + +on: + schedule: + - cron: "30 4 * * *" + workflow_dispatch: {} + +jobs: + discover: + runs-on: ubuntu-24.04 + outputs: + branches: ${{ steps.discover.outputs.branches }} + steps: + - uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ vars.FOG_WORKFLOWS_APPID }} + private-key: ${{ secrets.FOG_WORKFLOWS_PRIVATE_KEY }} + owner: FOGProject + repositories: "fogproject" + + - name: Find watched branches missing the stub + id: discover + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -e + all_branches=$(gh api repos/FOGProject/fogproject/branches --paginate --jq '.[].name') + + missing=() + for b in $all_branches; do + case "$b" in + working-1.6|dev-branch|rc-*|feature-*) ;; + *) continue ;; + esac + if ! gh api "repos/FOGProject/fogproject/contents/.github/workflows/check-fog-version.yml?ref=$b" >/dev/null 2>&1; then + missing+=("$b") + fi + done + + json=$(printf '%s\n' "${missing[@]:-}" | jq -R . | jq -sc 'map(select(length > 0))') + echo "branches=$json" >> "$GITHUB_OUTPUT" + echo "Missing the stub on: $json" + + bootstrap: + needs: discover + if: needs.discover.outputs.branches != '[]' + strategy: + matrix: + branch: ${{ fromJson(needs.discover.outputs.branches) }} + uses: ./.github/workflows/bootstrap-fog-version-check-stub.yml + with: + branch: ${{ matrix.branch }} diff --git a/.github/workflows/bootstrap-fog-version-check-stub.yml b/.github/workflows/bootstrap-fog-version-check-stub.yml new file mode 100644 index 0000000..4109991 --- /dev/null +++ b/.github/workflows/bootstrap-fog-version-check-stub.yml @@ -0,0 +1,89 @@ +name: Bootstrap FOG version check stub + +# Single-branch worker, called by bootstrap-fog-version-check-stub-sweep.yml +# for each fogproject branch missing the stub (also runnable by hand via +# workflow_dispatch against one branch). +# +# check-fog-version.yml's push trigger only fires on a branch that already +# has a copy of that stub file committed to it. New feature-*/rc-* branches +# cut from dev-branch inherit it for free once dev-branch has it, but a +# branch created from anywhere else (or before dev-branch had it) would +# silently never be watched. This copies the canonical stub - whatever is +# currently committed on dev-branch - onto the given branch directly, if it +# matches the watched patterns and doesn't already have it. +# +# Never touches `stable` (excluded from the watched patterns, same as +# check-fog-version.yml itself) or any branch outside those patterns. +# Deliberately not triggered by anything in fogproject itself (unlike +# check-fog-version.yml's push stub) - GitHub only resolves a `create`-event +# workflow from fogproject's default branch, which is `stable`, and this +# repo's automation is kept off of `stable` entirely. The sweep workflow +# polls instead. + +on: + workflow_call: + inputs: + branch: + required: true + type: string + workflow_dispatch: + inputs: + branch: + description: "fogproject branch to bootstrap the stub onto" + required: true + type: string + +jobs: + bootstrap-stub: + runs-on: ubuntu-24.04 + steps: + - name: Check branch matches watched patterns + id: match + env: + BRANCH: ${{ inputs.branch }} + run: | + matches=false + case "$BRANCH" in + working-1.6|dev-branch|rc-*|feature-*) matches=true ;; + esac + echo "matches=$matches" >> "$GITHUB_OUTPUT" + + - uses: actions/create-github-app-token@v3 + if: steps.match.outputs.matches == 'true' + id: app-token + with: + client-id: ${{ vars.FOG_WORKFLOWS_APPID }} + private-key: ${{ secrets.FOG_WORKFLOWS_PRIVATE_KEY }} + owner: FOGProject + repositories: "fogproject" + + - uses: actions/checkout@v7 + if: steps.match.outputs.matches == 'true' + with: + token: ${{ steps.app-token.outputs.token }} + repository: FOGProject/fogproject + ref: ${{ inputs.branch }} + + - name: Copy the stub from dev-branch if this branch doesn't have it yet + if: steps.match.outputs.matches == 'true' + env: + BRANCH: ${{ inputs.branch }} + BOT_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + set -e + stub_path=".github/workflows/check-fog-version.yml" + + if [ -f "$stub_path" ]; then + echo "Stub already present on $BRANCH - nothing to do." + exit 0 + fi + + git fetch --depth 1 origin dev-branch + mkdir -p "$(dirname "$stub_path")" + git show origin/dev-branch:"$stub_path" > "$stub_path" + + git config user.name "${BOT_SLUG}[bot]" + git config user.email "${BOT_SLUG}[bot]@users.noreply.github.com" + git add "$stub_path" + git commit -m "ci: bootstrap check-fog-version stub on ${BRANCH}" + git push origin "HEAD:${BRANCH}" diff --git a/.github/workflows/check-fog-version-sweep.yml b/.github/workflows/check-fog-version-sweep.yml new file mode 100644 index 0000000..a9e6ed9 --- /dev/null +++ b/.github/workflows/check-fog-version-sweep.yml @@ -0,0 +1,61 @@ +name: Sweep FOG version check across all watched branches + +# Failsafe catch-all, running daily in addition to the push-triggered stub in +# fogproject. The push trigger should catch essentially everything, but this +# covers anything it misses - a missed webhook delivery, an Actions outage +# during the original push, a branch whose stub hadn't been bootstrapped yet +# at push time, etc. +# +# Lists fogproject branches, filters to the ones check-fog-version.yml +# watches (working-1.6/dev-branch/rc-*/feature-*, `stable` excluded - owned +# by stable-releases.yml's release flow), and runs the real check-and-fix +# workflow against every one of them, whether or not the push stub is even +# present there yet. + +on: + schedule: + - cron: "45 4 * * *" + workflow_dispatch: {} + +jobs: + discover: + runs-on: ubuntu-24.04 + outputs: + branches: ${{ steps.discover.outputs.branches }} + steps: + - uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ vars.FOG_WORKFLOWS_APPID }} + private-key: ${{ secrets.FOG_WORKFLOWS_PRIVATE_KEY }} + owner: FOGProject + repositories: "fogproject" + + - name: List watched branches + id: discover + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -e + all_branches=$(gh api repos/FOGProject/fogproject/branches --paginate --jq '.[].name') + + watched=() + for b in $all_branches; do + case "$b" in + working-1.6|dev-branch|rc-*|feature-*) watched+=("$b") ;; + esac + done + + json=$(printf '%s\n' "${watched[@]:-}" | jq -R . | jq -sc 'map(select(length > 0))') + echo "branches=$json" >> "$GITHUB_OUTPUT" + echo "Watched branches: $json" + + check-version: + needs: discover + if: needs.discover.outputs.branches != '[]' + strategy: + matrix: + branch: ${{ fromJson(needs.discover.outputs.branches) }} + uses: ./.github/workflows/check-fog-version.yml + with: + branch: ${{ matrix.branch }} diff --git a/.github/workflows/check-fog-version.yml b/.github/workflows/check-fog-version.yml new file mode 100644 index 0000000..483c156 --- /dev/null +++ b/.github/workflows/check-fog-version.yml @@ -0,0 +1,132 @@ +name: Check FOG version + +# Reusable workflow backing: +# - the push-triggered stub in FOGProject/fogproject's +# .github/workflows/check-fog-version.yml (instant, per-branch), and +# - check-fog-version-sweep.yml in this repo (daily, all watched branches - +# a failsafe catch-all for anything the push trigger misses, e.g. a +# missed webhook delivery or an Actions outage during the original push). +# Also directly workflow_dispatch-able against a single branch by hand. +# See fog-docs' Development section for the full picture of why this is +# split across two repos. +# +# Recomputes FOG_VERSION/FOG_CHANNEL using the same formula as fogproject's +# .githooks/pre-commit. That hook is client-side and never runs on a PR merged +# via GitHub's web UI, so this independently recomputes what the version +# should be and, if it disagrees with what's actually committed, pushes the +# fix directly to the branch that drifted - no fix branch, no PR, since this +# is a mechanical correction of the same kind the hook already makes locally. +# +# Never invoked for `stable` - that branch's version is owned entirely by +# stable-releases.yml's release flow. + +on: + workflow_call: + inputs: + branch: + required: true + type: string + workflow_dispatch: + inputs: + branch: + description: "fogproject branch to check" + required: true + type: string + +jobs: + check-version: + runs-on: ubuntu-24.04 + steps: + - uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ vars.FOG_WORKFLOWS_APPID }} + private-key: ${{ secrets.FOG_WORKFLOWS_PRIVATE_KEY }} + owner: FOGProject + repositories: "fogproject" + + - uses: actions/checkout@v7 + with: + token: ${{ steps.app-token.outputs.token }} + repository: FOGProject/fogproject + ref: ${{ inputs.branch }} + fetch-depth: 0 + fetch-tags: true + + - name: Fetch master (needed by the version formula) + run: git fetch origin master:master + + - name: Compute expected FOG_VERSION / FOG_CHANNEL + env: + GITBRANCH: ${{ inputs.branch }} + run: | + set -e + system_file="packages/web/lib/fog/system.class.php" + + gitcom=$(git rev-list --tags --no-walk --max-count=1) + gitcount=$(git rev-list master..HEAD --count) + + branchon=$(echo "$GITBRANCH" | awk -F'-' '{print $1}') + branchend=$(echo "$GITBRANCH" | awk -F'-' '{print $2}') + + current_version=$(grep "define('FOG_VERSION'" "$system_file" | sed "s/.*FOG_VERSION', '\([^']*\)');/\1/") + current_channel=$(grep "define('FOG_CHANNEL'" "$system_file" | sed "s/.*FOG_CHANNEL', '\([^']*\)');/\1/") + + verbegin="" + channel="$current_channel" + trunkversion="$current_version" + + case "$branchon" in + dev) + tagversion=$(git describe --tags "$gitcom") + baseversion=${tagversion%.*} + trunkversion="${baseversion}.${gitcount}" + channel="Patches" + ;; + working) + verbegin="${branchend}.0-beta" + trunkversion="${verbegin}.${gitcount}" + channel="Beta" + ;; + rc) + channel="Release Candidate" + version_prefix="${branchend}.0-RC" + n=$(printf '%s\n' "$current_version" | sed -n "s/^${version_prefix}-\([0-9][0-9]*\)\$/\1/p") + if [ -n "$n" ]; then + trunkversion="${version_prefix}-$((n + 1))" + else + trunkversion="${version_prefix}-1" + fi + ;; + feature) + verbegin="${branchend}.0-feature" + trunkversion="${verbegin}.${gitcount}" + channel="Feature" + ;; + esac + + sed -i "s/define('FOG_VERSION',.*);/define('FOG_VERSION', '$trunkversion');/g" "$system_file" + sed -i "s/define('FOG_CHANNEL',.*);/define('FOG_CHANNEL', '$channel');/g" "$system_file" + + - name: Commit and push if the committed version is stale + env: + GITBRANCH: ${{ inputs.branch }} + BOT_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + set -e + system_file="packages/web/lib/fog/system.class.php" + + if git diff --quiet -- "$system_file"; then + echo "Version already correct on $GITBRANCH - nothing to do." + exit 0 + fi + + echo "Version drift detected on $GITBRANCH:" + git diff -- "$system_file" + + git config user.name "${BOT_SLUG}[bot]" + git config user.email "${BOT_SLUG}[bot]@users.noreply.github.com" + + git add "$system_file" + git commit -m "chore: fix stale FOG_VERSION/FOG_CHANNEL on ${GITBRANCH}" + git push origin "HEAD:${GITBRANCH}"