mirror of
https://github.com/awalsh128/cache-apt-pkgs-action.git
synced 2026-07-06 09:04:38 +00:00
remove docs, add best practices scripts and fix CI with v1 action tests
This commit is contained in:
parent
3f70aca939
commit
b3e03779c4
15
.eslintrc.cjs
Normal file
15
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
ignorePatterns: ["dist"],
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true
|
||||
}
|
||||
};
|
||||
35
.github/.codecov.yml
vendored
Normal file
35
.github/.codecov.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
codecov:
|
||||
require_ci_to_pass: true
|
||||
|
||||
comment:
|
||||
layout: "reach, diff, flags, files"
|
||||
behavior: default
|
||||
require_changes: false # Set to true to only comment if coverage changes
|
||||
require_base: false # Set to true to require a base commit to compare
|
||||
require_head: true # Set to true to require a head commit
|
||||
hide_project_coverage: false # Set to true to hide project-wide coverage
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto # Target coverage is the previous commit's coverage
|
||||
threshold: 1% # Allow a 1% drop without failing
|
||||
base: auto
|
||||
if_no_uploads: error
|
||||
if_not_found: success
|
||||
if_ci_failed: error
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
base: auto
|
||||
if_no_uploads: error
|
||||
if_not_found: success
|
||||
if_ci_failed: error
|
||||
|
||||
ignore:
|
||||
- "dist/**"
|
||||
- "test/**"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.spec.ts"
|
||||
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
10
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Basic template with warning about known issues.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: awalsh128
|
||||
|
||||
---
|
||||
|
||||
Please read about the limitation of [non-file dependencies](https://github.com/awalsh128/cache-apt-pkgs-action/blob/master/README.md#non-file-dependencies) before filing an issue.
|
||||
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
IMPORTANT: Please read about the limitation of [non-file dependencies](https://github.com/awalsh128/cache-apt-pkgs-action/blob/master/README.md#non-file-dependencies) before filing an issue.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
85
.github/actions/codehealth/action.yml
vendored
85
.github/actions/codehealth/action.yml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
name: "Verify code health and quality"
|
||||
description: "Runs linters, type checks, build, tests or coverage, optional CodeQL analysis, and API docs generation"
|
||||
|
||||
inputs:
|
||||
run_tests:
|
||||
description: "Whether to run tests as part of verification (default: true)"
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
run_coverage:
|
||||
description: "Whether to run coverage checks (default: false). When true, coverage run replaces normal tests."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
run_codeql:
|
||||
description: "Whether to run CodeQL analysis (default: false)"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Initialize CodeQL
|
||||
if: ${{ inputs.run_codeql == 'true' }}
|
||||
uses: "github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357" # v3.36.2
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
|
||||
- name: Autobuild CodeQL
|
||||
if: ${{ inputs.run_codeql == 'true' }}
|
||||
uses: "github/codeql-action/autobuild@dd903d2e4f5405488e5ef1422510ee31c8b32357" # v3.36.2
|
||||
|
||||
- name: Lint
|
||||
shell: bash
|
||||
run: npm run lint
|
||||
|
||||
- name: Typecheck
|
||||
shell: bash
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Type Coverage
|
||||
shell: bash
|
||||
run: npm run typecoverage
|
||||
|
||||
- name: Check Action Pinning
|
||||
shell: bash
|
||||
run: |
|
||||
go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest
|
||||
|
||||
pinact run -check .github/actions/codehealth/action.yml
|
||||
pinact run -check .github/actions/setup-env/action.yml
|
||||
pinact run -check .github/workflows/ci.yml
|
||||
pinact run -check .github/workflows/release.yml
|
||||
|
||||
- name: Verify ts-apt dependency reference is not locally switched
|
||||
shell: bash
|
||||
run: |
|
||||
if bash ./dev_scripts/switch_tsapt_local.sh --is_local_switched; then
|
||||
echo "Error: ts-apt dependency is switched to a local path. Please switch it back before running CI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: npm run build
|
||||
|
||||
- name: Generate API docs
|
||||
shell: bash
|
||||
run: npm run docs:api
|
||||
|
||||
- name: Tests
|
||||
if: ${{ inputs.run_tests == 'true' && inputs.run_coverage != 'true' }}
|
||||
shell: bash
|
||||
run: npm test
|
||||
|
||||
- name: Coverage
|
||||
if: ${{ inputs.run_coverage == 'true' }}
|
||||
shell: bash
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Analyze CodeQL
|
||||
if: ${{ inputs.run_codeql == 'true' }}
|
||||
uses: "github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357" # v3.36.2
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
23
.github/actions/setup-env/action.yml
vendored
23
.github/actions/setup-env/action.yml
vendored
|
|
@ -1,23 +0,0 @@
|
|||
name: "Setup environment"
|
||||
description: "Checks out code, sets up Node.js, and installs dependencies"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
|
||||
- name: Verify ts-apt dependency is not locally switched
|
||||
shell: bash
|
||||
run: |
|
||||
if bash ./dev_scripts/switch_tsapt_local.sh --is_local_switched; then
|
||||
echo "Error: ts-apt dependency is switched to a local workspace path. Switch it back before pushing or opening a pull request."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: npm ci
|
||||
67
.github/actions/setup/action.yml
vendored
Normal file
67
.github/actions/setup/action.yml
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
name: "Setup environment"
|
||||
description: "Checks out code, sets up Node.js, and builds the project or fetches cached build artifacts, caching build artifacts for faster subsequent runs"
|
||||
|
||||
inputs:
|
||||
operation:
|
||||
description: "The operation to perform. Can be 'build' or 'fetch' (default: 'fetch')."
|
||||
required: true
|
||||
default: "fetch"
|
||||
values:
|
||||
- "build"
|
||||
- "fetch"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: "npm"
|
||||
|
||||
- name: Get Bundle Artifacts Name
|
||||
id: get-artifacts-name
|
||||
shell: bash
|
||||
env:
|
||||
bundle_artifacts_name: "action-bundle-${{ github.event.pull_request.head.sha || github.sha }}"
|
||||
run: |
|
||||
echo "bundle-artifacts-name=${{ env.bundle_artifacts_name }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Try Fetch Bundle Artifacts
|
||||
if: ${{ inputs.operation == 'fetch' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: ${{ env.bundle-artifacts-name }}
|
||||
|
||||
- name: Check Fetch Bundle Artifacts
|
||||
id: check-artifact
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -d "dist" ] && [ "$(ls -A dist)" ]; then
|
||||
echo "artifacts_present=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "artifacts_present=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Fail Fetch Bundle Artifacts
|
||||
if: ${{ inputs.operation == 'fetch' && env.artifacts_present == 'false' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "No artifacts found for the current commit. Please run the workflow with 'build' operation first."
|
||||
exit 1
|
||||
|
||||
- name: Bundle Action
|
||||
if: ${{ inputs.operation == 'build' && env.artifacts_present == 'false' }}
|
||||
shell: bash
|
||||
run: npm run bundle
|
||||
|
||||
- name: Upload Bundle Artifacts
|
||||
if: ${{ inputs.operation == 'build' && env.artifacts_present == 'false' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ env.bundle-artifacts-name }}
|
||||
path: |
|
||||
action.yml
|
||||
dist/
|
||||
retention-days: 1
|
||||
752
.github/workflows/ci.yml
vendored
752
.github/workflows/ci.yml
vendored
File diff suppressed because it is too large
Load diff
85
.github/workflows/release.yml
vendored
85
.github/workflows/release.yml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag (for example v0.2.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [24]
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup-env
|
||||
|
||||
- name: Release preflight (tag/version)
|
||||
run: npm run release:preflight
|
||||
|
||||
- name: Check code health
|
||||
uses: ./.github/actions/codehealth
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup-env
|
||||
|
||||
- name: Build package
|
||||
run: npm run build
|
||||
|
||||
- name: Resolve npm dist-tag
|
||||
run: |
|
||||
TAG="${{ github.ref_type == 'tag' && github.ref_name || inputs.tag }}"
|
||||
if [[ "$TAG" == *-* ]]; then
|
||||
echo "NPM_DIST_TAG=next" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "NPM_DIST_TAG=latest" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Generate API docs
|
||||
run: npm run docs:api
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public --provenance --tag "$NPM_DIST_TAG"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: "softprops/action-gh-release@2bb465e97f322d3cb2a965294d483e0d26a67aa9" # v3.0.1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || inputs.tag }}
|
||||
|
||||
- name: Setup Pages
|
||||
uses: "actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d" # v6.0.0
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: "actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9" # v5.0.0
|
||||
with:
|
||||
path: docs/api
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: "actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128" # v5.0.0
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
target/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
|
|
|
|||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}",
|
||||
}
|
||||
]
|
||||
}
|
||||
111
AGENTS.md
Normal file
111
AGENTS.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# AGENT PROFILE
|
||||
|
||||
You are an expert developer focused on:
|
||||
|
||||
- TypeScript
|
||||
- Bash
|
||||
- DevOps and CI/CD
|
||||
- Git and GitHub workflows/actions
|
||||
|
||||
## Progressive Discovery (Read In Order)
|
||||
|
||||
## Instruction Efficiency (For AGENTS.md Updates)
|
||||
|
||||
- Keep Section 0 short and mandatory-only; move situational detail to later sections.
|
||||
- Use one rule per line: trigger + action + stop condition when possible.
|
||||
- Prefer explicit keywords for high-priority rules: MUST, NEVER, ASK FIRST.
|
||||
- Use emphasis sparingly; over-highlighting reduces salience.
|
||||
- Avoid restating model capabilities; only include constraints that change behavior.
|
||||
- Prefer links to external standards instead of copying long guidance text.
|
||||
- Keep examples minimal and only where ambiguity is likely.
|
||||
- Remove duplicated or stale directives during each AGENTS.md update.
|
||||
- Keep "Always Apply" stable; put fast-changing guidance in scoped sections.
|
||||
- Optimize for lowest-capability models: simple wording, short bullets, deterministic instructions.
|
||||
|
||||
### 0) Always Apply (Minimal Core)
|
||||
|
||||
- Be accurate, not agreeable.
|
||||
- Push back on unsafe, incorrect, or non-compliant requests.
|
||||
- Ask clarifying questions for ambiguous or risky changes.
|
||||
- Never apologize for refusing unsafe actions.
|
||||
- Prefer minimal, reversible changes.
|
||||
- Preserve existing architecture unless a change is explicitly requested.
|
||||
- Clarify before irreversible actions such as force-push, history rewrite, mass delete, permission changes, or release publishing.
|
||||
- Never claim commands, tests, or deployments succeeded unless verified.
|
||||
- Do not change public APIs or externally observable behavior unless explicitly requested.
|
||||
- Validate touched scope with the smallest relevant lint/type/test checks.
|
||||
- If the same fix fails twice, stop and report root cause with next options.
|
||||
- Never expose, request, or store secrets in model-visible channels.
|
||||
- Keep CI/workflow/runtime permissions at least privilege.
|
||||
- Always pin GitHub Actions to full commit SHAs; never use mutable tag-only refs (for example v4) in workflows or composite actions.
|
||||
- Prefer explicit failure with actionable errors over silent fallbacks.
|
||||
- State material assumptions before acting.
|
||||
|
||||
### 1) Apply When Writing/Changing Code
|
||||
|
||||
- Keep naming consistent for semantically identical constructs.
|
||||
- Follow Microsoft TypeScript style guidance.
|
||||
- For Bash, require shellcheck-clean scripts and `set -euo pipefail`.
|
||||
- Document exported types/functions and add context where behavior is not obvious.
|
||||
- Use consistent library patterns already present in the repo (`semantic-release`, `vitest`, etc.).
|
||||
|
||||
### 2) Apply When Writing Tests
|
||||
|
||||
- Test naming format: `<method> <conditions> <expected>`.
|
||||
- Test behavior, not implementation details.
|
||||
- Keep setup focused on the tested condition.
|
||||
- Use constants where exact values do not matter.
|
||||
- Use parameterized tests/helpers for repeated patterns.
|
||||
- Do not add logic (if/else/loops) in test bodies.
|
||||
- If a fix fails twice, stop and explain root cause before continuing.
|
||||
|
||||
### 3) Apply When Working On CI/CD
|
||||
|
||||
- Follow: https://github.com/github/awesome-copilot/blob/main/instructions/github-actions-ci-cd-best-practices.instructions.md
|
||||
- Abstract repeated workflow logic into reusable actions/scripts.
|
||||
- Treat `main` as production release (no known bugs).
|
||||
- Treat `staging` as experimental/integration branch.
|
||||
- Use least privilege permissions and avoid unnecessary token scopes.
|
||||
|
||||
### 4) Apply For File/Script Placement
|
||||
|
||||
- Use `dev_scripts/` for development tooling and CI-support utilities.
|
||||
- Use `scripts/` for packaging/release processes.
|
||||
|
||||
## Operational Guardrails
|
||||
|
||||
### Always Do
|
||||
|
||||
- Run lint/type/test checks relevant to touched files before finalizing.
|
||||
- Warn clearly about potential backward compatibility impact.
|
||||
- Confirm with user before introducing breaking changes.
|
||||
|
||||
### Ask First
|
||||
|
||||
- Changing branch protection or release governance.
|
||||
- Adding dependencies or changing major dependency versions.
|
||||
- Modifying security-sensitive CI permissions/policies.
|
||||
|
||||
### Never Do
|
||||
|
||||
- Commit secrets, credentials, or `.env` files.
|
||||
- Duplicate workflow logic when reusable abstraction is possible.
|
||||
- Modify `node_modules/` or `vendor/`.
|
||||
- Push directly to `main`.
|
||||
|
||||
## Quick Commands (Use As Needed)
|
||||
|
||||
```bash
|
||||
# Lint / Type
|
||||
npx eslint . --fix
|
||||
shellcheck **/*.sh
|
||||
npx tsc --noEmit
|
||||
|
||||
# Tests
|
||||
npx vitest run
|
||||
npx vitest run -t "pattern"
|
||||
|
||||
# Repo conventions
|
||||
./dev_scripts/setup.sh
|
||||
./scripts/release.sh
|
||||
```
|
||||
211
AGENTS_META.md
Normal file
211
AGENTS_META.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# AGENTS.md Metadata
|
||||
|
||||
IMPORTANT: THIS FILE IS NOT FOR AGENTIC AI
|
||||
|
||||
Metadata used in creation and update of agent role, behavior and directives.
|
||||
|
||||
## Creation
|
||||
|
||||
### Initial Prompt
|
||||
|
||||
Create `AGENTS.md` file text that includes these directives:
|
||||
|
||||
- You are an expert developer skilled in TypeScript, Bash, DevOps, Git, GitHub workflows, and GitHub Actions.
|
||||
- Avoid sycophant behavior in replies and actions.
|
||||
- Push back when needed and scrutinize requests for correctness and compliance with these guidelines.
|
||||
|
||||
Code must follow:
|
||||
|
||||
- Style guidance from the Microsoft TypeScript Style Guide and Shellcheck.
|
||||
- Consistent naming for semantically equivalent classes, interfaces, types, methods, variables, and member fields.
|
||||
- Consistent documentation for those naming targets.
|
||||
- Always document exported types.
|
||||
- Provide additional context for new readers where behavior is not obvious from signatures.
|
||||
|
||||
CI/CD must follow best practices:
|
||||
|
||||
- Reference: https://github.com/github/awesome-copilot/blob/main/instructions/github-actions-ci-cd-best-practices.instructions.md
|
||||
- Abstract shared logic across workflows and actions.
|
||||
- Treat `main` as the official release branch with no known bugs.
|
||||
- Treat `staging` as the branch for experimental features.
|
||||
|
||||
Security and dependency guidance:
|
||||
|
||||
- Follow security best practices and the principle of least privilege.
|
||||
- Use libraries consistently (for example: `semantic-release`, `vitest`).
|
||||
- Follow recommended usage patterns from maintainers/authors.
|
||||
|
||||
Testing patterns must be consistent:
|
||||
|
||||
- Naming format: `<method> <conditions> <expected>`
|
||||
- Focus on behavior under test.
|
||||
- Keep setup directly tied to conditions/expected behavior.
|
||||
- Use constants when exact values do not matter, only presence.
|
||||
- Use parameterized tests or helper functions for repeated patterns.
|
||||
- Do not use logic in tests.
|
||||
|
||||
For every code update:
|
||||
|
||||
- Run tests, fix failures, and iterate.
|
||||
- Avoid infinite loops and explain the reason when stopping.
|
||||
- Use `dev_scripts/` only for development tooling and CI/CD commands.
|
||||
- Use `scripts/` for release/packaging.
|
||||
- Warn about breaking changes that affect backward compatibility and confirm before proceeding.
|
||||
|
||||
### Refinement Prompts
|
||||
|
||||
#### Agent Characteristics Optimization
|
||||
|
||||
**Prompt**
|
||||
|
||||
```
|
||||
Optimize AGENTS.md for progressive discovery and avoiding context bloat that the models already are capable of. Assume the lowest common denominiter / capability evaluating all the models.
|
||||
```
|
||||
|
||||
**\_Response**
|
||||
|
||||
```
|
||||
I’m going to streamline AGENTS.md into a layered “read-minimum-first” format so lower-capability models can follow it without carrying unnecessary context. I’ll keep core guardrails and remove redundant detail.
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS_META.md)
|
||||
|
||||
I’m replacing AGENTS.md with a shorter layered version that starts with universal rules and only expands into domain-specific guidance when needed, to reduce token overhead for weaker models.
|
||||
|
||||
Generating patch (3 lines) in [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Created [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
The new layout is in place with progressive layers and trimmed redundancy. I’m doing a quick read-back to confirm formatting and that the guidance stayed aligned with your original constraints.
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Optimized AGENTS.md for progressive discovery and low-context model compatibility.
|
||||
|
||||
What changed:
|
||||
- Replaced long narrative sections with a layered “read in order” structure.
|
||||
- Put mandatory, model-agnostic rules in a minimal **Section 0**.
|
||||
- Moved conditional guidance into scoped sections:
|
||||
- coding
|
||||
- testing
|
||||
- CI/CD
|
||||
- script placement
|
||||
- Reduced redundancy and removed explanatory prose that models typically already infer.
|
||||
- Kept your critical constraints intact:
|
||||
- anti-sycophancy / push-back
|
||||
- style and naming consistency
|
||||
- test conventions and no logic in tests
|
||||
- CI/CD abstraction and branch intent (`main` vs `staging`)
|
||||
- security/least privilege
|
||||
- ask-first / never-do guardrails
|
||||
- Preserved quick command references in a compact block.
|
||||
```
|
||||
|
||||
#### Always-on Rules
|
||||
|
||||
**Prompt**
|
||||
|
||||
```
|
||||
- Explicitly include instruction-efficiency rules covering progressive discovery, salience control, and context-bloat reduction when creating/updating AGENTS.md.
|
||||
- Add always-on rules for:
|
||||
- irreversible action confirmation,
|
||||
- execution side effects action confirmation,
|
||||
- verification-only success claims,
|
||||
- minimal/safe diffs,
|
||||
- API stability by default,
|
||||
- narrow-scope validation,
|
||||
- bounded fix loops,
|
||||
- secret-safe handling,
|
||||
- least-privilege permissions,
|
||||
- fail-fast error handling, and
|
||||
- explicit assumptions.
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```
|
||||
I’m going to streamline AGENTS.md into a layered “read-minimum-first” format so lower-capability models can follow it without carrying unnecessary context. I’ll keep core guardrails and remove redundant detail.
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS_META.md)
|
||||
|
||||
I’m replacing AGENTS.md with a shorter layered version that starts with universal rules and only expands into domain-specific guidance when needed, to reduce token overhead for weaker models.
|
||||
|
||||
Generating patch (3 lines) in [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Created [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
The new layout is in place with progressive layers and trimmed redundancy. I’m doing a quick read-back to confirm formatting and that the guidance stayed aligned with your original constraints.
|
||||
|
||||
Read [](file:///home/awalsh128/cache-apt-pkgs-action/AGENTS.md)
|
||||
|
||||
Optimized AGENTS.md for progressive discovery and low-context model compatibility.
|
||||
|
||||
What changed:
|
||||
- Replaced long narrative sections with a layered “read in order” structure.
|
||||
- Put mandatory, model-agnostic rules in a minimal **Section 0**.
|
||||
- Moved conditional guidance into scoped sections:
|
||||
- coding
|
||||
- testing
|
||||
- CI/CD
|
||||
- script placement
|
||||
- Reduced redundancy and removed explanatory prose that models typically already infer.
|
||||
- Kept your critical constraints intact:
|
||||
- anti-sycophancy / push-back
|
||||
- style and naming consistency
|
||||
- test conventions and no logic in tests
|
||||
- CI/CD abstraction and branch intent (`main` vs `staging`)
|
||||
- security/least privilege
|
||||
- ask-first / never-do guardrails
|
||||
- Preserved quick command references in a compact block.
|
||||
```
|
||||
|
||||
#### Skills & Rules
|
||||
|
||||
**Prompt**
|
||||
|
||||
```
|
||||
Consider useful guidelines calling agent features like progressive discovery, salience, context bloat when creating or updating AGENTS.md and if it should be explicitly calling out in the file to speed up responses and reducing token usage
|
||||
```
|
||||
|
||||
**Response**
|
||||
|
||||
```
|
||||
Updated:
|
||||
- AGENTS.md
|
||||
- AGENTS_META.md
|
||||
|
||||
What was added:
|
||||
- A dedicated Instruction Efficiency section in AGENTS.md with concrete rules for:
|
||||
- progressive discovery layering
|
||||
- salience control (limited emphasis, explicit priority keywords)
|
||||
- context-bloat reduction (short core, scoped detail, no capability restatement)
|
||||
- low-capability model optimization (simple, deterministic wording)
|
||||
- A matching refinement directive in AGENTS_META.md so future regenerations preserve the same optimization intent.
|
||||
|
||||
This is the right approach for faster responses and lower token usage because it improves instruction ordering and reduces redundant prompt mass while keeping hard constraints explicit.
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### 1. Anti-Sycophancy Directive
|
||||
|
||||
The `Role & Behavior` section explicitly instructs the agent to **push back**and **scrutinize**. This prevents the common LLM tendency to agree with incorrect user premises. The agent is programmed to prioritize correctness over compliance with unsafe requests.
|
||||
|
||||
### 2. Directory Separation
|
||||
|
||||
The configuration enforces a strict separation between `dev_scripts/` (local tooling) and `scripts/` (release/packaging). This aligns with your requirement to isolate development commands from production release logic.
|
||||
|
||||
### 3. Testing Rigor
|
||||
|
||||
The `Testing Strategy` section encodes your specific constraints:
|
||||
|
||||
- **Naming:**Enforces the `<method> <conditions> <expected>` pattern.
|
||||
- **No Logic in Tests:**Explicitly forbids control flow within test bodies, ensuring tests remain simple assertions of state.
|
||||
- **Iteration Limit:**The instruction to "stop and explain" after two failed fix attempts prevents agents from getting stuck in correction loops.
|
||||
|
||||
### 4. CI/CD Abstraction
|
||||
|
||||
The file mandates abstracting common logic from GitHub Actions YAML into reusable components, directly addressing your requirement to follow best practices and reduce duplication.
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
function usage() {
|
||||
console.error(
|
||||
"usage: node dev_scripts/check_latest_action_pin.mjs <owner/repo[/path]@ref | owner/repo[/path] ref | --scan [directory]>",
|
||||
);
|
||||
}
|
||||
|
||||
function parseInput(argv) {
|
||||
if (argv[0] === "--scan") {
|
||||
return {
|
||||
mode: "scan",
|
||||
rootDir: argv[1] ?? ".github",
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.length === 1) {
|
||||
const atIndex = argv[0].lastIndexOf("@");
|
||||
if (atIndex <= 0 || atIndex === argv[0].length - 1) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "single",
|
||||
actionPath: argv[0].slice(0, atIndex),
|
||||
currentRef: argv[0].slice(atIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.length === 2) {
|
||||
return {
|
||||
mode: "single",
|
||||
actionPath: argv[0],
|
||||
currentRef: argv[1],
|
||||
};
|
||||
}
|
||||
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseRepoSlug(actionPath) {
|
||||
const parts = actionPath.split("/").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
throw new Error(
|
||||
`Action path '${actionPath}' must include at least owner/repo.`,
|
||||
);
|
||||
}
|
||||
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
|
||||
function git(args) {
|
||||
return execFileSync("git", args, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function buildTagMap(repoSlug) {
|
||||
const remoteUrl = `https://github.com/${repoSlug}.git`;
|
||||
const output = git(["ls-remote", "--tags", remoteUrl]);
|
||||
const tagMap = new Map();
|
||||
|
||||
for (const line of output.split("\n")) {
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [sha, ref] = line.split(/\s+/);
|
||||
const prefix = "refs/tags/";
|
||||
if (!sha || !ref || !ref.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawTag = ref.slice(prefix.length);
|
||||
const isPeeled = rawTag.endsWith("^{}");
|
||||
const tag = isPeeled ? rawTag.slice(0, -3) : rawTag;
|
||||
if (isPeeled || !tagMap.has(tag)) {
|
||||
tagMap.set(tag, sha.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return tagMap;
|
||||
}
|
||||
|
||||
function resolveRefSha(repoSlug, ref, tagMap) {
|
||||
const normalizedRef = ref.toLowerCase();
|
||||
if (/^[0-9a-f]{40}$/.test(normalizedRef)) {
|
||||
return normalizedRef;
|
||||
}
|
||||
|
||||
if (tagMap.has(ref)) {
|
||||
return tagMap.get(ref);
|
||||
}
|
||||
|
||||
const remoteUrl = `https://github.com/${repoSlug}.git`;
|
||||
const output = git(["ls-remote", remoteUrl, ref]);
|
||||
const firstLine = output.split("\n")[0] ?? "";
|
||||
const [sha] = firstLine.split(/\s+/);
|
||||
if (!sha) {
|
||||
throw new Error(`Unable to resolve ref '${ref}' for ${repoSlug}.`);
|
||||
}
|
||||
|
||||
return sha.toLowerCase();
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const headers = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"User-Agent": "cache-apt-pkgs-action-pin-checker",
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`GitHub API request failed (${response.status}): ${body}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function getLatestRelease(repoSlug) {
|
||||
return fetchJson(`https://api.github.com/repos/${repoSlug}/releases/latest`);
|
||||
}
|
||||
|
||||
async function getReleases(repoSlug) {
|
||||
const releases = await fetchJson(
|
||||
`https://api.github.com/repos/${repoSlug}/releases?per_page=100`,
|
||||
);
|
||||
return Array.isArray(releases) ? releases : [];
|
||||
}
|
||||
|
||||
function findReleaseBySha(releases, tagMap, sha) {
|
||||
return (
|
||||
releases.find(
|
||||
(release) => tagMap.get(release.tag_name)?.toLowerCase() === sha,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function printField(label, value) {
|
||||
console.log(`${label}: ${value ?? "none"}`);
|
||||
}
|
||||
|
||||
function walkFiles(rootDir) {
|
||||
const results = [];
|
||||
|
||||
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function findActionPins(rootDir) {
|
||||
const results = [];
|
||||
const usesPattern = /^\s*uses:\s*["']?([^"'\s#]+)["']?/;
|
||||
|
||||
for (const filePath of walkFiles(rootDir)) {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index] ?? "";
|
||||
const match = usesPattern.exec(line);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const spec = match[1] ?? "";
|
||||
if (
|
||||
!spec.includes("@") ||
|
||||
spec.startsWith("./") ||
|
||||
spec.startsWith("docker://")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const atIndex = spec.lastIndexOf("@");
|
||||
const actionPath = spec.slice(0, atIndex);
|
||||
const currentRef = spec.slice(atIndex + 1);
|
||||
if (!actionPath || !currentRef) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({
|
||||
filePath,
|
||||
lineNumber: index + 1,
|
||||
actionPath,
|
||||
currentRef,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
const repoCache = new Map();
|
||||
|
||||
async function getRepoData(repoSlug) {
|
||||
const cached = repoCache.get(repoSlug);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
const tagMap = buildTagMap(repoSlug);
|
||||
const [latestRelease, releases] = await Promise.all([
|
||||
getLatestRelease(repoSlug),
|
||||
getReleases(repoSlug),
|
||||
]);
|
||||
return { tagMap, latestRelease, releases };
|
||||
})();
|
||||
|
||||
repoCache.set(repoSlug, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function analyzeAction(actionPath, currentRef) {
|
||||
const repoSlug = parseRepoSlug(actionPath);
|
||||
const { tagMap, latestRelease, releases } = await getRepoData(repoSlug);
|
||||
const currentSha = resolveRefSha(repoSlug, currentRef, tagMap);
|
||||
const currentRelease = findReleaseBySha(releases, tagMap, currentSha);
|
||||
const latestSha = latestRelease
|
||||
? (tagMap.get(latestRelease.tag_name) ?? null)
|
||||
: null;
|
||||
const isCurrent = latestSha !== null && latestSha === currentSha;
|
||||
|
||||
return {
|
||||
actionPath,
|
||||
repoSlug,
|
||||
currentRef,
|
||||
currentSha,
|
||||
currentRelease,
|
||||
latestRelease,
|
||||
latestSha,
|
||||
isCurrent,
|
||||
};
|
||||
}
|
||||
|
||||
function printAnalysis(analysis) {
|
||||
console.log(`Action: ${analysis.actionPath}`);
|
||||
printField("Repository", analysis.repoSlug);
|
||||
printField("Current ref", analysis.currentRef);
|
||||
printField("Current sha", analysis.currentSha);
|
||||
printField("Current release", analysis.currentRelease?.tag_name ?? null);
|
||||
printField("Current release URL", analysis.currentRelease?.html_url ?? null);
|
||||
printField("Latest release", analysis.latestRelease?.tag_name ?? null);
|
||||
printField(
|
||||
"Latest pin",
|
||||
analysis.latestSha ? `${analysis.actionPath}@${analysis.latestSha}` : null,
|
||||
);
|
||||
printField("Latest release URL", analysis.latestRelease?.html_url ?? null);
|
||||
printField(
|
||||
"Status",
|
||||
analysis.latestSha === null
|
||||
? "no release found"
|
||||
: analysis.isCurrent
|
||||
? "up to date"
|
||||
: "update available",
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const input = parseInput(process.argv.slice(2));
|
||||
|
||||
if (input.mode === "scan") {
|
||||
const rootDir = path.resolve(process.cwd(), input.rootDir);
|
||||
const pins = findActionPins(rootDir);
|
||||
if (pins.length === 0) {
|
||||
console.log(`No pinned GitHub Actions found under ${input.rootDir}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let updateCount = 0;
|
||||
for (const pin of pins) {
|
||||
const analysis = await analyzeAction(pin.actionPath, pin.currentRef);
|
||||
console.log(`File: ${pin.filePath}:${pin.lineNumber}`);
|
||||
printAnalysis(analysis);
|
||||
console.log("");
|
||||
if (analysis.latestSha && !analysis.isCurrent) {
|
||||
updateCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Scanned ${pins.length} pinned action reference(s).`);
|
||||
console.log(`Updates available: ${updateCount}`);
|
||||
if (updateCount > 0) {
|
||||
process.exitCode = 2;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const analysis = await analyzeAction(input.actionPath, input.currentRef);
|
||||
printAnalysis(analysis);
|
||||
|
||||
if (analysis.latestSha && !analysis.isCurrent) {
|
||||
process.exitCode = 2;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
35
dev_scripts/check_release_tag.mjs
Normal file
35
dev_scripts/check_release_tag.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { fail, logSuccess, readJsonFile, ROOT_DIR } from "./lib.mjs";
|
||||
|
||||
const tag = process.env.RELEASE_TAG;
|
||||
if (!tag) {
|
||||
fail("RELEASE_TAG is required for release preflight checks.");
|
||||
}
|
||||
|
||||
if (!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(tag)) {
|
||||
fail(
|
||||
`Release tag '${tag}' is invalid. Expected semantic version format like v1.2.3 or v1.2.3-rc.1`,
|
||||
);
|
||||
}
|
||||
|
||||
const packageJsonPath = `${ROOT_DIR}/package.json`;
|
||||
const packageJson = readJsonFile(packageJsonPath);
|
||||
|
||||
if (
|
||||
typeof packageJson.version !== "string" ||
|
||||
packageJson.version.length === 0
|
||||
) {
|
||||
fail("package.json version is missing or invalid.");
|
||||
}
|
||||
|
||||
const expectedTag = `v${packageJson.version}`;
|
||||
if (tag !== expectedTag) {
|
||||
fail(
|
||||
`Release tag and package version mismatch: RELEASE_TAG='${tag}' but package.json version='${packageJson.version}'. Expected tag '${expectedTag}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
logSuccess(
|
||||
`Release preflight passed for tag ${tag} and package version ${packageJson.version}.`,
|
||||
);
|
||||
263
dev_scripts/create_testcase_logs.mjs
Normal file
263
dev_scripts/create_testcase_logs.mjs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { ROOT_DIR, fail, logInfo, logSuccess } from "./lib.mjs";
|
||||
|
||||
function toLog(result) {
|
||||
return `*** CMD_LINE ${result.cmdLine}\n*** EXIT_CODE ${result.exitCode}\n*** STDOUT\n${result.stdout}*** STDERR\n${result.stderr}`;
|
||||
}
|
||||
|
||||
async function execute(cmdLine) {
|
||||
return await new Promise((resolvePromise, rejectPromise) => {
|
||||
const parts = cmdLine.trim().split(/\s+/);
|
||||
const command = parts[0]; // "apt-get"
|
||||
const args = parts.slice(1);
|
||||
|
||||
const child = spawn("sudo", [command, ...args], {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${process.env.PATH}:/usr/bin`,
|
||||
DEBIAN_FRONTEND: "noninteractive",
|
||||
DEBCONF_NONINTERACTIVE_SEEN: "true",
|
||||
},
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
rejectPromise(error);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
rejectPromise(
|
||||
new Error(
|
||||
`Command '${cmdLine}' failed with exit code ${code ?? "unknown"}.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
resolvePromise({
|
||||
cmdLine,
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function writeLogFile(baseDir, relativePath, result) {
|
||||
const outputPath = resolve(baseDir, `${relativePath}.log`);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, toLog(result), "utf8");
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const testDataDir = resolve(ROOT_DIR, "test/data");
|
||||
let written = 0;
|
||||
|
||||
for (const entry of testLogFiles) {
|
||||
if (entry.preExecCmdLine) {
|
||||
for (const cmd of entry.preExecCmdLine) {
|
||||
await execute(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await execute(entry.execCmdLine);
|
||||
const outputPath = await writeLogFile(testDataDir, entry.filepath, result);
|
||||
written += 1;
|
||||
logInfo(`Wrote ${outputPath}`);
|
||||
}
|
||||
|
||||
await execute("apt-get upgrade -y");
|
||||
await execute("apt-get autoremove -y");
|
||||
|
||||
logSuccess(`Wrote ${written} command execution log(s).`);
|
||||
}
|
||||
|
||||
const testLogFiles = [
|
||||
{
|
||||
filepath: "autoclean",
|
||||
execCmdLine: "apt-get autoclean",
|
||||
},
|
||||
{
|
||||
filepath: "autoremove_found",
|
||||
preExecCmdLine: ["apt-get install -y xdot", "apt-get remove -y xdot"],
|
||||
execCmdLine: "apt-get autoremove -y",
|
||||
},
|
||||
{
|
||||
filepath: "autoremove_notfound",
|
||||
preExecCmdLine: ["apt-get autoremove -y"],
|
||||
execCmdLine: "apt-get autoremove -y",
|
||||
},
|
||||
{
|
||||
filepath: "cacheshow_mixedfoundnotfound",
|
||||
execCmdLine: "apt-cache show xdot python3 nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "cacheshow_multiplefound",
|
||||
execCmdLine: "apt-cache show xdot python3",
|
||||
},
|
||||
{
|
||||
filepath: "cacheshow_singlefound",
|
||||
execCmdLine: "apt-cache show python3",
|
||||
},
|
||||
{
|
||||
filepath: "cacheshow_singlenotfound",
|
||||
execCmdLine: "apt-cache show nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "install_mixedfoundnotfound",
|
||||
preExecCmdLine: ["apt-get remove -y xdot"],
|
||||
execCmdLine: "apt-get install -y xdot nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "install_mixedinstallstatus",
|
||||
preExecCmdLine: [
|
||||
"apt-get install -y xdot",
|
||||
"apt-get remove -y rolldice",
|
||||
"apt-get autoremove -y",
|
||||
],
|
||||
execCmdLine: "apt-get install -y rolldice xdot",
|
||||
},
|
||||
{
|
||||
filepath: "install_singleinstalled",
|
||||
preExecCmdLine: ["apt-get install -y xdot"],
|
||||
execCmdLine: "apt-get install -y xdot",
|
||||
},
|
||||
{
|
||||
filepath: "install_singlenotfound",
|
||||
execCmdLine: "apt-get install -y nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "install_singlenotinstalled",
|
||||
preExecCmdLine: ["apt-get remove -y xdot", "apt-get autoremove -y"],
|
||||
execCmdLine: "apt-get install -y xdot",
|
||||
},
|
||||
{
|
||||
filepath: "listinstalled",
|
||||
execCmdLine: "dpkg-query -W -f ${binary:Package}=${Version}\\n",
|
||||
},
|
||||
{
|
||||
filepath: "listinstalledfiles_found",
|
||||
execCmdLine: "dpkg-query -L xdot",
|
||||
},
|
||||
{
|
||||
filepath: "listinstalledfiles_notfound",
|
||||
execCmdLine: "dpkg-query -L nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "listupgradable_found",
|
||||
preExecCmdLine: [
|
||||
"apt-get remove -y firefox-locale-en",
|
||||
"apt-get install -y firefox-locale-en=75.0+build3-0ubuntu1",
|
||||
"apt-get remove -y uuid-dev",
|
||||
"apt-get install -y uuid-dev=2.34-0.1ubuntu9",
|
||||
],
|
||||
execCmdLine: "apt list --upgradeable",
|
||||
},
|
||||
{
|
||||
filepath: "listupgradable_notfound",
|
||||
preExecCmdLine: ["apt-get upgrade -y"],
|
||||
execCmdLine: "apt list --upgradeable",
|
||||
},
|
||||
{
|
||||
filepath: "remove_mixedfoundnotfound",
|
||||
preExecCmdLine: ["apt-get install -y xdot"],
|
||||
execCmdLine: "apt-get remove -y xdot nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "remove_mixedinstallstatus",
|
||||
preExecCmdLine: ["apt-get install -y xdot", "apt-get remove -y rolldice"],
|
||||
execCmdLine: "apt-get remove -y rolldice xdot",
|
||||
},
|
||||
{
|
||||
filepath: "remove_singlenotinstalled",
|
||||
preExecCmdLine: ["apt-get remove -y xdot"],
|
||||
execCmdLine: "apt-get remove -y xdot",
|
||||
},
|
||||
{
|
||||
filepath: "remove_singlenotfound",
|
||||
execCmdLine: "apt-get remove -y nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "remove_singleinstalled",
|
||||
preExecCmdLine: ["apt-get install -y xdot"],
|
||||
execCmdLine: "apt-get remove -y xdot",
|
||||
},
|
||||
{
|
||||
filepath: "search_multiplefound",
|
||||
execCmdLine: "apt search vim",
|
||||
},
|
||||
{
|
||||
filepath: "search_nonefound",
|
||||
execCmdLine: "apt search nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "search_singlefound",
|
||||
execCmdLine: "apt search vim-vimerl-syntax",
|
||||
},
|
||||
{
|
||||
filepath: "search_namesonlysinglefound",
|
||||
execCmdLine: "apt search --names-only bash",
|
||||
},
|
||||
{
|
||||
filepath: "update",
|
||||
execCmdLine: "apt-get update -y",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_mixedfoundnotfound",
|
||||
preExecCmdLine: ["apt-get remove -y xdot"],
|
||||
execCmdLine: "apt-get upgrade -y xdot nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_mixedupgradestatus",
|
||||
preExecCmdLine: [
|
||||
"apt-get install -y xdot",
|
||||
"apt-get remove -y xxd",
|
||||
"apt-get install -y xxd=2:8.1.2269-1ubuntu5.30",
|
||||
],
|
||||
execCmdLine: "apt-get upgrade -y xxd xdot",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_singleupgraded",
|
||||
preExecCmdLine: ["apt-get upgrade -y xdot"],
|
||||
execCmdLine: "apt-get upgrade -y xdot",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_singlenotfound",
|
||||
execCmdLine: "apt-get upgrade -y nonexistentpackage",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_singlenotupgraded",
|
||||
preExecCmdLine: [
|
||||
"apt-get remove -y xxd",
|
||||
"apt-get install -y xxd=2:8.1.2269-1ubuntu5.30",
|
||||
],
|
||||
execCmdLine: "apt-get upgrade -y xxd",
|
||||
},
|
||||
{
|
||||
filepath: "upgrade_singleupgraded",
|
||||
preExecCmdLine: ["apt-get install xdot"],
|
||||
execCmdLine: "apt-get upgrade xdot",
|
||||
},
|
||||
];
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
129
dev_scripts/hotfix_pr.mjs
Normal file
129
dev_scripts/hotfix_pr.mjs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import process from "node:process";
|
||||
import {
|
||||
ROOT_DIR,
|
||||
assertNonEmpty,
|
||||
confirmPrompt,
|
||||
fail,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
run,
|
||||
runCaptureOutput,
|
||||
tryRun,
|
||||
usage,
|
||||
} from "./lib.mjs";
|
||||
|
||||
const issueId = process.argv[2] ?? "";
|
||||
const base = process.argv[3] ?? "";
|
||||
const usageMessage = usage(process.argv[1], "<issue ID> <target branch>");
|
||||
|
||||
assertNonEmpty(issueId, "Issue ID is empty", usageMessage);
|
||||
assertNonEmpty(base, "Target branch is empty", usageMessage);
|
||||
|
||||
if (!/^\d+$/.test(issueId)) {
|
||||
fail("Issue ID must be an integer.");
|
||||
}
|
||||
|
||||
const branchSuffix = `issue-${issueId}`;
|
||||
const hotfixBranch = `hotfix/${branchSuffix}`;
|
||||
|
||||
function branchExistsOnOrigin(branch) {
|
||||
return tryRun("git", [
|
||||
"ls-remote",
|
||||
"--exit-code",
|
||||
"--heads",
|
||||
"origin",
|
||||
branch,
|
||||
]).ok;
|
||||
}
|
||||
|
||||
function createOrCheckoutBranch(checkoutBranch, baseBranch) {
|
||||
if (branchExistsOnOrigin(checkoutBranch)) {
|
||||
logInfo(
|
||||
`Branch ${checkoutBranch} exists. Checking out and merging ${baseBranch}...`,
|
||||
);
|
||||
run("git", ["checkout", checkoutBranch]);
|
||||
run("git", ["pull", "origin", checkoutBranch]);
|
||||
run("git", ["merge", baseBranch]);
|
||||
} else {
|
||||
logInfo(`Creating hotfix branch for issue ${issueId}...`);
|
||||
run("git", ["checkout", baseBranch]);
|
||||
run("git", ["checkout", "-b", checkoutBranch]);
|
||||
run("git", ["merge", baseBranch]);
|
||||
}
|
||||
}
|
||||
|
||||
function commitIfNeeded(message) {
|
||||
run("git", ["add", "."]);
|
||||
const commit = tryRun("git", ["commit", "-m", message]);
|
||||
if (commit.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const combined = `${commit.stdout}\n${commit.stderr}`;
|
||||
if (/nothing to commit|no changes added to commit/i.test(combined)) {
|
||||
logSuccess("No local changes to commit.");
|
||||
return;
|
||||
}
|
||||
|
||||
process.stderr.write(commit.stderr);
|
||||
process.exit(commit.status ?? 1);
|
||||
}
|
||||
|
||||
function pushChanges(fixType, syncBranch, syncBase) {
|
||||
const message = `${fixType}: resolve critical production issue in #${issueId}`;
|
||||
commitIfNeeded(message);
|
||||
|
||||
const prUrl = runCaptureOutput("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--head",
|
||||
syncBranch,
|
||||
"--base",
|
||||
syncBase,
|
||||
"--state",
|
||||
"open",
|
||||
"--json",
|
||||
"url",
|
||||
"--jq",
|
||||
".[0].url",
|
||||
]);
|
||||
|
||||
if (prUrl) {
|
||||
logSuccess(`PR already exists: ${prUrl}`);
|
||||
} else {
|
||||
logInfo("No PR found. Creating new PR...");
|
||||
run("gh", [
|
||||
"pr",
|
||||
"create",
|
||||
"--head",
|
||||
syncBranch,
|
||||
"--base",
|
||||
syncBase,
|
||||
"--title",
|
||||
message,
|
||||
]);
|
||||
}
|
||||
|
||||
logInfo(`Pushing changes from ${syncBase} to ${syncBranch}...`);
|
||||
run("git", ["push", "origin", syncBranch]);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
createOrCheckoutBranch(hotfixBranch, base);
|
||||
|
||||
await confirmPrompt(
|
||||
"Edit files and confirm to continue. This can always be rerun to pickup where you left off. Continue?",
|
||||
);
|
||||
|
||||
pushChanges("fix", hotfixBranch, base);
|
||||
|
||||
const syncBranch = `sync/staging-${branchSuffix}`;
|
||||
createOrCheckoutBranch(syncBranch, hotfixBranch);
|
||||
pushChanges("sync", syncBranch, "staging");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
241
dev_scripts/lib.mjs
Normal file
241
dev_scripts/lib.mjs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
accessSync,
|
||||
constants,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import readline from "node:readline/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export { fileURLToPath, path, process };
|
||||
|
||||
export class Paths {
|
||||
constructor(importMetaUrl) {
|
||||
this.importMetaUrl = importMetaUrl;
|
||||
}
|
||||
|
||||
scriptDir() {
|
||||
return path.dirname(fileURLToPath(this.importMetaUrl));
|
||||
}
|
||||
|
||||
repoRootDir() {
|
||||
return path.resolve(this.scriptDir(), "..");
|
||||
}
|
||||
}
|
||||
|
||||
export const ROOT_DIR = new Paths(import.meta.url).repoRootDir();
|
||||
|
||||
export const REPO_OWNER = "awalsh128";
|
||||
export const REPO_NAME = "cache-apt-pkgs-action";
|
||||
export const REPO_SLUG = `${REPO_OWNER}/${REPO_NAME}`;
|
||||
export const REPO_URL = `https://github.com/${REPO_SLUG}.git`;
|
||||
export const GITHUB_API_BASE_URL = "https://api.github.com";
|
||||
export const GITHUB_USER_AGENT = `${REPO_NAME}-dev-scripts`;
|
||||
|
||||
export const VSCODE_SETTINGS_RELPATH = ".vscode/settings.json";
|
||||
export const VSCODE_SETTINGS_DEFAULTS = {
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"*": true,
|
||||
},
|
||||
"chat.useAgentsMdFile": true,
|
||||
};
|
||||
|
||||
export const CURRENT_NODE_VERSION = "24";
|
||||
|
||||
export function usage(scriptPath, params) {
|
||||
return `usage: ${path.basename(scriptPath)} ${params}`;
|
||||
}
|
||||
|
||||
export function fail(message, exitCode = 1) {
|
||||
logError(message);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
export function logInfo(message) {
|
||||
console.log(`ℹ️ ${message}`);
|
||||
}
|
||||
|
||||
export function logSuccess(message) {
|
||||
console.log(`✅ ${message}`);
|
||||
}
|
||||
|
||||
export function logWarn(message) {
|
||||
console.log(`⚠️ ${message}`);
|
||||
}
|
||||
|
||||
export function logError(message) {
|
||||
console.error(`❌ ${message}`);
|
||||
}
|
||||
|
||||
export function assertNonEmpty(value, message, usageMessage) {
|
||||
if (!value || value.trim().length === 0) {
|
||||
if (usageMessage) {
|
||||
console.error(usageMessage);
|
||||
}
|
||||
fail(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function commandExists(command) {
|
||||
const isExecutable = (candidatePath) => {
|
||||
try {
|
||||
accessSync(candidatePath, constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (command.includes("/")) {
|
||||
return isExecutable(command);
|
||||
}
|
||||
|
||||
const pathValue = process.env.PATH ?? "";
|
||||
const dirs = pathValue.split(path.delimiter).filter(Boolean);
|
||||
return dirs.some((dirPath) => isExecutable(path.join(dirPath, command)));
|
||||
}
|
||||
|
||||
export function ensureDirExists(dirPath) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
export function readJsonFile(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||
}
|
||||
|
||||
export function writeJsonFile(filePath, value) {
|
||||
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
export function readNodeMajorVersion(rootDir = ROOT_DIR) {
|
||||
const nodeVersionPath = path.join(rootDir, ".node_ver");
|
||||
|
||||
let fileText;
|
||||
try {
|
||||
fileText = readFileSync(nodeVersionPath, "utf8");
|
||||
} catch (error) {
|
||||
fail(
|
||||
`Missing or unreadable version file at ${nodeVersionPath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const lines = fileText
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
if (lines.length !== 1) {
|
||||
fail(".node_ver must contain exactly one non-empty line.");
|
||||
}
|
||||
|
||||
const rawVersion = lines[0];
|
||||
if (!/^\d+$/.test(rawVersion)) {
|
||||
fail(
|
||||
".node_ver must contain only the Node.js major version (e.g. 20, 24).",
|
||||
);
|
||||
}
|
||||
|
||||
return rawVersion;
|
||||
}
|
||||
|
||||
function normalizeRunOptions(optionsOrQuiet) {
|
||||
if (typeof optionsOrQuiet === "boolean") {
|
||||
return {
|
||||
cwd: ROOT_DIR,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
stdio: optionsOrQuiet ? "pipe" : "inherit",
|
||||
};
|
||||
}
|
||||
|
||||
const options = optionsOrQuiet ?? {};
|
||||
return {
|
||||
cwd: options.cwd ?? ROOT_DIR,
|
||||
env: options.env ?? process.env,
|
||||
encoding: options.encoding ?? "utf8",
|
||||
stdio:
|
||||
options.stdio ?? (options.stdout || options.stderr ? "pipe" : "inherit"),
|
||||
};
|
||||
}
|
||||
|
||||
export function run(command, args = [], optionsOrQuiet = {}) {
|
||||
const options = normalizeRunOptions(optionsOrQuiet);
|
||||
try {
|
||||
return execFileSync(command, args, options);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Failed to execute '${command} ${args.join(" ")}'. ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function tryRun(command, args = [], optionsOrQuiet = {}) {
|
||||
const options = normalizeRunOptions(optionsOrQuiet);
|
||||
const result = spawnSync(command, args, options);
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(
|
||||
`Failed to spawn '${command} ${args.join(" ")}'. ${result.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: result.status === 0,
|
||||
status: result.status,
|
||||
stdout: result.stdout ?? "",
|
||||
stderr: result.stderr ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
export function runCaptureOutput(command, args = [], options = {}) {
|
||||
return run(command, args, {
|
||||
...options,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
}).trim();
|
||||
}
|
||||
|
||||
export async function confirmPrompt(
|
||||
message,
|
||||
yesPattern = /^[yY]$/,
|
||||
noPattern = /^[nN]$/,
|
||||
) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
try {
|
||||
const getText = (pattern) =>
|
||||
pattern.source
|
||||
.trim()
|
||||
.replace(/^\^/, "")
|
||||
.replace(/\$$/, "")
|
||||
.replace(/\\([^\\])/g, "$1");
|
||||
while (true) {
|
||||
const answer = await rl
|
||||
.question(
|
||||
`${message} [${getText(yesPattern)} | ${getText(noPattern)}] `,
|
||||
)
|
||||
.then((ans) => ans.trim().toLowerCase());
|
||||
if (yesPattern.test(answer) || answer.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (noPattern.test(answer)) {
|
||||
fail("Aborted.", 0);
|
||||
}
|
||||
logError(
|
||||
`Invalid option '${answer}' selected. Options are: ${getText(yesPattern)} or ${getText(noPattern)}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
LIB_EXIT_CODE=99
|
||||
|
||||
#######################################
|
||||
# Clone a repository and change directory to it.
|
||||
# Arguments:
|
||||
# The repository name.
|
||||
# The directory containing repository to rebase.
|
||||
# The tag to clone from, otherwise use HEAD.
|
||||
# Returns:
|
||||
# 0 if directory was changed, non-zero on error.
|
||||
#######################################
|
||||
function clone_repo {
|
||||
repo_name="${1}"
|
||||
repo_url="https://github.com/awalsh128/${repo_name}"
|
||||
repo_dir="${2}"
|
||||
repo_dir_parent=$(realpath "$(dirname "${repo_dir}")")
|
||||
wd=$(pwd)
|
||||
[[ -d ${repo_dir} ]] && rm -fr "${repo_dir}"
|
||||
cd "${repo_dir_parent}" || exit "${LIB_EXIT_CODE}"
|
||||
if [[ -n "${3}" ]]; then
|
||||
git clone -b "${3}" "${repo_url}"
|
||||
else
|
||||
git clone "${repo_url}"
|
||||
fi
|
||||
cd "${wd}" || exit "${LIB_EXIT_CODE}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Clone a repository and change directory to it.
|
||||
# Arguments:
|
||||
# The repository name.
|
||||
# The tag to clone from, otherwise use HEAD.
|
||||
# Returns:
|
||||
# 0 if directory was changed, non-zero on error.
|
||||
#######################################
|
||||
function clone_repo_and_cd {
|
||||
clone_repo "${1}" "/tmp/${1}" "${2}"
|
||||
cd "/tmp/${1}" || exit "${LIB_EXIT_CODE}"
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Yes or no prompt.
|
||||
# Arguments:
|
||||
# Message to display at prompt.
|
||||
# Returns:
|
||||
# None
|
||||
#######################################
|
||||
function confirm_prompt {
|
||||
while true; do
|
||||
read -rp "${1} [Y|n] " response
|
||||
case ${response} in
|
||||
[Yy]*) break;;
|
||||
[Nn]*) exit;;
|
||||
*) echo "Invalid option selected.";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Validate argument and exit if invalid.
|
||||
# Arguments:
|
||||
# Argument to validate.
|
||||
# Message to display on error.
|
||||
# Help message.
|
||||
# Returns:
|
||||
# None
|
||||
#######################################
|
||||
function validate_arg {
|
||||
if [[ -n "${1}" ]] || [[ -z "${1}" ]]; then
|
||||
printf "error: %s\n%s\n" "${2}" "${3}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
#######################################
|
||||
# Print out command usage.
|
||||
# Arguments:
|
||||
# Name of command.
|
||||
# Parameters
|
||||
# Returns:
|
||||
# Usage message.
|
||||
#######################################
|
||||
function usage {
|
||||
echo "usage: $(basename "${1}") ${2}"
|
||||
}
|
||||
305
dev_scripts/node_ver.mjs
Normal file
305
dev_scripts/node_ver.mjs
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import process from "node:process";
|
||||
import {
|
||||
GITHUB_USER_AGENT,
|
||||
ROOT_DIR,
|
||||
commandExists,
|
||||
confirmPrompt,
|
||||
fail,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
logWarn,
|
||||
readNodeMajorVersion,
|
||||
run,
|
||||
runCaptureOutput,
|
||||
tryRun,
|
||||
usage,
|
||||
} from "./lib.mjs";
|
||||
|
||||
const rootDir = ROOT_DIR;
|
||||
const usageMessage = usage(process.argv[1], "--verify | --update");
|
||||
|
||||
function parseMode(argv) {
|
||||
const mode = argv[2] ?? "";
|
||||
if (mode !== "--verify" && mode !== "--update") {
|
||||
fail(usageMessage);
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
function buildReferenceSpecs(nodeMajor) {
|
||||
return [
|
||||
{
|
||||
path: `${rootDir}/package.json`,
|
||||
replacements: [
|
||||
[/"node":\s*">=\d+(?:\.\d+\.\d+)?"/g, `"node": ">=${nodeMajor}"`],
|
||||
],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/dev_scripts/lib.mjs`,
|
||||
replacements: [
|
||||
[
|
||||
/export const CURRENT_NODE_VERSION = "\d+";/g,
|
||||
`export const CURRENT_NODE_VERSION = "${nodeMajor}";`,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/.github/actions/setup-env/action.yml`,
|
||||
replacements: [[/node-version:\s*\d+(?:\.x)?/g, `node-version: ${nodeMajor}`]],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/.github/workflows/ci.yml`,
|
||||
replacements: [
|
||||
[/node-version:\s*\[\d+(?:\.x)?\]/g, `node-version: [${nodeMajor}]`],
|
||||
],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/.github/workflows/pr.yml`,
|
||||
replacements: [[/node-version:\s*\d+(?:\.x)?/g, `node-version: ${nodeMajor}`]],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/.github/workflows/release.yml`,
|
||||
replacements: [
|
||||
[/node-version:\s*\[\d+(?:\.x)?\]/g, `node-version: [${nodeMajor}]`],
|
||||
],
|
||||
},
|
||||
{
|
||||
path: `${rootDir}/README.md`,
|
||||
replacements: [[/- Node\.js \d+\+/g, `- Node.js ${nodeMajor}+`]],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function applyReplacements(content, replacements) {
|
||||
let updated = content;
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
updated = updated.replace(pattern, replacement);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
function updateNodeVersionReferences(nodeMajor) {
|
||||
const files = buildReferenceSpecs(nodeMajor);
|
||||
let changedCount = 0;
|
||||
|
||||
for (const entry of files) {
|
||||
const original = readFileSync(entry.path, "utf8");
|
||||
const updated = applyReplacements(original, entry.replacements);
|
||||
|
||||
if (updated !== original) {
|
||||
writeFileSync(entry.path, updated, "utf8");
|
||||
changedCount += 1;
|
||||
logInfo(`Updated Node version references in ${entry.path}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedCount === 0) {
|
||||
logInfo("No Node version reference updates were needed.");
|
||||
} else {
|
||||
logSuccess(`Updated Node version references in ${changedCount} file(s).`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyNodeVersionReferences(nodeMajor) {
|
||||
const files = buildReferenceSpecs(nodeMajor);
|
||||
const drifted = [];
|
||||
|
||||
for (const entry of files) {
|
||||
const original = readFileSync(entry.path, "utf8");
|
||||
const updated = applyReplacements(original, entry.replacements);
|
||||
if (updated !== original) {
|
||||
drifted.push(entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (drifted.length > 0) {
|
||||
fail(`Node version references are out of sync in: ${drifted.join(", ")}`);
|
||||
}
|
||||
|
||||
logSuccess("All Node version references are synchronized with .node_ver.");
|
||||
}
|
||||
|
||||
async function getAvailableNodeMajors() {
|
||||
let packageJson;
|
||||
try {
|
||||
const response = await fetch("https://registry.npmjs.org/node", {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": GITHUB_USER_AGENT,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
fail(
|
||||
`Unable to read Node.js versions from npm registry: HTTP ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
packageJson = await response.json();
|
||||
} catch (error) {
|
||||
fail(
|
||||
`Unable to read Node.js versions from npm registry: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const versions = Object.keys(packageJson?.versions ?? {});
|
||||
if (!Array.isArray(versions) || versions.length === 0) {
|
||||
fail("Unexpected npm registry response for Node.js versions.");
|
||||
}
|
||||
|
||||
return [...new Set(versions.map((version) => String(version).split(".")[0]))]
|
||||
.filter((major) => /^\d+$/.test(major))
|
||||
.sort((left, right) => Number(left) - Number(right));
|
||||
}
|
||||
|
||||
function runScriptWithOptionalSudo(scriptText) {
|
||||
if (!commandExists("bash")) {
|
||||
fail("bash is required to run the NodeSource setup script.");
|
||||
}
|
||||
|
||||
const usesSudo = commandExists("sudo");
|
||||
const command = usesSudo ? "sudo" : "bash";
|
||||
const args = usesSudo ? ["-E", "bash", "-s", "--"] : ["-s", "--"];
|
||||
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
input: scriptText,
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(
|
||||
`Failed to execute NodeSource setup script: ${result.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`NodeSource setup script failed with exit code ${result.status ?? "unknown"}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupNodeSourceRepo(nodeMajor) {
|
||||
const setupUrl = `https://deb.nodesource.com/setup_${nodeMajor}.x`;
|
||||
|
||||
logInfo(`Configuring NodeSource for Node.js ${nodeMajor}.x...`);
|
||||
|
||||
let scriptText;
|
||||
try {
|
||||
const response = await fetch(setupUrl, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
"User-Agent": GITHUB_USER_AGENT,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
fail(
|
||||
`Unable to download NodeSource setup script: HTTP ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
scriptText = await response.text();
|
||||
} catch (error) {
|
||||
fail(
|
||||
`Unable to download NodeSource setup script: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!scriptText.trim()) {
|
||||
fail("Downloaded NodeSource setup script is empty.");
|
||||
}
|
||||
|
||||
runScriptWithOptionalSudo(scriptText);
|
||||
}
|
||||
|
||||
function installNodejs(nodeMajor) {
|
||||
if (!commandExists("apt-get")) {
|
||||
fail("APT is required to install Node.js packages.");
|
||||
}
|
||||
|
||||
const aptRunner = commandExists("sudo")
|
||||
? (args) => run("sudo", ["apt-get", ...args], { cwd: rootDir })
|
||||
: (args) => run("apt-get", args, { cwd: rootDir });
|
||||
|
||||
logInfo("Updating apt package index...");
|
||||
aptRunner(["update"]);
|
||||
|
||||
logInfo("Installing nodejs package...");
|
||||
aptRunner(["install", "-y", "nodejs"]);
|
||||
|
||||
const fullNodeVersion = runCaptureOutput("node", ["-v"], { cwd: rootDir });
|
||||
if (!fullNodeVersion.startsWith(`v${nodeMajor}.`)) {
|
||||
fail(
|
||||
`Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`,
|
||||
);
|
||||
}
|
||||
|
||||
logSuccess(
|
||||
`Node.js ${fullNodeVersion} installed/updated for major ${nodeMajor}.`,
|
||||
);
|
||||
}
|
||||
|
||||
function verifyInstalledNodeMajor(nodeMajor) {
|
||||
const nodeResult = tryRun("node", ["-v"], { cwd: rootDir, stdio: "pipe" });
|
||||
if (!nodeResult.ok) {
|
||||
fail("Node.js is not installed or not available on PATH.");
|
||||
}
|
||||
|
||||
const fullNodeVersion = (nodeResult.stdout ?? "").trim();
|
||||
if (!fullNodeVersion.startsWith(`v${nodeMajor}.`)) {
|
||||
fail(
|
||||
`Installed Node.js version (${fullNodeVersion}) does not match expected major v${nodeMajor}.`,
|
||||
);
|
||||
}
|
||||
|
||||
logSuccess(`Installed Node.js version matches major ${nodeMajor}.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mode = parseMode(process.argv);
|
||||
const nodeMajor = readNodeMajorVersion(rootDir);
|
||||
|
||||
if (mode === "--verify") {
|
||||
verifyNodeVersionReferences(nodeMajor);
|
||||
verifyInstalledNodeMajor(nodeMajor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!commandExists("apt") && !commandExists("apt-get")) {
|
||||
fail("APT is required to install Node.js packages.");
|
||||
}
|
||||
|
||||
const availableMajors = await getAvailableNodeMajors();
|
||||
if (!availableMajors.includes(nodeMajor)) {
|
||||
fail(
|
||||
`Node.js major ${nodeMajor} is not listed as available. Available majors: ${availableMajors.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
logWarn(
|
||||
`You are about to install/update Node.js for major ${nodeMajor}.x. This may break your development environment and NPM users.`,
|
||||
);
|
||||
await confirmPrompt(
|
||||
`Type "i confirm change to ${nodeMajor} to proceed:",
|
||||
new RegExp(`^i confirm change to ${nodeMajor}$`),
|
||||
/^n$/,
|
||||
);
|
||||
|
||||
updateNodeVersionReferences(nodeMajor);
|
||||
await setupNodeSourceRepo(nodeMajor);
|
||||
installNodejs(nodeMajor);
|
||||
verifyNodeVersionReferences(nodeMajor);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "..");
|
||||
const packageJsonPath = path.join(repoRoot, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
|
||||
const rawTag = process.env.RELEASE_TAG ?? process.argv[2] ?? "";
|
||||
if (!rawTag) {
|
||||
throw new Error("RELEASE_TAG must be provided for release preflight checks.");
|
||||
}
|
||||
|
||||
if (!/^v\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?$/.test(rawTag)) {
|
||||
throw new Error(`Release tag '${rawTag}' is not a valid semver-style tag.`);
|
||||
}
|
||||
|
||||
const expectedVersion = String(packageJson.version ?? "");
|
||||
const actualVersion = rawTag.slice(1);
|
||||
if (expectedVersion !== actualVersion) {
|
||||
throw new Error(
|
||||
`Release tag '${rawTag}' does not match package.json version '${expectedVersion}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (packageJson.private === true) {
|
||||
throw new Error("package.json must not be private for publish releases.");
|
||||
}
|
||||
|
||||
process.stdout.write(`Release preflight passed for ${rawTag}.\n`);
|
||||
216
dev_scripts/setup_devenv.mjs
Normal file
216
dev_scripts/setup_devenv.mjs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import {
|
||||
ROOT_DIR,
|
||||
commandExists,
|
||||
ensureDirExists,
|
||||
fail,
|
||||
logError,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
logWarn,
|
||||
readNodeMajorVersion,
|
||||
readJsonFile,
|
||||
run,
|
||||
tryRun,
|
||||
VSCODE_SETTINGS_DEFAULTS,
|
||||
VSCODE_SETTINGS_RELPATH,
|
||||
writeJsonFile,
|
||||
} from "./lib.mjs";
|
||||
|
||||
function ensureVscodeSettingsFile() {
|
||||
const vscodeDir = `${ROOT_DIR}/.vscode`;
|
||||
const vscodeSettingsPath = `${ROOT_DIR}/${VSCODE_SETTINGS_RELPATH}`;
|
||||
|
||||
ensureDirExists(vscodeDir);
|
||||
|
||||
if (!existsSync(vscodeSettingsPath)) {
|
||||
writeJsonFile(vscodeSettingsPath, VSCODE_SETTINGS_DEFAULTS);
|
||||
logWarn(`Created missing ${vscodeSettingsPath}`);
|
||||
}
|
||||
|
||||
return vscodeSettingsPath;
|
||||
}
|
||||
|
||||
function loadVscodeSettingsJson() {
|
||||
const vscodeSettingsPath = ensureVscodeSettingsFile();
|
||||
return {
|
||||
vscodeSettingsPath,
|
||||
vscodeSettingsJson: readJsonFile(vscodeSettingsPath),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureAgentsFilesUsed() {
|
||||
const { vscodeSettingsPath, vscodeSettingsJson } = loadVscodeSettingsJson();
|
||||
|
||||
if (!vscodeSettingsJson["chat.useAgentsMdFile"]) {
|
||||
vscodeSettingsJson["chat.useAgentsMdFile"] = true;
|
||||
writeJsonFile(vscodeSettingsPath, vscodeSettingsJson);
|
||||
logWarn(
|
||||
`Updated ${vscodeSettingsPath} to enable chat.useAgentsMdFile for AGENTS.md support.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAptDependencies() {
|
||||
if (!commandExists("apt-get")) {
|
||||
fail("apt-get is required for system dependency installation.");
|
||||
}
|
||||
|
||||
const aptPackages = [
|
||||
["git", "git"],
|
||||
["gh", "gh"],
|
||||
["jq", "jq"],
|
||||
["curl", "curl"],
|
||||
["shellcheck", "shellcheck"],
|
||||
["flock", "util-linux"],
|
||||
];
|
||||
|
||||
const missingPackages = [
|
||||
...new Set(
|
||||
aptPackages
|
||||
.filter(([command]) => !commandExists(command))
|
||||
.map(([, packageName]) => packageName),
|
||||
),
|
||||
];
|
||||
|
||||
if (missingPackages.length === 0) {
|
||||
logSuccess("All required apt CLI dependencies are already installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
const aptRunner = commandExists("sudo")
|
||||
? (args) => run("sudo", ["apt-get", ...args])
|
||||
: (args) => run("apt-get", args);
|
||||
|
||||
logInfo(`Installing missing apt packages: ${missingPackages.join(" ")}`);
|
||||
aptRunner(["update"]);
|
||||
aptRunner(["install", "-y", ...missingPackages]);
|
||||
}
|
||||
|
||||
function ensureNodeDependencies(nodeVersionMajor) {
|
||||
if (!commandExists("npm")) {
|
||||
fail(
|
||||
`npm is required but not installed. Install Node.js >=${nodeVersionMajor} first.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fast deterministic install path aligned with lockfile-based CI behavior.
|
||||
logInfo(
|
||||
"Installing npm dependencies (including devDependencies) via npm ci...",
|
||||
);
|
||||
run("npm", ["ci", "--include=dev"]);
|
||||
}
|
||||
|
||||
function parseAuditJson(outputText) {
|
||||
if (!outputText || outputText.trim().length === 0) {
|
||||
fail("npm audit produced empty output.");
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(outputText);
|
||||
} catch {
|
||||
const firstBraceIndex = outputText.indexOf("{");
|
||||
const lastBraceIndex = outputText.lastIndexOf("}");
|
||||
if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) {
|
||||
return JSON.parse(outputText.slice(firstBraceIndex, lastBraceIndex + 1));
|
||||
}
|
||||
fail("Unable to parse npm audit JSON output.");
|
||||
}
|
||||
}
|
||||
|
||||
function getAuditCounts() {
|
||||
const audit = tryRun("npm", ["audit", "--json"], {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
const report = parseAuditJson(audit.stdout ?? "");
|
||||
const vulnerabilities = report?.metadata?.vulnerabilities ?? {};
|
||||
|
||||
return {
|
||||
critical: Number(vulnerabilities.critical ?? 0),
|
||||
high: Number(vulnerabilities.high ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function runAuditFix(force = false) {
|
||||
const args = force ? ["audit", "fix", "--force"] : ["audit", "fix"];
|
||||
const result = tryRun("npm", args, {
|
||||
cwd: ROOT_DIR,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
logWarn(
|
||||
`npm ${args.join(" ")} exited with code ${result.status ?? "unknown"}. Continuing to verification...`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAuditRemediation() {
|
||||
let counts = getAuditCounts();
|
||||
logInfo(`Audit baseline: high=${counts.high}, critical=${counts.critical}.`);
|
||||
|
||||
if (counts.high === 0 && counts.critical === 0) {
|
||||
logSuccess("No high/critical npm vulnerabilities detected.");
|
||||
return;
|
||||
}
|
||||
|
||||
logWarn(
|
||||
`Detected vulnerabilities: high=${counts.high}, critical=${counts.critical}. Running npm audit fix...`,
|
||||
);
|
||||
runAuditFix(false);
|
||||
counts = getAuditCounts();
|
||||
logInfo(
|
||||
`Audit after npm audit fix: high=${counts.high}, critical=${counts.critical}.`,
|
||||
);
|
||||
|
||||
if (counts.high === 0 && counts.critical === 0) {
|
||||
logSuccess(
|
||||
"High/critical npm vulnerabilities remediated with npm audit fix.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logWarn(
|
||||
`High/critical vulnerabilities remain: high=${counts.high}, critical=${counts.critical}. Running npm audit fix --force...`,
|
||||
);
|
||||
runAuditFix(true);
|
||||
counts = getAuditCounts();
|
||||
logInfo(
|
||||
`Audit after npm audit fix --force: high=${counts.high}, critical=${counts.critical}.`,
|
||||
);
|
||||
|
||||
if (counts.high > 0 || counts.critical > 0) {
|
||||
fail(
|
||||
`Unable to remediate all high/critical vulnerabilities: high=${counts.high}, critical=${counts.critical}.`,
|
||||
);
|
||||
}
|
||||
|
||||
logSuccess("High/critical npm vulnerabilities remediated.");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const nodeVersionMajor = readNodeMajorVersion();
|
||||
logInfo("Ensuring .vscode settings file present...");
|
||||
ensureVscodeSettingsFile();
|
||||
logInfo("Ensuring chat.useAgentsMdFile is enabled...");
|
||||
ensureAgentsFilesUsed();
|
||||
logInfo("Ensuring required APT dependencies are installed...");
|
||||
ensureAptDependencies();
|
||||
logInfo(
|
||||
`Setting up development environment for Node.js v${nodeVersionMajor}.x...`,
|
||||
);
|
||||
ensureNodeDependencies(nodeVersionMajor);
|
||||
ensureAuditRemediation();
|
||||
logSuccess("Development environment setup complete.");
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
logError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
267
dev_scripts/verify_configs.mjs
Normal file
267
dev_scripts/verify_configs.mjs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
ROOT_DIR,
|
||||
fail,
|
||||
logError,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
runCaptureOutput,
|
||||
REPO_SLUG,
|
||||
} from "./lib.mjs";
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import vscodeSettingsJson from "../.vscode/settings.json" with { type: "json" };
|
||||
import tsconfigJson from "../tsconfig.json" with { type: "json" };
|
||||
import typedocJson from "../typedoc.json" with { type: "json" };
|
||||
|
||||
function tokenizePath(pathExpression) {
|
||||
const tokens = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < pathExpression.length) {
|
||||
const ch = pathExpression[index];
|
||||
|
||||
if (ch === ".") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "[") {
|
||||
const endIndex = pathExpression.indexOf("]", index);
|
||||
if (endIndex < 0) {
|
||||
throw new Error(`Invalid path expression '${pathExpression}'`);
|
||||
}
|
||||
|
||||
const indexText = pathExpression.slice(index + 1, endIndex).trim();
|
||||
if (!/^\d+$/.test(indexText)) {
|
||||
throw new Error(`Invalid array index in path '${pathExpression}'`);
|
||||
}
|
||||
|
||||
tokens.push(Number(indexText));
|
||||
index = endIndex + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let endIndex = index;
|
||||
while (
|
||||
endIndex < pathExpression.length &&
|
||||
pathExpression[endIndex] !== "." &&
|
||||
pathExpression[endIndex] !== "["
|
||||
) {
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
tokens.push(pathExpression.slice(index, endIndex));
|
||||
index = endIndex;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function getByPath(root, pathExpression) {
|
||||
const tokens = tokenizePath(pathExpression);
|
||||
let cursor = root;
|
||||
|
||||
for (const token of tokens) {
|
||||
if (typeof token === "number") {
|
||||
if (!Array.isArray(cursor) || token < 0 || token >= cursor.length) {
|
||||
return { found: false, value: undefined };
|
||||
}
|
||||
cursor = cursor[token];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cursor === null || typeof cursor !== "object") {
|
||||
return { found: false, value: undefined };
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(cursor, token)) {
|
||||
return { found: false, value: undefined };
|
||||
}
|
||||
|
||||
cursor = cursor[token];
|
||||
}
|
||||
|
||||
return { found: true, value: cursor };
|
||||
}
|
||||
|
||||
function createErrorCollector(fileLabel) {
|
||||
const errors = [];
|
||||
return {
|
||||
errors,
|
||||
add(message) {
|
||||
errors.push(`${fileLabel}: ${message}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function valueType(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return "array";
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function expectFieldType(doc, fieldPath, expectedType, addError) {
|
||||
const result = getByPath(doc, fieldPath);
|
||||
if (!result.found) {
|
||||
addError(`missing required field '${fieldPath}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const actualType = valueType(result.value);
|
||||
if (actualType !== expectedType) {
|
||||
addError(
|
||||
`field '${fieldPath}' type '${actualType}' does not match expected '${expectedType}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function expectFieldValue(
|
||||
doc,
|
||||
fieldPath,
|
||||
expectedValue,
|
||||
addError,
|
||||
errorMessage,
|
||||
) {
|
||||
const result = getByPath(doc, fieldPath);
|
||||
if (!result.found) {
|
||||
addError(`missing required field '${fieldPath}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.value !== expectedValue) {
|
||||
addError(
|
||||
`field '${fieldPath}' value '${result.value}' does not match expected '${expectedValue}'${errorMessage ? `: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePackageJson(json) {
|
||||
const { errors, add } = createErrorCollector("package.json");
|
||||
logInfo("Validating package.json...");
|
||||
|
||||
expectFieldValue(json, "name", "cache-apt-pkgs-action", add);
|
||||
expectFieldValue(
|
||||
json,
|
||||
"version",
|
||||
"0.0.0-semantically-released",
|
||||
add,
|
||||
"version is updated automatically by semantic-release and should not be changed manually",
|
||||
);
|
||||
expectFieldValue(json, "license", "Apache-2.0", add);
|
||||
expectFieldValue(json, "repository.type", "git", add);
|
||||
expectFieldValue(
|
||||
json,
|
||||
"repository.url",
|
||||
`https://github.com/${REPO_SLUG}.git`,
|
||||
add,
|
||||
);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTsconfigJson(json) {
|
||||
logInfo("Validating tsconfig.json...");
|
||||
const { errors, add } = createErrorCollector("tsconfig.json");
|
||||
|
||||
expectFieldType(json, "extends", "string", add);
|
||||
expectFieldType(json, "compilerOptions", "object", add);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateTypedocJson(json) {
|
||||
logInfo("Validating typedoc.json...");
|
||||
const { errors, add } = createErrorCollector("typedoc.json");
|
||||
|
||||
expectFieldType(json, "entryPoints", "array", add);
|
||||
expectFieldType(json, "out", "string", add);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateVscodeSettingsJson(json) {
|
||||
logInfo("Validating .vscode/settings.json...");
|
||||
const { errors, add } = createErrorCollector(".vscode/settings.json");
|
||||
|
||||
expectFieldType(json, "chat.tools.terminal.autoApprove", "object", add);
|
||||
expectFieldValue(json, "chat.useAgentsMdFile", true, add);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function runGitNameOnly(args, cwd) {
|
||||
const output = runCaptureOutput("git", args, {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
|
||||
function getProhibitedPathsChanges() {
|
||||
const prohibitedPaths = ["docs/api"];
|
||||
const staged = runGitNameOnly(
|
||||
["diff", "--name-only", "--cached", "--", ...prohibitedPaths],
|
||||
ROOT_DIR,
|
||||
);
|
||||
const unstaged = runGitNameOnly(
|
||||
["diff", "--name-only", "--", ...prohibitedPaths],
|
||||
ROOT_DIR,
|
||||
);
|
||||
const untracked = runGitNameOnly(
|
||||
["ls-files", "--others", "--exclude-standard", "--", ...prohibitedPaths],
|
||||
ROOT_DIR,
|
||||
);
|
||||
|
||||
return [...new Set([...staged, ...unstaged, ...untracked])].sort(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const errors = [
|
||||
...validatePackageJson(packageJson),
|
||||
...validateTsconfigJson(tsconfigJson),
|
||||
...validateTypedocJson(typedocJson),
|
||||
...validateVscodeSettingsJson(vscodeSettingsJson),
|
||||
];
|
||||
|
||||
const changedPaths = getProhibitedPathsChanges();
|
||||
if (changedPaths.length > 0) {
|
||||
errors.push(
|
||||
`git-change-check: detected changes in prohibited paths (docs/api): ${changedPaths.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
logError(`Found ${errors.length} validation issue(s):`);
|
||||
for (const errorText of errors) {
|
||||
logError(`- ${errorText}`);
|
||||
}
|
||||
fail("Validation failed.");
|
||||
}
|
||||
|
||||
logSuccess("Validation passed.");
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
185
dev_scripts/wipe_commit_history.mjs
Normal file
185
dev_scripts/wipe_commit_history.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import {
|
||||
ROOT_DIR,
|
||||
assertNonEmpty,
|
||||
fail,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
logWarn,
|
||||
REPO_SLUG,
|
||||
run,
|
||||
runCaptureOutput,
|
||||
tryRun,
|
||||
usage,
|
||||
} from "./lib.mjs";
|
||||
|
||||
const target = process.argv[2] ?? "";
|
||||
const confirmFlag = process.argv[3] ?? "";
|
||||
const usageMessage = usage(
|
||||
process.argv[1],
|
||||
"<target branch> --confirm-destructive",
|
||||
);
|
||||
|
||||
assertNonEmpty(target, "Target branch is empty", usageMessage);
|
||||
|
||||
if (confirmFlag !== "--confirm-destructive") {
|
||||
fail(
|
||||
"WARNING: This operation force-rewrites history and has irreversible side effects.\n" +
|
||||
"To proceed, re-run the command with the --confirm-destructive flag.",
|
||||
);
|
||||
}
|
||||
|
||||
const rootDir = ROOT_DIR;
|
||||
const timestamp = new Date().toISOString().replace(/[:T]/g, "_").slice(0, 19);
|
||||
function ensureRepo() {
|
||||
logInfo(`Ensuring ${rootDir} is a git repository...`);
|
||||
if (
|
||||
!tryRun("git", ["rev-parse", "--is-inside-work-tree"], { cwd: rootDir }).ok
|
||||
) {
|
||||
fail(`${rootDir} is not a git repository`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTargetRef(branch) {
|
||||
if (
|
||||
tryRun("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
||||
cwd: rootDir,
|
||||
}).ok
|
||||
) {
|
||||
logInfo(`Target branch ${branch} exists locally.`);
|
||||
return branch;
|
||||
}
|
||||
|
||||
if (
|
||||
tryRun(
|
||||
"git",
|
||||
["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`],
|
||||
{ cwd: rootDir },
|
||||
).ok
|
||||
) {
|
||||
return `origin/${branch}`;
|
||||
}
|
||||
|
||||
fail(`Target branch ${branch} does not exist locally or on origin`);
|
||||
}
|
||||
|
||||
function listGitHubRunIds() {
|
||||
const output = runCaptureOutput(
|
||||
"gh",
|
||||
["run", "list", "--limit", "1000", "--json", "databaseId"],
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
|
||||
if (!output) {
|
||||
logWarn("No GitHub workflow runs found.");
|
||||
return [];
|
||||
}
|
||||
|
||||
let runs;
|
||||
try {
|
||||
runs = JSON.parse(output);
|
||||
logInfo(`Found ${runs.length} GitHub workflow run(s) to delete.\n`);
|
||||
} catch (error) {
|
||||
fail(
|
||||
`Unable to parse gh run list response: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(runs)) {
|
||||
logWarn("No GitHub workflow runs found.");
|
||||
return [];
|
||||
}
|
||||
|
||||
return runs
|
||||
.map((runEntry) => String(runEntry?.databaseId ?? "").trim())
|
||||
.filter((runId) => /^\d+$/.test(runId));
|
||||
}
|
||||
|
||||
function deleteGitHubRuns() {
|
||||
const runIds = listGitHubRunIds();
|
||||
if (runIds.length === 0) {
|
||||
logInfo("No GitHub workflow runs found to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
logInfo(`Deleting ${runIds.length} GitHub workflow run(s)...`);
|
||||
for (const runId of runIds) {
|
||||
run("gh", ["run", "delete", runId, "--repo", REPO_SLUG], {
|
||||
cwd: rootDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
ensureRepo();
|
||||
|
||||
const targetRef = resolveTargetRef(target);
|
||||
const backupBranch = `backup/pre-wipe-${target}-${timestamp}`;
|
||||
const backupTag = `pre-wipe-${target}-${timestamp}`;
|
||||
const mirrorDir = path.resolve(
|
||||
rootDir,
|
||||
"..",
|
||||
`cache-apt-pkgs-action.mirror-backup.${timestamp}.git`,
|
||||
);
|
||||
|
||||
logInfo(`Creating mirror backup at ${mirrorDir}...`);
|
||||
if (tryRun("test", ["-d", mirrorDir]).ok) {
|
||||
fail(`${mirrorDir} already exists`);
|
||||
}
|
||||
|
||||
logInfo(`Cloning ${rootDir} to mirror backup...`);
|
||||
run("git", ["clone", "--mirror", ".", mirrorDir], { cwd: rootDir });
|
||||
|
||||
logInfo(`Creating backup refs from ${targetRef}...`);
|
||||
run("git", ["branch", backupBranch, targetRef], { cwd: rootDir });
|
||||
run(
|
||||
"git",
|
||||
[
|
||||
"tag",
|
||||
"-a",
|
||||
backupTag,
|
||||
"-m",
|
||||
`Backup before history rewrite of ${target}`,
|
||||
targetRef,
|
||||
],
|
||||
{ cwd: rootDir },
|
||||
);
|
||||
logInfo(`Pushing backup refs ${backupBranch} and ${backupTag} to origin...`);
|
||||
run("git", ["push", "origin", `refs/heads/${backupBranch}`], {
|
||||
cwd: rootDir,
|
||||
});
|
||||
run("git", ["push", "origin", `refs/tags/${backupTag}`], { cwd: rootDir });
|
||||
|
||||
logSuccess("Backup complete. Recovery refs:");
|
||||
logSuccess(` branch: ${backupBranch}`);
|
||||
logSuccess(` tag: ${backupTag}`);
|
||||
logSuccess(` mirror: ${mirrorDir}`);
|
||||
logWarn("To restore original history later:");
|
||||
logWarn(
|
||||
` git push --force origin refs/heads/${backupBranch}:refs/heads/${target}`,
|
||||
);
|
||||
|
||||
logInfo(`Rewriting history of ${target}...`);
|
||||
run("git", ["checkout", "-B", target, targetRef], { cwd: rootDir });
|
||||
run("git", ["checkout", "--orphan", `temp_wipe_${target}`], { cwd: rootDir });
|
||||
run("git", ["add", "-A"], { cwd: rootDir });
|
||||
run("git", ["commit", "-m", "Initial commit"], { cwd: rootDir });
|
||||
run("git", ["branch", "-D", target], { cwd: rootDir });
|
||||
run("git", ["branch", "-m", target], { cwd: rootDir });
|
||||
logInfo(`Force-pushing rewritten history to origin/${target}...`);
|
||||
run("git", ["push", "--force", "origin", target], { cwd: rootDir });
|
||||
|
||||
logInfo("Deleting GitHub workflow runs associated with the old history...");
|
||||
deleteGitHubRuns();
|
||||
|
||||
logSuccess("Wipe commit history complete.");
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
||||
|
|
@ -1 +0,0 @@
|
|||
window.hierarchyData = "eJyrVirKzy8pVrKKjtVRKkpNy0lNLsnMzytWsqqurQUAmx4Kpg=="
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
:root {
|
||||
--light-hl-0: #800000;
|
||||
--dark-hl-0: #569CD6;
|
||||
--light-hl-1: #000000;
|
||||
--dark-hl-1: #D4D4D4;
|
||||
--light-hl-2: #0000FF;
|
||||
--dark-hl-2: #CE9178;
|
||||
--light-hl-3: #0000FF;
|
||||
--dark-hl-3: #569CD6;
|
||||
--light-hl-4: #098658;
|
||||
--dark-hl-4: #B5CEA8;
|
||||
--light-hl-5: #AF00DB;
|
||||
--dark-hl-5: #C586C0;
|
||||
--light-code-background: #FFFFFF;
|
||||
--dark-code-background: #1E1E1E;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) { :root {
|
||||
--hl-0: var(--light-hl-0);
|
||||
--hl-1: var(--light-hl-1);
|
||||
--hl-2: var(--light-hl-2);
|
||||
--hl-3: var(--light-hl-3);
|
||||
--hl-4: var(--light-hl-4);
|
||||
--hl-5: var(--light-hl-5);
|
||||
--code-background: var(--light-code-background);
|
||||
} }
|
||||
|
||||
@media (prefers-color-scheme: dark) { :root {
|
||||
--hl-0: var(--dark-hl-0);
|
||||
--hl-1: var(--dark-hl-1);
|
||||
--hl-2: var(--dark-hl-2);
|
||||
--hl-3: var(--dark-hl-3);
|
||||
--hl-4: var(--dark-hl-4);
|
||||
--hl-5: var(--dark-hl-5);
|
||||
--code-background: var(--dark-code-background);
|
||||
} }
|
||||
|
||||
:root[data-theme='light'] {
|
||||
--hl-0: var(--light-hl-0);
|
||||
--hl-1: var(--light-hl-1);
|
||||
--hl-2: var(--light-hl-2);
|
||||
--hl-3: var(--light-hl-3);
|
||||
--hl-4: var(--light-hl-4);
|
||||
--hl-5: var(--light-hl-5);
|
||||
--code-background: var(--light-code-background);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--hl-0: var(--dark-hl-0);
|
||||
--hl-1: var(--dark-hl-1);
|
||||
--hl-2: var(--dark-hl-2);
|
||||
--hl-3: var(--dark-hl-3);
|
||||
--hl-4: var(--dark-hl-4);
|
||||
--hl-5: var(--dark-hl-5);
|
||||
--code-background: var(--dark-code-background);
|
||||
}
|
||||
|
||||
.hl-0 { color: var(--hl-0); }
|
||||
.hl-1 { color: var(--hl-1); }
|
||||
.hl-2 { color: var(--hl-2); }
|
||||
.hl-3 { color: var(--hl-3); }
|
||||
.hl-4 { color: var(--hl-4); }
|
||||
.hl-5 { color: var(--hl-5); }
|
||||
pre, code { background: var(--code-background); }
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
window.navigationData = "eJyNkk9PwzAMxb9LztUQE0yotzIJaRoDxHXaIWpdaq11qjiFDcR3R8pK/yXLdo3f+zn28/ZHGDgYEQuZGlQkIlFLU4hYVCprSuCb0/usMFUpIrFHykQ8j0RaYJlpIBFvO0RipW8y3csPeJEV9LS0lMw9zVGO+bfzh99ogn1viEBfIJ5El2ErqhvDPQzJgM5lOuWddJPh7xcO77Ux1wFbYYhISleyxG+wzdsdDdB5QxbVkf2GcYvF3aBDLTXDo1IlSApwh7IATTeUTG7HQXUah7MbkJAyOLgXaJ8nGxu6lMeirjrYpUwLz5GimtlK6I6sYA3H8+41HEOANievv62F7BkwaLSxu1/p949q5lEG0kROavOMbPhJAxdnmBNVMNVKEubAxk3pv3JVVhpktmkNCS/50/e1juioAyN/aTSwcX7poY6UnqF3f0nc1Lc="
|
||||
|
|
@ -1 +0,0 @@
|
|||
window.searchData = "eJy9mm1v2zYQx7+L8lZwzSfF9rtsWLGie0K3dS+MYFBlOiGqSIYkp+uCfPeBEmXd+SibitO9auHo/nckf3c8UXyKqvJLHa3WT9FnU2yiFY+jIn3Q0SpKs8aURRRH+yqPVtFDudnnun7T/Ty7bx7yKI6yPK1rXUerKHqOew3GFweVm/bx39Lsc3qnf7G/9YLOtBckD3pcxNEurXTRDNENThUbQs/Kom6qfdaU1TR3V9jylGuqAGdgzuUhmo2pm6qcGMjB6HViKCbP/JUzeR3/j7qqIU1hIQxWL46Cz+UAY60rk+bm36lTAe1ePh8kKz7si0KfIbR75v/IBeBpehq4oYzM+6e9yTdvTa5/MnXztqzcBIXH0wpsTa5zUzfbstodBC6ObWuKzbuibtI8/z2rzK4Jj8qams607k0vjudON9+n2b1+r7+GR3Knm8wafW6NXi2GD2U5YTr6IKrO6uIo3NTeFJs2GAdNHR6QE0iLTRvXbhC4OLairB7aivCu2O2b6aEd7I21/yaR9UH9ZZr7j10hfUmAfWxfTHP/OMhcHKfT/X2nM7M1uvqjPN8dwAD7uHr7pgzfsM5FVtX6u7LMdXpmw8LxVLX+dLC6OIpK12X+2K/ix5D9E4bjzN0sTdpIz8bVlNULstEZvibr1b64wV3q+Sj2xaGxvdh/k1YfdJ42xq5Tcx8eRZNWlTPcdYYXx7LfbdJG3+wau8dOWJbOLt01ubO7OJLHNDdW84eHXfN1Oie9ubbml9KikqOeq63XQzSmaHS1TbPjgLrHJrZduOvXn/Z3U/xc9RYBA3WjGHGt4cR/p+/TRwOav5BQ0Nx/GhQuD+0fne0bjRquSatx5RRQ3xWIx+nQdsekhkQzjc/TARy/I4X4n1TYiXuaHr/um6D8cM9dkiBpnuPNzRauSY6v0jzHG1zeSQTMRT/QkeDahvFHMzGe1urevE4Iuwsn5/VmJpGhfe92X7Qah7j8z0/EJpGnezPiFT71cl+0uyCODo9M9MLBS85G/0MO2dpfT56xAYWSmpfB53PtqxXZnE05a/9welimfMGZA5IOOmjoLeDoE84kpMLTfGFHJ9ssn4ej92Hf6zj2cKebU6/fHh9kHU45ea+/fsPV6NUnLYgNd6y4V9n4ihycuade7GVbVpn+s21g3xVZpR+s5VmvrVXX9hpg9eIoDjVuM9rskhgGm7MdREAEY6erxO25dsHvK/AMlXg7f2w6MjaQF2NHhaacuT99m6yA4qFJ0cc65Qz+2NXJQ4zzPk6QgNwEgEA9hXOAfAVhQL0l8F3qoEHq5LAnm3LmeXACHsCjqfs36beVru/93o4emuBpwPAhLcxWg96u38P7P5zcyWG3otPNz87mpv6+fvTEfNAkD5+O/RCk1/OXyjT65+NxeLyiB0M93sauR1o9HeheRXwmZssojrZG5xv7/bLPrqx8cPV8U2b79r+37rGP2iawfbh7+s08itfzWFzP5kre3sbr3rj9Q/tDrzH80hqyKF4znyEjhgwZ8ihe85jL2TxhyJATQ44MRRSvhc+jIIYCGcooXstYsNlSJshQEkOJDFUUr1XMk9k8WSBDRQwVMkyieJ3EfDFTCzzGhBgmyPA6itfXvjFeE8NrZLgYm9UFMVwgw2UUrxc+j0tiuMQAWB6WXgIoO+wInpYeL3fMww8GiFksmJ89yhDDEDGLBuNeY8oRwyAxiwcTPpQYZYlhmJhFhEmvZ8oTw0AxiwlTXmPKFMNQMYsKS7xhU64YBotZXJiXSUbZYhguZpFhXrwY5YthwLhlhi19YXNKGMeEccsM9xLGKWH8qES1NcpLGPdUKUwYt8xwL2GcEsYxYdwyw71VjlPCOCaMW2a4lzBOCeOYMG6Z4V7COCWMY8K4ZYYnXmNKGMeE8bZ2eQnjlDCOCePLsRrNKWAcAyYsMtxLp6CACQyYaAHz1j9BARMYMGGREV46BQVMHO2D7UbopVN4tkIMmBgvYYICJjBgQo0WEkEBExgwkYyms6CACQyYsMgIb1IJCpjAgAmLjPC3DhQwgQETlhnhTSpBCROYMDkf25YlBUxiwKRFRngTUlLAJAZMtoB5E1JSwCQGTLaAeRNSUsDkUbMlR4fsabcwX9ISI7wJKSlfEvMlLTHCm5CS8iUxX9ISI70JKSlfEvMlF2M1SFK8JMZLLsdaRUnpkpguZYGR3kKgKF4K46XY2DopSpfCdCk+1lErCpfCcCkx2lRTthRmS8mxyVKULXXUy1tapLeCKE87j9lSlhbprSCKsqUwW6ply1tBFGVLYbaUxUV6q4CicCkMl7K8SG8VUJQu91P7svmoq0Zv3nUvnev14SLsU/S3exOd9+++T9E8Wj09Pw/vnaunZ/Dqaf9m/XQSxn17HoS4GJS4nCBV9t/pgNZi0BJTwnInkN0L9KDHBjk2Qa1ylxoHoetB6DpMyP9ZDwx1CYYaGFx71DxICDBbUgVJjF5CHFSB6DJIM+s+v4C4JBjaBI32qyOQAYgKHi7TfsEAMmDtpAiTgWelYMkA6J3lovtHzt2/svtXha2nuywBHICJ42ELCk4LfYOXIGYVqOhubYMZBOsQJDFy9QIMVIGBJmGa/jsTQDMBmmFJ6rn8OuiB/GRhpcj/UQbMI5CUYaNGl2JBXQOpwcJgwzdbgRSskWFZ1n8lBCMDcy/DRNxHY6ABUBVh6zd6ORYMD6iyMHoNhh/SvwgTqPuraNvu4B3kI1BTYQgMp+tABhQKFTZXx1sjUOirVtjC+T77AVlApgyMbOQeMVhEMGvMRSvCWo0zd4CBDzAhLKxSejZPCbJJhVUN34A52LN4GHRjd4nBCEHRZWHsnW5hBBisCMstfMMYxAYKCLt2Kxy6Cg3uisAwZVhttJ+S+jRL68x+dwJLCvRU2FqMXFwGwwXry4I18dVjoAZaNhbWb4ErxEAH7E/ctTQiDBTwfXTQA2uauAZp6UpNWPKSK8YgQ0Cd4WGrfHxLGIgBkHlYFRy54Qs0YbcYlhweUCB7nRV3Uyhd76nCtNvvlt6tBKySCoDnNo52ZqdzU+hotb59fv4P0wXb3Q==";
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>cache-apt-pkgs-action</title><meta name="description" content="Documentation for cache-apt-pkgs-action"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><script defer src="assets/main.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">cache-apt-pkgs-action</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><h1>cache-apt-pkgs-action</h1></div><h2>Hierarchy Summary</h2></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="modules.html">cache-apt-pkgs-action</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
<!DOCTYPE html><html class="default" lang="en" data-base="./"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>cache-apt-pkgs-action</title><meta name="description" content="Documentation for cache-apt-pkgs-action"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="assets/style.css"/><link rel="stylesheet" href="assets/highlight.css"/><script defer src="assets/main.js"></script><script async src="assets/icons.js" id="tsd-icons-script"></script><script async src="assets/search.js" id="tsd-search-script"></script><script async src="assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="index.html" class="title">cache-apt-pkgs-action</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><h1>cache-apt-pkgs-action</h1></div><div class="tsd-panel tsd-typography"><h1 id="cache-apt-pkgs-action" class="tsd-anchor-link">cache-apt-pkgs-action<a href="#cache-apt-pkgs-action" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h1>
|
||||
<p><a href="https://github.com/awalsh128/fluentcpp/blob/master/LICENSE"><img src="https://shields.io/badge/license-apache2-blue.svg" alt="License: Apache2"></a>
|
||||
<a href="https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/master_test.yml"><img src="https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/master_test.yml/badge.svg" alt="Master Test status"></a>
|
||||
<a href="https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/dev_test.yml"><img src="https://github.com/awalsh128/cache-apt-pkgs-action-ci/actions/workflows/dev_test.yml/badge.svg" alt="Dev Test status"></a></p>
|
||||
<p>This action allows caching of Advanced Package Tool (APT) package dependencies to improve workflow execution time instead of installing the packages on every run.</p>
|
||||
<div class="tsd-alert tsd-alert-note"><div class="tsd-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><use href="assets/icons.svg#icon-alertNote"></use></svg><span>Note</span></div><p>
|
||||
The open source projects that I maintain are a labor of love. If you find this useful and want to support open source, <strong>please consider donating and <a href="http://buymeacoffee.com/awalsh128">Buy Me a Coffe</a></strong>.</p>
|
||||
</div>
|
||||
<div class="tsd-alert tsd-alert-note"><div class="tsd-alert-title"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><use href="assets/icons.svg#icon-alertNote"></use></svg><span>Note</span></div><p>
|
||||
Version 2 of the action is now available! See <a href="media/V2_FAQ.MD">Version 2 FAQ</a> for more information.</p>
|
||||
</div>
|
||||
<div class="tsd-alert tsd-alert-important"><div class="tsd-alert-title"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><use href="assets/icons.svg#icon-alertImportant"></use></svg><span>Important</span></div><p>
|
||||
Looking for co-maintainers to help review changes, and investigate issues. I haven't had as much time to stay on top of this action as I would like to and want to make sure it is still responsive and reliable for the community. If you are interested, please reach out.</p>
|
||||
</div>
|
||||
<h2 id="documentation" class="tsd-anchor-link">Documentation<a href="#documentation" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2>
|
||||
<p>This action is a composition of <a href="https://github.com/actions/cache/">actions/cache</a> and the <code>apt</code> utility. Some actions require additional APT based packages to be installed in order for other steps to be executed. Packages can be installed when ran but can consume much of the execution workflow time.</p>
|
||||
<h2 id="usage" class="tsd-anchor-link">Usage<a href="#usage" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2>
|
||||
<h3 id="pre-requisites" class="tsd-anchor-link">Pre-requisites<a href="#pre-requisites" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>Create a workflow <code>.yml</code> file in your repositories <code>.github/workflows</code> directory. An <a href="#example-workflow">example workflow</a> is available below. For more information, reference the GitHub Help Documentation for <a href="https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file">Creating a workflow file</a>.</p>
|
||||
<h3 id="versions" class="tsd-anchor-link">Versions<a href="#versions" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>There are three kinds of version labels you can use.</p>
|
||||
<ul>
|
||||
<li><code>@latest</code> - This will give you the latest release.</li>
|
||||
<li><code>@v#</code> - Major only will give you the latest release for that major version only (e.g. <code>v1</code>).</li>
|
||||
<li>Branch
|
||||
<ul>
|
||||
<li><code>@master</code> - Most recent manual and automated tested code. Possibly unstable since it is pre-release.</li>
|
||||
<li><code>@staging</code> - Most recent automated tested code and can sometimes contain experimental features. Is pulled from dev stable code.</li>
|
||||
<li><code>@dev</code> - Very unstable and contains experimental features. Automated testing may not show breaks since CI is also updated based on code in dev.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 id="inputs" class="tsd-anchor-link">Inputs<a href="#inputs" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<ul>
|
||||
<li><code>packages</code> - Space delimited list of packages to install.</li>
|
||||
<li><code>version</code> - Version of cache to load. Each version will have its own cache. Note, all characters except spaces are allowed.</li>
|
||||
<li><code>execute_install_scripts</code> - Execute Debian package pre and post install script upon restore. See <a href="#non-file-dependencies">Caveats / Non-file Dependencies</a> for more information.</li>
|
||||
<li><code>empty_packages_behavior</code> - Desired behavior when the given <code>packages</code> is empty. <code>'error'</code> (default), <code>'warn'</code> or <code>'ignore'</code>.</li>
|
||||
</ul>
|
||||
<h3 id="outputs" class="tsd-anchor-link">Outputs<a href="#outputs" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<ul>
|
||||
<li><code>cache-hit</code> - A boolean value to indicate a cache was found for the packages requested.</li>
|
||||
<li><code>package-version-list</code> - The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package1>=<version1>,<package2>=<version2>,...).</li>
|
||||
<li><code>all-package-version-list</code> - All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package1>=<version1>,<package2>=<version2>,...).</li>
|
||||
</ul>
|
||||
<h3 id="security-compliance" class="tsd-anchor-link">Security Compliance<a href="#security-compliance" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>This action runs as a JavaScript GitHub Action on the <code>node24</code> runtime and does not require consumers to run <code>npm install</code> in their workflow in order to use it. The implementation dependency surface is limited to the action runtime packages declared in <code>package.json</code>: <code>@actions/cache</code>, <code>@actions/core</code>, <code>tar</code>, <code>winston</code>, and <code>ts-apt</code>.</p>
|
||||
<p>For workflows with stricter compliance requirements, the main security characteristics are:</p>
|
||||
<ul>
|
||||
<li><code>packages</code> should be treated as an allowlisted input in your workflow. Prefer explicit package names and versions where reproducibility matters.</li>
|
||||
<li><code>version</code> can be used as a cache namespace so you can intentionally rotate caches when package policy or runner baselines change.</li>
|
||||
<li><code>empty_packages_behavior</code> can be left at the default <code>error</code> to fail closed when an expected package list is missing.</li>
|
||||
<li><code>execute_install_scripts</code> is disabled by default. Enable it only when required, because Debian maintainer scripts execute arbitrary package-provided shell logic during restore.</li>
|
||||
<li><code>debug</code> is disabled by default. Enable it only for investigation and review logs before sharing them outside your organization.</li>
|
||||
</ul>
|
||||
<h4 id="features" class="tsd-anchor-link">Features<a href="#features" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h4>
|
||||
<p>Security-relevant action features:</p>
|
||||
<ul>
|
||||
<li>Package inputs are normalized before cache lookup, which reduces accidental cache divergence from ordering, commas, backslashes, or duplicate whitespace in the package list.</li>
|
||||
<li>Unpinned package names are resolved to concrete package versions before the cache key is generated, which improves traceability of what was actually cached for a run.</li>
|
||||
<li>Cache keys are derived from the normalized package set, the user-provided <code>version</code>, and the runner architecture, which helps isolate caches across package changes and incompatible platforms.</li>
|
||||
<li>The action rejects invalid boolean inputs and rejects <code>version</code> values containing spaces, reducing ambiguous workflow configuration.</li>
|
||||
<li>When creating archives, the action records installed package manifests and only archives existing files and symlinks from installed packages, plus maintainer scripts when present.</li>
|
||||
<li>The <code>package-version-list</code> and <code>all-package-version-list</code> outputs can be captured by downstream workflow steps for audit logs, attestation inputs, or compliance reporting.</li>
|
||||
</ul>
|
||||
<h4 id="usage-recommendations" class="tsd-anchor-link">Usage Recommendations<a href="#usage-recommendations" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h4>
|
||||
<p>For GitHub Actions workflow hardening, prefer the following controls around this action:</p>
|
||||
<ul>
|
||||
<li>Pin this action to a major version you trust, or to a full commit SHA for stricter supply chain control.</li>
|
||||
<li>Use the minimum required workflow <code>permissions</code> instead of broad defaults.</li>
|
||||
<li>Run on GitHub-hosted or otherwise trusted runners with controlled APT sources.</li>
|
||||
<li>Limit who can modify workflow files and package lists through branch protection and pull request review.</li>
|
||||
<li>Review whether cached APT contents are acceptable for your repository's cache retention policy and data handling requirements.</li>
|
||||
</ul>
|
||||
<p>Example hardened usage:</p>
|
||||
<pre><code class="yaml"><span class="hl-0">permissions</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">contents</span><span class="hl-1">: </span><span class="hl-2">read</span><br/><br/><span class="hl-0">jobs</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">build</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">runs-on</span><span class="hl-1">: </span><span class="hl-2">ubuntu-latest</span><br/><span class="hl-1"> </span><span class="hl-0">steps</span><span class="hl-1">:</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">actions/checkout@v4</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">awalsh128/cache-apt-pkgs-action@v2</span><br/><span class="hl-1"> </span><span class="hl-0">with</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">packages</span><span class="hl-1">: </span><span class="hl-2">curl=8.5.0-2ubuntu10.6 jq=1.7.1-3build1</span><br/><span class="hl-1"> </span><span class="hl-0">version</span><span class="hl-1">: </span><span class="hl-2">ubuntu-24.04-v1</span><br/><span class="hl-1"> </span><span class="hl-0">empty_packages_behavior</span><span class="hl-1">: </span><span class="hl-2">error</span><br/><span class="hl-1"> </span><span class="hl-0">execute_install_scripts</span><span class="hl-1">: </span><span class="hl-3">false</span>
|
||||
</code><button type="button">Copy</button></pre>
|
||||
|
||||
<h3 id="cache-scopes" class="tsd-anchor-link">Cache scopes<a href="#cache-scopes" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>The cache is scoped to the packages given and the branch. The default branch cache is available to other branches.</p>
|
||||
<h3 id="example-workflow" class="tsd-anchor-link">Example workflow<a href="#example-workflow" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>This was a motivating use case for creating this action.</p>
|
||||
<pre><code class="yaml"><span class="hl-0">name</span><span class="hl-1">: </span><span class="hl-2">Create Documentation</span><br/><span class="hl-3">on</span><span class="hl-1">: </span><span class="hl-2">push</span><br/><span class="hl-0">jobs</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">build_and_deploy_docs</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">runs-on</span><span class="hl-1">: </span><span class="hl-2">ubuntu-latest</span><br/><span class="hl-1"> </span><span class="hl-0">name</span><span class="hl-1">: </span><span class="hl-2">Build Doxygen documentation and deploy</span><br/><span class="hl-1"> </span><span class="hl-0">steps</span><span class="hl-1">:</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">actions/checkout@v4</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">awalsh128/cache-apt-pkgs-action@latest</span><br/><span class="hl-1"> </span><span class="hl-0">with</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">packages</span><span class="hl-1">: </span><span class="hl-2">dia doxygen doxygen-doc doxygen-gui doxygen-latex graphviz mscgen</span><br/><span class="hl-1"> </span><span class="hl-0">version</span><span class="hl-1">: </span><span class="hl-4">1.0</span><br/><br/><span class="hl-1"> - </span><span class="hl-0">name</span><span class="hl-1">: </span><span class="hl-2">Build</span><br/><span class="hl-1"> </span><span class="hl-0">run</span><span class="hl-1">: </span><span class="hl-5">|</span><br/><span class="hl-2"> cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} </span><br/><span class="hl-2"> cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}</span><br/><br/><span class="hl-1"> - </span><span class="hl-0">name</span><span class="hl-1">: </span><span class="hl-2">Deploy</span><br/><span class="hl-1"> </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">JamesIves/github-pages-deploy-action@4.1.5</span><br/><span class="hl-1"> </span><span class="hl-0">with</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">branch</span><span class="hl-1">: </span><span class="hl-2">gh-pages</span><br/><span class="hl-1"> </span><span class="hl-0">folder</span><span class="hl-1">: </span><span class="hl-2">${{github.workspace}}/build/website</span>
|
||||
</code><button type="button">Copy</button></pre>
|
||||
|
||||
<pre><code class="yaml"><span class="hl-1">---</span><br/><span class="hl-0">install_doxygen_deps</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">runs-on</span><span class="hl-1">: </span><span class="hl-2">ubuntu-latest</span><br/><span class="hl-1"> </span><span class="hl-0">steps</span><span class="hl-1">:</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">actions/checkout@v4</span><br/><span class="hl-1"> - </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">awalsh128/cache-apt-pkgs-action@latest</span><br/><span class="hl-1"> </span><span class="hl-0">with</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">packages</span><span class="hl-1">: </span><span class="hl-2">dia doxygen doxygen-doc doxygen-gui doxygen-latex graphviz mscgen</span><br/><span class="hl-1"> </span><span class="hl-0">version</span><span class="hl-1">: </span><span class="hl-4">1.0</span>
|
||||
</code><button type="button">Copy</button></pre>
|
||||
|
||||
<h2 id="caveats" class="tsd-anchor-link">Caveats<a href="#caveats" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h2>
|
||||
<h3 id="non-file-dependencies" class="tsd-anchor-link">Non-file Dependencies<a href="#non-file-dependencies" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>This action is based on the principle that most packages can be cached as a fileset. There are situations though where this is not enough.</p>
|
||||
<ul>
|
||||
<li>Pre and post installation scripts needs to be ran from <code>/var/lib/dpkg/info/{package name}.[preinst, postinst]</code>.</li>
|
||||
<li>The Debian package database needs to be queried for scripts above (i.e. <code>dpkg-query</code>).</li>
|
||||
</ul>
|
||||
<p>The <code>execute_install_scripts</code> argument can be used to attempt to execute the install scripts but they are no guaranteed to resolve the issue.</p>
|
||||
<pre><code class="yaml"><span class="hl-1">- </span><span class="hl-0">uses</span><span class="hl-1">: </span><span class="hl-2">awalsh128/cache-apt-pkgs-action@latest</span><br/><span class="hl-1"> </span><span class="hl-0">with</span><span class="hl-1">:</span><br/><span class="hl-1"> </span><span class="hl-0">packages</span><span class="hl-1">: </span><span class="hl-2">mypackage</span><br/><span class="hl-1"> </span><span class="hl-0">version</span><span class="hl-1">: </span><span class="hl-4">1.0</span><br/><span class="hl-1"> </span><span class="hl-0">execute_install_scripts</span><span class="hl-1">: </span><span class="hl-3">true</span>
|
||||
</code><button type="button">Copy</button></pre>
|
||||
|
||||
<p>If this does not solve your issue, you will need to run <code>apt-get install</code> as a separate step for that particular package unfortunately.</p>
|
||||
<pre><code class="yaml"><span class="hl-0">run</span><span class="hl-1">: </span><span class="hl-2">apt-get install mypackage</span><br/><span class="hl-0">shell</span><span class="hl-1">: </span><span class="hl-2">bash</span>
|
||||
</code><button type="button">Copy</button></pre>
|
||||
|
||||
<p>Please reach out if you have found a workaround for your scenario and it can be generalized. There is only so much this action can do and can't get into the area of reverse engineering Debian package manager. It would be beyond the scope of this action and may result in a lot of extended support and brittleness. Also, it would be better to contribute to Debian packager instead at that point.</p>
|
||||
<p>For more context and information see <a href="https://github.com/awalsh128/cache-apt-pkgs-action/issues/57#issuecomment-1321024283">issue #57</a> which contains the investigation and conclusion.</p>
|
||||
<h3 id="cache-limits" class="tsd-anchor-link">Cache Limits<a href="#cache-limits" aria-label="Permalink" class="tsd-anchor-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="assets/icons.svg#icon-anchor"></use></svg></a></h3>
|
||||
<p>A repository can have up to 5GB of caches. Once the 5GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted. To get more information on how to access and manage your actions's caches, see <a href="https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#viewing-cache-entries">GitHub Actions / Using workflows / Cache dependencies</a>.</p>
|
||||
</div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div><details open class="tsd-accordion tsd-page-navigation"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="assets/icons.svg#icon-chevronDown"></use></svg><h3>On This Page</h3></summary><div class="tsd-accordion-details"><a href="#cache-apt-pkgs-action"><span>cache-<wbr/>apt-<wbr/>pkgs-<wbr/>action</span></a><ul><li><a href="#documentation"><span>Documentation</span></a></li><li><a href="#usage"><span>Usage</span></a></li><li><ul><li><a href="#pre-requisites"><span>Pre-<wbr/>requisites</span></a></li><li><a href="#versions"><span>Versions</span></a></li><li><a href="#inputs"><span>Inputs</span></a></li><li><a href="#outputs"><span>Outputs</span></a></li><li><a href="#security-compliance"><span>Security <wbr/>Compliance</span></a></li><li><ul><li><a href="#features"><span>Features</span></a></li><li><a href="#usage-recommendations"><span>Usage <wbr/>Recommendations</span></a></li></ul></li><li><a href="#cache-scopes"><span>Cache scopes</span></a></li><li><a href="#example-workflow"><span>Example workflow</span></a></li></ul></li><li><a href="#caveats"><span>Caveats</span></a></li><li><ul><li><a href="#non-file-dependencies"><span>Non-<wbr/>file <wbr/>Dependencies</span></a></li><li><a href="#cache-limits"><span>Cache <wbr/>Limits</span></a></li></ul></li></ul></div></details></div><div class="site-menu"><nav class="tsd-navigation"><a href="modules.html">cache-apt-pkgs-action</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,27 +0,0 @@
|
|||
# cache-apt-pkgs-action - Version 2 FAQ
|
||||
|
||||
## Why did you create a new version
|
||||
|
||||
### Bash Scripts
|
||||
|
||||
Bash shell scripting grew in complexity and became very unreadable. The action needed higher level language idioms like array and map handling, complex pattern matching, and complex text processing.
|
||||
|
||||
### Language Choice
|
||||
|
||||
Languages like Python, Go, Rust, etc. have performance and writeability trade offs that would greatly improve this action, but ultimately Typescript was chosen because:
|
||||
|
||||
- it is the most performant and well supported for GitHub actions;
|
||||
- native strongly types libraries for action args and actions like caching;
|
||||
- provides a balance of code comprehension for contributors and performance; and
|
||||
- allows for better integration, CI, regression testing, and release workflows.
|
||||
|
||||
## Where did all the APT functionality go?
|
||||
|
||||
Since this has been a complex part of the codebase, I decided to separate the concern as its own library. This can now be found in [npm/ts-apt](https://www.npmjs.com/package/ts-apt). For ease of use there is an option to bring in the workspace for inline source so it can be debugged and then committed to the [ts-apt](http://github.com/awalsh128/ts-apt) repository.
|
||||
|
||||
## How do I start using this?
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This is still in beta so use at your own risk!
|
||||
|
||||
For now you can use the `v2.*` release tags (pinning also supported). Once this version has had time to soak it will be released as the latest. `v1.*` will still get updates but in maintenance mode.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
<!DOCTYPE html><html class="default" lang="en" data-base="../"><head><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="IE=edge"/><title>index | cache-apt-pkgs-action</title><meta name="description" content="Documentation for cache-apt-pkgs-action"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="../assets/style.css"/><link rel="stylesheet" href="../assets/highlight.css"/><script defer src="../assets/main.js"></script><script async src="../assets/icons.js" id="tsd-icons-script"></script><script async src="../assets/search.js" id="tsd-search-script"></script><script async src="../assets/navigation.js" id="tsd-nav-script"></script></head><body><script>document.documentElement.dataset.theme = localStorage.getItem("tsd-theme") || "os";document.body.style.display="none";setTimeout(() => window.app?app.showPage():document.body.style.removeProperty("display"),500)</script><header class="tsd-page-toolbar"><div class="tsd-toolbar-contents container"><a href="../index.html" class="title">cache-apt-pkgs-action</a><div id="tsd-toolbar-links"></div><button id="tsd-search-trigger" class="tsd-widget" aria-label="Search"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-search"></use></svg></button><dialog id="tsd-search" aria-label="Search"><input role="combobox" id="tsd-search-input" aria-controls="tsd-search-results" aria-autocomplete="list" aria-expanded="true" autocapitalize="off" autocomplete="off" placeholder="Search the docs" maxLength="100"/><ul role="listbox" id="tsd-search-results"></ul><div id="tsd-search-status" aria-live="polite" aria-atomic="true"><div>Preparing search index...</div></div></dialog><a href="#" class="tsd-widget menu" id="tsd-toolbar-menu-trigger" data-toggle="menu" aria-label="Menu"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-menu"></use></svg></a></div></header><div class="container container-main"><div class="col-content"><div class="tsd-page-title"><ul class="tsd-breadcrumb" aria-label="Breadcrumb"><li><a href="" aria-current="page">index</a></li></ul><h1>Module index</h1></div></div><div class="col-sidebar"><div class="page-menu"><div class="tsd-navigation settings"><details class="tsd-accordion"><summary class="tsd-accordion-summary"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true"><use href="../assets/icons.svg#icon-chevronDown"></use></svg><h3>Settings</h3></summary><div class="tsd-accordion-details"><div class="tsd-filter-visibility"><span class="settings-label">Member Visibility</span><ul id="tsd-filter-options"><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-protected" name="protected"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Protected</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-inherited" name="inherited" checked/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>Inherited</span></label></li><li class="tsd-filter-item"><label class="tsd-filter-input"><input type="checkbox" id="tsd-filter-external" name="external"/><svg width="32" height="32" viewBox="0 0 32 32" aria-hidden="true"><rect class="tsd-checkbox-background" width="30" height="30" x="1" y="1" rx="6" fill="none"></rect><path class="tsd-checkbox-checkmark" d="M8.35422 16.8214L13.2143 21.75L24.6458 10.25" stroke="none" stroke-width="3.5" stroke-linejoin="round" fill="none"></path></svg><span>External</span></label></li></ul></div><div class="tsd-theme-toggle"><label class="settings-label" for="tsd-theme">Theme</label><select id="tsd-theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></div></div></details></div></div><div class="site-menu"><nav class="tsd-navigation"><a href="../modules.html">cache-apt-pkgs-action</a><ul class="tsd-small-nested-navigation" id="tsd-nav-container"><li>Loading...</li></ul></nav></div></div></div><footer><p class="tsd-generator">Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></footer><div class="overlay"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10266
package-lock.json
generated
10266
package-lock.json
generated
File diff suppressed because it is too large
Load diff
60
package.json
60
package.json
|
|
@ -1,29 +1,66 @@
|
|||
{
|
||||
"name": "cache-apt-pkgs-action",
|
||||
"version": "2.0.0",
|
||||
"description": "GitHub Action to install and cache APT packages",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"description": "TypeScript APT and APT-fast package management library",
|
||||
"author": "Andrew Walsh",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"apt",
|
||||
"apt-cache",
|
||||
"apt-get",
|
||||
"apt-fast"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/awalsh128/cache-apt-pkgs-action.git"
|
||||
},
|
||||
"files": [
|
||||
"dist/src",
|
||||
"!dist/test",
|
||||
"scripts",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/types.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"default": "./dist/src/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/src/index.d.cts",
|
||||
"default": "./dist/src/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"bundle": "ncc build src/main.ts --out dist --license LICENSE",
|
||||
"clean": "rm -rf dist coverage",
|
||||
"check:latest-action-pin": "node ./dev_scripts/check_latest_action_pin.mjs",
|
||||
"check:latest-action-pins": "node ./dev_scripts/check_latest_action_pin.mjs --scan",
|
||||
"clean": "rm -rf dist coverage",
|
||||
"check:configs": "node ./dev_scripts/verify_configs.mjs",
|
||||
"check": "npm-run-all check:*",
|
||||
"format:write": "npx prettier --write .",
|
||||
"format:check": "npx prettier --check .",
|
||||
"setup:dev": "node ./dev_scripts/setup_devenv.mjs",
|
||||
"lint": "eslint src test --ext .ts",
|
||||
"package": "npx rimraf ./dist && npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
||||
"package:watch": "npm run package -- --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecoverage": "type-coverage --at-least 95",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"docs:api": "typedoc --entryPointStrategy expand --entryPoints src --out docs/api",
|
||||
"release:preflight": "node ./dev_scripts/release-preflight.mjs"
|
||||
"test:unit": "vitest run --coverage",
|
||||
"test": "npm-run-all test:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/cache": "^6.1.0",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/core": "^3.0.1",
|
||||
"rollup": "^4.62.2",
|
||||
"tar": "^7.4.3",
|
||||
"ts-apt": "file:../ts-apt",
|
||||
"winston": "^3.19.0"
|
||||
|
|
@ -33,9 +70,10 @@
|
|||
"@typescript-eslint/parser": "^8.46.3",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.31.0",
|
||||
"jest": "^30.4.2",
|
||||
"type-coverage": "^2.29.7",
|
||||
"typedoc": "^0.28.19",
|
||||
"typescript": "^5.8.3",
|
||||
"vercel": "^54.20.1",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class CacheKey {
|
|||
) {}
|
||||
|
||||
serialize(): string {
|
||||
return `${this.version}|${this.forceUpdateIncrement}|${this.arch}|${this.normalizedPackages.join(",")}`;
|
||||
return `${this.version} | ${this.forceUpdateIncrement} | ${this.arch} | ${this.normalizedPackages.join(",")}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@
|
|||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ES2023"
|
||||
]
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "./tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
"test/**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
|
|||
Loading…
Reference in a new issue