@steve31415/baselib 3.3.0 → 3.3.1
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/dist/deploy/deploy.js +3 -6
- package/dist/deploy/plan.d.ts +27 -3
- package/dist/deploy/plan.js +55 -5
- package/dist/deploy/rollback.js +3 -6
- package/package.json +1 -1
package/dist/deploy/deploy.js
CHANGED
|
@@ -12,7 +12,7 @@ import { DEFAULT_ACCOUNT, loadConfig, loadHooks } from './config.js';
|
|
|
12
12
|
import { must, mustJson } from './exec.js';
|
|
13
13
|
import { openEvidence, PhaseTimer } from './evidence.js';
|
|
14
14
|
import { acquireLock, clearHold, publishRetainedAssets, readHold, readReleaseRecord, releaseLock, verifyLock, writeReleaseRecord, } from './gcs.js';
|
|
15
|
-
import { consistentBaseSha,
|
|
15
|
+
import { consistentBaseSha, readServingState, SHA40 } from './plan.js';
|
|
16
16
|
const RELEASE_DEADLINE_MS = 25 * 60_000;
|
|
17
17
|
const IMAGE_DIGEST = /@sha256:[0-9a-f]{64}$/;
|
|
18
18
|
export async function runDeploy(options) {
|
|
@@ -87,12 +87,9 @@ export async function runDeploy(options) {
|
|
|
87
87
|
}
|
|
88
88
|
const serving = [];
|
|
89
89
|
for (const service of config.services) {
|
|
90
|
-
const described = await
|
|
91
|
-
'run', 'services', 'describe', service.name,
|
|
92
|
-
...gcloudBase, '--region', config.region, '--format=json',
|
|
93
|
-
]);
|
|
90
|
+
const { described, state } = await readServingState(runner, gcloudBase, config.region, service.name);
|
|
94
91
|
await evidence.save(`${service.name}-before.json`, JSON.stringify(described, null, 2));
|
|
95
|
-
serving.push(
|
|
92
|
+
serving.push(state);
|
|
96
93
|
}
|
|
97
94
|
const base = consistentBaseSha(serving, sha);
|
|
98
95
|
if (!base.ok)
|
package/dist/deploy/plan.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServingState } from './types.js';
|
|
1
|
+
import type { Runner, ServingState } from './types.js';
|
|
2
2
|
export declare const SHA40: RegExp;
|
|
3
3
|
/** Lock staleness is judged from the GCS object's server-side timeCreated,
|
|
4
4
|
* never from anything the lock writer wrote (its clock may be wrong). */
|
|
@@ -41,8 +41,32 @@ export declare function pickRevisionForSha(revisions: unknown[], targetSha: stri
|
|
|
41
41
|
revision: string;
|
|
42
42
|
imageDigest: string;
|
|
43
43
|
} | null;
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/** The one revision carrying 100% of traffic in a `gcloud run services
|
|
45
|
+
* describe --format=json`, from *observed* traffic (`status.traffic`, never
|
|
46
|
+
* `spec.traffic`, which is only the desired state). */
|
|
47
|
+
export declare function servingRevisionName(service: string, described: unknown): string;
|
|
48
|
+
/**
|
|
49
|
+
* Extract serving state from a `gcloud run services describe --format=json`.
|
|
50
|
+
*
|
|
51
|
+
* The serving revision's BUILD_ID and image are read from the service
|
|
52
|
+
* template only when that revision IS the latest created one. When it is
|
|
53
|
+
* not — a previous release created a revision that never took traffic (a
|
|
54
|
+
* failed secret-access check, a crashed startup, an aborted deploy) — the
|
|
55
|
+
* template describes the *wrong* revision, so the caller must pass that
|
|
56
|
+
* revision's own `gcloud run revisions describe --format=json` as
|
|
57
|
+
* `servingRevision` (readServingState does). Lesson 2026-09-04, watchdog2:
|
|
58
|
+
* a re-run after a failed traffic switch read the new SHA out of the
|
|
59
|
+
* template, logged "already serving", wrote the release record, and left
|
|
60
|
+
* the old revision serving.
|
|
61
|
+
*/
|
|
62
|
+
export declare function servingStateOf(service: string, described: unknown, servingRevision?: unknown): ServingState;
|
|
63
|
+
/** Describe a service and resolve its serving state, describing the serving
|
|
64
|
+
* revision too whenever the template no longer belongs to it. Returns the
|
|
65
|
+
* raw service description as well, for evidence. */
|
|
66
|
+
export declare function readServingState(runner: Runner, gcloudBase: string[], region: string, service: string): Promise<{
|
|
67
|
+
described: unknown;
|
|
68
|
+
state: ServingState;
|
|
69
|
+
}>;
|
|
46
70
|
export declare function contentTypeFor(path: string): string;
|
|
47
71
|
/** Parse release-record listing rows into {sha, at} entries. */
|
|
48
72
|
export declare function releaseEntriesFrom(rows: {
|
package/dist/deploy/plan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pure decision logic for pw-deploy/pw-rollback: everything here is
|
|
2
2
|
// side-effect-free so the concurrency-sensitive choices (lock staleness,
|
|
3
3
|
// rollback targeting, serving-state consistency) are unit-tested directly.
|
|
4
|
+
import { mustJson } from './exec.js';
|
|
4
5
|
export const SHA40 = /^[0-9a-f]{40}$/;
|
|
5
6
|
/** Lock staleness is judged from the GCS object's server-side timeCreated,
|
|
6
7
|
* never from anything the lock writer wrote (its clock may be wrong). */
|
|
@@ -101,22 +102,71 @@ export function pickRevisionForSha(revisions, targetSha) {
|
|
|
101
102
|
matches.sort((a, b) => b.created - a.created);
|
|
102
103
|
return matches[0] ? { revision: matches[0].revision, imageDigest: matches[0].imageDigest } : null;
|
|
103
104
|
}
|
|
104
|
-
/**
|
|
105
|
-
|
|
105
|
+
/** The one revision carrying 100% of traffic in a `gcloud run services
|
|
106
|
+
* describe --format=json`, from *observed* traffic (`status.traffic`, never
|
|
107
|
+
* `spec.traffic`, which is only the desired state). */
|
|
108
|
+
export function servingRevisionName(service, described) {
|
|
106
109
|
const d = described;
|
|
107
110
|
const active = (d.status?.traffic ?? []).filter((t) => (t.percent ?? 0) === 100);
|
|
108
111
|
if (active.length !== 1 || !active[0].revisionName) {
|
|
109
112
|
throw new Error(`${service}: expected exactly one 100% traffic target`);
|
|
110
113
|
}
|
|
111
|
-
|
|
114
|
+
return active[0].revisionName;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extract serving state from a `gcloud run services describe --format=json`.
|
|
118
|
+
*
|
|
119
|
+
* The serving revision's BUILD_ID and image are read from the service
|
|
120
|
+
* template only when that revision IS the latest created one. When it is
|
|
121
|
+
* not — a previous release created a revision that never took traffic (a
|
|
122
|
+
* failed secret-access check, a crashed startup, an aborted deploy) — the
|
|
123
|
+
* template describes the *wrong* revision, so the caller must pass that
|
|
124
|
+
* revision's own `gcloud run revisions describe --format=json` as
|
|
125
|
+
* `servingRevision` (readServingState does). Lesson 2026-09-04, watchdog2:
|
|
126
|
+
* a re-run after a failed traffic switch read the new SHA out of the
|
|
127
|
+
* template, logged "already serving", wrote the release record, and left
|
|
128
|
+
* the old revision serving.
|
|
129
|
+
*/
|
|
130
|
+
export function servingStateOf(service, described, servingRevision) {
|
|
131
|
+
const d = described;
|
|
132
|
+
const revision = servingRevisionName(service, described);
|
|
133
|
+
const latest = d.status?.latestCreatedRevisionName;
|
|
134
|
+
let container;
|
|
135
|
+
if (latest === undefined || latest === revision) {
|
|
136
|
+
container = d.spec?.template?.spec?.containers?.[0];
|
|
137
|
+
}
|
|
138
|
+
else if (servingRevision !== undefined) {
|
|
139
|
+
container = servingRevision.spec?.containers?.[0];
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
throw new Error(`${service}: traffic serves ${revision} but the latest revision is ${latest}; ` +
|
|
143
|
+
'the serving revision must be described separately');
|
|
144
|
+
}
|
|
145
|
+
const env = container?.env ?? [];
|
|
112
146
|
const buildSha = env.find((e) => e.name === 'BUILD_ID')?.value ?? null;
|
|
113
147
|
return {
|
|
114
148
|
service,
|
|
115
|
-
revision
|
|
149
|
+
revision,
|
|
116
150
|
buildSha: buildSha !== null && SHA40.test(buildSha) ? buildSha : null,
|
|
117
|
-
imageDigest:
|
|
151
|
+
imageDigest: container?.image ?? null,
|
|
118
152
|
};
|
|
119
153
|
}
|
|
154
|
+
/** Describe a service and resolve its serving state, describing the serving
|
|
155
|
+
* revision too whenever the template no longer belongs to it. Returns the
|
|
156
|
+
* raw service description as well, for evidence. */
|
|
157
|
+
export async function readServingState(runner, gcloudBase, region, service) {
|
|
158
|
+
const described = await mustJson(runner, 'gcloud', [
|
|
159
|
+
'run', 'services', 'describe', service, ...gcloudBase, '--region', region, '--format=json',
|
|
160
|
+
]);
|
|
161
|
+
const revision = servingRevisionName(service, described);
|
|
162
|
+
const latest = described.status?.latestCreatedRevisionName;
|
|
163
|
+
const servingRevision = latest !== undefined && latest !== revision
|
|
164
|
+
? await mustJson(runner, 'gcloud', [
|
|
165
|
+
'run', 'revisions', 'describe', revision, ...gcloudBase, '--region', region, '--format=json',
|
|
166
|
+
])
|
|
167
|
+
: undefined;
|
|
168
|
+
return { described, state: servingStateOf(service, described, servingRevision) };
|
|
169
|
+
}
|
|
120
170
|
const CONTENT_TYPES = {
|
|
121
171
|
'.js': 'text/javascript',
|
|
122
172
|
'.mjs': 'text/javascript',
|
package/dist/deploy/rollback.js
CHANGED
|
@@ -8,7 +8,7 @@ import { must, mustJson } from './exec.js';
|
|
|
8
8
|
import { openEvidence, PhaseTimer } from './evidence.js';
|
|
9
9
|
import { acquireLock, listReleaseRecords, placeHold, releaseLock } from './gcs.js';
|
|
10
10
|
import { DEFAULT_ACCOUNT, loadConfig } from './config.js';
|
|
11
|
-
import { pickRevisionForSha, releaseEntriesFrom, selectRollbackTarget,
|
|
11
|
+
import { pickRevisionForSha, releaseEntriesFrom, selectRollbackTarget, readServingState, SHA40, } from './plan.js';
|
|
12
12
|
export async function runRollback(options) {
|
|
13
13
|
const log = options.log ?? ((m) => console.error(`[pw-rollback] ${m}`));
|
|
14
14
|
const runner = options.runner;
|
|
@@ -39,11 +39,8 @@ export async function runRollback(options) {
|
|
|
39
39
|
timer.enter('target');
|
|
40
40
|
const serving = [];
|
|
41
41
|
for (const service of config.services) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
...gcloudBase, '--region', config.region, '--format=json',
|
|
45
|
-
]);
|
|
46
|
-
serving.push(servingStateOf(service.name, described));
|
|
42
|
+
const { state } = await readServingState(runner, gcloudBase, config.region, service.name);
|
|
43
|
+
serving.push(state);
|
|
47
44
|
}
|
|
48
45
|
const evidence = await openEvidence(config.app, requested ?? serving[0].buildSha ?? 'rollback');
|
|
49
46
|
evidenceDir = evidence.dir;
|
package/package.json
CHANGED