From 8b86b489616ab0d3d705d97d8ece2906e4bddbc6 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Mon, 22 Dec 2025 08:34:29 -0600 Subject: [PATCH] Add warning for short SHA refs in uses (#260) --- languageservice/src/validate.ts | 31 +++ .../src/validate.uses-format.test.ts | 196 ++++++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/languageservice/src/validate.ts b/languageservice/src/validate.ts index ba26904..2365fd6 100644 --- a/languageservice/src/validate.ts +++ b/languageservice/src/validate.ts @@ -272,6 +272,31 @@ function validateCronExpression(diagnostics: Diagnostic[], token: StringToken): } } +// Matches a short SHA (7-8 hex characters) that looks like it should be a full SHA +const SHORT_SHA_PATTERN = /^[0-9a-f]{7,8}$/i; +const SHORT_SHA_DOCS_URL = + "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions"; + +/** + * Checks if a ref looks like a short SHA and adds a warning if so. + * Returns true if a warning was added. + */ +function warnIfShortSha(diagnostics: Diagnostic[], token: StringToken, ref: string): boolean { + if (SHORT_SHA_PATTERN.test(ref)) { + diagnostics.push({ + message: `The provided ref '${ref}' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.`, + severity: DiagnosticSeverity.Warning, + range: mapRange(token.range), + code: "short-sha-ref", + codeDescription: { + href: SHORT_SHA_DOCS_URL + } + }); + return true; + } + return false; +} + /** * Validates the format of a step's `uses` field. * @@ -343,6 +368,9 @@ function validateStepUsesFormat(diagnostics: Diagnostic[], token: StringToken): }); return; } + + // Warn if ref looks like a short SHA + warnIfShortSha(diagnostics, token, gitRef); } function addStepUsesFormatError(diagnostics: Diagnostic[], token: StringToken): void { @@ -501,6 +529,9 @@ function validateWorkflowUsesFormat(diagnostics: Diagnostic[], token: StringToke addWorkflowUsesFormatError(diagnostics, token, "invalid workflow file name"); return; } + + // Warn if version looks like a short SHA + warnIfShortSha(diagnostics, token, version); } function addWorkflowUsesFormatError(diagnostics: Diagnostic[], token: StringToken, reason: string): void { diff --git a/languageservice/src/validate.uses-format.test.ts b/languageservice/src/validate.uses-format.test.ts index b4fec04..248dcce 100644 --- a/languageservice/src/validate.uses-format.test.ts +++ b/languageservice/src/validate.uses-format.test.ts @@ -891,4 +891,200 @@ jobs: }); }); }); + + describe("short SHA warnings", () => { + describe("step uses", () => { + it("warns on 7-char short SHA", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@a1b2c3d +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([ + { + message: + "The provided ref 'a1b2c3d' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.", + severity: DiagnosticSeverity.Warning, + range: { + start: {line: 5, character: 12}, + end: {line: 5, character: 36} + }, + code: "short-sha-ref", + codeDescription: { + href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions" + } + } + ]); + }); + + it("warns on 8-char short SHA", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@a1b2c3d4 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([ + { + message: + "The provided ref 'a1b2c3d4' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.", + severity: DiagnosticSeverity.Warning, + range: { + start: {line: 5, character: 12}, + end: {line: 5, character: 37} + }, + code: "short-sha-ref", + codeDescription: { + href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions" + } + } + ]); + }); + + it("does not warn on full SHA (40 chars)", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on tag ref", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on branch ref", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on Docker action", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: docker://alpine:3.8 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on local action", async () => { + const input = `on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: ./my-action +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + }); + + describe("workflow uses", () => { + it("warns on 7-char short SHA in reusable workflow", async () => { + const input = `on: push +jobs: + test: + uses: owner/repo/.github/workflows/ci.yml@a1b2c3d +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([ + { + message: + "The provided ref 'a1b2c3d' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.", + severity: DiagnosticSeverity.Warning, + range: { + start: {line: 3, character: 10}, + end: {line: 3, character: 53} + }, + code: "short-sha-ref", + codeDescription: { + href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions" + } + } + ]); + }); + + it("warns on 8-char short SHA in reusable workflow", async () => { + const input = `on: push +jobs: + test: + uses: owner/repo/.github/workflows/ci.yml@a1b2c3d4 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([ + { + message: + "The provided ref 'a1b2c3d4' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.", + severity: DiagnosticSeverity.Warning, + range: { + start: {line: 3, character: 10}, + end: {line: 3, character: 54} + }, + code: "short-sha-ref", + codeDescription: { + href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions" + } + } + ]); + }); + + it("does not warn on full SHA in reusable workflow", async () => { + const input = `on: push +jobs: + test: + uses: owner/repo/.github/workflows/ci.yml@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on tag ref in reusable workflow", async () => { + const input = `on: push +jobs: + test: + uses: owner/repo/.github/workflows/ci.yml@v1 +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + + it("does not warn on local workflow", async () => { + const input = `on: push +jobs: + test: + uses: ./.github/workflows/ci.yml +`; + const result = await validate(createDocument("wf.yaml", input)); + expect(result).toEqual([]); + }); + }); + }); });