diff --git a/.github/workflows/build-release-assets.yml b/.github/workflows/build-release-assets.yml deleted file mode 100644 index 1736b741..00000000 --- a/.github/workflows/build-release-assets.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: 📦 Build & Upload Release Assets - -# Builds Dashy and uploads a pre-built tarball to the GitHub release. -# This allows non-Docker installs (e.g. Proxmox VE community scripts) to -# download a ready-to-run package without having to build from source. -# -# The tarball contains the compiled frontend (dist/) plus all server-side -# files. Users extract it and run `yarn install --production` + `node server`. -# -# Triggered whenever a new release is created, or when manually dispatched - -on: - release: - types: [created] - workflow_dispatch: - inputs: - tag: - description: 'Tag to build assets for (must already exist as a release)' - required: true - -permissions: - contents: write - -concurrency: - group: ${{ github.workflow }}-${{ github.event.release.tag_name || github.event.inputs.tag }} - cancel-in-progress: true - -jobs: - build-release-assets: - name: Build app & upload tarball - runs-on: ubuntu-latest - env: - TAG: ${{ github.event.release.tag_name || github.event.inputs.tag }} - - steps: - - name: Checkout code 🛎️ - uses: actions/checkout@v6 - with: - ref: refs/tags/${{ env.TAG }} - - - name: Setup Node.js ⚙️ - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'yarn' - - - name: Install dependencies 📥 - run: yarn install --frozen-lockfile --ignore-engines --network-timeout 300000 - - - name: Build app 🏗️ - run: NODE_OPTIONS=--openssl-legacy-provider yarn build --mode production - - - name: Package release artifact 📦 - run: | - STAGING="dashy-release-staging" - mkdir -p "$STAGING" - - # Runtime files - cp -r dist "$STAGING/" - cp -r services "$STAGING/" - cp -r public "$STAGING/" - cp -r user-data "$STAGING/" - cp server.js "$STAGING/" - cp yarn.lock "$STAGING/" - - # src/utils/ files referenced directly by the server at runtime - mkdir -p "$STAGING/src/utils/config" - cp src/utils/config/ConfigSchema.json "$STAGING/src/utils/config/" - - # Strip devDependencies so `yarn install --production` stays lean - node -e " - const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf8')); - delete pkg.devDependencies; - require('fs').writeFileSync('$STAGING/package.json', JSON.stringify(pkg, null, 2)); - " - - TARBALL="dashy-${TAG}.tar.gz" - tar -czf "${TARBALL}" -C "${STAGING}" . - echo "TARBALL=${TARBALL}" >> "$GITHUB_ENV" - echo "Size: $(du -sh ${TARBALL} | cut -f1)" - - - name: Upload tarball to GitHub Release 🚀 - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ env.TAG }} - files: ${{ env.TARBALL }} - token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/draft-release.yml b/.github/workflows/draft-release.yml deleted file mode 100644 index beaf1c37..00000000 --- a/.github/workflows/draft-release.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: 🏗️ Draft New Release - -on: - push: - tags: - - '*.*.*' - workflow_dispatch: - inputs: - tag: - description: 'Tag to draft a release for (must already exist)' - required: true - -permissions: - contents: write - -jobs: - create-draft-release: - runs-on: ubuntu-latest - env: - TAG: ${{ github.event.inputs.tag || github.ref_name }} - steps: - - name: Checkout code 🛎️ - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check if major or minor version changed 🔍 - id: version_check - env: - CURRENT_TAG: ${{ github.event.inputs.tag || github.ref_name }} - run: | - git fetch --tags --force - CURRENT_MM=$(echo "$CURRENT_TAG" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/') - - # Find the immediately previous tag (to detect patch-only bumps) - PREVIOUS_TAG=$(git tag --sort=-version:refname \ - | grep -v "^${CURRENT_TAG}$" | head -1) - - if [ -z "$PREVIOUS_TAG" ]; then - echo "No previous tag found, creating release" - echo "should_release=true" >> $GITHUB_OUTPUT - echo "previous_tag=" >> $GITHUB_OUTPUT - exit 0 - fi - - PREVIOUS_MM=$(echo "$PREVIOUS_TAG" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/') - if [ "$CURRENT_MM" = "$PREVIOUS_MM" ]; then - echo "Patch-only bump ($PREVIOUS_TAG -> $CURRENT_TAG), skipping" - echo "should_release=false" >> $GITHUB_OUTPUT - echo "previous_tag=" >> $GITHUB_OUTPUT - exit 0 - fi - - # Minor/major bump — find the last tag from the previous release - PREV_RELEASE_TAG=$(git tag --sort=-version:refname | while read -r t; do - [ "$t" = "$CURRENT_TAG" ] && continue - t_mm=$(echo "$t" | sed 's/^v//; s/\([0-9]*\.[0-9]*\)\..*/\1/') - if [ "$t_mm" != "$CURRENT_MM" ]; then echo "$t"; break; fi - done) - echo "Minor/major bump, comparing against ${PREV_RELEASE_TAG:-$PREVIOUS_TAG}" - echo "should_release=true" >> $GITHUB_OUTPUT - echo "previous_tag=${PREV_RELEASE_TAG:-$PREVIOUS_TAG}" >> $GITHUB_OUTPUT - - - name: Create draft release 📝 - if: steps.version_check.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch' - id: create_release - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ env.TAG }} - name: Release ${{ env.TAG }} - draft: true - prerelease: false - generate_release_notes: true - previous_tag: ${{ steps.version_check.outputs.previous_tag }} - token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - - - name: Job summary 📋 - if: always() - env: - REPO_URL: ${{ github.server_url }}/${{ github.repository }} - SHOULD_RELEASE: ${{ steps.version_check.outputs.should_release }} - RELEASE_URL: ${{ steps.create_release.outputs.url }} - PREV_TAG: ${{ steps.version_check.outputs.previous_tag }} - run: | - { - echo "## 🏗️ Draft Release" - echo "" - echo "| Step | Result |" - echo "|------|--------|" - echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |" - - if [ -n "$PREV_TAG" ]; then - echo "| Compared against | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |" - fi - - if [ -n "$RELEASE_URL" ]; then - echo "| Draft release | ✅ [Review and publish](${RELEASE_URL}) |" - elif [ "$SHOULD_RELEASE" = "false" ]; then - echo "| Draft release | ⏭️ Skipped (patch-only bump) |" - else - echo "| Draft release | ❌ Failed |" - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5b1c3f70 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,170 @@ +# Builds Dashy and drafts a GitHub release with the compiled tarball, +# along with SHA256 checksum and SLSA build-provenance attestation +# +# Triggered by: +# - Push of any major/minor (X.Y.0) git tag +# - Manual dispatch with any existing tag (any version) + +name: 🚀 Build & Release + +on: + push: + tags: ['*.*.0'] + workflow_dispatch: + inputs: + tag: + description: 'Existing git tag to release (e.g. 4.2.0)' + required: true + +concurrency: + group: ${{ github.workflow }}-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +jobs: + release: + name: 🚀 Build & Draft Release + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write + id-token: write + attestations: write + env: + TAG: ${{ inputs.tag || github.ref_name }} + steps: + - name: 🛎️ Checkout tag + uses: actions/checkout@v6 + with: + ref: refs/tags/${{ env.TAG }} + fetch-depth: 0 + + - name: 🔧 Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'yarn' + + - name: 📥 Install dependencies + run: yarn install --frozen-lockfile --ignore-engines --network-timeout 300000 + + - name: 🏗️ Build app + run: NODE_OPTIONS=--openssl-legacy-provider yarn build --mode production + + - name: 📦 Package release tarball + id: package + run: | + set -euo pipefail + STAGING="dashy-release-staging" + mkdir -p "$STAGING" + cp -r dist "$STAGING/" + cp -r services "$STAGING/" + cp -r public "$STAGING/" + cp -r user-data "$STAGING/" + cp server.js "$STAGING/" + cp yarn.lock "$STAGING/" + mkdir -p "$STAGING/src/utils/config" + cp src/utils/config/ConfigSchema.json "$STAGING/src/utils/config/" + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + delete pkg.devDependencies; + fs.writeFileSync('$STAGING/package.json', JSON.stringify(pkg, null, 2)); + " + TARBALL="dashy-${TAG}.tar.gz" + tar -czf "$TARBALL" -C "$STAGING" . + echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT" + echo "size=$(du -h "$TARBALL" | cut -f1)" >> "$GITHUB_OUTPUT" + + - name: 🔢 Generate SHA256 checksum + id: checksum + env: + TARBALL: ${{ steps.package.outputs.tarball }} + run: | + set -euo pipefail + CHECKSUM="${TARBALL}.sha256" + sha256sum "$TARBALL" > "$CHECKSUM" + echo "file=$CHECKSUM" >> "$GITHUB_OUTPUT" + + - name: 🪪 Generate build provenance attestation + id: attest + uses: actions/attest-build-provenance@v4 + with: + subject-path: ${{ steps.package.outputs.tarball }} + + - name: 📤 Rename attestation bundle + id: attest_asset + env: + TARBALL: ${{ steps.package.outputs.tarball }} + BUNDLE: ${{ steps.attest.outputs.bundle-path }} + run: | + set -euo pipefail + OUT="${TARBALL}.intoto.jsonl" + cp "$BUNDLE" "$OUT" + echo "file=$OUT" >> "$GITHUB_OUTPUT" + + - name: 🔎 Find previous release tag + id: prev + env: + CURRENT_TAG: ${{ env.TAG }} + run: | + set -euo pipefail + git fetch --tags --force + PREV=$({ echo "$CURRENT_TAG"; git tag | grep -E '^[0-9]+\.[0-9]+\.0$'; } \ + | sort -uV \ + | awk -v cur="$CURRENT_TAG" '$0 == cur { print prev; exit } { prev = $0 }') + echo "tag=$PREV" >> "$GITHUB_OUTPUT" + + - name: 📝 Create draft release + id: release + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ env.TAG }} + name: Release ${{ env.TAG }} + draft: true + prerelease: false + generate_release_notes: true + previous_tag: ${{ steps.prev.outputs.tag }} + fail_on_unmatched_files: true + files: | + ${{ steps.package.outputs.tarball }} + ${{ steps.checksum.outputs.file }} + ${{ steps.attest_asset.outputs.file }} + token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + + - name: 📋 Job summary + if: always() + env: + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + PREV_TAG: ${{ steps.prev.outputs.tag }} + RELEASE_URL: ${{ steps.release.outputs.url }} + TARBALL: ${{ steps.package.outputs.tarball }} + SIZE: ${{ steps.package.outputs.size }} + ATTEST_URL: ${{ steps.attest.outputs.attestation-url }} + run: | + set -euo pipefail + { + echo "## 🚀 Release Draft" + echo "" + echo "| Item | Value |" + echo "|------|-------|" + echo "| Tag | [\`${TAG}\`](${REPO_URL}/releases/tag/${TAG}) |" + if [ -n "$PREV_TAG" ]; then + echo "| Notes since | [\`${PREV_TAG}\`](${REPO_URL}/releases/tag/${PREV_TAG}) |" + fi + if [ -n "$TARBALL" ]; then + echo "| Tarball | \`${TARBALL}\` (${SIZE:-?}) |" + fi + if [ -n "$ATTEST_URL" ]; then + echo "| Attestation | ✅ [View](${ATTEST_URL}) |" + else + echo "| Attestation | ❌ Failed |" + fi + if [ -n "$RELEASE_URL" ]; then + echo "| Draft release | ✅ [Review and publish](${RELEASE_URL}) |" + else + echo "| Draft release | ❌ Failed |" + fi + } >> "$GITHUB_STEP_SUMMARY"