Skip to content

Instantly share code, notes, and snippets.

@ostenbom
Created January 8, 2026 12:11
Show Gist options
  • Select an option

  • Save ostenbom/7f5d7fe83690cb754ded3a9bd8215e5d to your computer and use it in GitHub Desktop.

Select an option

Save ostenbom/7f5d7fe83690cb754ded3a9bd8215e5d to your computer and use it in GitHub Desktop.
Send artifacts between Github Actions workflow jobs
name: Download Preview Data
description: Downloads preview-data artifact with retry logic and support for previous attempt fallback
inputs:
deploy-job-name:
required: false
default: 'deploy-preview'
description: Name of the deploy job to check status
max-wait-seconds:
required: false
default: '300'
description: Maximum time to wait for artifact (default 5 minutes)
check-interval-seconds:
required: false
default: '5'
description: Time between retry attempts (default 5 seconds)
github-token:
required: true
description: GitHub token for API access
path:
required: false
default: './'
description: Path to download artifact to
outputs:
artifact-found:
description: Whether artifact was found (true/false)
value: ${{ steps.check-and-download.outputs.artifact-found }}
used-previous-attempt:
description: Whether artifact was from previous attempt (true/false)
value: ${{ steps.check-and-download.outputs.used-previous-attempt }}
zip-path:
description: Path to the downloaded zip file
value: ${{ steps.check-and-download.outputs.zip-path }}
runs:
using: composite
steps:
- name: Check deploy job status and download artifact
id: check-and-download
uses: actions/github-script@v8
env:
DEPLOY_JOB_NAME: ${{ inputs.deploy-job-name }}
MAX_WAIT_SECONDS: ${{ inputs.max-wait-seconds }}
CHECK_INTERVAL_SECONDS: ${{ inputs.check-interval-seconds }}
DOWNLOAD_PATH: ${{ inputs.path }}
with:
github-token: ${{ inputs.github-token }}
script: |
const fs = require('fs');
const path = require('path');
const artifactBaseName = 'preview-data';
const deployJobName = process.env.DEPLOY_JOB_NAME;
const maxWaitSeconds = parseInt(process.env.MAX_WAIT_SECONDS);
const checkIntervalSeconds = parseInt(process.env.CHECK_INTERVAL_SECONDS);
const downloadPath = process.env.DOWNLOAD_PATH;
// Try to get run attempt from multiple sources
const runAttempt = context.runAttempt || parseInt(process.env.GITHUB_RUN_ATTEMPT || '1', 10);
console.log(`Run attempt: ${runAttempt} (from context: ${context.runAttempt}, env: ${process.env.GITHUB_RUN_ATTEMPT})`);
console.log(`Looking for artifact: ${artifactBaseName}`);
// Find the most recent UNIQUE deploy job execution using timestamp detection
let targetAttemptNumber = runAttempt;
let shouldUsePreviousArtifact = false;
try {
const jobs = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
filter: 'all'
});
console.log('=== ANALYZING DEPLOY JOBS ===');
// Get all deploy-preview jobs, sorted by run_attempt ascending
const deployJobs = jobs.data.jobs
.filter(j => j.name === deployJobName)
.sort((a, b) => (a.run_attempt || 0) - (b.run_attempt || 0));
if (deployJobs.length === 0) {
console.log('No deploy jobs found');
} else {
console.log(`Found ${deployJobs.length} deploy-preview job(s):`);
deployJobs.forEach(j => {
console.log(` - Attempt ${j.run_attempt}: started=${j.started_at}, completed=${j.completed_at}, status=${j.status}`);
});
// Group jobs by their unique timestamp signature
// The earliest attempt in each group is the one that actually ran
const timestampGroups = new Map();
for (const job of deployJobs) {
const timestampKey = `${job.started_at}|${job.completed_at}`;
if (!timestampGroups.has(timestampKey)) {
// First job with this timestamp - this is the real execution
timestampGroups.set(timestampKey, job);
console.log(` Attempt ${job.run_attempt} is unique execution`);
} else {
// Duplicate timestamp - this is a stale entry
console.log(` Attempt ${job.run_attempt} is stale (duplicate of attempt ${timestampGroups.get(timestampKey).run_attempt})`);
}
}
// Find the most recent unique execution (highest run_attempt among unique ones)
let mostRecentUniqueJob = null;
for (const job of timestampGroups.values()) {
if (!mostRecentUniqueJob || job.run_attempt > mostRecentUniqueJob.run_attempt) {
mostRecentUniqueJob = job;
}
}
if (mostRecentUniqueJob) {
console.log(`✓ Most recent unique execution: Attempt ${mostRecentUniqueJob.run_attempt}`);
console.log(` Started: ${mostRecentUniqueJob.started_at}`);
console.log(` Completed: ${mostRecentUniqueJob.completed_at}`);
}
if (mostRecentUniqueJob) {
targetAttemptNumber = mostRecentUniqueJob.run_attempt;
// If we're in a later attempt than the most recent unique execution,
// we should use the artifact from that earlier attempt
if (runAttempt > targetAttemptNumber) {
shouldUsePreviousArtifact = true;
console.log(`⚠️ Current attempt ${runAttempt} should use artifact from attempt ${targetAttemptNumber}`);
} else if (runAttempt === targetAttemptNumber) {
// Check if job is still running or needs to run
const isActive = ['queued', 'in_progress', 'pending', 'waiting'].includes(mostRecentUniqueJob.status);
if (isActive) {
console.log('Deploy job is active for current attempt - will wait for new artifact');
} else if (mostRecentUniqueJob.status === 'completed') {
console.log('Deploy job completed for current attempt - artifact should be available');
}
}
}
}
} catch (error) {
console.warn(`Could not analyze deploy job status: ${error.message}`);
// Fallback to simple logic if API fails
shouldUsePreviousArtifact = runAttempt > 1;
}
const startTime = Date.now();
const maxWaitMs = maxWaitSeconds * 1000;
const checkIntervalMs = checkIntervalSeconds * 1000;
let artifactFound = null;
let usedPreviousAttempt = false;
// Wait for artifact to appear
while (!artifactFound) {
const elapsed = Date.now() - startTime;
if (elapsed > maxWaitMs) {
throw new Error(`Timeout: No artifact found after ${maxWaitSeconds} seconds`);
}
try {
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
// Determine which artifact to look for based on timestamp analysis
const targetAttemptName = shouldUsePreviousArtifact
? `${artifactBaseName}-${targetAttemptNumber}`
: `${artifactBaseName}-${runAttempt}`;
let artifact = artifacts.data.artifacts.find(a => a.name === targetAttemptName);
if (artifact) {
if (shouldUsePreviousArtifact) {
console.log(`✓ Found artifact from unique execution (attempt ${targetAttemptNumber}): ${targetAttemptName} (ID: ${artifact.id})`);
console.log(` Created at: ${artifact.created_at}`);
usedPreviousAttempt = true;
} else {
console.log(`✓ Found artifact for current attempt: ${targetAttemptName} (ID: ${artifact.id})`);
usedPreviousAttempt = false;
}
artifactFound = artifact;
break;
}
// Fallback: if target artifact not found, try to find any matching artifact
if (shouldUsePreviousArtifact) {
const matchingArtifacts = artifacts.data.artifacts
.filter(a => a.name.startsWith(`${artifactBaseName}-`))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (matchingArtifacts.length > 0) {
artifact = matchingArtifacts[0];
console.log(`✓ Using most recent artifact as fallback: ${artifact.name} (ID: ${artifact.id})`);
console.log(` Created at: ${artifact.created_at}`);
artifactFound = artifact;
usedPreviousAttempt = true;
break;
}
}
console.log(`Artifact not found yet. Waiting ${checkIntervalSeconds}s... (elapsed: ${Math.round(elapsed / 1000)}s)`);
await new Promise(resolve => setTimeout(resolve, checkIntervalMs));
} catch (error) {
console.error(`Error checking for artifact: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, checkIntervalMs));
}
}
if (!artifactFound) {
throw new Error('No artifact found');
}
console.log(`Total wait time: ${Math.round((Date.now() - startTime) / 1000)}s`);
// Download the artifact
console.log('Downloading artifact...');
const download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifactFound.id,
archive_format: 'zip',
});
// Create download directory
fs.mkdirSync(downloadPath, { recursive: true });
// Write zip file
const zipPath = path.join(downloadPath, 'artifact.zip');
fs.writeFileSync(zipPath, Buffer.from(download.data));
console.log(`✓ Artifact downloaded to ${zipPath}`);
core.setOutput('artifact-found', 'true');
core.setOutput('used-previous-attempt', usedPreviousAttempt.toString());
core.setOutput('zip-path', zipPath);
- name: Extract artifact
shell: bash
run: |
cd "${{ inputs.path }}"
unzip -q artifact.zip
rm artifact.zip
echo "✓ Artifact extracted"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment