diff --git a/.github/workflows/pr-core-tests.yml b/.github/workflows/pr-core-tests.yml index b6a0e3653..f05d980e1 100644 --- a/.github/workflows/pr-core-tests.yml +++ b/.github/workflows/pr-core-tests.yml @@ -2,12 +2,23 @@ name: Core Tests on: pull_request: + workflow_dispatch: + inputs: + pull_request: + description: Pull request number containing the commit + required: true + type: string + commit_sha: + description: Exact reviewed 40-character PR head commit SHA + required: true + type: string permissions: contents: read jobs: leia-tests: + if: github.event_name == 'pull_request' runs-on: ${{ matrix.os }} env: TERM: xterm @@ -125,15 +136,96 @@ jobs: - name: Run Leia Tests if: >- github.event.pull_request.head.repo.full_name == github.repository || - !contains(fromJSON('["init-github", "plugins"]'), matrix.leia-test) + !contains(fromJSON('["l337", "lando-v4"]'), matrix.leia-test) uses: lando/run-leia-action@v2 - env: - GITHUB_PAT: >- - ${{ github.event.pull_request.head.repo.full_name == github.repository && - secrets.KYBER_AMPLIFICATION_MATRIX || github.token }} - GITHUB_KEY_NAME: "${{ github.sha }}${{ matrix.os }}" with: leia-test: "./examples/${{ matrix.leia-test }}/README.md" cleanup-header: "Destroy tests" shell: ${{ matrix.shell }} stdin: true + + privileged-leia-tests: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + leia-test: + - l337 + - lando-v4 + env: + TERM: xterm + steps: + - name: Resolve and validate pull request commit + id: resolve + env: + COMMIT_SHA: ${{ inputs.commit_sha }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pull_request }} + run: | + if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "pull_request must be a pull request number" + exit 1 + fi + if [[ ! "$COMMIT_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "commit_sha must be an exact 40-character commit SHA" + exit 1 + fi + PR_HEAD_SHA=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" --jq .head.sha) + PR_HEAD_REPO=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUMBER" --jq .head.repo.full_name) + if [[ "$COMMIT_SHA" != "$PR_HEAD_SHA" ]]; then + echo "commit_sha does not match the current head of pull request #$PR_NUMBER" + exit 1 + fi + echo "repository=$PR_HEAD_REPO" >> "$GITHUB_OUTPUT" + - name: Checkout code + uses: actions/checkout@v7 + with: + repository: ${{ steps.resolve.outputs.repository }} + ref: ${{ inputs.commit_sha }} + - name: Install SSH key + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ secrets.DEPLOY_KEY }} + known_hosts: unnecessary + if_key_exists: replace + - name: Install node 20 + uses: actions/setup-node@v7 + with: + node-version: "20" + registry-url: https://registry.npmjs.org + cache: npm + - name: Bundle Deps + uses: lando/prepare-release-action@v3 + with: + lando-plugin: true + version: dev + sync: false + - name: Install pkg dependencies + run: npm clean-install --prefer-offline --frozen-lockfile --production + - name: Package into node binary + uses: lando/pkg-action@v6 + id: pkg-action + with: + entrypoint: bin/lando + filename: lando + node-version: "20" + options: --options dns-result-order=ipv4first + upload: false + pkg: "@yao-pkg/pkg@5.16.1" + - name: Install full deps + run: npm clean-install --prefer-offline --frozen-lockfile + - name: Setup lando ${{ steps.pkg-action.outputs.file }} + uses: lando/setup-lando@v3 + with: + lando-version: ${{ steps.pkg-action.outputs.file }} + telemetry: false + config: | + setup.skipCommonPlugins=true + - name: Run Leia Tests + uses: lando/run-leia-action@v2 + with: + leia-test: "./examples/${{ matrix.leia-test }}/README.md" + cleanup-header: "Destroy tests" + shell: bash + stdin: true diff --git a/examples/init-github/README.md b/examples/init-github/README.md index cff5ead57..41916fe28 100644 --- a/examples/init-github/README.md +++ b/examples/init-github/README.md @@ -12,7 +12,7 @@ Run the following commands to get up and running with this example. # Should clone code down from GitHub mkdir -p github && cd github rm -rf ~/.lando/scripts -lando init --source github --recipe none --github-auth="$GITHUB_PAT" --github-repo="git@github.com:lando/lando.git" --github-key-name="$GITHUB_KEY_NAME" --yes +lando init --source github --recipe none --github-auth=false --github-repo="https://github.com/lando/lando.git" --yes ``` ## Verification commands @@ -30,9 +30,6 @@ cd github && cat .lando.yml | grep something | grep happening-here ## Destroy tests ```bash -# Should remove key -docker run --rm -v "$(pwd)":/data -w /data badouralix/curl-jq:alpine sh -c "/data/remove-key.sh $GITHUB_PAT $GITHUB_KEY_NAME" - # Should remove initialized code rm -rf github ``` diff --git a/examples/init-github/remove-key.sh b/examples/init-github/remove-key.sh deleted file mode 100755 index fa00914e3..000000000 --- a/examples/init-github/remove-key.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -set -e - -TOKEN="$1" -TITLE="$2" - -ID=$(curl -L \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/user/keys | jq -r --arg TITLE "$TITLE" '.[] | select(.title == $TITLE).id') - - -# TRY TO REMOVE KEY -echo "Trying to remove key $KEYID"... -curl -L \ - -X DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/user/keys/${ID}" - -echo "REMOVED!" diff --git a/examples/plugins/README.md b/examples/plugins/README.md index 50a493ef3..7098339d3 100644 --- a/examples/plugins/README.md +++ b/examples/plugins/README.md @@ -10,6 +10,20 @@ See the [Landofiles](https://docs.lando.dev/config/lando.html) in this directory ```bash # Should start successfully +docker run -d --name lando-plugin-registry -p 4873:4873 \ + --volume "$PWD/verdaccio.yml:/verdaccio/conf/config.yaml" \ + verdaccio/verdaccio:6 +until curl --silent --fail http://localhost:4873/-/ping; do sleep 1; done +REGISTRY_TOKEN=$(curl --silent --fail \ + --request PUT \ + --header "content-type: application/json" \ + --data '{"name":"lando","password":"lando-test","email":"lando@example.com","type":"user"}' \ + http://localhost:4873/-/user/org.couchdb.user:lando \ + | node -pe "JSON.parse(require('fs').readFileSync(0)).token") +npm publish ./registry-test-plugin \ + --access public \ + --registry http://localhost:4873 \ + --//localhost:4873/:_authToken="$REGISTRY_TOKEN" lando poweroff lando start ``` @@ -58,8 +72,8 @@ lando config | grep -q "plugins/@lando/php" lando plugin-remove "@lando/php" lando config | grep -qv "plugins/@lando/php" -# Should execute `lando plugin-login` -lando plugin-login --registry "https://npm.pkg.github.com" --password "$GITHUB_PAT" --username "rtfm-47" --scope "lando::registry=https://npm.pkg.github.com" +# Should execute `lando plugin-login` against an isolated registry +lando plugin-login --registry "http://localhost:4873" --password "lando-test" --username "lando" --scope "lando::registry=http://localhost:4873" # Should be able to add and remove a private plugin via a registry string. lando config | grep -qv "plugins/@lando/lando-plugin-test" @@ -75,4 +89,5 @@ lando config | grep -qv "plugins/@lando/lando-plugin-test" # Should destroy successfully lando destroy -y lando poweroff +docker rm --force lando-plugin-registry ``` diff --git a/examples/plugins/registry-test-plugin/index.js b/examples/plugins/registry-test-plugin/index.js new file mode 100644 index 000000000..7d90e78f2 --- /dev/null +++ b/examples/plugins/registry-test-plugin/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = () => ({'registry-plugin-test': true}); diff --git a/examples/plugins/registry-test-plugin/package.json b/examples/plugins/registry-test-plugin/package.json new file mode 100644 index 000000000..cf5e3362e --- /dev/null +++ b/examples/plugins/registry-test-plugin/package.json @@ -0,0 +1,10 @@ +{ + "name": "@lando/lando-plugin-test", + "version": "1.0.0", + "description": "A disposable plugin package used by the plugins Leia test", + "main": "index.js", + "keywords": [ + "lando-plugin" + ], + "license": "MIT" +} diff --git a/examples/plugins/verdaccio.yml b/examples/plugins/verdaccio.yml new file mode 100644 index 000000000..89fbeb30f --- /dev/null +++ b/examples/plugins/verdaccio.yml @@ -0,0 +1,27 @@ +storage: /verdaccio/storage + +auth: + htpasswd: + file: /verdaccio/conf/htpasswd + max_users: 1000 + +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +packages: + '@lando/*': + access: $authenticated + publish: $authenticated + unpublish: $authenticated + proxy: npmjs + '**': + access: $all + publish: $authenticated + unpublish: $authenticated + proxy: npmjs + +log: + type: stdout + format: pretty + level: http diff --git a/sources/github.js b/sources/github.js index 8045ee868..4081fd748 100644 --- a/sources/github.js +++ b/sources/github.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const fs = require('fs'); const getOctokit = require('../utils/get-octokit'); +const isDisabled = require('../utils/is-disabled'); const os = require('os'); const path = require('path'); @@ -67,7 +68,10 @@ const setCaches = (options, lando) => { const showTokenList = (source, tokens = []) => !_.isEmpty(tokens) && source === 'github'; // Helper to determine whether to show token password entry or not -const showTokenEntry = (source, answer, tkez = []) => ((_.isEmpty(tkez) || answer === 'more')) && source === 'github'; +const showTokenEntry = (source, answer, tkez = []) => { + const explicitlyDisabled = answer !== undefined && isDisabled(answer); + return !explicitlyDisabled && (_.isEmpty(tkez) || answer === 'more') && source === 'github'; +}; // Helper to get list of github projects const getRepos = answers => { @@ -96,6 +100,9 @@ const getAutoCompleteRepos = (answers, Promise, input = null) => { } }; +// Public HTTPS repositories do not need an SSH key uploaded to a GitHub account. +const requiresSshKey = repo => !_.startsWith(repo, 'https://') && !_.startsWith(repo, 'http://'); + module.exports = { sources: [{ name: 'github', @@ -142,19 +149,30 @@ module.exports = { default: 'Landokey', }, }), - build: (options, lando) => ([ - {name: 'wait-for-user', cmd: `/helpers/wait-for-user.sh www-data ${lando.config.uid}`}, - {name: 'generate-key', cmd: `/helpers/generate-key.sh ${gitHubLandoKey} ${gitHubLandoKeyComment}`}, - {name: 'post-key', func: (options, lando) => { - return postKey( - path.join(lando.config.userConfRoot, 'keys'), - options['github-auth'], - options['github-key-name'], + build: (options, lando) => { + const steps = [ + {name: 'wait-for-user', cmd: `/helpers/wait-for-user.sh www-data ${lando.config.uid}`}, + ]; + + if (requiresSshKey(options['github-repo'])) { + steps.push( + {name: 'generate-key', cmd: `/helpers/generate-key.sh ${gitHubLandoKey} ${gitHubLandoKeyComment}`}, + {name: 'post-key', func: (options, lando) => { + return postKey( + path.join(lando.config.userConfRoot, 'keys'), + options['github-auth'], + options['github-key-name'], + ); + }}, + {name: 'reload-keys', cmd: '/helpers/load-keys.sh --silent', user: 'root'}, ); - }}, - {name: 'reload-keys', cmd: '/helpers/load-keys.sh --silent', user: 'root'}, - {name: 'clone-repo', cmd: `/helpers/get-remote-url.sh ${options['github-repo']}`, remove: true}, - {name: 'set-caches', func: (options, lando) => setCaches(options, lando)}, - ]), + } + + steps.push({name: 'clone-repo', cmd: `/helpers/get-remote-url.sh ${options['github-repo']}`, remove: true}); + if (!isDisabled(options['github-auth'])) { + steps.push({name: 'set-caches', func: (options, lando) => setCaches(options, lando)}); + } + return steps; + }, }], }; diff --git a/test/github-source.spec.js b/test/github-source.spec.js new file mode 100644 index 000000000..cab938b5d --- /dev/null +++ b/test/github-source.spec.js @@ -0,0 +1,54 @@ +/* + * Tests for the GitHub init source. + * @file github-source.spec.js + */ + +'use strict'; + +const chai = require('chai'); +chai.should(); + +const source = require('../sources/github').sources[0]; +const lando = { + cache: {get: () => []}, + config: {uid: 1000, userConfRoot: '/tmp/lando'}, +}; + +describe('github source', () => { + it('should not prompt for a token when GitHub authentication is explicitly disabled', () => { + const authPrompt = source.options(lando)['github-auth-token'].interactive; + authPrompt.when({'source': 'github', 'github-auth': false}).should.equal(false); + }); + + it('should clone public HTTPS repositories without GitHub credentials or SSH key mutation', () => { + const steps = source.build({ + 'github-auth': false, + 'github-repo': 'https://github.com/lando/lando.git', + }, lando); + steps.map(step => step.name).should.deep.equal(['wait-for-user', 'clone-repo']); + }); + + it('should preserve SSH key setup and authenticated-user caching for SSH repositories', () => { + const steps = source.build({ + 'github-auth': 'token', + 'github-key-name': 'Landokey', + 'github-repo': 'git@github.com:lando/lando.git', + }, lando); + steps.map(step => step.name).should.deep.equal([ + 'wait-for-user', + 'generate-key', + 'post-key', + 'reload-keys', + 'clone-repo', + 'set-caches', + ]); + }); + + it('should cache credentials without uploading an SSH key for HTTPS repositories', () => { + const steps = source.build({ + 'github-auth': 'token', + 'github-repo': 'https://github.com/lando/lando.git', + }, lando); + steps.map(step => step.name).should.deep.equal(['wait-for-user', 'clone-repo', 'set-caches']); + }); +});