From c10ddc6fa831cae8dbbdc19c6cfdca2785aa2574 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 16 Dec 2025 14:04:40 +0000 Subject: [PATCH 1/3] feat(workflow): add scheduled nightly publishing with per-package change detection - Add scheduled trigger at 1 AM UTC for automatic nightly builds - Implement per-package dist hash computation in version strings - Check each package's dist folder against published npm version - Skip publishing if no packages have changed (exits early in prepare-publish) - Version format: X.Y.Z-nightly-branch-YYYYMMDD-HHMMSS-githash-disthash - Each package gets its own 8-character SHA256 dist hash - lerna publish from-package only publishes packages with new versions --- .github/workflows/publish-libs.yml | 127 ++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index fc9e1eca8b..16fa4c23b6 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -6,6 +6,9 @@ on: push: tags: - "v**" + schedule: + # Run nightly at 1 AM UTC + - cron: '0 1 * * *' permissions: contents: read @@ -154,8 +157,78 @@ jobs: yarn install env: CI: true - - name: Bump version - if: ${{ github.event_name == 'workflow_dispatch' }} + - name: Build + run: | + cd packages + yarn build + env: + CI: true + - name: Check for changes and bump versions (scheduled builds only) + if: ${{ github.event_name == 'schedule' }} + run: | + cd packages + + # Check each package for changes + PACKAGES="blueprints-integration server-core-integration shared-lib live-status-gateway-api openapi" + HAS_ANY_CHANGES=0 + + echo "## Package Change Detection" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for PKG in $PACKAGES; do + # Get current package name + if [ "${{ env.IS_UPSTREAM }}" = "true" ]; then + PACKAGE_NAME="@sofie-automation/$PKG" + else + PACKAGE_NAME="@${{ env.NPM_PACKAGE_SCOPE }}/${{ env.NPM_PACKAGE_PREFIX }}$PKG" + fi + + # Get latest nightly version from npm + LATEST_NIGHTLY=$(npm view $PACKAGE_NAME dist-tags.nightly 2>/dev/null || echo "") + + if [ -z "$LATEST_NIGHTLY" ]; then + echo "📦 **$PKG**: No nightly found, will publish" >> $GITHUB_STEP_SUMMARY + HAS_ANY_CHANGES=1 + continue + fi + + # Extract dist hash from version (format: X.Y.Z-nightly-branch-YYYYMMDD-HHMMSS-githash-disthash) + LAST_DIST_HASH=$(echo "$LATEST_NIGHTLY" | grep -oP '[0-9a-f]{8}$' || echo "") + + if [ -z "$LAST_DIST_HASH" ]; then + echo "📦 **$PKG**: Old version format ($LATEST_NIGHTLY), will publish" >> $GITHUB_STEP_SUMMARY + HAS_ANY_CHANGES=1 + continue + fi + + # Compute current dist hash for this package + if [ -d "$PKG/dist" ]; then + CURRENT_DIST_HASH=$(find "$PKG/dist" -type f | sort | xargs cat | sha256sum | cut -c1-8) + else + echo "📦 **$PKG**: No dist folder, will publish" >> $GITHUB_STEP_SUMMARY + HAS_ANY_CHANGES=1 + continue + fi + + if [ "$LAST_DIST_HASH" = "$CURRENT_DIST_HASH" ]; then + echo "✅ **$PKG**: No changes ($CURRENT_DIST_HASH)" >> $GITHUB_STEP_SUMMARY + else + echo "📦 **$PKG**: Changed ($LAST_DIST_HASH → $CURRENT_DIST_HASH)" >> $GITHUB_STEP_SUMMARY + HAS_ANY_CHANGES=1 + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $HAS_ANY_CHANGES -eq 0 ]; then + echo "**Result**: No packages changed, skipping publish" >> $GITHUB_STEP_SUMMARY + exit 1 # Fail the step to stop the workflow + fi + + echo "**Result**: Will publish changed packages" >> $GITHUB_STEP_SUMMARY + env: + CI: true + - name: Bump version with per-package dist hashes + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} run: | cd packages COMMIT_TIMESTAMP=$(git log -1 --pretty=format:%ct HEAD) @@ -166,13 +239,47 @@ jobs: git config --global user.email "info@superfly.tv" git config --global user.name "superflytvab" - yarn set-version-and-commit prerelease --preid $PRERELEASE_TAG-$COMMIT_DATE-$GIT_HASH - env: - CI: true - - name: Build - run: | - cd packages - yarn build + # Update each package version with its own dist hash + for PKG_DIR in blueprints-integration server-core-integration shared-lib live-status-gateway-api openapi; do + if [ -d "$PKG_DIR/dist" ]; then + # Compute dist hash for this specific package + DIST_HASH=$(find "$PKG_DIR/dist" -type f | sort | xargs cat | sha256sum | cut -c1-8) + + # Get current version from package.json + CURRENT_VERSION=$(node -p "require('./$PKG_DIR/package.json').version") + + # Compute new prerelease version + NEW_VERSION=$(node -e " + const semver = require('semver'); + const current = '$CURRENT_VERSION'; + const parsed = semver.parse(current); + const newVersion = \`\${parsed.major}.\${parsed.minor}.\${parsed.patch}-$PRERELEASE_TAG-$COMMIT_DATE-$GIT_HASH-$DIST_HASH\`; + console.log(newVersion); + ") + + # Update package.json + node -e " + const fs = require('fs'); + const path = './$PKG_DIR/package.json'; + const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); + pkg.version = '$NEW_VERSION'; + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); + " + + echo "Updated $PKG_DIR to $NEW_VERSION" + fi + done + + # Also update lerna.json with a representative version (use blueprints-integration) + if [ -f "blueprints-integration/package.json" ]; then + LERNA_VERSION=$(node -p "require('./blueprints-integration/package.json').version") + node -e " + const fs = require('fs'); + const lerna = JSON.parse(fs.readFileSync('./lerna.json', 'utf8')); + lerna.version = '$LERNA_VERSION'; + fs.writeFileSync('./lerna.json', JSON.stringify(lerna, null, 2) + '\n'); + " + fi env: CI: true @@ -256,7 +363,7 @@ jobs: yarn install NPM_TAG=nightly - if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then + if [ "${{ github.event_name }}" != "workflow_dispatch" ] && [ "${{ github.event_name }}" != "schedule" ]; then PACKAGE_NAME=$(node -p "require('./shared-lib/package.json').name") PUBLISHED_VERSION=$(yarn npm info --json $PACKAGE_NAME | jq -c '.version' -r) THIS_VERSION=$(node -p "require('./lerna.json').version") From b6a70b61a955a8d8c551b519a7e531cbc640b1df Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 18 Dec 2025 15:07:52 +0000 Subject: [PATCH 2/3] Implement skipping when there are no changes without failing CI --- .github/workflows/publish-libs.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 16fa4c23b6..8bc3cc4771 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -141,6 +141,9 @@ jobs: if: ${{ needs.check-publish.outputs.can-publish == '1' }} + outputs: + should_publish: ${{ steps.detect.outputs.should_publish }} + steps: - uses: actions/checkout@v6 with: @@ -163,10 +166,16 @@ jobs: yarn build env: CI: true - - name: Check for changes and bump versions (scheduled builds only) - if: ${{ github.event_name == 'schedule' }} + - name: Decide whether to publish (scheduled builds may skip) + id: detect run: | cd packages + + # Default: publish for non-scheduled events + if [ "${{ github.event_name }}" != "schedule" ]; then + echo "should_publish=1" >> $GITHUB_OUTPUT + exit 0 + fi # Check each package for changes PACKAGES="blueprints-integration server-core-integration shared-lib live-status-gateway-api openapi" @@ -203,7 +212,7 @@ jobs: # Compute current dist hash for this package if [ -d "$PKG/dist" ]; then - CURRENT_DIST_HASH=$(find "$PKG/dist" -type f | sort | xargs cat | sha256sum | cut -c1-8) + CURRENT_DIST_HASH=$(find "$PKG/dist" -type f -print0 | sort -z | xargs -0 cat | sha256sum | cut -c1-8) else echo "📦 **$PKG**: No dist folder, will publish" >> $GITHUB_STEP_SUMMARY HAS_ANY_CHANGES=1 @@ -221,14 +230,16 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [ $HAS_ANY_CHANGES -eq 0 ]; then echo "**Result**: No packages changed, skipping publish" >> $GITHUB_STEP_SUMMARY - exit 1 # Fail the step to stop the workflow + echo "should_publish=0" >> $GITHUB_OUTPUT + exit 0 fi echo "**Result**: Will publish changed packages" >> $GITHUB_STEP_SUMMARY + echo "should_publish=1" >> $GITHUB_OUTPUT env: CI: true - name: Bump version with per-package dist hashes - if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} + if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && steps.detect.outputs.should_publish == '1' }} run: | cd packages COMMIT_TIMESTAMP=$(git log -1 --pretty=format:%ct HEAD) @@ -243,7 +254,7 @@ jobs: for PKG_DIR in blueprints-integration server-core-integration shared-lib live-status-gateway-api openapi; do if [ -d "$PKG_DIR/dist" ]; then # Compute dist hash for this specific package - DIST_HASH=$(find "$PKG_DIR/dist" -type f | sort | xargs cat | sha256sum | cut -c1-8) + DIST_HASH=$(find "$PKG_DIR/dist" -type f -print0 | sort -z | xargs -0 cat | sha256sum | cut -c1-8) # Get current version from package.json CURRENT_VERSION=$(node -p "require('./$PKG_DIR/package.json').version") @@ -284,12 +295,14 @@ jobs: CI: true - name: Build OpenAPI client library + if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} run: | cd packages/openapi yarn build env: CI: true - name: Modify dependencies to use npm packages + if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} run: | node scripts/prepublish.js "${{ github.repository }}" "${{ env.NPM_PACKAGE_SCOPE }}" ${{ env.NPM_PACKAGE_PREFIX }} @@ -297,6 +310,7 @@ jobs: yarn install --no-immutable - name: Upload release artifact + if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} uses: actions/upload-artifact@v5 with: name: publish-dist @@ -317,6 +331,8 @@ jobs: - prepare-publish - test-packages + if: ${{ github.event_name != 'schedule' || needs.prepare-publish.outputs.should_publish == '1' }} + permissions: contents: write id-token: write # scoped for as short as possible, as this gives write access to npm From 8a1f2d93e8d667687fc3de8fe02a70806c87b2ca Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 18 Dec 2025 15:22:39 +0000 Subject: [PATCH 3/3] Build both release52 and release53 nightly --- .../nightly-publish-release-branches.yml | 71 +++++++++++++++++++ .github/workflows/publish-libs.yml | 34 +++++---- 2 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/nightly-publish-release-branches.yml diff --git a/.github/workflows/nightly-publish-release-branches.yml b/.github/workflows/nightly-publish-release-branches.yml new file mode 100644 index 0000000000..d38f06a08d --- /dev/null +++ b/.github/workflows/nightly-publish-release-branches.yml @@ -0,0 +1,71 @@ +name: Nightly publish release branches + +on: + schedule: + # Run nightly at 1 AM UTC + - cron: '0 1 * * *' + workflow_dispatch: + +permissions: + contents: read + actions: write + +jobs: + dispatch: + name: Dispatch publish workflow + runs-on: ubuntu-latest + steps: + - name: Dispatch publish-libs on release branches + uses: actions/github-script@v7 + with: + script: | + const workflowId = 'publish-libs.yml' + const branches = ['release53', 'release52'] + + core.info(`Evaluating branches for nightly publish: ${branches.join(', ')}`) + + for (const ref of branches) { + // Get current HEAD SHA for the branch + const branchInfo = await github.rest.repos.getBranch({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: ref, + }) + const headSha = branchInfo.data?.commit?.sha + if (!headSha) { + core.warning(`Could not determine HEAD SHA for ${ref}, dispatching anyway`) + } + + // Find the latest publish-libs run on that branch (workflow_dispatch) + const runs = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + branch: ref, + event: 'workflow_dispatch', + per_page: 1, + }) + + const latest = runs.data?.workflow_runs?.[0] + const latestSha = latest?.head_sha + const latestConclusion = latest?.conclusion + + if (headSha && latestSha === headSha && latestConclusion === 'success') { + core.info(`Skipping ${ref}: HEAD ${headSha.substring(0, 7)} already published successfully in last run ${latest.id}`) + continue + } + + core.info( + `Dispatching ${ref}: HEAD=${headSha ? headSha.substring(0, 7) : 'unknown'} (last=${latestSha ? latestSha.substring(0, 7) : 'none'}, conclusion=${latestConclusion ?? 'none'})` + ) + + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: workflowId, + ref, + inputs: { + nightly_mode: 'true', + }, + }) + } diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 8bc3cc4771..52aa40446f 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -3,12 +3,18 @@ name: Publish libraries on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: + inputs: + nightly_mode: + description: 'Enable nightly change detection + skip publishing when no packages changed' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' push: tags: - "v**" - schedule: - # Run nightly at 1 AM UTC - - cron: '0 1 * * *' permissions: contents: read @@ -17,6 +23,7 @@ env: IS_UPSTREAM: ${{ github.repository_owner == 'Sofie-Automation' }} NPM_PACKAGE_SCOPE: ${{ vars.NPM_PACKAGE_SCOPE }} # In the form of nrkno, without the @ NPM_PACKAGE_PREFIX: ${{ vars.NPM_PACKAGE_PREFIX }} # Set to anything to prefix the published package names with "sofie-". eg in combination with NPM_PACKAGE_SCOPE this will turn @sofie-automation/shared-lib into @nrkno/sofie-shared-lib + IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.nightly_mode == 'true') }} jobs: check-publish: @@ -171,8 +178,8 @@ jobs: run: | cd packages - # Default: publish for non-scheduled events - if [ "${{ github.event_name }}" != "schedule" ]; then + # Default: publish for non-nightly events + if [ "${{ env.IS_NIGHTLY }}" != "true" ]; then echo "should_publish=1" >> $GITHUB_OUTPUT exit 0 fi @@ -214,9 +221,9 @@ jobs: if [ -d "$PKG/dist" ]; then CURRENT_DIST_HASH=$(find "$PKG/dist" -type f -print0 | sort -z | xargs -0 cat | sha256sum | cut -c1-8) else - echo "📦 **$PKG**: No dist folder, will publish" >> $GITHUB_STEP_SUMMARY - HAS_ANY_CHANGES=1 - continue + echo "❌ **$PKG**: No dist folder after build" >> $GITHUB_STEP_SUMMARY + echo "Build did not produce $PKG/dist; failing." >&2 + exit 1 fi if [ "$LAST_DIST_HASH" = "$CURRENT_DIST_HASH" ]; then @@ -278,6 +285,9 @@ jobs: " echo "Updated $PKG_DIR to $NEW_VERSION" + else + echo "Missing $PKG_DIR/dist after build; failing." >&2 + exit 1 fi done @@ -295,14 +305,14 @@ jobs: CI: true - name: Build OpenAPI client library - if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} + if: ${{ env.IS_NIGHTLY != 'true' || steps.detect.outputs.should_publish == '1' }} run: | cd packages/openapi yarn build env: CI: true - name: Modify dependencies to use npm packages - if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} + if: ${{ env.IS_NIGHTLY != 'true' || steps.detect.outputs.should_publish == '1' }} run: | node scripts/prepublish.js "${{ github.repository }}" "${{ env.NPM_PACKAGE_SCOPE }}" ${{ env.NPM_PACKAGE_PREFIX }} @@ -310,7 +320,7 @@ jobs: yarn install --no-immutable - name: Upload release artifact - if: ${{ github.event_name != 'schedule' || steps.detect.outputs.should_publish == '1' }} + if: ${{ env.IS_NIGHTLY != 'true' || steps.detect.outputs.should_publish == '1' }} uses: actions/upload-artifact@v5 with: name: publish-dist @@ -331,7 +341,7 @@ jobs: - prepare-publish - test-packages - if: ${{ github.event_name != 'schedule' || needs.prepare-publish.outputs.should_publish == '1' }} + if: ${{ env.IS_NIGHTLY != 'true' || needs.prepare-publish.outputs.should_publish == '1' }} permissions: contents: write