name: Deploy on: push: branches: - main pull_request: workflow_dispatch: concurrency: group: ci-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: runs-on: selfhosted permissions: contents: read container: options: "-v tmp:/ci-share" outputs: dist_hash: ${{ steps.stage.outputs.dist_hash }} share_path: ${{ steps.stage.outputs.share_path }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Cache node_modules id: cache-node-modules uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: node_modules key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }} restore-keys: | node-modules-${{ runner.os }}- - name: Check Node run: | set -euo pipefail command -v node command -v npm node -v npm -v - name: Install if: steps.cache-node-modules.outputs.cache-hit != 'true' run: npm ci --prefer-offline --no-audit --no-fund - name: Lint id: lint continue-on-error: true run: npm run lint - name: Build id: build continue-on-error: true env: BASE_PATH: ${{ github.event_name == 'pull_request' && format('/pr-{0}/', github.event.pull_request.number) || '/' }} run: npm run build - name: Test id: test continue-on-error: true run: npm test - name: Check results run: | set -euo pipefail failed=0 if [ "${{ steps.lint.outcome }}" != "success" ]; then echo "Lint failed" >&2 failed=1 fi if [ "${{ github.event_name }}" = "pull_request" ] || [ "${{ github.event_name }}" = "push" ]; then if [ "${{ steps.build.outcome }}" != "success" ]; then echo "Build failed" >&2 failed=1 fi fi if [ "${{ steps.test.outcome }}" != "success" ]; then echo "Test failed" >&2 failed=1 fi exit "$failed" - name: Stage dist for deploy id: stage if: github.event_name == 'push' || github.event_name == 'pull_request' run: | set -euo pipefail share_path="/ci-share/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" rm -rf "$share_path" mkdir -p "$share_path/dist" cp -a "${GITHUB_WORKSPACE}/dist/." "$share_path/dist/" dist_hash="$(tar -C "$share_path/dist" -cf - . | sha256sum | awk '{print $1}')" echo "share_path=$share_path" >> "$GITHUB_OUTPUT" echo "dist_hash=$dist_hash" >> "$GITHUB_OUTPUT" echo "Staged dist at $share_path (sha256=$dist_hash)" deploy: needs: build if: github.event_name == 'push' || github.event_name == 'pull_request' runs-on: selfhosted permissions: contents: write pull-requests: write container: options: "-v tmp:/ci-share" steps: - name: Verify staged dist env: SHARE_PATH: ${{ needs.build.outputs.share_path }} EXPECTED_HASH: ${{ needs.build.outputs.dist_hash }} run: | set -euo pipefail if [ -z "${SHARE_PATH}" ] || [ -z "${EXPECTED_HASH}" ]; then echo "Missing share path or hash from build job" >&2 exit 1 fi if [ ! -d "${SHARE_PATH}/dist" ]; then echo "Staged dist not found at ${SHARE_PATH}/dist" >&2 exit 1 fi actual="$(tar -C "${SHARE_PATH}/dist" -cf - . | sha256sum | awk '{print $1}')" if [ "$actual" != "$EXPECTED_HASH" ]; then echo "Dist hash mismatch: expected ${EXPECTED_HASH}, got ${actual}" >&2 exit 1 fi echo "Verified dist hash ${actual}" - name: Publish dist branch env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} EVENT_NAME: ${{ github.event_name }} PR_NUMBER: ${{ github.event.pull_request.number }} BUILD_SRC: ${{ needs.build.outputs.share_path }}/dist run: | set -euo pipefail if [ -z "${GITHUB_TOKEN}" ]; then echo "GITHUB_TOKEN is required to push the dist branch" >&2 exit 1 fi server="${GITHUB_SERVER_URL:?}" server="${server#https://}" server="${server#http://}" remote="https://oauth2:${GITHUB_TOKEN}@${server}/${GITHUB_REPOSITORY}.git" build_src="${BUILD_SRC:?}" if [ "${EVENT_NAME}" = "pull_request" ]; then target_dir="pr-${PR_NUMBER:?}" else target_dir="" fi max_attempts=5 attempt=0 while [ "$attempt" -lt "$max_attempts" ]; do attempt=$((attempt + 1)) echo "Publish attempt ${attempt}/${max_attempts}" expected="$(git ls-remote "$remote" refs/heads/dist | awk '{print $1}')" workdir="$(mktemp -d)" if [ -n "${expected}" ]; then git clone --depth 1 --branch dist "$remote" "$workdir" fi if [ -n "${target_dir}" ]; then rm -rf "${workdir}/${target_dir}" mkdir -p "${workdir}/${target_dir}" cp -a "${build_src}/." "${workdir}/${target_dir}/" else # Replace site root but keep PR preview folders if [ -d "$workdir" ]; then find "$workdir" -mindepth 1 -maxdepth 1 \ ! -name 'pr-*' \ ! -name '.git' \ -exec rm -rf {} + fi mkdir -p "$workdir" cp -a "${build_src}/." "${workdir}/" fi rm -rf "${workdir}/.git" git -C "$workdir" init -b dist git -C "$workdir" config user.name "forgejo-actions[bot]" git -C "$workdir" config user.email "forgejo-actions[bot]@noreply.localhost" git -C "$workdir" add -A git -C "$workdir" commit -m "Deploy ${GITHUB_SHA}${target_dir:+ (${target_dir})}" set +e if [ -n "${expected}" ]; then git -C "$workdir" push --force-with-lease="refs/heads/dist:${expected}" "$remote" HEAD:dist else git -C "$workdir" push "$remote" HEAD:dist fi push_status=$? set -e rm -rf "$workdir" if [ "$push_status" -eq 0 ]; then echo "Published dist successfully" exit 0 fi echo "Push rejected (likely concurrent deploy); retrying..." >&2 sleep 1 done echo "Failed to publish dist after ${max_attempts} attempts" >&2 exit 1 - name: Ping update server env: UPDATE_SECRET: ${{ secrets.UPDATE_SECRET }} run: | set -euo pipefail if [ -z "${UPDATE_SECRET}" ]; then echo "UPDATE_SECRET secret is required" >&2 exit 1 fi git_repo="${GITHUB_SERVER_URL:?}/${GITHUB_REPOSITORY:?}.git" curl -fsS -X POST https://update.lschaefer.xyz/update-project \ --data-urlencode "secret=${UPDATE_SECRET}" \ --data-urlencode "git_repo=${git_repo}" \ --data-urlencode "output_directory=amalgolopoly" \ --data-urlencode "branch=dist" - name: Checkout comment script from main if: github.event_name == 'pull_request' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main sparse-checkout: .forgejo/scripts/comment-preview.mjs sparse-checkout-cone-mode: false - name: Comment preview link if: github.event_name == 'pull_request' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: node .forgejo/scripts/comment-preview.mjs - name: Cleanup share if: always() env: SHARE_PATH: ${{ needs.build.outputs.share_path }} run: | set -euo pipefail if [ -n "${SHARE_PATH:-}" ]; then rm -rf "${SHARE_PATH}" fi