Skip to content

3.0.5

3.0.5 #9

Workflow file for this run

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