@devopsplaybook.io/common-utils 1.6.0-beta.15.02b378d → 1.7.0-beta.16.179c4ff
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/reusable-merge-build.yml +147 -91
- package/.github/workflows/reusable-pr-verify.yml +19 -4
- package/AGENTS.md +4 -2
- package/README.md +42 -2
- package/package.json +1 -1
|
@@ -4,17 +4,17 @@ on:
|
|
|
4
4
|
workflow_call:
|
|
5
5
|
inputs:
|
|
6
6
|
docker_platforms:
|
|
7
|
-
description: "Docker platforms
|
|
7
|
+
description: "Docker platforms, used only by the fallback build when no PR image can be promoted"
|
|
8
8
|
required: false
|
|
9
9
|
type: string
|
|
10
10
|
default: "linux/arm64/v8,linux/amd64"
|
|
11
11
|
node_app_directories:
|
|
12
|
-
description: "
|
|
12
|
+
description: "Deprecated and unused. Kept so existing callers stay valid: the merge build runs no build, lint or test."
|
|
13
13
|
required: false
|
|
14
14
|
type: string
|
|
15
15
|
default: ""
|
|
16
16
|
node_version:
|
|
17
|
-
description: "Node.js
|
|
17
|
+
description: "Deprecated and unused. Kept so existing callers stay valid: the merge build runs no Node.js job."
|
|
18
18
|
required: false
|
|
19
19
|
type: string
|
|
20
20
|
default: "22"
|
|
@@ -28,84 +28,18 @@ on:
|
|
|
28
28
|
QUALITY_DASHBOARD_URL:
|
|
29
29
|
required: false
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
working-directory: ${{ matrix.app }}
|
|
41
|
-
steps:
|
|
42
|
-
- uses: actions/checkout@v7
|
|
43
|
-
|
|
44
|
-
- name: Setup Node.js
|
|
45
|
-
uses: actions/setup-node@v7
|
|
46
|
-
with:
|
|
47
|
-
node-version: ${{ inputs.node_version }}
|
|
48
|
-
cache: "npm"
|
|
49
|
-
cache-dependency-path: ${{ matrix.app }}/package-lock.json
|
|
50
|
-
|
|
51
|
-
- name: Install dependencies
|
|
52
|
-
run: npm ci
|
|
53
|
-
|
|
54
|
-
- name: Build
|
|
55
|
-
run: npm run build
|
|
56
|
-
|
|
57
|
-
- name: Lint
|
|
58
|
-
run: npm run lint
|
|
59
|
-
|
|
60
|
-
- name: Test
|
|
61
|
-
run: npm run test
|
|
62
|
-
|
|
63
|
-
- name: Zip coverage and upload to Quality Dashboard
|
|
64
|
-
if: always()
|
|
65
|
-
env:
|
|
66
|
-
QUALITY_DASHBOARD_URL: ${{ secrets.QUALITY_DASHBOARD_URL }}
|
|
67
|
-
QUALITY_DASHBOARD_TOKEN: ${{ secrets.QUALITY_DASHBOARD_TOKEN }}
|
|
68
|
-
run: |
|
|
69
|
-
APP_NAME="${{ matrix.app }}"
|
|
70
|
-
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
71
|
-
REPORT_KEY="$(echo "$APP_NAME" | sed 's/[^a-zA-Z0-9._:\-\/]/_/g')_coverage"
|
|
72
|
-
|
|
73
|
-
if [ -d coverage ]; then
|
|
74
|
-
if [ -z "$QUALITY_DASHBOARD_URL" ] || [ -z "$QUALITY_DASHBOARD_TOKEN" ]; then
|
|
75
|
-
echo "Quality Dashboard URL or token not set, skipping upload"
|
|
76
|
-
exit 0
|
|
77
|
-
fi
|
|
78
|
-
# Zip coverage contents at root level so the jest processor
|
|
79
|
-
# finds coverage-final.json / clover.xml directly
|
|
80
|
-
(cd coverage && zip -r "${{ runner.temp }}/coverage.zip" .)
|
|
81
|
-
|
|
82
|
-
META=$(jq -n \
|
|
83
|
-
--arg key "$REPORT_KEY" \
|
|
84
|
-
--arg pkg "$PACKAGE_NAME" \
|
|
85
|
-
'{
|
|
86
|
-
key: $key,
|
|
87
|
-
displayName: "Coverage: \($pkg)",
|
|
88
|
-
processor: "jest"
|
|
89
|
-
}')
|
|
90
|
-
|
|
91
|
-
echo "Uploading coverage: $REPORT_KEY"
|
|
92
|
-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
93
|
-
-X POST "$QUALITY_DASHBOARD_URL/api/reports/" \
|
|
94
|
-
-H "x-upload-token: $QUALITY_DASHBOARD_TOKEN" \
|
|
95
|
-
-F "meta=$META" \
|
|
96
|
-
-F "file=@${{ runner.temp }}/coverage.zip" \
|
|
97
|
-
--max-time 30)
|
|
98
|
-
|
|
99
|
-
if [ "$HTTP_CODE" = "201" ]; then
|
|
100
|
-
echo "\u2713 Coverage uploaded ($HTTP_CODE)"
|
|
101
|
-
else
|
|
102
|
-
echo "\u26a0 Upload returned HTTP $HTTP_CODE"
|
|
103
|
-
fi
|
|
104
|
-
else
|
|
105
|
-
echo "No coverage/ directory found, skipping upload"
|
|
106
|
-
fi
|
|
31
|
+
# Immutable build strategy: the image validated on the pull request is promoted
|
|
32
|
+
# as-is, so merging runs no build, lint or test.
|
|
33
|
+
#
|
|
34
|
+
# `contents: read` only. Callers run with the default read-only token, whose
|
|
35
|
+
# ceiling is `contents: read, pull-requests: none`, so requesting anything more
|
|
36
|
+
# here makes the caller fail to start with "Invalid workflow file". The PR is
|
|
37
|
+
# therefore resolved without needing pull-request scopes.
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
107
40
|
|
|
108
|
-
|
|
41
|
+
jobs:
|
|
42
|
+
promote-image:
|
|
109
43
|
runs-on: ubuntu-latest
|
|
110
44
|
steps:
|
|
111
45
|
- uses: actions/checkout@v7
|
|
@@ -122,20 +56,142 @@ jobs:
|
|
|
122
56
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
123
57
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
|
124
58
|
|
|
125
|
-
- name:
|
|
59
|
+
- name: Resolve the image validated by the merged PR
|
|
60
|
+
id: resolve
|
|
61
|
+
env:
|
|
62
|
+
GH_TOKEN: ${{ github.token }}
|
|
63
|
+
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
126
64
|
run: |
|
|
127
65
|
set -e
|
|
128
|
-
SERVICE_NAME=$(
|
|
129
|
-
SERVICE_VERSION=$(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
66
|
+
SERVICE_NAME=$(jq -r '.name' package.json)
|
|
67
|
+
SERVICE_VERSION=$(jq -r '.version' package.json)
|
|
68
|
+
IMAGE="${DOCKER_HUB_USERNAME}/${SERVICE_NAME}"
|
|
69
|
+
|
|
70
|
+
PR_NUMBER=""
|
|
71
|
+
PR_SOURCE="none"
|
|
72
|
+
|
|
73
|
+
# Preferred: ask the API which PR introduced this commit. This needs
|
|
74
|
+
# `pull-requests: read`, which callers on the default token are not
|
|
75
|
+
# granted, so a failure here is expected rather than exceptional.
|
|
76
|
+
PR_NUMBER=$(curl -sfL \
|
|
77
|
+
-H "Authorization: Bearer ${GH_TOKEN}" \
|
|
78
|
+
-H "Accept: application/vnd.github+json" \
|
|
79
|
+
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \
|
|
80
|
+
| jq -r '.[0].number // empty' || true)
|
|
81
|
+
if [ -n "${PR_NUMBER}" ]; then
|
|
82
|
+
PR_SOURCE="api"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Fallback: parse the commit title, where GitHub writes the PR number
|
|
86
|
+
# for squash ("<title> (#123)") and merge ("Merge pull request #123")
|
|
87
|
+
# commits. Only the title is read, so a "fixes #99" reference in the
|
|
88
|
+
# commit body cannot select the wrong pull request.
|
|
89
|
+
if [ -z "${PR_NUMBER}" ]; then
|
|
90
|
+
TITLE=$(git log -1 --pretty=%s)
|
|
91
|
+
PR_NUMBER=$(printf '%s' "${TITLE}" | grep -oE '\(#[0-9]+\)' | tail -1 | tr -d '(#)' || true)
|
|
92
|
+
if [ -z "${PR_NUMBER}" ]; then
|
|
93
|
+
PR_NUMBER=$(printf '%s' "${TITLE}" | grep -oE 'Merge pull request #[0-9]+' | grep -oE '[0-9]+' | head -1 || true)
|
|
94
|
+
fi
|
|
95
|
+
if [ -n "${PR_NUMBER}" ]; then
|
|
96
|
+
PR_SOURCE="commit-title"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
SOURCE_IMAGE=""
|
|
101
|
+
if [ -z "${PR_NUMBER}" ]; then
|
|
102
|
+
echo "::warning::No pull request found for ${GITHUB_SHA}, falling back to a build."
|
|
103
|
+
else
|
|
104
|
+
CANDIDATE="${IMAGE}:beta-pr-${PR_NUMBER}"
|
|
105
|
+
if docker buildx imagetools inspect "${CANDIDATE}" > /dev/null 2>&1; then
|
|
106
|
+
SOURCE_IMAGE="${CANDIDATE}"
|
|
107
|
+
else
|
|
108
|
+
echo "::warning::${CANDIDATE} not found in the registry, falling back to a build."
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ -n "${SOURCE_IMAGE}" ]; then
|
|
113
|
+
STRATEGY="retag"
|
|
114
|
+
else
|
|
115
|
+
STRATEGY="build"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
echo "image=${IMAGE}"
|
|
120
|
+
echo "service_name=${SERVICE_NAME}"
|
|
121
|
+
echo "service_version=${SERVICE_VERSION}"
|
|
122
|
+
echo "pr_number=${PR_NUMBER}"
|
|
123
|
+
echo "pr_source=${PR_SOURCE}"
|
|
124
|
+
echo "source_image=${SOURCE_IMAGE}"
|
|
125
|
+
echo "strategy=${STRATEGY}"
|
|
126
|
+
} >> "${GITHUB_OUTPUT}"
|
|
127
|
+
|
|
128
|
+
echo "Service: ${SERVICE_NAME} ${SERVICE_VERSION}"
|
|
129
|
+
echo "PR: ${PR_NUMBER:-none} (resolved via ${PR_SOURCE})"
|
|
130
|
+
echo "Source: ${SOURCE_IMAGE:-none}"
|
|
131
|
+
echo "Strategy: ${STRATEGY}"
|
|
132
|
+
|
|
133
|
+
- name: Promote the PR image to the release tags
|
|
134
|
+
if: steps.resolve.outputs.strategy == 'retag'
|
|
135
|
+
env:
|
|
136
|
+
RELEASE_IMAGE: ${{ steps.resolve.outputs.image }}
|
|
137
|
+
RELEASE_VERSION: ${{ steps.resolve.outputs.service_version }}
|
|
138
|
+
SOURCE_IMAGE: ${{ steps.resolve.outputs.source_image }}
|
|
139
|
+
MERGED_PR_NUMBER: ${{ steps.resolve.outputs.pr_number }}
|
|
140
|
+
run: |
|
|
141
|
+
set -e
|
|
142
|
+
MAJOR=$(echo "${RELEASE_VERSION}" | cut -f1 -d".")
|
|
143
|
+
MINOR=$(echo "${RELEASE_VERSION}" | cut -f1-2 -d".")
|
|
144
|
+
|
|
145
|
+
SOURCE_DIGEST=$(docker buildx imagetools inspect "${SOURCE_IMAGE}" --format '{{.Manifest.Digest}}')
|
|
146
|
+
|
|
147
|
+
echo "Promoting ${SOURCE_IMAGE} (PR #${MERGED_PR_NUMBER})"
|
|
148
|
+
echo " digest ${SOURCE_DIGEST}"
|
|
149
|
+
echo " -> ${RELEASE_IMAGE}:${RELEASE_VERSION}"
|
|
150
|
+
echo " -> ${RELEASE_IMAGE}:${MAJOR}"
|
|
151
|
+
echo " -> ${RELEASE_IMAGE}:${MINOR}"
|
|
152
|
+
echo " -> ${RELEASE_IMAGE}:latest"
|
|
153
|
+
|
|
154
|
+
# --prefer-index=false forces a carbon copy of the source manifest, so
|
|
155
|
+
# the released image keeps the exact digest validated on the pull
|
|
156
|
+
# request. Without it, a single-platform source would be rewrapped into
|
|
157
|
+
# a new image index and the digest would change.
|
|
158
|
+
docker buildx imagetools create \
|
|
159
|
+
--prefer-index=false \
|
|
160
|
+
-t "${RELEASE_IMAGE}:${RELEASE_VERSION}" \
|
|
161
|
+
-t "${RELEASE_IMAGE}:${MAJOR}" \
|
|
162
|
+
-t "${RELEASE_IMAGE}:${MINOR}" \
|
|
163
|
+
-t "${RELEASE_IMAGE}:latest" \
|
|
164
|
+
"${SOURCE_IMAGE}"
|
|
165
|
+
|
|
166
|
+
# Fail loudly rather than publish an artifact that differs from the one
|
|
167
|
+
# tested on the pull request.
|
|
168
|
+
for TAG in "${RELEASE_VERSION}" "${MAJOR}" "${MINOR}" "latest"; do
|
|
169
|
+
PROMOTED_DIGEST=$(docker buildx imagetools inspect "${RELEASE_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}')
|
|
170
|
+
if [ "${PROMOTED_DIGEST}" != "${SOURCE_DIGEST}" ]; then
|
|
171
|
+
echo "::error::${RELEASE_IMAGE}:${TAG} has digest ${PROMOTED_DIGEST}, expected ${SOURCE_DIGEST}"
|
|
172
|
+
exit 1
|
|
173
|
+
fi
|
|
174
|
+
echo " verified ${RELEASE_IMAGE}:${TAG} -> ${PROMOTED_DIGEST}"
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
- name: Build and Push Docker Image (fallback)
|
|
178
|
+
if: steps.resolve.outputs.strategy == 'build'
|
|
179
|
+
env:
|
|
180
|
+
RELEASE_IMAGE: ${{ steps.resolve.outputs.image }}
|
|
181
|
+
RELEASE_VERSION: ${{ steps.resolve.outputs.service_version }}
|
|
182
|
+
DOCKER_PLATFORMS: ${{ inputs.docker_platforms }}
|
|
183
|
+
run: |
|
|
184
|
+
set -e
|
|
185
|
+
MAJOR=$(echo "${RELEASE_VERSION}" | cut -f1 -d".")
|
|
186
|
+
MINOR=$(echo "${RELEASE_VERSION}" | cut -f1-2 -d".")
|
|
187
|
+
|
|
188
|
+
echo "Building ${RELEASE_IMAGE}:${RELEASE_VERSION} from ${GITHUB_SHA}"
|
|
133
189
|
docker buildx build \
|
|
134
|
-
--platform ${
|
|
190
|
+
--platform "${DOCKER_PLATFORMS}" \
|
|
135
191
|
--push \
|
|
136
192
|
-f Dockerfile \
|
|
137
|
-
-t ${
|
|
138
|
-
-t ${
|
|
139
|
-
-t ${
|
|
140
|
-
-t ${
|
|
193
|
+
-t "${RELEASE_IMAGE}:latest" \
|
|
194
|
+
-t "${RELEASE_IMAGE}:${RELEASE_VERSION}" \
|
|
195
|
+
-t "${RELEASE_IMAGE}:${MAJOR}" \
|
|
196
|
+
-t "${RELEASE_IMAGE}:${MINOR}" \
|
|
141
197
|
.
|
|
@@ -123,13 +123,28 @@ jobs:
|
|
|
123
123
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
|
124
124
|
|
|
125
125
|
- name: Build and Push Docker Image
|
|
126
|
+
env:
|
|
127
|
+
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
128
|
+
DOCKER_PLATFORMS: ${{ inputs.docker_platforms }}
|
|
129
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
126
130
|
run: |
|
|
127
131
|
set -e
|
|
128
|
-
SERVICE_NAME=$(
|
|
129
|
-
|
|
132
|
+
SERVICE_NAME=$(jq -r '.name' package.json)
|
|
133
|
+
IMAGE="${DOCKER_HUB_USERNAME}/${SERVICE_NAME}"
|
|
134
|
+
|
|
135
|
+
# `beta-pr-<number>` is the immutable per-PR reference: it identifies
|
|
136
|
+
# exactly the artifact validated by this pull request, and the merge
|
|
137
|
+
# build promotes it as-is to the semantic version tags.
|
|
138
|
+
PR_TAG="${PR_NUMBER}"
|
|
139
|
+
if [ -z "${PR_TAG}" ]; then
|
|
140
|
+
PR_TAG="manual-${GITHUB_SHA::7}"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
echo "Building ${IMAGE}:beta-pr-${PR_TAG} and ${IMAGE}:beta"
|
|
130
144
|
docker buildx build \
|
|
131
|
-
--platform ${
|
|
145
|
+
--platform "${DOCKER_PLATFORMS}" \
|
|
132
146
|
--push \
|
|
133
147
|
-f Dockerfile \
|
|
134
|
-
-t ${
|
|
148
|
+
-t "${IMAGE}:beta-pr-${PR_TAG}" \
|
|
149
|
+
-t "${IMAGE}:beta" \
|
|
135
150
|
.
|
package/AGENTS.md
CHANGED
|
@@ -38,8 +38,8 @@ src/
|
|
|
38
38
|
reusable-npm-merge.yml # Lint + test + build + publish release to npm
|
|
39
39
|
reusable-npm-pr.yml # Lint + test + build + publish beta tag + comment PR
|
|
40
40
|
reusable-npm-upgrade.yml # npm-check-updates + auto PR
|
|
41
|
-
reusable-pr-verify.yml # Matrix Node.js + multi-platform Docker build
|
|
42
|
-
reusable-merge-build.yml #
|
|
41
|
+
reusable-pr-verify.yml # Matrix Node.js + multi-platform Docker build -> beta-pr-<PR> and beta
|
|
42
|
+
reusable-merge-build.yml # Promotes the PR image to the version tags on merge (no build/lint/test)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Key Conventions
|
|
@@ -53,6 +53,8 @@ src/
|
|
|
53
53
|
- **ModuleLogger pattern**: `StandardLogger` only exposes `createModuleLogger(name)`. DB modules call `logger.createModuleLogger("ModuleName")` internally. Never call `.info()` or `.error()` directly on a `StandardLogger`.
|
|
54
54
|
- **SQLite-first SQL**: Write SQL with `?` placeholders. The `DbUtils` facade and `DbUtilsNoTelemetry` module auto-convert to `$1, $2, ...` for Postgres via `convertToPostgresPlaceholders()`.
|
|
55
55
|
- **Migration convention**: SQL files named `init-NNNN.sql`. `init-0000.sql` must create the `metadata` table. Subsequent files are applied in lexicographic order; applied versions are tracked in `metadata` for idempotency.
|
|
56
|
+
- **Immutable Docker builds**: images are built once on the pull request (`reusable-pr-verify.yml` pushes `beta-pr-<PR number>` and `beta`) and promoted unchanged on merge (`reusable-merge-build.yml` writes `<version>`, `<major>`, `<minor>` and `latest`). The merge workflow runs no build, lint or test: it resolves the merged PR, carbon-copies the manifest with `docker buildx imagetools create --prefer-index=false`, then reads back each published digest and fails on a mismatch. It falls back to a full build only when no `beta-pr-<PR number>` image can be promoted. Image name and version come from the root `package.json`, and PR branches must be up to date with the default branch before merging.
|
|
57
|
+
- **Reusable workflow permissions ceiling**: `reusable-merge-build.yml` may request only `contents: read`. A called workflow cannot exceed its caller's permissions, and callers use the default read-only token whose ceiling is `contents: read, pull-requests: none`. Adding `pull-requests: read` does not fail here — it fails in every consuming repo at startup with `Invalid workflow file ... requesting 'pull-requests: read', but is only allowed 'pull-requests: none'`. `GET /repos/{owner}/{repo}/commits/{sha}/pulls` works with `contents: read`, so no extra scope is needed.
|
|
56
58
|
|
|
57
59
|
## Build and Verification
|
|
58
60
|
|
package/README.md
CHANGED
|
@@ -484,8 +484,8 @@ The `.github/workflows/` directory contains **reusable workflows** that other re
|
|
|
484
484
|
| **NPM Merge** | `reusable-npm-merge.yml` | `workflow_call` | Lint, test, build, and publish a release to npm on merge to main. Uploads coverage to Quality Dashboard. Only publishes if the version doesn't already exist. |
|
|
485
485
|
| **NPM PR** | `reusable-npm-pr.yml` | `workflow_call` | Lint, test, and build on PR. Publishes a **beta** version tagged `beta` and comments the PR with install instructions. |
|
|
486
486
|
| **NPM Upgrade** | `reusable-npm-upgrade.yml` | `workflow_call` | Runs `npm-check-updates -u`, bumps the patch version, and opens a PR. Supports monorepo sub-folders via `npm_services` input. |
|
|
487
|
-
| **PR Verify** | `reusable-pr-verify.yml` | `workflow_call` | Matrix build/lint/test for multiple Node.js apps plus multi-platform Docker build
|
|
488
|
-
| **Merge Build** | `reusable-merge-build.yml` | `workflow_call` |
|
|
487
|
+
| **PR Verify** | `reusable-pr-verify.yml` | `workflow_call` | Matrix build/lint/test for multiple Node.js apps, plus a multi-platform Docker build pushed as `beta-pr-<PR number>` and `beta`. For monorepos with Docker images. |
|
|
488
|
+
| **Merge Build** | `reusable-merge-build.yml` | `workflow_call` | Promotes the image validated by the merged PR to the `latest`, version, major and minor tags. Runs no build, lint or test. |
|
|
489
489
|
|
|
490
490
|
### Inputs and Secrets
|
|
491
491
|
|
|
@@ -517,6 +517,8 @@ The `.github/workflows/` directory contains **reusable workflows** that other re
|
|
|
517
517
|
| `node_app_directories` | No | `""` | JSON array of Node.js app directories |
|
|
518
518
|
| `node_version` | No | `"22"` | Node.js version |
|
|
519
519
|
|
|
520
|
+
`reusable-merge-build` declares the same inputs so existing callers keep working, but it only reads `docker_platforms`, and only on the build fallback path. `node_app_directories` and `node_version` are ignored there because merging runs no Node.js job.
|
|
521
|
+
|
|
520
522
|
| Secret | Required | Description |
|
|
521
523
|
| ------------------------- | -------- | ------------------------------ |
|
|
522
524
|
| `DOCKER_HUB_USERNAME` | Yes | Docker Hub username |
|
|
@@ -524,6 +526,44 @@ The `.github/workflows/` directory contains **reusable workflows** that other re
|
|
|
524
526
|
| `QUALITY_DASHBOARD_URL` | No | Quality Dashboard URL |
|
|
525
527
|
| `QUALITY_DASHBOARD_TOKEN` | No | Quality Dashboard upload token |
|
|
526
528
|
|
|
529
|
+
`reusable-merge-build` also still declares the Quality Dashboard secrets for backward compatibility, but no longer uploads coverage: each report is produced once, on the pull request.
|
|
530
|
+
|
|
531
|
+
### Immutable Docker Build Strategy
|
|
532
|
+
|
|
533
|
+
Docker images are built **once**, on the pull request, and **promoted** on merge. The artifact you test is bit-for-bit the artifact you release.
|
|
534
|
+
|
|
535
|
+
| Stage | Workflow | Registry tags written |
|
|
536
|
+
| ---------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
537
|
+
| Pull request | `reusable-pr-verify` | `beta-pr-<PR number>` (identifies this PR's artifact) and `beta` (moving pointer to the latest PR build) |
|
|
538
|
+
| Merge to default | `reusable-merge-build` | `<version>`, `<major>`, `<minor>` and `latest`, copied from `beta-pr-<PR number>` |
|
|
539
|
+
|
|
540
|
+
On merge the workflow:
|
|
541
|
+
|
|
542
|
+
1. Resolves the pull request behind the commit pushed to the default branch through `GET /repos/{owner}/{repo}/commits/{sha}/pulls`, falling back to the PR number in the commit **title** (`<title> (#123)` for squash and rebase, `Merge pull request #123` for merge commits). Only the title is parsed, so a `fixes #99` reference in the commit body cannot select the wrong pull request.
|
|
543
|
+
2. Carbon-copies the manifest with `docker buildx imagetools create --prefer-index=false`, which preserves the exact digest and the full multi-platform image index.
|
|
544
|
+
3. Reads back the digest of every tag it published and fails if one differs from the source.
|
|
545
|
+
|
|
546
|
+
The resolve step logs how it reached its answer, which is the first thing to check when a merge falls back to a build:
|
|
547
|
+
|
|
548
|
+
```text
|
|
549
|
+
Service: planner-llm-agent 0.2.0
|
|
550
|
+
PR: 2 (resolved via api)
|
|
551
|
+
Source: <namespace>/planner-llm-agent:beta-pr-2
|
|
552
|
+
Strategy: retag
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
`Strategy` is `retag` when the PR image was found and `build` when it was not.
|
|
556
|
+
|
|
557
|
+
No `npm ci`, build, lint or test runs on merge, and no Docker build happens unless promotion is impossible. The build fallback keeps releases working when a commit reaches the default branch with no matching `beta-pr-<PR number>` image, such as a direct push, a rebase merge (which leaves no PR number in the commit title), or a PR whose image was deleted from the registry.
|
|
558
|
+
|
|
559
|
+
> **Callers need no `permissions:` block.** `reusable-merge-build` requests only `contents: read`. A reusable workflow cannot ask for more than its caller is allowed, and the default read-only token ceiling is `contents: read, pull-requests: none` — requesting `pull-requests: read` makes the caller fail to start with `Invalid workflow file … The workflow is requesting 'pull-requests: read', but is only allowed 'pull-requests: none'`. The API lookup above works with `contents: read` alone.
|
|
560
|
+
|
|
561
|
+
The image name and version come from the root `package.json`, so bump the minor version in the pull request that carries the change.
|
|
562
|
+
|
|
563
|
+
> **Keep PR branches up to date with the default branch before merging.** The promoted image is the one built from the PR head, so if the default branch moved in the meantime the released artifact will not contain those commits.
|
|
564
|
+
|
|
565
|
+
Validated end to end on `planner-llm-agent` 0.2.0: the pull request pushed `beta-pr-2` and `beta`, the merge promoted that image in **40 seconds** with no build, and `beta-pr-2`, `beta`, `0.2.0`, `0.2`, `0` and `latest` all resolve to the single digest `sha256:710fed19…` with identical per-architecture digests.
|
|
566
|
+
|
|
527
567
|
### Adopting in Your Project
|
|
528
568
|
|
|
529
569
|
Create a caller workflow in `.github/workflows/main-build.yml`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devopsplaybook.io/common-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-beta.16.179c4ff",
|
|
4
4
|
"description": "Shared utility modules for devopsplaybook.io projects (DB, Config, OTel context, auth/users, notifications, LLM, system helpers)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Open Telemetry",
|