3.0.5 #9
Workflow file for this run
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: Release | |
| on: | |
| release: | |
| types: [ created ] | |
| jobs: | |
| build-and-unit: | |
| name: Build & Unit Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 25 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Run unit tests | |
| run: npm run test:min | |
| - name: Build (prod) | |
| run: npm run build:all:prod | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: build-output | |
| path: dist/ | |
| retention-days: 1 | |
| integration-tests: | |
| name: Integration (${{ matrix.browser }}, Shard ${{ matrix.shard }}/4) | |
| needs: build-and-unit | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| browser: [ chromium, firefox ] | |
| shard: [ 1, 2, 3, 4 ] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 25 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: build-output | |
| path: dist/ | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v5 | |
| id: playwright-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ matrix.browser }}-${{ hashFiles('package-lock.json') }} | |
| - name: Install Playwright browser | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: npx playwright install --with-deps ${{ matrix.browser }} | |
| - name: Install Playwright deps only | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: npx playwright install-deps ${{ matrix.browser }} | |
| - name: Run integration tests (${{ matrix.browser }}, shard ${{ matrix.shard }}/4) | |
| run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}/4 --reporter=list | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-results-${{ matrix.browser }}-shard-${{ matrix.shard }} | |
| path: test-results/ | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| publish: | |
| name: Publish to npm | |
| needs: [ build-and-unit, integration-tests ] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 25 | |
| registry-url: 'https://registry.npmjs.org' | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: build-output | |
| path: dist/ | |
| # Determine npm tag based on version | |
| - name: Determine npm tag | |
| id: npm_tag | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| if [[ "$VERSION" == *"-alpha"* ]]; then | |
| echo "tag=alpha" >> $GITHUB_OUTPUT | |
| elif [[ "$VERSION" == *"-beta"* ]]; then | |
| echo "tag=beta" >> $GITHUB_OUTPUT | |
| elif [[ "$VERSION" == *"-rc"* ]]; then | |
| echo "tag=rc" >> $GITHUB_OUTPUT | |
| else | |
| echo "tag=latest" >> $GITHUB_OUTPUT | |
| fi | |
| echo "Publishing version $VERSION with tag: $(grep tag= $GITHUB_OUTPUT | cut -d= -f2)" | |
| # Generate CycloneDX SBOM for the production dependency tree. | |
| # We invoke the locally-installed binary directly (rather than | |
| # `npx @cyclonedx/cyclonedx-npm`) so that a missing or moved | |
| # devDependency fails the build immediately instead of silently | |
| # fetching an unpinned version from the registry. Version is | |
| # pinned via the devDependency in package.json so Dependabot's | |
| # npm ecosystem keeps it up to date. The `overrides` block in | |
| # package.json replaces cyclonedx-npm's optional libxmljs2 | |
| # native dep with a no-op stub since we only emit JSON; this | |
| # avoids slow node-gyp builds and Node ABI/prebuild | |
| # compatibility issues on CI runners. | |
| - name: Generate CycloneDX SBOM | |
| run: | | |
| ./node_modules/.bin/cyclonedx-npm \ | |
| --omit dev \ | |
| --output-format JSON \ | |
| --output-file sbom.cdx.json \ | |
| --spec-version 1.6 | |
| echo "SBOM generated:" | |
| ls -la sbom.cdx.json | |
| # Pack the npm tarball that will be published. Building it | |
| # explicitly here lets us attest and publish the *same* bytes | |
| # rather than trusting two separate `npm publish` packs to | |
| # produce identical output. | |
| - name: Pack npm tarball | |
| id: pack | |
| run: | | |
| TARBALL=$(npm pack --silent) | |
| echo "tarball=$TARBALL" >> $GITHUB_OUTPUT | |
| ls -la "$TARBALL" | |
| # Attest build provenance for the npm tarball that will be | |
| # published. This is the canonical artifact; it includes | |
| # everything `npm pack` puts on disk (files matched by | |
| # package.json "files", plus npm's automatic inclusions like | |
| # package.json, README, and LICENSE). | |
| - name: Attest build provenance | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: ${{ steps.pack.outputs.tarball }} | |
| # Attest the SBOM against the same tarball, so consumers can | |
| # verify the SBOM corresponds to the exact npm artifact. | |
| - name: Attest SBOM | |
| uses: actions/attest-sbom@v4 | |
| with: | |
| subject-path: ${{ steps.pack.outputs.tarball }} | |
| sbom-path: 'sbom.cdx.json' | |
| # Attach the SBOM to the GitHub Release as a downloadable asset | |
| - name: Upload SBOM to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| cp sbom.cdx.json "scorm-again-${{ steps.npm_tag.outputs.version }}.cdx.json" | |
| gh release upload "${{ github.event.release.tag_name }}" \ | |
| "scorm-again-${{ steps.npm_tag.outputs.version }}.cdx.json" \ | |
| --clobber | |
| # Publish to npm using OIDC (no token needed). Runs last so | |
| # that the only irreversible side-effect happens after SBOM, | |
| # attestations, and release-asset upload have all succeeded. | |
| # Publish the pre-packed tarball so what's attested above is | |
| # exactly what's published. | |
| - name: Publish to npm | |
| run: | | |
| npm publish "${{ steps.pack.outputs.tarball }}" \ | |
| --tag ${{ steps.npm_tag.outputs.tag }} \ | |
| --provenance \ | |
| --access public | |
| # Check if release has manual notes | |
| - name: Check for manual release notes | |
| id: check_notes | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const body = context.payload.release.body || ''; | |
| const hasNotes = body.trim().length > 0; | |
| core.setOutput('has_manual_notes', hasNotes.toString()); | |
| console.log(`Release has manual notes: ${hasNotes}`); | |
| # Get previous release for changelog (only if no manual notes) | |
| - name: Get previous release tag | |
| id: prev_release | |
| if: steps.check_notes.outputs.has_manual_notes == 'false' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const releases = await github.rest.repos.listReleases({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 10 | |
| }); | |
| // Find current release index and get the next one (previous release) | |
| const currentTag = context.payload.release.tag_name; | |
| const currentIndex = releases.data.findIndex(r => r.tag_name === currentTag); | |
| if (currentIndex >= 0 && currentIndex < releases.data.length - 1) { | |
| const prevTag = releases.data[currentIndex + 1].tag_name; | |
| core.setOutput('tag', prevTag); | |
| console.log(`Previous release: ${prevTag}`); | |
| } else { | |
| core.setOutput('tag', ''); | |
| console.log('No previous release found'); | |
| } | |
| # Generate changelog from commits (only if no manual notes) | |
| - name: Generate changelog | |
| id: changelog | |
| if: steps.check_notes.outputs.has_manual_notes == 'false' | |
| run: | | |
| PREV_TAG="${{ steps.prev_release.outputs.tag }}" | |
| if [ -n "$PREV_TAG" ]; then | |
| COMMITS=$(git log ${PREV_TAG}..HEAD --oneline --no-merges | head -50) | |
| COMMIT_COUNT=$(git log ${PREV_TAG}..HEAD --oneline --no-merges | wc -l | tr -d ' ') | |
| else | |
| COMMITS=$(git log --oneline --no-merges -20) | |
| COMMIT_COUNT="20+" | |
| fi | |
| # Write to file to preserve newlines | |
| echo "## Changes" > changelog.md | |
| echo "" >> changelog.md | |
| echo "$COMMITS" | while read line; do | |
| echo "- $line" >> changelog.md | |
| done | |
| if [ "$COMMIT_COUNT" -gt 50 ] 2>/dev/null; then | |
| echo "" >> changelog.md | |
| echo "_...and $((COMMIT_COUNT - 50)) more commits_" >> changelog.md | |
| fi | |
| cat changelog.md | |
| # Update GitHub release with changelog (only if no manual notes) | |
| - name: Update GitHub release | |
| if: steps.check_notes.outputs.has_manual_notes == 'false' | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| // Read generated changelog | |
| if (fs.existsSync('./changelog.md')) { | |
| const changelog = fs.readFileSync('./changelog.md', 'utf8'); | |
| // Update the release with generated notes | |
| await github.rest.repos.updateRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: context.payload.release.id, | |
| body: changelog | |
| }); | |
| } | |
| # Calculate next version | |
| - name: Calculate next version | |
| id: next_version | |
| run: | | |
| VERSION="${{ steps.npm_tag.outputs.version }}" | |
| if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-z]+)\.([0-9]+)$ ]]; then | |
| # Pre-release version: 3.0.0-alpha.1 -> 3.0.0-alpha.2 | |
| MAJOR="${BASH_REMATCH[1]}" | |
| MINOR="${BASH_REMATCH[2]}" | |
| PATCH="${BASH_REMATCH[3]}" | |
| PRERELEASE="${BASH_REMATCH[4]}" | |
| PRENUM="${BASH_REMATCH[5]}" | |
| NEXT_VERSION="${MAJOR}.${MINOR}.${PATCH}-${PRERELEASE}.$((PRENUM + 1))" | |
| elif [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then | |
| # Stable version: 3.0.0 -> 3.0.1 | |
| MAJOR="${BASH_REMATCH[1]}" | |
| MINOR="${BASH_REMATCH[2]}" | |
| PATCH="${BASH_REMATCH[3]}" | |
| NEXT_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" | |
| else | |
| echo "Unable to parse version: $VERSION" | |
| exit 1 | |
| fi | |
| echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT | |
| echo "Bumping version from $VERSION to $NEXT_VERSION" | |
| # Update package.json with next version | |
| - name: Bump version | |
| run: | | |
| npm version ${{ steps.next_version.outputs.next_version }} --no-git-tag-version | |
| echo "Updated package.json to version ${{ steps.next_version.outputs.next_version }}" | |
| # Commit version bump to master | |
| - name: Commit version bump | |
| uses: stefanzweifel/git-auto-commit-action@v6 | |
| with: | |
| commit_message: "[skip ci] Bump version to ${{ steps.next_version.outputs.next_version }}" | |
| file_pattern: "package.json package-lock.json" | |
| branch: master |