@metasession.co/devaudit-cli 0.3.10 → 0.3.12
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/index.js +110 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/HOST_ADAPTER.md +17 -0
- package/sdlc/SKILLS.md +8 -0
- package/sdlc/ai-rules/SDLC_RULES.md +27 -7
- package/sdlc/files/_common/0-project-setup.md +4 -4
- package/sdlc/files/_common/scripts/extract-release-metadata.sh +38 -0
- package/sdlc/files/_common/scripts/extract-release-metadata.test.sh +26 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.sh +11 -0
- package/sdlc/files/_common/scripts/generate-bundled-changes.test.sh +4 -0
- package/sdlc/files/_common/scripts/upload-evidence.sh +31 -1
- package/sdlc/files/_common/scripts/upload-evidence.test.sh +119 -0
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.sh +153 -2
- package/sdlc/files/_common/scripts/validate-compliance-artifacts.test.sh +155 -0
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +36 -1
- package/sdlc/files/ci/check-release-approval.yml.template +18 -28
- package/sdlc/files/ci/ci-status-fallback.yml.template +4 -0
- package/sdlc/files/ci/ci.yml.template +13 -7
- package/sdlc/files/ci/compliance-evidence.yml.template +30 -29
- package/sdlc/files/hosts/_schema/adapter.schema.json +31 -0
- package/sdlc/files/hosts/railway/adapter.json +8 -1
- package/sdlc/package.json +1 -1
- package/sdlc/src/blueprints/3-compile-evidence.raw.md +68 -0
- package/sdlc/src/blueprints/4-submit-for-review.raw.md +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command, Option } from 'commander';
|
|
|
3
3
|
import { createConsola } from 'consola';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import { join, resolve, basename, dirname, relative } from 'path';
|
|
6
|
-
import { promises } from 'fs';
|
|
6
|
+
import { promises, openAsBlob } from 'fs';
|
|
7
7
|
import envPaths from 'env-paths';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { validateManifest } from '@metasession.co/devaudit-plugin-sdk';
|
|
@@ -56,7 +56,7 @@ function emitJsonResult(payload) {
|
|
|
56
56
|
|
|
57
57
|
// package.json
|
|
58
58
|
var package_default = {
|
|
59
|
-
version: "0.3.
|
|
59
|
+
version: "0.3.12"};
|
|
60
60
|
|
|
61
61
|
// src/lib/version.ts
|
|
62
62
|
var CLI_VERSION = package_default.version;
|
|
@@ -602,6 +602,7 @@ var INITIAL_BACKOFF_MS = 1e3;
|
|
|
602
602
|
var DEFAULT_PRESIGNED_THRESHOLD_BYTES = 26214400;
|
|
603
603
|
var DEFAULT_PRESIGNED_MAX_ATTEMPTS = 3;
|
|
604
604
|
var DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS = 300;
|
|
605
|
+
var STUB_PREFIX_BYTES = 4096;
|
|
605
606
|
function maxAttempts() {
|
|
606
607
|
const raw = Number.parseInt(process.env["UPLOAD_MAX_ATTEMPTS"] ?? "", 10);
|
|
607
608
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_ATTEMPTS;
|
|
@@ -629,20 +630,71 @@ async function collectFiles(filePath) {
|
|
|
629
630
|
throw new Error(`${filePath} is neither a file nor a directory.`);
|
|
630
631
|
}
|
|
631
632
|
var STUB_BANNER = /STARTER TEMPLATE.+REPLACE BEFORE/;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
633
|
+
var TEXT_STUB_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
634
|
+
".md",
|
|
635
|
+
".markdown",
|
|
636
|
+
".txt",
|
|
637
|
+
".json",
|
|
638
|
+
".yml",
|
|
639
|
+
".yaml",
|
|
640
|
+
".csv",
|
|
641
|
+
".html",
|
|
642
|
+
".xml",
|
|
643
|
+
".log"
|
|
644
|
+
]);
|
|
635
645
|
function delay(ms) {
|
|
636
646
|
return new Promise((res) => setTimeout(res, ms));
|
|
637
647
|
}
|
|
638
|
-
function
|
|
648
|
+
function extensionOf(file) {
|
|
649
|
+
const dot = file.lastIndexOf(".");
|
|
650
|
+
return dot === -1 ? "" : file.slice(dot).toLowerCase();
|
|
651
|
+
}
|
|
652
|
+
function isTextLikeEvidenceFile(file) {
|
|
653
|
+
return TEXT_STUB_EXTENSIONS.has(extensionOf(file));
|
|
654
|
+
}
|
|
655
|
+
async function readFilePrefix(file, bytes) {
|
|
656
|
+
const handle = await promises.open(file, "r");
|
|
657
|
+
try {
|
|
658
|
+
const buf = Buffer.alloc(bytes);
|
|
659
|
+
const { bytesRead } = await handle.read(buf, 0, bytes, 0);
|
|
660
|
+
return buf.subarray(0, bytesRead).toString("utf-8");
|
|
661
|
+
} finally {
|
|
662
|
+
await handle.close();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function isUneditedStub(file) {
|
|
666
|
+
if (!isTextLikeEvidenceFile(file)) return false;
|
|
667
|
+
const prefix = await readFilePrefix(file, STUB_PREFIX_BYTES);
|
|
668
|
+
return STUB_BANNER.test(prefix);
|
|
669
|
+
}
|
|
670
|
+
async function createUploadSource(file) {
|
|
671
|
+
const mimeType = deriveMimeType(file);
|
|
672
|
+
const { size } = await promises.stat(file);
|
|
673
|
+
try {
|
|
674
|
+
return {
|
|
675
|
+
blob: await openAsBlob(file, { type: mimeType }),
|
|
676
|
+
size,
|
|
677
|
+
mimeType
|
|
678
|
+
};
|
|
679
|
+
} catch {
|
|
680
|
+
return {
|
|
681
|
+
blob: new Blob([await promises.readFile(file)], { type: mimeType }),
|
|
682
|
+
size,
|
|
683
|
+
mimeType
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function buildUploadForm(file, source, opts) {
|
|
688
|
+
const metadata = {
|
|
689
|
+
...opts.metadata ?? {},
|
|
690
|
+
...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
|
|
691
|
+
};
|
|
639
692
|
const form = new FormData();
|
|
640
|
-
|
|
641
|
-
form.set("file", blob, basename(file));
|
|
693
|
+
form.set("file", source.blob, basename(file));
|
|
642
694
|
form.set("projectSlug", opts.projectSlug);
|
|
643
695
|
form.set("requirementId", opts.requirementId);
|
|
644
696
|
form.set("evidenceType", opts.evidenceType);
|
|
645
|
-
form.set("metadata", JSON.stringify(
|
|
697
|
+
form.set("metadata", JSON.stringify(metadata));
|
|
646
698
|
if (opts.releaseVersion) form.set("releaseVersion", opts.releaseVersion);
|
|
647
699
|
if (opts.createReleaseIfMissing) form.set("createReleaseIfMissing", "true");
|
|
648
700
|
if (opts.environment) form.set("environment", opts.environment);
|
|
@@ -654,6 +706,8 @@ function buildUploadForm(file, buf, opts) {
|
|
|
654
706
|
if (opts.gateStatus) form.set("gateStatus", opts.gateStatus);
|
|
655
707
|
if (opts.sdlcStage) form.set("sdlcStage", opts.sdlcStage);
|
|
656
708
|
if (opts.testCycleId) form.set("testCycleId", opts.testCycleId);
|
|
709
|
+
if (opts.sentinelContent) form.set("sentinelContent", opts.sentinelContent);
|
|
710
|
+
if (opts.commitTimestamp) form.set("commitTimestamp", opts.commitTimestamp);
|
|
657
711
|
return form;
|
|
658
712
|
}
|
|
659
713
|
function uploadFailureMessage(err, timeoutSeconds) {
|
|
@@ -684,7 +738,7 @@ async function probeBaseUrlDrift(baseUrl) {
|
|
|
684
738
|
return null;
|
|
685
739
|
}
|
|
686
740
|
}
|
|
687
|
-
async function uploadOne(file,
|
|
741
|
+
async function uploadOne(file, source, opts) {
|
|
688
742
|
const attempts = maxAttempts();
|
|
689
743
|
const timeoutSeconds = uploadMaxTimeSeconds();
|
|
690
744
|
const url = `${opts.baseUrl.replace(/\/$/, "")}/api/evidence/upload`;
|
|
@@ -698,7 +752,7 @@ async function uploadOne(file, buf, opts) {
|
|
|
698
752
|
res = await fetch(url, {
|
|
699
753
|
method: "POST",
|
|
700
754
|
headers: { authorization: `Bearer ${opts.apiKey}` },
|
|
701
|
-
body: buildUploadForm(file,
|
|
755
|
+
body: buildUploadForm(file, source, opts),
|
|
702
756
|
signal: controller.signal
|
|
703
757
|
});
|
|
704
758
|
} catch (err) {
|
|
@@ -731,15 +785,17 @@ async function uploadOne(file, buf, opts) {
|
|
|
731
785
|
return { file, ok: false, status: 0, error: "max retries exhausted" };
|
|
732
786
|
}
|
|
733
787
|
var PRESIGNED_FALLBACK = /* @__PURE__ */ Symbol("presigned-fallback");
|
|
734
|
-
async function uploadPresigned(file,
|
|
788
|
+
async function uploadPresigned(file, source, opts) {
|
|
735
789
|
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
736
790
|
const presignUrl = `${baseUrl}/api/evidence/upload-url`;
|
|
737
791
|
const completeUrl = `${baseUrl}/api/evidence/upload-complete`;
|
|
738
792
|
const maxAttemptsPresigned = DEFAULT_PRESIGNED_MAX_ATTEMPTS;
|
|
739
793
|
const timeoutSeconds = uploadMaxTimeSeconds();
|
|
740
794
|
const presignedTimeoutSeconds = DEFAULT_PRESIGNED_UPLOAD_MAX_TIME_SECONDS;
|
|
741
|
-
const
|
|
742
|
-
|
|
795
|
+
const metadata = {
|
|
796
|
+
...opts.metadata ?? {},
|
|
797
|
+
...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
|
|
798
|
+
};
|
|
743
799
|
let uploadUrl = "";
|
|
744
800
|
let evidenceId = "";
|
|
745
801
|
let attempt = 1;
|
|
@@ -760,8 +816,8 @@ async function uploadPresigned(file, buf, opts) {
|
|
|
760
816
|
requirementId: opts.requirementId,
|
|
761
817
|
evidenceType: opts.evidenceType,
|
|
762
818
|
fileName: basename(file),
|
|
763
|
-
fileSizeBytes:
|
|
764
|
-
mimeType,
|
|
819
|
+
fileSizeBytes: source.size,
|
|
820
|
+
mimeType: source.mimeType,
|
|
765
821
|
metadata,
|
|
766
822
|
...opts.releaseVersion ? { releaseVersion: opts.releaseVersion } : {},
|
|
767
823
|
...opts.createReleaseIfMissing ? { createReleaseIfMissing: true } : {},
|
|
@@ -770,8 +826,11 @@ async function uploadPresigned(file, buf, opts) {
|
|
|
770
826
|
...opts.evidenceCategory ? { evidenceCategory: opts.evidenceCategory } : {},
|
|
771
827
|
...opts.releaseTitle ? { releaseTitle: opts.releaseTitle } : {},
|
|
772
828
|
...opts.releaseSummary ? { releaseSummary: opts.releaseSummary } : {},
|
|
829
|
+
...opts.changeType ? { changeType: opts.changeType } : {},
|
|
773
830
|
...opts.sdlcStage ? { sdlcStage: opts.sdlcStage } : {},
|
|
774
|
-
...opts.testCycleId ? { testCycleId: opts.testCycleId } : {}
|
|
831
|
+
...opts.testCycleId ? { testCycleId: opts.testCycleId } : {},
|
|
832
|
+
...opts.sentinelContent ? { sentinelContent: opts.sentinelContent } : {},
|
|
833
|
+
...opts.commitTimestamp ? { commitTimestamp: opts.commitTimestamp } : {}
|
|
775
834
|
}),
|
|
776
835
|
signal: controller.signal
|
|
777
836
|
});
|
|
@@ -816,8 +875,8 @@ async function uploadPresigned(file, buf, opts) {
|
|
|
816
875
|
try {
|
|
817
876
|
res = await fetch(uploadUrl, {
|
|
818
877
|
method: "PUT",
|
|
819
|
-
headers: { "content-type": mimeType },
|
|
820
|
-
body:
|
|
878
|
+
headers: { "content-type": source.mimeType },
|
|
879
|
+
body: source.blob,
|
|
821
880
|
signal: controller.signal
|
|
822
881
|
});
|
|
823
882
|
} catch (err) {
|
|
@@ -898,25 +957,26 @@ async function uploadEvidence(opts) {
|
|
|
898
957
|
}
|
|
899
958
|
const results = [];
|
|
900
959
|
for (const file of files) {
|
|
901
|
-
|
|
902
|
-
if (isUneditedStub(buf)) {
|
|
960
|
+
if (await isUneditedStub(file)) {
|
|
903
961
|
results.push({ file, ok: true, status: 0, skipped: true });
|
|
904
962
|
continue;
|
|
905
963
|
}
|
|
906
|
-
|
|
907
|
-
|
|
964
|
+
const source = await createUploadSource(file);
|
|
965
|
+
if (source.size >= DEFAULT_PRESIGNED_THRESHOLD_BYTES) {
|
|
966
|
+
const presignedResult = await uploadPresigned(file, source, opts);
|
|
908
967
|
if (presignedResult !== PRESIGNED_FALLBACK) {
|
|
909
968
|
results.push(presignedResult);
|
|
910
969
|
continue;
|
|
911
970
|
}
|
|
912
971
|
}
|
|
913
|
-
results.push(await uploadOne(file,
|
|
972
|
+
results.push(await uploadOne(file, source, opts));
|
|
914
973
|
}
|
|
915
974
|
return results;
|
|
916
975
|
}
|
|
917
976
|
|
|
918
977
|
// src/commands/push.ts
|
|
919
978
|
var DEFAULT_BASE_URL3 = "https://devaudit.metasession.co";
|
|
979
|
+
var TRACKED_CHANGE_TYPES = /* @__PURE__ */ new Set(["feat", "fix", "refactor", "perf", "compliance", "revert"]);
|
|
920
980
|
function buildMetadata(options) {
|
|
921
981
|
const metadata = {};
|
|
922
982
|
if (options.gitSha) metadata["gitSha"] = options.gitSha;
|
|
@@ -928,6 +988,28 @@ function buildMetadata(options) {
|
|
|
928
988
|
}
|
|
929
989
|
return metadata;
|
|
930
990
|
}
|
|
991
|
+
async function resolveSentinelContext(options) {
|
|
992
|
+
if (!options.changeType || !TRACKED_CHANGE_TYPES.has(options.changeType)) {
|
|
993
|
+
return {};
|
|
994
|
+
}
|
|
995
|
+
const sentinelPath = join(process.cwd(), ".sdlc-implementer-invoked");
|
|
996
|
+
let sentinelContent;
|
|
997
|
+
try {
|
|
998
|
+
const raw = await promises.readFile(sentinelPath, "utf8");
|
|
999
|
+
if (raw.trim()) sentinelContent = raw;
|
|
1000
|
+
} catch {
|
|
1001
|
+
sentinelContent = void 0;
|
|
1002
|
+
}
|
|
1003
|
+
let commitTimestamp;
|
|
1004
|
+
try {
|
|
1005
|
+
const target = options.gitSha?.trim() ? options.gitSha.trim() : "HEAD";
|
|
1006
|
+
const { stdout } = await execa("git", ["show", "-s", "--format=%cI", target], { reject: false });
|
|
1007
|
+
if (stdout.trim()) commitTimestamp = stdout.trim();
|
|
1008
|
+
} catch {
|
|
1009
|
+
commitTimestamp = void 0;
|
|
1010
|
+
}
|
|
1011
|
+
return { sentinelContent, commitTimestamp };
|
|
1012
|
+
}
|
|
931
1013
|
function validateOptions(options) {
|
|
932
1014
|
if (options.environment && !options.release) {
|
|
933
1015
|
return "--environment requires --release (evidence without a release is orphaned)";
|
|
@@ -998,6 +1080,8 @@ async function runPush(options) {
|
|
|
998
1080
|
await runHook(plugins, "beforePush", ctx);
|
|
999
1081
|
}
|
|
1000
1082
|
const metadata = buildMetadata(options);
|
|
1083
|
+
const { sentinelContent, commitTimestamp } = await resolveSentinelContext(options);
|
|
1084
|
+
if (commitTimestamp) metadata["commitTimestamp"] = commitTimestamp;
|
|
1001
1085
|
const results = await uploadEvidence({
|
|
1002
1086
|
projectSlug: options.projectSlug,
|
|
1003
1087
|
requirementId: options.requirementId,
|
|
@@ -1016,6 +1100,8 @@ async function runPush(options) {
|
|
|
1016
1100
|
...options.gateStatus !== void 0 ? { gateStatus: options.gateStatus } : {},
|
|
1017
1101
|
...options.sdlcStage !== void 0 ? { sdlcStage: options.sdlcStage } : {},
|
|
1018
1102
|
...options.testCycleId !== void 0 ? { testCycleId: options.testCycleId } : {},
|
|
1103
|
+
...sentinelContent !== void 0 ? { sentinelContent } : {},
|
|
1104
|
+
...commitTimestamp !== void 0 ? { commitTimestamp } : {},
|
|
1019
1105
|
metadata
|
|
1020
1106
|
});
|
|
1021
1107
|
let okCount = 0;
|