diff --git a/.github/actions/resolve-token/action.yml b/.github/actions/resolve-token/action.yml deleted file mode 100644 index 377b48da..00000000 --- a/.github/actions/resolve-token/action.yml +++ /dev/null @@ -1,84 +0,0 @@ -# --------------------------------------------------------------------------- -# Composite action: resolve the cross-repo token in ONE place. -# -# This is the single switch point between auth models. -# -# OPTION A — GitHub App (production / live org) -# OPTION B — service user PAT (CURRENTLY ACTIVE: test repos + future org) -# -# The test setup (ASUWebPlatforms/webspark-composer-test) uses a PAT because a -# fresh test repo has no GitHub App installed. This also exercises the exact -# auth model the future org prefers. -# -# To switch back to the App: comment OPTION B, uncomment OPTION A, and point -# the `token` output value at steps.app-token.outputs.token. -# --------------------------------------------------------------------------- -name: 'Resolve cross-repo token' -description: >- - Mints the token used for cross-repo release uploads, repository_dispatch, - and committing packages.json. Switch between GitHub App and service-user PAT - here, in one place. - -inputs: - # --- Inputs for OPTION A (GitHub App) --- - app-id: - description: 'GitHub App ID (vars.MIRROR_APP_ID). Used by OPTION A only.' - required: false - app-private-key: - description: 'GitHub App private key (secrets.WEBSPARK_MIRROR_SECRET). OPTION A only.' - required: false - owner: - description: 'Org/owner the token should be scoped to. OPTION A only.' - required: false - repositories: - description: 'Comma-separated repo list the App token may access. OPTION A only.' - required: false - - # --- Inputs for OPTION B (service user PAT) --- - service-user-pat: - description: 'Service user fine-grained PAT (secrets.SERVICE_USER_PAT). OPTION B only.' - required: false - -outputs: - token: - description: 'The resolved token to use for gh / API / git operations.' - # ACTIVE: OPTION B (service user PAT). - value: ${{ steps.pat.outputs.token }} - # OPTION A value (use this line instead when on the GitHub App): - # value: ${{ steps.app-token.outputs.token }} - -runs: - using: 'composite' - steps: - # ===== OPTION A: GitHub App (PRODUCTION) ============================== - # Uncomment this step, comment OPTION B below, and switch the `token` - # output value above to steps.app-token.outputs.token. - # - # - id: app-token - # uses: actions/create-github-app-token@v1 - # with: - # app-id: ${{ inputs.app-id }} - # private-key: ${{ inputs.app-private-key }} - # owner: ${{ inputs.owner }} - # repositories: ${{ inputs.repositories }} - - # ===== OPTION B: service user PAT (CURRENTLY ACTIVE) ================= - # Prerequisites for OPTION B: - # - A machine/service user with WRITE on the publisher repo - # (webspark-composer-test) AND the target repo - # (composer-packages-test) -- directly or via a narrow team. - # - A fine-grained PAT scoped to ONLY those two repos with - # Contents: read/write (release assets + committing packages.json) - # plus the permission needed to send repository_dispatch. - # - PAT must be SSO-authorized if the org enforces SAML/SSO. - # - Store it as the secret SERVICE_USER_PAT on the publisher repo. - - id: pat - shell: bash - env: - SERVICE_USER_PAT: ${{ inputs.service-user-pat }} - run: | - if [ -z "${SERVICE_USER_PAT}" ]; then - echo "::error::SERVICE_USER_PAT is empty (set the secret on the repo)" >&2 - exit 1 - fi - echo "token=${SERVICE_USER_PAT}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/composer-packages-publish.yml b/.github/workflows/composer-packages-publish.yml index 2736eae7..9158415e 100644 --- a/.github/workflows/composer-packages-publish.yml +++ b/.github/workflows/composer-packages-publish.yml @@ -1,12 +1,24 @@ -name: 'Publish Composer Packages (TEST)' +name: 'Publish Composer Packages + Deploy Pages' -# TEST harness for the Satis-replacement pipeline. Runs on -# ASUWebPlatforms/webspark-composer-test (in place of webspark-mirror) and -# publishes to ASUWebPlatforms/composer-packages-test (in place of -# composer-packages). Auth uses a service-user PAT (secrets.SERVICE_USER_PAT). +# Single-repo Composer registry pipeline. Everything happens here in +# ASUWebPlatforms/webspark-composer-test: # -# To promote to production: change TARGET_REPO + the repo gate back to the -# live names and switch the resolve-token action to the GitHub App path. +# ensure-release -> create (once) the GitHub Release that holds this ref's +# zip assets, on THIS repo. +# publish (matrix)-> zip each package subdirectory and upload it as a Release +# asset, then verify it downloads + checksums. +# rebuild -> merge the new version records into the committed +# packages.json accumulator, commit it back to main, and +# deploy packages.json + index.html to GitHub Pages. +# +# This replaces the former two-repo design (publisher + separate registry repo +# wired together by a cross-repo token and repository_dispatch). Because the +# registry now lives in the same repo, the built-in GITHUB_TOKEN can do +# everything -- no PAT, no GitHub App, no resolve-token action. +# +# Note on loops: pushes made with GITHUB_TOKEN do NOT trigger new workflow +# runs, so the bot's packages.json commit cannot re-trigger this workflow. The +# "[skip ci]" marker on that commit is kept as belt-and-suspenders. on: push: @@ -18,15 +30,20 @@ on: - '[0-9]+.[0-9]+.[0-9]+' workflow_dispatch: -# Don't let two publishes for the same ref race each other. +# Serialize the whole pipeline so concurrent pushes never race on the release, +# the packages.json commit, or the Pages deploy. Queue them (do NOT cancel) -- +# cancelling mid-push/mid-deploy can drop an update or wedge the Pages backend. concurrency: - group: composer-packages-publish-${{ github.ref }} + group: composer-packages-publish cancel-in-progress: false env: - # Single place to flip between test and production targets. - PUBLISHER_REPO: ASUWebPlatforms/webspark-composer-test - TARGET_REPO: ASUWebPlatforms/composer-packages-test + # Single place to flip between test and production. For production this + # becomes the live repo (e.g. ASUWebPlatforms/webspark-mirror) and its Pages + # URL; nothing else in the workflow hard-codes the repo. + REPO: ASUWebPlatforms/webspark-composer-test + BASE_URL: https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download + PAGES_URL: https://asuwebplatforms.github.io/webspark-composer-test jobs: # --------------------------------------------------------------------------- @@ -35,17 +52,14 @@ jobs: ensure-release: if: github.repository == 'ASUWebPlatforms/webspark-composer-test' runs-on: ubuntu-latest + permissions: + contents: write # create the release on this repo outputs: tag: ${{ steps.compute.outputs.tag }} version: ${{ steps.compute.outputs.version }} steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/resolve-token - id: token - with: - service-user-pat: ${{ secrets.SERVICE_USER_PAT }} - - id: compute shell: bash run: | @@ -60,14 +74,14 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - - name: Ensure release exists on target repo + - name: Ensure release exists env: - GH_TOKEN: ${{ steps.token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.compute.outputs.tag }} run: | - if ! gh release view "$TAG" --repo "$TARGET_REPO" >/dev/null 2>&1; then + if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then gh release create "$TAG" \ - --repo "$TARGET_REPO" \ + --repo "$REPO" \ --title "$TAG" \ --notes "Composer package artifacts for $TAG" \ ${{ steps.compute.outputs.version == 'dev-main' && '--prerelease' || '' }} @@ -81,6 +95,8 @@ jobs: publish: needs: ensure-release runs-on: ubuntu-latest + permissions: + contents: write # upload assets to the release strategy: fail-fast: false matrix: @@ -94,11 +110,6 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/resolve-token - id: token - with: - service-user-pat: ${{ secrets.SERVICE_USER_PAT }} - - name: Package subdirectory env: TAG: ${{ needs.ensure-release.outputs.tag }} @@ -108,7 +119,7 @@ jobs: - name: Upload asset (retry up to 2x, then fail) env: - GH_TOKEN: ${{ steps.token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.ensure-release.outputs.tag }} run: | set -euo pipefail @@ -117,7 +128,7 @@ jobs: max=3 # 1 initial + 2 retries until [ "$attempt" -gt "$max" ]; do echo "Upload attempt $attempt/$max for $ZIP" - if gh release upload "$TAG" "$ZIP" --repo "$TARGET_REPO" --clobber; then + if gh release upload "$TAG" "$ZIP" --repo "$REPO" --clobber; then echo "Upload succeeded." break fi @@ -135,7 +146,7 @@ jobs: run: | set -euo pipefail META="$(ls dist/*.json)" - URL="https://github.com/${TARGET_REPO}/releases/download/${TAG}/$(jq -r '.filename' "$META")" + URL="https://github.com/${REPO}/releases/download/${TAG}/$(jq -r '.filename' "$META")" EXPECTED="$(jq -r '.shasum' "$META")" # -L follows the 302 to the signed objects URL. curl -fsSL "$URL" -o downloaded.zip @@ -146,7 +157,7 @@ jobs: fi echo "Verified $URL" - - name: Stash metadata record for dispatch + - name: Stash metadata record for merge uses: actions/upload-artifact@v4 with: name: meta-${{ strategy.job-index }} @@ -154,18 +165,41 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # 3) After ALL uploads succeed, fire ONE repository_dispatch with metadata. + # 3) Merge records into packages.json, commit, and deploy to Pages. # --------------------------------------------------------------------------- - dispatch-rebuild: + rebuild: needs: publish runs-on: ubuntu-latest + # Fail cleanly if a Pages deploy ever wedges, instead of hanging until a + # human cancels it. A healthy run finishes well under a minute. + timeout-minutes: 15 + permissions: + contents: write # commit the updated packages.json back to this repo + pages: write # deploy to Pages + id-token: write # required by actions/deploy-pages (OIDC) steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/resolve-token - id: token - with: - service-user-pat: ${{ secrets.SERVICE_USER_PAT }} + - name: Guard - packages.json is valid and well-shaped + run: | + set -euo pipefail + # Fail fast (and clearly) if the committed accumulator is missing, + # not valid JSON, or not the expected { "packages": { ... } } shape. + if [ ! -f packages.json ]; then + echo "::error file=packages.json::packages.json is missing from the repo root." >&2 + exit 1 + fi + if ! jq empty packages.json 2>/dev/null; then + echo "::error file=packages.json::packages.json is not valid JSON." >&2 + head -n 20 packages.json >&2 || true + exit 1 + fi + if [ "$(jq -r 'type' packages.json)" != "object" ] \ + || [ "$(jq -r '.packages | type' packages.json)" != "object" ]; then + echo "::error file=packages.json::packages.json must be an object with a top-level \"packages\" object." >&2 + exit 1 + fi + echo "packages.json OK ($(jq '.packages | length' packages.json) package(s))." - name: Collect all metadata records uses: actions/download-artifact@v4 @@ -174,27 +208,92 @@ jobs: path: meta merge-multiple: true - - name: Build dispatch payload - id: payload + - name: Build records.json run: | set -euo pipefail - # Combine every per-package record into a single JSON array. - jq -s '.' meta/*.json > records.json - echo "Records to dispatch:" + # Combine every per-package record into a single JSON array. If there + # are somehow no records (shouldn't happen after a successful publish + # matrix), fall back to an empty array so we just redeploy as-is. + if ls meta/*.json >/dev/null 2>&1; then + jq -s '.' meta/*.json > records.json + else + echo "No metadata records found; redeploying existing packages.json." + echo '[]' > records.json + fi jq . records.json - # Build the FULL request body with a properly nested client_payload. - # gh api -f/-F send flat form fields and will NOT construct nested - # objects (client_payload[records] would not become real nested JSON), - # so we assemble the body explicitly and send it via --input. - # client_payload has a documented ~64KB cap; 6 small records fit. - jq -n --argjson records "$(cat records.json)" \ - '{event_type: "packages-publish", client_payload: {records: $records}}' \ - > dispatch-body.json - jq . dispatch-body.json - - - name: Dispatch rebuild on target repo + + - name: Merge + commit + push (race-safe) env: - GH_TOKEN: ${{ steps.token.outputs.token }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + chmod +x scripts/merge-packages-json.sh + git config user.name "ws2-release-bot" + git config user.email "ws2-release-bot@asu.edu" + + attempt=1 + max=5 + until [ "$attempt" -gt "$max" ]; do + # Always re-apply the merge onto the freshest main. The merge is + # additive/idempotent, so re-applying after a remote update is + # always correct. + git fetch origin main + git reset --hard origin/main + + ./scripts/merge-packages-json.sh packages.json records.json "$BASE_URL" + + if git diff --quiet -- packages.json; then + echo "packages.json already up to date on origin/main; nothing to push." + break + fi + + git add packages.json + git commit -m "Update packages.json [skip ci]" + + if git push origin HEAD:main; then + echo "Pushed on attempt $attempt." + break + fi + + if [ "$attempt" -eq "$max" ]; then + echo "::error::failed to push packages.json after $max attempts (persistent contention)" >&2 + exit 1 + fi + echo "Push rejected (remote moved); retrying ($attempt/$max)..." + attempt=$((attempt+1)) + sleep $((attempt * 3)) + done + + - name: Prepare Pages artifact run: | set -euo pipefail - gh api --method POST "repos/${TARGET_REPO}/dispatches" --input dispatch-body.json + mkdir -p _site + cp packages.json _site/packages.json + # Optional human index, if present in the repo root. + [ -f index.html ] && cp index.html _site/index.html || true + + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + - id: deploy + uses: actions/deploy-pages@v4 + + - name: Verify deployed packages.json + run: | + set -euo pipefail + # Give Pages a moment to propagate the new deploy. + sleep 10 + curl -fsSL "${PAGES_URL}/packages.json" -o served.json + jq empty served.json + # If this run carried records, confirm each name->version is present. + if [ "$(jq 'length' records.json)" -gt 0 ]; then + while read -r name version; do + if [ "$(jq --arg n "$name" --arg v "$version" \ + '.packages[$n][$v] != null' served.json)" != "true" ]; then + echo "::error::served packages.json missing ${name}@${version}" >&2 + exit 1 + fi + done < <(jq -r '.[] | "\(.name) \(.version)"' records.json) + fi + echo "Verified ${PAGES_URL}/packages.json" diff --git a/.github/workflows/conventional-commit-pr.yml b/.github/workflows/conventional-commit-pr.yml deleted file mode 100644 index 62ea72eb..00000000 --- a/.github/workflows/conventional-commit-pr.yml +++ /dev/null @@ -1,229 +0,0 @@ -name: 'Conventional Commit PR' - -on: - pull_request: - types: - - opened - - edited - - synchronize - - reopened - # Run for pull requests targeting the main branch. - branches: - - main - workflow_dispatch: - -jobs: - conventional-commit-pr: - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: read - steps: - - uses: actions/checkout@v3 - - name: Conventional Commit In Pull Requests - uses: amannn/action-semantic-pull-request@v6 - id: pr-conventional-commits - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # Configure which types are allowed (newline-delimited). - # These are regex patterns auto-wrapped in `^ $`. - types: | - a11y - fix - feat - chore - # Configure which scopes are allowed (newline-delimited). - # These are regex patterns auto-wrapped in `^ $`. - scopes: | - webspark - partner - stack - # Configure that a scope must always be provided. - requireScope: true - - - name: Inject Jira Ticket into PR Title - id: jira-validation - if: always() && (steps.pr-conventional-commits.outputs.scope == 'webspark' || steps.pr-conventional-commits.outputs.scope == 'stack') - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - # 1. Extract the ticket - TICKET="$(printf '%s' "$PR_BODY" | grep -oP 'https://asudev\.jira\.com/browse/\K[A-Z0-9]+-[0-9]+' || true)" - echo "TICKET extracted: '$TICKET'" - - if [[ -z "$TICKET" ]]; then - echo "error_message=No Jira ticket found in the pull request body with the format: https://asudev.jira.com/browse/PROJECT-123" >> $GITHUB_OUTPUT - exit 1 - fi - - # 2. Check if ticket is already in title - if [[ "$PR_TITLE" == *"$TICKET"* ]]; then - echo "Ticket $TICKET is already in the title. Exiting." - exit 0 - fi - - # 3. Build the new title by inserting ticket after the scope - echo "Testing sed pattern..." - NEW_TITLE="$(printf '%s' "$PR_TITLE" | sed -E "s/^([a-z0-9]+)\(([a-z]+)\): (.*)$/\1(\2): [$TICKET] \3/")" - echo "Sed pattern result: '$NEW_TITLE'" - - # Debug: Test if the pattern matches - if printf '%s' "$PR_TITLE" | grep -qE "^[a-z0-9]+\([a-z]+\): "; then - echo "✓ Pattern matches!" - else - echo "✗ Pattern does NOT match" - echo " Expected format: type(scope): description" - echo " Got: $PR_TITLE" - fi - - # 4. Only update if the title actually changed - if [[ "$NEW_TITLE" != "$PR_TITLE" ]]; then - echo "Title changed, updating PR..." - gh pr edit "$PR_NUMBER" --title "$NEW_TITLE" - echo "PR title updated successfully" - else - echo "Sed pattern did not match the title format. No changes made." - fi - - - name: Comment PR title linting errors - uses: marocchino/sticky-pull-request-comment@v2 - if: always() && (steps.pr-conventional-commits.outputs.error_message != null || steps.jira-validation.outputs.error_message != null) - with: - header: pr-title-lint-error - message: | - ⚠️ Warning ⚠️ - We require pull request titles to follow a customized subset of the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary). - - Example: - ``` - feat(webspark): [JIRA-1234] add button component - ^ ^ ^ - | | |__ Subject with Jira ticket (required if the scope is "webspark" or "stack") - | |_______ Scope - |____________ Type - ``` - We use 4 `types`: - - a11y: a commit of the type a11y includes fixes that improve accessibility. - - fix: a commit of the type fix patches a bug in your codebase. - - feat: a commit of the type feat introduces a new feature to the codebase. - - chore: a commit of the type chore is used for anything else, such as updating documentation, refactoring code, or performing other maintenance tasks. - - We also require a `scope`, which relates to where the change is being made. - - webspark: changes made on the Webspark profile and associated modules/themes/etc. - - partner: changes made by Trusted Partners on their custom code. - - stack: changes made by the ET Web Team to the shared stack codebase. - - Please adjust your PR title as recommended below: - ``` - ${{ steps.pr-conventional-commits.outputs.error_message }}${{ steps.jira-validation.outputs.error_message }} - ``` - - - name: Delete resolved PR title lint error comment - if: always() && steps.pr-conventional-commits.outputs.error_message == null && steps.jira-validation.outputs.error_message == null - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pr-title-lint-error - delete: true - - - name: Sync Conventional Labels - if: github.event_action != 'closed' && steps.pr-conventional-commits.outputs.type != '' - uses: actions/github-script@v6 - with: - script: | - const type = '${{ steps.pr-conventional-commits.outputs.type }}'; - const prTitle = '${{ github.event.pull_request.title }}'; - const scope = '${{ steps.pr-conventional-commits.outputs.scope }}'; - - // Detect breaking change by looking for ! before the colon - // Pattern: type(scope)!: description - const isBreaking = /^[a-z]+\([a-z]+\)!:/.test(prTitle); - - console.log('Type:', type); - console.log('PR Title:', prTitle); - console.log('Is Breaking:', isBreaking); - - const typeMapping = { - 'feat': { name: 'feature', color: '0E8A16' }, // Green - 'fix': { name: 'bug fix', color: 'd876e3' }, // Pink - 'chore': { name: 'maintenance', color: 'FBCA04' }, // Yellow - 'a11y': { name: 'a11y', color: '0075CA' } // Blue - }; - const scopeMapping = { - 'webspark': { name: 'webspark', color: '8C1D40' }, // Maroon - 'partner': { name: 'trusted-partner', color: 'FF7F32' }, // Orange - 'stack': { name: 'stack', color: '008672' } // Aquamarine - }; - const breakingLabelName = 'breaking-change'; - - // 1. Get all existing labels in the repo - const repoLabels = await github.rest.issues.listLabelsForRepo({ - owner: context.repo.owner, - repo: context.repo.repo - }); - const existingLabelNames = repoLabels.data.map(l => l.name); - - // 2. Helper to create label if it doesn't exist - async function ensureLabelExists(name, color) { - if (!existingLabelNames.includes(name)) { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: name, - color: color - }); - } - } - - // 3. Ensure our target labels exist - if (typeMapping[type]) await ensureLabelExists(typeMapping[type].name, typeMapping[type].color); - if (scopeMapping[scope]) await ensureLabelExists(scopeMapping[scope].name, scopeMapping[scope].color); - await ensureLabelExists(breakingLabelName, 'D73A4A'); - - // 4. Update Labels on PR - const currentLabels = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - const currentLabelNames = currentLabels.data.map(l => l.name); - - let labelsToSet = [...currentLabelNames]; - - // Add new type label, remove old type labels if they changed - Object.values(typeMapping).forEach(config => { - if (config.name === typeMapping[type]?.name) { - if (!labelsToSet.includes(config.name)) labelsToSet.push(config.name); - } else { - labelsToSet = labelsToSet.filter(l => l !== config.name); - } - }); - - // Add scope label, remove old scope labels if they changed - Object.values(scopeMapping).forEach(config => { - if (config.name === scopeMapping[scope]?.name) { - if (!labelsToSet.includes(config.name)) labelsToSet.push(config.name); - } else { - labelsToSet = labelsToSet.filter(l => l !== config.name); - } - }); - - // Handle breaking-change toggle - if (isBreaking) { - if (!labelsToSet.includes(breakingLabelName)) labelsToSet.push(breakingLabelName); - } else { - labelsToSet = labelsToSet.filter(l => l !== breakingLabelName); - } - - console.log('Final labels:', labelsToSet); - - await github.rest.issues.setLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: labelsToSet - }); diff --git a/.github/workflows/mono-package-split.yml b/.github/workflows/mono-package-split.yml deleted file mode 100644 index 23bc29d1..00000000 --- a/.github/workflows/mono-package-split.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: 'Split ASU Packages' - -# Run action only on code deployments with a tag that matches the regex. -# This will make it so that this only runs when we create a release. -on: - push: - # Sync on pushes to the main branch OR tagged releases. - branches: - - main - tags: - # Sync on tags that include a version number with or without a leading 'v'. - - 'v[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+' - # Allow us to manually trigger the workflow - workflow_dispatch: - -jobs: - packages_split: - # Limit this workflow to a specific repo. This will ensure that - # it only runs on the webspark-mirror repo, and not any downstream - # repositories that will also include this file. - if: github.repository == 'asuwebplatforms/webspark-mirror' - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - # define package to repository map - package: - - local_path: docroot/profiles/contrib/webspark/modules/asu_brand - split_repository: asu_brand - - local_path: docroot/profiles/contrib/webspark/modules/asu_user - split_repository: asu_user - - local_path: docroot/profiles/contrib/webspark/modules/asu_react_core - split_repository: asu_react_core - - local_path: docroot/profiles/contrib/webspark/modules/asu_react_integration - split_repository: asu_react_integration - - local_path: docroot/profiles/contrib/webspark/modules/asu_config_utility - split_repository: asu_config_utility - - local_path: docroot/profiles/contrib/webspark - split_repository: webspark - - steps: - - uses: actions/checkout@v4 - # Utilize a Github App to generate a temporary token for access to the various repositories. - # This allows us to avoid hard coding a personal access token. - # Inspired by https://devopsjournal.io/blog/2022/01/03/GitHub-Tokens, - # but we are using a newer, better, official github action instead of - # the community contributed one used in the blog post. - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.MIRROR_APP_ID }} - private-key: ${{ secrets.WEBSPARK_MIRROR_SECRET }} - owner: ${{ github.repository_owner }} - # This app allows us to select all of the repositories we will be adding - # our access permissions to via the app-token. Just list them below - # in a comma separated string (without any spaces). - repositories: 'asu_user,asu_brand,asu_react_core,asu_react_integration,asu_config_utility,webspark' - - # The danharrin/monorepo-split-github-action below is the tool that we are using - # to sync the contents of the modules into their respective standalone, - # read-only github repositories. This may seem awkward, but it's because - # Packagist requires each repo to be standalone, unlike what we are able - # to do with JavaScript where the monorepo splits out via tagging and - # does not require "shadow" repositories. - # See https://blog.logrocket.com/hosting-all-your-php-packages-together-in-a-monorepo - - # no tag - - - if: "!startsWith(github.ref, 'refs/tags/')" - uses: danharrin/monorepo-split-github-action@v2.3.0 - env: - GITHUB_TOKEN: x-access-token:${{ steps.app-token.outputs.token }} - with: - package_directory: '${{ matrix.package.local_path }}' - repository_organization: ASUWebPlatforms - repository_name: '${{ matrix.package.split_repository }}' - user_name: ws2-release-bot - user_email: ws2-release-bot@asu.edu - - # with tag - - - if: "startsWith(github.ref, 'refs/tags/')" - uses: danharrin/monorepo-split-github-action@v2.3.0 - env: - GITHUB_TOKEN: x-access-token:${{ steps.app-token.outputs.token }} - with: - tag: ${GITHUB_REF#refs/tags/} - package_directory: '${{ matrix.package.local_path }}' - repository_organization: ASUWebPlatforms - repository_name: '${{ matrix.package.split_repository }}' - user_name: ws2-release-bot - user_email: ws2-release-bot@asu.edu diff --git a/README.md b/README.md index 197ca73f..2f0335c8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Sites running the Webspark build. [Local Development](#local-development) • [Code Quality Tools](#code-quality-tools) • +[Composer Package Registry](#composer-package-registry) • [Resources](#resources) @@ -109,6 +110,84 @@ Use the `--help` flag to see available options for each command. For example:

+## Composer Package Registry + +This repo doubles as a static [Composer repository](https://getcomposer.org/doc/05-repositories.md#composer) +for some of the Webspark packages it contains. Selected subdirectories are packaged as +zip artifacts, stored as GitHub Release assets on this repo, and advertised via +a `packages.json` served from GitHub Pages — a simple, [Satis](https://composer.github.io/satis) alternative +custom-built for ASU Webspark. + +Browse the published packages at +[https://asuwebplatforms.github.io/webspark-composer-test](https://asuwebplatforms.github.io/webspark-composer-test). + +### Consumer usage (test) + +Add the repository to your project's `composer.json`: + +```json +{ + "repositories": [ + { "type": "composer", "url": "https://asuwebplatforms.github.io/webspark-composer-test/" } + ] +} +``` + +Then require packages as normal: + +```bash +composer require asuwebplatforms/asu_brand +``` + +Notes for consumers: + +- The repository must remain **public** for unauthenticated `composer install`. +- Branch builds are published as the `dev-main` version + (`composer require asuwebplatforms/asu_brand:dev-main`). +- Release builds are published under the tag's version + (`composer require asuwebplatforms/asu_brand:0.0.5`). + +### How it works + +This all runs in **one repo** — there is no separate registry repo, and no +cross-repo token. The single workflow +`.github/workflows/composer-packages-publish.yml` does everything using the +built-in `GITHUB_TOKEN`: + +1. **`ensure-release`** — on a tag push (or `dev-main` for a branch build), + creates the GitHub Release that will hold this version's assets. +2. **`publish`** (matrix) — zips each selected package subdirectory with + `scripts/package.sh` and uploads it as a Release asset, then verifies the + asset downloads and its checksum matches. +3. **`rebuild`** — merges the new version records into the committed + `packages.json` accumulator with `scripts/merge-packages-json.sh`, commits + it back to `main` (marked `[skip ci]`), and deploys `packages.json` + + `index.html` to GitHub Pages. + +`packages.json` is an **append-only accumulator of record** — it is committed +to the repo and only ever has version entries added or replaced. Removing a +package from the publish matrix stops new versions being added; it does **not** +remove previously published entries. + +`scripts/package.sh` archives exactly the files **git tracks** (via +`git ls-files`), so `.gitignore` is honored precisely — including build +artifacts force-added under `node_modules`, which must ship with the package. + +### Registry files + +- `packages.json` — the accumulator of record (auto-generated; do not hand-edit). +- `index.html` — human-readable browse page served alongside `packages.json`. +- `scripts/package.sh` — zips a subdirectory + emits its metadata record. +- `scripts/merge-packages-json.sh` — merges new version records into `packages.json`. +- `.github/workflows/composer-packages-publish.yml` — the combined publish + Pages deploy pipeline. + +### Promoting to production + +The workflow's `REPO`, `BASE_URL`, and `PAGES_URL` env values are the single +flip-point: change them to the production repo (`webspark-mirror`) and its Pages +URL. Production will also be public, so the built-in `GITHUB_TOKEN` model +carries over unchanged — no PAT or GitHub App required. + ## Resources - [DDEV setup for Acquia](https://docs.google.com/document/d/1R-wFpJnxUmQJ35bbFEhtoXF4YZXkHe6ZVVy8oBUn9kQ/edit?usp=sharing) diff --git a/index.html b/index.html new file mode 100644 index 00000000..5672d66e --- /dev/null +++ b/index.html @@ -0,0 +1,215 @@ + + + + + + ASU Webspark Composer Packages + + + +
+

ASU Webspark Composer Packages

+

A static Composer repository served via GitHub Pages.

+
+ +
+
+

Add this repository

+

Add the following to your project's composer.json, then require packages as normal:

+
{
+    "repositories": [
+        { "type": "composer", "url": "https://asuwebplatforms.github.io/webspark-composer-test/" }
+    ]
+}
+

Require a package

+
composer require asuwebplatforms/asu_brand
+
+ +
+

Available packages

+

Loading packages…

+
+ + +
+ + + + diff --git a/packages.json b/packages.json new file mode 100644 index 00000000..64cfead5 --- /dev/null +++ b/packages.json @@ -0,0 +1,448 @@ +{ + "packages": { + "asuwebplatforms/asu_brand": { + "dev-main": { + "name": "asuwebplatforms/asu_brand", + "version": "dev-main", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14", + "asuwebplatforms/asu_react_integration": "^2.14", + "asuwebplatforms/asu_user": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/asu_brand-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "4deb0f3cd7d8d1e6491cbb63a020d4b15a4f1d8a" + } + }, + "0.0.4": { + "name": "asuwebplatforms/asu_brand", + "version": "0.0.4", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14", + "asuwebplatforms/asu_react_integration": "^2.14", + "asuwebplatforms/asu_user": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/asu_brand-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "a17e6ee724514126d1662adb69c723c5df88dbec" + } + }, + "0.0.5": { + "name": "asuwebplatforms/asu_brand", + "version": "0.0.5", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14", + "asuwebplatforms/asu_react_integration": "^2.14", + "asuwebplatforms/asu_user": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/asu_brand-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "c00bc1b8b8a179d11c81037fefcd94c502f61e1c" + } + } + }, + "asuwebplatforms/asu_config_utility": { + "dev-main": { + "name": "asuwebplatforms/asu_config_utility", + "version": "dev-main", + "type": "drupal-module", + "require": { + "drupal/config_update": "2.0.0-alpha3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/asu_config_utility-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "a711e282342881a678835acf9751a82dc9707772" + } + }, + "0.0.4": { + "name": "asuwebplatforms/asu_config_utility", + "version": "0.0.4", + "type": "drupal-module", + "require": { + "drupal/config_update": "2.0.0-alpha3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/asu_config_utility-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "e30e37fd53f200867afbbb5dd2e1f73dc9511534" + } + }, + "0.0.5": { + "name": "asuwebplatforms/asu_config_utility", + "version": "0.0.5", + "type": "drupal-module", + "require": { + "drupal/config_update": "2.0.0-alpha3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/asu_config_utility-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "aba816b504ab476c7c205f5248d1a314464e3282" + } + } + }, + "asuwebplatforms/asu_react_core": { + "dev-main": { + "name": "asuwebplatforms/asu_react_core", + "version": "dev-main", + "type": "drupal-module", + "require": { + "drupal/paragraphs": "^1.17.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/asu_react_core-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "0c4fb6930537be79f4c686a6427dcd24e701f2e8" + } + }, + "0.0.4": { + "name": "asuwebplatforms/asu_react_core", + "version": "0.0.4", + "type": "drupal-module", + "require": { + "drupal/paragraphs": "^1.17.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/asu_react_core-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "1cf83505d39801441de341a6ac89a9cb91303625" + } + }, + "0.0.5": { + "name": "asuwebplatforms/asu_react_core", + "version": "0.0.5", + "type": "drupal-module", + "require": { + "drupal/paragraphs": "^1.17.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/asu_react_core-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "9a3f9de72b785470602c372dc44ed59140f29cb5" + } + } + }, + "asuwebplatforms/asu_react_integration": { + "dev-main": { + "name": "asuwebplatforms/asu_react_integration", + "version": "dev-main", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/asu_react_integration-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "c63137fbcfb6721726e745490cd105bf9284ec16" + } + }, + "0.0.4": { + "name": "asuwebplatforms/asu_react_integration", + "version": "0.0.4", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/asu_react_integration-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "80711053296f68a3bc08a106915723f4b589dfe5" + } + }, + "0.0.5": { + "name": "asuwebplatforms/asu_react_integration", + "version": "0.0.5", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_react_core": "^2.14" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/asu_react_integration-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "d89789639036ab553508627bb28484f5d35812f5" + } + } + }, + "asuwebplatforms/asu_user": { + "dev-main": { + "name": "asuwebplatforms/asu_user", + "version": "dev-main", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_config_utility": "^2.14", + "drupal/cas": "^2.3.2" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/asu_user-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "984876e08ea610d5e114e88ca7af6c98d29a24f4" + } + }, + "0.0.4": { + "name": "asuwebplatforms/asu_user", + "version": "0.0.4", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_config_utility": "^2.14", + "drupal/cas": "^2.3.2" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/asu_user-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "9a29210836cc7f25ad44161712c285bf3468af05" + } + }, + "0.0.5": { + "name": "asuwebplatforms/asu_user", + "version": "0.0.5", + "type": "drupal-module", + "require": { + "asuwebplatforms/asu_config_utility": "^2.14", + "drupal/cas": "^2.3.2" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/asu_user-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "414480f8f02302420bb2685171f4041b3ce36d02" + } + } + }, + "asuwebplatforms/webspark": { + "dev-main": { + "name": "asuwebplatforms/webspark", + "version": "dev-main", + "type": "drupal-profile", + "require": { + "asuwebplatforms/asu_governance": "1.2.1", + "cweagans/composer-patches": "1.7.3", + "drupal/acquia_purge": "1.5.0", + "drupal/admin_toolbar": "3.6.1", + "drupal/allowed_formats": "3.0.1", + "drupal/anchor_link": "3.0.3", + "drupal/block_field": "1.0.0-rc4", + "drupal/captcha": "2.0.10", + "drupal/cas": "2.3.2", + "drupal/ckeditor": "1.0.2", + "drupal/ckeditor_responsive_plugin": "2.1.0-beta1", + "drupal/components": "3.2.0", + "drupal/config_readonly": "1.0.0-beta5", + "drupal/config_update": "2.0.0-alpha3", + "drupal/core-composer-scaffold": "10.5.11", + "drupal/core-recommended": "10.5.11", + "drupal/crop": "2.3.0", + "drupal/ctools": "4.1.0", + "drupal/current_page_crumb": "1.5.0", + "drupal/decorative_image_widget": "1.0.1", + "drupal/devel": "5.4.0", + "drupal/devel_kint_extras": "1.1.0", + "drupal/editor_advanced_link": "2.3.4", + "drupal/editoria11y": "2.2.18", + "drupal/environment_indicator": "4.0.14", + "drupal/field_group": "3.6.0", + "drupal/field_menu": "2.2.0", + "drupal/field_states_ui": "3.0.0", + "drupal/fontawesome": "3.0.0", + "drupal/image_widget_crop": "2.4.0", + "drupal/imagemagick": "4.0.2", + "drupal/layout_builder_component_attributes": "2.2.0", + "drupal/layout_builder_restrictions": "2.19.0", + "drupal/layout_builder_usage_reports": "1.0.2", + "drupal/layout_section_classes": "1.5.0", + "drupal/linkit": "7.0.15", + "drupal/maxlength": "2.1.2", + "drupal/media_library_form_element": "2.1.0", + "drupal/metatag": "2.2.0", + "drupal/paragraphs": "1.17.0", + "drupal/pathauto": "1.13.0", + "drupal/radix": "5.0.11", + "drupal/redirect": "1.9.0", + "drupal/robotstxt": "1.5.0", + "drupal/schema_metatag": "3.0.1", + "drupal/seckit": "2.0.3", + "drupal/select2": "1.15.0", + "drupal/simple_sitemap": "4.2.3", + "drupal/smtp": "1.2.0", + "drupal/webform": "6.2.3", + "drush/drush": "12.5.3", + "fontawesome/fontawesome": "*", + "northernco/ckeditor5-anchor-drupal": "*", + "npm-asset/fonticonpicker--fonticonpicker": "3.1.1", + "npm-asset/select2": "4.1.0-RC.0", + "oomphinc/composer-installers-extender": "2.0.1", + "php": ">=8.3.0", + "zaporylie/composer-drupal-optimizations": "1.2.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/dev-main/webspark-dev-main.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "403e88abee960178738831dc902707f592918d72" + } + }, + "0.0.4": { + "name": "asuwebplatforms/webspark", + "version": "0.0.4", + "type": "drupal-profile", + "require": { + "asuwebplatforms/asu_governance": "1.2.1", + "cweagans/composer-patches": "1.7.3", + "drupal/acquia_purge": "1.5.0", + "drupal/admin_toolbar": "3.6.1", + "drupal/allowed_formats": "3.0.1", + "drupal/anchor_link": "3.0.3", + "drupal/block_field": "1.0.0-rc4", + "drupal/captcha": "2.0.10", + "drupal/cas": "2.3.2", + "drupal/ckeditor": "1.0.2", + "drupal/ckeditor_responsive_plugin": "2.1.0-beta1", + "drupal/components": "3.2.0", + "drupal/config_readonly": "1.0.0-beta5", + "drupal/config_update": "2.0.0-alpha3", + "drupal/core-composer-scaffold": "10.5.11", + "drupal/core-recommended": "10.5.11", + "drupal/crop": "2.3.0", + "drupal/ctools": "4.1.0", + "drupal/current_page_crumb": "1.5.0", + "drupal/decorative_image_widget": "1.0.1", + "drupal/devel": "5.4.0", + "drupal/devel_kint_extras": "1.1.0", + "drupal/editor_advanced_link": "2.3.4", + "drupal/editoria11y": "2.2.18", + "drupal/environment_indicator": "4.0.14", + "drupal/field_group": "3.6.0", + "drupal/field_menu": "2.2.0", + "drupal/field_states_ui": "3.0.0", + "drupal/fontawesome": "3.0.0", + "drupal/image_widget_crop": "2.4.0", + "drupal/imagemagick": "4.0.2", + "drupal/layout_builder_component_attributes": "2.2.0", + "drupal/layout_builder_restrictions": "2.19.0", + "drupal/layout_builder_usage_reports": "1.0.2", + "drupal/layout_section_classes": "1.5.0", + "drupal/linkit": "7.0.15", + "drupal/maxlength": "2.1.2", + "drupal/media_library_form_element": "2.1.0", + "drupal/metatag": "2.2.0", + "drupal/paragraphs": "1.17.0", + "drupal/pathauto": "1.13.0", + "drupal/radix": "5.0.11", + "drupal/redirect": "1.9.0", + "drupal/robotstxt": "1.5.0", + "drupal/schema_metatag": "3.0.1", + "drupal/seckit": "2.0.3", + "drupal/select2": "1.15.0", + "drupal/simple_sitemap": "4.2.3", + "drupal/smtp": "1.2.0", + "drupal/webform": "6.2.3", + "drush/drush": "12.5.3", + "fontawesome/fontawesome": "*", + "northernco/ckeditor5-anchor-drupal": "*", + "npm-asset/fonticonpicker--fonticonpicker": "3.1.1", + "npm-asset/select2": "4.1.0-RC.0", + "oomphinc/composer-installers-extender": "2.0.1", + "php": ">=8.3.0", + "zaporylie/composer-drupal-optimizations": "1.2.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.4/webspark-0.0.4.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "df0bde83143a46bf3fad52962292adf3a4a508ca" + } + }, + "0.0.5": { + "name": "asuwebplatforms/webspark", + "version": "0.0.5", + "type": "drupal-profile", + "require": { + "asuwebplatforms/asu_governance": "1.2.1", + "cweagans/composer-patches": "1.7.3", + "drupal/acquia_purge": "1.5.0", + "drupal/admin_toolbar": "3.6.1", + "drupal/allowed_formats": "3.0.1", + "drupal/anchor_link": "3.0.3", + "drupal/block_field": "1.0.0-rc4", + "drupal/captcha": "2.0.10", + "drupal/cas": "2.3.2", + "drupal/ckeditor": "1.0.2", + "drupal/ckeditor_responsive_plugin": "2.1.0-beta1", + "drupal/components": "3.2.0", + "drupal/config_readonly": "1.0.0-beta5", + "drupal/config_update": "2.0.0-alpha3", + "drupal/core-composer-scaffold": "10.5.11", + "drupal/core-recommended": "10.5.11", + "drupal/crop": "2.3.0", + "drupal/ctools": "4.1.0", + "drupal/current_page_crumb": "1.5.0", + "drupal/decorative_image_widget": "1.0.1", + "drupal/devel": "5.4.0", + "drupal/devel_kint_extras": "1.1.0", + "drupal/editor_advanced_link": "2.3.4", + "drupal/editoria11y": "2.2.18", + "drupal/environment_indicator": "4.0.14", + "drupal/field_group": "3.6.0", + "drupal/field_menu": "2.2.0", + "drupal/field_states_ui": "3.0.0", + "drupal/fontawesome": "3.0.0", + "drupal/image_widget_crop": "2.4.0", + "drupal/imagemagick": "4.0.2", + "drupal/layout_builder_component_attributes": "2.2.0", + "drupal/layout_builder_restrictions": "2.19.0", + "drupal/layout_builder_usage_reports": "1.0.2", + "drupal/layout_section_classes": "1.5.0", + "drupal/linkit": "7.0.15", + "drupal/maxlength": "2.1.2", + "drupal/media_library_form_element": "2.1.0", + "drupal/metatag": "2.2.0", + "drupal/paragraphs": "1.17.0", + "drupal/pathauto": "1.13.0", + "drupal/radix": "5.0.11", + "drupal/redirect": "1.9.0", + "drupal/robotstxt": "1.5.0", + "drupal/schema_metatag": "3.0.1", + "drupal/seckit": "2.0.3", + "drupal/select2": "1.15.0", + "drupal/simple_sitemap": "4.2.3", + "drupal/smtp": "1.2.0", + "drupal/webform": "6.2.3", + "drush/drush": "12.5.3", + "fontawesome/fontawesome": "*", + "northernco/ckeditor5-anchor-drupal": "*", + "npm-asset/fonticonpicker--fonticonpicker": "3.1.1", + "npm-asset/select2": "4.1.0-RC.0", + "oomphinc/composer-installers-extender": "2.0.1", + "php": ">=8.3.0", + "zaporylie/composer-drupal-optimizations": "1.2.0" + }, + "dist": { + "type": "zip", + "url": "https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download/0.0.5/webspark-0.0.5.zip", + "reference": "33238d9a808724adc307ee9049ca91c848b97ee3", + "shasum": "a082ecf399244e3da54ba7563ed2ee1f5b0db634" + } + } + } + } +} diff --git a/scripts/merge-packages-json.sh b/scripts/merge-packages-json.sh new file mode 100755 index 00000000..e747d607 --- /dev/null +++ b/scripts/merge-packages-json.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# merge-packages-json.sh +# +# Merge one or more package metadata records into the committed packages.json +# accumulator. Existing versions are preserved; only the named name->version +# entries in the metadata are added or replaced. +# +# This is the only logic that builds packages.json. There is no manifest: +# every record is derived from a package's own composer.json at build time +# (see webspark-composer-test scripts/package.sh). +# +# Usage: +# merge-packages-json.sh +# +# Path to the committed accumulator (created if missing). +# JSON array of records produced by package.sh, e.g. +# [ { "name": "...", "version": "...", "type": "...", +# "require": {...}, "filename": "...", "tag": "...", +# "reference": "...", "shasum": "..." }, ... ] +# Release download base, e.g. +# https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download +# +set -euo pipefail + +PACKAGES_JSON="${1:?path to packages.json required}" +METADATA="${2:?path to metadata.json required}" +BASE_URL="${3:?release download base url required}" + +command -v jq >/dev/null 2>&1 || { echo "::error::jq is required" >&2; exit 1; } +[ -f "$METADATA" ] || { echo "::error::metadata file not found: $METADATA" >&2; exit 1; } + +# Ensure the accumulator exists and has the expected shape. +if [ ! -f "$PACKAGES_JSON" ]; then + echo '{"packages":{}}' > "$PACKAGES_JSON" +fi + +# Validate inputs are JSON before we touch anything. +jq empty "$PACKAGES_JSON" +jq empty "$METADATA" + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +jq \ + --arg base "$BASE_URL" \ + --slurpfile recs "$METADATA" \ + ' + # Start from the existing accumulator, fold each record in. + reduce $recs[0][] as $r (.; + .packages[$r.name][$r.version] = { + name: $r.name, + version: $r.version, + type: $r.type, + require: ($r.require // {}), + dist: { + type: "zip", + url: ($base + "/" + $r.tag + "/" + $r.filename), + reference: $r.reference, + shasum: $r.shasum + } + } + ) + ' "$PACKAGES_JSON" > "$tmp" + +# Final shape check, then atomically replace. +jq empty "$tmp" +mv "$tmp" "$PACKAGES_JSON" +trap - EXIT + +echo "Merged $(jq '.[] | .name' -r "$METADATA" | wc -l | tr -d ' ') record(s) into $PACKAGES_JSON"