VPM Publish #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: VPM Publish | |
| on: | |
| schedule: | |
| # Saturday 14:00 UTC | |
| - cron: '0 14 * * 6' | |
| workflow_dispatch: | |
| inputs: | |
| tag_override: | |
| description: 'Tag override (default: vpm-YYYY-Www.RUN). Required when stable=true.' | |
| required: false | |
| stable: | |
| description: 'Cut a stable (non-prerelease) release. Uses package.json versions as-is, no dev suffix.' | |
| type: boolean | |
| default: false | |
| # Serialize publishes so cron + manual dispatch can never race on the vpm branch. | |
| # Each run produces its own tag (vpm-YYYY-Www.RUN) so release-tag races aren't | |
| # possible, but the shared vpm branch still needs ordering. | |
| concurrency: | |
| group: vpm-publish | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # release creation + push to the vpm branch | |
| env: | |
| VPM_BRANCH: vpm | |
| VPM_INDEX_FILE: vpm-repo.json | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Compute tag and dev suffix | |
| id: meta | |
| env: | |
| TAG_OVERRIDE: ${{ inputs.tag_override }} | |
| RUN_NUMBER: ${{ github.run_number }} | |
| STABLE: ${{ inputs.stable }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${STABLE:-false}" = "true" ]; then | |
| # Stable mode: package.json versions are shipped as-is. A tag | |
| # override is mandatory so stable runs don't collide with the | |
| # weekly cron's vpm-YYYY-Www.RUN namespace. | |
| if [ -z "${TAG_OVERRIDE}" ]; then | |
| echo "::error::stable=true requires tag_override (e.g. v1.2.3)" | |
| exit 1 | |
| fi | |
| TAG="${TAG_OVERRIDE}" | |
| SUFFIX="" | |
| else | |
| # Weekly/dev mode: suffix derived from week + run number, | |
| # deterministic for re-runs of the same workflow run, fresh for | |
| # each new run. Maintainers don't bump package.json — the cron | |
| # snapshot does it for them. | |
| TAG="${TAG_OVERRIDE:-vpm-$(date -u +%G-W%V).${RUN_NUMBER}}" | |
| SUFFIX="dev.$(date -u +%G.W%V).${RUN_NUMBER}" | |
| fi | |
| { | |
| echo "tag=$TAG" | |
| echo "suffix=$SUFFIX" | |
| echo "stable=${STABLE:-false}" | |
| } >> "$GITHUB_OUTPUT" | |
| # Stage the previous index for update-index to merge into. | |
| # branch + non-empty file → normal merge | |
| # branch + missing/empty file → ABORT (corruption / accidental delete) | |
| # branch absent → first-run bootstrap | |
| - name: Stage previous index | |
| id: stage | |
| run: | | |
| set -euo pipefail | |
| mkdir -p prev | |
| if git ls-remote --exit-code --heads origin "$VPM_BRANCH" >/dev/null 2>&1; then | |
| git fetch --depth=1 origin "$VPM_BRANCH" | |
| git show "FETCH_HEAD:$VPM_INDEX_FILE" > "prev/$VPM_INDEX_FILE" 2>/dev/null \ | |
| || { echo "::error::$VPM_BRANCH branch exists but $VPM_INDEX_FILE is missing on it"; exit 1; } | |
| [ -s "prev/$VPM_INDEX_FILE" ] \ | |
| || { echo "::error::$VPM_INDEX_FILE on $VPM_BRANCH is empty"; exit 1; } | |
| echo "first_run=false" >> "$GITHUB_OUTPUT" | |
| else | |
| : > "prev/$VPM_INDEX_FILE" | |
| echo "first_run=true" >> "$GITHUB_OUTPUT" | |
| echo "First run: $VPM_BRANCH branch does not exist yet." | |
| fi | |
| - name: Build packages | |
| env: | |
| DEV_SUFFIX: ${{ steps.meta.outputs.suffix }} | |
| run: python3 .github/scripts/vpmpackagegen.py build --dev-suffix "$DEV_SUFFIX" --out out | |
| # Merge per-package metadata into the prior index. The added.txt lists | |
| # zip filenames of newly-added entries — empty only if the workflow run | |
| # is being re-attempted with the same run_number on the same source | |
| # state (every entry already in the index from the prior attempt). In | |
| # the normal weekly path, every package gets a fresh dev-suffixed | |
| # version so all 29 are added. | |
| - name: Generate new index | |
| id: index | |
| env: | |
| REPO: ${{ github.repository }} | |
| SERVER_URL: ${{ github.server_url }} | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| STABLE: ${{ steps.meta.outputs.stable }} | |
| run: | | |
| set -euo pipefail | |
| BASE_URL="${SERVER_URL}/${REPO}/releases/download/${TAG}" | |
| SELF_URL="https://raw.githubusercontent.com/${REPO}/${VPM_BRANCH}/${VPM_INDEX_FILE}" | |
| echo "::notice title=VPM Repo URL::$SELF_URL" | |
| python3 .github/scripts/vpmpackagegen.py update-index \ | |
| --require-superset \ | |
| --self-url "$SELF_URL" \ | |
| --added-list added.txt \ | |
| "prev/$VPM_INDEX_FILE" \ | |
| "$BASE_URL" \ | |
| out/meta > new-index.json | |
| if [ -s added.txt ]; then | |
| count=$(wc -l < added.txt) | |
| echo "Added ${count} package version(s)." | |
| echo "has_changes=true" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "### Added packages (${count})" | |
| echo | |
| while IFS=$'\t' read -r name version _zip; do | |
| echo "- \`${name}\` @ \`${version}\`" | |
| done < added.txt | |
| } > release-body.md | |
| cat release-body.md >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| if [ "${STABLE}" = "true" ]; then | |
| echo "::error::stable=true but no new package versions were added — bump versions in package.json or use a fresh tag." | |
| exit 1 | |
| fi | |
| echo "Re-run with no new versions; release and push will be skipped." | |
| echo "has_changes=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Upload zips BEFORE pushing the index so the index never references | |
| # a missing asset. Re-runs are safe — the action uploads to the | |
| # existing release if the tag already exists, and the build is | |
| # deterministic so identical bytes produce identical asset content. | |
| - name: Create release and upload zips | |
| if: steps.index.outputs.has_changes == 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.meta.outputs.tag }} | |
| name: VPM ${{ steps.meta.outputs.tag }} | |
| body_path: release-body.md | |
| prerelease: ${{ steps.meta.outputs.stable != 'true' }} | |
| files: out/artifacts/*.zip | |
| fail_on_unmatched_files: true | |
| # Push the new index to the vpm branch in a fresh clone so we don't | |
| # entangle the main checkout's worktree state. | |
| - name: Publish to vpm branch | |
| if: steps.index.outputs.has_changes == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| FIRST_RUN: ${{ steps.stage.outputs.first_run }} | |
| run: | | |
| set -euo pipefail | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| URL="https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" | |
| if [ "$FIRST_RUN" = "true" ]; then | |
| git init -q -b "$VPM_BRANCH" vpm-pub | |
| git -C vpm-pub remote add origin "$URL" | |
| else | |
| git clone -q --depth 1 --branch "$VPM_BRANCH" --single-branch "$URL" vpm-pub | |
| fi | |
| cp new-index.json "vpm-pub/$VPM_INDEX_FILE" | |
| cd vpm-pub | |
| git add "$VPM_INDEX_FILE" | |
| git commit -q -m "vpm: $TAG" | |
| git push -u origin "$VPM_BRANCH" |