Skip to content

Merge pull request #549 from ChBrain/culture/schleswig_holstein #81

Merge pull request #549 from ChBrain/culture/schleswig_holstein

Merge pull request #549 from ChBrain/culture/schleswig_holstein #81

name: release-gating action
# When culture content enters culture/release, stage its release gating
# data -- the data/countries.json registry entry -- on main BEFORE the
# culture/release -> main promotion.
#
# For every culture present in culture/release, generate a skeleton
# countries.json entry (scripts/generate_country_entry.py); if that
# changes the registry, open a governance/release-gating-* PR against
# main. A human completes the TODO fields (anchor.iso, name_source) --
# the release audit -- and the blocking gate (release-gating-gate.yml)
# holds the promotion until this PR merges. See issue #257.
#
# Branch lane: data/countries.json is in GOVERNANCE_FILES
# (tests/branch_scope.py) because the registry schema ships with the
# release pipeline. The pr-gate rejects governance-file edits on
# chore/* heads, so the workflow opens its auto-PR on a governance/*
# branch and the alignment cross-check
# (tests/test_culture_completeness_alignment.py) skips on that head.
#
# Scope: this stages the COUNTRY REGISTRY only. Persona names continue to
# sync via sync_name_register.yml -- name_register.json is governance-lane
# and cannot share this PR; the blocking gate covers the registry only.
# Gating name_register would be a separate extension.
on:
push:
branches:
- culture/release
# Concurrency: when multiple culture PRs land on culture/release in quick
# succession, each push fires its own run. Without a guard, 7 parallel
# culture PRs => up to 7 governance/release-gating-* PRs with overlapping
# countries.json edits. The detect step computes the FULL main <-> release
# delta, so any single run is naturally cumulative -- only the latest
# matters. Cancel in-progress runs and let the newest one produce the
# single PR that covers every culture currently outstanding.
concurrency:
group: release-gating-cultures
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
stage-registry:
runs-on: ubuntu-latest
steps:
- name: Checkout main
# PR target tree: peter-evans bases the chore branch off this
# checkout's HEAD, so the resulting PR diff is main + the
# countries.json change only.
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Checkout culture/release into ./release
# Source-of-truth tree for the generator. The reason this whole
# workflow exists is that the culture's folder
# (regions/<region>/<parent>?/<slug>/) does not yet exist on
# main -- it's still in culture/release waiting for the
# promotion. scripts/generate_country_entry.py reads that folder
# to derive `language` from hofstede_bag.yaml and to confirm the
# sovereign-vs-sub layout, so it needs the release tree mounted
# somewhere on disk. Sibling worktree keeps the main checkout
# untouched for peter-evans below.
uses: actions/checkout@v4
with:
ref: culture/release
fetch-depth: 0
path: release
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install pyyaml
- name: Detect cultures present in culture/release
id: detect
# Sub-national-aware slug extraction (see
# scripts/extract_culture_slugs.py): depth-3 sovereign-state
# and depth-4 sub-national share one Python helper that
# release-gating-gate.yml also uses, so the two workflows
# can't drift on the depth contract.
run: |
git fetch origin culture/release
SLUGS=$(git diff --name-only main origin/culture/release \
| python3 scripts/extract_culture_slugs.py \
| tr '\n' ' ' | xargs)
echo "slugs=$SLUGS" >> "$GITHUB_OUTPUT"
echo "first=$(echo "$SLUGS" | awk '{print $1}')" >> "$GITHUB_OUTPUT"
echo "Cultures in culture/release: ${SLUGS:-<none>}"
- name: Generate skeleton registry entries
# Run from release/ so the script's REPO_ROOT resolves to the
# culture/release tree where the slug's folder lives. The
# script reads and writes release/data/countries.json; the next
# step copies the result back into the main checkout for the
# PR.
#
# Invariant relied on: data/countries.json on culture/release
# equals main's. Registry edits land governance-side via this
# same automation (governance/release-gating-registry -> main)
# and then sync back into culture/release through the standard
# sync/release-from-main-* PRs -- never a direct write to
# culture/release. If the invariant ever drifts, the copy step
# would silently revert release-side changes; the diff step
# below would still surface that as a real change against main.
if: steps.detect.outputs.slugs != ''
working-directory: release
run: python scripts/generate_country_entry.py ${{ steps.detect.outputs.slugs }}
- name: Copy registry change back to main worktree
# peter-evans below commits whatever the main checkout's working
# tree carries; bring the countries.json delta there.
if: steps.detect.outputs.slugs != ''
run: cp release/data/countries.json data/countries.json
- name: Detect registry change
id: diff
run: |
if git diff --quiet data/countries.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Open gating-data PR
if: steps.diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Stable branch name -- the same PR is updated on each
# culture/release push instead of cloning a new one per commit
# SHA. peter-evans/create-pull-request@v6 3-way-merges user
# edits (TODO fills) on top of the regenerated skeleton, so
# filled-in fields are preserved across re-runs. The generator
# (scripts/generate_country_entry.py) is idempotent: a slug
# already in countries.json is left untouched, so once a
# culture's entry merges to main, subsequent runs ignore it.
#
# governance/ lane (not chore/): data/countries.json is in
# GOVERNANCE_FILES, and the pr-gate's "Diff within branch
# scope" check rejects governance-file edits on chore/* heads.
# See the top-of-file comment for the routing.
branch: governance/release-gating-registry
base: main
# Restrict the commit to the registry file. The sibling
# `release/` checkout above is a separate git worktree; a
# blanket `git add .` would capture its .git as a submodule
# gitlink and the PR would carry a bogus subproject entry.
# add-paths is the supported way to scope what peter-evans
# commits.
add-paths: data/countries.json
commit-message: 'chore: stage release gating data for ${{ steps.detect.outputs.first }}'
title: 'chore: release gating data - ${{ steps.detect.outputs.first }}'
body: |
Automated release gating data -- generated because culture
content for an unregistered culture is present in `culture/release`.
Skeleton `data/countries.json` entries were added. **Complete every
`TODO` field** (`anchor.iso`, `name_source`) before merging -- that
completion is the release audit.
The blocking gate (`release-gating-gate.yml`) holds the
`culture/release -> main` promotion until this PR is merged.
See issue #257.
labels: chore
draft: false