diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..64d50084 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,97 @@ +name: NPM Release Workflow + +on: + workflow_call: + inputs: + ref: + description: "The branch or tag ref to checkout. Defaults to the default branch." + required: false + default: ${{ github.ref }} + type: string + sha: + description: "The commit SHA to checkout. Defaults to the SHA of the ref." + required: false + default: ${{ github.sha }} + type: string + repository: + description: "The repository to checkout. Defaults to the current repository." + required: false + default: ${{ github.repository }} + type: string + token: + description: "The token to use for authentication. Defaults to the token of the current workflow run." + required: false + default: ${{ github.token }} + type: string + secrets: + RELEASE_TOKEN: + required: true + repository_dispatch: + types: [semantic-release] + +concurrency: + group: ci-${{ inputs.sha }} + cancel-in-progress: true + +permissions: + actions: write + contents: write # to be able to publish a GitHub release + issues: write # to be able to comment on released issues + pull-requests: write # to be able to comment on released pull requests + id-token: write # to enable use of OIDC for npm provenance + +jobs: + deploy: + name: Deploy NPM build + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + SKIP_PREFLIGHT_CHECK: true + steps: + - uses: actions/checkout@v6.0.1 + with: + ref: ${{ inputs.sha }} + token: ${{ secrets.RELEASE_TOKEN }} + + - name: Install compatible Nodejs version + id: setup-node + uses: ./.github/actions/setup-node + + - name: Configure PATH + run: | + mkdir -p "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "${GITHUB_PATH}" + echo "HOME=$HOME" >> "${GITHUB_ENV}" + + - name: Configure Git + run: | + git config --global user.email "${{ github.event.pusher.email || 'stack@bitflight.io' }}" + git config --global user.name "${{ github.event.pusher.name || 'GitHub[bot]' }}" + git fetch --tags + git status --porcelain -u + + - name: Install Deps + id: deps + run: | + npm ci + + - name: Ensure dependencies are compatible with the version of node + run: npx --yes ls-engines + + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures + + - run: npm run build --if-present + + - run: | + git add -f dist + npm run generate-docs + git commit -n -m 'build(release): bundle distribution files' + + - name: Setup Node 24 for semantic-release + uses: actions/setup-node@v6.1.0 + with: + node-version: "24.x" + + - run: npx --yes semantic-release@latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..5167c2ea --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,80 @@ +name: Tag and Release Updated NPM Package + +on: + pull_request_target: + push: + branches: + - main + - next + - beta + - "*.x" + repository_dispatch: + types: [semantic-release] + +concurrency: + group: ci-${{ github.event.pull_request.number }}${{ github.ref }}${{ github.workflow }} + cancel-in-progress: true + +permissions: + issues: write + pull-requests: write + statuses: write + checks: write + actions: write + id-token: write + contents: write + +jobs: + run-tests: + name: Run unit tests (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["20.0.0", "20.17.0", "24.x"] + env: + SKIP_PREFLIGHT_CHECK: true + steps: + - uses: actions/checkout@v6.0.1 + with: + ref: ${{ github.head_ref || github.ref }} + + - name: Install compatible Nodejs version + id: setup-node + uses: ./.github/actions/setup-node + with: + version: ${{ matrix.node-version }} + + - name: Configure PATH + run: | + mkdir -p "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "${GITHUB_PATH}" + echo "HOME=$HOME" >> "${GITHUB_ENV}" + + - run: npm install + - run: npm run test + - run: npm run coverage + - name: "Report Coverage" + if: always() + continue-on-error: true + uses: davelosert/vitest-coverage-report-action@v2.9.0 + with: + json-summary-path: "./out/coverage-summary.json" + json-final-path: ./out/coverage-final.json + - run: npm run build + - run: npm run generate-docs + call-workflow-passing-data: + needs: run-tests + if: ${{ github.event_name == 'push' }} + permissions: + issues: write + pull-requests: write + statuses: write + checks: write + actions: write + id-token: write + contents: write + uses: ./.github/workflows/deploy.yml + with: + ref: ${{ github.head_ref || github.ref }} + secrets: inherit diff --git a/__tests__/inputs.test.ts b/__tests__/inputs.test.ts index 81704c15..a2354c8c 100644 --- a/__tests__/inputs.test.ts +++ b/__tests__/inputs.test.ts @@ -194,7 +194,7 @@ describe('inputs', () => { vi.stubEnv('INPUT_ACTION', 'testaction'); expect(() => loadRequiredConfig(log, config)).toThrowError( - /Missing required keys: paths:action, paths:readme, owner, repo/, + /Missing required keys: owner, repo/, ); }); @@ -242,22 +242,28 @@ describe('inputs', () => { test('Inputs stringify', async ({ task }) => { const log = new LogTask(task.name); const { default: Action } = await import('../src/Action.js'); + + // Clear beforeEach env vars and set test-specific ones + vi.unstubAllEnvs(); vi.stubEnv('DEBUG', 'true'); - const action = new Action(actTestYmlPath); - const sections = ['usage'] as ReadmeSection[]; vi.stubEnv('INPUT_OWNER', 'stringowner'); vi.stubEnv('INPUT_REPO', 'stringrepo'); vi.stubEnv('INPUT_README', 'stringreadme'); vi.stubEnv('INPUT_ACTION', 'stringaction'); + vi.stubEnv('GITHUB_REPOSITORY', ''); // Prevent fallback + vi.stubEnv('GITHUB_EVENT_PATH', ''); // Prevent payload file read + + const action = new Action(actTestYmlPath); + const sections = ['usage'] as ReadmeSection[]; const providedInputContext: InputContext = { action, sections, + configPath: '/tmp/nonexistent.json', // Prevent loading .ghadocs.json }; const inputs = new Inputs(providedInputContext, log); const result = inputs.stringify(); expect(typeof result).toBe('string'); - expect(result).toMatch(/owner: stringowner/); expect(result).toMatch(/repo: stringrepo/); expect(result).toMatch(/sections:\n {2}- usage/); diff --git a/src/inputs.ts b/src/inputs.ts index 5ed1ad47..b802e0fd 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url'; import * as core from '@actions/core'; import { Context } from '@actions/github/lib/context.js'; -import { IOptions, Provider } from 'nconf'; +import nconf from 'nconf'; import YAML from 'yaml'; import Action, { Input } from './Action.js'; @@ -18,7 +18,9 @@ import { configFileName, ConfigKeys, README_SECTIONS, ReadmeSection } from './co import { repositoryFinder } from './helpers.js'; import LogTask from './logtask/index.js'; import ReadmeEditor from './readme-editor.js'; -// import workingDirectory from './working-directory.js'; + +const { Provider } = nconf; +type IOptions = nconf.IOptions; /** * Get the filename from the import.meta.url @@ -302,10 +304,6 @@ export function transformGitHubInputsToArgv( log.debug(`Parsing input: ${obj.key} with ith value: ${obj.value}`); const keyParsed = obj.key.replace(/^(INPUT|input)_/, '').toLocaleLowerCase(); const key = ConfigKeysInputsMap[keyParsed] || keyParsed; - // eslint-disable-next-line no-param-reassign - obj.key = key; - // TODO: This is a hack to get around the fact that nconf doesn't support just returning the new value like its documentation says. - config.set(key, obj.value); log.debug(`New input is ${key} with the value ${obj.value}`); return { key, value: obj.value }; @@ -356,6 +354,9 @@ export function collectAllDefaultValuesFromAction( } = {}, ): IOptions { log.debug('Collecting default values from action.yml'); + // This loads the defaults from THIS action's own action.yml file (github-action-readme-generator's action.yml) + // NOT the user's action.yml file (which is loaded separately via the 'action' input parameter) + // Therefore, we use __dirname to find this package's action.yml regardless of where it's installed const thisActionPath = path.join(__dirname, providedMetaActionPath ?? metaActionPath); try { const defaultValues = {} as IOptions; @@ -373,7 +374,12 @@ export function collectAllDefaultValuesFromAction( log.debug(JSON.stringify(defaultValues, null, 2)); return defaultValues; } catch (error) { - throw new Error(`failed to load defaults from this action's action.yml: ${error}`); + // When running as a CLI tool (e.g., via npx or yarn dlx), the tool's own action.yml + // may not be present in the node_modules. This is expected behavior, as the tool + // should still work to generate documentation for other actions. + log.debug(`Could not load defaults from this tool's action.yml at ${thisActionPath}: ${error}`); + log.debug('Continuing without default values from action.yml'); + return {} as IOptions; } } @@ -400,16 +406,17 @@ export function loadConfig( log.debug(`Config file not found: ${configFilePath}`); } } + config .env({ lowerCase: true, parseValues: true, - match: /^(INPUT|input)_[A-Z_a-z]\w*$/, transform: (obj: KVPairType): undefined | KVPairType => { return transformGitHubInputsToArgv(log, config, obj); }, }) .argv(argvOptions); + return config; } @@ -427,10 +434,14 @@ export function loadDefaultConfig( log.debug('Loading default config'); const defaultValues = collectAllDefaultValuesFromAction(log); const context = providedContext ?? new Context(); - const repositoryDetail = repositoryFinder( - `${process.env.INPUT_OWNER ?? ''}/${process.env.INPUT_REPO ?? ''}`, - context, - ); + + // Get owner/repo from config (which includes CLI args), falling back to env vars for GitHub Actions + const ownerFromConfig = config.get('owner') as string | undefined; + const repoFromConfig = config.get('repo') as string | undefined; + const ownerInput = ownerFromConfig ?? process.env.INPUT_OWNER ?? ''; + const repoInput = repoFromConfig ?? process.env.INPUT_REPO ?? ''; + + const repositoryDetail = repositoryFinder(`${ownerInput}/${repoInput}`, context); log.debug(`repositoryDetail: ${repositoryDetail}`); // Apply the default values from the action.yml file return config.defaults({