@mannyc1/ts-release 0.2.2 → 0.3.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/ARCHITECTURE.md +60 -0
- package/CHANGELOG.md +82 -0
- package/README.md +129 -6
- package/SPEC.md +51 -0
- package/apps/release-ts/README.md +7 -0
- package/apps/ts-release-agents/package.json +1 -1
- package/apps/ts-release-agents/src/check.ts +7 -2
- package/apps/ts-release-agents/test/contract.test.ts +2 -2
- package/dist/bin/ts-release.js +154 -47
- package/dist/drivers/archive.d.ts +2 -1
- package/dist/drivers/archive.d.ts.map +1 -1
- package/dist/drivers/archive.js +12 -8
- package/dist/drivers/archive.js.map +1 -1
- package/dist/drivers/process.d.ts.map +1 -1
- package/dist/drivers/process.js +4 -4
- package/dist/drivers/process.js.map +1 -1
- package/dist/drivers/python-wheel.d.ts +21 -0
- package/dist/drivers/python-wheel.d.ts.map +1 -0
- package/dist/drivers/python-wheel.js +109 -0
- package/dist/drivers/python-wheel.js.map +1 -0
- package/dist/drivers/workspace.d.ts +3 -0
- package/dist/drivers/workspace.d.ts.map +1 -1
- package/dist/drivers/workspace.js +5 -1
- package/dist/drivers/workspace.js.map +1 -1
- package/dist/operation-journal/authority.d.ts +3 -0
- package/dist/operation-journal/authority.d.ts.map +1 -0
- package/dist/operation-journal/authority.js +147 -0
- package/dist/operation-journal/authority.js.map +1 -0
- package/dist/operation-journal/aws/deadline.d.ts +12 -0
- package/dist/operation-journal/aws/deadline.d.ts.map +1 -0
- package/dist/operation-journal/aws/deadline.js +38 -0
- package/dist/operation-journal/aws/deadline.js.map +1 -0
- package/dist/operation-journal/aws/oidc.d.ts +27 -0
- package/dist/operation-journal/aws/oidc.d.ts.map +1 -0
- package/dist/operation-journal/aws/oidc.js +361 -0
- package/dist/operation-journal/aws/oidc.js.map +1 -0
- package/dist/operation-journal/aws/policy.d.ts +42 -0
- package/dist/operation-journal/aws/policy.d.ts.map +1 -0
- package/dist/operation-journal/aws/policy.js +404 -0
- package/dist/operation-journal/aws/policy.js.map +1 -0
- package/dist/operation-journal/aws/s3-boundary.d.ts +17 -0
- package/dist/operation-journal/aws/s3-boundary.d.ts.map +1 -0
- package/dist/operation-journal/aws/s3-boundary.js +417 -0
- package/dist/operation-journal/aws/s3-boundary.js.map +1 -0
- package/dist/operation-journal/aws.d.ts +11 -0
- package/dist/operation-journal/aws.d.ts.map +1 -0
- package/dist/operation-journal/aws.js +50 -0
- package/dist/operation-journal/aws.js.map +1 -0
- package/dist/operation-journal/canonical.d.ts +52 -0
- package/dist/operation-journal/canonical.d.ts.map +1 -0
- package/dist/operation-journal/canonical.js +282 -0
- package/dist/operation-journal/canonical.js.map +1 -0
- package/dist/operation-journal/model.d.ts +251 -0
- package/dist/operation-journal/model.d.ts.map +1 -0
- package/dist/operation-journal/model.js +56 -0
- package/dist/operation-journal/model.js.map +1 -0
- package/dist/operation-journal/reducer.d.ts +28 -0
- package/dist/operation-journal/reducer.d.ts.map +1 -0
- package/dist/operation-journal/reducer.js +73 -0
- package/dist/operation-journal/reducer.js.map +1 -0
- package/dist/operation-journal/s3.d.ts +7 -0
- package/dist/operation-journal/s3.d.ts.map +1 -0
- package/dist/operation-journal/s3.js +667 -0
- package/dist/operation-journal/s3.js.map +1 -0
- package/dist/operation-journal.d.ts +14 -0
- package/dist/operation-journal.d.ts.map +1 -0
- package/dist/operation-journal.js +12 -0
- package/dist/operation-journal.js.map +1 -0
- package/dist/platform/credentials.d.ts.map +1 -1
- package/dist/platform/credentials.js +65 -3
- package/dist/platform/credentials.js.map +1 -1
- package/dist/publication/github.d.ts.map +1 -1
- package/dist/publication/github.js +66 -15
- package/dist/publication/github.js.map +1 -1
- package/dist/release/capabilities.d.ts.map +1 -1
- package/dist/release/capabilities.js +2 -2
- package/dist/release/capabilities.js.map +1 -1
- package/dist/release/prepare.d.ts.map +1 -1
- package/dist/release/prepare.js +4 -5
- package/dist/release/prepare.js.map +1 -1
- package/dist/release/prepared-store.d.ts.map +1 -1
- package/dist/release/prepared-store.js +11 -9
- package/dist/release/prepared-store.js.map +1 -1
- package/package.json +19 -4
- package/src/drivers/archive.ts +17 -8
- package/src/drivers/process.ts +3 -4
- package/src/drivers/python-wheel.ts +145 -0
- package/src/drivers/workspace.ts +7 -1
- package/src/operation-journal/authority.ts +162 -0
- package/src/operation-journal/aws/deadline.ts +41 -0
- package/src/operation-journal/aws/oidc.ts +439 -0
- package/src/operation-journal/aws/policy.ts +491 -0
- package/src/operation-journal/aws/s3-boundary.ts +562 -0
- package/src/operation-journal/aws.ts +60 -0
- package/src/operation-journal/canonical.ts +373 -0
- package/src/operation-journal/model.ts +301 -0
- package/src/operation-journal/reducer.ts +112 -0
- package/src/operation-journal/s3.ts +884 -0
- package/src/operation-journal.ts +51 -0
- package/src/platform/credentials.ts +71 -3
- package/src/publication/github.ts +83 -15
- package/src/release/capabilities.ts +3 -2
- package/src/release/prepare.ts +3 -5
- package/src/release/prepared-store.ts +8 -7
- package/templates/README.md +2 -2
- package/templates/github-actions/release.yml +24 -7
- package/templates/github-actions/reviewed-release.yml +49 -14
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-neutral durable operation journal.
|
|
3
|
+
*
|
|
4
|
+
* Consumers own payload codecs and provider semantics. This subpath owns only
|
|
5
|
+
* the canonical envelope, finite transition machine, S3 conditional commit,
|
|
6
|
+
* durable acknowledgment, and exact-chain replay.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
JournalAuthorityMismatch,
|
|
10
|
+
JournalConflict,
|
|
11
|
+
JournalInputError,
|
|
12
|
+
JournalIntegrityError,
|
|
13
|
+
JournalStorageOutcomeUnknown,
|
|
14
|
+
JournalStorageUnavailable,
|
|
15
|
+
JournalTransitionError,
|
|
16
|
+
S3JournalBoundaryError,
|
|
17
|
+
journalRecordTags,
|
|
18
|
+
operationJournalByteLimits
|
|
19
|
+
} from "./operation-journal/model.js"
|
|
20
|
+
export type {
|
|
21
|
+
CanonicalOperationJournalShape,
|
|
22
|
+
JournalAcknowledgement,
|
|
23
|
+
JournalAppendError,
|
|
24
|
+
JournalAppendRequest,
|
|
25
|
+
JournalOperation,
|
|
26
|
+
JournalReadError,
|
|
27
|
+
JournalRecord,
|
|
28
|
+
JournalRecordTag,
|
|
29
|
+
JournalSnapshot,
|
|
30
|
+
JournalStateTag,
|
|
31
|
+
JournalWorkflowCoordinate,
|
|
32
|
+
S3JournalAuthority,
|
|
33
|
+
S3JournalBoundaryShape,
|
|
34
|
+
S3JournalDeleteMarker,
|
|
35
|
+
S3JournalNamespaceListing,
|
|
36
|
+
S3JournalObjectVersion,
|
|
37
|
+
S3JournalOidcClaims,
|
|
38
|
+
S3JournalOidcTrustConditions,
|
|
39
|
+
S3JournalPutCondition,
|
|
40
|
+
S3JournalPutRequest,
|
|
41
|
+
S3JournalPutResult
|
|
42
|
+
} from "./operation-journal/model.js"
|
|
43
|
+
export {
|
|
44
|
+
deriveOperationKey,
|
|
45
|
+
journalEventKey,
|
|
46
|
+
journalHeadKey,
|
|
47
|
+
journalNamespace
|
|
48
|
+
} from "./operation-journal/canonical.js"
|
|
49
|
+
export { admitJournalTransition } from "./operation-journal/reducer.js"
|
|
50
|
+
export { makeS3CanonicalOperationJournal } from "./operation-journal/s3.js"
|
|
51
|
+
export type { S3CanonicalOperationJournalOptions } from "./operation-journal/s3.js"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer"
|
|
2
|
-
import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { basename, isAbsolute, join, resolve } from "node:path"
|
|
5
5
|
import * as Config from "effect/Config"
|
|
@@ -588,6 +588,7 @@ const makeAuthorizedMutationHttp = (
|
|
|
588
588
|
type NpmUserConfigMetadata = {
|
|
589
589
|
readonly directory: string
|
|
590
590
|
readonly path: string
|
|
591
|
+
readonly globalConfigPath: string
|
|
591
592
|
readonly subject: SubjectId
|
|
592
593
|
readonly grant: ScopedSecret
|
|
593
594
|
readonly token: Redacted.Redacted<string>
|
|
@@ -620,9 +621,18 @@ const makeNpmUserConfigResource = (
|
|
|
620
621
|
const directory = await mkdtemp(join(root, "ts-release-npm-"))
|
|
621
622
|
try {
|
|
622
623
|
const path = join(directory, "userconfig")
|
|
624
|
+
const globalConfigPath = join(directory, "globalconfig")
|
|
623
625
|
const contents = `${npmAuthLine(input.registryUrl, token)}\nignore-scripts=true\n`
|
|
624
626
|
await writeFile(path, contents, { mode: 0o600, flag: "wx" })
|
|
625
|
-
|
|
627
|
+
await writeFile(globalConfigPath, "", { mode: 0o600, flag: "wx" })
|
|
628
|
+
npmUserConfigs.set(handle, {
|
|
629
|
+
directory,
|
|
630
|
+
path,
|
|
631
|
+
globalConfigPath,
|
|
632
|
+
subject: input.operation.subject,
|
|
633
|
+
grant,
|
|
634
|
+
token
|
|
635
|
+
})
|
|
626
636
|
return handle
|
|
627
637
|
} catch (cause) {
|
|
628
638
|
await rm(directory, { recursive: true, force: true })
|
|
@@ -671,6 +681,58 @@ const optionalPath = Config.option(Config.string("PATH")).pipe(
|
|
|
671
681
|
const closedBaseEnvironment = (path: string | undefined): Record<string, string> =>
|
|
672
682
|
path === undefined ? {} : { PATH: path }
|
|
673
683
|
|
|
684
|
+
const optionalReleaseHome = Config.option(Config.string("TS_RELEASE_HOME")).pipe(
|
|
685
|
+
Effect.map(Option.getOrUndefined),
|
|
686
|
+
Effect.orElseSucceed(() => undefined)
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
const privateTrustedNpmEnvironment = Effect.fn("CertifiedPublisherSpawn.privateTrustedNpmEnvironment")(
|
|
690
|
+
function*(grant: WorkloadIdentity) {
|
|
691
|
+
const configured = yield* optionalReleaseHome
|
|
692
|
+
if (configured === undefined || !isAbsolute(configured) || resolve(configured) !== configured) {
|
|
693
|
+
return yield* new CredentialUnavailable({
|
|
694
|
+
subject: grant.subject,
|
|
695
|
+
provider: grant.provider,
|
|
696
|
+
purpose: "publish",
|
|
697
|
+
reason: "Trusted npm publishing requires one canonical absolute TS_RELEASE_HOME."
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
return yield* Effect.tryPromise({
|
|
701
|
+
try: async () => {
|
|
702
|
+
const canonicalHome = await realpath(configured)
|
|
703
|
+
if (canonicalHome !== configured) throw new Error("TS_RELEASE_HOME is not canonical.")
|
|
704
|
+
const homeMetadata = await stat(canonicalHome)
|
|
705
|
+
const owner = typeof process.getuid === "function" ? process.getuid() : undefined
|
|
706
|
+
if (!homeMetadata.isDirectory() || (homeMetadata.mode & 0o077) !== 0 ||
|
|
707
|
+
(owner !== undefined && homeMetadata.uid !== owner)) {
|
|
708
|
+
throw new Error("TS_RELEASE_HOME is not one private caller-owned directory.")
|
|
709
|
+
}
|
|
710
|
+
const userConfig = join(canonicalHome, "npm-userconfig")
|
|
711
|
+
const globalConfig = join(canonicalHome, "npm-globalconfig")
|
|
712
|
+
for (const path of [userConfig, globalConfig]) {
|
|
713
|
+
if (await realpath(path) !== path) throw new Error("A trusted npm config path is not canonical.")
|
|
714
|
+
const metadata = await stat(path)
|
|
715
|
+
if (!metadata.isFile() || (metadata.mode & 0o777) !== 0o600 ||
|
|
716
|
+
(owner !== undefined && metadata.uid !== owner) || (await readFile(path)).length !== 0) {
|
|
717
|
+
throw new Error("A trusted npm config is not an empty private caller-owned file.")
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
HOME: canonicalHome,
|
|
722
|
+
NPM_CONFIG_USERCONFIG: userConfig,
|
|
723
|
+
NPM_CONFIG_GLOBALCONFIG: globalConfig
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
catch: () => new CredentialUnavailable({
|
|
727
|
+
subject: grant.subject,
|
|
728
|
+
provider: grant.provider,
|
|
729
|
+
purpose: "publish",
|
|
730
|
+
reason: "Trusted npm publishing requires validated empty private npm configuration files."
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
|
|
674
736
|
const workloadEnvironment = Effect.fn("CertifiedPublisherSpawn.workloadEnvironment")(function*(
|
|
675
737
|
trustedWorkloads: TrustedWorkloadVault,
|
|
676
738
|
grant: WorkloadIdentity
|
|
@@ -693,7 +755,11 @@ const workloadEnvironment = Effect.fn("CertifiedPublisherSpawn.workloadEnvironme
|
|
|
693
755
|
})
|
|
694
756
|
}
|
|
695
757
|
const path = yield* optionalPath
|
|
696
|
-
const
|
|
758
|
+
const privateEnvironment = yield* privateTrustedNpmEnvironment(grant)
|
|
759
|
+
const env: Record<string, string> = {
|
|
760
|
+
...closedBaseEnvironment(path),
|
|
761
|
+
...privateEnvironment
|
|
762
|
+
}
|
|
697
763
|
for (const name of oidcNames) {
|
|
698
764
|
const value = material.oidc.get(name)
|
|
699
765
|
if (value === undefined) {
|
|
@@ -858,7 +924,9 @@ const publisherEnvironment = Effect.fn("CertifiedPublisherSpawn.environment")(fu
|
|
|
858
924
|
const path = yield* optionalPath
|
|
859
925
|
return {
|
|
860
926
|
...closedBaseEnvironment(path),
|
|
927
|
+
HOME: metadata.directory,
|
|
861
928
|
NPM_CONFIG_USERCONFIG: metadata.path,
|
|
929
|
+
NPM_CONFIG_GLOBALCONFIG: metadata.globalConfigPath,
|
|
862
930
|
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
|
863
931
|
}
|
|
864
932
|
}
|
|
@@ -135,6 +135,10 @@ type GithubMutationPlan =
|
|
|
135
135
|
readonly uploadUrl: string
|
|
136
136
|
readonly assetIndexes: ReadonlyArray<number>
|
|
137
137
|
}
|
|
138
|
+
| {
|
|
139
|
+
readonly _tag: "PublishRelease"
|
|
140
|
+
readonly releaseId: number
|
|
141
|
+
}
|
|
138
142
|
|
|
139
143
|
type PageResult =
|
|
140
144
|
| { readonly _tag: "Complete", readonly assets: ReadonlyArray<ReleaseAssetFacts> }
|
|
@@ -451,7 +455,8 @@ const listAllAssets = Effect.fn("GitHubReleaseSubject.listAllAssets")(function*(
|
|
|
451
455
|
|
|
452
456
|
const compareReleaseMetadata = (
|
|
453
457
|
publication: PreparedGitHubPublication,
|
|
454
|
-
facts: ReleaseFacts
|
|
458
|
+
facts: ReleaseFacts,
|
|
459
|
+
options: { readonly ignoreDraft?: boolean } = {}
|
|
455
460
|
): ReadonlyArray<Difference> => {
|
|
456
461
|
const differences: Array<Difference> = []
|
|
457
462
|
if (facts.tag !== publication.tag.toString()) {
|
|
@@ -462,7 +467,7 @@ const compareReleaseMetadata = (
|
|
|
462
467
|
}
|
|
463
468
|
const expectedBody = publication.body ?? ""
|
|
464
469
|
if (facts.body !== expectedBody) differences.push(textDifference("release.body", expectedBody, facts.body))
|
|
465
|
-
if (facts.draft !== publication.draft) {
|
|
470
|
+
if (options.ignoreDraft !== true && facts.draft !== publication.draft) {
|
|
466
471
|
differences.push(scalarDifference("release.draft", publication.draft, facts.draft))
|
|
467
472
|
}
|
|
468
473
|
if (facts.prerelease !== publication.prerelease) {
|
|
@@ -521,6 +526,14 @@ const decideFor = (
|
|
|
521
526
|
})
|
|
522
527
|
})
|
|
523
528
|
}
|
|
529
|
+
if (plan?._tag === "PublishRelease") {
|
|
530
|
+
return NeedsMutation.make({
|
|
531
|
+
subject: publication.authority.subject,
|
|
532
|
+
precondition: MutationPrecondition.make({
|
|
533
|
+
kind: NonEmptyName.make("github-exact-draft-assets-complete")
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
}
|
|
524
537
|
return ProviderBlocked.make({
|
|
525
538
|
subject: publication.authority.subject,
|
|
526
539
|
reason: SafeReason.make("GitHub absence carried no exact mutation plan.")
|
|
@@ -557,7 +570,9 @@ const appliedAttempt = (publication: PreparedGitHubPublication): Applied => Appl
|
|
|
557
570
|
subject: publication.authority.subject,
|
|
558
571
|
fact: ProviderMutationFact.make({
|
|
559
572
|
subject: publication.authority.subject,
|
|
560
|
-
detail: SafeReason.make(
|
|
573
|
+
detail: SafeReason.make(
|
|
574
|
+
"GitHub accepted every mutation dispatched in this convergence phase; final equivalence still requires provider reobservation."
|
|
575
|
+
)
|
|
561
576
|
})
|
|
562
577
|
})
|
|
563
578
|
|
|
@@ -720,7 +735,10 @@ export const makeGithubSubjects = (
|
|
|
720
735
|
if (release === undefined || !validateUploadUrl(publication, release.id, release.uploadUrl)) {
|
|
721
736
|
return inconclusive(publication, "The GitHub release response omitted identity facts or carried an invalid repository-scoped upload template.")
|
|
722
737
|
}
|
|
723
|
-
const
|
|
738
|
+
const stagedForPublicPromotion = publication.draft === false && release.draft === true
|
|
739
|
+
const releaseDifferences = compareReleaseMetadata(publication, release, {
|
|
740
|
+
ignoreDraft: stagedForPublicPromotion
|
|
741
|
+
})
|
|
724
742
|
if (releaseDifferences.length > 0) return presentDifferent(publication, releaseDifferences)
|
|
725
743
|
|
|
726
744
|
const listed = yield* listAllAssets(publication, http, grant, release.id)
|
|
@@ -799,6 +817,11 @@ export const makeGithubSubjects = (
|
|
|
799
817
|
}
|
|
800
818
|
}
|
|
801
819
|
if (missingIndexes.length > 0) {
|
|
820
|
+
if (!release.draft) {
|
|
821
|
+
return presentDifferent(publication, missingIndexes.map((index) => scalarDifference(
|
|
822
|
+
`asset.${publication.assets[index]!.name}.count`, 1, 0
|
|
823
|
+
)))
|
|
824
|
+
}
|
|
802
825
|
if (mutationMayStillBecomeVisible) {
|
|
803
826
|
return pending(publication, "One or more intended GitHub assets are not yet visible after the accepted mutation.")
|
|
804
827
|
}
|
|
@@ -814,6 +837,14 @@ export const makeGithubSubjects = (
|
|
|
814
837
|
"A full paginated release-asset enumeration proved at least one intended asset name absent."
|
|
815
838
|
)
|
|
816
839
|
}
|
|
840
|
+
if (stagedForPublicPromotion) {
|
|
841
|
+
mutationPlan = { _tag: "PublishRelease", releaseId: release.id }
|
|
842
|
+
return absent(
|
|
843
|
+
publication,
|
|
844
|
+
"github-exact-draft-assets-complete",
|
|
845
|
+
"The exact private draft and complete asset set are ready for one final public transition."
|
|
846
|
+
)
|
|
847
|
+
}
|
|
817
848
|
return PresentEquivalent.make({ subject: publication.authority.subject })
|
|
818
849
|
})
|
|
819
850
|
|
|
@@ -837,14 +868,19 @@ export const makeGithubSubjects = (
|
|
|
837
868
|
decision.proof.kind === "github-authenticated-repository-and-unique-tag"
|
|
838
869
|
: plan._tag === "CreateRelease"
|
|
839
870
|
? decision._tag === "NeedsMutation" && decision.precondition.kind === "github-release-absent-at-exact-tag"
|
|
840
|
-
:
|
|
871
|
+
: plan._tag === "UploadAssets"
|
|
872
|
+
? decision._tag === "NeedsMutation" &&
|
|
841
873
|
decision.precondition.kind === "github-intended-assets-authoritatively-absent"
|
|
874
|
+
: decision._tag === "NeedsMutation" &&
|
|
875
|
+
decision.precondition.kind === "github-exact-draft-assets-complete"
|
|
842
876
|
if (!expectedDecision) {
|
|
843
877
|
return yield* mutationFailure(publication, "The GitHub mutation decision does not match its observed precondition.")
|
|
844
878
|
}
|
|
845
879
|
const indexes = plan._tag === "CreateRelease"
|
|
846
880
|
? publication.assets.map((_, index) => index)
|
|
847
|
-
:
|
|
881
|
+
: plan._tag === "UploadAssets"
|
|
882
|
+
? [...plan.assetIndexes]
|
|
883
|
+
: []
|
|
848
884
|
const bytesByIndex = new Map<number, Uint8Array>()
|
|
849
885
|
for (const index of indexes) {
|
|
850
886
|
const asset = publication.assets[index]
|
|
@@ -866,7 +902,10 @@ export const makeGithubSubjects = (
|
|
|
866
902
|
decision
|
|
867
903
|
}
|
|
868
904
|
let acceptedWrites = 0
|
|
869
|
-
const dispatch = Effect.fn("GitHubReleaseSubject.dispatch")(function*(
|
|
905
|
+
const dispatch = Effect.fn("GitHubReleaseSubject.dispatch")(function*(
|
|
906
|
+
request: MutationHttpRequest,
|
|
907
|
+
acceptedStatus: 200 | 201
|
|
908
|
+
) {
|
|
870
909
|
const exchange = yield* executeMutation(mutationHttp, operation, request, grant)
|
|
871
910
|
if (exchange._tag === "Failure") {
|
|
872
911
|
if (acceptedWrites > 0 || exchange.error.commitment === "unknown") {
|
|
@@ -881,7 +920,7 @@ export const makeGithubSubjects = (
|
|
|
881
920
|
"before-dispatch"
|
|
882
921
|
)
|
|
883
922
|
}
|
|
884
|
-
if (exchange.response.status ===
|
|
923
|
+
if (exchange.response.status === acceptedStatus) {
|
|
885
924
|
acceptedWrites += 1
|
|
886
925
|
return { _tag: "Accepted", response: exchange.response } as const
|
|
887
926
|
}
|
|
@@ -891,14 +930,15 @@ export const makeGithubSubjects = (
|
|
|
891
930
|
) } as const
|
|
892
931
|
})
|
|
893
932
|
|
|
894
|
-
let
|
|
933
|
+
let releaseId: number
|
|
934
|
+
let uploadUrl: string | undefined
|
|
895
935
|
if (plan._tag === "CreateRelease") {
|
|
896
936
|
const body = JSON.stringify({
|
|
897
937
|
tag_name: publication.tag.toString(),
|
|
898
938
|
target_commitish: publication.targetCommit.toString(),
|
|
899
939
|
name: publication.title.toString(),
|
|
900
940
|
body: publication.body ?? "",
|
|
901
|
-
draft:
|
|
941
|
+
draft: true,
|
|
902
942
|
prerelease: publication.prerelease
|
|
903
943
|
})
|
|
904
944
|
const created = yield* dispatch({
|
|
@@ -910,23 +950,28 @@ export const makeGithubSubjects = (
|
|
|
910
950
|
"content-length": String(new TextEncoder().encode(body).length)
|
|
911
951
|
},
|
|
912
952
|
body
|
|
913
|
-
})
|
|
953
|
+
}, 201)
|
|
914
954
|
if (created._tag === "Attempt") return created.attempt
|
|
915
955
|
const facts = releaseFacts(parseJson(created.response.body))
|
|
916
|
-
if (facts === undefined ||
|
|
917
|
-
|
|
956
|
+
if (facts === undefined || facts.draft !== true ||
|
|
957
|
+
!validateUploadUrl(publication, facts.id, facts.uploadUrl) ||
|
|
958
|
+
compareReleaseMetadata(publication, facts, { ignoreDraft: publication.draft === false }).length > 0) {
|
|
918
959
|
return unknownAttempt(
|
|
919
960
|
publication,
|
|
920
961
|
"GitHub accepted release creation but its response did not establish the exact release identity and repository-scoped upload template."
|
|
921
962
|
)
|
|
922
963
|
}
|
|
964
|
+
releaseId = facts.id
|
|
923
965
|
uploadUrl = facts.uploadUrl
|
|
924
|
-
} else {
|
|
966
|
+
} else if (plan._tag === "UploadAssets") {
|
|
967
|
+
releaseId = plan.releaseId
|
|
925
968
|
uploadUrl = plan.uploadUrl
|
|
969
|
+
} else {
|
|
970
|
+
releaseId = plan.releaseId
|
|
926
971
|
}
|
|
927
972
|
for (const index of indexes) {
|
|
928
973
|
const bytes = bytesByIndex.get(index)!
|
|
929
|
-
const uploaded = yield* dispatch(uploadRequest(publication, uploadUrl
|
|
974
|
+
const uploaded = yield* dispatch(uploadRequest(publication, uploadUrl!, index, bytes), 201)
|
|
930
975
|
if (uploaded._tag === "Attempt") return uploaded.attempt
|
|
931
976
|
const intended = publication.assets[index]!
|
|
932
977
|
const facts = releaseAssetFacts(publication, parseJson(uploaded.response.body))
|
|
@@ -939,6 +984,29 @@ export const makeGithubSubjects = (
|
|
|
939
984
|
)
|
|
940
985
|
}
|
|
941
986
|
}
|
|
987
|
+
if (plan._tag === "PublishRelease") {
|
|
988
|
+
const body = JSON.stringify({ draft: false })
|
|
989
|
+
const published = yield* dispatch({
|
|
990
|
+
method: "PATCH",
|
|
991
|
+
url: `${publication.authority.audience}/releases/${releaseId}`,
|
|
992
|
+
headers: {
|
|
993
|
+
...githubJsonHeaders,
|
|
994
|
+
"content-type": "application/json",
|
|
995
|
+
"content-length": String(new TextEncoder().encode(body).length)
|
|
996
|
+
},
|
|
997
|
+
body
|
|
998
|
+
}, 200)
|
|
999
|
+
if (published._tag === "Attempt") return published.attempt
|
|
1000
|
+
const facts = releaseFacts(parseJson(published.response.body))
|
|
1001
|
+
if (facts === undefined || facts.id !== releaseId || facts.draft !== false ||
|
|
1002
|
+
!validateUploadUrl(publication, releaseId, facts.uploadUrl) ||
|
|
1003
|
+
compareReleaseMetadata(publication, facts).length > 0) {
|
|
1004
|
+
return unknownAttempt(
|
|
1005
|
+
publication,
|
|
1006
|
+
"GitHub accepted final release publication but its response did not establish the exact public release identity."
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
942
1010
|
return appliedAttempt(publication)
|
|
943
1011
|
})
|
|
944
1012
|
|
|
@@ -113,8 +113,9 @@ export const contributeSourceArtifacts = (
|
|
|
113
113
|
id, argv: preparation.run,
|
|
114
114
|
cwd, inputs,
|
|
115
115
|
outputs: [
|
|
116
|
-
output(preparation.outputs[0]!.id, preparation.outputs[0]!.path, preparation.outputs[0]!.kind ?? "file", preparation.outputs[0]!.mediaType),
|
|
117
|
-
...preparation.outputs.slice(1).map((item) =>
|
|
116
|
+
output(preparation.outputs[0]!.id, render(preparation.outputs[0]!.path, config), preparation.outputs[0]!.kind ?? "file", preparation.outputs[0]!.mediaType),
|
|
117
|
+
...preparation.outputs.slice(1).map((item) =>
|
|
118
|
+
output(item.id, render(item.path, config), item.kind ?? "file", item.mediaType))
|
|
118
119
|
]
|
|
119
120
|
}))
|
|
120
121
|
else preparations.push(GraphCommandCollection.make({
|
package/src/release/prepare.ts
CHANGED
|
@@ -5,16 +5,14 @@ import {
|
|
|
5
5
|
existsSync,
|
|
6
6
|
lstatSync,
|
|
7
7
|
mkdirSync,
|
|
8
|
-
mkdtempSync,
|
|
9
8
|
readFileSync,
|
|
10
9
|
readdirSync,
|
|
11
10
|
realpathSync,
|
|
12
11
|
rmSync
|
|
13
12
|
} from "node:fs"
|
|
14
13
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path"
|
|
15
|
-
import { tmpdir } from "node:os"
|
|
16
14
|
import { tarGz, zip, type ArchiveEntry } from "../drivers/archive.js"
|
|
17
|
-
import { secureRead, secureWrite } from "../drivers/workspace.js"
|
|
15
|
+
import { makeCanonicalTemporaryDirectory, secureRead, secureWrite } from "../drivers/workspace.js"
|
|
18
16
|
import type {
|
|
19
17
|
BunCompileRuntimeIdentity,
|
|
20
18
|
CommandOutcome,
|
|
@@ -956,12 +954,12 @@ const runtimeIdentity = (
|
|
|
956
954
|
: encodeCanonicalJson(bunCompileRuntimes),
|
|
957
955
|
npmPack: npmPack === undefined ? "not-used" : encodeCanonicalJson(npmPack),
|
|
958
956
|
releaseGraph,
|
|
959
|
-
preparer: "@mannyc1/ts-release@0.
|
|
957
|
+
preparer: "@mannyc1/ts-release@0.3.0"
|
|
960
958
|
})
|
|
961
959
|
|
|
962
960
|
export const prepareRelease = Effect.fn("prepareRelease")(function*(input: PreparationRequest) {
|
|
963
961
|
const sourceRoot = input.context.workspace.toString()
|
|
964
|
-
const root = yield* attempt(() =>
|
|
962
|
+
const root = yield* attempt(() => makeCanonicalTemporaryDirectory("ts-release-prepare-"))
|
|
965
963
|
try {
|
|
966
964
|
const sourceSnapshot = yield* input.materializeSource(input.context, WorkspaceRoot.make(root)).pipe(Effect.mapError(failure))
|
|
967
965
|
const context = stagedContext(input.context, root)
|
|
@@ -161,8 +161,9 @@ const causeReason = (cause: unknown): string =>
|
|
|
161
161
|
: String(cause)
|
|
162
162
|
const canonicalDirectory = (directory: string): string => {
|
|
163
163
|
if (!existsSync(directory)) mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
164
|
+
if (lstatSync(directory).isSymbolicLink()) fail("Prepared store root must not be a symlink.")
|
|
164
165
|
const real = realpathSync(directory)
|
|
165
|
-
if (real
|
|
166
|
+
if (!lstatSync(real).isDirectory()) fail("Prepared store root must be a directory.")
|
|
166
167
|
return real
|
|
167
168
|
}
|
|
168
169
|
const syncDirectory = (directory: string): void => {
|
|
@@ -220,16 +221,16 @@ export interface CommittedPreparedRelease {
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
const readBundle = (directory: string): PreparedBundle => {
|
|
224
|
+
if (lstatSync(directory).isSymbolicLink()) fail("Prepared bundle directory must not be a symlink.")
|
|
223
225
|
const real = realpathSync(directory)
|
|
224
|
-
|
|
225
|
-
const bytes = secureRead(directory, "prepared-release.json").bytes
|
|
226
|
+
const bytes = secureRead(real, "prepared-release.json").bytes
|
|
226
227
|
const manifest = decodePreparedRelease(bytes)
|
|
227
228
|
if (manifest.kind !== "complete") fail("Prepared store refuses every unknown or partial bundle kind.")
|
|
228
229
|
const manifestDigest = sha256Digest(bytes).hex
|
|
229
|
-
if (basename(
|
|
230
|
-
const entries = readdirSync(
|
|
230
|
+
if (basename(real) !== manifestDigest) fail("Prepared bundle directory does not match its manifest digest.")
|
|
231
|
+
const entries = readdirSync(real, { withFileTypes: true })
|
|
231
232
|
if (entries.some((entry) => entry.name !== "prepared-release.json" && entry.name !== "blobs")) fail("Prepared bundle contains an unexpected top-level entry.")
|
|
232
|
-
const blobDirectory = join(
|
|
233
|
+
const blobDirectory = join(real, "blobs")
|
|
233
234
|
if (!existsSync(blobDirectory) || lstatSync(blobDirectory).isSymbolicLink() || !statSync(blobDirectory).isDirectory()) fail("Prepared bundle is missing its real blobs directory.")
|
|
234
235
|
const artifacts = artifactsById(manifest)
|
|
235
236
|
validatePublications(manifest, artifacts)
|
|
@@ -241,7 +242,7 @@ const readBundle = (directory: string): PreparedBundle => {
|
|
|
241
242
|
if (actualBlobs.size !== expectedBlobs.size || [...expectedBlobs].some((blob) => !actualBlobs.has(blob))) fail("Prepared bundle blob set does not match its manifest.")
|
|
242
243
|
const blobs = new Map<string, Uint8Array>()
|
|
243
244
|
for (const artifact of artifacts.values()) {
|
|
244
|
-
const blob = secureRead(
|
|
245
|
+
const blob = secureRead(real, `blobs/${artifact.blob.hex}`).bytes
|
|
245
246
|
if (blob.length !== artifact.size || !digestEquals(sha256Digest(blob), artifact.digest)) fail(`Prepared blob ${artifact.id} failed size or digest verification.`)
|
|
246
247
|
blobs.set(artifact.id.toString(), new Uint8Array(blob))
|
|
247
248
|
}
|
package/templates/README.md
CHANGED
|
@@ -29,7 +29,7 @@ its publish job fails after preparation, rerun that failed job in the same
|
|
|
29
29
|
workflow run so `${{ needs.prepare.outputs.prepared-ref }}` continues to name
|
|
30
30
|
the original bundle. Do not create a new prepare run as recovery.
|
|
31
31
|
|
|
32
|
-
The trusted-publishing job installs the Node 22.22.2 and npm 11.
|
|
32
|
+
The trusted-publishing job installs the Node 22.22.2 and npm 11.11.0
|
|
33
33
|
publisher boundary explicitly. The reviewed prepare job has no OIDC or publish
|
|
34
34
|
authority; only its publish job installs that publisher toolchain.
|
|
35
35
|
Trusted configuration must name the invoking workflow and exact ref: the
|
|
@@ -49,7 +49,7 @@ explicit token-mode migration template for non-OIDC hosts; it names a
|
|
|
49
49
|
credential reference but never contains a credential value.
|
|
50
50
|
|
|
51
51
|
All public Action templates use the exact intended monorepo coordinate
|
|
52
|
-
`mannyc2/ts-release/apps/ts-release-action@v0.
|
|
52
|
+
`mannyc2/ts-release/apps/ts-release-action@v0.3.0`. Do not switch it to a
|
|
53
53
|
floating branch. Candidate certification must stop unless the immutable tag
|
|
54
54
|
can exist at the exact result commit before a package README naming it is
|
|
55
55
|
published.
|
|
@@ -19,38 +19,55 @@ permissions:
|
|
|
19
19
|
jobs:
|
|
20
20
|
release:
|
|
21
21
|
if: ${{ github.ref == 'refs/heads/main' && github.sha == inputs.candidate_sha }}
|
|
22
|
-
runs-on: ubuntu-
|
|
22
|
+
runs-on: ubuntu-24.04
|
|
23
23
|
permissions:
|
|
24
24
|
actions: read
|
|
25
25
|
contents: write
|
|
26
26
|
id-token: write
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
29
29
|
with:
|
|
30
30
|
persist-credentials: false
|
|
31
|
-
-
|
|
31
|
+
- id: setup-node
|
|
32
|
+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
32
33
|
with:
|
|
33
34
|
node-version: "22.22.2"
|
|
34
|
-
-
|
|
35
|
+
- id: setup-bun
|
|
36
|
+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
|
35
37
|
with:
|
|
36
38
|
bun-version: "1.3.14"
|
|
37
|
-
-
|
|
39
|
+
- id: release-tools
|
|
40
|
+
name: Bind exact release tool paths
|
|
41
|
+
env:
|
|
42
|
+
SETUP_BUN_PATH: ${{ steps.setup-bun.outputs.bun-path }}
|
|
43
|
+
run: |-
|
|
44
|
+
node_path="$(type -P node)"
|
|
45
|
+
bun_path="$(type -P bun)"
|
|
46
|
+
[[ "$node_path" = /* && "$bun_path" = /* ]]
|
|
47
|
+
[[ "$("$node_path" --version)" == v22.22.2 ]]
|
|
48
|
+
[[ "$("$bun_path" --version)" == 1.3.14 ]]
|
|
49
|
+
[[ "$(realpath "$bun_path")" == "$(realpath "$SETUP_BUN_PATH")" ]]
|
|
50
|
+
echo "node-bin=$node_path" >> "$GITHUB_OUTPUT"
|
|
51
|
+
echo "bun-bin=$bun_path" >> "$GITHUB_OUTPUT"
|
|
52
|
+
- run: bun add --global npm@11.11.0
|
|
38
53
|
- name: Prime isolated Bun dependency cache
|
|
39
54
|
if: ${{ inputs.prepared_ref == '' }}
|
|
40
55
|
run: |-
|
|
41
56
|
bun install --frozen-lockfile --ignore-scripts --no-save --linker=hoisted
|
|
42
57
|
bun --no-env-file --no-install -e 'await (await import("node:fs/promises")).rm("node_modules", { recursive: true, force: true })'
|
|
43
58
|
- id: release
|
|
44
|
-
uses: mannyc2/ts-release/apps/ts-release-action@v0.
|
|
59
|
+
uses: mannyc2/ts-release/apps/ts-release-action@v0.3.0
|
|
45
60
|
env:
|
|
46
61
|
GITHUB_TOKEN: ${{ github.token }}
|
|
62
|
+
TS_RELEASE_NODE_BIN: ${{ steps.release-tools.outputs.node-bin }}
|
|
63
|
+
TS_RELEASE_BUN_BIN: ${{ steps.release-tools.outputs.bun-bin }}
|
|
47
64
|
with:
|
|
48
65
|
command: ${{ inputs.prepared_ref == '' && 'release' || 'publish' }}
|
|
49
66
|
config: ${{ inputs.prepared_ref == '' && 'release.config.json' || '' }}
|
|
50
67
|
prepared: ${{ inputs.prepared_ref }}
|
|
51
68
|
- name: Upload redacted release report
|
|
52
69
|
if: ${{ always() && steps.release.outputs.report-ref != '' }}
|
|
53
|
-
uses: actions/upload-artifact@v4
|
|
70
|
+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
54
71
|
with:
|
|
55
72
|
name: ts-release-report-${{ github.run_attempt }}
|
|
56
73
|
path: ${{ steps.release.outputs.report-ref }}
|