diff --git a/.github/actions/container-logs-check/action.yml b/.github/actions/container-logs-check/action.yml index 34a12bd..9bd8e60 100644 --- a/.github/actions/container-logs-check/action.yml +++ b/.github/actions/container-logs-check/action.yml @@ -25,7 +25,7 @@ runs: using: composite steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_NAME: ${{ inputs.name }} INPUT_TYPE: ${{ inputs.type }} @@ -107,47 +107,85 @@ runs: const statusLabel = type === 'service' ? 'service' : 'container'; const statusCheck = type === 'service' ? checkServiceStatus : checkContainerStatus; const logArgs = type === 'service' ? ['service', 'logs', '-f', name] : ['logs', '-f', name]; - const childProcess = spawn('docker', logArgs); + const childProcess = spawn('sh', ['-c', 'exec docker "$@" 2>&1', 'docker', ...logArgs]); - let matchFound = false; + let settled = false; let intervalId; + let logBuffer = ''; + + function cleanup() { + clearInterval(intervalId); + clearTimeout(timeoutId); + childProcess.stdout.off('data', handleStdoutData); + childProcess.stdout.destroy(); + childProcess.kill(); + } + + function resolveOnce(message) { + if (settled) { + return; + } + settled = true; + cleanup(); + resolve(message); + } + + function rejectOnce(error) { + if (settled) { + return; + } + settled = true; + cleanup(); + reject(error); + } const timeoutId = setTimeout(() => { - if (!matchFound) { - clearInterval(intervalId); - childProcess.kill(); - reject(`String "${searchString}" not found in ${statusLabel} logs within ${timeout / 1000} seconds`); + if (!settled) { + rejectOnce(`String "${searchString}" not found in ${statusLabel} logs within ${timeout / 1000} seconds`); } }, timeout); async function checkTargetStatus() { const err = await statusCheck(name); if (err.length > 0) { - clearInterval(intervalId); - clearTimeout(timeoutId); - childProcess.kill(); - reject(err); + rejectOnce(err); } } - const handleStreamData = async (streamData) => { - const lines = streamData.toString().split('\n'); - for (const line of lines) { - if (line.trim() !== '') { - core.info(line); - if (line.includes(searchString)) { - matchFound = true; - clearInterval(intervalId); - clearTimeout(timeoutId); - childProcess.kill(); - resolve(`🎉 Found "${searchString}" in ${statusLabel} logs`); - } + function handleLogLine(line) { + if (settled) { + return; + } + if (line.trim() !== '') { + core.info(line); + if (line.includes(searchString)) { + resolveOnce(`🎉 Found "${searchString}" in ${statusLabel} logs`); } } - }; + } + + function handleStdoutData(streamData) { + logBuffer += streamData.toString(); + const lines = logBuffer.split('\n'); + logBuffer = lines.pop(); + for (const line of lines) { + handleLogLine(line.replace(/\r$/, '')); + } + } - childProcess.stdout.on('data', handleStreamData); - childProcess.stderr.on('data', handleStreamData); + childProcess.stdout.on('data', handleStdoutData); + childProcess.on('error', (err) => { + rejectOnce(`Failed to read ${statusLabel} logs: ${err.message}`); + }); + childProcess.on('close', () => { + if (logBuffer) { + handleLogLine(logBuffer.replace(/\r$/, '')); + logBuffer = ''; + } + if (!settled) { + rejectOnce(`Log stream closed before finding "${searchString}" in ${statusLabel} logs`); + } + }); intervalId = setInterval(checkTargetStatus, 5000); }); diff --git a/.github/actions/docker-scout/action.yml b/.github/actions/docker-scout/action.yml index 18b5890..11d2e63 100644 --- a/.github/actions/docker-scout/action.yml +++ b/.github/actions/docker-scout/action.yml @@ -24,7 +24,7 @@ runs: using: composite steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 id: run env: INPUT_VERSION: ${{ inputs.version }} diff --git a/.github/actions/gotest-annotations/action.yml b/.github/actions/gotest-annotations/action.yml index 5f02a47..addb5ea 100644 --- a/.github/actions/gotest-annotations/action.yml +++ b/.github/actions/gotest-annotations/action.yml @@ -7,29 +7,34 @@ inputs: description: 'Test reports dir' required: true +outputs: + annotations: + description: 'Number of test failure annotations emitted' + value: ${{ steps.annotate.outputs.annotations }} + runs: using: composite steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - await core.group(`Install npm deps`, async () => { - await exec.exec('npm', ['install', 'line-by-line']); - }); - - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + id: annotate env: INPUT_DIRECTORY: ${{ inputs.directory }} with: script: | - const lineReader = require('line-by-line'); + const fs = require('fs'); + const readline = require('readline'); + const testRegex = /(\s*[\w\d]+.go:\d+:)(.*?)(---\sFAIL:.*)/gs; + let annotations = 0; let tests = {}; const globber = await glob.create(`${core.getInput('directory')}/**/*.json`); for await (const jsonReport of globber.globGenerator()) { - let lr = new lineReader(jsonReport); - lr.on('line', function(line) { + const reader = readline.createInterface({ + input: fs.createReadStream(jsonReport), + crlfDelay: Infinity + }); + for await (const line of reader) { const currentLine = JSON.parse(line); const testName = currentLine.Test; let output = currentLine.Output; @@ -47,25 +52,25 @@ runs: } else { tests[key].output += output; } - }); - lr.on('end', function() { - for (const [key, test] of Object.entries(tests)) { - if (!test.output.includes("FAIL") || !test.output.includes(".go")) { - continue; - } - var result; - while ((result = testRegex.exec(test.output)) !== null) { - const parts = result[0].split(":"); - const file = `${test.package}/${parts[0].trimStart()}`; - const lineNumber = parts[1]; - core.startGroup(key); - core.error(test.output, { - title: `Failed: ${key}`, - file: file, - startLine: lineNumber - }); - core.endGroup(); - } - } - }); + } + } + for (const [key, test] of Object.entries(tests)) { + if (!test.output.includes("FAIL") || !test.output.includes(".go")) { + continue; + } + var result; + while ((result = testRegex.exec(test.output)) !== null) { + const parts = result[0].split(":"); + const file = `${test.package}/${parts[0].trimStart()}`; + const lineNumber = parts[1]; + core.startGroup(key); + core.error(test.output, { + title: `Failed: ${key}`, + file: file, + startLine: lineNumber + }); + annotations++; + core.endGroup(); + } } + core.setOutput('annotations', annotations); diff --git a/.github/actions/install-k3s/action.yml b/.github/actions/install-k3s/action.yml index fa965f6..32b4681 100644 --- a/.github/actions/install-k3s/action.yml +++ b/.github/actions/install-k3s/action.yml @@ -12,7 +12,7 @@ runs: using: "composite" steps: - - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: INPUT_VERSION: ${{ inputs.version }} with: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cbb7b00..41834f1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,7 +2,9 @@ version: 2 updates: - package-ecosystem: "github-actions" open-pull-requests-limit: 10 - directory: "/" + directories: + - "/" + - "/.github/actions/*" schedule: interval: "daily" cooldown: diff --git a/.github/workflows/.test.yml b/.github/workflows/.test.yml index 4e88721..574a716 100644 --- a/.github/workflows/.test.yml +++ b/.github/workflows/.test.yml @@ -140,13 +140,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install k3s uses: ./.github/actions/install-k3s - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: driver: kubernetes driver-opts: qemu.install=true @@ -168,34 +168,94 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ matrix.commit }} + gotest-annotations: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - + name: Create test report + run: | + mkdir -p pkg/foo test-results + for _ in $(seq 1 12); do + echo "// fixture" + done > pkg/foo/foo_test.go + cat > test-results/report.json <<'EOF' + {"Action":"output","Package":"github.com/docker/example/pkg/foo","Test":"TestBroken","Output":" foo_test.go:12: expected ok\n"} + {"Action":"output","Package":"github.com/docker/example/pkg/foo","Test":"TestBroken","Output":"--- FAIL: TestBroken (0.00s)\n"} + EOF + - + name: Annotate failed tests + id: annotate + uses: ./.github/actions/gotest-annotations + with: + directory: test-results + - + name: Check annotations + run: | + if [ "${{ steps.annotate.outputs.annotations }}" != "1" ]; then + echo "::error::Expected 1 annotation, got ${{ steps.annotate.outputs.annotations }}" + exit 1 + fi + container-logs-check: runs-on: ubuntu-latest + services: + cloudflared: + image: crazymax/cloudflared:latest@sha256:9b4e856d18f6f6367330c56d91452f5fe3b6bd235f1210dcd9f6bd7373cad9be + options: >- + --label "diun.enable=true" + --label "diun.watch_repo=true" steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - name: Run container + name: Create Diun config run: | - docker run -d --name test crazymax/samba:latest + cat > diun.yml <<'EOF' + watch: + workers: 20 + schedule: "0 */6 * * *" + jitter: 5m + + providers: + docker: + watchByDefault: true + EOF + - + name: Run Diun + run: | + docker run -d --name diun \ + --health-cmd "diun healthcheck" \ + --health-interval 30s \ + --health-timeout 5s \ + --health-retries 3 \ + --health-start-period 60s \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(pwd)/diun.yml:/diun.yml:ro" \ + -e "TZ=Europe/Paris" \ + -e "LOG_LEVEL=info" \ + crazymax/diun:latest - name: Check container logs uses: ./.github/actions/container-logs-check with: - name: test - log_check: " started." - timeout: 20 + name: diun + log_check: "Next run in" + timeout: 240 container-logs-check-notfound: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Run container run: | @@ -222,7 +282,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Run container run: | @@ -249,7 +309,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Init swarm run: | @@ -257,7 +317,15 @@ jobs: - name: Run service run: | - docker service create --name test busybox sh -c "echo 'service ready' && sleep 600" + docker service create --name test busybox sh -c ' + i=1 + while [ "$i" -le 100 ]; do + echo "service log $i" + i=$((i + 1)) + done + echo "service ready" + sleep 600 + ' - name: Check service logs uses: ./.github/actions/container-logs-check @@ -288,10 +356,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/releases-json.yml b/.github/workflows/releases-json.yml index 9b839c3..ff10be5 100644 --- a/.github/workflows/releases-json.yml +++ b/.github/workflows/releases-json.yml @@ -36,7 +36,18 @@ jobs: with: script: | await core.group(`Install npm deps`, async () => { - await exec.exec('npm', ['install', 'semver']); + await exec.exec('npm', [ + 'install', + '--loglevel=error', + '--no-save', + '--package-lock=false', + '--ignore-scripts', + '--omit=dev', + '--prefer-offline', + '--fund=false', + '--audit=false', + 'semver@7.8.2' + ]); }); - name: Generate diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 8b08911..a10fd8e 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - @@ -81,7 +81,7 @@ jobs: name: Create GitHub App token id: app-token if: ${{ steps.github-app-auth.outputs.enabled == 'true' }} - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ inputs.github-app-client-id }} private-key: ${{ secrets.github-app-private-key }} @@ -174,7 +174,7 @@ jobs: - name: Upload SARIF report if: ${{ always() && steps.zizmor.outputs.sarif-path != '' }} - uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: sarif_file: ${{ steps.zizmor.outputs.sarif-path }} category: zizmor