Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .agents/skills/subtrack-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ subtrack is a Node.js CLI tool for managing subscription services from the termi

- Development execution: `tsx src/index.ts`
- Build: `tsdown` (configured as `pnpm build`)
- Type check: `pnpm lint:types` (`tsc --noEmit`)
- Don't use `bun` or `node --loader` for running TypeScript directly
- Build output is `subtrack/dist/index.mjs`

Expand Down Expand Up @@ -69,23 +70,26 @@ Keep concerns separated. Don't put DB queries in display logic or prompt logic i
## Code Style

- TypeScript strict mode enabled (see `tsconfig.json`)
- Test files (`*.test.ts`) excluded from `tsconfig.json` — type-checked separately by vitest
- ESM modules (`"type": "module"` in package.json)
- No semicolons in imports/exports
- Target: ESNext, module: Preserve, moduleResolution: bundler
- `verbatimModuleSyntax`: enforce correct import/value separation
- No semicolons (`"noUnusedLocals": false` and `"noUnusedParameters": false` are relaxed)
- `noUncheckedIndexedAccess`: disabled (codebase not compatible)

## Dependencies

Key packages and their purpose:

| Package | Usage |
|---|---|
| `commander` | CLI argument parsing (`Command`, `.command()`, `.option()`, `.action()`) |
|---|---|---|
| `gunshi` | CLI argument parsing (`cli()`, `define()`, `.args`, `.run()`) |
| `@inquirer/prompts` | Interactive prompts (`input`, `confirm`, `checkbox`, `select`) |
| `consola` | Logging (`consola.info`, `consola.success`, `consola.error`, `consola.warn`, `consola.fail`) |
| `cli-table3` | Table rendering (customizable chars, styles, column widths) |
| `picocolors` | Terminal colors |
| `sql.js` | SQLite via WASM (`new SQL.Database()`, `.run()`, `.exec()`) |
| `@types/node` | Node.js type definitions (dev) |

## Linting

Expand Down
18 changes: 18 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
subtrack:
- changed-files:
- any-glob-to-any-file: subtrack/**

docs:
- changed-files:
- any-glob-to-any-file: docs/**

ci:
- changed-files:
- any-glob-to-any-file: .github/workflows/**
- any-glob-to-any-file: .github/labeler.yml

dependencies:
- changed-files:
- any-glob-to-any-file: subtrack/package.json
- any-glob-to-any-file: package.json
- any-glob-to-any-file: pnpm-lock.yaml
62 changes: 24 additions & 38 deletions .github/workflows/app-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,39 @@ on:
branches: [main]
paths:
- "subtrack/**"
- ".github/**"
- ".agents/**"
- ".claude/**"
- "opencode.jsonc"
- ".github/workflows/check.yml"
- ".github/workflows/app-ci.yml"
- "pnpm-workspace.yaml"
- "package.json"
pull_request:
branches: [main]
paths:
- "subtrack/**"
- ".github/**"
- ".agents/**"
- ".claude/**"
- "opencode.jsonc"
- ".github/workflows/check.yml"
- ".github/workflows/app-ci.yml"
- "pnpm-workspace.yaml"
- "package.json"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
check:
uses: ./.github/workflows/check.yml

compatibility:
needs: check
strategy:
matrix:
os: [ubuntu-latest]
node-version: [22, 23, 24]
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: macos-latest
node-version: 22
- os: windows-latest
node-version: 22
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

Expand All @@ -38,43 +46,21 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Get pnpm store path
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm build

- run: pnpm test

- name: Verify CLI starts (built binary)
- name: Verify CLI runs (built binary)
if: runner.os != 'Windows'
working-directory: ./subtrack
run: node dist/index.mjs --help

- name: Verify CLI list command (dev mode)
working-directory: ./subtrack
run: pnpm start list

- name: Verify CLI export csv command
working-directory: ./subtrack
run: pnpm start export csv

- name: Verify CLI export md command
working-directory: ./subtrack
run: pnpm start export md

- name: Spell check with typos
if: runner.os == 'Linux'
- name: Spell check
if: runner.os == 'Linux' && matrix.node-version == 22
uses: crate-ci/typos@v1.47.2
with:
files: subtrack/
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: check

on:
workflow_call:

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
Comment on lines +12 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/check.yml

Repository: nazozokc/subtrack

Length of output: 695


🏁 Script executed:

# Check the file size first to avoid reading truncated output
wc -l .github/workflows/check.yml

Repository: nazozokc/subtrack

Length of output: 92


🌐 Web query:

GitHub Actions security best practices tag vs SHA pinning immutable reference

💡 Result:

For GitHub Actions, pinning to a full-length commit SHA is the recommended security best practice, whereas using tags (e.g., v4) is considered a mutable, less secure reference [1][2][3]. Why SHA Pinning is the Security Standard: - Immutability: Git tags can be moved, deleted, or recreated to point to different commits [4][5]. If a repository is compromised, an attacker can update an existing tag to point to malicious code, and your workflows will automatically run that new code without any changes to your workflow file [4][6][5]. - Protection against Retargeting: Pinning to a full-length commit SHA (e.g., @a1b2c3d4...) ensures your workflow always executes the exact, immutable code you originally reviewed and trusted, regardless of what happens to tags or branches in the upstream repository [7][4][5]. - Policy Enforcement: GitHub supports organization and repository-level policies that mandate SHA pinning [1][8][4]. When enforced, these policies cause workflows to fail if they use tags or branches, forcing adherence to immutable references [4][2]. Best Practices for Managing Pins: - Use Automation Tools: Manually updating SHAs is inefficient. Tools like Renovate or Dependabot can be configured to automatically open pull requests that bump SHAs while keeping a human-readable version tag in a comment (e.g., uses: owner/action@a1b2c3d4... # v4.2.0) [7][6]. Other tools like pinact can also automate the initial conversion of existing workflows to SHA-pinned references [2]. - Maintain Human-Readable Context: Always include the original tag or version in a trailing comment within the workflow file [7][6]. This allows automated tools to correlate the pinned SHA back to a semantic version for easier tracking [7]. - Consider the Full Dependency Tree: Remember that pinning a top-level action does not protect against "nested" dependencies [2]. Some actions may internally execute other actions, scripts, or Docker images that are not pinned [2]. - Beyond Pinning: While SHA pinning prevents tag-retargeting attacks, it does not protect against malicious code that is already present at the chosen SHA [7][6]. Always vet the actions you use and consider using GitHub’s policy features to explicitly allow or block specific actions across your organization [8][2][6]. As of 2026, GitHub is continuing to evolve its security roadmap to make workflows more deterministic, including plans to introduce a native dependencies section in workflow YAML to lock both direct and transitive dependencies [9].

Citations:


🌐 Web query:

GitHub Actions checkout persist-credentials security implications

💡 Result:

The persist-credentials setting in the GitHub Actions checkout action controls whether the authentication token (typically GITHUB_TOKEN) used for the checkout process is stored in the runner's local Git configuration [1][2]. By default, this is set to true [1][3]. Security Implications: - Credential Exposure: When enabled (the default), the token is persisted in the environment, making it potentially accessible to subsequent steps or third-party actions within the same workflow [3][4]. If a workflow creates and uploads build artifacts that include the repository directory (e.g., using actions/upload-artifact), there is a risk that these credentials could be inadvertently leaked if not properly excluded [4]. - Improved Security Mechanisms: Recent versions of the checkout action have improved credential security by storing tokens in a separate file under $RUNNER_TEMP rather than directly in.git/config [1][5]. While this reduces the risk of direct leakage through basic file operations, the underlying credential remains accessible to processes running within the job [1][6]. - Best Practices: Security guidance, including the OWASP GitHub Actions Security Cheat Sheet, recommends setting persist-credentials: false explicitly unless the workflow specifically requires the ability to perform authenticated Git operations (like git push) back to the repository [7][3]. If push access is needed, ensure the GITHUB_TOKEN has the minimum required permissions, and avoid relying on persistence if those operations can be performed via other authorized means [3]. For most workflows that only need to download code, disabling this setting is a recommended hardening step to adhere to the principle of least privilege [7][3].

Citations:


Harden action execution and token handling in CI.

Lines 10, 12, and 14 use mutable tag pins (@v4) instead of immutable commit SHAs, increasing supply-chain risk. Line 10's checkout action persists credentials by default and should explicitly disable this since the workflow only reads code. Add an explicit minimal permissions block scoped to read-only.

Suggested patch
+permissions:
+  contents: read
+
 jobs:
   check:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 # v4
+        with:
+          persist-credentials: false
 
-      - uses: pnpm/action-setup@v4
+      - uses: pnpm/action-setup@c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0 # v4
 
-      - uses: actions/setup-node@v4
+      - uses: actions/setup-node@b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0 # v4
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 10-10: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 10-10: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 12-12: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 14-14: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check.yml around lines 10 - 14, Replace the mutable
version tags (`@v4`) for the three actions (actions/checkout, pnpm/action-setup,
and actions/setup-node) with immutable commit SHAs to reduce supply-chain risk.
Additionally, add persist-credentials: false to the actions/checkout
configuration to disable default credential persistence since the workflow only
performs read-only operations. Finally, add a permissions block at the top level
of the workflow (after the name field) with minimal read-only permissions to
further harden token access.

Source: Linters/SAST tools

with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Type check
working-directory: ./subtrack
run: pnpm lint:types

- run: pnpm build

- run: pnpm test
29 changes: 29 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: codeql

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1"

permissions:
contents: read
security-events: write

jobs:
analyze:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [javascript-typescript]
steps:
- uses: actions/checkout@v4

- uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- uses: github/codeql-action/analyze@v3
Comment on lines +23 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unpinned actions across workflows
rg -nP '^\s*-\s*uses:\s*[^@]+@(?![0-9a-f]{40}$)[^ \n]+' .github/workflows

Repository: nazozokc/subtrack

Length of output: 1836


🏁 Script executed:

#!/bin/bash
# Check for security policy or pinning requirements
find . -maxdepth 2 -type f \( -name "*SECURITY*" -o -name "*POLICY*" -o -name "*security*" -o -name "*policy*" \) 2>/dev/null | head -20

Repository: nazozokc/subtrack

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check codeql.yml file content to see exact state
cat -n .github/workflows/codeql.yml

Repository: nazozokc/subtrack

Length of output: 772


🌐 Web query:

GitHub Actions best practices pinning commit SHA security risk tag retargeting

💡 Result:

The primary security risk of using tags or branch names (e.g., @v4, @main) to reference GitHub Actions is that these references are mutable [1][2][3]. Because a tag in Git is simply a movable pointer, an attacker who compromises an action's repository can retarget that tag to point to a malicious commit [4][5][6]. Downstream users who reference the action via that tag will then automatically pull and execute the malicious code without any changes to their own workflow files, as the resolution happens at runtime [5][7][6]. This "tag retargeting" or "tag poisoning" attack has been observed in several real-world incidents, including compromises affecting tj-actions (2025), xygeni-action (2026), and actions-cool (2026) [8][5][6][9]. Best Practices and Mitigation: 1. Pin to Full Commit SHAs: The only way to ensure an action is immutable is to reference it by its full-length (40-character) commit SHA [1][10]. This guarantees the exact code is executed every time, regardless of what happens to the tags in the source repository [2][8][11]. 2. Enforce via Organization Policy: Administrators can enable an organization-level policy in GitHub that mandates SHA pinning [12]. When active, any workflow attempting to use a non-pinned action will fail, effectively preventing the use of mutable references across the organization [12][2]. 3. Automate Maintenance: To manage the maintenance overhead of SHA pinning, use tools like Renovate or Dependabot [12][8][4]. For instance, Renovate offers presets (e.g., helpers:pinGitHubActionDigests) that automatically keep commit SHAs updated while allowing you to include the corresponding version tag as a comment for readability [8][4]. 4. Audit Sub-actions: Be aware that some actions internally reference other actions [2]. If you maintain an action, you should pin those internal references to full SHAs as well, as your users may have enforced organization-wide policies that will cause tag-based sub-references to fail [2]. 5. Defense in Depth: SHA pinning is a critical hardening step but should be part of a broader strategy. Continue to follow the principle of least privilege by limiting the secrets and permissions granted to workflows, and monitor for unexpected changes in runner behavior, as pinning does not protect against a malicious commit that you intentionally (but mistakenly) choose to pin [11][10][3].

Citations:


Pin all GitHub Actions to immutable commit SHAs.

These uses: references are tag-pinned only (@v4/@v3), which leaves the workflow open to upstream tag retargeting attacks. Best practice requires pinning to full-length commit SHAs.

Suggested hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<full-length-commit-sha> # v4

-      - uses: github/codeql-action/init@v3
+      - uses: github/codeql-action/init@<full-length-commit-sha> # v3

-      - uses: github/codeql-action/analyze@v3
+      - uses: github/codeql-action/analyze@<full-length-commit-sha> # v3
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 23-23: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 23-23: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 29-29: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/codeql.yml around lines 23 - 29, The GitHub Actions
references in the workflow are using version tags (`@v4` and `@v3`) which are
mutable and vulnerable to tag retargeting attacks. Replace each of the three
uses directives for actions/checkout, github/codeql-action/init, and
github/codeql-action/analyze with their full-length commit SHAs instead of the
tag references. Look up the commit SHA for each action version and replace the
`@vX` references with @<full-commit-sha> to ensure workflow integrity and
security.

Source: Linters/SAST tools

17 changes: 17 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: dependency-review

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4
Comment on lines +14 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the actual workflow file to confirm current state
cat -n .github/workflows/dependency-review.yml | head -20

Repository: nazozokc/subtrack

Length of output: 463


🌐 Web query:

GitHub Actions security best practices pinning actions by commit SHA

💡 Result:

Pinning GitHub Actions to a full-length commit SHA is a critical security best practice designed to mitigate supply chain risks by ensuring that the code executed in your workflows is immutable [1][2][3]. Why Pin to a Commit SHA Unlike version tags (e.g., @v4) or branch names (e.g., @main), which are mutable and can be updated by maintainers or malicious actors to point to different code at any time, a full-length commit SHA is content-addressed and immutable [4][5][3]. If a repository is compromised, an attacker can move an existing tag to point to malicious code, causing every workflow using that tag to automatically execute the attacker's changes [6][2]. Pinning to a SHA prevents this, as the workflow will continue to use the specific, verified commit regardless of any changes made to tags in the upstream repository [1][4]. Implementation Best Practices To maintain security while minimizing maintenance overhead, use the following approach: 1. Use Full SHAs: Always use the full 40-character commit SHA, not the short version, to ensure uniqueness and security [1][5]. 2. Include Human-Readable Comments: When pinning an action, include the version tag in a trailing comment [2][5]. This makes the workflow file readable and allows automated tools to identify which version the SHA corresponds to [2][3]. Example: - uses: actions/checkout@3df4ab1 # v4.0.0 3. Automate Updates: Use dependency management tools like Dependabot or Renovate to keep your pinned SHAs up to date [4][6][2]. These tools can be configured to automatically open pull requests when new versions of the action are released, ensuring you benefit from security patches without sacrificing the safety of pinned dependencies [6][2]. Organizational Policy GitHub now supports organization-level policies that mandate SHA pinning [7][4][2]. Administrators can enforce this requirement in the allowed actions policy; if enabled, any workflow attempting to use a tag or branch reference for an action will fail [7][4]. It is recommended to enable this policy to proactively secure your CI/CD environment [7][2]. Note for Action Maintainers If you maintain your own GitHub Actions, you should also pin any sub-actions or dependencies you reference internally to full commit SHAs [4]. This ensures that your action remains compatible with organizations that enforce strict SHA pinning policies [4]. Top Results: [1][7][4][6][2]

Citations:


🌐 Web query:

actions/checkout v4 recommended pinning practice GitHub security

💡 Result:

The recommended security practice for using actions/checkout (and all third-party GitHub Actions) is to pin them to a full-length, 40-character commit SHA rather than a version tag (e.g., @v4) or branch name [1][2][3]. Why Pin to a Commit SHA: Tags and branches are mutable references in Git, meaning they can be deleted, moved, or updated to point to different code at any time [4][5][6]. If an attacker compromises the action's repository or the maintainer's account, they could modify an existing tag to include malicious code, which would then automatically run in any workflow using that tag [5][7][3]. Pinning to an immutable, full-length commit SHA ensures that your workflow always executes the exact same code, mitigating the risk of supply chain attacks [1][8][6]. Recommended Implementation: To maintain both security and readability, use the commit SHA in your workflow configuration and include the version tag as an inline comment [4][6][3]. This allows you to benefit from immutability while still knowing which version is being used [5][8]. Example: - uses: actions/checkout@692973e # v4.1.7 Maintenance: Because pinned SHAs do not update automatically, they require maintenance to stay current [9][6]. It is highly recommended to use automated dependency management tools like Dependabot or Renovate [10][6][3]. These tools can be configured to monitor the action's repository and open pull requests to update your SHAs whenever a new tagged version is released, ensuring you receive security patches and feature updates while maintaining control over the upgrade process [9][6]. Organizational Policy: GitHub allows administrators to enforce this practice at the organization or repository level [1][11]. When such a policy is enabled, any workflow that references an action by a mutable tag will fail, requiring all actions (including nested dependencies) to be pinned to full commit SHAs [11]. Top Results: [1], [2], [6], [3]

Citations:


Pin workflow actions to full commit SHAs instead of version tags.

The actions actions/checkout@v4 and actions/dependency-review-action@v4 use mutable version tags that can be compromised. Pinning to full 40-character commit SHAs ensures supply-chain integrity by preventing tag-hijacking attacks. Include the version tag as an inline comment for readability and use Dependabot or Renovate to keep SHAs updated automatically.

Suggested hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
-      - uses: actions/dependency-review-action@v4
+      - uses: actions/dependency-review-action@c9b6f9d9f36f5e5c5e5c5e5c5e5c5e5c5e5c5e5c # v4.1.0
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 14-14: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 14-14: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dependency-review.yml around lines 14 - 15, The
actions/checkout@v4 and actions/dependency-review-action@v4 are using mutable
version tags that present a supply-chain security risk. Replace the version tags
(v4) with full 40-character commit SHAs for both actions. Include the original
version tag as an inline comment next to each action for readability and
maintainability. After making these changes, consider setting up Dependabot or
Renovate to automatically keep these commit SHAs updated when new versions are
released.

Source: Linters/SAST tools

with:
fail-on-severity: high
15 changes: 15 additions & 0 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: labeler

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin GitHub Action to a commit SHA

Line 15 uses a mutable tag (@v5). Pin to a full commit SHA to prevent upstream tag retargeting/supply-chain drift.

Suggested hardening
-      - uses: actions/labeler@v5
+      - uses: actions/labeler@<full-length-commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/labeler.yml at line 15, The actions/labeler action on line
15 is using a mutable tag (`@v5`) which creates a security risk through potential
upstream tag retargeting. Replace the tag reference with a full commit SHA to
pin the action to a specific immutable version. Change the uses line from
actions/labeler@v5 to actions/labeler@<full-commit-sha> where the commit SHA
corresponds to the desired release version, ensuring the workflow uses a fixed
version that cannot be altered upstream.

Source: Linters/SAST tools

103 changes: 75 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,58 +1,105 @@
name: Publish to npm
name: Release

on:
release:
types:
- published
types: [published]
workflow_dispatch:
inputs:
dry-run:
description: "Validate without publishing"
type: boolean
default: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

jobs:
check:
permissions:
contents: read
uses: ./.github/workflows/check.yml

publish:
needs: check
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest

permissions:
contents: read
contents: write
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 22
registry-url: https://registry.npmjs.org
cache: pnpm

- name: Set package.json version from release tag
- name: Validate version tag
if: github.event_name == 'release'
working-directory: ./subtrack
run: |
VERSION="${GITHUB_REF_NAME#v}"
node -e "const fs=require('fs');
const pkg=require('./package.json');
pkg.version='$VERSION';
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n');"
cat package.json
TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_VERSION=$(node -p "require('./package.json').version")

# Must be valid semver
if ! echo "$TAG_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "❌ Invalid semver: $TAG_VERSION"
Comment on lines +48 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten tag validation before injecting into node -e source.

Line 44 accepts prefixed matches only, so a crafted suffix can pass validation and be injected at Line 64. Validate the full string and avoid inline JS template injection.

Suggested patch
-          if ! echo "$TAG_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
+          if ! echo "$TAG_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
             echo "❌ Invalid semver: $TAG_VERSION"
             exit 1
           fi
@@
-          node -e "
-            const fs = require('fs')
-            const pkg = require('./package.json')
-            pkg.version = '${{ steps.version.outputs.version }}'
-            fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
-          "
+          VERSION='${{ steps.version.outputs.version }}' node -e "
+            const fs = require('node:fs')
+            const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
+            pkg.version = process.env.VERSION
+            fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
+          "

Also applies to: 61-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 44 - 45, The semver validation
regex pattern for TAG_VERSION on line 44 only checks for a prefix match using
^[0-9]+\.[0-9]+\.[0-9]+ which allows invalid suffixes like "-malicious" to pass
validation. This creates a security risk because the validated TAG_VERSION is
later injected into a node -e command at lines 61-65, enabling potential code
injection. Tighten the regex pattern by adding a $ anchor at the end to match
the complete string, ensuring only valid full semver versions (with no suffix)
pass validation before being used in the node command.

Source: Linters/SAST tools

exit 1
fi

- name: Install dependencies
run: pnpm install --frozen-lockfile
# Package version must match tag (or be incremented from it)
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "⚠ Package version ($PKG_VERSION) differs from tag ($TAG_VERSION) — will overwrite"
fi

- name: Build
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
echo "✅ Version: $TAG_VERSION"
id: version

- name: Set package.json version from tag
if: github.event_name == 'release'
working-directory: ./subtrack
run: pnpm build
run: |
node -e "
const fs = require('fs')
const pkg = require('./package.json')
pkg.version = '${{ steps.version.outputs.version }}'
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n')
"

- run: pnpm install --frozen-lockfile

- run: pnpm build

- name: Verify build output
- name: Verify build
working-directory: ./subtrack
run: |
test -f dist/index.mjs
ls -R dist
node -e "
const bytes = require('fs').readFileSync('dist/index.mjs').length;
const tag = '${{ steps.version.outputs.version }}';
console.log('📦 subtrack v' + tag + ' | dist/index.mjs (' + (bytes / 1024).toFixed(1) + ' KB)');
"

- name: Audit dependencies
- name: Dry run publish
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == true)
working-directory: ./subtrack
run: pnpm audit --audit-level=high
run: pnpm publish --no-git-checks --provenance --dry-run

- name: Publish to npm
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == false)
working-directory: ./subtrack
run: pnpm publish --no-git-checks --provenance

- name: Upload release asset
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ github.token }}
working-directory: ./subtrack
run: |
TAG="${{ github.event.release.tag_name }}"
gh release upload "$TAG" dist/index.mjs#subtrack.mjs
echo "✅ Uploaded subtrack.mjs to $TAG"
Loading
Loading