Update packages image #24
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: Publish | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| source_repo: | |
| required: false | |
| type: string | |
| source_version: | |
| required: false | |
| type: string | |
| force_publish: | |
| description: "Force regenerate/sign/publish even if collection.json didn't change" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: pages-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| runs-on: [self-hosted, macOS] | |
| outputs: | |
| no_changes: ${{ steps.detect.outputs.no_changes }} | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: true | |
| submodules: false | |
| clean: true | |
| fetch-depth: 0 | |
| - name: Clone Swift Package Collection Generator (5.10) | |
| run: | | |
| set -euo pipefail | |
| git clone --branch 5.10 --depth 1 https://github.com/swiftlang/swift-package-collection-generator.git | |
| - name: Prepare previous collection for diff | |
| run: | | |
| set -euo pipefail | |
| mkdir -p public .tmp | |
| curl -fsSL "https://thatfactory.github.io/swift-package-collection/collection.json" \ | |
| -o .tmp/collection.prev.json || echo '{}' > .tmp/collection.prev.json | |
| - name: Configure git auth for GitHub clones (incl submodules) | |
| run: | | |
| set -euo pipefail | |
| git config --global url."https://x-access-token:${{ secrets.PACKAGE_COLLECTION_TOKEN }}@github.com/".insteadOf "https://github.com/" | |
| - name: Generate collection.json from packages.json | |
| run: | | |
| set -euo pipefail | |
| mkdir -p public .tmp | |
| cd swift-package-collection-generator | |
| LOG="../.tmp/collection.generate.log" | |
| swift run -c release package-collection-generate \ | |
| ../packages.json ../public/collection.json \ | |
| --auth-token github:github.com:${{ secrets.PACKAGE_COLLECTION_TOKEN }} \ | |
| 2>&1 | tee "$LOG" | |
| if grep -E -n "Failed to fetch additional metadata:|Failed to generate metadata|invalidAuthToken" "$LOG"; then | |
| echo "Generator reported metadata errors; failing." | |
| exit 1 | |
| fi | |
| EXPECTED=$(jq '.packages | length' ../packages.json) | |
| ACTUAL=$(jq '.packages | length' ../public/collection.json) | |
| if [ "$ACTUAL" -ne "$EXPECTED" ]; then | |
| echo "Package count mismatch: expected $EXPECTED, got $ACTUAL." | |
| exit 1 | |
| fi | |
| - name: Detect changes + bump revision | |
| id: detect | |
| env: | |
| FORCE_PUBLISH: ${{ inputs.force_publish }} | |
| run: | | |
| set -euo pipefail | |
| PREV=".tmp/collection.prev.json" | |
| NEXT="public/collection.json" | |
| # If manually forced, treat as changed so the pipeline continues. | |
| if [ "${FORCE_PUBLISH:-false}" = "true" ]; then | |
| echo "NO_CHANGES=false" >> "$GITHUB_ENV" | |
| echo "no_changes=false" >> "$GITHUB_OUTPUT" | |
| echo "Force publish enabled; continuing even if collection.json is identical." | |
| else | |
| if cmp -s "$PREV" "$NEXT"; then | |
| echo "NO_CHANGES=true" >> "$GITHUB_ENV" | |
| echo "no_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "No changes in collection.json" | |
| exit 0 | |
| fi | |
| echo "NO_CHANGES=false" >> "$GITHUB_ENV" | |
| echo "no_changes=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Always bump revision when publishing (real change or forced). | |
| REV=$(jq -r '.revision // 0' packages.json) | |
| NEW_REV=$((REV+1)) | |
| jq ".revision=$NEW_REV" packages.json > .tmp/packages.bumped.json | |
| mv .tmp/packages.bumped.json packages.json | |
| echo "NEW_REV=$NEW_REV" >> "$GITHUB_ENV" | |
| echo "Bumped revision: $REV -> $NEW_REV" | |
| - name: Regenerate collection.json with bumped revision | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| cd swift-package-collection-generator | |
| LOG="../.tmp/collection.regenerate.log" | |
| swift run -c release package-collection-generate \ | |
| ../packages.json ../public/collection.json \ | |
| --auth-token github:github.com:${{ secrets.PACKAGE_COLLECTION_TOKEN }} \ | |
| 2>&1 | tee "$LOG" | |
| if grep -E -n "Failed to fetch additional metadata:|Failed to generate metadata|invalidAuthToken" "$LOG"; then | |
| echo "Generator reported metadata errors; failing." | |
| exit 1 | |
| fi | |
| EXPECTED=$(jq '.packages | length' ../packages.json) | |
| ACTUAL=$(jq '.packages | length' ../public/collection.json) | |
| if [ "$ACTUAL" -ne "$EXPECTED" ]; then | |
| echo "Package count mismatch: expected $EXPECTED, got $ACTUAL." | |
| exit 1 | |
| fi | |
| - name: Generate CHANGELOG entry | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .tmp | |
| PREV=".tmp/collection.prev.json" | |
| NEXT="public/collection.json" | |
| # Extract "url version" pairs from both | |
| jq -r ' | |
| .packages? // [] | | |
| .[] as $p | | |
| ($p.repositoryURL // $p.url // "") as $u | | |
| ($p.versions? // [])[] | | |
| "\($u) \(.version)" | |
| ' "$PREV" | sort -u > .tmp/prev_pairs.txt || true | |
| jq -r ' | |
| .packages? // [] | | |
| .[] as $p | | |
| ($p.repositoryURL // $p.url // "") as $u | | |
| ($p.versions? // [])[] | | |
| "\($u) \(.version)" | |
| ' "$NEXT" | sort -u > .tmp/next_pairs.txt || true | |
| # Added/removed pairs | |
| comm -13 .tmp/prev_pairs.txt .tmp/next_pairs.txt > .tmp/added_pairs.txt || true | |
| comm -23 .tmp/prev_pairs.txt .tmp/next_pairs.txt > .tmp/removed_pairs.txt || true | |
| python3 - <<'PY' | |
| from collections import defaultdict | |
| from datetime import datetime, timezone | |
| import os | |
| def read_pairs(path): | |
| d = defaultdict(list) | |
| if not os.path.exists(path): | |
| return d | |
| with open(path, "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| url, ver = line.split(" ", 1) | |
| d[url].append(ver) | |
| for k in d: | |
| d[k] = sorted(set(d[k])) | |
| return d | |
| added = read_pairs(".tmp/added_pairs.txt") | |
| removed = read_pairs(".tmp/removed_pairs.txt") | |
| now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") | |
| src_repo = os.environ.get("SRC_REPO", "").strip() | |
| src_ver = os.environ.get("SRC_VER", "").strip() | |
| revision = os.environ.get("NEW_REV", "").strip() | |
| lines = [] | |
| title_bits = [now] | |
| if revision: | |
| title_bits.append(f"revision {revision}") | |
| header = " — ".join(title_bits) | |
| lines.append(f"## {header}") | |
| if src_repo or src_ver: | |
| lines.append(f"_Triggered by_ `{src_repo}` `{src_ver}`") | |
| lines.append("") | |
| def render_section(name, data): | |
| lines.append(f"### {name}") | |
| if not data: | |
| lines.append("- _(none)_") | |
| lines.append("") | |
| return | |
| for url in sorted(data.keys()): | |
| vers = ", ".join(f"`{v}`" for v in data[url]) | |
| lines.append(f"- **{url}**: {vers}") | |
| lines.append("") | |
| render_section("Added", added) | |
| render_section("Removed", removed) | |
| lines.append("---") | |
| lines.append("") | |
| with open(".tmp/CHANGELOG_ENTRY.md", "w", encoding="utf-8") as f: | |
| f.write("\n".join(lines)) | |
| PY | |
| env: | |
| SRC_REPO: ${{ inputs.source_repo }} | |
| SRC_VER: ${{ inputs.source_version }} | |
| NEW_REV: ${{ env.NEW_REV }} | |
| - name: Prepend CHANGELOG entry | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| touch CHANGELOG.md | |
| cat .tmp/CHANGELOG_ENTRY.md CHANGELOG.md > .tmp/CHANGELOG_NEW.md | |
| mv .tmp/CHANGELOG_NEW.md CHANGELOG.md | |
| - name: Commit revision bump + changelog | |
| if: env.NO_CHANGES != 'true' | |
| uses: iarekylew00t/verified-bot-commit@v2 | |
| with: | |
| message: > | |
| Update collection (rev ${{ env.NEW_REV }}) | |
| ${{ inputs.source_repo && format('— {0} {1}', inputs.source_repo, inputs.source_version) || '' }} | |
| files: | | |
| packages.json | |
| CHANGELOG.md | |
| - name: Sign collection.json | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| # Recreate key + full cert chain from secrets (DER certs) | |
| echo "${{ secrets.PACKAGE_COLLECTION_SIGNING_KEY }}" | base64 --decode > private-key.pem | |
| echo "${{ secrets.PACKAGE_COLLECTION_SIGNING_CERT }}" | base64 --decode > signing-cert.cer | |
| echo "${{ secrets.PACKAGE_COLLECTION_CA_INTERMEDIATE }}" | base64 --decode > intermediate.cer | |
| echo "${{ secrets.PACKAGE_COLLECTION_CA_ROOT }}" | base64 --decode > root.cer | |
| cd swift-package-collection-generator | |
| # Sign: input, output, key, then full chain (leaf -> WWDR G3 -> Apple Root CA) | |
| swift run -c release package-collection-sign \ | |
| ../public/collection.json \ | |
| ../public/collection.signed.json \ | |
| ../private-key.pem \ | |
| ../signing-cert.cer \ | |
| ../intermediate.cer \ | |
| ../root.cer | |
| # Replace unsigned with signed | |
| mv ../public/collection.signed.json ../public/collection.json | |
| # Clean up secrets from workspace | |
| rm ../private-key.pem ../signing-cert.cer ../intermediate.cer ../root.cer | |
| - name: Copy extra files for the site | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| if [ -f README.md ]; then | |
| cp README.md public/index.md | |
| fi | |
| if [ -f packages.png ]; then | |
| cp packages.png public/ | |
| fi | |
| # Publish changelog on Pages too | |
| if [ -f CHANGELOG.md ]; then | |
| cp CHANGELOG.md public/CHANGELOG.md | |
| fi | |
| - name: Generate Shields badges endpoints | |
| if: env.NO_CHANGES != 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p public/badges | |
| REV=$(jq -r '.revision // 0' packages.json) | |
| UPDATED=$(date -u +"%Y-%m-%d") | |
| cat > public/badges/revision.json <<EOF | |
| { | |
| "schemaVersion": 1, | |
| "label": "Revision", | |
| "message": "${REV}", | |
| "color": "blue" | |
| } | |
| EOF | |
| cat > public/badges/updated.json <<EOF | |
| { | |
| "schemaVersion": 1, | |
| "label": "Updated", | |
| "message": "${UPDATED}", | |
| "color": "blue" | |
| } | |
| EOF | |
| - name: Upload Pages artifact | |
| if: env.NO_CHANGES != 'true' | |
| uses: actions/upload-pages-artifact@v4 | |
| with: | |
| path: ./public | |
| name: github-pages | |
| - name: Cleanup git auth rewrite | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| git config --global --unset-all url."https://x-access-token:${{ secrets.PACKAGE_COLLECTION_TOKEN }}@github.com/".insteadOf || true | |
| deploy: | |
| if: github.ref == 'refs/heads/main' && needs.build.outputs.no_changes != 'true' | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 |