Skip to content
Merged

Dev #2147

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
167 changes: 147 additions & 20 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,92 @@
#
# Version source of truth: docs/image-tag-spec.md.
#
# Tag namespaces (same repository, tag differs by branch):
# main branch -> stable tags from W9_VERSION
# dev branch -> dev-<git-sha> (primary) + dev-latest (rolling alias)
# Tag namespaces (same repository, tag differs by branch/event):
# dev push -> dev-<git-sha> (primary) + dev-latest (rolling alias)
# amd64 is required; arm64 is best-effort and warns on failure
# final tags are assembled from digests; no temporary arch tags are published
# PR merged to main -> promote the PR head's dev-<sha> to stable tags (no rebuild)
# main push -> not wired; main promotion happens only on a merged PR
#
# Build contract: the Dockerfile MUST declare a version ARG whose name is
# ${APP^^}_VERSION (e.g. apps/akeneo -> AKENEO_VERSION). CI reads W9_VERSION
# from apps/<app>/.env, injects it as that build-arg, and tags from W9_VERSION.
# Apps that do not declare this ARG are not built by CI; migrate them when touched.
#
# Manual runs (workflow_dispatch) target one app:
# run on dev -> build dev-<sha> + dev-latest for the chosen app
# run on main -> promote the chosen app; source_sha (the validated dev-<sha>) is required
#
# If you add more paths for trigger, please update app_list= at set-matrix for it also.

name: Build image to DockerHub

on:
push:
branches: [main, dev]
branches: [dev]
paths:
- "apps/*/Dockerfile"
- "apps/*/.env"
- "apps/*/src/**"
- "apps/*/cmd.sh"
- "apps/*/entrypoint.sh"
pull_request:
types: [closed]
branches: [main]
paths:
- "apps/*/Dockerfile"
- "apps/*/.env"
- "apps/*/src/**"
- "apps/*/cmd.sh"
- "apps/*/entrypoint.sh"
workflow_dispatch:
inputs:
app:
description: "App name to build or promote (e.g. strapi)"
required: true
type: string
source_sha:
description: "Validated dev commit SHA to promote (required for manual promote on main)"
required: false
type: string

jobs:
setup:
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.merged && github.event.pull_request.base.ref == 'main') }}
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 2
# Pin the PR head (dev commit) because the pull_request merge ref can be
# gone by the time a merged PR's `closed` event runs.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0

- id: set-matrix
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_APP: ${{ inputs.app }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
changed_files=$(git diff --name-only HEAD^ HEAD)
app_list=$(echo "$changed_files" | grep -E 'apps/.*/(Dockerfile|.env|cmd.sh|entrypoint.sh)$' | awk -F'/' '{print $2}' | sort | uniq)
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
app_list="$INPUT_APP"
elif [ "$EVENT_NAME" = "pull_request" ]; then
app_list=$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" | grep -E 'apps/.*/(Dockerfile|.env|cmd.sh|entrypoint.sh|src/.*)$' | awk -F'/' '{print $2}' | sort | uniq)
else
changed_files=$(git diff --name-only HEAD^ HEAD)
app_list=$(echo "$changed_files" | grep -E 'apps/.*/(Dockerfile|.env|cmd.sh|entrypoint.sh|src/.*)$' | awk -F'/' '{print $2}' | sort | uniq)
fi
valid_list=()
for app in $app_list; do
if [ -z "$app" ]; then
continue
fi
if [ ! -f "apps/$app/Dockerfile" ]; then
echo "skip (no Dockerfile): $app"
continue
fi
app_upper=$(echo "$app" | tr '[:lower:]' '[:upper:]')
Expand All @@ -52,17 +98,24 @@ jobs:
fi
valid_list+=("$app")
done
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${#valid_list[@]}" -eq 0 ]; then
echo "error: app '$INPUT_APP' is not buildable (missing Dockerfile or <APP>_VERSION ARG)" >&2
exit 1
fi
app_list_json=$(jq -cn '$ARGS.positional' --args "${valid_list[@]}")
echo "::set-output name=matrix::{\"app\": $app_list_json}"
echo "matrix={\"app\": $app_list_json}" >> "$GITHUB_OUTPUT"

build:
needs: setup
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.merged && github.event.pull_request.base.ref == 'main') }}
runs-on: ubuntu-latest
strategy:
matrix: ${{fromJson(needs.setup.outputs.matrix)}}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}

- name: Set up Python
uses: actions/setup-python@v7
Expand All @@ -75,17 +128,45 @@ jobs:
pip install pyyaml

- name: Resolve channel
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
INPUT_SOURCE_SHA: ${{ inputs.source_sha }}
run: |
echo "APP=${{ matrix.app }}" >> $GITHUB_ENV
if [ "${{ github.ref_name }}" = "main" ]; then
if [ "$EVENT_NAME" = "pull_request" ]; then
# main promotion: promote the exact dev commit that was merged and built.
echo "CHANNEL=promote" >> $GITHUB_ENV
echo "SOURCE_SHA=$PR_HEAD_SHA" >> $GITHUB_ENV
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
case "$REF_NAME" in
dev)
echo "CHANNEL=dev" >> $GITHUB_ENV
echo "SOURCE_SHA=" >> $GITHUB_ENV
;;
main)
if [ -z "$INPUT_SOURCE_SHA" ]; then
echo "error: manual promote on main requires source_sha (the validated dev-<sha>)" >&2
exit 1
fi
echo "CHANNEL=promote" >> $GITHUB_ENV
echo "SOURCE_SHA=$INPUT_SOURCE_SHA" >> $GITHUB_ENV
;;
*)
echo "error: manual runs must target the dev or main branch (got '$REF_NAME')" >&2
exit 1
;;
esac
else
# push to dev: build candidate images.
echo "CHANNEL=dev" >> $GITHUB_ENV
echo "SOURCE_SHA=" >> $GITHUB_ENV
fi

- name: Generate build/promote plan
run: |
PYTHONPATH=cli python -c "import json,sys; from libs.app_build import build_plan; print(json.dumps(build_plan(app_name=sys.argv[1], channel=sys.argv[2], git_sha=sys.argv[3]), indent=2, ensure_ascii=False))" "${{ env.APP }}" "${{ env.CHANNEL }}" "${{ github.sha }}" > plan.json
PYTHONPATH=cli python -c "import json,os,sys; from libs.app_build import build_plan; print(json.dumps(build_plan(app_name=sys.argv[1], channel=sys.argv[2], git_sha=sys.argv[3], source_sha=(os.environ.get('SOURCE_SHA') or None)), indent=2, ensure_ascii=False))" "${{ env.APP }}" "${{ env.CHANNEL }}" "${{ github.sha }}" > plan.json
cat plan.json
echo "CHANNEL=$(jq -r '.channel' plan.json)" >> $GITHUB_ENV
echo "DIRECTORY=$(jq -r '.context' plan.json)" >> $GITHUB_ENV
Expand Down Expand Up @@ -122,26 +203,72 @@ jobs:

- name: Build and push Docker image (dev)
if: env.CHANNEL == 'dev'
uses: docker/build-push-action@v7
with:
context: ${{env.DIRECTORY}}
file: ${{env.DOCKERFILE}}
push: true
tags: ${{env.TAGS}}
platforms: linux/amd64
build-args: |
${{ env.VERSION_ARG }}=${{ env.W9_VERSION }}
shell: bash
run: |
IFS=',' read -r -a TAG_ARRAY <<< "${{ env.TAGS }}"

IMAGE_REPO="${TAG_ARRAY[0]%:*}"
AMD64_META=$(mktemp)
ARM64_META=$(mktemp)
trap 'rm -f "$AMD64_META" "$ARM64_META"' EXIT

docker buildx build \
--platform linux/amd64 \
-f "${{ env.DOCKERFILE }}" \
--build-arg "${{ env.VERSION_ARG }}=${{ env.W9_VERSION }}" \
--metadata-file "$AMD64_META" \
--output "type=image,name=${IMAGE_REPO},push-by-digest=true,name-canonical=true,push=true" \
"${{ env.DIRECTORY }}"

AMD64_DIGEST=$(jq -r '."containerimage.digest" // empty' "$AMD64_META")
if [ -z "$AMD64_DIGEST" ]; then
echo "ERROR: amd64 build completed but no digest was recorded for ${{ matrix.app }}" >&2
exit 1
fi

ARM64_OK=true
if ! docker buildx build \
--platform linux/arm64 \
-f "${{ env.DOCKERFILE }}" \
--build-arg "${{ env.VERSION_ARG }}=${{ env.W9_VERSION }}" \
--metadata-file "$ARM64_META" \
--output "type=image,name=${IMAGE_REPO},push-by-digest=true,name-canonical=true,push=true" \
"${{ env.DIRECTORY }}"; then
ARM64_OK=false
echo "::warning::arm64 image build failed for ${{ matrix.app }}; published amd64-only tags"
fi

ARM64_DIGEST=""
if [ "$ARM64_OK" = true ]; then
ARM64_DIGEST=$(jq -r '."containerimage.digest" // empty' "$ARM64_META")
if [ -z "$ARM64_DIGEST" ]; then
ARM64_OK=false
echo "::warning::arm64 build completed but no digest was recorded for ${{ matrix.app }}; published amd64-only tags"
fi
fi

for tag in "${TAG_ARRAY[@]}"; do
if [ "$ARM64_OK" = true ]; then
docker buildx imagetools create --tag "$tag" "${IMAGE_REPO}@${AMD64_DIGEST}" "${IMAGE_REPO}@${ARM64_DIGEST}"
else
docker buildx imagetools create --tag "$tag" "${IMAGE_REPO}@${AMD64_DIGEST}"
fi
done

- name: Promote dev image to stable tags (main)
if: env.CHANNEL == 'promote'
run: |
if ! docker buildx imagetools inspect "${{ env.SOURCE_IMAGE }}" >/dev/null 2>&1; then
if ! SOURCE_DIGEST=$(docker buildx imagetools inspect "${{ env.SOURCE_IMAGE }}" --format '{{.Manifest.Digest}}' 2>/dev/null); then
echo "ERROR: source dev image missing: ${{ env.SOURCE_IMAGE }}" >&2
echo "Expected this image to be built and pushed by the dev branch workflow before main promotion." >&2
exit 1
fi
TAG_ARGS=""
for tag in $(echo "${{ env.TAGS }}" | tr ',' ' '); do
existing=$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}' 2>/dev/null || true)
if [ -n "$existing" ] && [ "$existing" != "$SOURCE_DIGEST" ]; then
echo "::warning::overwriting existing $tag ($existing) with ${{ env.SOURCE_IMAGE }} ($SOURCE_DIGEST)"
fi
TAG_ARGS="$TAG_ARGS --tag $tag"
done
docker buildx imagetools create $TAG_ARGS "${{ env.SOURCE_IMAGE }}"
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Read in this order:
- Follow env conventions. The canonical `W9_*` reference is `docs/w9-env-spec.md` (semantics + decision rules); use `metadata/templates/new-app/.env.tmpl` for layout. Read both before editing `.env` or `docker-compose.yml`.
- Runtime/scaffold apps follow `docs/runtime-app-spec.md` (entrypoint hooks + optional `DATABASE_URL`).
- Validate by deployment when the task changes runnable behavior.
- When `.secrets/remote.env` exists, prefer remote-first for all runtime, deployment, debugging, and validation work; do not default to local container execution unless the user explicitly asks for local reproduction.

## i18n

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and versions prior to 0.8.0 do not strictly follow this format.
## [0.8.0] - Unreleased

### Added
- `libs dns-bind` / `libs dns-delete` for Aliyun DNS wildcard records, plus the `aliyun` provider in `make connector`
- Appstore Publish workflow with v2/catalog/library/manifest output model
- Channel-aware distribution merge for dev channel
- `workflow_dispatch` support for manual dev/rc/release publishing
Expand Down
42 changes: 33 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,48 @@ connector:
@bash -lc 'set -e; \
current_choice=1; \
if [ "${PROVIDER:-}" = "cloudflare" ] || [ "${PROVIDER:-}" = "2" ]; then current_choice=2; fi; \
printf "Available providers:\n 1) contentful\n 2) cloudflare\n 3) dockerhub\n"; \
if [ "${PROVIDER:-}" = "dockerhub" ] || [ "${PROVIDER:-}" = "3" ]; then current_choice=3; fi; \
if [ "${PROVIDER:-}" = "aliyun" ] || [ "${PROVIDER:-}" = "4" ]; then current_choice=4; fi; \
printf "Available providers:\n 1) contentful\n 2) cloudflare\n 3) dockerhub\n 4) aliyun (DNS)\n"; \
read -r -p "provider [$$current_choice]: " input_choice; input_choice="$${input_choice:-$$current_choice}"; \
case "$$input_choice" in \
1|contentful) provider="contentful"; file=".secrets/contentful.env"; key="CONTENTFUL_ACCESS_TOKEN" ;; \
2|cloudflare) provider="cloudflare"; file=".secrets/cloudflare.env"; key="CLOUDFLARE_API_TOKEN" ;; \
3|dockerhub) provider="dockerhub"; file=".secrets/dockerhub.env"; key="DOCKERHUB_TOKEN" ;; \
4|aliyun) provider="aliyun"; file=".secrets/aliyun.env"; key="ALIYUN_ACCESS_KEY_SECRET" ;; \
*) echo "unsupported provider selection: $$input_choice" >&2; exit 1 ;; \
esac; \
if [ -f "$$file" ]; then echo "updating $$file"; else echo "creating $$file"; fi; \
if [ "$$provider" = "dockerhub" ]; then \
read -r -p "DOCKERHUB_USERNAME: " input_user; \
read -r -s -p "DOCKERHUB_PASSWORD (leave empty to use token): " input_password; echo; \
if [ -n "$$input_password" ]; then \
if [ -z "$$input_user" ]; then echo "username is required" >&2; exit 1; fi; \
printf "DOCKERHUB_USERNAME=%s\nDOCKERHUB_PASSWORD=%s\n" "$$input_user" "$$input_password" > "$$file"; \
if [ "$$provider" = "aliyun" ]; then \
cur_id=""; cur_secret=""; cur_domain=""; \
if [ -f "$$file" ]; then \
cur_id="$$(grep -E '^ALIYUN_ACCESS_KEY_ID=' "$$file" | cut -d= -f2-)"; \
cur_secret="$$(grep -E '^ALIYUN_ACCESS_KEY_SECRET=' "$$file" | cut -d= -f2-)"; \
cur_domain="$$(grep -E '^ALIYUN_DNS_DOMAIN=' "$$file" | cut -d= -f2-)"; \
fi; \
read -r -p "ALIYUN_ACCESS_KEY_ID [$$cur_id]: " input_id; input_id="$${input_id:-$$cur_id}"; \
read -r -s -p "ALIYUN_ACCESS_KEY_SECRET [keep existing]: " input_secret; echo; input_secret="$${input_secret:-$$cur_secret}"; \
read -r -p "ALIYUN_DNS_DOMAIN (wildcard base, e.g. libs.websoft9.cn) [$$cur_domain]: " input_domain; input_domain="$${input_domain:-$$cur_domain}"; \
if [ -z "$$input_id" ] || [ -z "$$input_secret" ]; then echo "access key id and secret are required" >&2; exit 1; fi; \
if [ -n "$$input_domain" ]; then \
printf "ALIYUN_ACCESS_KEY_ID=%s\nALIYUN_ACCESS_KEY_SECRET=%s\nALIYUN_DNS_DOMAIN=%s\n" "$$input_id" "$$input_secret" "$$input_domain" > "$$file"; \
else \
printf "ALIYUN_ACCESS_KEY_ID=%s\nALIYUN_ACCESS_KEY_SECRET=%s\n" "$$input_id" "$$input_secret" > "$$file"; \
fi; \
elif [ "$$provider" = "dockerhub" ]; then \
cur_user=""; cur_token=""; cur_org=""; \
if [ -f "$$file" ]; then \
cur_user="$$(grep -E '^DOCKERHUB_USERNAME=' "$$file" | cut -d= -f2-)"; \
cur_token="$$(grep -E '^DOCKERHUB_TOKEN=' "$$file" | cut -d= -f2-)"; \
cur_org="$$(grep -E '^DOCKERHUB_ORG=' "$$file" | cut -d= -f2-)"; \
fi; \
read -r -p "DOCKERHUB_USERNAME [$$cur_user]: " input_user; input_user="$${input_user:-$$cur_user}"; \
read -r -s -p "DOCKERHUB_TOKEN [keep existing]: " input_token; echo; input_token="$${input_token:-$$cur_token}"; \
read -r -p "DOCKERHUB_ORG (optional default push namespace) [$$cur_org]: " input_org; input_org="$${input_org:-$$cur_org}"; \
if [ -z "$$input_user" ] || [ -z "$$input_token" ]; then echo "username and token are required" >&2; exit 1; fi; \
if [ -n "$$input_org" ]; then \
printf "DOCKERHUB_USERNAME=%s\nDOCKERHUB_TOKEN=%s\nDOCKERHUB_ORG=%s\n" "$$input_user" "$$input_token" "$$input_org" > "$$file"; \
else \
read -r -s -p "DOCKERHUB_TOKEN: " input_token; echo; \
if [ -z "$$input_user" ] || [ -z "$$input_token" ]; then echo "username and token are required" >&2; exit 1; fi; \
printf "DOCKERHUB_USERNAME=%s\nDOCKERHUB_TOKEN=%s\n" "$$input_user" "$$input_token" > "$$file"; \
fi; \
else \
Expand Down
14 changes: 13 additions & 1 deletion Notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,17 @@ Docker Model Runner
InfluxDB 开源时序数据库
OpenClaw
Canvas LMS
laravel
Semaphore


cloudreve,compreface,commafeed, coze,dashy,ejbca, frigate, falcon

Qdrant
e2e test for: vaultwarden, varnish
varnish not have config file
pangolin.net
RustDesk
elizaOS
Nuclear

port define?
11 changes: 11 additions & 0 deletions apps/activemq/variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
]
}
],
"access": {
"defaultScheme": "http",
"web": {
"port": 8161,
"path": "/"
},
"admin": {
"port": 8161,
"path": "/admin"
}
},
"requirements": {
"cpu": "1",
"memory": "2",
Expand Down
Loading
Loading