name: CI on: push: branches: [main] pull_request: workflow_dispatch: permissions: contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: # The Host half's boundaries are testable without DSH and without a network: # what a hand-written patch is allowed to ask for, and what a repository name # turns into once it becomes one argv element of a `gh` invocation. The live # half of the same file is opt-in (DSH_CICD_LIVE=1) because it needs an # authenticated gh and a specific repository state; a suite that fails for # reasons unrelated to the change trains everyone to ignore it. suites: name: Host checks (node ${{ matrix.node }}) runs-on: ubuntu-24.04 timeout-minutes: 10 strategy: fail-fast: false matrix: # 20 is the floor package.json declares, 24 is the runtime the desktop # app actually ships — the two ends of the supported range. node: ['20', '24'] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} - name: host checks run: node tests/host-checks.mjs # Separates "the functions are correct" from "the package activates": this # one calls apply() with a real webServer and talks to the mounted routes. - name: mount check run: node tests/mount-check.mjs # The only route that writes to a checkout: a scratch clone, a scratch bare # origin, and every refusal proven to leave the manifest byte-identical. - name: bump e2e run: node tests/bump-e2e.mjs # The browser half, rendered in a stub React: proves the row that offers 发布 # versus 【升版本并发布】 really renders that way, and that the reason a release # is blocked reaches the screen instead of staying in a log. - name: client render run: node tests/client-render.mjs # The Bilibili update notes, with a fake transport and a `gh` that cannot # exist: one comment per release is the property that matters, and a live # account cannot test it (the second run of a live test is the bug). - name: bilibili notes run: node tests/bilibili-check.mjs # 发布同台, with every outside action injected: the order (npm before the draft # goes public), the refusals, and what a Host restart in the middle of a build # leaves behind. A real release takes minutes, publishes what the whole world # sees, and cannot be taken back — so the pipeline is driven here instead. - name: release pipeline run: node tests/pipeline-check.mjs # The bundle half of CI: continuous build. It checks the manifest contract and # then builds the exact archive the release workflow would publish, so a broken # package fails on the push that introduced it rather than at tag time. bundle: name: Bundle contract (node ${{ matrix.node }}) runs-on: ubuntu-24.04 timeout-minutes: 10 strategy: fail-fast: false matrix: node: ['20', '24'] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} - name: bundle contract run: node scripts/verify-bundle.mjs package: name: Build the release package runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: '24' - name: verify the bundle contract run: node scripts/verify-bundle.mjs - name: build the package run: | set -euo pipefail NAME="$(node -p 'require("./package.json").name')" VERSION="$(node -p 'require("./package.json").version')" mkdir -p dist git archive --format=zip --prefix="${NAME}-${VERSION}/" -o "dist/${NAME}-${VERSION}.zip" "$GITHUB_SHA" # The installable artifact, built on every push so a change to `files` # that breaks the published package fails here and not at tag time. npm pack --pack-destination dist >/dev/null (cd dist && sha256sum "${NAME}-${VERSION}.zip" "${NAME}-${VERSION}.tgz" > SHA256SUMS.txt) ls -l dist - name: prove the archive is a complete package run: | set -euo pipefail NAME="$(node -p 'require("./package.json").name')" VERSION="$(node -p 'require("./package.json").version')" STAGE="$(mktemp -d)" unzip -q "dist/${NAME}-${VERSION}.zip" -d "$STAGE" node "$STAGE/${NAME}-${VERSION}/scripts/verify-bundle.mjs" "$STAGE/${NAME}-${VERSION}" # The checks travel with the archive, so a release can be exercised the # same way the push was. node "$STAGE/${NAME}-${VERSION}/tests/host-checks.mjs" - name: prove the tarball is self-contained run: | set -euo pipefail NAME="$(node -p 'require("./package.json").name')" VERSION="$(node -p 'require("./package.json").version')" STAGE="$(mktemp -d)" tar -xzf "dist/${NAME}-${VERSION}.tgz" -C "$STAGE" # `npm pack` carries only the `files` allow-list, so this is what proves # the allow-list is complete — including the script the panel tells the # user to run. test -f "$STAGE/package/scripts/configure.mjs" test -f "$STAGE/package/cordis.patch.yml" node "$STAGE/package/scripts/verify-bundle.mjs" "$STAGE/package" node "$STAGE/package/tests/host-checks.mjs" node "$STAGE/package/tests/mount-check.mjs" "$STAGE/package" node "$STAGE/package/tests/bilibili-check.mjs" "$STAGE/package" node "$STAGE/package/tests/pipeline-check.mjs" "$STAGE/package" - uses: actions/upload-artifact@v7 with: name: ${{ github.event.repository.name }}-build path: dist/* if-no-files-found: error retention-days: 30