diff --git a/.github/workflows/gh-pages-deploy.yml b/.github/workflows/gh-pages-deploy.yml index 804fabb..57bd594 100644 --- a/.github/workflows/gh-pages-deploy.yml +++ b/.github/workflows/gh-pages-deploy.yml @@ -1,10 +1,14 @@ --- name: Deploy to GitHub Pages -on: +on: # yamllint disable-line rule:truthy push: branches: [main] +permissions: + contents: write + pages: write + jobs: deploy: name: Deploy to GitHub Pages diff --git a/.github/workflows/gh-pages-test-deploy.yml b/.github/workflows/gh-pages-test-deploy.yml deleted file mode 100644 index 145e681..0000000 --- a/.github/workflows/gh-pages-test-deploy.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: GitHub Pages test deployment - -on: - pull_request: - branches: [main] - -jobs: - test-deploy: - name: GitHub Pages test deployment - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Test build - run: | - npm ci - npm run build diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 23d3e5b..8bf9827 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -14,11 +14,14 @@ name: Lint Code Base ############################# # Start the job on all push # ############################# -on: +on: # yamllint disable-line rule:truthy push: pull_request: branches: [main] +permissions: + contents: read + ############### # Set the Job # ############### diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..8512e34 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,187 @@ +--- +name: Preview Deployment + +on: # yamllint disable-line rule:truthy + pull_request: + types: [opened, synchronize, reopened, closed] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + env: + PROJECT: ${{ github.event.repository.name }} + PR_BRANCH: pr-${{ github.event.pull_request.number }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + - name: Create Cloudflare Pages project if needed + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: >- + pages project create ${{ env.PROJECT }} + --production-branch=main + continue-on-error: true + + - name: Deploy to Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: >- + pages deploy build + --project-name=${{ env.PROJECT }} + --branch=${{ env.PR_BRANCH }} + + - name: Comment preview URL on PR + uses: actions/github-script@v7 + env: + DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }} + with: + script: | + const url = process.env.DEPLOY_URL; + const sha = (context.payload.pull_request?.head?.sha + || context.sha).substring(0, 7); + const now = new Date().toUTCString(); + const body = [ + '### Preview Deployment', + '', + 'This PR has been deployed for preview:', + '', + '| Site | URL | Commit |', + '|---|---|---|', + `| DevOps Training | ${url} | \`${sha}\` |`, + '', + `> Last updated: ${now}`, + '', + ].join('\n'); + + const comments = await github.paginate( + github.rest.issues.listComments, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + } + ); + const existing = comments.find(c => + c.user.type === 'Bot' && + c.body.includes('### Preview Deployment') + ); + + const params = { + owner: context.repo.owner, + repo: context.repo.repo, + }; + if (existing) { + await github.rest.issues.updateComment({ + ...params, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + ...params, + issue_number: context.issue.number, + body, + }); + } + + cleanup-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Delete preview deployments + uses: actions/github-script@v7 + env: + CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CF_ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PROJECT: ${{ github.event.repository.name }} + BRANCH: pr-${{ github.event.pull_request.number }} + with: + script: | + const acct = process.env.CF_ACCOUNT; + const proj = process.env.PROJECT; + const base = 'https://api.cloudflare.com/client/v4'; + const url = `${base}/accounts/${acct}` + + `/pages/projects/${proj}/deployments`; + const headers = { + 'Authorization': `Bearer ${process.env.CF_TOKEN}`, + 'Content-Type': 'application/json', + }; + + const deployments = []; + let page = 1; + + const maxPages = 50; + + while (page <= maxPages) { + const res = await fetch( + `${url}?page=${page}`, { headers } + ); + if (!res.ok) { + core.warning( + `Failed to list deployments: ${res.status}` + ); + break; + } + + const data = await res.json(); + const items = data.result || []; + deployments.push(...items); + + const info = data.result_info; + if (!info || page >= (info.total_pages || 0)) { + break; + } + page += 1; + } + + const matched = deployments.filter(d => + d.deployment_trigger?.metadata?.branch === + process.env.BRANCH + ); + + core.info( + `Found ${matched.length} deployment(s) ` + + `for ${process.env.BRANCH}` + ); + + for (const d of matched) { + const delUrl = `${url}/${d.id}?force=true`; + const res = await fetch(delUrl, { + method: 'DELETE', + headers, + }); + if (res.ok) { + core.info(`Deleted deployment ${d.id}`); + } else { + core.warning( + `Failed to delete ${d.id}: ${res.status}` + ); + } + } diff --git a/Dockerfile b/Dockerfile index be78611..45c4a28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,13 @@ FROM node:lts WORKDIR /app EXPOSE 3000 35729 -COPY ./ /app +COPY --chown=node:node ./ /app RUN yarn install \ && yarn cache clean +USER node + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/ || exit 1 + CMD ["yarn", "start"]