diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 1f6eeae..6ebeaed 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,13 +1,19 @@ name: Build & Publish Docker Image on: - # Automatic trigger when a release is published. - # NOTE: This only works when releases are created with a GitHub App token - # (not GITHUB_TOKEN). Until the App is configured, use workflow_dispatch. + # Automatic trigger when a release is published (works with GitHub App tokens). release: types: [published] - # Manual fallback for publishing when the release event doesn't fire. + # Automatic trigger after Release workflow completes (works with GITHUB_TOKEN). + # release-please creates releases using GITHUB_TOKEN which doesn't fire the + # release event above. workflow_run IS triggered by GITHUB_TOKEN completions. + workflow_run: + workflows: [Release] + types: [completed] + branches: [main] + + # Manual fallback. workflow_dispatch: inputs: version: @@ -31,6 +37,13 @@ jobs: build-and-push: runs-on: ubuntu-latest timeout-minutes: 20 + # For workflow_run: only run if the Release workflow succeeded AND a new + # release was actually created (check for a tag matching the latest release). + # For release/workflow_dispatch: always run. + if: | + github.event_name == 'release' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') steps: - name: Checkout @@ -38,24 +51,46 @@ jobs: - name: Determine version id: version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" - else - # Extract version from release tag (e.g. scrolly-v1.2.0 -> 1.2.0, v1.2.0 -> 1.2.0) + elif [ "${{ github.event_name }}" = "release" ]; then TAG="${{ github.event.release.tag_name }}" VERSION="${TAG#scrolly-v}" VERSION="${VERSION#v}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + else + # workflow_run trigger: look up the latest release + RELEASE=$(gh release view --json tagName --jq '.tagName' 2>/dev/null || echo "") + if [ -z "$RELEASE" ]; then + echo "No release found — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + VERSION="${RELEASE#scrolly-v}" + VERSION="${VERSION#v}" + # Check if this version is already published + EXISTING=$(gh api "/orgs/312-dev/packages/container/scrolly/versions" --jq ".[].metadata.container.tags[]" 2>/dev/null | grep -x "$VERSION" || true) + if [ -n "$EXISTING" ]; then + echo "Version $VERSION already published — skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" fi - name: Set up QEMU + if: steps.version.outputs.skip != 'true' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx + if: steps.version.outputs.skip != 'true' uses: docker/setup-buildx-action@v3 - name: Log in to GHCR + if: steps.version.outputs.skip != 'true' uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} @@ -63,6 +98,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata + if: steps.version.outputs.skip != 'true' id: meta uses: docker/metadata-action@v5 with: @@ -72,6 +108,7 @@ jobs: type=raw,value=latest - name: Build and push + if: steps.version.outputs.skip != 'true' uses: docker/build-push-action@v6 with: context: . @@ -85,6 +122,7 @@ jobs: cache-to: type=gha,mode=max - name: Set package visibility to public + if: steps.version.outputs.skip != 'true' run: | curl -sf -X PATCH \ -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ diff --git a/.github/workflows/release-pr-checks.yml b/.github/workflows/release-pr-checks.yml new file mode 100644 index 0000000..dfa85ca --- /dev/null +++ b/.github/workflows/release-pr-checks.yml @@ -0,0 +1,102 @@ +# Runs CI and CodeQL on release-please PRs. +# +# Why this exists: release-please creates PRs using GITHUB_TOKEN, which does +# NOT trigger other workflows (pull_request events). pull_request_target IS +# triggered by GITHUB_TOKEN events because it runs in the base branch context. +# Without this, release PRs have no status checks and can't be merged when +# branch protection requires them. +# +# Security: restricted to release-please branches only. The checkout uses the +# PR's HEAD SHA, which is safe because release-please PRs come from within the +# same repository (not forks) and only modify version/changelog files. + +name: Release PR Checks + +on: + pull_request_target: + branches: [main] + types: [opened, synchronize, reopened] + +permissions: read-all + +jobs: + # Gate: only run for release-please PRs + should-run: + if: startsWith(github.head_ref, 'release-please--') + runs-on: ubuntu-latest + steps: + - run: echo "Running CI for release-please PR" + + lint-and-check: + needs: [should-run] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint (ratcheted) + run: npm run lint:ci + + - name: Format check + run: npm run format:check + + - name: Type check + run: npm run type-check + + - name: Tests with coverage + run: npm run test:coverage + + - name: Production build + run: npm run build + + ci: + runs-on: ubuntu-latest + if: always() + needs: [lint-and-check] + steps: + - name: Check CI status + run: | + if [[ "${{ needs.lint-and-check.result }}" == "failure" || "${{ needs.lint-and-check.result }}" == "cancelled" ]]; then + echo "CI failed" + exit 1 + fi + echo "CI passed" + + codeql: + needs: [should-run] + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + security-events: write + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + config-file: .github/codeql/codeql-config.yml + queries: security-extended + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:javascript-typescript' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04fb3bc..d46746a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,20 +4,10 @@ on: push: branches: [main] -# TODO: Replace GITHUB_TOKEN with a GitHub App token to properly trigger -# CI/Security workflows on release-please PRs and docker-publish on releases. -# See: https://github.com/actions/create-github-app-token -# -# Once configured, add to this workflow: -# - name: Generate GitHub App token -# id: app-token -# uses: actions/create-github-app-token@v2 -# with: -# app-id: ${{ vars.RELEASE_APP_ID }} -# private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} -# -# Then pass `token: ${{ steps.app-token.outputs.token }}` to release-please. -# This eliminates all workarounds for GITHUB_TOKEN event limitations. +# Note: release-please uses GITHUB_TOKEN, which doesn't trigger other workflows. +# CI/CodeQL checks on release PRs are handled by release-pr-checks.yml using +# pull_request_target. Docker publishing is handled by docker-publish.yml using +# workflow_run on this workflow (to detect when a release is created). permissions: contents: write diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 90e3eab..4c81d93 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -57,9 +57,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 needs: [changes] + # Always run on PRs (branch protection requires CodeQL results even for + # non-code changes like workflow YAML). Skip only on push/schedule when + # no security-relevant files changed. if: | !inputs.skip_codeql && - (needs.changes.outputs.security_relevant == 'true' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + (github.event_name == 'pull_request' || needs.changes.outputs.security_relevant == 'true' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') permissions: security-events: write steps: