Skip to content

Build and Publish

Build and Publish #103

Workflow file for this run

name: Build and Publish
on:
schedule:
# At 04:00 on day-of-month 1.
- cron: "0 4 1 * *"
workflow_dispatch:
push:
branches:
- "main"
permissions:
contents: write
env:
DOCKERHUB_IMAGE: betterweb/node
FORGEJO_IMAGE: code.bettercorp.dev/bettercorp/docker-node
jobs:
prepare-matrix:
runs-on: blacksmith-2vcpu-ubuntu-2404
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
latest: ${{ steps.latest-version.outputs.output }}
devcontainer_matrix: ${{ steps.set-dev-matrix.outputs.matrix }}
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Check Docker image platforms
id: check-platforms
run: |
curl -s "https://nodejs.org/download/release/index.json" >node_versions.json
# Filter out versions older than 2 years
two_years_ago=$(date -u -d "2 years ago" +%s)
jq --argjson two_years_ago "$two_years_ago" '[.[] | select((.date | sub("T.*"; "") | strptime("%Y-%m-%d") | mktime) > $two_years_ago)]' node_versions.json >node_versions_recent.json
# Function to check Docker image platforms
check_platforms() {
version=$1
# Get platforms from manifest
if manifest=$(docker manifest inspect "node:$version-alpine" 2>/dev/null); then
# Extract all valid platforms (excluding unknown)
platforms=$(echo "$manifest" | jq -r '.manifests[] |
select(.platform.os != "unknown" and .platform.architecture != "unknown") |
if .platform.variant then
"linux/" + .platform.architecture + "/" + .platform.variant
else
"linux/" + .platform.architecture
end' | tr '\n' ',' | sed 's/,$//')
if [ -n "$platforms" ]; then
echo "$platforms"
return
fi
fi
# Default platforms if manifest check fails
echo "linux/amd64,linux/arm64/v8"
}
# Process versions and add platform information (using filtered recent versions)
jq -c '[.[] | select(.version | test("v(1[8-9]|[2-9][0-9]).*"))] | group_by(.version | match("v(\\d+).*").captures[0].string | tonumber) | map(max_by(.version | match("v(\\d+\\.\\d+).*").captures[0].string | split(".") as $parts | ($parts[0] | tonumber) * 1000 + ($parts[1] | tonumber))) | map({version: .version, major: (.version | match("v(\\d+).*").captures[0].string | tonumber)})' node_versions_recent.json >versions.json
# Add platform information to each version
platforms_json="["
first=true
while IFS= read -r version_info; do
version=$(echo "$version_info" | jq -r '.version')
major=$(echo "$version_info" | jq -r '.major')
version_without_v="${version#v}" # Remove 'v' prefix for Docker tag
platforms=$(check_platforms "$version_without_v")
if [ "$first" = true ]; then
first=false
else
platforms_json="$platforms_json,"
fi
platforms_json="$platforms_json{\"version\":\"$version\",\"major\":$major,\"platforms\":\"$platforms\"}"
done < <(jq -c '.[]' versions.json)
platforms_json="$platforms_json]"
echo "$platforms_json" >config.json
- uses: cloudposse/github-action-jq@main
id: latest-version
with:
compact: true
raw-output: true
input: config.json
script: 'sort_by(.major) | reverse | group_by(.major) | map(.[0]) | sort_by(.major) | reverse | .[0].version'
- uses: cloudposse/github-action-jq@main
id: reparse-tags
with:
compact: true
input: config.json
script: '{"tag": . }'
- run: echo 'matrix=${{ steps.reparse-tags.outputs.output }}' >> $GITHUB_OUTPUT
id: set-matrix
# Pick the top 3 major versions for devcontainers
- uses: cloudposse/github-action-jq@main
id: reparse-dev-tags
with:
compact: true
input: config.json
script: 'sort_by(.major) | reverse | group_by(.major) | map(.[0]) | sort_by(.major) | reverse | .[:3] | {"tag": . }'
- run: echo 'matrix=${{ steps.reparse-dev-tags.outputs.output }}' >> $GITHUB_OUTPUT
id: set-dev-matrix
build:
needs: prepare-matrix
runs-on: blacksmith-2vcpu-ubuntu-2404
strategy:
matrix: ${{fromJson(needs.prepare-matrix.outputs.matrix)}}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4
- name: Login to Forgejo
uses: docker/login-action@v4
with:
registry: code.bettercorp.dev
username: ${{ secrets.BC_CODE_USER }}
password: ${{ secrets.BC_CODE_PACKAGE_TOKEN }}
logout: true
- name: Parse version
id: parse_version
run: |
VERSION=${{ matrix.tag.version }}
PARSED_VERSION=${VERSION#v}
echo "PARSED_VERSION=$PARSED_VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Is Latest
id: is_latest
run: |
if [ "${{ matrix.tag.version }}" = "${{ needs.prepare-matrix.outputs.latest }}" ]; then
echo "IS_LATEST=true" >> $GITHUB_OUTPUT
echo "This is the latest version"
else
echo "IS_LATEST=false" >> $GITHUB_OUTPUT
echo "This is not the latest version"
fi
- name: Build and push to Forgejo
id: build_image
uses: docker/build-push-action@v7
with:
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
context: .
platforms: ${{ matrix.tag.platforms }}
push: true
tags: |
${{ env.FORGEJO_IMAGE }}:${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.FORGEJO_IMAGE }}:${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:latest', env.FORGEJO_IMAGE) || '' }}
- name: Login to DockerHub
continue-on-error: true
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
logout: true
- name: Build and push to DockerHub
continue-on-error: true
uses: docker/build-push-action@v7
with:
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
context: .
platforms: ${{ matrix.tag.platforms }}
push: true
tags: |
${{ env.DOCKERHUB_IMAGE }}:${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.DOCKERHUB_IMAGE }}:${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:latest', env.DOCKERHUB_IMAGE) || '' }}
- name: Write manifest part
run: |
TAGS=$(jq -nc \
--arg v "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--arg m "${{ matrix.tag.major }}" \
--arg latest "${{ steps.is_latest.outputs.IS_LATEST }}" \
--arg dockerhub "${{ env.DOCKERHUB_IMAGE }}" \
--arg forgejo "${{ env.FORGEJO_IMAGE }}" \
'[($dockerhub + ":" + $v), ($dockerhub + ":" + $m), ($forgejo + ":" + $v), ($forgejo + ":" + $m)] + (if $latest == "true" then [($dockerhub + ":latest"), ($forgejo + ":latest")] else [] end)')
PLATFORMS=$(echo "${{ matrix.tag.platforms }}" | jq -Rc 'split(",")')
jq -n \
--arg version "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--argjson major ${{ matrix.tag.major }} \
--arg variant "base" \
--argjson platforms "$PLATFORMS" \
--argjson tags "$TAGS" \
--arg digest "${{ steps.build_image.outputs.digest }}" \
--argjson is_latest ${{ steps.is_latest.outputs.IS_LATEST }} \
'{version:$version, major:$major, variant:$variant, platforms:$platforms, tags:$tags, digest:$digest, is_latest:$is_latest}' \
> part.json
cat part.json
- uses: actions/upload-artifact@v4
with:
name: manifest-part-base-${{ matrix.tag.major }}
path: part.json
build-dev:
needs: [prepare-matrix, build]
runs-on: blacksmith-2vcpu-ubuntu-2404
strategy:
matrix: ${{fromJson(needs.prepare-matrix.outputs.devcontainer_matrix)}}
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Forgejo
uses: docker/login-action@v4
with:
registry: code.bettercorp.dev
username: ${{ secrets.BC_CODE_USER }}
password: ${{ secrets.BC_CODE_PACKAGE_TOKEN }}
logout: true
- name: Parse version
id: parse_version
run: |
VERSION=${{ matrix.tag.version }}
PARSED_VERSION=${VERSION#v}
echo "PARSED_VERSION=$PARSED_VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Is Latest
id: is_latest
run: |
if [ "${{ matrix.tag.version }}" = "${{ needs.prepare-matrix.outputs.latest }}" ]; then
echo "IS_LATEST=true" >> $GITHUB_OUTPUT
echo "This is the latest version"
else
echo "IS_LATEST=false" >> $GITHUB_OUTPUT
echo "This is not the latest version"
fi
- name: Build and push dev image to Forgejo
id: build_image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.dev
push: true
build-args: |
BASE_IMAGE=${{ env.FORGEJO_IMAGE }}
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.FORGEJO_IMAGE }}:dev-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.FORGEJO_IMAGE }}:dev-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:dev', env.FORGEJO_IMAGE) || '' }}
- name: Login to DockerHub
continue-on-error: true
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
logout: true
- name: Build and push dev image to DockerHub
continue-on-error: true
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.dev
push: true
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.DOCKERHUB_IMAGE }}:dev-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.DOCKERHUB_IMAGE }}:dev-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:dev', env.DOCKERHUB_IMAGE) || '' }}
- name: Write manifest part
run: |
TAGS=$(jq -nc \
--arg v "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--arg m "${{ matrix.tag.major }}" \
--arg latest "${{ steps.is_latest.outputs.IS_LATEST }}" \
--arg dockerhub "${{ env.DOCKERHUB_IMAGE }}" \
--arg forgejo "${{ env.FORGEJO_IMAGE }}" \
'[($dockerhub + ":dev-" + $v), ($dockerhub + ":dev-" + $m), ($forgejo + ":dev-" + $v), ($forgejo + ":dev-" + $m)] + (if $latest == "true" then [($dockerhub + ":dev"), ($forgejo + ":dev")] else [] end)')
jq -n \
--arg version "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--argjson major ${{ matrix.tag.major }} \
--arg variant "dev" \
--argjson platforms '["linux/amd64"]' \
--argjson tags "$TAGS" \
--arg digest "${{ steps.build_image.outputs.digest }}" \
--argjson is_latest ${{ steps.is_latest.outputs.IS_LATEST }} \
'{version:$version, major:$major, variant:$variant, platforms:$platforms, tags:$tags, digest:$digest, is_latest:$is_latest}' \
> part.json
cat part.json
- uses: actions/upload-artifact@v4
with:
name: manifest-part-dev-${{ matrix.tag.major }}
path: part.json
build-devcontainers:
needs: prepare-matrix
runs-on: blacksmith-2vcpu-ubuntu-2404
strategy:
matrix: ${{fromJson(needs.prepare-matrix.outputs.devcontainer_matrix)}}
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Forgejo
uses: docker/login-action@v4
with:
registry: code.bettercorp.dev
username: ${{ secrets.BC_CODE_USER }}
password: ${{ secrets.BC_CODE_PACKAGE_TOKEN }}
logout: true
- name: Parse version
id: parse_version
run: |
VERSION=${{ matrix.tag.version }}
PARSED_VERSION=${VERSION#v}
echo "PARSED_VERSION=$PARSED_VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Is Latest
id: is_latest
run: |
if [ "${{ matrix.tag.version }}" = "${{ needs.prepare-matrix.outputs.latest }}" ]; then
echo "IS_LATEST=true" >> $GITHUB_OUTPUT
echo "This is the latest version"
else
echo "IS_LATEST=false" >> $GITHUB_OUTPUT
echo "This is not the latest version"
fi
- name: Build and push devcontainer image to Forgejo
id: build_image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.devcontainer
push: true
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.FORGEJO_IMAGE }}:devcontainer-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.FORGEJO_IMAGE }}:devcontainer-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:devcontainer-latest', env.FORGEJO_IMAGE) || '' }}
- name: Login to DockerHub
continue-on-error: true
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
logout: true
- name: Build and push devcontainer image to DockerHub
continue-on-error: true
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.devcontainer
push: true
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.DOCKERHUB_IMAGE }}:devcontainer-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.DOCKERHUB_IMAGE }}:devcontainer-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:devcontainer-latest', env.DOCKERHUB_IMAGE) || '' }}
- name: Write manifest part
run: |
TAGS=$(jq -nc \
--arg v "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--arg m "${{ matrix.tag.major }}" \
--arg latest "${{ steps.is_latest.outputs.IS_LATEST }}" \
--arg dockerhub "${{ env.DOCKERHUB_IMAGE }}" \
--arg forgejo "${{ env.FORGEJO_IMAGE }}" \
'[($dockerhub + ":devcontainer-" + $v), ($dockerhub + ":devcontainer-" + $m), ($forgejo + ":devcontainer-" + $v), ($forgejo + ":devcontainer-" + $m)] + (if $latest == "true" then [($dockerhub + ":devcontainer-latest"), ($forgejo + ":devcontainer-latest")] else [] end)')
jq -n \
--arg version "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--argjson major ${{ matrix.tag.major }} \
--arg variant "devcontainer" \
--argjson platforms '["linux/amd64"]' \
--argjson tags "$TAGS" \
--arg digest "${{ steps.build_image.outputs.digest }}" \
--argjson is_latest ${{ steps.is_latest.outputs.IS_LATEST }} \
'{version:$version, major:$major, variant:$variant, platforms:$platforms, tags:$tags, digest:$digest, is_latest:$is_latest}' \
> part.json
cat part.json
- uses: actions/upload-artifact@v4
with:
name: manifest-part-devcontainer-${{ matrix.tag.major }}
path: part.json
build-dind:
needs: [prepare-matrix, build]
runs-on: blacksmith-2vcpu-ubuntu-2404
strategy:
matrix: ${{fromJson(needs.prepare-matrix.outputs.devcontainer_matrix)}}
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Forgejo
uses: docker/login-action@v4
with:
registry: code.bettercorp.dev
username: ${{ secrets.BC_CODE_USER }}
password: ${{ secrets.BC_CODE_PACKAGE_TOKEN }}
logout: true
- name: Parse version
id: parse_version
run: |
VERSION=${{ matrix.tag.version }}
PARSED_VERSION=${VERSION#v}
echo "PARSED_VERSION=$PARSED_VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Is Latest
id: is_latest
run: |
if [ "${{ matrix.tag.version }}" = "${{ needs.prepare-matrix.outputs.latest }}" ]; then
echo "IS_LATEST=true" >> $GITHUB_OUTPUT
echo "This is the latest version"
else
echo "IS_LATEST=false" >> $GITHUB_OUTPUT
echo "This is not the latest version"
fi
- name: Build and push dind image to Forgejo
id: build_image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.dind
push: true
build-args: |
BASE_IMAGE=${{ env.FORGEJO_IMAGE }}
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.FORGEJO_IMAGE }}:dind-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.FORGEJO_IMAGE }}:dind-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:dind-latest', env.FORGEJO_IMAGE) || '' }}
- name: Login to DockerHub
continue-on-error: true
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
logout: true
- name: Build and push dind image to DockerHub
continue-on-error: true
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.dind
push: true
build-args: |
NODE_MAJOR=${{ matrix.tag.major }}
NODE_VERSION=${{ steps.parse_version.outputs.PARSED_VERSION }}
tags: |
${{ env.DOCKERHUB_IMAGE }}:dind-${{steps.parse_version.outputs.PARSED_VERSION}}
${{ env.DOCKERHUB_IMAGE }}:dind-${{matrix.tag.major}}
${{ steps.is_latest.outputs.IS_LATEST == 'true' && format('{0}:dind-latest', env.DOCKERHUB_IMAGE) || '' }}
- name: Write manifest part
run: |
TAGS=$(jq -nc \
--arg v "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--arg m "${{ matrix.tag.major }}" \
--arg latest "${{ steps.is_latest.outputs.IS_LATEST }}" \
--arg dockerhub "${{ env.DOCKERHUB_IMAGE }}" \
--arg forgejo "${{ env.FORGEJO_IMAGE }}" \
'[($dockerhub + ":dind-" + $v), ($dockerhub + ":dind-" + $m), ($forgejo + ":dind-" + $v), ($forgejo + ":dind-" + $m)] + (if $latest == "true" then [($dockerhub + ":dind-latest"), ($forgejo + ":dind-latest")] else [] end)')
jq -n \
--arg version "${{ steps.parse_version.outputs.PARSED_VERSION }}" \
--argjson major ${{ matrix.tag.major }} \
--arg variant "dind" \
--argjson platforms '["linux/amd64"]' \
--argjson tags "$TAGS" \
--arg digest "${{ steps.build_image.outputs.digest }}" \
--argjson is_latest ${{ steps.is_latest.outputs.IS_LATEST }} \
'{version:$version, major:$major, variant:$variant, platforms:$platforms, tags:$tags, digest:$digest, is_latest:$is_latest}' \
> part.json
cat part.json
- uses: actions/upload-artifact@v4
with:
name: manifest-part-dind-${{ matrix.tag.major }}
path: part.json
publish-manifest:
needs: [prepare-matrix, build, build-dev, build-devcontainers, build-dind]
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- uses: actions/download-artifact@v4
with:
pattern: manifest-part-*
merge-multiple: true
path: parts/
- name: Merge manifest parts
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LATEST_VERSION="${{ needs.prepare-matrix.outputs.latest }}"
LATEST_VERSION_STRIPPED="${LATEST_VERSION#v}"
jq -s \
--arg ts "$TIMESTAMP" \
--arg latest "$LATEST_VERSION_STRIPPED" \
--arg run_id "${{ github.run_id }}" \
--arg run_number "${{ github.run_number }}" \
--arg sha "${{ github.sha }}" \
'{
build: {
run_id: ($run_id | tonumber),
run_number: ($run_number | tonumber),
sha: $sha,
timestamp: $ts
},
latest: $latest,
builds: (
group_by(.version)
| map({
version: .[0].version,
major: .[0].major,
is_latest: .[0].is_latest,
platforms: (map(.platforms) | add | unique),
variants: (
map({(.variant): {tags: .tags, platforms: .platforms, digest: .digest}}) | add
)
})
| sort_by(.major) | reverse
)
}' parts/*.json > manifest.json
cat manifest.json
- uses: actions/upload-artifact@v4
with:
name: manifest
path: manifest.json
- name: Create release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="build-${{ github.run_number }}"
TITLE="Build #${{ github.run_number }} — node ${{ needs.prepare-matrix.outputs.latest }}"
# Drop existing release+tag if re-running the same workflow.
gh release delete "$TAG" -y --cleanup-tag --repo "${{ github.repository }}" 2>/dev/null || true
gh release create "$TAG" \
--repo "${{ github.repository }}" \
--title "$TITLE" \
--notes "Auto-generated manifest of betterweb/node multi-arch builds. Fetch latest via: https://github.com/${{ github.repository }}/releases/latest/download/manifest.json" \
--latest \
manifest.json