@git.zone/cli 6.4.0 → 6.6.0
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/assets/templates/asset_tspack_release_gitea/.gitea/workflows/release.yml +60 -0
- package/assets/templates/asset_tspack_release_gitea/scripts/gitzone-release-api.mjs +150 -0
- package/assets/templates/asset_tspack_release_gitea/scripts/gitzone-sealed-release.mjs +122 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/helpers.sealedrelease.d.ts +2 -0
- package/dist_ts/helpers.sealedrelease.js +14 -0
- package/dist_ts/mod_config/index.js +10 -2
- package/dist_ts/mod_format/formatters/assets.formatter.js +16 -1
- package/dist_ts/mod_release/helpers.releasebranch.d.ts +2 -0
- package/dist_ts/mod_release/helpers.releasebranch.js +37 -5
- package/dist_ts/mod_release/helpers.releasepublication.js +6 -3
- package/package.json +2 -1
- package/readme.md +56 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/helpers.sealedrelease.ts +13 -0
- package/ts/mod_config/index.ts +9 -1
- package/ts/mod_format/formatters/assets.formatter.ts +15 -0
- package/ts/mod_release/helpers.releasebranch.ts +74 -11
- package/ts/mod_release/helpers.releasepublication.ts +5 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Sealed release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: sealed-release-${-{ github.repository }-}-${-{ github.ref }-}
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
container:
|
|
16
|
+
image: code.foss.global/host.today/ht-docker-node:latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout exact source
|
|
19
|
+
uses: https://github.com/actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
persist-credentials: false
|
|
23
|
+
- name: Set up pinned Deno
|
|
24
|
+
uses: https://github.com/denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3
|
|
25
|
+
with:
|
|
26
|
+
deno-version: '{{assets.sealedRelease.denoVersion}}'
|
|
27
|
+
- name: Install frozen dependencies
|
|
28
|
+
run: pnpm install --frozen-lockfile
|
|
29
|
+
- name: Find the original retained release
|
|
30
|
+
id: retention
|
|
31
|
+
env:
|
|
32
|
+
GITHUB_TOKEN: ${-{ secrets.GITHUB_TOKEN }-}
|
|
33
|
+
run: node scripts/gitzone-sealed-release.mjs discover
|
|
34
|
+
- name: Build and seal new artifacts
|
|
35
|
+
if: steps.retention.outputs.retained != 'true'
|
|
36
|
+
id: prepared
|
|
37
|
+
run: node scripts/gitzone-sealed-release.mjs prepare
|
|
38
|
+
# Gitea's legacy Actions artifact protocol is supported by the v3 actions.
|
|
39
|
+
- name: Retain the complete output before publication
|
|
40
|
+
if: steps.retention.outputs.retained != 'true'
|
|
41
|
+
uses: https://github.com/actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20
|
|
42
|
+
with:
|
|
43
|
+
name: ${-{ steps.retention.outputs.artifact }-}
|
|
44
|
+
path: ${-{ steps.retention.outputs.output }-}/
|
|
45
|
+
if-no-files-found: error
|
|
46
|
+
retention-days: 90
|
|
47
|
+
- name: Download retained bytes into a fresh directory
|
|
48
|
+
uses: https://github.com/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
|
49
|
+
with:
|
|
50
|
+
name: ${-{ steps.retention.outputs.artifact }-}
|
|
51
|
+
path: dist_gitzone_retained
|
|
52
|
+
- name: Verify retained component and complete archives
|
|
53
|
+
env:
|
|
54
|
+
EXPECTED_MANIFEST: ${-{ steps.prepared.outputs.manifest }-}
|
|
55
|
+
run: node scripts/gitzone-sealed-release.mjs restore
|
|
56
|
+
- name: Publish or resume the verified draft
|
|
57
|
+
env:
|
|
58
|
+
GITHUB_TOKEN: ${-{ secrets.GITHUB_TOKEN }-}
|
|
59
|
+
EXPECTED_MANIFEST: ${-{ steps.prepared.outputs.manifest }-}
|
|
60
|
+
run: node scripts/gitzone-sealed-release.mjs publish
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Managed by GitZone's asset_tspack_release_gitea template.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const requireValue = (value, message) => { if (!value) throw new Error(message); };
|
|
5
|
+
const identifier = (value) => Number.isSafeInteger(value) && value > 0;
|
|
6
|
+
|
|
7
|
+
/** Gitea transport for retained, already-verified TsPack files. No compiler or packer runs here. */
|
|
8
|
+
export class GiteaSealedRelease {
|
|
9
|
+
constructor({ server, repository, token, fetch: fetchArg = globalThis.fetch }) {
|
|
10
|
+
const origin = new URL(server);
|
|
11
|
+
requireValue(origin.protocol === 'https:' && !origin.username && !origin.password &&
|
|
12
|
+
origin.pathname === '/' && !origin.search && !origin.hash &&
|
|
13
|
+
/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repository) && typeof token === 'string' && token.length > 0,
|
|
14
|
+
'Expected an HTTPS Gitea origin, repository and job token.');
|
|
15
|
+
this.origin = origin.origin;
|
|
16
|
+
this.base = `${origin.origin}/api/v1/repos/${repository}`;
|
|
17
|
+
this.token = token;
|
|
18
|
+
this.fetch = fetchArg;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async response(url, options = {}, expected = [200]) {
|
|
22
|
+
requireValue(new URL(url).origin === this.origin, 'Refusing to send a job token outside Gitea.');
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await this.fetch(url, { ...options, redirect: 'error',
|
|
26
|
+
signal: AbortSignal.timeout(15 * 60 * 1000),
|
|
27
|
+
headers: { ...options.headers, Authorization: `token ${this.token}` } });
|
|
28
|
+
} catch { throw new Error('Gitea request failed; retain this run and retry it.'); }
|
|
29
|
+
requireValue(expected.includes(response.status), `Unexpected Gitea HTTP status ${response.status}; retain this run and retry it.`);
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async json(path, options = {}, expected = [200]) {
|
|
34
|
+
const response = await this.response(`${this.base}${path}`, options, expected);
|
|
35
|
+
if (response.status === 404) return null;
|
|
36
|
+
const chunks = []; let size = 0;
|
|
37
|
+
for await (const chunk of response.body) {
|
|
38
|
+
size += chunk.length;
|
|
39
|
+
requireValue(size <= 4 * 1024 * 1024, 'Gitea metadata exceeds its limit.');
|
|
40
|
+
chunks.push(chunk);
|
|
41
|
+
}
|
|
42
|
+
try { return JSON.parse(Buffer.concat(chunks).toString('utf8')); }
|
|
43
|
+
catch { throw new Error('Gitea returned invalid metadata.'); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findRetention({ name, runId, tag }) {
|
|
47
|
+
requireValue(/^tspack-(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(name) && identifier(runId) && /^v\d+\.\d+\.\d+$/.test(tag), 'Invalid release retention identity.');
|
|
48
|
+
const result = await this.json(`/actions/artifacts?name=${name}`);
|
|
49
|
+
requireValue(Array.isArray(result?.artifacts) && result.artifacts.every((item) => item.name === name) &&
|
|
50
|
+
result.total_count === result.artifacts.length && result.artifacts.length <= 1,
|
|
51
|
+
'Release retention is incomplete or ambiguous; inspect the original run.');
|
|
52
|
+
const artifact = result.artifacts[0];
|
|
53
|
+
if (artifact) {
|
|
54
|
+
requireValue(identifier(artifact.id) && identifier(artifact.workflow_run?.id) &&
|
|
55
|
+
artifact.workflow_run.head_sha === name.slice(7) && artifact.expired === false && artifact.size_in_bytes > 0,
|
|
56
|
+
'Retained release expired or is incomplete; recover its original bytes.');
|
|
57
|
+
requireValue(artifact.workflow_run.id === runId, `Retained release belongs to run ${artifact.workflow_run.id}; rerun that original run.`);
|
|
58
|
+
return artifact.id;
|
|
59
|
+
}
|
|
60
|
+
requireValue(await this.json(`/releases/tags/${tag}`, {}, [200, 404]) === null,
|
|
61
|
+
'A release exists without retained artifacts in this run; recover the original bytes before retrying.');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assertRelease(release, tag, commit) {
|
|
66
|
+
requireValue(identifier(release?.id) && release.tag_name === tag && release.target_commitish === commit &&
|
|
67
|
+
typeof release.draft === 'boolean' && release.prerelease === false,
|
|
68
|
+
'Existing release does not match this sealed source identity.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async download(urlArg) {
|
|
72
|
+
let url = new URL(urlArg);
|
|
73
|
+
requireValue(url.origin === this.origin, 'Unexpected release attachment origin.');
|
|
74
|
+
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
75
|
+
requireValue(url.protocol === 'https:' && !url.username && !url.password, 'Unsafe attachment redirect.');
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await this.fetch(url.href, { redirect: 'manual', signal: AbortSignal.timeout(15 * 60 * 1000),
|
|
79
|
+
headers: url.origin === this.origin ? { Authorization: `token ${this.token}` } : {} });
|
|
80
|
+
} catch { throw new Error('Gitea attachment download failed.'); }
|
|
81
|
+
if (response.status === 200) return response;
|
|
82
|
+
requireValue([301, 302, 303, 307, 308].includes(response.status) && response.headers.get('location'), 'Unexpected attachment download response.');
|
|
83
|
+
await response.body?.cancel();
|
|
84
|
+
url = new URL(response.headers.get('location'), url);
|
|
85
|
+
}
|
|
86
|
+
throw new Error('Too many attachment redirects.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async verifyAssets(releaseId, files, complete) {
|
|
90
|
+
const assets = await this.json(`/releases/${releaseId}/assets`);
|
|
91
|
+
requireValue(Array.isArray(assets) && assets.length <= files.length, 'Unexpected release attachments.');
|
|
92
|
+
const found = new Set();
|
|
93
|
+
for (const asset of assets) {
|
|
94
|
+
const file = files.find((item) => item.name === asset.name);
|
|
95
|
+
requireValue(file && identifier(asset.id) && !found.has(asset.name) && asset.size === file.size &&
|
|
96
|
+
typeof asset.browser_download_url === 'string', 'Release attachment identity conflict.');
|
|
97
|
+
const response = await this.download(asset.browser_download_url);
|
|
98
|
+
const hash = createHash('sha256'); let size = 0;
|
|
99
|
+
for await (const chunk of response.body) {
|
|
100
|
+
size += chunk.length;
|
|
101
|
+
requireValue(size <= file.size, 'Release attachment exceeds its sealed size.');
|
|
102
|
+
hash.update(chunk);
|
|
103
|
+
}
|
|
104
|
+
requireValue(size === file.size && hash.digest('hex') === file.sha256, 'Release attachment digest conflict.');
|
|
105
|
+
found.add(file.name);
|
|
106
|
+
}
|
|
107
|
+
requireValue(!complete || found.size === files.length, 'Release attachments are incomplete.');
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async publish({ tag, commit, manifestSha256, files }) {
|
|
112
|
+
requireValue(/^v\d+\.\d+\.\d+$/.test(tag) && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit) &&
|
|
113
|
+
/^[a-f0-9]{64}$/.test(manifestSha256) && Array.isArray(files) && files.length >= 3 && files.length <= 130 &&
|
|
114
|
+
new Set(files.map((file) => file.name)).size === files.length &&
|
|
115
|
+
files.every((file) => /^[a-zA-Z0-9_.-]+$/.test(file.name) && Number.isSafeInteger(file.size) && file.size > 0 &&
|
|
116
|
+
/^[a-f0-9]{64}$/.test(file.sha256) && typeof file.blob === 'function') &&
|
|
117
|
+
files.some((file) => file.name === 'tspack-manifest.json' && file.sha256 === manifestSha256) &&
|
|
118
|
+
files.some((file) => file.name === 'SHA256SUMS.txt'), 'Expected a verified sealed artifact set.');
|
|
119
|
+
const remoteTag = await this.json(`/tags/${tag}`);
|
|
120
|
+
requireValue(remoteTag?.name === tag && remoteTag.commit?.sha === commit, 'Remote tag does not identify the sealed source.');
|
|
121
|
+
let release = await this.json(`/releases/tags/${tag}`, {}, [200, 404]);
|
|
122
|
+
if (!release) {
|
|
123
|
+
release = await this.json('/releases', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ tag_name: tag, target_commitish: commit, name: tag,
|
|
125
|
+
body: `Sealed TsPack release. Manifest SHA-256: ${manifestSha256}. See changelog.md at this tag.`,
|
|
126
|
+
draft: true, prerelease: false }) }, [201]);
|
|
127
|
+
}
|
|
128
|
+
this.assertRelease(release, tag, commit);
|
|
129
|
+
const found = await this.verifyAssets(release.id, files, !release.draft);
|
|
130
|
+
if (!release.draft) return { id: release.id, published: true, reused: true };
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
if (found.has(file.name)) continue;
|
|
133
|
+
const form = new FormData();
|
|
134
|
+
const blob = await file.blob();
|
|
135
|
+
requireValue(blob.size === file.size, 'Local sealed attachment changed before upload.');
|
|
136
|
+
form.set('attachment', blob, file.name);
|
|
137
|
+
const asset = await this.json(`/releases/${release.id}/assets?name=${file.name}`, { method: 'POST', body: form }, [201]);
|
|
138
|
+
requireValue(identifier(asset?.id) && asset.name === file.name && asset.size === file.size, 'Invalid attachment upload acknowledgement.');
|
|
139
|
+
}
|
|
140
|
+
await this.verifyAssets(release.id, files, true);
|
|
141
|
+
const finalTag = await this.json(`/tags/${tag}`);
|
|
142
|
+
requireValue(finalTag?.commit?.sha === commit, 'Remote tag changed during publication.');
|
|
143
|
+
const published = await this.json(`/releases/${release.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
|
144
|
+
body: JSON.stringify({ draft: false }) });
|
|
145
|
+
this.assertRelease(published, tag, commit);
|
|
146
|
+
requireValue(published.draft === false, 'Gitea did not confirm publication.');
|
|
147
|
+
await this.verifyAssets(published.id, files, true);
|
|
148
|
+
return { id: published.id, published: true, reused: false };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Managed by GitZone's asset_tspack_release_gitea template.
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import { createReadStream, openAsBlob } from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { GiteaSealedRelease } from './gitzone-release-api.mjs';
|
|
10
|
+
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const requireValue = (value, message) => { if (!value) throw new Error(message); };
|
|
13
|
+
const readJson = async (file) => {
|
|
14
|
+
const stat = await fs.lstat(file);
|
|
15
|
+
requireValue(stat.isFile() && stat.size <= 1024 * 1024, 'Expected bounded regular release metadata.');
|
|
16
|
+
const bytes = await fs.readFile(file);
|
|
17
|
+
requireValue(bytes.length <= 1024 * 1024, 'Release metadata exceeds its limit.');
|
|
18
|
+
try { return JSON.parse(bytes.toString('utf8')); } catch { throw new Error('Invalid release metadata.'); }
|
|
19
|
+
};
|
|
20
|
+
const digest = async (file) => {
|
|
21
|
+
const hash = createHash('sha256'); let size = 0;
|
|
22
|
+
for await (const chunk of createReadStream(file)) { hash.update(chunk); size += chunk.length; }
|
|
23
|
+
return { size, sha256: hash.digest('hex') };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const root = await fs.realpath(fileURLToPath(new URL('../', import.meta.url)));
|
|
28
|
+
const packageJson = await readJson(path.join(root, 'package.json'));
|
|
29
|
+
const cli = (await readJson(path.join(root, '.smartconfig.json')))['@git.zone/cli'];
|
|
30
|
+
const config = cli?.assets?.sealedRelease;
|
|
31
|
+
const command = process.argv[2];
|
|
32
|
+
requireValue(process.argv.length === 3 && ['discover', 'prepare', 'restore', 'publish'].includes(command), 'Select discover, prepare, restore or publish.');
|
|
33
|
+
requireValue(/^[a-zA-Z0-9_-]+$/.test(config?.outputDirectory) && config.outputDirectory.startsWith('dist_'),
|
|
34
|
+
'Sealed release outputDirectory must be a single dist_ directory.');
|
|
35
|
+
const output = path.join(root, config.outputDirectory);
|
|
36
|
+
const incoming = path.join(root, 'dist_gitzone_retained');
|
|
37
|
+
requireValue(output !== incoming, 'Release output conflicts with the retention download directory.');
|
|
38
|
+
const commit = (await exec('git', ['rev-parse', 'HEAD'], { cwd: root })).stdout.trim();
|
|
39
|
+
const tag = `v${packageJson.version}`;
|
|
40
|
+
requireValue(/^v\d+\.\d+\.\d+$/.test(tag) && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(commit) &&
|
|
41
|
+
process.env.GITHUB_REF === `refs/tags/${tag}` && process.env.GITHUB_SHA === commit &&
|
|
42
|
+
!(await exec('git', ['status', '--porcelain'], { cwd: root })).stdout.trim() &&
|
|
43
|
+
(await exec('git', ['rev-parse', `${tag}^{commit}`], { cwd: root })).stdout.trim() === commit,
|
|
44
|
+
'Release job requires the exact clean tagged package source.');
|
|
45
|
+
const ignoredPaths = [`${config.outputDirectory}/.probe`, 'dist_gitzone_retained/.probe'];
|
|
46
|
+
requireValue((await exec('git', ['check-ignore', ...ignoredPaths], { cwd: root })).stdout.trim().split('\n').join() === ignoredPaths.join(),
|
|
47
|
+
'Both release output directories must be Git-ignored.');
|
|
48
|
+
const artifactName = `tspack-${commit}`;
|
|
49
|
+
const repository = `${cli.module.gitscope}/${cli.module.gitrepo}`;
|
|
50
|
+
requireValue(process.env.GITHUB_REPOSITORY === repository, 'Release job repository mismatch.');
|
|
51
|
+
const api = () => new GiteaSealedRelease({ server: `https://${cli.module.githost}`, repository, token: process.env.GITHUB_TOKEN });
|
|
52
|
+
const setOutputs = async (values) => {
|
|
53
|
+
requireValue(process.env.GITHUB_OUTPUT, 'Expected the Actions step output file.');
|
|
54
|
+
await fs.appendFile(process.env.GITHUB_OUTPUT, Object.entries(values).map(([key, value]) => `${key}=${value}\n`).join(''));
|
|
55
|
+
};
|
|
56
|
+
const runAdapter = async (args) => {
|
|
57
|
+
requireValue(Array.isArray(args) && args.length > 0 && args.every((value) => typeof value === 'string' && !value.includes('\0')), 'Expected an explicit component command argument array.');
|
|
58
|
+
const environment = { ...process.env };
|
|
59
|
+
delete environment.GITHUB_TOKEN;
|
|
60
|
+
delete environment.ACTIONS_RUNTIME_TOKEN;
|
|
61
|
+
let result;
|
|
62
|
+
try { result = await exec(args[0], args.slice(1), { cwd: root, env: environment, maxBuffer: 1024 * 1024, timeout: 30 * 60 * 1000 }); }
|
|
63
|
+
catch { throw new Error('Component preparation or verification failed. Run its committed command to inspect the build.'); }
|
|
64
|
+
try { return JSON.parse(result.stdout); } catch { throw new Error('Component command must return one JSON packaging result.'); }
|
|
65
|
+
};
|
|
66
|
+
const verify = async (result) => {
|
|
67
|
+
requireValue(typeof result?.directory === 'string' && /^[a-f0-9]{64}$/.test(result.manifestSha256), 'Invalid component packaging result.');
|
|
68
|
+
const directory = path.resolve(root, result.directory);
|
|
69
|
+
requireValue(path.dirname(directory) === output && await fs.realpath(directory) === directory &&
|
|
70
|
+
await fs.realpath(output) === output, 'Sealed directory must be canonical and directly inside the configured output root.');
|
|
71
|
+
const { TsPack } = await import('@git.zone/tspack');
|
|
72
|
+
const verified = await new TsPack(root).verify(directory, { requireRelease: true,
|
|
73
|
+
expectedManifestSha256: result.manifestSha256, expectedSourceCommit: commit });
|
|
74
|
+
requireValue(verified.manifest.packageName === packageJson.name && verified.manifest.version === packageJson.version,
|
|
75
|
+
'Sealed package identity mismatch.');
|
|
76
|
+
return { directory, manifestSha256: result.manifestSha256, manifest: verified.manifest };
|
|
77
|
+
};
|
|
78
|
+
const receiptFile = path.join(output, 'gitzone-release.json');
|
|
79
|
+
if (command === 'discover') {
|
|
80
|
+
const retained = await api().findRetention({ name: artifactName, runId: Number(process.env.GITHUB_RUN_ID), tag });
|
|
81
|
+
await setOutputs({ artifact: artifactName, retained: retained ? 'true' : 'false', output: config.outputDirectory });
|
|
82
|
+
} else if (command === 'prepare') {
|
|
83
|
+
requireValue(await fs.lstat(output).then(() => false, (error) => { if (error.code === 'ENOENT') return true; throw error; }),
|
|
84
|
+
'Fresh preparation requires an absent output root; reuse retained output instead.');
|
|
85
|
+
const verified = await verify(await runAdapter(config.prepareCommand));
|
|
86
|
+
await fs.writeFile(receiptFile, `${JSON.stringify({ format: 'gitzone.sealed-release.v1', tag, commit,
|
|
87
|
+
directory: path.basename(verified.directory), manifestSha256: verified.manifestSha256 }, null, 2)}\n`, { flag: 'wx' });
|
|
88
|
+
await setOutputs({ manifest: verified.manifestSha256 });
|
|
89
|
+
} else {
|
|
90
|
+
if (command === 'restore') {
|
|
91
|
+
requireValue(await fs.realpath(incoming) === incoming, 'Expected a fresh canonical artifact download.');
|
|
92
|
+
// Only this job's ignored compiler output is replaced, after the remote artifact download succeeds.
|
|
93
|
+
const existing = await fs.lstat(output).catch((error) => { if (error.code === 'ENOENT') return null; throw error; });
|
|
94
|
+
requireValue(!existing || (existing.isDirectory() && !existing.isSymbolicLink()), 'Unsafe existing release output.');
|
|
95
|
+
if (existing) await fs.rm(output, { recursive: true });
|
|
96
|
+
await fs.rename(incoming, output);
|
|
97
|
+
}
|
|
98
|
+
const receipt = await readJson(receiptFile);
|
|
99
|
+
requireValue(receipt.format === 'gitzone.sealed-release.v1' && receipt.tag === tag && receipt.commit === commit &&
|
|
100
|
+
/^[a-zA-Z0-9_.-]+$/.test(receipt.directory) && /^[a-f0-9]{64}$/.test(receipt.manifestSha256) &&
|
|
101
|
+
(!process.env.EXPECTED_MANIFEST || process.env.EXPECTED_MANIFEST === receipt.manifestSha256), 'Retained release receipt mismatch.');
|
|
102
|
+
const verified = await verify(await runAdapter(config.verifyCommand));
|
|
103
|
+
requireValue(verified.directory === path.join(output, receipt.directory) && verified.manifestSha256 === receipt.manifestSha256,
|
|
104
|
+
'Component reuse does not match the retained release receipt.');
|
|
105
|
+
if (command === 'publish') {
|
|
106
|
+
requireValue((await exec('git', ['rev-parse', 'HEAD'], { cwd: root })).stdout.trim() === commit &&
|
|
107
|
+
!(await exec('git', ['status', '--porcelain'], { cwd: root })).stdout.trim(), 'Source changed during component verification.');
|
|
108
|
+
requireValue(await api().findRetention({ name: artifactName, runId: Number(process.env.GITHUB_RUN_ID), tag }), 'Publication requires retained artifacts.');
|
|
109
|
+
const files = [];
|
|
110
|
+
for (const name of [...verified.manifest.artifacts.map((file) => file.name), 'tspack-manifest.json', 'SHA256SUMS.txt']) {
|
|
111
|
+
const file = path.join(verified.directory, name);
|
|
112
|
+
files.push({ name, ...await digest(file), blob: () => openAsBlob(file) });
|
|
113
|
+
}
|
|
114
|
+
console.log(JSON.stringify(await api().publish({ tag, commit, manifestSha256: verified.manifestSha256, files })));
|
|
115
|
+
} else console.log(JSON.stringify({ restored: true, manifestSha256: verified.manifestSha256 }));
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Deliberately omit transport errors, subprocess output, response bodies and token-bearing URLs.
|
|
119
|
+
console.error(error instanceof Error && error.message && !error.message.includes('\n') && !error.message.includes('://')
|
|
120
|
+
? error.message : 'Sealed release operation failed. Retain the original run and its artifacts.');
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@git.zone/cli',
|
|
6
|
-
version: '6.
|
|
6
|
+
version: '6.6.0',
|
|
7
7
|
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSxlQUFlO0lBQ3JCLE9BQU8sRUFBRSxPQUFPO0lBQ2hCLFdBQVcsRUFBRSwrTEFBK0w7Q0FDN00sQ0FBQSJ9
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared validation for the formatter and config doctor. Commands are literal argv. */
|
|
2
|
+
export const getSealedReleaseConfigError = (value) => {
|
|
3
|
+
const config = value;
|
|
4
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) ||
|
|
5
|
+
typeof config.denoVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(config.denoVersion) ||
|
|
6
|
+
typeof config.outputDirectory !== 'string' || !/^dist_[a-zA-Z0-9_-]+$/.test(config.outputDirectory) ||
|
|
7
|
+
config.outputDirectory === 'dist_gitzone_retained' ||
|
|
8
|
+
!['prepareCommand', 'verifyCommand'].every((key) => Array.isArray(config[key]) && config[key].length > 0 &&
|
|
9
|
+
config[key].every((item) => typeof item === 'string' && item.length > 0 && !item.includes('\0')))) {
|
|
10
|
+
return 'tspackRelease requires a pinned Deno version, a dist_ output directory and explicit prepare/verify command arrays.';
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVscGVycy5zZWFsZWRyZWxlYXNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvaGVscGVycy5zZWFsZWRyZWxlYXNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLHdGQUF3RjtBQUN4RixNQUFNLENBQUMsTUFBTSwyQkFBMkIsR0FBRyxDQUFDLEtBQWMsRUFBc0IsRUFBRTtJQUNoRixNQUFNLE1BQU0sR0FBRyxLQUF1QyxDQUFDO0lBQ3ZELElBQUksQ0FBQyxNQUFNLElBQUksT0FBTyxNQUFNLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxDQUFDO1FBQzlELE9BQU8sTUFBTSxDQUFDLFdBQVcsS0FBSyxRQUFRLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQztRQUNyRixPQUFPLE1BQU0sQ0FBQyxlQUFlLEtBQUssUUFBUSxJQUFJLENBQUMsdUJBQXVCLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUM7UUFDbkcsTUFBTSxDQUFDLGVBQWUsS0FBSyx1QkFBdUI7UUFDbEQsQ0FBQyxDQUFDLGdCQUFnQixFQUFFLGVBQWUsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQUcsRUFBRSxFQUFFLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUM7WUFDdEcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLElBQWEsRUFBRSxFQUFFLENBQUMsT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxNQUFNLEdBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNqSCxPQUFPLG9IQUFvSCxDQUFDO0lBQzlILENBQUM7SUFDRCxPQUFPLFNBQVMsQ0FBQztBQUNuQixDQUFDLENBQUMifQ==
|