mirror of
https://github.com/yeasy/docker_practice.git
synced 2026-08-10 08:27:25 +00:00
fix(ci): harden publishing and validate examples
This commit is contained in:
@@ -6,46 +6,71 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check project rules
|
- name: Install locked Node dependencies
|
||||||
run: python3 check_project_rules.py
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Check release metadata
|
- name: Check project rules and metadata
|
||||||
run: npm test
|
run: |
|
||||||
|
python3 check_project_rules.py
|
||||||
|
npm test
|
||||||
|
|
||||||
- name: Install Chromium and CJK fonts
|
- name: Install Chromium, CJK fonts, and PDF inspection tools
|
||||||
id: setupchrome
|
id: setupchrome
|
||||||
uses: browser-actions/setup-chrome@v2
|
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
- name: Install CJK fonts
|
|
||||||
|
- name: Install CJK fonts and PDF inspection tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
|
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra poppler-utils
|
||||||
|
|
||||||
- name: Install mdpress 0.7.10
|
- name: Install mdPress 0.7.10
|
||||||
env:
|
env:
|
||||||
MDPRESS_VERSION: "0.7.10"
|
MDPRESS_VERSION: "0.7.10"
|
||||||
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
||||||
run: |
|
run: |
|
||||||
archive="/tmp/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
archive="$RUNNER_TEMP/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
||||||
echo "Installing mdpress ${MDPRESS_VERSION}"
|
curl -fsSL --retry 3 \
|
||||||
curl -fsSL "https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" -o "$archive"
|
"https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$archive"
|
||||||
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
||||||
tar xzf "$archive" -C /tmp mdpress
|
tar xzf "$archive" -C "$RUNNER_TEMP" mdpress
|
||||||
sudo mv /tmp/mdpress /usr/local/bin/
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
mdpress --version
|
install -m 0755 "$RUNNER_TEMP/mdpress" "$RUNNER_TEMP/bin/mdpress"
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Install Pandoc 3.5
|
||||||
|
env:
|
||||||
|
PANDOC_VERSION: "3.5"
|
||||||
|
PANDOC_SHA256: "4f41d817d262ef3d17953a3e6e0fefddb971aa4f2f121544a9a86db449d945e1"
|
||||||
|
run: |
|
||||||
|
package="$RUNNER_TEMP/pandoc-${PANDOC_VERSION}-1-amd64.deb"
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-amd64.deb" \
|
||||||
|
-o "$package"
|
||||||
|
echo "${PANDOC_SHA256} $package" | sha256sum -c -
|
||||||
|
sudo dpkg -i "$package"
|
||||||
|
pandoc --version | head -1
|
||||||
|
|
||||||
|
- name: Extract safe tag name
|
||||||
id: tag
|
id: tag
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||||
tag_name="${GITHUB_REF#refs/tags/}"
|
tag_name="${GITHUB_REF#refs/tags/}"
|
||||||
@@ -61,49 +86,69 @@ jobs:
|
|||||||
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
||||||
|
|
||||||
- name: Build PDF
|
- name: Build PDF
|
||||||
|
env:
|
||||||
|
SAFE_TAG_NAME: ${{ steps.tag.outputs.SAFE_TAG_NAME }}
|
||||||
working-directory: ${{ runner.temp }}/docker_practice-pdf-src
|
working-directory: ${{ runner.temp }}/docker_practice-pdf-src
|
||||||
run: mdpress build --format pdf --output "${GITHUB_WORKSPACE}/docker_practice-${{ steps.tag.outputs.SAFE_TAG_NAME || 'latest' }}.pdf"
|
run: |
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||||
- name: Create Release with PDF
|
mdpress build --format pdf \
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
--output "$GITHUB_WORKSPACE/dist/docker_practice-${SAFE_TAG_NAME}.pdf"
|
||||||
uses: softprops/action-gh-release@v3
|
|
||||||
with:
|
|
||||||
generate_release_notes: true
|
|
||||||
files: docker_practice-${{ steps.tag.outputs.SAFE_TAG_NAME }}.pdf
|
|
||||||
|
|
||||||
- name: Upload PDF as artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: docker_practice-pdf
|
|
||||||
path: "docker_practice-*.pdf"
|
|
||||||
|
|
||||||
- name: Build HTML reader
|
- name: Build HTML reader
|
||||||
id: htmlreader
|
|
||||||
continue-on-error: true
|
|
||||||
env:
|
env:
|
||||||
CHROME_BIN: ${{ steps.setupchrome.outputs.chrome-path }}
|
CHROME_BIN: ${{ steps.setupchrome.outputs.chrome-path }}
|
||||||
|
SAFE_TAG_NAME: ${{ steps.tag.outputs.SAFE_TAG_NAME }}
|
||||||
run: |
|
run: |
|
||||||
# recent pandoc (apt's is too old for --embed-resources); + mermaid-cli using system Chrome
|
title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])')
|
||||||
curl -fsSL https://github.com/jgm/pandoc/releases/download/3.5/pandoc-3.5-1-amd64.deb -o /tmp/pandoc.deb
|
PATH="$GITHUB_WORKSPACE/node_modules/.bin:$PATH" \
|
||||||
sudo dpkg -i /tmp/pandoc.deb && pandoc --version | head -1
|
python3 tools/render_mermaid.py --book-dir . --svg-out "$RUNNER_TEMP/mmsvg"
|
||||||
PUPPETEER_SKIP_DOWNLOAD=true npm install -g @mermaid-js/mermaid-cli@10
|
python3 tools/build_html_reader.py \
|
||||||
TITLE=$(python3 -c "import json,os;print((json.load(open('book.json')).get('title') if os.path.exists('book.json') else '') or '${{ github.event.repository.name }}')")
|
--book-dir . \
|
||||||
TAG="${{ steps.tag.outputs.SAFE_TAG_NAME || 'latest' }}"
|
--title "$title" \
|
||||||
python3 tools/render_mermaid.py --book-dir . --svg-out /tmp/mmsvg
|
--svg-dir "$RUNNER_TEMP/mmsvg" \
|
||||||
python3 tools/build_html_reader.py --book-dir . --title "$TITLE" --svg-dir /tmp/mmsvg \
|
--out "dist/docker_practice-${SAFE_TAG_NAME}.html"
|
||||||
--out "${{ github.event.repository.name }}-${TAG}.html"
|
|
||||||
ls -lh ${{ github.event.repository.name }}-${TAG}.html
|
|
||||||
|
|
||||||
- name: Attach HTML to release
|
- name: Verify release artifacts
|
||||||
if: steps.htmlreader.outcome == 'success' && startsWith(github.ref, 'refs/tags/')
|
env:
|
||||||
uses: softprops/action-gh-release@v3
|
SAFE_TAG_NAME: ${{ steps.tag.outputs.SAFE_TAG_NAME }}
|
||||||
with:
|
run: |
|
||||||
tag_name: ${{ steps.tag.outputs.TAG_NAME }}
|
title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])')
|
||||||
files: "${{ github.event.repository.name }}-${{ steps.tag.outputs.SAFE_TAG_NAME }}.html"
|
python3 tools/verify_artifacts.py \
|
||||||
|
--title "$title" \
|
||||||
|
--pdf "dist/docker_practice-${SAFE_TAG_NAME}.pdf" \
|
||||||
|
--html "dist/docker_practice-${SAFE_TAG_NAME}.html" \
|
||||||
|
--checksums dist/SHA256SUMS
|
||||||
|
(cd dist && sha256sum -c SHA256SUMS)
|
||||||
|
|
||||||
- name: Upload HTML as artifact
|
- name: Upload verified release bundle
|
||||||
if: steps.htmlreader.outcome == 'success'
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
with:
|
||||||
name: html-edition
|
name: docker-practice-release-bundle
|
||||||
path: "${{ github.event.repository.name }}-*.html"
|
path: dist/
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download verified release bundle
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: docker-practice-release-bundle
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Recheck artifact checksums
|
||||||
|
run: (cd dist && sha256sum -c SHA256SUMS)
|
||||||
|
|
||||||
|
- name: Create GitHub release
|
||||||
|
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||||
|
with:
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
files: |
|
||||||
|
dist/docker_practice-*.pdf
|
||||||
|
dist/docker_practice-*.html
|
||||||
|
dist/SHA256SUMS
|
||||||
|
|||||||
@@ -3,63 +3,69 @@ name: Check link
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-link:
|
check-link:
|
||||||
name: check-link
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- name: Checkout
|
||||||
# search Issues :-(
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
- run: |
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check Markdown links
|
||||||
|
run: |
|
||||||
docker run -i --rm \
|
docker run -i --rm \
|
||||||
-v $PWD:/mnt:ro \
|
-v "$GITHUB_WORKSPACE:/mnt:ro" \
|
||||||
dkhamsing/awesome_bot \
|
-w /mnt \
|
||||||
--white-list "localhost","0.0.0.0",\
|
dkhamsing/awesome_bot@sha256:a8adaeb3b3bd5745304743e4d8a6d512127646e420544a6d22d9f58a07f35884 \
|
||||||
"server","example.com","docker",\
|
--white-list "localhost","0.0.0.0",\
|
||||||
"docker.domain.com","YourIP","register",\
|
"server","example.com","docker",\
|
||||||
"172.16.238.100","172.16.238.101","172.16.238.102",\
|
"docker.domain.com","YourIP","register",\
|
||||||
"192.168.199.100",\
|
"172.16.238.100","172.16.238.101","172.16.238.102",\
|
||||||
"github.com/settings",\
|
"192.168.199.100",\
|
||||||
"github.com/docker/compose/releases/download",\
|
"github.com/settings",\
|
||||||
"github.com/etcd-io/etcd/releases/download",\
|
"github.com/docker/compose/releases/download",\
|
||||||
"github.com/tianon/gosu/releases/download",\
|
"github.com/etcd-io/etcd/releases/download",\
|
||||||
"github.com/yeasy/docker_practice",\
|
"github.com/tianon/gosu/releases/download",\
|
||||||
"github.com/AliyunContainerService/k8s-for-docker-desktop/raw",\
|
"github.com/yeasy/docker_practice",\
|
||||||
"dl-cdn.alpinelinux.org/alpine/edge/testing",\
|
"github.com/AliyunContainerService/k8s-for-docker-desktop/raw",\
|
||||||
"www.w3.org/1999/xhtml",\
|
"dl-cdn.alpinelinux.org/alpine/edge/testing",\
|
||||||
"cr.console.aliyun.com",\
|
"www.w3.org/1999/xhtml",\
|
||||||
"cloud.tencent.com",\
|
"cr.console.aliyun.com",\
|
||||||
"nodejs.org/dist/",\
|
"cloud.tencent.com",\
|
||||||
"c.163.com/hub",\
|
"nodejs.org/dist/",\
|
||||||
"drone.yeasy.com",\
|
"c.163.com/hub",\
|
||||||
"docs.docker.com",\
|
"drone.yeasy.com",\
|
||||||
"dockerhub.azk8s.cn",\
|
"docs.docker.com",\
|
||||||
"reg-mirror.qiniu.com",\
|
"dockerhub.azk8s.cn",\
|
||||||
"registry.docker-cn.com",\
|
"reg-mirror.qiniu.com",\
|
||||||
"mirror.ccs.tencentyun.com",\
|
"registry.docker-cn.com",\
|
||||||
"vuepress.mirror.docker-practice.com",\
|
"mirror.ccs.tencentyun.com",\
|
||||||
"mc.qcloudimg.com/static/img",\
|
"vuepress.mirror.docker-practice.com",\
|
||||||
"www.daocloud.io/mirror",\
|
"mc.qcloudimg.com/static/img",\
|
||||||
"download.docker.com",\
|
"www.daocloud.io/mirror",\
|
||||||
"www.ubuntu.com",\
|
"download.docker.com",\
|
||||||
"archive.ubuntu.com",\
|
"www.ubuntu.com",\
|
||||||
"security.ubuntu.com/ubuntu",\
|
"archive.ubuntu.com",\
|
||||||
"nginx.com",\
|
"security.ubuntu.com/ubuntu",\
|
||||||
"img.shields.io/github/release/yeasy/docker_practice",\
|
"nginx.com",\
|
||||||
"launchpad.net",\
|
"img.shields.io/github/release/yeasy/docker_practice",\
|
||||||
"www.w3.org/1999",\
|
"launchpad.net",\
|
||||||
"chat.freenode.net",\
|
"www.w3.org/1999",\
|
||||||
"en.wikipedia.org/wiki/UnionFS",\
|
"chat.freenode.net",\
|
||||||
"product.china-pub.com",\
|
"en.wikipedia.org/wiki/UnionFS",\
|
||||||
"union-click.jd.com",\
|
"product.china-pub.com",\
|
||||||
"x.x.x.x/base",\
|
"union-click.jd.com",\
|
||||||
"x.x.x.x:9090",\
|
"x.x.x.x/base",\
|
||||||
"yeasy.gitbooks.io",\
|
"x.x.x.x:9090",\
|
||||||
"download.fastgit.org",\
|
"yeasy.gitbooks.io",\
|
||||||
"www.aliyun.com" \
|
"download.fastgit.org",\
|
||||||
--allow-dupe \
|
"www.aliyun.com" \
|
||||||
--skip-save-results \
|
--allow-dupe \
|
||||||
-t 10 \
|
--skip-save-results \
|
||||||
`find . \( -path "./mesos" -o -path "./swarm_mode" \) -prune -o -name "*.md" -exec ls {} \;`
|
-t 10 \
|
||||||
name: check-link
|
$(find . \( -path "./mesos" -o -path "./swarm_mode" \) -prune -o -name "*.md" -print)
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
|
|||||||
+80
-23
@@ -7,48 +7,105 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
permissions:
|
||||||
|
contents: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- name: Checkout
|
||||||
- name: Check project rules
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
run: python3 check_project_rules.py
|
with:
|
||||||
- name: Check release metadata
|
persist-credentials: false
|
||||||
run: npm test
|
|
||||||
- name: Install Chromium and CJK fonts
|
- name: Install locked Node dependencies
|
||||||
uses: browser-actions/setup-chrome@v2
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install canonical-example validators
|
||||||
|
env:
|
||||||
|
KUBECONFORM_VERSION: "0.8.0"
|
||||||
|
KUBECONFORM_SHA256: "9bc2bffbf71f261128533edaf912153948b7ff238f9a531ae6d34466ec287883"
|
||||||
|
ACTIONLINT_VERSION: "1.7.12"
|
||||||
|
ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
|
kube_archive="$RUNNER_TEMP/kubeconform.tar.gz"
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \
|
||||||
|
-o "$kube_archive"
|
||||||
|
echo "${KUBECONFORM_SHA256} $kube_archive" | sha256sum -c -
|
||||||
|
tar xzf "$kube_archive" -C "$RUNNER_TEMP/bin" kubeconform
|
||||||
|
|
||||||
|
actionlint_archive="$RUNNER_TEMP/actionlint.tar.gz"
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$actionlint_archive"
|
||||||
|
echo "${ACTIONLINT_SHA256} $actionlint_archive" | sha256sum -c -
|
||||||
|
tar xzf "$actionlint_archive" -C "$RUNNER_TEMP/bin" actionlint
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Check project rules, metadata, and canonical examples
|
||||||
|
run: |
|
||||||
|
python3 check_project_rules.py
|
||||||
|
npm test
|
||||||
|
python3 tools/test_examples.py --require-tools
|
||||||
|
|
||||||
|
- name: Install Chromium
|
||||||
|
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
- name: Install CJK fonts
|
|
||||||
|
- name: Install CJK fonts and PDF inspection tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
|
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra poppler-utils
|
||||||
- name: Install mdpress 0.7.10
|
|
||||||
|
- name: Install mdPress 0.7.10
|
||||||
env:
|
env:
|
||||||
MDPRESS_VERSION: "0.7.10"
|
MDPRESS_VERSION: "0.7.10"
|
||||||
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
||||||
run: |
|
run: |
|
||||||
archive="/tmp/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
archive="$RUNNER_TEMP/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
||||||
echo "Installing mdpress ${MDPRESS_VERSION}"
|
curl -fsSL --retry 3 \
|
||||||
curl -fsSL "https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" -o "$archive"
|
"https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$archive"
|
||||||
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
||||||
tar xzf "$archive"
|
tar xzf "$archive" -C "$RUNNER_TEMP" mdpress
|
||||||
sudo mv mdpress /usr/local/bin/
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
mdpress --version
|
install -m 0755 "$RUNNER_TEMP/mdpress" "$RUNNER_TEMP/bin/mdpress"
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Prepare PDF sources
|
- name: Prepare PDF sources
|
||||||
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
||||||
|
|
||||||
- name: Build PDF
|
- name: Build PDF
|
||||||
working-directory: ${{ runner.temp }}/docker_practice-pdf-src
|
working-directory: ${{ runner.temp }}/docker_practice-pdf-src
|
||||||
run: mdpress build --format pdf --output "$GITHUB_WORKSPACE/docker_practice.pdf"
|
run: |
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||||
|
mdpress build --format pdf --output "$GITHUB_WORKSPACE/dist/docker_practice.pdf"
|
||||||
|
|
||||||
- name: Build site
|
- name: Build site
|
||||||
run: npm run build
|
run: npm run build
|
||||||
- name: Upload PDF as artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
- name: Verify build artifacts
|
||||||
|
run: |
|
||||||
|
title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])')
|
||||||
|
python3 tools/verify_artifacts.py \
|
||||||
|
--title "$title" \
|
||||||
|
--pdf dist/docker_practice.pdf \
|
||||||
|
--site _site \
|
||||||
|
--checksums dist/SHA256SUMS
|
||||||
|
(cd dist && sha256sum -c SHA256SUMS)
|
||||||
|
|
||||||
|
- name: Upload verified PDF
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker_practice-pdf
|
name: docker_practice-pdf
|
||||||
path: docker_practice.pdf
|
path: |
|
||||||
|
dist/docker_practice.pdf
|
||||||
|
dist/SHA256SUMS
|
||||||
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
name: Dependabot auto-merge
|
name: Dependabot auto-merge
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
checks: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dependabot:
|
dependabot:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
checks: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.actor == 'dependabot[bot]'
|
if: github.actor == 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3
|
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: Pages
|
||||||
|
|
||||||
|
# One-time repository setup remains manual: Settings > Pages > Source -> GitHub Actions.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install locked Node dependencies
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Check project rules and metadata
|
||||||
|
run: |
|
||||||
|
python3 check_project_rules.py
|
||||||
|
npm test
|
||||||
|
|
||||||
|
- name: Install mdPress 0.7.10
|
||||||
|
env:
|
||||||
|
MDPRESS_VERSION: "0.7.10"
|
||||||
|
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
||||||
|
run: |
|
||||||
|
archive="$RUNNER_TEMP/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$archive"
|
||||||
|
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
||||||
|
tar xzf "$archive" -C "$RUNNER_TEMP" mdpress
|
||||||
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
|
install -m 0755 "$RUNNER_TEMP/mdpress" "$RUNNER_TEMP/bin/mdpress"
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Build mdPress site
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Verify site output
|
||||||
|
run: test -s _site/index.html
|
||||||
|
|
||||||
|
- name: Configure Pages
|
||||||
|
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||||
|
|
||||||
|
- name: Upload Pages artifact
|
||||||
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||||
|
with:
|
||||||
|
path: _site
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||||
@@ -6,49 +6,57 @@ on:
|
|||||||
- master
|
- master
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: write
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: preview-pdf
|
group: preview-pdf
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-preview-pdf:
|
build:
|
||||||
name: Update preview release
|
permissions:
|
||||||
|
contents: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check project rules
|
- name: Install locked Node dependencies
|
||||||
run: python3 check_project_rules.py
|
env:
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: "true"
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Check release metadata
|
- name: Check project rules and metadata
|
||||||
run: npm test
|
run: |
|
||||||
|
python3 check_project_rules.py
|
||||||
|
npm test
|
||||||
|
|
||||||
- name: Install Chromium and CJK fonts
|
- name: Install Chromium
|
||||||
uses: browser-actions/setup-chrome@v2
|
uses: browser-actions/setup-chrome@2e1d749697dd1612b833dba4a722266286fbefcd # v2.1.2
|
||||||
with:
|
with:
|
||||||
chrome-version: stable
|
chrome-version: stable
|
||||||
- name: Install CJK fonts
|
|
||||||
|
- name: Install CJK fonts and PDF inspection tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
|
sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra poppler-utils
|
||||||
|
|
||||||
- name: Install mdpress 0.7.10
|
- name: Install mdPress 0.7.10
|
||||||
env:
|
env:
|
||||||
MDPRESS_VERSION: "0.7.10"
|
MDPRESS_VERSION: "0.7.10"
|
||||||
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
MDPRESS_SHA256: "17e53e455996940bbbce64c69c43b3fb543f1501e03b74cf0434074efebd2db4"
|
||||||
run: |
|
run: |
|
||||||
archive="/tmp/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
archive="$RUNNER_TEMP/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz"
|
||||||
echo "Installing mdpress ${MDPRESS_VERSION}"
|
curl -fsSL --retry 3 \
|
||||||
curl -fsSL "https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" -o "$archive"
|
"https://github.com/yeasy/mdPress/releases/download/v${MDPRESS_VERSION}/mdpress_${MDPRESS_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$archive"
|
||||||
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
echo "${MDPRESS_SHA256} $archive" | sha256sum -c -
|
||||||
tar xzf "$archive" -C /tmp mdpress
|
tar xzf "$archive" -C "$RUNNER_TEMP" mdpress
|
||||||
sudo mv /tmp/mdpress /usr/local/bin/
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
mdpress --version
|
install -m 0755 "$RUNNER_TEMP/mdpress" "$RUNNER_TEMP/bin/mdpress"
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Prepare PDF sources
|
- name: Prepare PDF sources
|
||||||
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
run: python3 tools/prepare_pdf_sources.py --book-dir . --out "$RUNNER_TEMP/docker_practice-pdf-src"
|
||||||
@@ -59,6 +67,39 @@ jobs:
|
|||||||
mkdir -p "$GITHUB_WORKSPACE/dist"
|
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||||
mdpress build --format pdf --output "$GITHUB_WORKSPACE/dist/docker_practice.pdf"
|
mdpress build --format pdf --output "$GITHUB_WORKSPACE/dist/docker_practice.pdf"
|
||||||
|
|
||||||
|
- name: Verify preview PDF
|
||||||
|
run: |
|
||||||
|
title=$(python3 -c 'import json; print(json.load(open("book.json", encoding="utf-8"))["title"])')
|
||||||
|
python3 tools/verify_artifacts.py \
|
||||||
|
--title "$title" \
|
||||||
|
--pdf dist/docker_practice.pdf \
|
||||||
|
--checksums dist/SHA256SUMS
|
||||||
|
(cd dist && sha256sum -c SHA256SUMS)
|
||||||
|
|
||||||
|
- name: Upload verified preview bundle
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: docker-practice-preview
|
||||||
|
path: dist/
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
publish:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
steps:
|
||||||
|
- name: Download verified preview bundle
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: docker-practice-preview
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Recheck preview checksum
|
||||||
|
run: (cd dist && sha256sum -c SHA256SUMS)
|
||||||
|
|
||||||
- name: Write release notes
|
- name: Write release notes
|
||||||
run: |
|
run: |
|
||||||
cat > dist/release-notes.md <<EOF
|
cat > dist/release-notes.md <<EOF
|
||||||
@@ -69,7 +110,7 @@ jobs:
|
|||||||
- Run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
|
- Run: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Create preview release
|
- name: Create preview release when absent
|
||||||
run: |
|
run: |
|
||||||
gh release view preview-pdf >/dev/null 2>&1 || \
|
gh release view preview-pdf >/dev/null 2>&1 || \
|
||||||
gh release create preview-pdf \
|
gh release create preview-pdf \
|
||||||
@@ -87,8 +128,10 @@ jobs:
|
|||||||
--prerelease \
|
--prerelease \
|
||||||
--target "$GITHUB_SHA"
|
--target "$GITHUB_SHA"
|
||||||
|
|
||||||
- name: Remove legacy preview asset
|
- name: Replace preview assets
|
||||||
run: gh release delete-asset preview-pdf latest.pdf -y || true
|
run: |
|
||||||
|
gh release delete-asset preview-pdf latest.pdf -y || true
|
||||||
- name: Upload preview PDF
|
gh release upload preview-pdf \
|
||||||
run: gh release upload preview-pdf dist/docker_practice.pdf --clobber
|
dist/docker_practice.pdf \
|
||||||
|
dist/SHA256SUMS \
|
||||||
|
--clobber
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ format_report.txt
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
06_repository/demo/auth/nginx.htpasswd
|
06_repository/demo/auth/nginx.htpasswd
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ Dockerfile 是一个文本文件,其内包含了一条条的 **指令 (Instruc
|
|||||||
|
|
||||||
Dockerfile 一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。
|
Dockerfile 一般分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。
|
||||||
|
|
||||||
|
可运行 `docker buildx build --check` 校验的[完整 Dockerfile 示例](../examples/validated/dockerfile/Dockerfile)与配套 `index.html` 保存在仓库中。章节内的指令片段用于聚焦语法;需要完整构建上下文时,应使用这份示例。
|
||||||
|
|
||||||
### 指令详解
|
### 指令详解
|
||||||
|
|
||||||
本章将详细讲解 Dockerfile 中的各个指令:
|
本章将详细讲解 Dockerfile 中的各个指令:
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
默认的模板文件名称为 `compose.yaml` (也兼容 `docker-compose.yml` 等历史文件名),格式为 YAML。
|
默认的模板文件名称为 `compose.yaml` (也兼容 `docker-compose.yml` 等历史文件名),格式为 YAML。
|
||||||
|
|
||||||
|
本书同时维护一份可直接运行 `docker compose config` 校验的[完整 Compose 示例](../examples/validated/compose/compose.yaml)。章节中的片段用于解释单个字段;需要复制完整文件时,应以该示例为准。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
webapp:
|
webapp:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
本章将通过一个具体的案例:部署一个 Nginx 网站,并为其配置 Service,来串联前面学到的知识。
|
本章将通过一个具体的案例:部署一个 Nginx 网站,并为其配置 Service,来串联前面学到的知识。
|
||||||
|
|
||||||
|
Deployment 与 Service 的[合并清单](../examples/validated/kubernetes/web.yaml)会通过严格模式的 `kubeconform` 校验。下面按步骤拆开讲解;复制完整清单时,应以仓库中的合并版本为准。
|
||||||
|
|
||||||
开始前请先准备好可用的 Kubernetes 集群和 `kubectl` 上下文。你可以先完成 [14.3 Docker Desktop](../14_kubernetes_setup/14.3_docker-desktop.md) 或 [14.4 Kind](../14_kubernetes_setup/14.4_kind.md),并确认:
|
开始前请先准备好可用的 Kubernetes 集群和 `kubectl` 上下文。你可以先完成 [14.3 Docker Desktop](../14_kubernetes_setup/14.3_docker-desktop.md) 或 [14.4 Kind](../14_kubernetes_setup/14.4_kind.md),并确认:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ jobs:
|
|||||||
```
|
```
|
||||||
该示例会在 GitHub Actions 中构建当前仓库的 Docker 镜像(不推送到 registry)。
|
该示例会在 GitHub Actions 中构建当前仓库的 Docker 镜像(不推送到 registry)。
|
||||||
|
|
||||||
|
仓库还提供一份经 `actionlint` 校验、并将 `checkout` 固定到完整 commit SHA 的[可执行工作流示例](../examples/validated/github-actions/validate.yml)。为了让正文突出结构,下面较长的教学片段仍用主版本 tag;生产工作流应采用完整 SHA。
|
||||||
|
|
||||||
### 21.2.2 构建并推送到 Registry
|
### 21.2.2 构建并推送到 Registry
|
||||||
|
|
||||||
实际项目中通常需要在 CI 中构建镜像并推送到容器 Registry。以下示例展示了多阶段构建 + 登录 + 推送的完整流程:
|
实际项目中通常需要在 CI 中构建镜像并推送到容器 Registry。以下示例展示了多阶段构建 + 登录 + 推送的完整流程:
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 经自动验证的示例
|
||||||
|
|
||||||
|
本目录保存书中四类关键示例的单一真相源:Compose、Dockerfile、Kubernetes 清单和 GitHub Actions 工作流。`tools/test_examples.py` 会调用这些工具各自的原生校验命令;CI 中缺少任一工具都会失败,本地环境缺少工具则明确报告 `SKIP`。
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
name: docker-practice
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:1.28-alpine
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx:1.28-alpine
|
||||||
|
|
||||||
|
COPY --chown=nginx:nginx index.html /usr/share/nginx/html/index.html
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-Hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Docker 实践指南示例</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Docker 实践指南</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Validate container examples
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install pinned validators
|
||||||
|
env:
|
||||||
|
KUBECONFORM_VERSION: "0.8.0"
|
||||||
|
KUBECONFORM_SHA256: "9bc2bffbf71f261128533edaf912153948b7ff238f9a531ae6d34466ec287883"
|
||||||
|
ACTIONLINT_VERSION: "1.7.12"
|
||||||
|
ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
|
||||||
|
run: |
|
||||||
|
mkdir -p "$RUNNER_TEMP/bin"
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/yannh/kubeconform/releases/download/v${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \
|
||||||
|
-o "$RUNNER_TEMP/kubeconform.tar.gz"
|
||||||
|
echo "${KUBECONFORM_SHA256} $RUNNER_TEMP/kubeconform.tar.gz" | sha256sum -c -
|
||||||
|
tar xzf "$RUNNER_TEMP/kubeconform.tar.gz" -C "$RUNNER_TEMP/bin" kubeconform
|
||||||
|
|
||||||
|
curl -fsSL --retry 3 \
|
||||||
|
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
-o "$RUNNER_TEMP/actionlint.tar.gz"
|
||||||
|
echo "${ACTIONLINT_SHA256} $RUNNER_TEMP/actionlint.tar.gz" | sha256sum -c -
|
||||||
|
tar xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP/bin" actionlint
|
||||||
|
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Validate canonical examples
|
||||||
|
run: python3 tools/test_examples.py --require-tools
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-deployment
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.28-alpine
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nginx-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: nginx
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: http
|
||||||
Generated
+26320
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -4,12 +4,13 @@
|
|||||||
"description": "docker_practice",
|
"description": "docker_practice",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@mermaid-js/mermaid-cli": "10.9.1",
|
||||||
"vuepress": "1.9.10",
|
"vuepress": "1.9.10",
|
||||||
"vuepress-plugin-container": "^2.1.5",
|
"vuepress-plugin-container": "^2.1.5",
|
||||||
"vuepress-theme-hope": "^1.0.0"
|
"vuepress-theme-hope": "^1.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node scripts/check_metadata.js && python3 -m unittest tools.test_prepare_pdf_sources",
|
"test": "node scripts/check_metadata.js && python3 -m unittest tools.test_prepare_pdf_sources && python3 -m unittest discover -s tests -p 'test_*.py' && python3 tools/test_examples.py",
|
||||||
"mdpress:help": "mdpress --help",
|
"mdpress:help": "mdpress --help",
|
||||||
"build": "rm -rf _site _site_site && mdpress build --format site --output _site && rm -rf _site && mv _site_site _site",
|
"build": "rm -rf _site _site_site && mdpress build --format site --output _site && rm -rf _site && mv _site_site _site",
|
||||||
"serve": "mdpress serve",
|
"serve": "mdpress serve",
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER = ROOT / "tools" / "test_examples.py"
|
||||||
|
FIXTURES = {
|
||||||
|
"compose": ROOT / "examples" / "validated" / "compose" / "compose.yaml",
|
||||||
|
"dockerfile": ROOT / "examples" / "validated" / "dockerfile" / "Dockerfile",
|
||||||
|
"kubernetes": ROOT / "examples" / "validated" / "kubernetes" / "web.yaml",
|
||||||
|
"github-actions": ROOT / "examples" / "validated" / "github-actions" / "validate.yml",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleValidationTests(unittest.TestCase):
|
||||||
|
def run_runner(self, *args, path=None, env=None):
|
||||||
|
run_env = os.environ.copy()
|
||||||
|
run_env.update(env or {})
|
||||||
|
if path is not None:
|
||||||
|
run_env["PATH"] = path
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(RUNNER), *args],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=run_env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_canonical_fixture_files_exist(self):
|
||||||
|
missing = [name for name, path in FIXTURES.items() if not path.is_file()]
|
||||||
|
|
||||||
|
self.assertEqual(missing, [])
|
||||||
|
|
||||||
|
def test_relevant_chapters_link_to_canonical_fixtures(self):
|
||||||
|
references = {
|
||||||
|
ROOT / "11_compose" / "11.5_compose_file.md": "../examples/validated/compose/compose.yaml",
|
||||||
|
ROOT / "07_dockerfile" / "README.md": "../examples/validated/dockerfile/Dockerfile",
|
||||||
|
ROOT / "13_kubernetes_concepts" / "13.5_practice.md": "../examples/validated/kubernetes/web.yaml",
|
||||||
|
ROOT / "21_case_devops" / "21.2_github_actions.md": "../examples/validated/github-actions/validate.yml",
|
||||||
|
}
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for chapter, fixture in references.items():
|
||||||
|
if fixture not in chapter.read_text(encoding="utf-8"):
|
||||||
|
missing.append(f"{chapter.relative_to(ROOT)} -> {fixture}")
|
||||||
|
self.assertEqual(missing, [])
|
||||||
|
|
||||||
|
def test_github_actions_fixture_installs_integrity_pinned_validators(self):
|
||||||
|
text = FIXTURES["github-actions"].read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("KUBECONFORM_SHA256", text)
|
||||||
|
self.assertIn("ACTIONLINT_SHA256", text)
|
||||||
|
self.assertGreaterEqual(text.count("sha256sum -c -"), 2)
|
||||||
|
self.assertIn("tools/test_examples.py --require-tools", text)
|
||||||
|
|
||||||
|
def test_local_run_reports_skips_when_tools_are_unavailable(self):
|
||||||
|
result = self.run_runner(path="/usr/bin:/bin")
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertEqual(result.stdout.count("SKIP"), 4, result.stdout)
|
||||||
|
self.assertIn("docker compose", result.stdout)
|
||||||
|
self.assertIn("docker buildx", result.stdout)
|
||||||
|
self.assertIn("kubeconform", result.stdout)
|
||||||
|
self.assertIn("actionlint", result.stdout)
|
||||||
|
|
||||||
|
def test_required_run_fails_when_tools_are_unavailable(self):
|
||||||
|
result = self.run_runner("--require-tools", path="/usr/bin:/bin")
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertEqual(result.stdout.count("UNAVAILABLE"), 4, result.stdout)
|
||||||
|
|
||||||
|
def test_available_tools_receive_the_canonical_validation_commands(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
log = tmp_path / "commands.log"
|
||||||
|
shim = tmp_path / "validator-shim"
|
||||||
|
shim.write_text(
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"printf '%s %s\\n' \"$(basename \"$0\")\" \"$*\" >> \"$COMMAND_LOG\"\n"
|
||||||
|
"exit 0\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
shim.chmod(0o755)
|
||||||
|
for name in ("docker", "kubeconform", "actionlint"):
|
||||||
|
(tmp_path / name).symlink_to(shim)
|
||||||
|
|
||||||
|
result = self.run_runner(
|
||||||
|
"--require-tools",
|
||||||
|
path=f"{tmp_path}:/usr/bin:/bin",
|
||||||
|
env={"COMMAND_LOG": str(log)},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
commands = log.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("docker compose version", commands)
|
||||||
|
self.assertIn("docker compose -f", commands)
|
||||||
|
self.assertIn("config --quiet", commands)
|
||||||
|
self.assertIn("docker buildx version", commands)
|
||||||
|
self.assertIn("docker buildx build --check", commands)
|
||||||
|
self.assertIn("kubeconform -strict -summary", commands)
|
||||||
|
self.assertIn("actionlint", commands)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
WORKFLOW = ROOT / ".github" / "workflows" / "pages.yml"
|
||||||
|
FULL_ACTION_SHA = re.compile(r"^[^@\s]+@[0-9a-f]{40}$")
|
||||||
|
|
||||||
|
|
||||||
|
class PagesWorkflowTests(unittest.TestCase):
|
||||||
|
def workflow_text(self):
|
||||||
|
self.assertTrue(WORKFLOW.is_file(), "custom Pages workflow is missing")
|
||||||
|
return WORKFLOW.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def test_custom_pages_workflow_exists(self):
|
||||||
|
self.assertTrue(WORKFLOW.is_file(), "custom Pages workflow is missing")
|
||||||
|
|
||||||
|
def test_builds_mdpress_site_without_jekyll(self):
|
||||||
|
text = self.workflow_text()
|
||||||
|
|
||||||
|
self.assertIn("npm run build", text)
|
||||||
|
self.assertIn("MDPRESS_SHA256", text)
|
||||||
|
self.assertIn('install -m 0755 "$RUNNER_TEMP/mdpress" "$RUNNER_TEMP/bin/mdpress"', text)
|
||||||
|
self.assertIn('echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"', text)
|
||||||
|
self.assertRegex(text, r"path:\s*_site\b")
|
||||||
|
self.assertNotIn("jekyll", text.lower())
|
||||||
|
|
||||||
|
def test_build_and_deploy_jobs_have_minimum_permissions(self):
|
||||||
|
text = self.workflow_text()
|
||||||
|
|
||||||
|
self.assertRegex(
|
||||||
|
text,
|
||||||
|
r"(?ms)^ build:\n permissions:\n contents: read\n pages: read\b",
|
||||||
|
)
|
||||||
|
self.assertRegex(
|
||||||
|
text,
|
||||||
|
r"(?ms)^ deploy:.*?permissions:\n pages: write\n id-token: write\b",
|
||||||
|
)
|
||||||
|
self.assertRegex(text, r"(?ms)^ deploy:.*?needs: build\b")
|
||||||
|
self.assertIn("environment:", text)
|
||||||
|
self.assertIn("name: github-pages", text)
|
||||||
|
|
||||||
|
def test_actions_are_immutable_and_checkout_drops_credentials(self):
|
||||||
|
text = self.workflow_text()
|
||||||
|
actions = re.findall(r"\buses:\s*([^\s#]+)", text)
|
||||||
|
|
||||||
|
self.assertGreater(len(actions), 0)
|
||||||
|
self.assertTrue(all(FULL_ACTION_SHA.fullmatch(action) for action in actions), actions)
|
||||||
|
self.assertRegex(text, r"actions/checkout@[0-9a-f]{40}\s+# v\d")
|
||||||
|
self.assertRegex(
|
||||||
|
text,
|
||||||
|
r"(?ms)actions/checkout@[0-9a-f]{40}.*?with:\n\s+persist-credentials: false",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_documents_manual_pages_source_setting(self):
|
||||||
|
text = self.workflow_text()
|
||||||
|
|
||||||
|
self.assertIn("Settings > Pages > Source", text)
|
||||||
|
self.assertIn("GitHub Actions", text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import hashlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
VERIFIER = ROOT / "tools" / "verify_artifacts.py"
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyArtifactsTests(unittest.TestCase):
|
||||||
|
def run_verifier(self, *args):
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(VERIFIER), *args],
|
||||||
|
cwd=ROOT,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_verifies_html_title_and_writes_sha256_manifest(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
artifact = Path(tmp) / "reader.html"
|
||||||
|
checksums = Path(tmp) / "SHA256SUMS"
|
||||||
|
artifact.write_text(
|
||||||
|
"<!doctype html><html><head><title>Docker —— 从入门到实践</title></head></html>",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_verifier(
|
||||||
|
"--title",
|
||||||
|
"Docker —— 从入门到实践",
|
||||||
|
"--html",
|
||||||
|
str(artifact),
|
||||||
|
"--checksums",
|
||||||
|
str(checksums),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
expected = hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||||
|
self.assertEqual(checksums.read_text(encoding="utf-8"), f"{expected} reader.html\n")
|
||||||
|
|
||||||
|
def test_rejects_missing_artifact(self):
|
||||||
|
result = self.run_verifier(
|
||||||
|
"--title",
|
||||||
|
"Docker —— 从入门到实践",
|
||||||
|
"--html",
|
||||||
|
"/does/not/exist.html",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("does not exist", result.stderr)
|
||||||
|
|
||||||
|
def test_rejects_wrong_html_title(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
artifact = Path(tmp) / "reader.html"
|
||||||
|
artifact.write_text("<title>Wrong book</title>", encoding="utf-8")
|
||||||
|
|
||||||
|
result = self.run_verifier(
|
||||||
|
"--title",
|
||||||
|
"Docker —— 从入门到实践",
|
||||||
|
"--html",
|
||||||
|
str(artifact),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("title mismatch", result.stderr)
|
||||||
|
|
||||||
|
def test_accepts_mdpress_site_title_with_page_suffix(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
site = Path(tmp) / "_site"
|
||||||
|
site.mkdir()
|
||||||
|
(site / "index.html").write_text(
|
||||||
|
"<title>Docker 从入门到实践 - Docker 从入门到实践</title>",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.run_verifier(
|
||||||
|
"--title",
|
||||||
|
"Docker 从入门到实践",
|
||||||
|
"--site",
|
||||||
|
str(site),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
WORKFLOW_DIR = ROOT / ".github" / "workflows"
|
||||||
|
FULL_ACTION_SHA = re.compile(r"^[^@\s]+@[0-9a-f]{40}$")
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowSecurityTests(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def workflows():
|
||||||
|
return sorted(WORKFLOW_DIR.glob("*.y*ml"))
|
||||||
|
|
||||||
|
def test_all_actions_are_immutable_with_version_comments(self):
|
||||||
|
failures = []
|
||||||
|
for workflow in self.workflows():
|
||||||
|
for number, line in enumerate(workflow.read_text(encoding="utf-8").splitlines(), 1):
|
||||||
|
match = re.search(r"\buses:\s*([^\s#]+)(?:\s+#\s*(\S+))?", line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
action, version = match.groups()
|
||||||
|
if not FULL_ACTION_SHA.fullmatch(action) or not version or not version.startswith("v"):
|
||||||
|
failures.append(f"{workflow.name}:{number}: {line.strip()}")
|
||||||
|
self.assertEqual(failures, [])
|
||||||
|
|
||||||
|
def test_checkout_never_persists_credentials(self):
|
||||||
|
failures = []
|
||||||
|
for workflow in self.workflows():
|
||||||
|
lines = workflow.read_text(encoding="utf-8").splitlines()
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if "uses: actions/checkout@" not in line:
|
||||||
|
continue
|
||||||
|
step = "\n".join(lines[index : index + 8])
|
||||||
|
if "persist-credentials: false" not in step:
|
||||||
|
failures.append(f"{workflow.name}:{index + 1}")
|
||||||
|
self.assertEqual(failures, [])
|
||||||
|
|
||||||
|
def test_every_workflow_declares_permissions(self):
|
||||||
|
failures = []
|
||||||
|
for workflow in self.workflows():
|
||||||
|
text = workflow.read_text(encoding="utf-8")
|
||||||
|
before_jobs = text.split("\njobs:", 1)[0]
|
||||||
|
if not re.search(r"(?m)^permissions:", before_jobs):
|
||||||
|
failures.append(workflow.name)
|
||||||
|
self.assertEqual(failures, [])
|
||||||
|
|
||||||
|
def test_release_and_preview_separate_read_only_builds_from_writes(self):
|
||||||
|
expectations = {
|
||||||
|
"auto-release.yml": ("release",),
|
||||||
|
"preview-pdf.yml": ("publish",),
|
||||||
|
}
|
||||||
|
for name, write_jobs in expectations.items():
|
||||||
|
text = (WORKFLOW_DIR / name).read_text(encoding="utf-8")
|
||||||
|
self.assertRegex(text, r"(?ms)^ build:\n permissions:\n contents: read\b", name)
|
||||||
|
for job in write_jobs:
|
||||||
|
self.assertRegex(
|
||||||
|
text,
|
||||||
|
rf"(?ms)^ {job}:.*?permissions:\n contents: write\b.*?needs: build\b",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_downloads_dependencies_and_link_checker_are_integrity_pinned(self):
|
||||||
|
combined = "\n".join(path.read_text(encoding="utf-8") for path in self.workflows())
|
||||||
|
link_text = (WORKFLOW_DIR / "check-link.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertNotRegex(combined, r"npm install\s+-g\s+@mermaid-js/mermaid-cli")
|
||||||
|
self.assertIn("PANDOC_SHA256", combined)
|
||||||
|
self.assertIn("KUBECONFORM_SHA256", combined)
|
||||||
|
self.assertIn("ACTIONLINT_SHA256", combined)
|
||||||
|
self.assertRegex(link_text, r"dkhamsing/awesome_bot@sha256:[0-9a-f]{64}")
|
||||||
|
|
||||||
|
def test_mermaid_is_exact_and_lockfile_backed(self):
|
||||||
|
package = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
||||||
|
version = package["devDependencies"]["@mermaid-js/mermaid-cli"]
|
||||||
|
|
||||||
|
self.assertRegex(version, r"^\d+\.\d+\.\d+$")
|
||||||
|
self.assertTrue((ROOT / "package-lock.json").is_file())
|
||||||
|
ignored = {
|
||||||
|
line.strip()
|
||||||
|
for line in (ROOT / ".gitignore").read_text(encoding="utf-8").splitlines()
|
||||||
|
if line.strip() and not line.lstrip().startswith("#")
|
||||||
|
}
|
||||||
|
self.assertNotIn("package-lock.json", ignored)
|
||||||
|
self.assertTrue(all("npm ci" in path.read_text(encoding="utf-8") for path in self.workflows() if path.name != "check-link.yml" and path.name != "dependabot-automerge.yml"))
|
||||||
|
|
||||||
|
def test_artifacts_are_smoke_tested_and_html_failures_are_not_silent(self):
|
||||||
|
verifier = ROOT / "tools" / "verify_artifacts.py"
|
||||||
|
self.assertTrue(verifier.is_file())
|
||||||
|
for name in ("auto-release.yml", "ci.yaml", "preview-pdf.yml"):
|
||||||
|
text = (WORKFLOW_DIR / name).read_text(encoding="utf-8")
|
||||||
|
self.assertIn("tools/verify_artifacts.py", text, name)
|
||||||
|
self.assertIn("SHA256SUMS", text, name)
|
||||||
|
auto_release = (WORKFLOW_DIR / "auto-release.yml").read_text(encoding="utf-8")
|
||||||
|
self.assertNotIn("continue-on-error: true", auto_release)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate the book's canonical container examples with their native tools."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FIXTURE_ROOT = ROOT / "examples" / "validated"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Check:
|
||||||
|
name: str
|
||||||
|
executable: str
|
||||||
|
command: List[str]
|
||||||
|
probe: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
CHECKS = (
|
||||||
|
Check(
|
||||||
|
"docker compose",
|
||||||
|
"docker",
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
str(FIXTURE_ROOT / "compose" / "compose.yaml"),
|
||||||
|
"config",
|
||||||
|
"--quiet",
|
||||||
|
],
|
||||||
|
["docker", "compose", "version"],
|
||||||
|
),
|
||||||
|
Check(
|
||||||
|
"docker buildx",
|
||||||
|
"docker",
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"buildx",
|
||||||
|
"build",
|
||||||
|
"--check",
|
||||||
|
"--file",
|
||||||
|
str(FIXTURE_ROOT / "dockerfile" / "Dockerfile"),
|
||||||
|
str(FIXTURE_ROOT / "dockerfile"),
|
||||||
|
],
|
||||||
|
["docker", "buildx", "version"],
|
||||||
|
),
|
||||||
|
Check(
|
||||||
|
"kubeconform",
|
||||||
|
"kubeconform",
|
||||||
|
[
|
||||||
|
"kubeconform",
|
||||||
|
"-strict",
|
||||||
|
"-summary",
|
||||||
|
"-kubernetes-version",
|
||||||
|
"1.31.0",
|
||||||
|
str(FIXTURE_ROOT / "kubernetes" / "web.yaml"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Check(
|
||||||
|
"actionlint",
|
||||||
|
"actionlint",
|
||||||
|
["actionlint", str(FIXTURE_ROOT / "github-actions" / "validate.yml")],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command):
|
||||||
|
return subprocess.run(command, cwd=ROOT, capture_output=True, text=True)
|
||||||
|
|
||||||
|
|
||||||
|
def unavailable(check, require_tools, detail):
|
||||||
|
status = "UNAVAILABLE" if require_tools else "SKIP"
|
||||||
|
print(f"[{status}] {check.name}: {detail}")
|
||||||
|
return require_tools
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--require-tools",
|
||||||
|
action="store_true",
|
||||||
|
help="fail instead of skipping when a validator is unavailable (for CI)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for check in CHECKS:
|
||||||
|
if shutil.which(check.executable) is None:
|
||||||
|
failed = unavailable(check, args.require_tools, f"{check.executable} not found") or failed
|
||||||
|
continue
|
||||||
|
|
||||||
|
if check.probe is not None:
|
||||||
|
probe = run_command(check.probe)
|
||||||
|
if probe.returncode != 0:
|
||||||
|
detail = (probe.stderr or probe.stdout or "plugin probe failed").strip()
|
||||||
|
failed = unavailable(check, args.require_tools, detail) or failed
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = run_command(check.command)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"[FAIL] {check.name}")
|
||||||
|
print((result.stderr or result.stdout).strip())
|
||||||
|
failed = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[PASS] {check.name}")
|
||||||
|
|
||||||
|
return 1 if failed else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -2,12 +2,24 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
from tools.prepare_pdf_sources import normalize_markdown_asset_paths, prepare_pdf_sources
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class PreparePdfSourcesTest(unittest.TestCase):
|
class PreparePdfSourcesTest(unittest.TestCase):
|
||||||
|
def test_module_imports_with_running_python(self):
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", "import tools.prepare_pdf_sources"],
|
||||||
|
cwd=Path(__file__).resolve().parents[1],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
def test_normalizes_relative_markdown_and_html_image_paths(self):
|
def test_normalizes_relative_markdown_and_html_image_paths(self):
|
||||||
|
from tools.prepare_pdf_sources import normalize_markdown_asset_paths
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
book_dir = Path(tmp)
|
book_dir = Path(tmp)
|
||||||
(book_dir / "_images").mkdir()
|
(book_dir / "_images").mkdir()
|
||||||
@@ -31,6 +43,8 @@ class PreparePdfSourcesTest(unittest.TestCase):
|
|||||||
self.assertEqual(count, 2)
|
self.assertEqual(count, 2)
|
||||||
|
|
||||||
def test_prepares_temp_tree_without_mutating_source_markdown(self):
|
def test_prepares_temp_tree_without_mutating_source_markdown(self):
|
||||||
|
from tools.prepare_pdf_sources import prepare_pdf_sources
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
source = Path(tmp) / "source"
|
source = Path(tmp) / "source"
|
||||||
target = Path(tmp) / "target"
|
target = Path(tmp) / "target"
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Smoke-test built book artifacts and write a portable SHA-256 manifest."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import html
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
print(f"artifact verification failed: {message}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def normalized_title(value):
|
||||||
|
return " ".join(html.unescape(value).split())
|
||||||
|
|
||||||
|
|
||||||
|
def require_file(path):
|
||||||
|
if not path.is_file():
|
||||||
|
fail(f"{path} does not exist or is not a file")
|
||||||
|
if path.stat().st_size == 0:
|
||||||
|
fail(f"{path} is empty")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_html(path, expected_title):
|
||||||
|
require_file(path)
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"<title(?:\s[^>]*)?>(.*?)</title>", text, re.IGNORECASE | re.DOTALL)
|
||||||
|
actual = normalized_title(match.group(1)) if match else ""
|
||||||
|
expected = normalized_title(expected_title)
|
||||||
|
accepted = actual == expected or actual.startswith(f"{expected} - ") or actual.endswith(f" - {expected}")
|
||||||
|
if not accepted:
|
||||||
|
fail(f"{path} title mismatch: expected {expected_title!r}, got {actual!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def command_output(command):
|
||||||
|
result = subprocess.run(command, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
fail(f"command failed ({' '.join(command)}): {(result.stderr or result.stdout).strip()}")
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def verify_pdf(path, expected_title):
|
||||||
|
require_file(path)
|
||||||
|
if not path.read_bytes().startswith(b"%PDF-"):
|
||||||
|
fail(f"{path} does not have a PDF signature")
|
||||||
|
|
||||||
|
if shutil.which("pdfinfo") is None or shutil.which("pdftotext") is None:
|
||||||
|
fail("pdfinfo and pdftotext are required for PDF title verification")
|
||||||
|
|
||||||
|
metadata = command_output(["pdfinfo", str(path)])
|
||||||
|
title_match = re.search(r"(?m)^Title:\s*(.*)$", metadata)
|
||||||
|
metadata_title = normalized_title(title_match.group(1)) if title_match else ""
|
||||||
|
expected = normalized_title(expected_title)
|
||||||
|
if metadata_title == expected:
|
||||||
|
return
|
||||||
|
|
||||||
|
first_pages = normalized_title(
|
||||||
|
command_output(["pdftotext", "-f", "1", "-l", "2", str(path), "-"])
|
||||||
|
)
|
||||||
|
if expected not in first_pages:
|
||||||
|
fail(
|
||||||
|
f"{path} title mismatch: expected {expected_title!r}; "
|
||||||
|
f"PDF metadata title was {metadata_title!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_checksums(paths, destination):
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
lines = []
|
||||||
|
for path in sorted(paths, key=lambda item: item.name):
|
||||||
|
if path.parent.resolve() != destination.parent.resolve():
|
||||||
|
fail(f"{path} must be beside checksum manifest {destination}")
|
||||||
|
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
lines.append(f"{digest} {path.name}\n")
|
||||||
|
destination.write_text("".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--title", required=True)
|
||||||
|
parser.add_argument("--pdf", type=Path)
|
||||||
|
parser.add_argument("--html", type=Path)
|
||||||
|
parser.add_argument("--site", type=Path)
|
||||||
|
parser.add_argument("--checksums", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
artifacts = []
|
||||||
|
if args.pdf:
|
||||||
|
verify_pdf(args.pdf, args.title)
|
||||||
|
artifacts.append(args.pdf)
|
||||||
|
if args.html:
|
||||||
|
verify_html(args.html, args.title)
|
||||||
|
artifacts.append(args.html)
|
||||||
|
if args.site:
|
||||||
|
verify_html(args.site / "index.html", args.title)
|
||||||
|
if not artifacts and not args.site:
|
||||||
|
parser.error("at least one of --pdf, --html, or --site is required")
|
||||||
|
if args.checksums:
|
||||||
|
if not artifacts:
|
||||||
|
fail("a checksum manifest requires at least one file artifact")
|
||||||
|
write_checksums(artifacts, args.checksums)
|
||||||
|
|
||||||
|
for path in artifacts:
|
||||||
|
print(f"verified artifact: {path}")
|
||||||
|
if args.site:
|
||||||
|
print(f"verified site: {args.site / 'index.html'}")
|
||||||
|
if args.checksums:
|
||||||
|
print(f"wrote checksums: {args.checksums}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user