Durable @next integration-line publish: on next pushes, compute <patch+1>-next.<pipeline#> prerelease versions (in-CI, uncommitted) and publish @mosaicstack/* under the next dist-tag; gateway image sha-only on next. Strict guardrails: next-only, never writes latest, never tags from next; main path unchanged. PR-event CI 1631 fully green + review-of-record APPROVE (head b1a887a2). Guardrails independently verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
297 lines
13 KiB
YAML
297 lines
13 KiB
YAML
# Build, publish npm packages, and push Docker images
|
|
# Runs on main for stable publishes and on next for integration-line prereleases/images
|
|
|
|
variables:
|
|
# Pre-baked CI base (see .woodpecker/ci-image.yml): node:24-alpine +
|
|
# toolchain + warm pnpm store. Kills the second cold install publish pays.
|
|
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
|
|
- &enable_pnpm 'corepack enable'
|
|
# Heavy kaniko image builds (~25 min) — gate them so a merge that only touches
|
|
# the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform
|
|
# images (gateway/appservice/web do not depend on @mosaicstack/mosaic). Releases
|
|
# (tags) always build everything. Exclude-list keeps the default SAFE: any
|
|
# non-excluded change still builds, so no transitive dep can silently go stale.
|
|
# (Woodpecker: `when` entries are OR'd; `path` applies to push/PR only — hence
|
|
# the separate `event: tag` entry.)
|
|
- &image_build_when
|
|
- event: tag
|
|
- event: [push, manual]
|
|
branch: main
|
|
path:
|
|
exclude:
|
|
- 'packages/mosaic/**'
|
|
- 'docs/**'
|
|
- '**/*.md'
|
|
- '.woodpecker/**'
|
|
- event: [push, manual]
|
|
branch: next
|
|
- &main_image_build_when
|
|
- event: tag
|
|
- event: [push, manual]
|
|
branch: main
|
|
path:
|
|
exclude:
|
|
- 'packages/mosaic/**'
|
|
- 'docs/**'
|
|
- '**/*.md'
|
|
- '.woodpecker/**'
|
|
|
|
when:
|
|
- branch: [main, next]
|
|
event: [push, manual, tag]
|
|
|
|
steps:
|
|
install:
|
|
image: *node_image
|
|
commands:
|
|
- corepack enable
|
|
# Resolve from the baked pnpm store instead of a cold network fetch.
|
|
- pnpm install --frozen-lockfile --prefer-offline
|
|
|
|
build:
|
|
image: *node_image
|
|
commands:
|
|
- *enable_pnpm
|
|
- pnpm build
|
|
depends_on:
|
|
- install
|
|
|
|
publish-npm:
|
|
image: *node_image
|
|
# Publish only when a publishable package changed (or on a release tag); a
|
|
# pure-docs merge runs no publish. Cheap step, but gated for cleanliness.
|
|
when:
|
|
- event: tag
|
|
- event: [push, manual]
|
|
branch: main
|
|
path:
|
|
include:
|
|
- 'packages/**'
|
|
environment:
|
|
NPM_TOKEN:
|
|
from_secret: gitea_token
|
|
commands:
|
|
- *enable_pnpm
|
|
# Configure auth for Gitea npm registry
|
|
- |
|
|
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
|
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
|
|
# Publish non-private packages to Gitea.
|
|
#
|
|
# The only publish failure we tolerate is "version already exists" —
|
|
# that legitimately happens when only some packages were bumped in
|
|
# the merge. Any other failure (registry 404, auth error, network
|
|
# error) MUST fail the pipeline loudly: the previous
|
|
# `|| echo "... continuing"` fallback silently hid a 404 from the
|
|
# Gitea org rename and caused every @mosaicstack/* publish to fall
|
|
# on the floor while CI still reported green.
|
|
- |
|
|
# Portable sh (Alpine ash) — avoid bashisms like PIPESTATUS.
|
|
set +e
|
|
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" publish --no-git-checks --access public >/tmp/publish.log 2>&1
|
|
EXIT=$?
|
|
set -e
|
|
cat /tmp/publish.log
|
|
if [ "$EXIT" -eq 0 ]; then
|
|
echo "[publish] all packages published successfully"
|
|
exit 0
|
|
fi
|
|
# Hard registry / auth / network errors → fatal. Match npm's own
|
|
# error lines specifically to avoid false positives on arbitrary
|
|
# log text that happens to contain "E404" etc.
|
|
if grep -qE "npm (error|ERR!) code (E404|E401|ENEEDAUTH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND)" /tmp/publish.log; then
|
|
echo "[publish] FATAL: registry/auth/network error detected — failing pipeline" >&2
|
|
exit 1
|
|
fi
|
|
# Only tolerate the explicit "version already published" case.
|
|
# npm returns this as E403 with body "You cannot publish over..."
|
|
# or EPUBLISHCONFLICT depending on version.
|
|
if grep -qE "EPUBLISHCONFLICT|You cannot publish over|previously published" /tmp/publish.log; then
|
|
echo "[publish] some packages already at this version — continuing (non-fatal)"
|
|
exit 0
|
|
fi
|
|
echo "[publish] FATAL: publish failed with unrecognized error — failing pipeline" >&2
|
|
exit 1
|
|
depends_on:
|
|
- build
|
|
|
|
publish-next-npm:
|
|
image: *node_image
|
|
# Durable @next integration-line publish. Runs only on next; never writes
|
|
# the latest dist-tag and never commits the computed prerelease versions.
|
|
when:
|
|
- event: [push, manual]
|
|
branch: next
|
|
environment:
|
|
NPM_TOKEN:
|
|
from_secret: gitea_token
|
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
CI_PIPELINE_NUMBER: ${CI_PIPELINE_NUMBER}
|
|
commands:
|
|
- *enable_pnpm
|
|
- |
|
|
if [ "$CI_COMMIT_BRANCH" != "next" ]; then
|
|
echo "[publish-next] FATAL: publish-next-npm may only run on next (got '$CI_COMMIT_BRANCH')" >&2
|
|
exit 1
|
|
fi
|
|
if [ -z "$CI_PIPELINE_NUMBER" ]; then
|
|
echo "[publish-next] FATAL: CI_PIPELINE_NUMBER is required for prerelease versioning" >&2
|
|
exit 1
|
|
fi
|
|
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
|
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
|
|
DIST_TAGS_JSON="$(npm view @mosaicstack/mosaic dist-tags --registry https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ --json)"
|
|
DIST_TAGS_JSON="$DIST_TAGS_JSON" node -e 'const tags = JSON.parse(process.env.DIST_TAGS_JSON || "{}"); if (!tags || typeof tags !== "object" || !Object.hasOwn(tags, "latest")) { throw new Error("Gitea npm registry did not return a usable dist-tags object"); } console.log("[publish-next] registry dist-tags OK: latest=" + tags.latest);'
|
|
node <<'NODE'
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
|
const roots = ['apps', 'packages', 'plugins'];
|
|
const updated = [];
|
|
|
|
function walk(dir) {
|
|
if (!fs.existsSync(dir)) return;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
const packagePath = path.join(fullPath, 'package.json');
|
|
if (fs.existsSync(packagePath)) updatePackage(packagePath);
|
|
walk(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updatePackage(packagePath) {
|
|
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
if (!manifest.name?.startsWith('@mosaicstack/') || manifest.private) return;
|
|
const stableMatch = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(manifest.version);
|
|
if (!stableMatch) {
|
|
throw new Error(manifest.name + " has unsupported semver version '" + manifest.version + "'");
|
|
}
|
|
const [, major, minor, patch] = stableMatch;
|
|
const oldVersion = manifest.version;
|
|
manifest.version = major + '.' + minor + '.' + (Number(patch) + 1) + '-next.' + pipelineNumber;
|
|
fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
|
updated.push(manifest.name + ' ' + oldVersion + ' -> ' + manifest.version);
|
|
}
|
|
|
|
for (const root of roots) walk(root);
|
|
if (updated.length === 0) throw new Error('No publishable @mosaicstack/* packages found');
|
|
console.log('[publish-next] computed prerelease versions for ' + updated.length + ' packages:');
|
|
for (const line of updated) console.log('[publish-next] ' + line);
|
|
NODE
|
|
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" --filter "!@mosaicstack/mosaic-as" publish --no-git-checks --access public --tag next
|
|
EXPECTED_VERSION="$(node -p "require('./packages/mosaic/package.json').version")"
|
|
RESOLVED_VERSION="$(npm view @mosaicstack/mosaic@next version --registry https://git.mosaicstack.dev/api/packages/mosaicstack/npm/)"
|
|
if [ "$RESOLVED_VERSION" != "$EXPECTED_VERSION" ]; then
|
|
echo "[publish-next] FATAL: @mosaicstack/mosaic@next resolved '$RESOLVED_VERSION', expected '$EXPECTED_VERSION'" >&2
|
|
exit 1
|
|
fi
|
|
echo "[publish-next] @mosaicstack/mosaic@next resolves to $RESOLVED_VERSION"
|
|
depends_on:
|
|
- build
|
|
|
|
# TODO: Uncomment when ready to publish to npmjs.org
|
|
# publish-npmjs:
|
|
# image: *node_image
|
|
# environment:
|
|
# NPM_TOKEN:
|
|
# from_secret: npmjs_token
|
|
# commands:
|
|
# - *enable_pnpm
|
|
# - apk add --no-cache jq bash
|
|
# - bash scripts/publish-npmjs.sh
|
|
# depends_on:
|
|
# - build
|
|
# when:
|
|
# - event: [tag]
|
|
|
|
build-gateway:
|
|
image: gcr.io/kaniko-project/executor:debug
|
|
when: *image_build_when
|
|
environment:
|
|
REGISTRY_USER:
|
|
from_secret: gitea_username
|
|
REGISTRY_PASS:
|
|
from_secret: gitea_password
|
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
|
commands:
|
|
- mkdir -p /kaniko/.docker
|
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
|
- |
|
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
|
|
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
|
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
echo "[publish] FATAL: next gateway publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
|
|
exit 1
|
|
fi
|
|
echo "[publish] next gateway publish is sha-only"
|
|
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:latest"
|
|
elif [ -z "$CI_COMMIT_TAG" ]; then
|
|
echo "[publish] FATAL: gateway image publish may only run for main, next, or tag events" >&2
|
|
exit 1
|
|
fi
|
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG"
|
|
fi
|
|
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
|
|
depends_on:
|
|
- build
|
|
|
|
build-appservice:
|
|
image: gcr.io/kaniko-project/executor:debug
|
|
when: *main_image_build_when
|
|
environment:
|
|
REGISTRY_USER:
|
|
from_secret: gitea_username
|
|
REGISTRY_PASS:
|
|
from_secret: gitea_password
|
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
|
commands:
|
|
- mkdir -p /kaniko/.docker
|
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
|
- |
|
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
|
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
|
|
fi
|
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
|
|
fi
|
|
/kaniko/executor --context . --dockerfile docker/appservice.Dockerfile $DESTINATIONS
|
|
depends_on:
|
|
- build
|
|
|
|
build-web:
|
|
image: gcr.io/kaniko-project/executor:debug
|
|
when: *main_image_build_when
|
|
environment:
|
|
REGISTRY_USER:
|
|
from_secret: gitea_username
|
|
REGISTRY_PASS:
|
|
from_secret: gitea_password
|
|
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
|
|
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
|
|
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
|
|
commands:
|
|
- mkdir -p /kaniko/.docker
|
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
|
- |
|
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
|
fi
|
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
|
fi
|
|
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
|
|
depends_on:
|
|
- build
|