redlinegate 0.0.1 → 0.0.2

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.
Files changed (58) hide show
  1. package/README.md +22 -6
  2. package/dist/bin/redline.js +101 -11
  3. package/dist/bin/redline.js.map +1 -1
  4. package/dist/commands/init.js +94 -23
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/remove.js +3 -1
  7. package/dist/commands/remove.js.map +1 -1
  8. package/dist/commands/verify.js +82 -2
  9. package/dist/commands/verify.js.map +1 -1
  10. package/dist/config/redline-json.js +45 -1
  11. package/dist/config/redline-json.js.map +1 -1
  12. package/dist/detect/existing.js +151 -0
  13. package/dist/detect/existing.js.map +1 -0
  14. package/dist/exempt/parse.js +8 -1
  15. package/dist/exempt/parse.js.map +1 -1
  16. package/dist/platforms/azure/install.js +8 -4
  17. package/dist/platforms/azure/install.js.map +1 -1
  18. package/dist/platforms/azure/verify.js +6 -0
  19. package/dist/platforms/azure/verify.js.map +1 -1
  20. package/dist/platforms/github/install.js +88 -10
  21. package/dist/platforms/github/install.js.map +1 -1
  22. package/dist/platforms/github/preflight.js +63 -0
  23. package/dist/platforms/github/preflight.js.map +1 -0
  24. package/dist/platforms/github/verify.js +62 -5
  25. package/dist/platforms/github/verify.js.map +1 -1
  26. package/dist/platforms/types.js +4 -0
  27. package/dist/platforms/types.js.map +1 -1
  28. package/dist/policy/diff.js +26 -9
  29. package/dist/policy/diff.js.map +1 -1
  30. package/dist/render/contexts.js +39 -0
  31. package/dist/render/contexts.js.map +1 -0
  32. package/dist/render/profile.js +44 -10
  33. package/dist/render/profile.js.map +1 -1
  34. package/dist/render/standards.js +2 -0
  35. package/dist/render/standards.js.map +1 -1
  36. package/dist/render/vendors.js +5 -1
  37. package/dist/render/vendors.js.map +1 -1
  38. package/dist/ui/facts.js +82 -0
  39. package/dist/ui/facts.js.map +1 -0
  40. package/dist/ui/prompt.js +201 -0
  41. package/dist/ui/prompt.js.map +1 -0
  42. package/dist/ui/tty.js +243 -0
  43. package/dist/ui/tty.js.map +1 -0
  44. package/dist/ui/wizard.js +179 -0
  45. package/dist/ui/wizard.js.map +1 -0
  46. package/package.json +10 -1
  47. package/platforms/azure/gate-template-github.yml +152 -0
  48. package/platforms/azure/gate-template.yml +59 -6
  49. package/scripts/check-pins.mjs +84 -1
  50. package/standards/contexts/speckit.md +22 -0
  51. package/standards/contexts/tmf.md +25 -0
  52. package/standards/manifest.json +1 -1
  53. package/templates/redline.yml +5 -9
  54. package/workflows/dashboard.yml +1 -1
  55. package/workflows/redline-collect.yml +1 -1
  56. package/workflows/redline-gate.yml +10 -3
  57. package/workflows/seed-canary.yml +2 -2
  58. package/workflows/weekly-digest.yml +1 -1
@@ -0,0 +1,179 @@
1
+ import { RUNGS } from "../enforce/ladder.js";
2
+ // Plain-English, at the moment of choosing. Every one of these was a term the
3
+ // operator met for the first time in output that assumed they knew it.
4
+ const RUNG_HINTS = {
5
+ observe: 'comments only — the check is always green. Start here',
6
+ warn: 'comments and labels the PR — still never blocks a merge',
7
+ 'block-blocker': 'a BLOCKER finding fails the check and stops the merge',
8
+ 'block-high': 'BLOCKER and HIGH both stop the merge — the strictest rung',
9
+ };
10
+ const CAPABILITY_HINTS = {
11
+ gate: 'the pull request check that runs the review',
12
+ 'merge-policy': 'branch protection requiring the check and an approval',
13
+ labels: 'the labels the gate uses to mark and exempt a pull request',
14
+ 'review-ownership': 'seeds CODEOWNERS — off unless you ask, it can block every PR',
15
+ };
16
+ // Which Redline check a tool the repository already runs makes redundant.
17
+ // Phrased as what the operator would otherwise get twice, because "stands down
18
+ // the dependencies job" means nothing to someone meeting Redline today.
19
+ const STAND_DOWN_LABEL = {
20
+ secrets: 'secret scanning',
21
+ dependencies: 'dependency vulnerability review',
22
+ policy: 'static analysis',
23
+ };
24
+ function profileChoices(manifest, detected) {
25
+ const ids = Object.keys(manifest.profiles).sort((a, b) => {
26
+ if (a === detected)
27
+ return -1;
28
+ if (b === detected)
29
+ return 1;
30
+ return a.localeCompare(b);
31
+ });
32
+ return ids.map((id) => {
33
+ // The stacks' own titles, not their ids. `JavaScript, React (web)` tells an
34
+ // operator what they are choosing; `javascript, react` makes them guess at
35
+ // the difference between two profiles that share a prefix.
36
+ const stacks = (manifest.profiles[id] ?? []).map((stack) => manifest.stacks[stack]?.title ?? stack);
37
+ return {
38
+ value: id,
39
+ label: id,
40
+ hint: stacks.join(', ') + (id === detected ? ' (detected)' : ''),
41
+ };
42
+ });
43
+ }
44
+ // Every vendor the standards know about, including the ones this organisation
45
+ // has switched off — shown greyed with the reason rather than filtered out.
46
+ //
47
+ // Hiding them was wrong for the same reason hiding a covered capability would
48
+ // be: an operator looking for Cursor and not finding it cannot tell whether
49
+ // Redline forgot about Cursor, does not support it, or was told not to render
50
+ // for it here. Only the last is true, and only the last is something they can
51
+ // go and change.
52
+ function vendorChoices(manifest) {
53
+ return Object.entries(manifest.vendors).map(([id, vendor]) => ({
54
+ value: id,
55
+ label: id,
56
+ ...(vendor.enabled
57
+ ? { hint: vendor.title }
58
+ : // Reason first: a long vendor title truncates, and the half worth
59
+ // keeping is the part that says why the row cannot be picked.
60
+ { disabled: `not enabled for this organisation — ${vendor.title}` }),
61
+ }));
62
+ }
63
+ /**
64
+ * Run the menu.
65
+ *
66
+ * Every question is skippable in the sense that it has a preselected answer;
67
+ * none is skippable in the sense of being hidden. A capability the repository
68
+ * already covers is still shown — deselected, with the tool that covers it
69
+ * named — because an operator who cannot see a decision cannot overrule it,
70
+ * and this repository's detection is wrong often enough that overruling has to
71
+ * stay one keystroke away.
72
+ */
73
+ export async function runWizard(p, facts) {
74
+ const { manifest, survey, recorded } = facts;
75
+ p.intro('Redline');
76
+ // Multi-select, because a repository is routinely more than one bundle: a
77
+ // React application with its own Terraform beside it is `web,infra`, and a
78
+ // single-choice question made it pick the half that fitted worst.
79
+ //
80
+ // `.redline.json` still records one string — resolveProfile takes the list
81
+ // and sorts it — so nothing downstream had to learn a new shape.
82
+ const recordedProfiles = recorded?.profile?.split(',').map((s) => s.trim());
83
+ const profiles = await p.multiselect('Which standards apply here?', profileChoices(manifest, facts.detectedProfile), recordedProfiles ?? [facts.detectedProfile]);
84
+ // Deselecting everything renders no rules at all, which is never what the
85
+ // operator meant by "none of these" — they meant they could not find theirs.
86
+ // Falling back to what was detected keeps the run useful and keeps the answer
87
+ // visible in the summary, where it can be changed.
88
+ const profile = profiles.length > 0 ? profiles.join(',') : facts.detectedProfile;
89
+ const hostChoices = [
90
+ {
91
+ value: 'github',
92
+ label: 'GitHub',
93
+ hint: 'GitHub.com or GitHub Enterprise Server' + (facts.detectedHost === 'github' ? ' (detected)' : ''),
94
+ },
95
+ {
96
+ value: 'azure',
97
+ label: 'Azure DevOps',
98
+ // Deliberately no hostnames: cli/platforms/__tests__/boundary.test.ts
99
+ // holds the line that only an adapter knows what a host's URLs look like,
100
+ // and a menu hint is not an adapter.
101
+ hint: 'Azure Repos' + (facts.detectedHost === 'azure' ? ' (detected)' : ''),
102
+ },
103
+ ];
104
+ const host = await p.select('Where does this repository live?', hostChoices, facts.detectedHost ?? 'github');
105
+ // Asked separately from the host because the two genuinely come apart: every
106
+ // VFUK repository is on github.com and built by Azure Pipelines. Deriving the
107
+ // pipeline from the host is what put a GitHub Actions caller into a repo
108
+ // whose real gate is `cicd/pre-merge.yaml`.
109
+ const pipelineChoices = [
110
+ {
111
+ value: 'github-actions',
112
+ label: 'GitHub Actions',
113
+ hint: 'a workflow under .github/workflows',
114
+ },
115
+ {
116
+ value: 'azure-pipelines',
117
+ label: 'Azure Pipelines',
118
+ hint: facts.existingPipeline === null
119
+ ? 'a stage in an Azure pipeline definition'
120
+ : `a stage added to ${facts.existingPipeline} (detected)`,
121
+ },
122
+ ];
123
+ const pipeline = await p.select('What runs your pull request checks?', pipelineChoices, facts.existingPipeline !== null ? 'azure-pipelines' : 'github-actions');
124
+ const vendors = await p.multiselect('Which assistants should read the standards?', vendorChoices(manifest), [...(recorded?.vendors ?? facts.detectedVendors)]);
125
+ const contexts = await p.multiselect('Extra context to render beside the rules', [
126
+ {
127
+ value: 'speckit',
128
+ label: 'Spec-driven development',
129
+ hint: 'how to work from a spec — dropped automatically if you already run Spec Kit',
130
+ },
131
+ {
132
+ value: 'tmf',
133
+ label: 'TM Forum',
134
+ hint: 'resource naming, @type, paging, error bodies. Only if you implement TMF APIs',
135
+ },
136
+ ], ['speckit']);
137
+ // A capability a detected tool already covers starts deselected and says
138
+ // which tool. `standDown` is advisory: nothing here is decided for the
139
+ // operator, only defaulted.
140
+ const covered = new Map();
141
+ for (const tool of survey.tools) {
142
+ if (tool.standsDown === null)
143
+ continue;
144
+ covered.set(tool.standsDown, tool.label);
145
+ }
146
+ const capabilityChoices = Object.entries(CAPABILITY_HINTS).map(([id, hint]) => ({ value: id, label: id, hint }));
147
+ const capabilities = await p.multiselect('What should Redline install?', capabilityChoices, ['gate', 'merge-policy', 'labels']);
148
+ if (covered.size > 0) {
149
+ const named = [...covered]
150
+ .map(([job, label]) => `${label} already covers ${STAND_DOWN_LABEL[job] ?? job}`)
151
+ .join('; ');
152
+ p.note(`${named}. Redline adds its own on top rather than replacing them.`);
153
+ }
154
+ const rung = await p.select('How hard should the check bite?', RUNGS.map((value) => ({ value, label: value, hint: RUNG_HINTS[value] })), recorded?.rung ?? 'observe');
155
+ const action = await p.select('Ready?', [
156
+ {
157
+ value: 'dry-run',
158
+ label: 'Dry run',
159
+ hint: 'print the plan — writes nothing, contacts no host, needs no credential',
160
+ },
161
+ {
162
+ value: 'apply',
163
+ label: 'Apply',
164
+ hint: 'write the files and open a pull request on redline/onboard',
165
+ },
166
+ ], 'dry-run');
167
+ return {
168
+ profile,
169
+ vendors,
170
+ host,
171
+ pipeline,
172
+ speckit: contexts.includes('speckit'),
173
+ tmf: contexts.includes('tmf'),
174
+ capabilities,
175
+ rung,
176
+ action,
177
+ };
178
+ }
179
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../cli/ui/wizard.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "redlinegate",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Engineering control plane for AI-assisted development: standards, merge gates and evidence across GitHub and Azure DevOps",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "homepage": "https://redline-gate.vercel.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/moelzanaty3/redline.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/moelzanaty3/redline/issues"
14
+ },
15
+ "author": "Mohamed Elzanaty (https://github.com/moelzanaty3)",
8
16
  "keywords": [
9
17
  "code-review",
10
18
  "ai-code-review",
@@ -28,6 +36,7 @@
28
36
  "redlinegate": "dist/bin/redline.js"
29
37
  },
30
38
  "files": [
39
+ "README.md",
31
40
  "LICENSE",
32
41
  "dist/",
33
42
  "scripts/",
@@ -0,0 +1,152 @@
1
+ # Managed by Redline; regenerate with `redline init` rather than editing here.
2
+ # Redline merge gate for a GitHub-hosted repository built by Azure Pipelines.
3
+ #
4
+ # This is the combination the estate actually runs: the code lives on GitHub,
5
+ # the pull request checks are Azure Pipelines. Neither of Redline's two original
6
+ # paths fitted it — the GitHub path writes an Actions workflow the repository
7
+ # never runs, and the Azure path installs a Build Validation policy against an
8
+ # Azure Repos repository that does not exist here.
9
+ #
10
+ # CONTRACT: this pipeline reports to GitHub as a check named after its own
11
+ # pipeline definition, published by the Azure Pipelines GitHub App. There is no
12
+ # status POST here, unlike the Azure Repos template beside it: on a GitHub-hosted
13
+ # repository the build result IS the check, and posting to the Azure Repos
14
+ # statuses API would address a pull request that does not exist. The name a
15
+ # branch ruleset must require is therefore the pipeline's name in Azure DevOps —
16
+ # `redline init` prints it, and `redline verify` reads the reported names back.
17
+ #
18
+ # A `pr:` block IS honoured here. Azure Pipelines ignores `pr:` triggers only
19
+ # for Azure *Repos*; for a GitHub-hosted repository they are the trigger
20
+ # mechanism, which is why the Azure Repos template beside this one has none.
21
+
22
+ pr:
23
+ branches:
24
+ include:
25
+ - main
26
+ - master
27
+
28
+ trigger: none
29
+
30
+ pool:
31
+ vmImage: ubuntu-latest
32
+
33
+ variables:
34
+ ADR_DIFF_THRESHOLD: 300
35
+ FAIL_ON_DEPENDENCY_SEVERITY: high
36
+ SOFT_FAIL_LABELS: redline-exempt,redline-sync
37
+ # Declared with a default so the final step can read it even when the scan
38
+ # passed and never set it. An undeclared Azure variable interpolates as the
39
+ # literal string "$(NAME)" rather than empty, which is the kind of quiet
40
+ # wrong answer this gate exists to prevent.
41
+ REDLINE_SECURITY_FAILED: 'false'
42
+ # Pinned by image digest, not by tag: a tag is mutable and this scans the diff
43
+ # of every pull request in the repository. The version in the trailing comment
44
+ # is what scripts/check-pins.mjs re-resolves the digest against, exactly as it
45
+ # does for the SHA-pinned actions on the GitHub side of the gate.
46
+ TRUFFLEHOG_IMAGE: trufflesecurity/trufflehog@sha256:deb2af10659a488a14d262a323addcde099d99827a1cf1dc4e93c17915c39f08 # 3.97.1
47
+
48
+ steps:
49
+ - checkout: self
50
+ fetchDepth: 0
51
+
52
+ - task: NodeTool@0
53
+ displayName: Node 22
54
+ inputs:
55
+ versionSpec: '22.x'
56
+
57
+ # No substitute in Mend or SonarQube: both scan a state of the tree, and a
58
+ # credential committed and reverted inside one pull request is still in the
59
+ # history the moment it merges. This scans the RANGE.
60
+ #
61
+ # `--results=verified` only: `unknown` produces false positives and this
62
+ # check cannot be waived by a label.
63
+ - script: |
64
+ set -euo pipefail
65
+ target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
66
+ # The PR build checks out a merge commit, and the target branch is not
67
+ # guaranteed to be a local ref on a fresh agent. Fetch it by name before
68
+ # asking for a merge base, or `git merge-base` fails and the scan silently
69
+ # degrades to scanning nothing.
70
+ git fetch --no-tags origin "$target"
71
+ base=$(git merge-base FETCH_HEAD HEAD)
72
+ echo "scanning $base..HEAD"
73
+
74
+ # Recorded BEFORE the exit, because the exemption branch in the final step
75
+ # must never downgrade this one.
76
+ if ! docker run --rm -v "$(pwd):/repo" "$TRUFFLEHOG_IMAGE" \
77
+ git file:///repo --since-commit "$base" --results=verified --fail
78
+ then
79
+ echo "##vso[task.setvariable variable=REDLINE_SECURITY_FAILED]true"
80
+ exit 1
81
+ fi
82
+ displayName: Secret scan (diff)
83
+ name: secrets
84
+ env:
85
+ TRUFFLEHOG_IMAGE: $(TRUFFLEHOG_IMAGE)
86
+
87
+ # --package pins npx to the redlinegate package explicitly. `redline` alone
88
+ # names a different, unrelated package on the public registry.
89
+ - script: npx --yes --package=redlinegate@latest redline verify --gate
90
+ displayName: Redline gate
91
+ name: gate
92
+ # A failed secret scan must not hide the configuration findings: the author
93
+ # deserves the whole list in one run rather than one blocker at a time.
94
+ condition: succeededOrFailed()
95
+ env:
96
+ GH_TOKEN: $(GH_TOKEN)
97
+ ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
98
+ FAIL_ON_DEPENDENCY_SEVERITY: $(FAIL_ON_DEPENDENCY_SEVERITY)
99
+ SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
100
+
101
+ - script: |
102
+ set -euo pipefail
103
+ if [ "$AGENT_JOBSTATUS" = "Succeeded" ]; then
104
+ echo "gate passed"
105
+ exit 0
106
+ fi
107
+
108
+ # The secret scan is never waivable, exactly as on the GitHub side where
109
+ # the security jobs sit outside label exemption. A label that could waive
110
+ # a verified credential in the diff would make the label the
111
+ # vulnerability.
112
+ if [ "${REDLINE_SECURITY_FAILED:-}" = "true" ]; then
113
+ echo "##vso[task.logissue type=error]The secret scan failed. This check cannot be waived by a label."
114
+ exit 1
115
+ fi
116
+
117
+ # Soft-fail escape hatch: a pull request carrying one of SOFT_FAIL_LABELS
118
+ # reports success instead of blocking, so a reviewer can accept a process
119
+ # failure deliberately. It can only ever turn a failure into a success.
120
+ #
121
+ # Fails CLOSED when no token is configured. Without GH_TOKEN the labels
122
+ # cannot be read, and "could not check for an exemption" must block rather
123
+ # than wave through — an unreadable exemption is not an exemption.
124
+ if [ -z "${GH_TOKEN:-}" ] || [ -z "${SOFT_FAIL_LABELS:-}" ]; then
125
+ echo "##vso[task.logissue type=error]Redline gate failed."
126
+ exit 1
127
+ fi
128
+
129
+ pr="${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER:-}"
130
+ repo="${BUILD_REPOSITORY_NAME:-}"
131
+ # The token must never appear in argv — visible via `ps` to any process on
132
+ # the agent. It is already an env var; hand it to curl through a stdin
133
+ # config instead of putting it on the command line with -H.
134
+ labels=$(printf 'header = "Authorization: Bearer %s"\n' "$GH_TOKEN" \
135
+ | curl -K - -sS -H "Accept: application/vnd.github+json" \
136
+ "https://api.github.com/repos/${repo}/issues/${pr}/labels") || labels=''
137
+
138
+ for label in ${SOFT_FAIL_LABELS//,/ }; do
139
+ if printf '%s' "$labels" | jq -e --arg l "$label" '[.[]?.name] | index($l)' >/dev/null 2>&1; then
140
+ echo "##vso[task.logissue type=warning]Redline gate failed but the \"$label\" label is applied — a reviewer accepted this deliberately"
141
+ exit 0
142
+ fi
143
+ done
144
+
145
+ echo "##vso[task.logissue type=error]Redline gate failed."
146
+ exit 1
147
+ displayName: Redline gate verdict
148
+ condition: always()
149
+ env:
150
+ GH_TOKEN: $(GH_TOKEN)
151
+ SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
152
+ REDLINE_SECURITY_FAILED: $(REDLINE_SECURITY_FAILED)
@@ -22,6 +22,18 @@ variables:
22
22
  ADR_DIFF_THRESHOLD: 300
23
23
  FAIL_ON_DEPENDENCY_SEVERITY: high
24
24
  SOFT_FAIL_LABELS: redline-exempt,redline-sync
25
+ # Declared with a default so the status step can read it even when the scan
26
+ # passed and never set it. An undeclared Azure variable interpolates as the
27
+ # literal string "$(NAME)" rather than empty, which is the kind of quiet
28
+ # wrong answer this gate exists to prevent.
29
+ REDLINE_SECURITY_FAILED: 'false'
30
+ # Pinned by image digest, not by tag: a tag is mutable and this scans the diff
31
+ # of every pull request in the repository. The version in the trailing comment
32
+ # is what scripts/check-pins.mjs re-resolves the digest against, exactly as it
33
+ # does for the SHA-pinned actions on the GitHub side of the gate. Docker Hub
34
+ # spells the tag without the `v` the git tag carries; both sides of the gate
35
+ # deliberately run the same version.
36
+ TRUFFLEHOG_IMAGE: trufflesecurity/trufflehog@sha256:deb2af10659a488a14d262a323addcde099d99827a1cf1dc4e93c17915c39f08 # 3.97.1
25
37
 
26
38
  steps:
27
39
  - checkout: self
@@ -32,12 +44,48 @@ steps:
32
44
  inputs:
33
45
  versionSpec: '22.x'
34
46
 
47
+ # The GitHub gate runs this as its own job (workflows/redline-gate.yml,
48
+ # `secrets`). It has no substitute in Mend or SonarQube: both scan a state of
49
+ # the tree, and a credential committed and reverted inside one pull request
50
+ # is still in the history the moment it merges. This scans the RANGE.
51
+ #
52
+ # `--results=verified` only, matching the GitHub job for the same reason:
53
+ # `unknown` produces false positives and this check cannot be waived.
54
+ - script: |
55
+ set -euo pipefail
56
+ target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
57
+ # The PR build checks out a merge commit, and the target branch is not
58
+ # guaranteed to be a local ref on a fresh agent. Fetch it by name before
59
+ # asking for a merge base, or `git merge-base` fails and the scan silently
60
+ # degrades to scanning nothing.
61
+ git fetch --no-tags origin "$target"
62
+ base=$(git merge-base FETCH_HEAD HEAD)
63
+ echo "scanning $base..HEAD"
64
+
65
+ # The failure is recorded BEFORE the exit, because the exemption branch in
66
+ # the status step below must never downgrade this one. On GitHub the
67
+ # security jobs sit outside label exemption; this variable is how that
68
+ # exclusion survives the two being one pipeline here.
69
+ if ! docker run --rm -v "$(pwd):/repo" "$TRUFFLEHOG_IMAGE" \
70
+ git file:///repo --since-commit "$base" --results=verified --fail
71
+ then
72
+ echo "##vso[task.setvariable variable=REDLINE_SECURITY_FAILED]true"
73
+ exit 1
74
+ fi
75
+ displayName: Secret scan (diff)
76
+ name: secrets
77
+ env:
78
+ TRUFFLEHOG_IMAGE: $(TRUFFLEHOG_IMAGE)
79
+
35
80
  # --package pins npx to the redlinegate package explicitly. `redline` alone
36
81
  # names a different, unrelated package on the public registry — `redline`
37
82
  # is only this package's bin name, never resolve npx against it directly.
38
83
  - script: npx --yes --package=redlinegate@latest redline verify --gate
39
84
  displayName: Redline gate
40
85
  name: gate
86
+ # A failed secret scan must not hide the configuration findings: the author
87
+ # deserves the whole list in one run rather than one blocker at a time.
88
+ condition: succeededOrFailed()
41
89
  env:
42
90
  SYSTEM_ACCESSTOKEN: $(System.AccessToken)
43
91
  ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
@@ -72,12 +120,16 @@ steps:
72
120
  # accept a process failure deliberately. It can only ever turn a
73
121
  # failure into a success, never the reverse.
74
122
  #
75
- # NOTE for whoever splits this job up: on GitHub the security jobs
76
- # (dependency-review, secrets) are excluded from the exemption. Here the
77
- # gate is one `redline verify --gate` run that checks configuration
78
- # drift only no scanning step exists to downgrade. Adding one means
79
- # excluding it from this branch, exactly as the GitHub workflow does.
80
- if [ "$state" = "failed" ] && [ -n "${SOFT_FAIL_LABELS:-}" ]; then
123
+ # The secret scan is excluded, exactly as it is on GitHub, where the
124
+ # security jobs sit outside the exemption. There the exclusion is
125
+ # structural separate jobs, and the label only reaches some of them.
126
+ # Here the whole gate is one pipeline, so the scan records
127
+ # REDLINE_SECURITY_FAILED before it exits and this branch refuses to
128
+ # downgrade it. A label that could waive a verified credential in the
129
+ # diff would make the label the vulnerability.
130
+ if [ "${REDLINE_SECURITY_FAILED:-}" = "true" ]; then
131
+ echo "##vso[task.logissue type=error]The secret scan failed. This check cannot be waived by a label."
132
+ elif [ "$state" = "failed" ] && [ -n "${SOFT_FAIL_LABELS:-}" ]; then
81
133
  labels=$(curl_authed -sS "$pr_url/labels?api-version=7.1") || labels=''
82
134
  for label in ${SOFT_FAIL_LABELS//,/ }; do
83
135
  if printf '%s' "$labels" \
@@ -103,3 +155,4 @@ steps:
103
155
  env:
104
156
  SYSTEM_ACCESSTOKEN: $(System.AccessToken)
105
157
  SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
158
+ REDLINE_SECURITY_FAILED: $(REDLINE_SECURITY_FAILED)
@@ -39,6 +39,14 @@ const files = [
39
39
  ? readdirSync(join(ROOT, '.github/workflows')).map((f) => `.github/workflows/${f}`)
40
40
  : []),
41
41
  'templates/redline.yml',
42
+ // The Azure gate. It was outside this list while it pinned nothing, and the
43
+ // moment it pinned a container digest that omission would have made the pin
44
+ // unverifiable — the exact rot this script exists to catch.
45
+ 'platforms/azure/gate-template.yml',
46
+ // The same gate for a GitHub-hosted repository built by Azure Pipelines. It
47
+ // carries the same container digest, so leaving it out here would let the two
48
+ // halves of the same gate drift to different versions of the scanner.
49
+ 'platforms/azure/gate-template-github.yml',
42
50
  ].filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
43
51
 
44
52
  const pins = [];
@@ -84,5 +92,80 @@ for (const pin of pins) {
84
92
  }
85
93
  }
86
94
 
87
- console.log(`\n${pins.length} pin(s), ${errors} error(s), ${updates} update(s) available`);
95
+ // The Azure gate runs its secret scan from a container rather than an action,
96
+ // so the pin is an image digest with the version in a trailing comment — the
97
+ // same shape as `uses: repo@sha # tag`, verified the same way. A digest that no
98
+ // longer matches the tag it claims means someone edited one and not the other,
99
+ // and the scan of every Azure pull request is then running an unreviewed image.
100
+ const IMAGE_PIN = /([\w.-]+\/[\w.-]+)@(sha256:[0-9a-f]{64})\s*#\s*(\S+)/g;
101
+ const imagePins = [];
102
+ for (const file of files) {
103
+ const body = readFileSync(join(ROOT, file), 'utf8');
104
+ for (const [, image, digest, tag] of body.matchAll(IMAGE_PIN)) {
105
+ imagePins.push({ file, image, digest, tag });
106
+ }
107
+ }
108
+
109
+ for (const pin of imagePins) {
110
+ try {
111
+ const res = await fetch(`https://hub.docker.com/v2/repositories/${pin.image}/tags/${pin.tag}`);
112
+ if (!res.ok) throw new Error(`docker hub: ${res.status}`);
113
+ const resolved = (await res.json()).digest;
114
+ if (resolved !== pin.digest) {
115
+ console.error(
116
+ `FAIL ${pin.file}: ${pin.image}@${pin.digest} is commented as ${pin.tag}, but ${pin.tag} resolves to ${resolved}`
117
+ );
118
+ errors += 1;
119
+ continue;
120
+ }
121
+ console.log(`ok ${pin.image}:${pin.tag} → ${pin.digest.slice(7, 19)}`);
122
+ } catch (err) {
123
+ console.error(`FAIL ${pin.file}: could not verify ${pin.image}:${pin.tag} — ${err.message}`);
124
+ errors += 1;
125
+ }
126
+ }
127
+
128
+ // The gate also pins the CLI it shells out to, as `REDLINE_CLI_VERSION` rather
129
+ // than as a `uses:` SHA, so the loop above cannot see it. It is the pin that
130
+ // rots hardest: it is a plain literal in two places, nothing in the release
131
+ // wires it to a publish, and until someone edits it by hand a fix shipped in
132
+ // the CLI reaches no onboarded repository at all — the gate keeps running the
133
+ // version named here. A published version behind the latest is reported the
134
+ // same way a stale action tag is.
135
+ const CLI_PIN = /REDLINE_CLI_VERSION:\s*'([^']+)'/g;
136
+ const cliPins = [];
137
+ for (const file of files) {
138
+ const body = readFileSync(join(ROOT, file), 'utf8');
139
+ for (const [, version] of body.matchAll(CLI_PIN)) cliPins.push({ file, version });
140
+ }
141
+
142
+ if (cliPins.length) {
143
+ const pinned = [...new Set(cliPins.map((p) => p.version))];
144
+ if (pinned.length > 1) {
145
+ console.error(
146
+ `FAIL REDLINE_CLI_VERSION disagrees with itself: ${pinned.join(', ')} — every job in one gate must run the same CLI`
147
+ );
148
+ errors += 1;
149
+ }
150
+ try {
151
+ const res = await fetch('https://registry.npmjs.org/redlinegate');
152
+ if (!res.ok) throw new Error(`registry: ${res.status}`);
153
+ const latest = (await res.json())['dist-tags']?.latest;
154
+ for (const version of pinned) {
155
+ if (latest && latest !== version) {
156
+ console.warn(` update available: redlinegate@${version} → ${latest}`);
157
+ updates += 1;
158
+ } else {
159
+ console.log(`ok redlinegate@${version} is the published latest`);
160
+ }
161
+ }
162
+ } catch (err) {
163
+ console.error(`FAIL could not check redlinegate against the registry — ${err.message}`);
164
+ errors += 1;
165
+ }
166
+ }
167
+
168
+ console.log(
169
+ `\n${pins.length + cliPins.length + imagePins.length} pin(s), ${errors} error(s), ${updates} update(s) available`
170
+ );
88
171
  process.exit(errors || (STRICT && updates) ? 1 : 0);
@@ -0,0 +1,22 @@
1
+ # Context: spec-driven development
2
+
3
+ This repository works spec-first. A change begins as a specification, becomes a
4
+ plan, becomes tasks, and only then becomes code. Treat the specification as the
5
+ statement of intent that the diff is measured against.
6
+
7
+ When reviewing:
8
+
9
+ - Measure the diff against the specification it claims to implement, not against
10
+ what the code appears to be trying to do. Code that works and does something
11
+ the spec does not ask for is still a finding — say which section it departs
12
+ from.
13
+ - A change with no specification is not automatically wrong. Trivial fixes,
14
+ dependency bumps and revert commits do not need one. A new capability does.
15
+ - Where the specification and the code disagree, the specification is not
16
+ automatically right either. Say which one you believe is wrong and why, rather
17
+ than silently assuming the text wins.
18
+ - Do not restate the specification back to the author. They wrote it.
19
+
20
+ Spec Kit is a separate tool with its own installer and its own templates.
21
+ Redline does not create, edit or version its files; this section only tells a
22
+ reviewer that the repository works this way.
@@ -0,0 +1,25 @@
1
+ # Context: TM Forum
2
+
3
+ This repository implements TM Forum interfaces. The published Open API and ODA
4
+ specifications are part of the contract, and a consumer integrating against them
5
+ is entitled to what those documents describe.
6
+
7
+ When reviewing:
8
+
9
+ - Resource and field names follow the published TMF API for the domain. A field
10
+ renamed for local convenience breaks every consumer generated from the spec.
11
+ - Polymorphism carries `@type`, and `@baseType` / `@schemaLocation` where the
12
+ API defines them. Dropping them because "nothing reads it here" removes the
13
+ discriminator a consumer needs.
14
+ - Collections page with `offset` and `limit`, filter through query parameters,
15
+ and honour `fields` for attribute selection. A hand-rolled paging scheme on a
16
+ TMF resource is a contract break.
17
+ - Errors use the TMF error body — `code`, `reason`, and `message` where present
18
+ — rather than a local error shape.
19
+ - Notifications follow the hub/listener pattern the API defines rather than a
20
+ bespoke webhook.
21
+ - A breaking change to a published interface needs a version, not an edit. Say
22
+ so explicitly when you see one.
23
+
24
+ State the specific TMF API and version when a finding depends on it, so the
25
+ author can check the same document you did.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "Single source of truth for Redline. scripts/render.mjs turns this into vendor-specific artifacts. Never hand-edit generated output.",
3
- "version": "0.0.3",
3
+ "version": "0.0.2",
4
4
  "core": {
5
5
  "title": "Redline Core Engineering Standards",
6
6
  "source": "standards/core.md"
@@ -1,13 +1,9 @@
1
- # Managed by Redline; regenerate with `redline init` rather than editing here.
2
- # Redline caller. Installed into every onboarded repo as .github/workflows/redline.yml
3
- # by `redline init`. Kept in sync by workflows/redline-sync.yml once `redline sync`
4
- # ships in Phase 3 — see CHANGELOG.md.
1
+ # Managed by Redline. Regenerate with `redline init`; edits here are overwritten.
5
2
  #
6
- # DO NOT RENAME THE JOB. The branch ruleset requires the check "redline-gate / gate",
7
- # which is derived from this job id plus the aggregate job id in the reusable workflow.
8
- # Renaming it makes the required check unreportable and blocks every PR in this repo.
9
- #
10
- # Replace <org> with the organisation that hosts the `.github` repo.
3
+ # Keep the job id `redline-gate`: the branch ruleset requires the check
4
+ # `redline-gate / gate`, which is built from this id. Renaming it makes that
5
+ # check unreportable.
6
+
11
7
  name: Redline
12
8
 
13
9
  on:
@@ -73,7 +73,7 @@ jobs:
73
73
  env:
74
74
  STANDARDS_VERSION: ${{ vars.STANDARDS_VERSION }}
75
75
  run: |
76
- npx --yes "redlinegate@0.0.1" metrics dashboard \
76
+ npx --yes "redlinegate@0.0.2" metrics dashboard \
77
77
  --org "${{ github.repository_owner }}" \
78
78
  --data data \
79
79
  --days 90 \