Build and Publish Docker Image workflow (nickrunning/wechat-selkies)
The Build and Publish Docker Image workflow from nickrunning/wechat-selkies, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build and Publish Docker Image workflow from the nickrunning/wechat-selkies repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Build and Publish Docker Image
on:
workflow_dispatch:
push:
branches:
- master
paths:
- 'versions/upstream.env'
tags:
- 'v*'
env:
REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-publish:
runs-on: ubuntu-latest
environment: DOCKERHUB
permissions:
contents: write
packages: write
id-token: write
attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: vars.ENABLE_DOCKERHUB == 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKERHUB_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }},enable=${{ vars.ENABLE_DOCKERHUB == 'true' }}
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') || startsWith(github.ref, 'refs/tags/') }}
- name: Build and push Docker image
id: build-image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for minimal image
id: meta-minimal
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }},enable=${{ vars.ENABLE_DOCKERHUB == 'true' }}
flavor: |
latest=false
suffix=-minimal
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=minimal,enable=${{ github.ref == format('refs/heads/{0}', 'master') || startsWith(github.ref, 'refs/tags/') }}
- name: Build and push minimal Docker image
id: build-minimal
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta-minimal.outputs.tags }}
labels: ${{ steps.meta-minimal.outputs.labels }}
build-args: |
INSTALL_QQ=false
INSTALL_PCMANFM=false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate artifact attestation
if: github.event_name != 'workflow_dispatch'
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build-image.outputs.digest }}
push-to-registry: true
- name: Output Docker image information
run: |
echo "## π³ Docker Images Published" >> $GITHUB_STEP_SUMMARY
echo "### GitHub Container Registry" >> $GITHUB_STEP_SUMMARY
echo "**Registry:** ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY
echo "**Repository:** ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
echo "**Full image digest:** \`${{ steps.build-image.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Minimal image digest:** \`${{ steps.build-minimal.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Tags (full):**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "**Tags (minimal):**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta-minimal.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
if [ "${{ vars.ENABLE_DOCKERHUB }}" == "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Hub" >> $GITHUB_STEP_SUMMARY
echo "**Registry:** ${{ env.DOCKERHUB_REGISTRY }}" >> $GITHUB_STEP_SUMMARY
echo "**Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
echo "**Digest:** \`${{ steps.build-image.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Tags:**" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Hub" >> $GITHUB_STEP_SUMMARY
echo "β οΈ **Skipped** - Docker Hub push disabled (set ENABLE_DOCKERHUB=true to enable)" >> $GITHUB_STEP_SUMMARY
fi
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
make_latest: true
body: |
## π³ Docker Images
### Full (WeChat + QQ + File Manager)
**GitHub Container Registry:**
```bash
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
```
**Docker Hub** (if enabled):
```bash
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ steps.meta.outputs.version }}
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest
```
### Minimal (WeChat only)
```bash
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:minimal
```
## π Quick Start
**Full:**
```bash
docker run -it -p 3001:3001 -v ./config:/config --device /dev/dri:/dev/dri ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
```
**Minimal:**
```bash
docker run -it -p 3001:3001 -v ./config:/config --device /dev/dri:/dev/dri ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:minimal
```
Then visit: https://localhost:3001
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Build and Publish Docker Image on: workflow_dispatch: push: branches: - master paths: - 'versions/upstream.env' tags: - 'v*' env: REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io IMAGE_NAME: ${{ github.repository }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-and-publish: timeout-minutes: 30 runs-on: latchkey-small environment: DOCKERHUB permissions: contents: write packages: write id-token: write attestations: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub if: vars.ENABLE_DOCKERHUB == 'true' uses: docker/login-action@v3 with: registry: ${{ env.DOCKERHUB_REGISTRY }} username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }},enable=${{ vars.ENABLE_DOCKERHUB == 'true' }} flavor: | latest=false tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') || startsWith(github.ref, 'refs/tags/') }} - name: Build and push Docker image id: build-image uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Extract metadata for minimal image id: meta-minimal uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }},enable=${{ vars.ENABLE_DOCKERHUB == 'true' }} flavor: | latest=false suffix=-minimal tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=raw,value=minimal,enable=${{ github.ref == format('refs/heads/{0}', 'master') || startsWith(github.ref, 'refs/tags/') }} - name: Build and push minimal Docker image id: build-minimal uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta-minimal.outputs.tags }} labels: ${{ steps.meta-minimal.outputs.labels }} build-args: | INSTALL_QQ=false INSTALL_PCMANFM=false cache-from: type=gha cache-to: type=gha,mode=max - name: Generate artifact attestation if: github.event_name != 'workflow_dispatch' uses: actions/attest-build-provenance@v1 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build-image.outputs.digest }} push-to-registry: true - name: Output Docker image information run: | echo "## π³ Docker Images Published" >> $GITHUB_STEP_SUMMARY echo "### GitHub Container Registry" >> $GITHUB_STEP_SUMMARY echo "**Registry:** ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY echo "**Repository:** ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY echo "**Full image digest:** \`${{ steps.build-image.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY echo "**Minimal image digest:** \`${{ steps.build-minimal.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY echo "**Tags (full):**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "**Tags (minimal):**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "${{ steps.meta-minimal.outputs.tags }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY if [ "${{ vars.ENABLE_DOCKERHUB }}" == "true" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "### Docker Hub" >> $GITHUB_STEP_SUMMARY echo "**Registry:** ${{ env.DOCKERHUB_REGISTRY }}" >> $GITHUB_STEP_SUMMARY echo "**Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY echo "**Digest:** \`${{ steps.build-image.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY echo "**Tags:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY else echo "" >> $GITHUB_STEP_SUMMARY echo "### Docker Hub" >> $GITHUB_STEP_SUMMARY echo "β οΈ **Skipped** - Docker Hub push disabled (set ENABLE_DOCKERHUB=true to enable)" >> $GITHUB_STEP_SUMMARY fi - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: generate_release_notes: true make_latest: true body: | ## π³ Docker Images ### Full (WeChat + QQ + File Manager) **GitHub Container Registry:** ```bash docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ``` **Docker Hub** (if enabled): ```bash docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ steps.meta.outputs.version }} docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest ``` ### Minimal (WeChat only) ```bash docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:minimal ``` ## π Quick Start **Full:** ```bash docker run -it -p 3001:3001 -v ./config:/config --device /dev/dri:/dev/dri ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ``` **Minimal:** ```bash docker run -it -p 3001:3001 -v ./config:/config --device /dev/dri:/dev/dri ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:minimal ``` Then visit: https://localhost:3001
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
5 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Container pulls and builds
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.